Skip to content

Commit

Permalink
Merge pull request #22 from Lickability/feature/swift-6
Browse files Browse the repository at this point in the history
Swift 6 and Concurrency update
  • Loading branch information
Cordavi authored Nov 7, 2024
2 parents 928c04f + 264027d commit ffcbb02
Show file tree
Hide file tree
Showing 13 changed files with 74 additions and 76 deletions.
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
"repositoryURL": "https://github.com/Lickability/Networking",
"state": {
"branch": null,
"revision": "c7013a352d5a52ed433a34b7af50b3bc008d37cb",
"version": "2.2.2"
"revision": "89772986b8c38ed33dfcf2e7e89e649280c35049",
"version": "3.0.0"
}
},
{
"package": "Persister",
"repositoryURL": "https://github.com/Lickability/Persister",
"state": {
"branch": null,
"revision": "556198feaa1b37cfb54328af43dcb2709002972a",
"version": "1.0.0"
"revision": "f51e2226f0c8b0c9c37ded2739a42181accf2953",
"version": "2.0.0"
}
}
]
Expand Down
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -7,16 +7,16 @@ let name = "Provider"
let package = Package(
name: name,
defaultLocalization: "en",
platforms: [.iOS(.v13)],
platforms: [.iOS(.v16)],
products: [.library(name: name, targets: [name])],
dependencies: [
.package(
url: "https://github.com/Lickability/Networking",
.upToNextMajor(from: "2.0.0")
.upToNextMajor(from: "3.0.0")
),
.package(
url: "https://github.com/Lickability/Persister",
.upToNextMajor(from: "1.0.0")
.upToNextMajor(from: "2.0.0")
)
],
targets: [.target(name: name, dependencies: ["Networking", "Persister"], resources: [.process("Resources")])]
Expand Down
25 changes: 16 additions & 9 deletions Provider.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,9 @@
4C04A5FA24E6EEBA00D73E0E /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1200;
LastUpgradeCheck = 1610;
ORGANIZATIONNAME = Lickability;
TargetAttributes = {
4C04A60124E6EEBA00D73E0E = {
Expand Down Expand Up @@ -378,6 +379,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
Expand Down Expand Up @@ -411,6 +413,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand All @@ -432,13 +435,15 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
4C04A62024E6EEBC00D73E0E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
Expand Down Expand Up @@ -472,6 +477,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -486,6 +492,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand All @@ -499,6 +506,7 @@
DEVELOPMENT_TEAM = JL4AKR8DVC;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -519,6 +527,7 @@
DEVELOPMENT_TEAM = JL4AKR8DVC;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Example/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -533,12 +542,11 @@
4C04A62524E6EEBC00D73E0E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = JL4AKR8DVC;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -555,12 +563,11 @@
4C04A62624E6EEBC00D73E0E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = JL4AKR8DVC;
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -611,24 +618,24 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Lickability/Persister";
requirement = {
branch = main;
kind = branch;
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
4CC4416124E6F7EB00F48427 /* XCRemoteSwiftPackageReference "Networking" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Lickability/Networking";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.2;
minimumVersion = 3.0.0;
};
};
F27A5791250BF23900FBAD8F /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/AliSoftware/OHHTTPStubs.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.0.0;
minimumVersion = 9.1.0;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lickability/Networking",
"state" : {
"revision" : "c7013a352d5a52ed433a34b7af50b3bc008d37cb",
"version" : "2.2.2"
"revision" : "89772986b8c38ed33dfcf2e7e89e649280c35049",
"version" : "3.0.0"
}
},
{
"identity" : "ohhttpstubs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/AliSoftware/OHHTTPStubs.git",
"state" : {
"revision" : "e92b5a5746ef16add2a1424f1fc19529d9a75cde",
"version" : "9.0.0"
"revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9",
"version" : "9.1.0"
}
},
{
"identity" : "persister",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lickability/Persister",
"state" : {
"branch" : "main",
"revision" : "ac8f39433b3ced5df28f6c414d06ddc50a57ba7c"
"revision" : "f51e2226f0c8b0c9c37ded2739a42181accf2953",
"version" : "2.0.0"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Sources/Provider/Identifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
public typealias Key = String

/// Describes a type that can be uniquely identified by a `Key`.
public protocol Identifiable {
public protocol Identifiable: Sendable {

/// The key used to uniquely identify the receiver.
var identifier: Key { get }
Expand Down
30 changes: 14 additions & 16 deletions Sources/Provider/ItemProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import Networking
import Persister

/// Retrieves items from persistence or networking and stores them in persistence.
public final class ItemProvider {
@MainActor
public final class ItemProvider: Sendable {

/// The policy for how the provider checks the cache and/or the network for items.
public enum FetchPolicy {
public enum FetchPolicy: Sendable {
/// Only request from the network if we don't have items in the cache. If items exist in the cache and are expired, it returns items from the cache and the network.
case returnFromCacheElseNetwork

Expand All @@ -33,7 +34,6 @@ public final class ItemProvider {

private let fetchPolicy: FetchPolicy
private let defaultProviderBehaviors: [ProviderBehavior]
private let providerQueue = DispatchQueue(label: "ProviderQueue", attributes: .concurrent)
private var cancellables = Set<AnyCancellable?>()

/// Creates a new `ItemProvider`.
Expand All @@ -54,8 +54,7 @@ extension ItemProvider: Provider {

// MARK: - Provider

@discardableResult
public func provide<Item: Providable>(request: any ProviderRequest, decoder: ItemDecoder = JSONDecoder(), providerBehaviors: [ProviderBehavior] = [], requestBehaviors: [RequestBehavior] = [], handlerQueue: DispatchQueue = .main, allowExpiredItem: Bool = false, itemHandler: @escaping (Result<Item, ProviderError>) -> Void) -> AnyCancellable? {
@discardableResult public func provide<Item: Providable>(request: any ProviderRequest, decoder: ItemDecoder = JSONDecoder(), providerBehaviors: [ProviderBehavior] = [], requestBehaviors: [RequestBehavior] = [], handlerQueue: DispatchQueue = .main, allowExpiredItem: Bool = false, itemHandler: @escaping (Result<Item, ProviderError>) -> Void) -> AnyCancellable? {

var cancellable: AnyCancellable?
cancellable = provide(request: request,
Expand All @@ -68,21 +67,21 @@ extension ItemProvider: Provider {
switch result {
case let .failure(error):
itemHandler(.failure(error))
case .finished: break
case .finished:
break
}

self?.cancellables.remove(cancellable)
}, receiveValue: { (item: Item) in
itemHandler(.success(item))
})

handlerQueue.async { self.cancellables.insert(cancellable) }
self.cancellables.insert(cancellable)

return cancellable
}

@discardableResult
public func provideItems<Item: Providable>(request: any ProviderRequest, decoder: ItemDecoder = JSONDecoder(), providerBehaviors: [ProviderBehavior] = [], requestBehaviors: [RequestBehavior] = [], handlerQueue: DispatchQueue = .main, allowExpiredItems: Bool = false, itemsHandler: @escaping (Result<[Item], ProviderError>) -> Void) -> AnyCancellable? {
@discardableResult public func provideItems<Item: Providable>(request: any ProviderRequest, decoder: ItemDecoder = JSONDecoder(), providerBehaviors: [ProviderBehavior] = [], requestBehaviors: [RequestBehavior] = [], handlerQueue: DispatchQueue = .main, allowExpiredItems: Bool = false, itemsHandler: @escaping (Result<[Item], ProviderError>) -> Void) -> AnyCancellable? {

var cancellable: AnyCancellable?
cancellable = provideItems(request: request,
Expand All @@ -95,19 +94,20 @@ extension ItemProvider: Provider {
switch result {
case let .failure(error):
itemsHandler(.failure(error))
case .finished: break
case .finished:
break
}

self?.cancellables.remove(cancellable)
}, receiveValue: { (items: [Item]) in
itemsHandler(.success(items))
})

handlerQueue.async { self.cancellables.insert(cancellable) }
self.cancellables.insert(cancellable)

return cancellable
}

public func provide<Item: Providable>(request: any ProviderRequest, decoder: ItemDecoder = JSONDecoder(), providerBehaviors: [ProviderBehavior] = [], requestBehaviors: [RequestBehavior] = [], allowExpiredItem: Bool = false) -> AnyPublisher<Item, ProviderError> {

let cachePublisher: Result<ItemContainer<Item>?, ProviderError>.Publisher = itemCachePublisher(for: request)
Expand Down Expand Up @@ -154,7 +154,6 @@ extension ItemProvider: Provider {
}, receiveOutput: { item in
providerBehaviors.providerDidProvide(item: item, forRequest: request)
})
.subscribe(on: providerQueue)
.eraseToAnyPublisher()
}

Expand All @@ -173,7 +172,7 @@ extension ItemProvider: Provider {
}

private func itemNetworkPublisher<Item: Providable>(for request: any ProviderRequest, behaviors: [RequestBehavior], decoder: ItemDecoder) -> AnyPublisher<Item, ProviderError> {
return networkRequestPerformer.send(request, requestBehaviors: behaviors)
return networkRequestPerformer.send(request, scheduler: DispatchQueue.main, requestBehaviors: behaviors)
.mapError { ProviderError.networkError($0) }
.unpackData(errorTransform: { _ in ProviderError.networkError(.noData) })
.decodeItem(decoder: decoder, errorTransform: { ProviderError.decodingError($0) })
Expand Down Expand Up @@ -244,7 +243,6 @@ extension ItemProvider: Provider {
}, receiveOutput: { item in
providerBehaviors.providerDidProvide(item: item, forRequest: request)
})
.subscribe(on: providerQueue)
.eraseToAnyPublisher()
}

Expand Down Expand Up @@ -280,7 +278,7 @@ extension ItemProvider: Provider {

private func itemsNetworkPublisher<Item: Providable>(for request: any ProviderRequest, behaviors: [RequestBehavior], decoder: ItemDecoder) -> AnyPublisher<[Item], ProviderError> {

return networkRequestPerformer.send(request, requestBehaviors: behaviors)
return networkRequestPerformer.send(request, scheduler: DispatchQueue.main, requestBehaviors: behaviors)
.mapError { ProviderError.networkError($0) }
.unpackData(errorTransform: { _ in ProviderError.networkError(.noData) })
.decodeItems(decoder: decoder, errorTransform: { ProviderError.decodingError($0) })
Expand Down
17 changes: 8 additions & 9 deletions Sources/Provider/ProvideItemRequestStateController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import Foundation
import Networking
import Persister
import Combine
@preconcurrency import Combine

/// A class responsible for representing the state and value of a provider item request being made.
public final class ProvideItemRequestStateController<Item: Providable> {
@MainActor
public final class ProvideItemRequestStateController<Item: Providable>: Sendable {

/// The state of a provider request's lifecycle.
public enum ProvideItemRequestState {
Expand Down Expand Up @@ -87,18 +88,18 @@ public final class ProvideItemRequestStateController<Item: Providable> {
}

/// A `Publisher` that can be subscribed to in order to receive updates about the status of a request.
public private(set) lazy var publisher: AnyPublisher<ProvideItemRequestState, Never> = {
return providerStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
}()
public let publisher: AnyPublisher<ProvideItemRequestState, Never>

private let provider: Provider
private let providerStatePublisher = PassthroughSubject<ProvideItemRequestState, Never>()
private let providerStatePublisher: PassthroughSubject<ProvideItemRequestState, Never>
private var cancellables = Set<AnyCancellable>()

/// Initializes the `ProvideItemRequestStateController` with the specified parameters.
/// - Parameter provider: The `Provider` used to provide a response from.
public init(provider: Provider) {
self.provider = provider
self.providerStatePublisher = PassthroughSubject<ProvideItemRequestState, Never>()
self.publisher = providerStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
}

/// Sends a request with the specified parameters to provide back an item.
Expand All @@ -119,15 +120,13 @@ public final class ProvideItemRequestStateController<Item: Providable> {
.receive(on: scheduler)
.sink { [providerStatePublisher] result in
providerStatePublisher.send(.completed(result))
}
.store(in: &cancellables)
}.store(in: &cancellables)
}

/// Resets the state of the `providerStatePublisher` and cancels any in flight requests that may be ongoing. Cancellation is not guaranteed, and requests that are near completion may end up finishing, despite being cancelled.
public func resetState() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()

providerStatePublisher.send(.notInProgress)
}
}
Loading

0 comments on commit ffcbb02

Please sign in to comment.