From 3410eb4deccfe70a773b29018c961a5d58a61f9f Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:31:57 -0700 Subject: [PATCH 01/10] Add async pagination --- Package.swift | 15 ++ .../Example/Example.xcodeproj/project.pbxproj | 20 +- native/swift/Example/Example/ExampleApp.swift | 22 ++- .../swift/Example/Example/ListViewData.swift | 19 +- .../swift/Example/Example/ListViewModel.swift | 175 +++++++++++++++--- .../swift/Example/Example/UI/ListView.swift | 9 +- .../Example/Example/UI/RootListView.swift | 67 +++++-- .../Sources/wordpress-api-combine/Posts.swift | 47 +++++ .../Sources/wordpress-api/Pagination.swift | 86 ++++++++- .../wordpress-api/SafeRequestExecutor.swift | 2 +- scripts/xcodebuild-test.sh | 2 +- 11 files changed, 409 insertions(+), 55 deletions(-) create mode 100644 native/swift/Sources/wordpress-api-combine/Posts.swift diff --git a/Package.swift b/Package.swift index fe58a6f88..ace2266a7 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,10 @@ var package = Package( .library( name: "WordPressAPI", targets: ["WordPressAPI"] + ), + .library( + name: "WordPressAPI+Combine", + targets: ["WordPressAPICombine"] ) ], dependencies: [], @@ -40,6 +44,17 @@ var package = Package( .enableExperimentalFeature("StrictConcurrency"), ] ), + .target( + name: "WordPressAPICombine", + dependencies: [ + .target(name: "WordPressAPI") + ], + path: "native/swift/Sources/wordpress-api-combine", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .target( name: "WordPressAPIInternal", dependencies: [ diff --git a/native/swift/Example/Example.xcodeproj/project.pbxproj b/native/swift/Example/Example.xcodeproj/project.pbxproj index cd5c0f6ba..c5c1b4fa7 100644 --- a/native/swift/Example/Example.xcodeproj/project.pbxproj +++ b/native/swift/Example/Example.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 2479BF932B621E9B0014A01D /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2479BF922B621E9B0014A01D /* ListViewModel.swift */; }; 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */; }; 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C3352BAA874C00162AD1 /* LoginManager.swift */; }; + 24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 24E77D022CE44DD900F6998C /* WordPressAPI */; }; + 24E77D052CE44DD900F6998C /* WordPressAPI+Combine in Frameworks */ = {isa = PBXBuildFile; productRef = 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -40,7 +42,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 24E77D052CE44DD900F6998C /* WordPressAPI+Combine in Frameworks */, 2479BF912B621CCA0014A01D /* WordPressAPI in Frameworks */, + 24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -123,6 +127,8 @@ name = Example; packageProductDependencies = ( 2479BF902B621CCA0014A01D /* WordPressAPI */, + 24E77D022CE44DD900F6998C /* WordPressAPI */, + 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */, ); productName = Example; productReference = 2479BF7D2B621CB60014A01D /* Example.app */; @@ -153,7 +159,7 @@ ); mainGroup = 2479BF742B621CB60014A01D; packageReferences = ( - 2479BF8F2B621CCA0014A01D /* XCLocalSwiftPackageReference "../../.." */, + 24E77D012CE44DD900F6998C /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */, ); productRefGroup = 2479BF7E2B621CB60014A01D /* Products */; projectDirPath = ""; @@ -409,9 +415,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 2479BF8F2B621CCA0014A01D /* XCLocalSwiftPackageReference "../../.." */ = { + 24E77D012CE44DD900F6998C /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */ = { isa = XCLocalSwiftPackageReference; - relativePath = ../../..; + relativePath = "../../../../wordpress-rs"; }; /* End XCLocalSwiftPackageReference section */ @@ -420,6 +426,14 @@ isa = XCSwiftPackageProductDependency; productName = WordPressAPI; }; + 24E77D022CE44DD900F6998C /* WordPressAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = WordPressAPI; + }; + 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */ = { + isa = XCSwiftPackageProductDependency; + productName = "WordPressAPI+Combine"; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2479BF752B621CB60014A01D /* Project object */; diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index 30272e1cc..8b0699298 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -1,5 +1,7 @@ import SwiftUI import WordPressAPI +import WordPressAPICombine +import Combine @main struct ExampleApp: App { @@ -27,9 +29,23 @@ struct ExampleApp: App { value.asListViewData } }), - RootListData(name: "Posts", callback: { - try await WordPressAPI.globalInstance.posts.paginatedWithEditContext(params: PostListParams(perPage: 100)) - .map { $0.asListViewData } + RootListData(name: "Posts with Combine", stream: { + let stream = try WordPressAPI.globalInstance.posts.paginatedStream(params: PostListParams(perPage: 5)) + + return ListViewDataStream( + publisher: stream.getPublisher().map { $0.asListViewData() }.eraseToAnyPublisher(), + underlyingStream: stream + ) + }), + RootListData(name: "Posts with AsyncSequence", sequence: { + do { + let sequence = try WordPressAPI.globalInstance + .posts + .paginatedSequenceWithEditContext(params: PostListParams(perPage: 5)) + return ListViewSequence(underlyingSequence: sequence) + } catch { + abort() + } }), RootListData(name: "Site Health Tests", callback: { let items: [any ListViewDataConvertable] = [ diff --git a/native/swift/Example/Example/ListViewData.swift b/native/swift/Example/Example/ListViewData.swift index ab5df794c..63112ea40 100644 --- a/native/swift/Example/Example/ListViewData.swift +++ b/native/swift/Example/Example/ListViewData.swift @@ -1,11 +1,16 @@ import Foundation import WordPressAPI +import WordPressAPIInternal -struct ListViewData: Identifiable { +struct ListViewData: Identifiable, Comparable, Hashable { let id: String let title: String let subtitle: String let fields: [String: String] + + static func < (lhs: ListViewData, rhs: ListViewData) -> Bool { + lhs.title < rhs.title + } } protocol ListViewDataConvertable: Identifiable { @@ -151,3 +156,15 @@ extension PostWithEditContext: ListViewDataConvertable { ListViewData(id: self.id, title: self.title.raw, subtitle: self.slug, fields: [:]) } } + +extension [PostWithEditContext] { + func asListViewData() -> [ListViewData] { + self.map { $0.asListViewData } + } +} + +extension [ListViewDataConvertable] { + func asListViewData() -> [ListViewData] { + self.map { $0.asListViewData } + } +} diff --git a/native/swift/Example/Example/ListViewModel.swift b/native/swift/Example/Example/ListViewModel.swift index 1d20dd164..ec2216cfd 100644 --- a/native/swift/Example/Example/ListViewModel.swift +++ b/native/swift/Example/Example/ListViewModel.swift @@ -1,47 +1,132 @@ import Foundation import SwiftUI import WordPressAPI +import WordPressAPICombine +import Combine -@Observable class ListViewModel { +@MainActor +protocol ListViewModel { + + /// Guarantee only one object with each ID, but allow updating the object when new data comes in + var listItems: [String: ListViewData] { get } + + var shouldPresentAlert: Bool { get set } + + var error: MyError? { get set } + + func task() async +} + +@Observable class SequenceListViewModel: ListViewModel { + var listItems: [String: ListViewData] = [String: ListViewData](minimumCapacity: 250) + + typealias SequenceProvider = () -> ListViewSequence + + private let sequenceProvider: SequenceProvider + + init(sequenceProvider: @escaping SequenceProvider) { + self.sequenceProvider = sequenceProvider + } + + var shouldPresentAlert: Bool = false + + var error: MyError? + + var sequence: ListViewSequence? + + func task() async { + do { + for try await page in self.sequenceProvider() { + for item in page { + self.listItems[item.id] = item + } + } + } catch { + self.error = .init(underlyingError: error) + self.shouldPresentAlert = true + } + } + + func reset() { + + } +} + +@Observable class TaskListViewModel: ListViewModel { typealias FetchDataTask = () async throws -> [ListViewData] - var listItems: [ListViewData] = [] + var listItems: [String: ListViewData] = [:] private var dataCallback: FetchDataTask - private var dataTask: Task? var isLoading: Bool = false var error: MyError? var shouldPresentAlert = false - let loginManager: LoginManager - - init(loginManager: LoginManager, dataCallback: @escaping FetchDataTask) { - self.loginManager = loginManager + init(dataCallback: @escaping FetchDataTask) { self.dataCallback = dataCallback } - func startFetching() { - self.error = nil + func task() async { + self.isLoading = true self.shouldPresentAlert = false - self.dataTask = Task { @MainActor in - self.isLoading = true - self.shouldPresentAlert = false - - do { - self.listItems = try await dataCallback() - } catch { - self.error = MyError(underlyingError: error) - self.shouldPresentAlert = true + do { + for item in try await dataCallback() { + listItems[item.id] = item } - - self.isLoading = false + } catch { + self.error = MyError(underlyingError: error) + self.shouldPresentAlert = true } + + self.isLoading = false } +} + +@Observable class CombineListViewModel: ListViewModel { + + public typealias StreamProvider = () throws -> ListViewDataStream + + var listItems: [String: ListViewData] = [:] + var isLoading: Bool = false - func stopFetching() { - self.dataTask?.cancel() + var shouldPresentAlert: Bool = false + + var error: MyError? + var cancellables: Set = [] + + private let streamProvider: StreamProvider + private var currentStream: ListViewDataStream? + + init(streamProvider: @escaping StreamProvider) { + self.streamProvider = streamProvider + self.currentStream = nil + } + + func task() async { + self.error = nil + self.currentStream = try? self.streamProvider() + self.currentStream?.getPublisher() + .sink { completion in + switch completion { + case .finished: + self.isLoading = false + + case .failure(let error): + self.error = MyError(underlyingError: error) + self.shouldPresentAlert = true + } + } receiveValue: { newValue in + withAnimation { + for item in newValue { + self.listItems[item.id] = item + } + } + } + .store(in: &cancellables) + + try? await self.currentStream?.fetch() } } @@ -60,3 +145,49 @@ struct MyError: LocalizedError { underlyingError.localizedDescription } } + +struct ListViewDataStream: WordPressAPICombine.Stream { + typealias ValueType = [ListViewData] + + let publisher: AnyPublisher<[ListViewData], Error> + let underlyingStream: any WordPressAPICombine.Stream + + func fetch() async throws { + try await self.underlyingStream.fetch() + } + + func getPublisher() -> AnyPublisher<[ListViewData], any Error> { + publisher + } +} + +struct ListViewSequence: AsyncSequence { + typealias Element = [ListViewData] + + private let underlyingSequence: any AsyncSequence + + init(underlyingSequence: any AsyncSequence) { + self.underlyingSequence = underlyingSequence + } + + struct ListViewIterator: AsyncIteratorProtocol { + var underlyingSequence: any AsyncIteratorProtocol + + mutating func next() async throws -> Element? { + guard let nextElement = try await underlyingSequence.next() else { + return nil + } + + guard let listViewData = nextElement as? [any ListViewDataConvertable] else { + debugPrint("Unable to convert data to `ListViewDataConvertable`") + return nil + } + + return listViewData.asListViewData() + } + } + + func makeAsyncIterator() -> ListViewIterator { + ListViewIterator(underlyingSequence: underlyingSequence.makeAsyncIterator()) + } +} diff --git a/native/swift/Example/Example/UI/ListView.swift b/native/swift/Example/Example/UI/ListView.swift index 79353474c..11742b1df 100644 --- a/native/swift/Example/Example/UI/ListView.swift +++ b/native/swift/Example/Example/UI/ListView.swift @@ -6,7 +6,7 @@ struct ListView: View { var viewModel: ListViewModel var body: some View { - List(viewModel.listItems) { item in + List(viewModel.listItems.values.sorted(), id: \.id) { item in VStack(alignment: .leading) { Text(item.title).font(.headline) Text(item.subtitle).font(.footnote) @@ -29,14 +29,15 @@ struct ListView: View { } } ) - .onAppear(perform: viewModel.startFetching) - .onDisappear(perform: viewModel.stopFetching) + .task { + await viewModel.task() + } } } #Preview { - let viewModel = ListViewModel(loginManager: LoginManager(), dataCallback: { + let viewModel = TaskListViewModel(dataCallback: { [ ListViewData(id: "1234", title: "Item 1", subtitle: "Subtitle", fields: [:]) ] diff --git a/native/swift/Example/Example/UI/RootListView.swift b/native/swift/Example/Example/UI/RootListView.swift index 2a7167f24..7d1d92831 100644 --- a/native/swift/Example/Example/UI/RootListView.swift +++ b/native/swift/Example/Example/UI/RootListView.swift @@ -1,5 +1,7 @@ import SwiftUI import WordPressAPI +import WordPressAPICombine +import Combine struct RootListView: View { @@ -16,28 +18,67 @@ struct RootListViewItem: View { let item: RootListData var body: some View { - VStack(alignment: .leading, spacing: 4.0) { - NavigationLink { - ListView( - viewModel: ListViewModel( - loginManager: LoginManager(), - dataCallback: self.item.callback + switch item { + case .callback(let name, let fetchDataTask): + VStack(alignment: .leading, spacing: 4.0) { + NavigationLink { + ListView( + viewModel: TaskListViewModel(dataCallback: fetchDataTask) ) - ) - } label: { - Text(item.name) + } label: { + Text(name) + } + } + + case .combine(let name, let streamProvider): + VStack(alignment: .leading, spacing: 4.0) { + NavigationLink { + ListView( + viewModel: CombineListViewModel(streamProvider: streamProvider) + ) + } label: { + Text(name) + } + } + + case .sequence(let name, let sequenceProvider): + VStack(alignment: .leading, spacing: 4.0) { + NavigationLink { + ListView( + viewModel: SequenceListViewModel(sequenceProvider: sequenceProvider) + ) + } label: { + Text(name) + } } } } } -struct RootListData: Identifiable { +enum RootListData: Identifiable { - let name: String - let callback: ListViewModel.FetchDataTask + case callback(String, TaskListViewModel.FetchDataTask) + case combine(String, CombineListViewModel.StreamProvider) + case sequence(String, SequenceListViewModel.SequenceProvider) var id: String { - self.name + switch self { + case .callback(let id, _): id + case .combine(let id, _): id + case .sequence(let id, _): id + } + } + + init(name: String, callback: @escaping TaskListViewModel.FetchDataTask) { + self = .callback(name, callback) + } + + init(name: String, stream: @escaping CombineListViewModel.StreamProvider) { + self = .combine(name, stream) + } + + init(name: String, sequence: @escaping SequenceListViewModel.SequenceProvider) { + self = .sequence(name, sequence) } } diff --git a/native/swift/Sources/wordpress-api-combine/Posts.swift b/native/swift/Sources/wordpress-api-combine/Posts.swift new file mode 100644 index 000000000..397f5fb0b --- /dev/null +++ b/native/swift/Sources/wordpress-api-combine/Posts.swift @@ -0,0 +1,47 @@ +import Combine +import WordPressAPI + +public extension PostsRequestExecutor { + func paginatedStream(params: PostListParams) -> PostsStream { + PostsStream(executor: self, params: params) + } +} + +public protocol Stream { + associatedtype Element + + func fetch() async throws + func getPublisher() -> AnyPublisher +} + +public class PostsStream: Stream { + private let publisher: CurrentValueSubject<[PostWithEditContext], Error> + private let executor: PostsRequestExecutor + private let params: PostListParams + + private var accumulatedObjects: [PostWithEditContext] = [] + + public init(executor: PostsRequestExecutor, params: PostListParams) { + self.publisher = CurrentValueSubject([]) + self.executor = executor + self.params = params + } + + public func getPublisher() -> AnyPublisher<[PostWithEditContext], any Error> { + publisher.eraseToAnyPublisher() + } + + public func fetch() async throws { + self.accumulatedObjects = [] + self.publisher.send(self.accumulatedObjects) + + do { + for try await posts in executor.paginatedSequenceWithEditContext(params: params) { + accumulatedObjects.append(contentsOf: posts) + publisher.send(accumulatedObjects) + } + } catch { + publisher.send(completion: .failure(error)) + } + } +} diff --git a/native/swift/Sources/wordpress-api/Pagination.swift b/native/swift/Sources/wordpress-api/Pagination.swift index 92c7e8ea5..7ffce6e1c 100644 --- a/native/swift/Sources/wordpress-api/Pagination.swift +++ b/native/swift/Sources/wordpress-api/Pagination.swift @@ -1,6 +1,6 @@ import Foundation -public protocol PaginatableResponse { +public protocol PaginatableResponse: Sendable { associatedtype ParamsType associatedtype DataType @@ -32,6 +32,18 @@ public protocol PaginationAwareExecutor { func paginatedWithEmbedContext( params: EmbedContextResponseType.ParamsType ) async throws -> [EmbedContextResponseType.DataType] + + func paginatedSequenceWithEditContext( + params: EditContextResponseType.ParamsType + ) -> PaginationSequence + + func paginatedSequenceWithViewContext( + params: ViewContextResponseType.ParamsType + ) -> PaginationSequence + + func paginatedSequenceWithEmbedContext( + params: EmbedContextResponseType.ParamsType + ) -> PaginationSequence } extension PaginationAwareExecutor { @@ -106,20 +118,80 @@ extension PaginationAwareExecutor { return allObjects } + + public func paginatedSequenceWithEditContext( + params: EditContextResponseType.ParamsType + ) -> PaginationSequence { + PaginationSequence(params: params) { params in + try await self.listWithEditContext(params: params) + } + } + + public func paginatedSequenceWithViewContext( + params: ViewContextResponseType.ParamsType + ) -> PaginationSequence { + PaginationSequence(params: params) { params in + try await self.listWithViewContext(params: params) + } + } + + public func paginatedSequenceWithEmbedContext( + params: EmbedContextResponseType.ParamsType + ) -> PaginationSequence { + PaginationSequence(params: params) { params in + try await self.listWithEmbedContext(params: params) + } + } +} + +public struct PaginationSequence: AsyncSequence { + public typealias Transformer = (ResponseType.ParamsType) async throws -> ResponseType + + private let params: ResponseType.ParamsType + private let transform: Transformer + + public init(params: ResponseType.ParamsType, transform: @escaping Transformer) { + self.params = params + self.transform = transform + } + + public struct AsyncIterator: AsyncIteratorProtocol { + private var nextPageParams: ResponseType.ParamsType? + private let transform: Transformer + + init(params: ResponseType.ParamsType, transform: @escaping Transformer) { + self.nextPageParams = params + self.transform = transform + } + + public mutating func next() async throws -> [ResponseType.DataType]? { + guard let nextPageParams else { + return nil + } + + let response = try await self.transform(nextPageParams) + self.nextPageParams = response.nextPageParams + return response.data + } + } + + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(params: params, transform: self.transform) + } } // MARK: - Posts -extension PostsRequestListWithEditContextResponse: PaginatableResponse { +extension PostsRequestListWithEditContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = PostListParams public typealias DataType = PostWithEditContext } -extension PostsRequestListWithViewContextResponse: PaginatableResponse { +extension PostsRequestListWithViewContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = PostListParams public typealias DataType = PostWithViewContext } -extension PostsRequestListWithEmbedContextResponse: PaginatableResponse { +extension PostsRequestListWithEmbedContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = PostListParams public typealias DataType = PostWithEmbedContext } @@ -131,17 +203,17 @@ extension PostsRequestExecutor: PaginationAwareExecutor { } // MARK: - Users -extension UsersRequestListWithEditContextResponse: PaginatableResponse { +extension UsersRequestListWithEditContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = UserListParams public typealias DataType = UserWithEditContext } -extension UsersRequestListWithViewContextResponse: PaginatableResponse { +extension UsersRequestListWithViewContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = UserListParams public typealias DataType = UserWithViewContext } -extension UsersRequestListWithEmbedContextResponse: PaginatableResponse { +extension UsersRequestListWithEmbedContextResponse: PaginatableResponse, @unchecked Sendable { public typealias ParamsType = UserListParams public typealias DataType = UserWithEmbedContext } diff --git a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift index 3449f7390..ab0e38895 100644 --- a/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift +++ b/native/swift/Sources/wordpress-api/SafeRequestExecutor.swift @@ -7,7 +7,7 @@ import WordPressAPIInternal import FoundationNetworking #endif -public protocol SafeRequestExecutor: RequestExecutor { +public protocol SafeRequestExecutor: RequestExecutor, Sendable { func execute(_ request: WpNetworkRequest) async -> Result } diff --git a/scripts/xcodebuild-test.sh b/scripts/xcodebuild-test.sh index 1afeb8d5f..b48af587f 100755 --- a/scripts/xcodebuild-test.sh +++ b/scripts/xcodebuild-test.sh @@ -11,7 +11,7 @@ device_id=$(xcrun simctl list --json devices available | jq -re ".devices.\"com. export NSUnbufferedIO=YES xcodebuild \ - -scheme WordPressAPI \ + -scheme WordPressAPI-Package \ -derivedDataPath DerivedData \ -destination "id=${device_id}" \ -skipPackagePluginValidation \ From 4ca6c677b71f1251c600eafea540666bf077c32f Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:39:24 -0700 Subject: [PATCH 02/10] Add Sendable Tests --- .../Sources/wordpress-api/Pagination.swift | 2 ++ .../Tests/wordpress-api/SendableTests.swift | 25 +++++++++++++++++++ .../wordpress-api/Support/Extensions.swift | 6 +++++ 3 files changed, 33 insertions(+) create mode 100644 native/swift/Tests/wordpress-api/SendableTests.swift diff --git a/native/swift/Sources/wordpress-api/Pagination.swift b/native/swift/Sources/wordpress-api/Pagination.swift index 7ffce6e1c..d0f4ee594 100644 --- a/native/swift/Sources/wordpress-api/Pagination.swift +++ b/native/swift/Sources/wordpress-api/Pagination.swift @@ -8,6 +8,8 @@ public protocol PaginatableResponse: Sendable { var prevPageParams: ParamsType? { get } var data: [DataType] { get } + + init(data: [DataType], headerMap: WpNetworkHeaderMap, nextPageParams: ParamsType?, prevPageParams: ParamsType?) } public protocol PaginationAwareExecutor { diff --git a/native/swift/Tests/wordpress-api/SendableTests.swift b/native/swift/Tests/wordpress-api/SendableTests.swift new file mode 100644 index 000000000..7125c29f1 --- /dev/null +++ b/native/swift/Tests/wordpress-api/SendableTests.swift @@ -0,0 +1,25 @@ +import Testing +import WordPressAPI + +struct SendableTests { + + private static let sendables: [Sendable] = [ + PostsRequestListWithEditContextResponse.empty, + PostsRequestListWithViewContextResponse.empty, + PostsRequestListWithEmbedContextResponse.empty, + + UsersRequestListWithEditContextResponse.empty, + UsersRequestListWithViewContextResponse.empty, + UsersRequestListWithEmbedContextResponse.empty, + ] + + /// This might seem like a weird test – why are we checking such a specific implementation detail? + /// + /// This ensures that we don't inadvertently change these types (which we are adding an unsafe `Sendable` + /// conformance to) in Uniffi from `uniffi::Record` to `uniffi::Object`. This removes their ability to + /// be `Sendable`, and our conformance would no longer be safe + @Test("Test that late-conforming sendable types are safe", arguments: sendables) + func testThatTypesAreSendable(_ type: Sendable) { + #expect(Mirror(reflecting: type).displayStyle == .struct) + } +} diff --git a/native/swift/Tests/wordpress-api/Support/Extensions.swift b/native/swift/Tests/wordpress-api/Support/Extensions.swift index a24b603dd..3ae3012c0 100644 --- a/native/swift/Tests/wordpress-api/Support/Extensions.swift +++ b/native/swift/Tests/wordpress-api/Support/Extensions.swift @@ -13,6 +13,12 @@ extension WpNetworkHeaderMap { } } +extension PaginatableResponse { + static var empty: Self { + Self(data: [], headerMap: .empty, nextPageParams: nil, prevPageParams: nil) + } +} + // These `Sendable` conformances are **NOT** safe – they're for the test suite only. // // Until or unless `WpNetworkRequest` and `WpNetworkRequest` become `uniffi::Record` (thus Structs) From d100aca82f66a0e12023ffe2fe863534db0ca350 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:29:37 -0700 Subject: [PATCH 03/10] Make the Combine paginator generic --- .../Sources/wordpress-api-combine/Posts.swift | 47 --------------- .../wordpress-api-combine/Stream.swift | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 47 deletions(-) delete mode 100644 native/swift/Sources/wordpress-api-combine/Posts.swift create mode 100644 native/swift/Sources/wordpress-api-combine/Stream.swift diff --git a/native/swift/Sources/wordpress-api-combine/Posts.swift b/native/swift/Sources/wordpress-api-combine/Posts.swift deleted file mode 100644 index 397f5fb0b..000000000 --- a/native/swift/Sources/wordpress-api-combine/Posts.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Combine -import WordPressAPI - -public extension PostsRequestExecutor { - func paginatedStream(params: PostListParams) -> PostsStream { - PostsStream(executor: self, params: params) - } -} - -public protocol Stream { - associatedtype Element - - func fetch() async throws - func getPublisher() -> AnyPublisher -} - -public class PostsStream: Stream { - private let publisher: CurrentValueSubject<[PostWithEditContext], Error> - private let executor: PostsRequestExecutor - private let params: PostListParams - - private var accumulatedObjects: [PostWithEditContext] = [] - - public init(executor: PostsRequestExecutor, params: PostListParams) { - self.publisher = CurrentValueSubject([]) - self.executor = executor - self.params = params - } - - public func getPublisher() -> AnyPublisher<[PostWithEditContext], any Error> { - publisher.eraseToAnyPublisher() - } - - public func fetch() async throws { - self.accumulatedObjects = [] - self.publisher.send(self.accumulatedObjects) - - do { - for try await posts in executor.paginatedSequenceWithEditContext(params: params) { - accumulatedObjects.append(contentsOf: posts) - publisher.send(accumulatedObjects) - } - } catch { - publisher.send(completion: .failure(error)) - } - } -} diff --git a/native/swift/Sources/wordpress-api-combine/Stream.swift b/native/swift/Sources/wordpress-api-combine/Stream.swift new file mode 100644 index 000000000..7ab75173a --- /dev/null +++ b/native/swift/Sources/wordpress-api-combine/Stream.swift @@ -0,0 +1,59 @@ +import Combine +import WordPressAPI + +public extension PaginationAwareExecutor { + func paginatedEditStream(params: Self.EditContextResponseType.ParamsType) -> Stream { + Stream(executor: self, params: params) { executor, params in + executor.paginatedSequenceWithEditContext(params: params) + } + } + + func paginatedViewStream(params: Self.ViewContextResponseType.ParamsType) -> Stream { + Stream(executor: self, params: params) { executor, params in + executor.paginatedSequenceWithViewContext(params: params) + } + } + + func paginatedEmbedStream(params: Self.EmbedContextResponseType.ParamsType) -> Stream { + Stream(executor: self, params: params) { executor, params in + executor.paginatedSequenceWithEmbedContext(params: params) + } + } +} + +public class Stream { + private let publisher: CurrentValueSubject<[ResponseType.DataType], Error> + private let executor: Executor + private let params: ResponseType.ParamsType + + var accumulatedObjects: [ResponseType.DataType] = [] + + public typealias StreamProvider = (Executor, ResponseType.ParamsType) -> PaginationSequence + + var streamProvider: StreamProvider + + public init(executor: Executor, params: ResponseType.ParamsType, streamProvider: @escaping StreamProvider) { + self.publisher = CurrentValueSubject([]) + self.executor = executor + self.params = params + self.streamProvider = streamProvider + } + + public func getPublisher() -> AnyPublisher<[ResponseType.DataType], Error> { + publisher.eraseToAnyPublisher() + } + + public func fetch() async throws { + self.accumulatedObjects.append(contentsOf: []) + self.publisher.send(self.accumulatedObjects) + + do { + for try await objects in streamProvider(executor, params) { + accumulatedObjects.append(contentsOf: objects) + publisher.send(accumulatedObjects) + } + } catch { + publisher.send(completion: .failure(error)) + } + } +} From 1aa377f363b380fa699100d30dc1e8f4f4170f7d Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:49:59 -0700 Subject: [PATCH 04/10] Clean up API naming --- native/swift/Example/Example/ExampleApp.swift | 11 +++-- .../swift/Example/Example/ListViewModel.swift | 16 +++--- .../wordpress-api-combine/Stream.swift | 49 +++++++++---------- .../Sources/wordpress-api/Pagination.swift | 12 ++--- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index 8b0699298..26716d0cd 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -3,6 +3,9 @@ import WordPressAPI import WordPressAPICombine import Combine +private let userListParams = UserListParams(perPage: 5) +private let postListParams = PostListParams(perPage: 5) + @main struct ExampleApp: App { @@ -16,7 +19,7 @@ struct ExampleApp: App { .map { $0.asListViewData } }), RootListData(name: "Users", callback: { - try await WordPressAPI.globalInstance.users.paginatedWithEditContext(params: UserListParams(perPage: 100)) + try await WordPressAPI.globalInstance.users.paginatedWithEditContext(params: userListParams) .map { $0.asListViewData } }), RootListData(name: "Plugins", callback: { @@ -30,7 +33,7 @@ struct ExampleApp: App { } }), RootListData(name: "Posts with Combine", stream: { - let stream = try WordPressAPI.globalInstance.posts.paginatedStream(params: PostListParams(perPage: 5)) + let stream = try WordPressAPI.globalInstance.posts.streamWithEditContext(params: postListParams) return ListViewDataStream( publisher: stream.getPublisher().map { $0.asListViewData() }.eraseToAnyPublisher(), @@ -39,9 +42,7 @@ struct ExampleApp: App { }), RootListData(name: "Posts with AsyncSequence", sequence: { do { - let sequence = try WordPressAPI.globalInstance - .posts - .paginatedSequenceWithEditContext(params: PostListParams(perPage: 5)) + let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams) return ListViewSequence(underlyingSequence: sequence) } catch { abort() diff --git a/native/swift/Example/Example/ListViewModel.swift b/native/swift/Example/Example/ListViewModel.swift index ec2216cfd..4b57154bf 100644 --- a/native/swift/Example/Example/ListViewModel.swift +++ b/native/swift/Example/Example/ListViewModel.swift @@ -97,17 +97,17 @@ protocol ListViewModel { var cancellables: Set = [] private let streamProvider: StreamProvider - private var currentStream: ListViewDataStream? init(streamProvider: @escaping StreamProvider) { self.streamProvider = streamProvider - self.currentStream = nil } func task() async { self.error = nil - self.currentStream = try? self.streamProvider() - self.currentStream?.getPublisher() + + guard var currentStream = try? self.streamProvider() else { return } + + currentStream.getPublisher() .sink { completion in switch completion { case .finished: @@ -126,7 +126,7 @@ protocol ListViewModel { } .store(in: &cancellables) - try? await self.currentStream?.fetch() + try? await currentStream.fetch() } } @@ -146,13 +146,13 @@ struct MyError: LocalizedError { } } -struct ListViewDataStream: WordPressAPICombine.Stream { +struct ListViewDataStream { typealias ValueType = [ListViewData] let publisher: AnyPublisher<[ListViewData], Error> - let underlyingStream: any WordPressAPICombine.Stream + var underlyingStream: WordPressAPICombine.Fetchable - func fetch() async throws { + mutating func fetch() async throws { try await self.underlyingStream.fetch() } diff --git a/native/swift/Sources/wordpress-api-combine/Stream.swift b/native/swift/Sources/wordpress-api-combine/Stream.swift index 7ab75173a..581696b37 100644 --- a/native/swift/Sources/wordpress-api-combine/Stream.swift +++ b/native/swift/Sources/wordpress-api-combine/Stream.swift @@ -2,53 +2,52 @@ import Combine import WordPressAPI public extension PaginationAwareExecutor { - func paginatedEditStream(params: Self.EditContextResponseType.ParamsType) -> Stream { - Stream(executor: self, params: params) { executor, params in - executor.paginatedSequenceWithEditContext(params: params) - } + func streamWithEditContext( + params: Self.EditContextResponseType.ParamsType + ) -> Stream { + Stream(executor: self, params: params, sequence: self.sequenceWithEditContext(params: params) ) } - func paginatedViewStream(params: Self.ViewContextResponseType.ParamsType) -> Stream { - Stream(executor: self, params: params) { executor, params in - executor.paginatedSequenceWithViewContext(params: params) - } + func streamWithViewContext( + params: Self.ViewContextResponseType.ParamsType + ) -> Stream { + Stream(executor: self, params: params, sequence: self.sequenceWithViewContext(params: params) ) } - func paginatedEmbedStream(params: Self.EmbedContextResponseType.ParamsType) -> Stream { - Stream(executor: self, params: params) { executor, params in - executor.paginatedSequenceWithEmbedContext(params: params) - } + func streamWithEmbedContext( + params: Self.EmbedContextResponseType.ParamsType + ) -> Stream { + Stream(executor: self, params: params, sequence: self.sequenceWithEmbedContext(params: params) ) } } -public class Stream { - private let publisher: CurrentValueSubject<[ResponseType.DataType], Error> +public protocol Fetchable { + mutating func fetch() async throws +} + +public struct Stream: Fetchable { + private let publisher = CurrentValueSubject<[ResponseType.DataType], Error>([]) private let executor: Executor private let params: ResponseType.ParamsType + private let sequence: PaginationSequence - var accumulatedObjects: [ResponseType.DataType] = [] - - public typealias StreamProvider = (Executor, ResponseType.ParamsType) -> PaginationSequence - - var streamProvider: StreamProvider + private var accumulatedObjects: [ResponseType.DataType] = [] - public init(executor: Executor, params: ResponseType.ParamsType, streamProvider: @escaping StreamProvider) { - self.publisher = CurrentValueSubject([]) + public init(executor: Executor, params: ResponseType.ParamsType, sequence: PaginationSequence) { self.executor = executor self.params = params - self.streamProvider = streamProvider + self.sequence = sequence } public func getPublisher() -> AnyPublisher<[ResponseType.DataType], Error> { publisher.eraseToAnyPublisher() } - public func fetch() async throws { - self.accumulatedObjects.append(contentsOf: []) + public mutating func fetch() async throws { self.publisher.send(self.accumulatedObjects) do { - for try await objects in streamProvider(executor, params) { + for try await objects in sequence { accumulatedObjects.append(contentsOf: objects) publisher.send(accumulatedObjects) } diff --git a/native/swift/Sources/wordpress-api/Pagination.swift b/native/swift/Sources/wordpress-api/Pagination.swift index d0f4ee594..5993328cd 100644 --- a/native/swift/Sources/wordpress-api/Pagination.swift +++ b/native/swift/Sources/wordpress-api/Pagination.swift @@ -35,15 +35,15 @@ public protocol PaginationAwareExecutor { params: EmbedContextResponseType.ParamsType ) async throws -> [EmbedContextResponseType.DataType] - func paginatedSequenceWithEditContext( + func sequenceWithEditContext( params: EditContextResponseType.ParamsType ) -> PaginationSequence - func paginatedSequenceWithViewContext( + func sequenceWithViewContext( params: ViewContextResponseType.ParamsType ) -> PaginationSequence - func paginatedSequenceWithEmbedContext( + func sequenceWithEmbedContext( params: EmbedContextResponseType.ParamsType ) -> PaginationSequence } @@ -121,7 +121,7 @@ extension PaginationAwareExecutor { return allObjects } - public func paginatedSequenceWithEditContext( + public func sequenceWithEditContext( params: EditContextResponseType.ParamsType ) -> PaginationSequence { PaginationSequence(params: params) { params in @@ -129,7 +129,7 @@ extension PaginationAwareExecutor { } } - public func paginatedSequenceWithViewContext( + public func sequenceWithViewContext( params: ViewContextResponseType.ParamsType ) -> PaginationSequence { PaginationSequence(params: params) { params in @@ -137,7 +137,7 @@ extension PaginationAwareExecutor { } } - public func paginatedSequenceWithEmbedContext( + public func sequenceWithEmbedContext( params: EmbedContextResponseType.ParamsType ) -> PaginationSequence { PaginationSequence(params: params) { params in From c9e793006074b57c2829f5deb499159a1d989a64 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:05:02 -0700 Subject: [PATCH 05/10] Allow SequenceProvider to wire errors to the UI --- native/swift/Example/Example/ExampleApp.swift | 8 ++------ native/swift/Example/Example/ListViewModel.swift | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index 26716d0cd..89aa95a14 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -41,12 +41,8 @@ struct ExampleApp: App { ) }), RootListData(name: "Posts with AsyncSequence", sequence: { - do { - let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams) - return ListViewSequence(underlyingSequence: sequence) - } catch { - abort() - } + let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams) + return ListViewSequence(underlyingSequence: sequence) }), RootListData(name: "Site Health Tests", callback: { let items: [any ListViewDataConvertable] = [ diff --git a/native/swift/Example/Example/ListViewModel.swift b/native/swift/Example/Example/ListViewModel.swift index 4b57154bf..edcb7ce7c 100644 --- a/native/swift/Example/Example/ListViewModel.swift +++ b/native/swift/Example/Example/ListViewModel.swift @@ -20,7 +20,7 @@ protocol ListViewModel { @Observable class SequenceListViewModel: ListViewModel { var listItems: [String: ListViewData] = [String: ListViewData](minimumCapacity: 250) - typealias SequenceProvider = () -> ListViewSequence + typealias SequenceProvider = () throws -> ListViewSequence private let sequenceProvider: SequenceProvider @@ -36,7 +36,7 @@ protocol ListViewModel { func task() async { do { - for try await page in self.sequenceProvider() { + for try await page in try self.sequenceProvider() { for item in page { self.listItems[item.id] = item } From ba6ba903bc897dac6b89ed3e353bce2ee94960b9 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:38:47 -0700 Subject: [PATCH 06/10] Remove Combine Support (for now) --- Package.resolved | 2 +- Package.swift | 15 ----- .../wordpress-api-combine/Stream.swift | 58 ------------------- scripts/xcodebuild-test.sh | 2 +- 4 files changed, 2 insertions(+), 75 deletions(-) delete mode 100644 native/swift/Sources/wordpress-api-combine/Stream.swift diff --git a/Package.resolved b/Package.resolved index 11857128b..16067b181 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "0385aaff7e0d6e30186b9f42ad615ad3dbcb283b4464c0202bebb483e0b4058f", + "originHash" : "9aafa5656d7a6c49905ab42cb52d23fe8b7cefe1a04a14e537053255e511790a", "pins" : [ { "identity" : "collectionconcurrencykit", diff --git a/Package.swift b/Package.swift index ace2266a7..fe58a6f88 100644 --- a/Package.swift +++ b/Package.swift @@ -26,10 +26,6 @@ var package = Package( .library( name: "WordPressAPI", targets: ["WordPressAPI"] - ), - .library( - name: "WordPressAPI+Combine", - targets: ["WordPressAPICombine"] ) ], dependencies: [], @@ -44,17 +40,6 @@ var package = Package( .enableExperimentalFeature("StrictConcurrency"), ] ), - .target( - name: "WordPressAPICombine", - dependencies: [ - .target(name: "WordPressAPI") - ], - path: "native/swift/Sources/wordpress-api-combine", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency"), - ] - ), - .target( name: "WordPressAPIInternal", dependencies: [ diff --git a/native/swift/Sources/wordpress-api-combine/Stream.swift b/native/swift/Sources/wordpress-api-combine/Stream.swift deleted file mode 100644 index 581696b37..000000000 --- a/native/swift/Sources/wordpress-api-combine/Stream.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Combine -import WordPressAPI - -public extension PaginationAwareExecutor { - func streamWithEditContext( - params: Self.EditContextResponseType.ParamsType - ) -> Stream { - Stream(executor: self, params: params, sequence: self.sequenceWithEditContext(params: params) ) - } - - func streamWithViewContext( - params: Self.ViewContextResponseType.ParamsType - ) -> Stream { - Stream(executor: self, params: params, sequence: self.sequenceWithViewContext(params: params) ) - } - - func streamWithEmbedContext( - params: Self.EmbedContextResponseType.ParamsType - ) -> Stream { - Stream(executor: self, params: params, sequence: self.sequenceWithEmbedContext(params: params) ) - } -} - -public protocol Fetchable { - mutating func fetch() async throws -} - -public struct Stream: Fetchable { - private let publisher = CurrentValueSubject<[ResponseType.DataType], Error>([]) - private let executor: Executor - private let params: ResponseType.ParamsType - private let sequence: PaginationSequence - - private var accumulatedObjects: [ResponseType.DataType] = [] - - public init(executor: Executor, params: ResponseType.ParamsType, sequence: PaginationSequence) { - self.executor = executor - self.params = params - self.sequence = sequence - } - - public func getPublisher() -> AnyPublisher<[ResponseType.DataType], Error> { - publisher.eraseToAnyPublisher() - } - - public mutating func fetch() async throws { - self.publisher.send(self.accumulatedObjects) - - do { - for try await objects in sequence { - accumulatedObjects.append(contentsOf: objects) - publisher.send(accumulatedObjects) - } - } catch { - publisher.send(completion: .failure(error)) - } - } -} diff --git a/scripts/xcodebuild-test.sh b/scripts/xcodebuild-test.sh index b48af587f..1afeb8d5f 100755 --- a/scripts/xcodebuild-test.sh +++ b/scripts/xcodebuild-test.sh @@ -11,7 +11,7 @@ device_id=$(xcrun simctl list --json devices available | jq -re ".devices.\"com. export NSUnbufferedIO=YES xcodebuild \ - -scheme WordPressAPI-Package \ + -scheme WordPressAPI \ -derivedDataPath DerivedData \ -destination "id=${device_id}" \ -skipPackagePluginValidation \ From d597868ae19fdffc4ba6dc280189c49ca906a4ee Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:49:32 -0700 Subject: [PATCH 07/10] Reduce `PaginationSequence` init visibility --- native/swift/Sources/wordpress-api/Pagination.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/swift/Sources/wordpress-api/Pagination.swift b/native/swift/Sources/wordpress-api/Pagination.swift index 5993328cd..a7b6dec5c 100644 --- a/native/swift/Sources/wordpress-api/Pagination.swift +++ b/native/swift/Sources/wordpress-api/Pagination.swift @@ -152,7 +152,7 @@ public struct PaginationSequence: AsyncSequen private let params: ResponseType.ParamsType private let transform: Transformer - public init(params: ResponseType.ParamsType, transform: @escaping Transformer) { + init(params: ResponseType.ParamsType, transform: @escaping Transformer) { self.params = params self.transform = transform } From 71ebf1a7daf0e4b5f6d963b451fc9b30dddc2653 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:04:27 -0700 Subject: [PATCH 08/10] Remove Combine from Example App --- .../Example/Example.xcodeproj/project.pbxproj | 18 +++--- native/swift/Example/Example/ExampleApp.swift | 9 --- .../swift/Example/Example/ListViewModel.swift | 63 ------------------- .../Example/Example/UI/RootListView.swift | 18 ------ 4 files changed, 9 insertions(+), 99 deletions(-) diff --git a/native/swift/Example/Example.xcodeproj/project.pbxproj b/native/swift/Example/Example.xcodeproj/project.pbxproj index c5c1b4fa7..27cda073e 100644 --- a/native/swift/Example/Example.xcodeproj/project.pbxproj +++ b/native/swift/Example/Example.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 242132C82CE69CE80021D8E8 /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 242132C72CE69CE80021D8E8 /* WordPressAPI */; }; 242D648E2C3602C1007CA96C /* ListViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D648D2C3602C1007CA96C /* ListViewData.swift */; }; 242D64922C360687007CA96C /* RootListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64912C360687007CA96C /* RootListView.swift */; }; 242D64942C3608C6007CA96C /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 242D64932C3608C6007CA96C /* ListView.swift */; }; @@ -19,7 +20,6 @@ 24A3C32F2BA8F96F00162AD1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C32E2BA8F96F00162AD1 /* LoginView.swift */; }; 24A3C3362BAA874C00162AD1 /* LoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A3C3352BAA874C00162AD1 /* LoginManager.swift */; }; 24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 24E77D022CE44DD900F6998C /* WordPressAPI */; }; - 24E77D052CE44DD900F6998C /* WordPressAPI+Combine in Frameworks */ = {isa = PBXBuildFile; productRef = 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -42,8 +42,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 24E77D052CE44DD900F6998C /* WordPressAPI+Combine in Frameworks */, 2479BF912B621CCA0014A01D /* WordPressAPI in Frameworks */, + 242132C82CE69CE80021D8E8 /* WordPressAPI in Frameworks */, 24E77D032CE44DD900F6998C /* WordPressAPI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -128,7 +128,7 @@ packageProductDependencies = ( 2479BF902B621CCA0014A01D /* WordPressAPI */, 24E77D022CE44DD900F6998C /* WordPressAPI */, - 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */, + 242132C72CE69CE80021D8E8 /* WordPressAPI */, ); productName = Example; productReference = 2479BF7D2B621CB60014A01D /* Example.app */; @@ -159,7 +159,7 @@ ); mainGroup = 2479BF742B621CB60014A01D; packageReferences = ( - 24E77D012CE44DD900F6998C /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */, + 242132C62CE69CE80021D8E8 /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */, ); productRefGroup = 2479BF7E2B621CB60014A01D /* Products */; projectDirPath = ""; @@ -415,24 +415,24 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 24E77D012CE44DD900F6998C /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */ = { + 242132C62CE69CE80021D8E8 /* XCLocalSwiftPackageReference "../../../../wordpress-rs" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../../../wordpress-rs"; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2479BF902B621CCA0014A01D /* WordPressAPI */ = { + 242132C72CE69CE80021D8E8 /* WordPressAPI */ = { isa = XCSwiftPackageProductDependency; productName = WordPressAPI; }; - 24E77D022CE44DD900F6998C /* WordPressAPI */ = { + 2479BF902B621CCA0014A01D /* WordPressAPI */ = { isa = XCSwiftPackageProductDependency; productName = WordPressAPI; }; - 24E77D042CE44DD900F6998C /* WordPressAPI+Combine */ = { + 24E77D022CE44DD900F6998C /* WordPressAPI */ = { isa = XCSwiftPackageProductDependency; - productName = "WordPressAPI+Combine"; + productName = WordPressAPI; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index 89aa95a14..ff4b2e3ab 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -1,6 +1,5 @@ import SwiftUI import WordPressAPI -import WordPressAPICombine import Combine private let userListParams = UserListParams(perPage: 5) @@ -32,14 +31,6 @@ struct ExampleApp: App { value.asListViewData } }), - RootListData(name: "Posts with Combine", stream: { - let stream = try WordPressAPI.globalInstance.posts.streamWithEditContext(params: postListParams) - - return ListViewDataStream( - publisher: stream.getPublisher().map { $0.asListViewData() }.eraseToAnyPublisher(), - underlyingStream: stream - ) - }), RootListData(name: "Posts with AsyncSequence", sequence: { let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams) return ListViewSequence(underlyingSequence: sequence) diff --git a/native/swift/Example/Example/ListViewModel.swift b/native/swift/Example/Example/ListViewModel.swift index edcb7ce7c..02e3d9db0 100644 --- a/native/swift/Example/Example/ListViewModel.swift +++ b/native/swift/Example/Example/ListViewModel.swift @@ -1,8 +1,6 @@ import Foundation import SwiftUI import WordPressAPI -import WordPressAPICombine -import Combine @MainActor protocol ListViewModel { @@ -84,52 +82,6 @@ protocol ListViewModel { } } -@Observable class CombineListViewModel: ListViewModel { - - public typealias StreamProvider = () throws -> ListViewDataStream - - var listItems: [String: ListViewData] = [:] - var isLoading: Bool = false - - var shouldPresentAlert: Bool = false - - var error: MyError? - var cancellables: Set = [] - - private let streamProvider: StreamProvider - - init(streamProvider: @escaping StreamProvider) { - self.streamProvider = streamProvider - } - - func task() async { - self.error = nil - - guard var currentStream = try? self.streamProvider() else { return } - - currentStream.getPublisher() - .sink { completion in - switch completion { - case .finished: - self.isLoading = false - - case .failure(let error): - self.error = MyError(underlyingError: error) - self.shouldPresentAlert = true - } - } receiveValue: { newValue in - withAnimation { - for item in newValue { - self.listItems[item.id] = item - } - } - } - .store(in: &cancellables) - - try? await currentStream.fetch() - } -} - struct MyError: LocalizedError { var underlyingError: Error @@ -146,21 +98,6 @@ struct MyError: LocalizedError { } } -struct ListViewDataStream { - typealias ValueType = [ListViewData] - - let publisher: AnyPublisher<[ListViewData], Error> - var underlyingStream: WordPressAPICombine.Fetchable - - mutating func fetch() async throws { - try await self.underlyingStream.fetch() - } - - func getPublisher() -> AnyPublisher<[ListViewData], any Error> { - publisher - } -} - struct ListViewSequence: AsyncSequence { typealias Element = [ListViewData] diff --git a/native/swift/Example/Example/UI/RootListView.swift b/native/swift/Example/Example/UI/RootListView.swift index 7d1d92831..8e079a831 100644 --- a/native/swift/Example/Example/UI/RootListView.swift +++ b/native/swift/Example/Example/UI/RootListView.swift @@ -1,6 +1,5 @@ import SwiftUI import WordPressAPI -import WordPressAPICombine import Combine struct RootListView: View { @@ -30,17 +29,6 @@ struct RootListViewItem: View { } } - case .combine(let name, let streamProvider): - VStack(alignment: .leading, spacing: 4.0) { - NavigationLink { - ListView( - viewModel: CombineListViewModel(streamProvider: streamProvider) - ) - } label: { - Text(name) - } - } - case .sequence(let name, let sequenceProvider): VStack(alignment: .leading, spacing: 4.0) { NavigationLink { @@ -58,13 +46,11 @@ struct RootListViewItem: View { enum RootListData: Identifiable { case callback(String, TaskListViewModel.FetchDataTask) - case combine(String, CombineListViewModel.StreamProvider) case sequence(String, SequenceListViewModel.SequenceProvider) var id: String { switch self { case .callback(let id, _): id - case .combine(let id, _): id case .sequence(let id, _): id } } @@ -73,10 +59,6 @@ enum RootListData: Identifiable { self = .callback(name, callback) } - init(name: String, stream: @escaping CombineListViewModel.StreamProvider) { - self = .combine(name, stream) - } - init(name: String, sequence: @escaping SequenceListViewModel.SequenceProvider) { self = .sequence(name, sequence) } From 47660ac19ab4af817acdf0f5dbdc50bb3970b898 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:05:43 -0700 Subject: [PATCH 09/10] Use paginated users in example app --- native/swift/Example/Example/ExampleApp.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/native/swift/Example/Example/ExampleApp.swift b/native/swift/Example/Example/ExampleApp.swift index ff4b2e3ab..7bc51d1ce 100644 --- a/native/swift/Example/Example/ExampleApp.swift +++ b/native/swift/Example/Example/ExampleApp.swift @@ -17,9 +17,9 @@ struct ExampleApp: App { .data .map { $0.asListViewData } }), - RootListData(name: "Users", callback: { - try await WordPressAPI.globalInstance.users.paginatedWithEditContext(params: userListParams) - .map { $0.asListViewData } + RootListData(name: "Users", sequence: { + let sequence = try WordPressAPI.globalInstance.users.sequenceWithEditContext(params: userListParams) + return ListViewSequence(underlyingSequence: sequence) }), RootListData(name: "Plugins", callback: { try await WordPressAPI.globalInstance.plugins.listWithEditContext(params: .init()) @@ -31,7 +31,7 @@ struct ExampleApp: App { value.asListViewData } }), - RootListData(name: "Posts with AsyncSequence", sequence: { + RootListData(name: "Posts", sequence: { let sequence = try WordPressAPI.globalInstance.posts.sequenceWithEditContext(params: postListParams) return ListViewSequence(underlyingSequence: sequence) }), From 2641f629087ae728920384b9f90397195a26a573 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:06:47 -0700 Subject: [PATCH 10/10] Lintfix --- native/swift/Tests/wordpress-api/SendableTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/swift/Tests/wordpress-api/SendableTests.swift b/native/swift/Tests/wordpress-api/SendableTests.swift index 7125c29f1..82c41eac1 100644 --- a/native/swift/Tests/wordpress-api/SendableTests.swift +++ b/native/swift/Tests/wordpress-api/SendableTests.swift @@ -7,10 +7,10 @@ struct SendableTests { PostsRequestListWithEditContextResponse.empty, PostsRequestListWithViewContextResponse.empty, PostsRequestListWithEmbedContextResponse.empty, - + UsersRequestListWithEditContextResponse.empty, UsersRequestListWithViewContextResponse.empty, - UsersRequestListWithEmbedContextResponse.empty, + UsersRequestListWithEmbedContextResponse.empty ] /// This might seem like a weird test – why are we checking such a specific implementation detail?