From 129d2ab1b0b0fd73756c5e0cfc27ea52a5e0ead5 Mon Sep 17 00:00:00 2001 From: Ken Ackerson Date: Thu, 27 Mar 2025 11:05:55 -0400 Subject: [PATCH 1/2] Moving Store off main actor --- .../MockBannerNetworkStateController.swift | 13 +++++-------- Sources/Store/MainQueueScheduler.swift | 3 +-- Sources/Store/Store.swift | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Example/Photos/With Stores/Banner Feature/MockBannerNetworkStateController.swift b/Example/Photos/With Stores/Banner Feature/MockBannerNetworkStateController.swift index 32d0fcc..5be1a22 100644 --- a/Example/Photos/With Stores/Banner Feature/MockBannerNetworkStateController.swift +++ b/Example/Photos/With Stores/Banner Feature/MockBannerNetworkStateController.swift @@ -6,10 +6,10 @@ // import Foundation -import Combine +@preconcurrency import Combine /// A really contrived fake interface similar to networking state controller for updating `Banner` models on a nonexistent server. -final class MockBannerNetworkStateController { +final class MockBannerNetworkStateController: Sendable { /// Represents the state of a network request for a banner. enum NetworkState { @@ -67,22 +67,19 @@ final class MockBannerNetworkStateController { } /// A publisher that sends updates of the `NetworkState`. - public var publisher: PassthroughSubject = .init() + public let publisher: PassthroughSubject = .init() /// Uploads a `Banner` to a fake server. /// - Parameter banner: The `Banner` to upload. - @MainActor func upload(banner: Banner) { self.publisher.send(.inProgress) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // Pick whether you would like to get a successful (`.finished(.success...`) state or any error for this "network request". - //self.publisher.send(.finished(.success(banner))) + self.publisher.send(.finished(.success(banner))) - self.publisher.send(.finished(.failure(.intentionalFailure))) - } } diff --git a/Sources/Store/MainQueueScheduler.swift b/Sources/Store/MainQueueScheduler.swift index fa5675d..6f024db 100644 --- a/Sources/Store/MainQueueScheduler.swift +++ b/Sources/Store/MainQueueScheduler.swift @@ -11,8 +11,7 @@ import SwiftUI /// Scheduler that allows you to either have a `default` implementation of the `DispatchQueue.main` scheduler, a `synchronous` implementation that will immediately call back, a `test` scheduler for fine grained control of time, and an `animated` scheduler to drive animations. /// You will want to use this to avoid the behavior of `DispatchQueue.main`'s scheduler to schedule work asynchronously by default. -@MainActor -public final class MainQueueScheduler: @preconcurrency Scheduler { +public final class MainQueueScheduler: Scheduler { /// Describes the characteristics of the scheduler. public enum SchedulerType: Equatable { diff --git a/Sources/Store/Store.swift b/Sources/Store/Store.swift index 011bb8c..0c83896 100644 --- a/Sources/Store/Store.swift +++ b/Sources/Store/Store.swift @@ -9,7 +9,6 @@ import SwiftUI import Combine /// A store is an `ObservableObject` that allows us to separate business and/or view level logic and the rendering of views in a way that is repeatable, prescriptive, flexible, and testable by default. -@MainActor public protocol Store: ObservableObject { /// A container type for state associated with the corresponding domain. From 00724ca791354ddd69459cba1aabbaebf2b8d3e7 Mon Sep 17 00:00:00 2001 From: Ken Ackerson Date: Thu, 27 Mar 2025 11:14:51 -0400 Subject: [PATCH 2/2] Fixing warnings --- .../View Level/BannerUpdateViewStore.swift | 6 ++++-- Example/Photos/With Stores/PhotoListViewStore.swift | 3 +++ Sources/Store/Store+BindingAdditions.swift | 10 ++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Example/Photos/With Stores/Banner Feature/View Level/BannerUpdateViewStore.swift b/Example/Photos/With Stores/Banner Feature/View Level/BannerUpdateViewStore.swift index c02d243..50d4aa4 100644 --- a/Example/Photos/With Stores/Banner Feature/View Level/BannerUpdateViewStore.swift +++ b/Example/Photos/With Stores/Banner Feature/View Level/BannerUpdateViewStore.swift @@ -18,7 +18,7 @@ final class BannerUpdateViewStore: Store { // MARK: - Store /// Represents the state of the `BannerUpdateViewStore` - struct State { + struct State: Sendable { /// Stores the state of the nested `BannerDataStore` let bannerViewState: BannerDataStore.State @@ -51,7 +51,7 @@ final class BannerUpdateViewStore: Store { } } - enum Action { + enum Action: Sendable { /// Action to update the title of the banner with a given string case updateTitle(String) @@ -106,11 +106,13 @@ final class BannerUpdateViewStore: Store { extension BannerUpdateViewStoreType { /// Computed property that creates a binding for the working title + @MainActor var workingTitle: Binding { makeBinding(stateKeyPath: \.workingCopy.title, actionCasePath: /Action.updateTitle) } /// Computed property that creates a binding for the error presentation state + @MainActor var isErrorPresented: Binding { .init(get: { return self.state.error != nil diff --git a/Example/Photos/With Stores/PhotoListViewStore.swift b/Example/Photos/With Stores/PhotoListViewStore.swift index f50290f..6f2a199 100644 --- a/Example/Photos/With Stores/PhotoListViewStore.swift +++ b/Example/Photos/With Stores/PhotoListViewStore.swift @@ -165,11 +165,13 @@ extension PhotoListViewStoreType { } /// Computed property that creates a binding for the `showUpdateView` state + @MainActor var showUpdateView: Binding { makeBinding(stateKeyPath: \.showUpdateView, actionCasePath: /PhotoListViewStore.Action.showUpdateView) } /// Computed property that creates a binding for the `showsPhotoCount` state + @MainActor var showsPhotoCount: Binding { // // return Binding { @@ -183,6 +185,7 @@ extension PhotoListViewStoreType { } /// Computed property that creates a binding for the `searchText` state + @MainActor var searchText: Binding { makeBinding(stateKeyPath: \.searchText, actionCasePath: /Action.search) } diff --git a/Sources/Store/Store+BindingAdditions.swift b/Sources/Store/Store+BindingAdditions.swift index da05e15..6f8ada3 100644 --- a/Sources/Store/Store+BindingAdditions.swift +++ b/Sources/Store/Store+BindingAdditions.swift @@ -6,8 +6,8 @@ // import SwiftUI -import CasePaths -import Combine +@preconcurrency import CasePaths +@preconcurrency import Combine /// An extension on `Store` that provides conveniences for creating `Binding`s. public extension Store { @@ -16,6 +16,7 @@ public extension Store { /// - Parameters: /// - stateKeyPath: The `KeyPath` to the `State` property that this binding wraps. /// - actionCasePath: The `CasePath` to the `Action` case associated with the `State` property. + @MainActor func makeBinding(stateKeyPath: KeyPath, actionCasePath: CasePath) -> Binding { return .init { self.state[keyPath: stateKeyPath] @@ -28,6 +29,7 @@ public extension Store { /// - Parameters: /// - stateKeyPath: The `KeyPath` to the `State` property that this binding wraps. /// - actionCasePath: The `CasePath` to the `Action` case associated with the `State` property. + @MainActor func makeBinding(stateKeyPath: KeyPath, actionCasePath: CasePath) -> Binding { return .init { self.state[keyPath: stateKeyPath] @@ -40,6 +42,7 @@ public extension Store { /// - Parameters: /// - stateKeyPath: The `KeyPath` to the optional `State` property whose existence determines the wrapped value. /// - actionCasePath: The `CasePath` to the `Action` case associated with the `State` property. + @MainActor func makeBinding(stateKeyPath: KeyPath, actionCasePath: CasePath) -> Binding { return .init { self.state[keyPath: stateKeyPath] != nil @@ -52,6 +55,7 @@ public extension Store { /// - Parameters: /// - stateKeyPath: The `KeyPath` to the optional `State` property whose existence determines the wrapped value. /// - actionCasePath: The `CasePath` to the `Action` case associated with the `State` property. + @MainActor func makeBinding(stateKeyPath: KeyPath, actionCasePath: CasePath) -> Binding { return .init { self.state[keyPath: stateKeyPath] != nil @@ -68,6 +72,7 @@ public extension Store { /// - Parameters: /// - stateKeyPath: The `KeyPath` to the `State` property that this binding wraps. /// - publisher: The publisher to send the value to. + @MainActor func makeBinding(stateKeyPath: KeyPath, publisher: PassthroughSubject) -> Binding { return .init { self.state[keyPath: stateKeyPath] @@ -80,6 +85,7 @@ public extension Store { /// - Parameters: /// - StateKeyPath: The `KeyPath` to the `State` property that this binding wraps. /// - publisher: The publisher to send the value to. + @MainActor func makeBinding(stateKeyPath: KeyPath, publisher: CurrentValueSubject) -> Binding { return .init { self.state[keyPath: stateKeyPath]