diff --git a/Package.swift b/Package.swift index 79aba66..a872f4f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 // This is a Skip (https://skip.dev) package, // containing a Swift Package Manager project // that will use the Skip build plugin to transpile the diff --git a/Sources/SkipKit/Cache.swift b/Sources/SkipKit/Cache.swift index 6fda47b..f86baba 100644 --- a/Sources/SkipKit/Cache.swift +++ b/Sources/SkipKit/Cache.swift @@ -10,7 +10,7 @@ public class Cache { private let cacheLock = NSLock() private let manager: CacheManager - public init(evictOnBackground: Bool = true, limit: Int? = nil, cost: ((Value) -> Int)? = nil) { + public init(evictOnBackground: Bool = true, limit: Int? = nil, cost: (@Sendable (Value) -> Int)? = nil) { self.manager = CacheManager(evictOnBackground: evictOnBackground, limit: limit, cost: cost) } @@ -154,10 +154,10 @@ private final class LRUCostCache: android.util.LruCache } #else -private final class CacheManager: NSObject, NSCacheDelegate { +private final class CacheManager: NSObject, NSCacheDelegate, @unchecked Sendable { let cache = NSCache() let limit: Int? - let cost: ((Value) -> Int)? + let cost: (@Sendable (Value) -> Int)? private var didEnterBackgroundObserver: NSObjectProtocol? #if canImport(UIKit) /// https://developer.apple.com/documentation/uikit/uiapplication/didenterbackgroundnotification @@ -167,7 +167,7 @@ private final class CacheManager: NSObject, NSCacheDelegat private let backgroundNotificationName = Notification.Name("NSApplicationDidResignActiveNotification") #endif - public init(evictOnBackground: Bool, limit: Int?, cost: ((Value) -> Int)?) { + public init(evictOnBackground: Bool, limit: Int?, cost: (@Sendable (Value) -> Int)?) { self.limit = limit self.cost = cost diff --git a/Sources/SkipKit/DeviceInfo.swift b/Sources/SkipKit/DeviceInfo.swift index be22020..9f3e95e 100644 --- a/Sources/SkipKit/DeviceInfo.swift +++ b/Sources/SkipKit/DeviceInfo.swift @@ -90,7 +90,7 @@ public final class DeviceInfo { let dm = context.getResources().getDisplayMetrics() return Double(dm.widthPixels) / Double(dm.density) #elseif canImport(UIKit) - return Double(UIScreen.main.bounds.width) + return MainActor.assumeIsolated { Double(UIScreen.main.bounds.width) } #elseif os(macOS) return Double(NSScreen.main?.frame.width ?? 0) #else @@ -105,7 +105,7 @@ public final class DeviceInfo { let dm = context.getResources().getDisplayMetrics() return Double(dm.heightPixels) / Double(dm.density) #elseif canImport(UIKit) - return Double(UIScreen.main.bounds.height) + return MainActor.assumeIsolated { Double(UIScreen.main.bounds.height) } #elseif os(macOS) return Double(NSScreen.main?.frame.height ?? 0) #else @@ -119,7 +119,7 @@ public final class DeviceInfo { let context = ProcessInfo.processInfo.androidContext return Double(context.getResources().getDisplayMetrics().density) #elseif canImport(UIKit) - return Double(UIScreen.main.scale) + return MainActor.assumeIsolated { Double(UIScreen.main.scale) } #elseif os(macOS) return Double(NSScreen.main?.backingScaleFactor ?? 1.0) #else @@ -146,12 +146,14 @@ public final class DeviceInfo { return .phone } #elseif canImport(UIKit) - switch UIDevice.current.userInterfaceIdiom { - case .phone: return .phone - case .pad: return .tablet - case .tv: return .tv - case .mac: return .desktop - default: return .unknown + return MainActor.assumeIsolated { + switch UIDevice.current.userInterfaceIdiom { + case .phone: return .phone + case .pad: return .tablet + case .tv: return .tv + case .mac: return .desktop + default: return .unknown + } } #elseif os(macOS) return .desktop @@ -231,13 +233,15 @@ public final class DeviceInfo { if level < 0 { return nil } return Double(level) / 100.0 #elseif canImport(UIKit) - let device = UIDevice.current - let wasEnabled = device.isBatteryMonitoringEnabled - device.isBatteryMonitoringEnabled = true - let level = device.batteryLevel - if !wasEnabled { device.isBatteryMonitoringEnabled = false } - if level < 0 { return nil } - return Double(level) + return MainActor.assumeIsolated { + let device = UIDevice.current + let wasEnabled = device.isBatteryMonitoringEnabled + device.isBatteryMonitoringEnabled = true + let level = device.batteryLevel + if !wasEnabled { device.isBatteryMonitoringEnabled = false } + if level < 0 { return nil } + return Double(level) + } #else return nil #endif @@ -258,17 +262,19 @@ public final class DeviceInfo { } return .unplugged #elseif canImport(UIKit) - let device = UIDevice.current - let wasEnabled = device.isBatteryMonitoringEnabled - device.isBatteryMonitoringEnabled = true - let state = device.batteryState - if !wasEnabled { device.isBatteryMonitoringEnabled = false } - switch state { - case .unplugged: return .unplugged - case .charging: return .charging - case .full: return .full - case .unknown: return .unknown - @unknown default: return .unknown + return MainActor.assumeIsolated { + let device = UIDevice.current + let wasEnabled = device.isBatteryMonitoringEnabled + device.isBatteryMonitoringEnabled = true + let state = device.batteryState + if !wasEnabled { device.isBatteryMonitoringEnabled = false } + switch state { + case .unplugged: return .unplugged + case .charging: return .charging + case .full: return .full + case .unknown: return .unknown + @unknown default: return .unknown + } } #else return .unknown @@ -289,16 +295,17 @@ public final class DeviceInfo { #elseif canImport(Network) let monitor = NWPathMonitor() let queue = DispatchQueue(label: "skip.kit.network.snapshot") - var result: NetworkStatus = .offline + final class Box: @unchecked Sendable { var value: NetworkStatus = .offline } + let result = Box() let semaphore = DispatchSemaphore(value: 0) monitor.pathUpdateHandler = { path in - result = Self.mapNWPath(path) + result.value = Self.mapNWPath(path) semaphore.signal() } monitor.start(queue: queue) _ = semaphore.wait(timeout: .now() + 1.0) monitor.cancel() - return result + return result.value #else return .offline #endif diff --git a/Sources/SkipKit/HapticFeedback.swift b/Sources/SkipKit/HapticFeedback.swift index 60c53cc..4490fa6 100644 --- a/Sources/SkipKit/HapticFeedback.swift +++ b/Sources/SkipKit/HapticFeedback.swift @@ -7,7 +7,7 @@ import CoreHaptics #endif /// A single haptic event within a pattern. -public struct HapticEvent { +public struct HapticEvent: Sendable { /// The type of haptic primitive to play. public let type: HapticEventType /// Intensity from 0.0 to 1.0. @@ -23,7 +23,7 @@ public struct HapticEvent { } /// The type of haptic primitive. -public enum HapticEventType { +public enum HapticEventType: Sendable { /// A short, sharp tap. The most common haptic element. case tap /// A subtle, light tick. Good for selections and fine adjustments. @@ -39,7 +39,7 @@ public enum HapticEventType { } /// A sequence of haptic events that form a complete feedback pattern. -public struct HapticPattern { +public struct HapticPattern: Sendable { public let events: [HapticEvent] public init(_ events: [HapticEvent]) { @@ -135,7 +135,7 @@ extension HapticPattern { /// Plays custom haptic patterns on both iOS and Android. public final class HapticFeedback { #if canImport(CoreHaptics) - private static var engine: CHHapticEngine? + nonisolated(unsafe) private static var engine: CHHapticEngine? #endif /// Play a haptic pattern. Call from any thread. diff --git a/Sources/SkipKit/MailComposer.swift b/Sources/SkipKit/MailComposer.swift index 1f587c9..3663340 100644 --- a/Sources/SkipKit/MailComposer.swift +++ b/Sources/SkipKit/MailComposer.swift @@ -105,7 +105,7 @@ public enum MailComposer { intent.setData(Uri.parse("mailto:")) return intent.resolveActivity(context.getPackageManager()) != nil #elseif os(iOS) - return MFMailComposeViewController.canSendMail() + return MainActor.assumeIsolated { MFMailComposeViewController.canSendMail() } #else return false #endif diff --git a/Sources/SkipKit/PermissionManager.swift b/Sources/SkipKit/PermissionManager.swift index 7fd9a30..349e684 100644 --- a/Sources/SkipKit/PermissionManager.swift +++ b/Sources/SkipKit/PermissionManager.swift @@ -5,8 +5,6 @@ import Foundation import SwiftUI #if !SKIP import Photos -// import Contacts // TODO: create framework skip-contacts -// import EventKit // TODO: create framework skip-calendar import AVFoundation import CoreLocation import UserNotifications @@ -453,7 +451,7 @@ public class PermissionManager { /// A delegate that encapsulates a `CLLocationManager` and handles `locationManagerDidChangeAuthorization` class LocationDelegate: NSObject, CLLocationManagerDelegate { /// For some reason, we need to keep just a single reference to CLLocationManager around for the locationManagerDidChangeAuthorization to get called reliably - static let shared = LocationDelegate() + nonisolated(unsafe) static let shared = LocationDelegate() lazy var locationManager = CLLocationManager() var continuation: CheckedContinuation? @@ -510,7 +508,7 @@ public enum PermissionAuthorization : String { } /// The encapsulation of a permission name -public struct PermissionType : Equatable { +public struct PermissionType : Equatable, Sendable { public let androidPermissionName: String public init(androidPermissionName: String) { diff --git a/Sources/SkipKit/WebBrowser.swift b/Sources/SkipKit/WebBrowser.swift index 1569e11..51542f9 100644 --- a/Sources/SkipKit/WebBrowser.swift +++ b/Sources/SkipKit/WebBrowser.swift @@ -204,7 +204,8 @@ private struct SafariViewWrapper: UIViewControllerRepresentable { } } -private class SafariCoordinator: NSObject, SFSafariViewControllerDelegate { +@MainActor +private class SafariCoordinator: NSObject, @preconcurrency SFSafariViewControllerDelegate { let parent: SafariViewWrapper init(parent: SafariViewWrapper) {