Skip to content

Instantly share code, notes, and snippets.

@ThomasHack
Last active August 24, 2020 03:45
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ThomasHack/a646e9c59dd83414084d17dfe192d054 to your computer and use it in GitHub Desktop.
Save ThomasHack/a646e9c59dd83414084d17dfe192d054 to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// ComposableTest
//
import ComposableArchitecture
import SwiftUI
struct ContentView: View {
var store: Store<Main.State, Main.Action>
enum ViewKind: Hashable {
case home, shared
}
@State var selection: ViewKind = .home
var body: some View {
WithViewStore(self.store) { viewStore in
TabView(selection: self.$selection,
content: {
HomeView(store: Main.store.home)
.tabItem {
Image(systemName: "house")
Text("Home")
}
.tag(ViewKind.home)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(store: Main.store)
}
}
// MARK: - Main
enum Main {
struct State: Equatable {
var shared: Shared.State
var home: Home.State
var settings: Settings.State
var homeFeature: Home.HomeFeature {
get { Home.HomeFeature(home: self.home, shared: self.shared) }
set {
self.home = newValue.home
self.shared = newValue.shared
}
}
var settingsFeature: Settings.SettingsFeature {
get { Settings.SettingsFeature(settings: self.settings, shared: self.shared) }
set {
self.settings = newValue.settings
self.shared = newValue.shared
}
}
}
enum Action {
case shared(Shared.Action)
case home(Home.Action)
case settings(Settings.Action)
}
struct Environment {
let mainQueue: AnySchedulerOf<DispatchQueue>
}
static let reducer = Reducer<State, Action, Environment>.combine(
Reducer<State, Action, Environment>{ _, _, _ in
return .none
},
Shared.reducer.pullback(
state: \State.shared,
action: /Action.shared,
environment: { $0 }
),
Home.reducer.pullback(
state: \State.homeFeature,
action: /Action.home,
environment: { $0 }
),
Settings.reducer.pullback(
state: \State.settingsFeature,
action: /Action.settings,
environment: { $0 }
)
)
static let store = Store(
initialState: State(
shared: Shared.initialState,
home: Home.initialState,
settings: Settings.initialState
),
reducer: reducer,
environment: Environment(
mainQueue: DispatchQueue.main.eraseToAnyScheduler()
)
)
}
extension Store where State == Main.State, Action == Main.Action {
var home: Store<Home.HomeFeature, Home.Action> {
scope(state: \.homeFeature, action: Main.Action.home)
}
var settings: Store<Settings.SettingsFeature, Settings.Action> {
scope(state: \.settingsFeature, action: Main.Action.settings)
}
}
// MARK: - Shared
enum Shared {
struct State: Equatable {
var sharedProperty: Bool = false
var showSettingsModal: Bool = false
}
enum Action {
case shared
case showSettingsModal
case hideSettingsModal
case toggleSettingsModal(Bool)
}
typealias Environment = Main.Environment
static let reducer = Reducer<Shared.State, Shared.Action, Environment> { state, action, environment in
switch action {
case .shared:
state.sharedProperty = false
case .showSettingsModal:
state.showSettingsModal = true
case .hideSettingsModal:
state.showSettingsModal = false
case .toggleSettingsModal(let show):
state.showSettingsModal = show
}
return .none
}
static let initialState = State(
sharedProperty: false
)
}
// MARK: - Home
enum Home {
@dynamicMemberLookup
struct HomeFeature: Equatable {
var home: Home.State
var shared: Shared.State
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Home.State, T>) -> T {
get { home[keyPath: keyPath] }
set { home[keyPath: keyPath] = newValue }
}
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Shared.State, T>) -> T {
get { shared[keyPath: keyPath] }
set { shared[keyPath: keyPath] = newValue }
}
}
struct State: Equatable {
var homeProperty: Bool = false
}
enum Action {
case home
case shared(Shared.Action)
}
typealias Environment = Main.Environment
static let reducer = Reducer<HomeFeature, Action, Environment>.combine(
Reducer { state, action, environment in
switch action {
case .home:
state.homeProperty.toggle()
return .none
case .shared:
break
}
return .none
},
Shared.reducer.pullback(
state: \HomeFeature.shared,
action: /Action.shared,
environment: { $0 }
)
)
static let initialState = Home.State(
homeProperty: false
)
}
struct HomeView: View {
let store: Store<Home.HomeFeature, Home.Action>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack(alignment: .center, spacing: 16) {
Button(action: { viewStore.send(.home) }) {
Text("Local Action \(viewStore.homeProperty ? "true" : "false")")
}
Button(action: { viewStore.send(.shared(.showSettingsModal)) }) {
Text("Shared Action \(viewStore.sharedProperty ? "true" : "false")")
}
}.sheet(isPresented: viewStore.binding( get: { $0.showSettingsModal }, send: Home.Action.shared(.hideSettingsModal))) {
SettingsView(store: Main.store.settings)
}
}
}
}
// MARK: - Settings
enum Settings {
@dynamicMemberLookup
struct SettingsFeature: Equatable {
var settings: Settings.State
var shared: Shared.State
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Settings.State, T>) -> T {
get { settings[keyPath: keyPath] }
set { settings[keyPath: keyPath] = newValue }
}
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Shared.State, T>) -> T {
get { shared[keyPath: keyPath] }
set { shared[keyPath: keyPath] = newValue }
}
}
struct State: Equatable {
var settingsProperty: Bool = false
}
enum Action {
case settings
case shared(Shared.Action)
}
typealias Environment = Main.Environment
static let reducer = Reducer<SettingsFeature, Action, Environment>.combine(
Reducer { state, action, _ in
switch action {
case .settings:
state.settingsProperty.toggle()
return .none
case .shared:
break
}
return .none
},
Shared.reducer.pullback(
state: \SettingsFeature.shared,
action: /Action.shared,
environment: { $0 }
)
)
static let initialState = Settings.State(
settingsProperty: false
)
}
struct SettingsView: View {
let store: Store<Settings.SettingsFeature, Settings.Action>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack(alignment: .center, spacing: 16) {
Button(action: { viewStore.send(.settings) }) {
Text("Local Action \(viewStore.settingsProperty ? "true" : "false")")
}
Button(action: { viewStore.send(Settings.Action.shared(.hideSettingsModal)) }) {
Text("Shared Action \(viewStore.sharedProperty ? "true" : "false")")
}
}
}
}
}
// MARK: - API
enum Api {
struct State: Equatable {
var connectivity: ConnectivityStatus = .disconnected
enum ConnectivityStatus {
case connected
case disconnected
}
}
enum Action {
case shared
}
typealias Environment = Main.Environment
static let reducer = Reducer<State, Action, Environment> { _, _, _ in
return .none
}
static let initialState = State()
}
@mdonati-cj
Copy link

Thanks for sharing this! I took this as a base to start organising our code in a better way.
I added a protocol for the feature lookup:

@dynamicMemberLookup
protocol FeatureType {
    associatedtype State
    associatedtype Shared
    var state: State { get set }
    var shared: Shared { get set }
}

extension FeatureType {
    public subscript<T>(dynamicMember keyPath: WritableKeyPath<State, T>) -> T {
        get { state[keyPath: keyPath] }
        set { state[keyPath: keyPath] = newValue }
    }

    public subscript<T>(dynamicMember keyPath: WritableKeyPath<Shared, T>) -> T {
        get { shared[keyPath: keyPath] }
        set { shared[keyPath: keyPath] = newValue }
    }
}

That way a Feature state will unlock the getters/setters for free just by having the state and shared properties. Lastly, instead of combining the shared reducer with each feature reducer, why not let the feature reducer act on shared state since the feature provides a writable key path anyways? I think that reduces verbosity and it gives a feature reducer the flexibility to change a shared state in whatever way it needs. Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment