Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution using presents #1

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
27 changes: 12 additions & 15 deletions Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
3136F93A24B4D0D400A17C89 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3136F93924B4D0D400A17C89 /* SceneDelegate.swift */; };
3136F93C24B4D0D400A17C89 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3136F93B24B4D0D400A17C89 /* App.swift */; };
3136F93E24B4D0D600A17C89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3136F93D24B4D0D600A17C89 /* Assets.xcassets */; };
3136F94424B4D0D600A17C89 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3136F94224B4D0D600A17C89 /* LaunchScreen.storyboard */; };
3136F94D24B4D25F00A17C89 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 3136F94C24B4D25F00A17C89 /* ComposableArchitecture */; };
31CBF90424B5143500968607 /* Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CBF90324B5143500968607 /* Lifecycle.swift */; };
31F1C4F124B4D3CC0028825C /* Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F1C4F024B4D3CC0028825C /* Detail.swift */; };
A3458DDB257F759A00C909BB /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3458DDA257F759A00C909BB /* Avatar.swift */; };
A362BEE025A753D600B183C9 /* Cancellation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A362BEDF25A753D600B183C9 /* Cancellation.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -22,9 +24,11 @@
3136F93924B4D0D400A17C89 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
3136F93B24B4D0D400A17C89 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
3136F93D24B4D0D600A17C89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3136F94324B4D0D600A17C89 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
3136F94524B4D0D600A17C89 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
31CBF90324B5143500968607 /* Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lifecycle.swift; sourceTree = "<group>"; };
31F1C4F024B4D3CC0028825C /* Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Detail.swift; sourceTree = "<group>"; };
A3458DDA257F759A00C909BB /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
A362BEDF25A753D600B183C9 /* Cancellation.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Cancellation.swift; sourceTree = "<group>"; tabWidth = 2; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -62,9 +66,11 @@
3136F93724B4D0D400A17C89 /* AppDelegate.swift */,
3136F93D24B4D0D600A17C89 /* Assets.xcassets */,
31F1C4F024B4D3CC0028825C /* Detail.swift */,
A3458DDA257F759A00C909BB /* Avatar.swift */,
3136F94524B4D0D600A17C89 /* Info.plist */,
3136F94224B4D0D600A17C89 /* LaunchScreen.storyboard */,
31CBF90324B5143500968607 /* Lifecycle.swift */,
3136F93924B4D0D400A17C89 /* SceneDelegate.swift */,
A362BEDF25A753D600B183C9 /* Cancellation.swift */,
);
path = Demo;
sourceTree = "<group>";
Expand Down Expand Up @@ -133,7 +139,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3136F94424B4D0D600A17C89 /* LaunchScreen.storyboard in Resources */,
3136F93E24B4D0D600A17C89 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -145,26 +150,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A3458DDB257F759A00C909BB /* Avatar.swift in Sources */,
3136F93824B4D0D400A17C89 /* AppDelegate.swift in Sources */,
3136F93A24B4D0D400A17C89 /* SceneDelegate.swift in Sources */,
31F1C4F124B4D3CC0028825C /* Detail.swift in Sources */,
31CBF90424B5143500968607 /* Lifecycle.swift in Sources */,
3136F93C24B4D0D400A17C89 /* App.swift in Sources */,
A362BEE025A753D600B183C9 /* Cancellation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
3136F94224B4D0D600A17C89 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
3136F94324B4D0D600A17C89 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
3136F94624B4D0D600A17C89 /* Debug */ = {
isa = XCBuildConfiguration;
Expand Down
78 changes: 55 additions & 23 deletions Demo/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,59 @@ import ComposableArchitecture
import SwiftUI

struct AppState: Equatable {
var detail: DetailState?
var detail1: DetailState?
var detail2: DetailState? = DetailState()
}

enum AppAction {
case presentDetail
case dismissDetail
case detail(DetailAction)
case presentDetail1
case dismissDetail1
case detail1(DetailAction)
case detail2(DetailAction)
}

let appReducer = Reducer<AppState, AppAction, Void>.combine(
detailReducer.optional.pullback(
state: \.detail,
action: /AppAction.detail,
environment: { _ in () }
),
Reducer { state, action, _ in
struct AppEnvironment {
let cancellationId: AnyHashable
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
Reducer { state, action, env in
switch action {
case .presentDetail:
state.detail = .init()
case .presentDetail1:
state.detail1 = .init()
return .none

case .dismissDetail:
state.detail = nil

case .dismissDetail1:
state.detail1 = nil
return .none

case .detail1(.onDisappear):
state.detail1 = nil
return .none

case .detail(_):
case .detail1(_):
return .none

case .detail2(_):
return .none
}
}
}.presents(
detailReducer,
cancelEffectsOnDismiss: true,
state: \.detail1,
action: /AppAction.detail1,
environment: { env in
DetailEnvironment(cancellationId: [env.cancellationId, 1])
}
).presents(
detailReducer,
cancelEffectsOnDismiss: true,
state: \.detail2,
action: /AppAction.detail2,
environment: { env in
DetailEnvironment(cancellationId: [env.cancellationId, 2])
}
)
)

struct AppView: View {
Expand All @@ -41,21 +64,30 @@ struct AppView: View {
WithViewStore(store) { viewStore in
VStack(spacing: 16) {
Text("App").font(.title).padding(.top, 100)
if viewStore.state.detail != nil {
Button(action: { viewStore.send(.dismissDetail) }) {
if viewStore.state.detail1 != nil {
Button(action: { viewStore.send(.dismissDetail1) }) {
Text("Dismiss Detail")
}
} else {
Button(action: { viewStore.send(.presentDetail) }) {
Button(action: { viewStore.send(.presentDetail1) }) {
Text("Present Detail")
}
}

IfLetStore(self.store.scope(
state: \.detail1,
action: AppAction.detail1
)) { detailStore in
DetailView(store: detailStore)
}

IfLetStore(self.store.scope(
state: \.detail,
action: AppAction.detail
state: \.detail2,
action: AppAction.detail2
)) { detailStore in
DetailView(store: detailStore)
}

Spacer()
}
}
Expand Down
98 changes: 98 additions & 0 deletions Demo/Avatar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ComposableArchitecture
import Combine
import SwiftUI

struct AvatarState: Equatable {
var image: UIImage?
var error: AvatarError?
}

enum AvatarAction: Equatable {
case onAppear
case onDisappear
case load
case loaded(String)
case failed(AvatarError)
}

enum AvatarError: Error, Equatable {
case network
}

struct AvatarEnvironment {
var cancellationId: AnyHashable
}

let avatarReducer = Reducer<AvatarState, AvatarAction, AvatarEnvironment> { state, action, env in

switch action {

case .load:
return Just("faceid")
.map { AvatarAction.loaded($0) }
.replaceError(with: AvatarAction.failed(.network))
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.handleEvents(receiveSubscription: { (sub) in
print("receiveSubscription avatar")
}, receiveOutput: { (output) in
print("receiveOutput avatar")
}, receiveCompletion: { (completion) in
print("receiveCompletion avatar")
}, receiveCancel: {
print("receiveCancel avatar")
}, receiveRequest: { (demand) in
print("receiveRequest avatar")
})
.eraseToEffect()
.cancellable(id: env.cancellationId)

case .loaded(let name):
state.image = UIImage(systemName: name)
return .none

case .failed(let error):
state.error = error
return .none

case .onAppear:
return .init(value: .load)

case .onDisappear:
return .cancel(id: env.cancellationId)
}
}

struct AvatarView: View {
var store: Store<AvatarState, AvatarAction>

var body: some View {
WithViewStore(store) { viewStore in
VStack(spacing: 16) {
if let _ = viewStore.error {
Image(systemName: "person.crop.circle.badge.xmark")
} else if let image = viewStore.image {
Image(uiImage: image)
} else {
Image(systemName: "clock")
}
}.onAppear {
viewStore.send(.onAppear)
}
.onDisappear {
viewStore.send(.onDisappear)
}
}
}
}

#if DEBUG
struct AvatarView_Previews: PreviewProvider {
static var previews: some View {
DetailView(store: Store(
initialState: .init(),
reducer: .empty,
environment: ()
))
}
}
#endif
25 changes: 0 additions & 25 deletions Demo/Base.lproj/LaunchScreen.storyboard

This file was deleted.

44 changes: 44 additions & 0 deletions Demo/Cancellation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import ComposableArchitecture

extension Reducer {

fileprivate func nonCrashingOptional(_ file: StaticString = #file, _ line: UInt = #line) -> Reducer<
State?, Action, Environment
> {
.init { state, action, environment in
guard state != nil else {
return .none
}
return self.run(&state!, action, environment)
}
}

public func presents<LocalState, LocalAction, LocalEnvironment>(
_ localReducer: Reducer<LocalState, LocalAction, LocalEnvironment>,
cancelEffectsOnDismiss: Bool,
state toLocalState: WritableKeyPath<State, LocalState?>,
action toLocalAction: CasePath<Action, LocalAction>,
environment toLocalEnvironment: @escaping (Environment) -> LocalEnvironment
) -> Self {
let id = UUID()
return Self { state, action, environment in
let hadLocalState = state[keyPath: toLocalState] != nil
let localEffects = localReducer
.nonCrashingOptional()
.pullback(state: toLocalState, action: toLocalAction, environment: toLocalEnvironment)
.run(&state, action, environment)
.cancellable(id: id)
let globalEffects = self.run(&state, action, environment)
let hasLocalState = state[keyPath: toLocalState] != nil
print("Presents with cancellation id \(id)")
if cancelEffectsOnDismiss && hadLocalState && !hasLocalState {
print("Cancels cancellation id \(id)")
}
return .merge(
localEffects,
globalEffects,
cancelEffectsOnDismiss && hadLocalState && !hasLocalState ? .cancel(id: id) : .none
)
}
}
}
Loading