-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce @SharedReader #2979
Merged
Merged
Introduce @SharedReader #2979
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
36445a9
wip
stephencelis b7c74a1
wip
stephencelis 1116fdb
wip
stephencelis 9015a0b
wip
stephencelis da98f5d
wip
mbrandonw ea157f4
wip
stephencelis bb3bbfd
Introduce @SharedReader.
mbrandonw 0f54644
wip
mbrandonw 3bc8c51
Merge remote-tracking branch 'origin/shared-state-beta' into shared-s…
mbrandonw 6a1fc98
wip
mbrandonw 558387c
sendable
mbrandonw 64f9f85
wip
mbrandonw 6d6c49d
Merge remote-tracking branch 'origin/shared-state-beta' into shared-s…
mbrandonw d90939f
wip
mbrandonw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
Examples/CaseStudies/SwiftUICaseStudies/02-SharedState-Notifications.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import ComposableArchitecture | ||
import SwiftUI | ||
|
||
private let readMe = """ | ||
This application demonstrates how to use the `@SharedReader` tool to introduce a piece of \ | ||
read-only state to your feature whose true value lives in an external system. In this case, \ | ||
the state is the number of times a screenshot is taken, which is counted from the \ | ||
`userDidTakeScreenshotNotification` notification. | ||
|
||
Run this application in the simulator, and take a few screenshots by going to \ | ||
*Device › Trigger Screenshot* in the menu, and observe that the UI counts the number of times \ | ||
that happens. | ||
|
||
The `@SharedReader` state will update automatically when the screenshot notification is posted \ | ||
by the system, and further you can use the `.publisher` property on `@SharedReader` to listen \ | ||
for any changes to the data. | ||
""" | ||
|
||
@Reducer | ||
struct SharedStateNotifications { | ||
@ObservableState | ||
struct State: Equatable { | ||
var fact: String? | ||
@SharedReader(.screenshotCount) var screenshotCount = 0 | ||
} | ||
enum Action { | ||
case factResponse(Result<String, Error>) | ||
case onAppear | ||
} | ||
@Dependency(\.factClient) var factClient | ||
var body: some ReducerOf<Self> { | ||
Reduce { state, action in | ||
switch action { | ||
case let .factResponse(.success(fact)): | ||
state.fact = fact | ||
return .none | ||
|
||
case .factResponse(.failure): | ||
return .none | ||
|
||
case .onAppear: | ||
return .run { [screenshotCount = state.$screenshotCount] send in | ||
for await count in screenshotCount.publisher.values { | ||
await send(.factResponse(Result { try await factClient.fetch(count) })) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
struct SharedStateNotificationsView: View { | ||
let store: StoreOf<SharedStateNotifications> | ||
|
||
var body: some View { | ||
Form { | ||
Section { | ||
AboutView(readMe: readMe) | ||
} | ||
|
||
Text("A screenshot of this screen has been taken \(store.screenshotCount) times.") | ||
.font(.headline) | ||
|
||
if let fact = store.fact { | ||
Text("\(fact)") | ||
} | ||
} | ||
.navigationTitle("Long-living effects") | ||
.task { await store.send(.onAppear).finish() } | ||
} | ||
} | ||
|
||
extension PersistenceReaderKey where Self == NotificationReaderKey<Int> { | ||
static var screenshotCount: Self { | ||
NotificationReaderKey( | ||
initialValue: 0, | ||
name: MainActor.assumeIsolated { | ||
UIApplication.userDidTakeScreenshotNotification | ||
} | ||
) { value, _ in | ||
value += 1 | ||
} | ||
} | ||
} | ||
|
||
struct NotificationReaderKey<Value: Sendable>: PersistenceReaderKey { | ||
let name: Notification.Name | ||
private let transform: @Sendable (Notification) -> Value | ||
|
||
init( | ||
initialValue: Value, | ||
name: Notification.Name, | ||
transform: @Sendable @escaping (inout Value, Notification) -> Void | ||
) { | ||
self.name = name | ||
let value = LockIsolated(initialValue) | ||
self.transform = { notification in | ||
value.withValue { [notification = UncheckedSendable(notification)] in | ||
transform(&$0, notification.wrappedValue) | ||
} | ||
return value.value | ||
} | ||
} | ||
|
||
func load(initialValue: Value?) -> Value? { nil } | ||
|
||
func subscribe( | ||
initialValue: Value?, | ||
didSet: @Sendable @escaping (Value?) -> Void | ||
) -> Shared<Value>.Subscription { | ||
let token = NotificationCenter.default.addObserver( | ||
forName: name, | ||
object: nil, | ||
queue: nil, | ||
using: { notification in | ||
didSet(self.transform(notification)) | ||
} | ||
) | ||
return Shared.Subscription { | ||
NotificationCenter.default.removeObserver(token) | ||
} | ||
} | ||
|
||
static func == (lhs: Self, rhs: Self) -> Bool { | ||
lhs.name == rhs.name | ||
} | ||
func hash(into hasher: inout Hasher) { | ||
hasher.combine(name) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
Sources/ComposableArchitecture/Internal/DefaultSubscript.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
final class DefaultSubscript<Value>: Hashable { | ||
var value: Value | ||
init(_ value: Value) { | ||
self.value = value | ||
} | ||
static func == (lhs: DefaultSubscript, rhs: DefaultSubscript) -> Bool { | ||
lhs === rhs | ||
} | ||
func hash(into hasher: inout Hasher) { | ||
hasher.combine(ObjectIdentifier(self)) | ||
} | ||
} | ||
|
||
extension Optional { | ||
subscript(default defaultSubscript: DefaultSubscript<Wrapped>) -> Wrapped { | ||
get { self ?? defaultSubscript.value } | ||
set { | ||
defaultSubscript.value = newValue | ||
if self != nil { self = newValue } | ||
} | ||
} | ||
} | ||
|
||
extension RandomAccessCollection where Self: MutableCollection { | ||
subscript( | ||
position: Index, default defaultSubscript: DefaultSubscript<Element> | ||
) -> Element { | ||
get { self.indices.contains(position) ? self[position] : defaultSubscript.value } | ||
set { | ||
defaultSubscript.value = newValue | ||
if self.indices.contains(position) { self[position] = newValue } | ||
} | ||
} | ||
} |
38 changes: 21 additions & 17 deletions
38
Sources/ComposableArchitecture/SharedState/PersistenceKey.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import Dependencies | ||
import Foundation | ||
|
||
extension PersistenceKey { | ||
extension PersistenceReaderKey { | ||
/// Creates a persistence key that can read and write to a boolean user default. | ||
/// | ||
/// - Parameter key: The key to read and write the value to in the user defaults store. | ||
|
@@ -288,7 +288,8 @@ extension AppStorageKey: PersistenceKey { | |
} | ||
|
||
public func subscribe( | ||
initialValue: Value?, didSet: @escaping (_ newValue: Value?) -> Void | ||
initialValue: Value?, | ||
didSet: @Sendable @escaping (_ newValue: Value?) -> Void | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making |
||
) -> Shared<Value>.Subscription { | ||
let userDefaultsDidChange = NotificationCenter.default.addObserver( | ||
forName: UserDefaults.didChangeNotification, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think something like this could be moved into library code, but for now I've kept it in this case study.