mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
* wip * wip * remove old case study for shared notifications * wip * wip * wip * wip * wip * 1.17 migration guide * basics migration guide * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip --------- Co-authored-by: Brandon Williams <mbrandonw@hey.com>
223 lines
5.4 KiB
Swift
223 lines
5.4 KiB
Swift
import ComposableArchitecture
|
|
import SwiftUI
|
|
|
|
private let readMe = """
|
|
This screen demonstrates how multiple independent screens can share state in the Composable \
|
|
Architecture through user defaults (i.e. "app storage"). Each tab manages its own state, and \
|
|
could be in separate modules, but changes in one tab are immediately reflected in the other, and \
|
|
all changes are persisted to use defaults.
|
|
|
|
This tab has its own state, consisting of a count value that can be incremented and decremented, \
|
|
as well as an alert value that is set when asking if the current count is prime.
|
|
"""
|
|
|
|
@Reducer
|
|
struct SharedStateUserDefaults {
|
|
enum Tab { case counter, profile }
|
|
|
|
@ObservableState
|
|
struct State: Equatable {
|
|
var currentTab = Tab.counter
|
|
var counter = CounterTab.State()
|
|
var profile = ProfileTab.State()
|
|
}
|
|
|
|
enum Action {
|
|
case counter(CounterTab.Action)
|
|
case profile(ProfileTab.Action)
|
|
case selectTab(Tab)
|
|
}
|
|
|
|
var body: some Reducer<State, Action> {
|
|
Scope(state: \.counter, action: \.counter) {
|
|
CounterTab()
|
|
}
|
|
|
|
Scope(state: \.profile, action: \.profile) {
|
|
ProfileTab()
|
|
}
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .counter, .profile:
|
|
return .none
|
|
case let .selectTab(tab):
|
|
state.currentTab = tab
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SharedStateUserDefaultsView: View {
|
|
@Bindable var store: StoreOf<SharedStateUserDefaults>
|
|
|
|
var body: some View {
|
|
TabView(selection: $store.currentTab.sending(\.selectTab)) {
|
|
CounterTabView(
|
|
store: store.scope(state: \.counter, action: \.counter)
|
|
)
|
|
.tag(SharedStateUserDefaults.Tab.counter)
|
|
.tabItem { Text("Counter") }
|
|
|
|
ProfileTabView(
|
|
store: store.scope(state: \.profile, action: \.profile)
|
|
)
|
|
.tag(SharedStateUserDefaults.Tab.profile)
|
|
.tabItem { Text("Profile") }
|
|
}
|
|
.navigationTitle("Shared State Demo")
|
|
}
|
|
}
|
|
|
|
extension SharedStateUserDefaults {
|
|
@Reducer
|
|
struct CounterTab {
|
|
@ObservableState
|
|
struct State: Equatable {
|
|
@Presents var alert: AlertState<Action.Alert>?
|
|
@Shared(.count) var count = 0
|
|
}
|
|
|
|
enum Action {
|
|
case alert(PresentationAction<Alert>)
|
|
case decrementButtonTapped
|
|
case incrementButtonTapped
|
|
case isPrimeButtonTapped
|
|
|
|
enum Alert: Equatable {}
|
|
}
|
|
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .alert:
|
|
return .none
|
|
|
|
case .decrementButtonTapped:
|
|
state.$count.withLock { $0 -= 1 }
|
|
return .none
|
|
|
|
case .incrementButtonTapped:
|
|
state.$count.withLock { $0 += 1 }
|
|
return .none
|
|
|
|
case .isPrimeButtonTapped:
|
|
state.alert = AlertState {
|
|
TextState(
|
|
isPrime(state.count)
|
|
? "👍 The number \(state.count) is prime!"
|
|
: "👎 The number \(state.count) is not prime :("
|
|
)
|
|
}
|
|
return .none
|
|
}
|
|
}
|
|
.ifLet(\.$alert, action: \.alert)
|
|
}
|
|
}
|
|
|
|
@Reducer
|
|
struct ProfileTab {
|
|
@ObservableState
|
|
struct State: Equatable {
|
|
@Shared(.count) var count = 0
|
|
}
|
|
|
|
enum Action {
|
|
case resetStatsButtonTapped
|
|
}
|
|
|
|
var body: some Reducer<State, Action> {
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .resetStatsButtonTapped:
|
|
state.$count.withLock { $0 = 0 }
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct CounterTabView: View {
|
|
@Bindable var store: StoreOf<SharedStateUserDefaults.CounterTab>
|
|
|
|
var body: some View {
|
|
Form {
|
|
Text(template: readMe, .caption)
|
|
|
|
VStack(spacing: 16) {
|
|
HStack {
|
|
Button {
|
|
store.send(.decrementButtonTapped)
|
|
} label: {
|
|
Image(systemName: "minus")
|
|
}
|
|
|
|
Text("\(store.count)")
|
|
.monospacedDigit()
|
|
|
|
Button {
|
|
store.send(.incrementButtonTapped)
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
|
|
Button("Is this prime?") { store.send(.isPrimeButtonTapped) }
|
|
}
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.alert($store.scope(state: \.alert, action: \.alert))
|
|
}
|
|
}
|
|
|
|
private struct ProfileTabView: View {
|
|
let store: StoreOf<SharedStateUserDefaults.ProfileTab>
|
|
|
|
var body: some View {
|
|
Form {
|
|
Text(
|
|
template: """
|
|
This tab shows the count from the previous tab, and it is capable of resetting the count \
|
|
back to 0.
|
|
|
|
This shows that it is possible for each screen to model its state in the way that makes \
|
|
the most sense for it, while still allowing the state and mutations to be shared \
|
|
across independent screens.
|
|
""",
|
|
.caption
|
|
)
|
|
|
|
VStack(spacing: 16) {
|
|
Text("Current count: \(store.count)")
|
|
Button("Reset") { store.send(.resetStatsButtonTapped) }
|
|
}
|
|
}
|
|
.buttonStyle(.borderless)
|
|
}
|
|
}
|
|
|
|
extension SharedKey where Self == AppStorageKey<Int> {
|
|
fileprivate static var count: Self {
|
|
appStorage("sharedStateDemoCount")
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SharedStateUserDefaultsView(
|
|
store: Store(initialState: SharedStateUserDefaults.State()) { SharedStateUserDefaults() }
|
|
)
|
|
}
|
|
|
|
/// Checks if a number is prime or not.
|
|
private func isPrime(_ p: Int) -> Bool {
|
|
if p <= 1 { return false }
|
|
if p <= 3 { return true }
|
|
for i in 2...Int(sqrtf(Float(p))) {
|
|
if p % i == 0 { return false }
|
|
}
|
|
return true
|
|
}
|