diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 758a091f6883..3b546f96e8f0 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -40,7 +40,9 @@ struct RootView: View { AlertAndConfirmationDialog() } ) { store in - AlertAndConfirmationDialogView(store: store) + UIViewControllerRepresenting { + AlertAndConfirmationDialogViewController(store: store) + } } } NavigationLink("Focus State") { diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift index 3814d141b1f3..0b2246a60a04 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift @@ -101,6 +101,67 @@ struct AlertAndConfirmationDialog { } } +import UIKit +final class AlertAndConfirmationDialogViewController: UIViewController { + @UIBindable var store: StoreOf + + init(store: StoreOf) { + self.store = store + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let countLabel = UILabel() + + let alertButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.store.send(.alertButtonTapped) + }) + alertButton.setTitle("Alert", for: .normal) + + let confirmationDialogButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.store.send(.confirmationDialogButtonTapped) + }) + confirmationDialogButton.setTitle("Confirmation Dialog", for: .normal) + + let stack = UIStackView(arrangedSubviews: [ + countLabel, + alertButton, + confirmationDialogButton, + ]) + stack.axis = .vertical + stack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: view.topAnchor), + stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), + stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + countLabel.text = "Count: \(store.count)" + } + + present(item: $store.scope(state: \.alert, action: \.alert)) { store in + UIAlertController(store: store) + } + present( + item: $store.scope(state: \.confirmationDialog, action: \.confirmationDialog) + ) { store in + UIAlertController(store: store) + } + } +} + struct AlertAndConfirmationDialogView: View { @Bindable var store: StoreOf diff --git a/Package.swift b/Package.swift index a9be7e1f27d7..50c9e3db2461 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.2"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.4"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.2.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.4.0"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 4bb1790a82e4..e77090b98e7c 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.2"), .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.5.4"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.2.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.4.0"), .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"), diff --git a/Sources/ComposableArchitecture/Observation/Store+Observation.swift b/Sources/ComposableArchitecture/Observation/Store+Observation.swift index 078a70c59fc4..935e99b193f9 100644 --- a/Sources/ComposableArchitecture/Observation/Store+Observation.swift +++ b/Sources/ComposableArchitecture/Observation/Store+Observation.swift @@ -36,7 +36,24 @@ extension Store: Hashable { } } -extension Store: Identifiable {} +extension Store: Identifiable { + public struct ID: Hashable { + fileprivate let objectIdentifier: ObjectIdentifier + fileprivate let stateIdentifier: AnyHashableSendable? + } + + public nonisolated var id: ID { + ID( + objectIdentifier: ObjectIdentifier(self), + stateIdentifier: Thread.isMainThread + ? MainActor._assumeIsolated { + ((currentState as? any Identifiable)?.id as? any Hashable) + .map(AnyHashableSendable.init) + } + : nil + ) + } +} extension Store where State: ObservableState { /// Scopes the store to optional child state and actions. @@ -454,7 +471,7 @@ extension Store where State: ObservableState { set { if newValue == nil, let childState = self.state[keyPath: state], - id == _identifiableID(childState), + id == nil || id == _identifiableID(childState), !self.core.isInvalid { self.send(action(.dismiss))