diff --git a/Package.swift b/Package.swift index 7001e46..b1cf904 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/BSWFoundation/APIClient/APIClient.swift b/Sources/BSWFoundation/APIClient/APIClient.swift index f21ca16..1243f06 100644 --- a/Sources/BSWFoundation/APIClient/APIClient.swift +++ b/Sources/BSWFoundation/APIClient/APIClient.swift @@ -102,9 +102,9 @@ open class APIClient: Identifiable { extension APIClient { /// Encapsualtes the Request to be sent to the server. - public struct Request{ + public struct Request: Sendable { - public typealias Validator = (APIClient.Response) throws -> () + public typealias Validator = @Sendable (APIClient.Response) throws -> () /// The Endpoint where to send this request. public let endpoint: Endpoint @@ -128,7 +128,7 @@ extension APIClient { } /// Errors thrown from the `APIClient`. - public enum Error: Swift.Error { + public enum Error: Swift.Error, Sendable { /// The URL resulting from generating the `URLRequest` is not valid. case malformedURL /// The response received from the Server is malformed. @@ -308,7 +308,7 @@ extension URLSession: APIClientNetworkFetcher { } public typealias HTTPHeaders = [String: String] -public struct VoidResponse: Decodable, Hashable {} +public struct VoidResponse: Decodable, Hashable, Sendable {} private extension Swift.Error { var is401: Bool { diff --git a/Sources/BSWFoundation/AuthStorage/AuthStorage.swift b/Sources/BSWFoundation/AuthStorage/AuthStorage.swift index 05d98a6..e5be77b 100644 --- a/Sources/BSWFoundation/AuthStorage/AuthStorage.swift +++ b/Sources/BSWFoundation/AuthStorage/AuthStorage.swift @@ -4,16 +4,22 @@ // import Foundation -import KeychainAccess +@preconcurrency import KeychainAccess /// A class that's useful to store sensitive information using the device's Keychain. -public class AuthStorage { +public final class AuthStorage: Sendable { /// A standard `AuthStorage` with a `.simple` style. public static let defaultStorage = AuthStorage() - @UserDefaultsBacked(key: Keys.HasAppBeenExecuted, defaultValue: false) - private var appBeenExecuted: Bool! + private var appBeenExecuted: Bool { + get { + UserDefaults.standard.bool(forKey: Keys.HasAppBeenExecuted) + } + set { + UserDefaults.standard.setValue(newValue, forKey: Keys.HasAppBeenExecuted) + } + } private let keychain: Keychain /// Where should `AppStorage` put it's values. diff --git a/Sources/BSWFoundation/Extensions/UIApplication+Tests.swift b/Sources/BSWFoundation/Extensions/UIApplication+Tests.swift index 46d4507..2d11562 100644 --- a/Sources/BSWFoundation/Extensions/UIApplication+Tests.swift +++ b/Sources/BSWFoundation/Extensions/UIApplication+Tests.swift @@ -7,6 +7,12 @@ public extension UIApplication { /// If the application is executing Unit Tests. @inlinable var isRunningTests: Bool { + UIApplication.isRunningTests + } + + /// If the application is executing Unit Tests. + @inlinable + static var isRunningTests: Bool { #if DEBUG return NSClassFromString("XCTest") != nil #else diff --git a/Sources/BSWFoundation/Location/LocationFetcher.swift b/Sources/BSWFoundation/Location/LocationFetcher.swift index 34fb0ac..2f086cf 100644 --- a/Sources/BSWFoundation/Location/LocationFetcher.swift +++ b/Sources/BSWFoundation/Location/LocationFetcher.swift @@ -15,6 +15,7 @@ import CoreLocation let currentLocation = try await LocationFetcher.fetcher.fetchCurrentLocation() ``` */ +@MainActor public final class LocationFetcher: NSObject, CLLocationManagerDelegate { public enum LocationErrors: Swift.Error { @@ -85,19 +86,23 @@ public final class LocationFetcher: NSObject, CLLocationManagerDelegate { //MARK:- CLLocationManagerDelegate - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.first { - self.lastKnownLocation = location - completeCurrentRequest(.success(location)) + public nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + MainActor.assumeIsolated { + if let location = locations.first { + self.lastKnownLocation = location + completeCurrentRequest(.success(location)) + } } } - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + public nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Error finding location: \(error.localizedDescription)") - completeCurrentRequest() + MainActor.assumeIsolated { + completeCurrentRequest() + } } - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + public nonisolated func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { guard status != .notDetermined else { return } @@ -111,7 +116,9 @@ public final class LocationFetcher: NSObject, CLLocationManagerDelegate { if isStatusAuthorized { manager.requestLocation() } else { - completeCurrentRequest() + MainActor.assumeIsolated { + completeCurrentRequest() + } } } } diff --git a/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift b/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift index 43cb881..90aa1b2 100644 --- a/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift +++ b/Sources/BSWFoundation/PropertyWrappers/UserDefaultsBacked.swift @@ -8,7 +8,7 @@ import Foundation /// /// The value parameter can be only property list objects: `NSData`, `NSString`, `NSNumber`, `NSDate`, `NSArray`, or `NSDictionary`. @propertyWrapper -public class UserDefaultsBacked { +public final class UserDefaultsBacked: @unchecked Sendable { private let key: String private let defaultValue: T? private let store: UserDefaults @@ -51,7 +51,7 @@ public extension UserDefaultsBacked { /// Stores the given `T` type on User Defaults (as long as it's `Codable`) @propertyWrapper -public class CodableUserDefaultsBacked { +public final class CodableUserDefaultsBacked: @unchecked Sendable { private let key: String private let defaultValue: T? private let store: UserDefaults diff --git a/Tests/BSWFoundationTests/APIClient/APIClientTests.swift b/Tests/BSWFoundationTests/APIClient/APIClientTests.swift index 8809e93..9b12708 100644 --- a/Tests/BSWFoundationTests/APIClient/APIClientTests.swift +++ b/Tests/BSWFoundationTests/APIClient/APIClientTests.swift @@ -5,7 +5,7 @@ import XCTest import BSWFoundation -class APIClientTests: XCTestCase { +class APIClientTests: XCTestCase, @unchecked Sendable { var sut: APIClient! @@ -199,7 +199,7 @@ private func generateRandomData() -> Data { private class MockAPIClientDelegate: NSObject, APIClientDelegate { func apiClientDidReceiveUnauthorized(forRequest atPath: String, apiClientID: APIClient.ID) async throws -> Bool { failedPath = atPath - XCTAssert(Thread.isMainThread) + dispatchPrecondition(condition: .onQueue(.main)) return false } var failedPath: String? diff --git a/Tests/BSWFoundationTests/Extensions/ProgressObserverTests.swift b/Tests/BSWFoundationTests/Extensions/ProgressObserverTests.swift index 2f96017..d5d0a14 100644 --- a/Tests/BSWFoundationTests/Extensions/ProgressObserverTests.swift +++ b/Tests/BSWFoundationTests/Extensions/ProgressObserverTests.swift @@ -5,6 +5,7 @@ import BSWFoundation class ProgressObserverTests: XCTestCase { + @MainActor func testProgressObserving() { let progress = Progress(totalUnitCount: 2) diff --git a/Tests/BSWFoundationTests/Extensions/ThrottlerTests.swift b/Tests/BSWFoundationTests/Extensions/ThrottlerTests.swift index 7edbba4..2ea420c 100644 --- a/Tests/BSWFoundationTests/Extensions/ThrottlerTests.swift +++ b/Tests/BSWFoundationTests/Extensions/ThrottlerTests.swift @@ -2,7 +2,7 @@ import Foundation import XCTest import BSWFoundation -class ThrottlerTests: XCTestCase { +class ThrottlerTests: XCTestCase, @unchecked Sendable { func testItWorks() { let sut = Throttler(seconds: 0.5) var numberOfTimesThisIsExecuted = 0 @@ -28,7 +28,7 @@ class ThrottlerTests: XCTestCase { let sut = Throttler(seconds: maxInterval) var numberOfTimesThisIsExecuted = 0 let exp = expectation(description: "must be filled once") - var areWeDoneHere = false + nonisolated(unsafe) var areWeDoneHere = false DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(maxInterval * 100) - 10)) { exp.fulfill() areWeDoneHere = true diff --git a/Tests/BSWFoundationTests/Extensions/UIApplicationTests.swift b/Tests/BSWFoundationTests/Extensions/UIApplicationTests.swift index 01d2505..4ff7854 100644 --- a/Tests/BSWFoundationTests/Extensions/UIApplicationTests.swift +++ b/Tests/BSWFoundationTests/Extensions/UIApplicationTests.swift @@ -5,6 +5,7 @@ import XCTest import BSWFoundation class UIApplicationTests: XCTestCase { + @MainActor func testItWorks() { XCTAssert(UIApplication.shared.isRunningTests) }