Skip to content

Instantly share code, notes, and snippets.

@nikitaame
Last active June 5, 2022 06:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nikitaame/c6629fe71e5ae12760aaf17fa146b994 to your computer and use it in GitHub Desktop.
Save nikitaame/c6629fe71e5ae12760aaf17fa146b994 to your computer and use it in GitHub Desktop.
TCA Circular Reference
import ComposableArchitecture
import SwiftUI
@main
struct TCARecursionApp: App {
var body: some Scene {
WindowGroup {
HomeView(store: .init(initialState: HomeState(), reducer: homeReducer, environment: HomeEnvironment(uuid: UUID.init)))
}
}
}
// MARK: - Home
struct HomeState: Equatable {
@Cow var isAActive = false
@Cow var screenA: ScreenAState?
}
enum HomeAction: Equatable {
case screenA(ScreenAAction)
case setScreenA(isActive: Bool)
}
struct HomeEnvironment {
var uuid: () -> UUID
}
let homeReducer: Reducer<HomeState, HomeAction, HomeEnvironment> = Reducer.combine(
screenAReducer
.optional()
.pullback(
state: \HomeState.screenA,
action: /HomeAction.screenA,
environment: { ScreenAEnvironment(uuid: $0.uuid) }
),
Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, environment in
switch action {
case .screenA:
return .none
case .setScreenA(isActive: true):
state.isAActive = true
state.screenA = .init(id: environment.uuid())
return .none
case .setScreenA(isActive: false):
state.isAActive = false
state.screenA = nil
return .none
}
}
)
struct HomeView: View {
let store: Store<HomeState, HomeAction>
var body: some View {
WithViewStore(self.store) { viewStore in
NavigationView {
VStack {
NavigationLink(
"Screen A",
destination: IfLetStore(
self.store.scope(
state: \.screenA,
action: HomeAction.screenA
),
then: ScreenA.init(store:),
else: ProgressView.init
),
isActive: viewStore.binding(
get: \.isAActive,
send: HomeAction.setScreenA(isActive:)
)
).isDetailLink(false)
}
.navigationBarTitle("Home Screen")
}
}
}
}
// MARK: - Screen A
struct ScreenAState: Equatable, Identifiable {
@Cow var id: UUID
@Cow var isAActive = false
@Cow var isBActive = false
@Cow var screenA: ScreenAState?
@Cow var screenB: ScreenBState?
}
indirect enum ScreenAAction: Equatable {
case screenA(ScreenAAction)
case screenB(ScreenBAction)
case setScreenA(isActive: Bool)
case setScreenB(isActive: Bool)
}
struct ScreenAEnvironment {
var uuid: () -> UUID
}
let screenAReducer: Reducer<ScreenAState, ScreenAAction, ScreenAEnvironment> = Reducer<
ScreenAState, ScreenAAction, ScreenAEnvironment
>.combine(
.recurse { `self`, state, action, environment in
switch action {
case .screenA:
return self
.optional()
.pullback(
state: \.screenA,
action: /ScreenAAction.screenA,
environment: { $0 }
)
.run(&state, action, environment)
case .screenB:
return screenBReducer
.optional()
.pullback(
state: \ScreenAState.screenB,
action: /ScreenAAction.screenB,
environment: { ScreenBEnvironment(uuid: $0.uuid) }
)
.run(&state, action, environment)
case .setScreenA(isActive: true):
state.isAActive = true
state.screenA = .init(id: environment.uuid())
return .none
case .setScreenA(isActive: false):
state.isAActive = false
state.screenA = nil
return .none
case .setScreenB(isActive: true):
state.isBActive = true
state.screenB = .init(id: environment.uuid())
return .none
case .setScreenB(isActive: false):
state.isBActive = false
state.screenB = nil
return .none
}
}
)
struct ScreenA: View {
let store: Store<ScreenAState, ScreenAAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
NavigationLink(
"Screen A",
destination: IfLetStore(
self.store.scope(
state: \.screenA,
action: ScreenAAction.screenA
),
then: ScreenA.init(store:),
else: ProgressView.init
),
isActive: viewStore.binding(
get: \.isAActive,
send: ScreenAAction.setScreenA(isActive:)
)
)
.isDetailLink(false)
NavigationLink(
"Screen B",
destination: IfLetStore(
self.store.scope(
state: \.screenB,
action: ScreenAAction.screenB
),
then: ScreenB.init(store:),
else: ProgressView.init
),
isActive: viewStore.binding(
get: \.isBActive,
send: ScreenAAction.setScreenB(isActive:)
)
)
.isDetailLink(false)
}
.navigationBarTitle("Screen A")
}
}
}
// MARK: - Screen B
struct ScreenBState: Equatable, Identifiable {
@Cow var id: UUID
@Cow var isAActive = false
@Cow var screenA: ScreenAState?
}
indirect enum ScreenBAction: Equatable {
case screenA(ScreenAAction)
case setScreenA(isActive: Bool)
}
struct ScreenBEnvironment {
var uuid: () -> UUID
}
let screenBReducer: Reducer<ScreenBState, ScreenBAction, ScreenBEnvironment> = Reducer.combine(
screenAReducer
.optional()
.pullback(
state: \ScreenBState.screenA,
action: /ScreenBAction.screenA,
environment: { ScreenAEnvironment(uuid: $0.uuid) }
),
Reducer<ScreenBState, ScreenBAction, ScreenBEnvironment> { state, action, environment in
switch action {
case .screenA:
return .none
case .setScreenA(isActive: true):
state.isAActive = true
state.screenA = .init(id: environment.uuid())
return .none
case .setScreenA(isActive: false):
state.isAActive = false
state.screenA = nil
return .none
}
}
)
struct ScreenB: View {
let store: Store<ScreenBState, ScreenBAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
NavigationLink(
"Screen A",
destination: IfLetStore(
self.store.scope(
state: \.screenA,
action: ScreenBAction.screenA
),
then: ScreenA.init(store:),
else: ProgressView.init
),
isActive: viewStore.binding(
get: \.isAActive,
send: ScreenBAction.setScreenA(isActive:)
)
)
.isDetailLink(false)
}
.navigationBarTitle("Screen B")
}
}
}
// MARK: Reducer
extension Reducer {
static func recurse(
_ reducer: @escaping (Reducer, inout State, Action, Environment) -> Effect<Action, Never>
) -> Reducer {
var `self`: Reducer!
self = Reducer { state, action, environment in
reducer(self, &state, action, environment)
}
return self
}
}
// MARK: - Cow
@propertyWrapper
public struct Cow<T> {
private final class Ref<T> {
var val: T
init(_ v: T) { val = v }
}
private var ref: Ref<T>
public init(_ x: T) { self.init(wrappedValue: x) }
public init(wrappedValue: T) { ref = Ref(wrappedValue) }
private var value: T {
get { ref.val }
set {
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
public var wrappedValue: T {
get { value }
set { value = newValue }
}
}
extension Cow: Equatable where T: Equatable {
public static func == (lhs: Cow<T>, rhs: Cow<T>) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment