Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/SkipKit/Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class Cache<Key: Hashable, Value> {
private let cacheLock = NSLock()
private let manager: CacheManager<Key, Value>

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)
}

Expand Down Expand Up @@ -154,10 +154,10 @@ private final class LRUCostCache<Key, Value>: android.util.LruCache<Key, Value>
}

#else
private final class CacheManager<Key: Hashable, Value>: NSObject, NSCacheDelegate {
private final class CacheManager<Key: Hashable, Value>: NSObject, NSCacheDelegate, @unchecked Sendable {
let cache = NSCache<CacheKey, CacheValue>()
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
Expand All @@ -167,7 +167,7 @@ private final class CacheManager<Key: Hashable, Value>: 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

Expand Down
67 changes: 37 additions & 30 deletions Sources/SkipKit/DeviceInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/SkipKit/HapticFeedback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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]) {
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipKit/MailComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions Sources/SkipKit/PermissionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Void, Never>?
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/SkipKit/WebBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down