Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Last active March 30, 2024 02:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukeredpath/2bf827a04a7d25b8482ff475c24ea5ec to your computer and use it in GitHub Desktop.
Save lukeredpath/2bf827a04a7d25b8482ff475c24ea5ec to your computer and use it in GitHub Desktop.
Demo of TCA shared state using default-providing key
//
// 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