Instantly share code, notes, and snippets.
Last active
March 30, 2024 02:30
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save lukeredpath/2bf827a04a7d25b8482ff475c24ea5ec to your computer and use it in GitHub Desktop.
Demo of TCA shared state using default-providing key
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
// | |
// SharedStateDemoApp.swift | |
// SharedStateDemo | |
// | |
// Created by Luke Redpath on 29/03/2024. | |
// | |
import ComposableArchitecture | |
import SwiftUI | |
// MARK: - Models | |
struct User: Equatable, Hashable, Sendable, Codable { | |
let id: UUID | |
let name: String | |
} | |
extension User { | |
static var placeholder = User(id: .init(0), name: "") | |
static let joe = User(id: .init(1), name: "Joe") | |
static let bob = User(id: .init(2), name: "Bob") | |
} | |
// MARK: - Reducers | |
@Reducer | |
struct AppFeature: Reducer { | |
@ObservableState | |
struct State: Equatable { | |
@Shared(.user) | |
var user | |
var session = Session.State.loggedOut(.init()) | |
} | |
enum Action { | |
case session(Session.Action) | |
} | |
var body: some ReducerOf<Self> { | |
Scope(state: \.session, action: \.session) { | |
Session.body | |
} | |
Reduce<State, Action> { state, action in | |
switch action { | |
case .session(.loggedOut(.loginButtonTapped(let user))): | |
state.session = .loggedIn(.init(user: user)) | |
return .none | |
case .session(.loggedIn(.logoutButtonTapped)): | |
// If we want to reset our shared user state as soon as a | |
// user logs out, we can do that here. | |
// state.user = .placeholder | |
state.session = .loggedOut(.init()) | |
return .none | |
case .session: | |
return .none | |
} | |
} | |
} | |
@Reducer(state: .equatable) | |
enum Session { | |
case loggedIn(LoggedIn) | |
case loggedOut(LoggedOut) | |
} | |
} | |
@Reducer | |
struct LoggedIn: Reducer { | |
@ObservableState | |
struct State: Equatable { | |
@Shared(.user) | |
var user: User | |
@Presents | |
var someLoggedInFeature: SomeLoggedInFeature.State? | |
init(user: User) { | |
// We want to update the shared user every time a new | |
// user logs in to the app. | |
self.user = user | |
} | |
} | |
enum Action { | |
case openSomeFeatureButtonTapped | |
case logoutButtonTapped | |
case someLoggedInFeature(PresentationAction<SomeLoggedInFeature.Action>) | |
} | |
var body: some ReducerOf<Self> { | |
Reduce<State, Action> { state, action in | |
switch action { | |
case .openSomeFeatureButtonTapped: | |
state.someLoggedInFeature = .init(user: state.$user) | |
return .none | |
case .logoutButtonTapped: | |
return .none | |
case .someLoggedInFeature: | |
return .none | |
} | |
} | |
.ifLet(\.$someLoggedInFeature, action: \.someLoggedInFeature) { | |
SomeLoggedInFeature() | |
} | |
} | |
} | |
@Reducer | |
struct LoggedOut: Reducer { | |
@ObservableState | |
struct State: Equatable { | |
@Shared(.user) | |
var user | |
} | |
enum Action { | |
case loginButtonTapped(user: User) | |
} | |
} | |
@Reducer | |
struct SomeLoggedInFeature: Reducer { | |
@ObservableState | |
struct State: Equatable { | |
@Shared(.user) | |
var user: User | |
} | |
} | |
// MARK: - Persistence | |
extension PersistenceKey where Self == DefaultProvidingKey<InMemoryKey<User>> { | |
static var user: Self { | |
inMemory("user", defaultValue: .placeholder) | |
} | |
} | |
// MARK: - App and Views | |
@main | |
struct SharedStateDemoApp: App { | |
@Bindable | |
var store = StoreOf<AppFeature>(initialState: AppFeature.State()) { | |
AppFeature()._printChanges() | |
} | |
var body: some Scene { | |
WindowGroup { | |
switch store.scope(state: \.session, action: \.session).case { | |
case let .loggedIn(store): | |
LoggedInView(store: store) | |
case let .loggedOut(store): | |
LoggedOutView(store: store) | |
} | |
} | |
} | |
} | |
struct LoggedInView: View { | |
@Bindable | |
var store: StoreOf<LoggedIn> | |
var body: some View { | |
NavigationStack { | |
List { | |
Section { | |
Text("Logged in as \(store.user.name)") | |
} | |
Section { | |
Button("Open some feature") { | |
store.send(.openSomeFeatureButtonTapped) | |
} | |
} | |
Section { | |
Button("Log Out") { | |
store.send(.logoutButtonTapped) | |
} | |
.foregroundStyle(.red) | |
} | |
} | |
.navigationTitle("Logged In") | |
.navigationBarTitleDisplayMode(.inline) | |
.sheet(item: $store.scope(state: \.someLoggedInFeature, action: \.someLoggedInFeature)) { | |
SomeFeatureView(store: $0) | |
} | |
} | |
} | |
} | |
struct LoggedOutView: View { | |
let store: StoreOf<LoggedOut> | |
var body: some View { | |
NavigationStack { | |
List { | |
Section { | |
Button("Login as Joe") { | |
store.send(.loginButtonTapped(user: .joe)) | |
} | |
Button("Login as Bob") { | |
store.send(.loginButtonTapped(user: .bob)) | |
} | |
} | |
} | |
.navigationTitle("Logged Out") | |
.navigationBarTitleDisplayMode(.inline) | |
} | |
} | |
} | |
struct SomeFeatureView: View { | |
let store: StoreOf<SomeLoggedInFeature> | |
@Environment(\.dismiss) | |
private var dismiss | |
var body: some View { | |
NavigationStack { | |
Text("Hello \(store.user.name)") | |
.navigationTitle("Some Feature") | |
.navigationBarTitleDisplayMode(.inline) | |
.toolbar { | |
ToolbarItem(placement: .primaryAction) { | |
Button("Done") { dismiss() } | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment