From e3a34983c8ae7ec29abedb8f9167ebec5c534477 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 20 Jun 2020 18:13:38 -0500 Subject: [PATCH 01/29] alerts --- .../SwiftUICaseStudies/00-RootView.swift | 97 ++++++++++++ .../DownloadComponent.swift | 138 ++++++++++-------- .../ReusableComponents-Download.swift | 2 +- 3 files changed, 173 insertions(+), 64 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 27a8fc892bc0..3aa468e9db3e 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -334,3 +334,100 @@ struct RootView_Previews: PreviewProvider { RootView() } } + + +enum AlertState { + case dismissed + case show(Alert) + + struct Alert { + var message: String? + var primaryButton: Button + var secondaryButton: Button? + var title: String + + struct Button { + var action: Action + var label: String + var type: `Type` + + enum `Type` { + case cancel + case `default` + case destructive + } + + func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + switch self.type { + case .cancel: + return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } + case .default: + return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } + case .destructive: + return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } + } + } + } + + func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert { + let title = Text(self.title) + let message = self.message.map { Text($0) } + + if let secondaryButton = self.secondaryButton { + return SwiftUI.Alert( + title: title, + message: message, + primaryButton: self.primaryButton.toSwiftUI(send: send), + secondaryButton: secondaryButton.toSwiftUI(send: send) + ) + } else { + return SwiftUI.Alert( + title: title, + message: message, + dismissButton: self.primaryButton.toSwiftUI(send: send) + ) + } + } + } +} + +extension AlertState.Alert: Equatable where Action: Equatable {} +extension AlertState.Alert.Button: Equatable where Action: Equatable {} +extension AlertState: Equatable where Action: Equatable {} + +extension AlertState.Alert: Hashable where Action: Hashable {} +extension AlertState.Alert.Button: Hashable where Action: Hashable {} +extension AlertState: Hashable where Action: Hashable {} + +extension AlertState.Alert: Identifiable where Action: Hashable { + var id: Self { self } +} + +extension View { + func _alert( + item: Binding>, + send: @escaping (Action) -> Void, + @ViewBuilder content: @escaping (AlertState.Alert) -> Content + ) -> some View where Action: Hashable { + + self.alert( + item: Binding.Alert?>( + get: { + switch item.wrappedValue { + case .dismissed: + return nil + case let .show(alert): + return alert + } + }, + set: { + if let alert = $0 { + item.wrappedValue = .show(alert) + } else { + item.wrappedValue = .dismissed + } + }), + content: { $0.toSwiftUI(send: send) } + ) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 3f3f87519e47..9c39a768f020 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -2,42 +2,42 @@ import ComposableArchitecture import SwiftUI struct DownloadComponentState: Equatable { - var alert: DownloadAlert? + var alert: AlertState = .dismissed // DownloadAlert? let id: ID var mode: Mode let url: URL } -struct DownloadAlert: Equatable, Identifiable { - var primaryButton: Button - var secondaryButton: Button - var title: String - - var id: String { self.title } - - struct Button: Equatable { - var action: DownloadComponentAction - var label: String - var type: `Type` - - enum `Type` { - case cancel - case `default` - case destructive - } - - func toSwiftUI(action: @escaping (DownloadComponentAction) -> Void) -> Alert.Button { - switch self.type { - case .cancel: - return .cancel(Text(self.label)) { action(self.action) } - case .default: - return .default(Text(self.label)) { action(self.action) } - case .destructive: - return .destructive(Text(self.label)) { action(self.action) } - } - } - } -} +//struct DownloadAlert: Equatable, Identifiable { +// var primaryButton: Button +// var secondaryButton: Button +// var title: String +// +// var id: String { self.title } +// +// struct Button: Equatable { +// var action: DownloadComponentAction +// var label: String +// var type: `Type` +// +// enum `Type` { +// case cancel +// case `default` +// case destructive +// } +// +// func toSwiftUI(action: @escaping (DownloadComponentAction) -> Void) -> Alert.Button { +// switch self.type { +// case .cancel: +// return .cancel(Text(self.label)) { action(self.action) } +// case .default: +// return .default(Text(self.label)) { action(self.action) } +// case .destructive: +// return .destructive(Text(self.label)) { action(self.action) } +// } +// } +// } +//} enum Mode: Equatable { case downloaded @@ -90,18 +90,18 @@ extension Reducer { switch action { case .alert(.cancelButtonTapped): state.mode = .notDownloaded - state.alert = nil + state.alert = .dismissed return environment.downloadClient.cancel(state.id) .fireAndForget() case .alert(.deleteButtonTapped): - state.alert = nil + state.alert = .dismissed state.mode = .notDownloaded return .none case .alert(.nevermindButtonTapped), .alert(.dismiss): - state.alert = nil + state.alert = .dismissed return .none case .buttonTapped: @@ -129,7 +129,7 @@ extension Reducer { case .downloadClient(.success(.response)): state.mode = .downloaded - state.alert = nil + state.alert = .dismissed return .none case let .downloadClient(.success(.updateProgress(progress))): @@ -138,7 +138,7 @@ extension Reducer { case .downloadClient(.failure): state.mode = .notDownloaded - state.alert = nil + state.alert = .dismissed return .none } } @@ -148,27 +148,33 @@ extension Reducer { } } -private let deleteAlert = DownloadAlert( - primaryButton: .init( - action: .alert(.deleteButtonTapped), - label: "Delete", - type: .destructive - ), - secondaryButton: nevermindButton, - title: "Do you want to delete this map from your offline storage?" +private let deleteAlert = AlertState.show( + .init( + message: nil, + primaryButton: .init( + action: .alert(.deleteButtonTapped), + label: "Delete", + type: .destructive + ), + secondaryButton: nevermindButton, + title: "Do you want to delete this map from your offline storage?" + ) ) -private let cancelAlert = DownloadAlert( - primaryButton: .init( - action: .alert(.cancelButtonTapped), - label: "Cancel", - type: .destructive - ), - secondaryButton: nevermindButton, - title: "Do you want to cancel downloading this map?" +private let cancelAlert = AlertState.show( + .init( + message: nil, + primaryButton: .init( + action: .alert(.cancelButtonTapped), + label: "Cancel", + type: .destructive + ), + secondaryButton: nevermindButton, + title: "Do you want to cancel downloading this map?" + ) ) -let nevermindButton = DownloadAlert.Button( +let nevermindButton = AlertState.Alert.Button( action: .alert(.nevermindButtonTapped), label: "Nevermind", type: .default @@ -178,7 +184,10 @@ struct DownloadComponent: View { let store: Store, DownloadComponentAction> var body: some View { - WithViewStore(self.store) { viewStore in + + let tmp = ViewStore(self.store).binding(get: { $0.alert }, send: DownloadComponentAction.alert) + + return WithViewStore(self.store) { viewStore in Button(action: { viewStore.send(.buttonTapped) }) { if viewStore.mode == .downloaded { Image(systemName: "checkmark.circle") @@ -205,15 +214,18 @@ struct DownloadComponent: View { } } } - .alert( - item: viewStore.binding(get: { $0.alert }, send: .alert(.dismiss)) - ) { alert in - Alert( - title: Text(alert.title), - primaryButton: alert.primaryButton.toSwiftUI(action: viewStore.send), - secondaryButton: alert.secondaryButton.toSwiftUI(action: viewStore.send) - ) - } +// ._alert(item: <#T##Binding>#>, send: <#T##(Hashable) -> Void#>, content: <#T##(AlertState.Alert) -> View#>) + +// ._alert( +// item: viewStore.binding(get: { $0.alert }, send: { $0 }) +// ) { alert in +// alert.toSwiftUI(send: viewStore.send) +//// Alert( +//// title: Text(alert.title), +//// primaryButton: alert.primaryButton.toSwiftUI(action: viewStore.send), +//// secondaryButton: alert.secondaryButton.toSwiftUI(action: viewStore.send) +//// ) +// } } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift index dbeb01a19fa2..fdb8620e6380 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift @@ -23,7 +23,7 @@ struct CityMap: Equatable, Identifiable { } struct CityMapState: Equatable, Identifiable { - var downloadAlert: DownloadAlert? + var downloadAlert: AlertState = .dismissed var downloadMode: Mode var cityMap: CityMap From 96539fdb92e84ece53c2dc43666a06ab11fbbb73 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 21 Jun 2020 11:56:20 -0500 Subject: [PATCH 02/29] wip --- .../DownloadClient.swift | 4 ++-- .../DownloadComponent.swift | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift index 09eb3175e9a4..0abbc0ced54a 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift @@ -6,9 +6,9 @@ struct DownloadClient { var cancel: (AnyHashable) -> Effect var download: (AnyHashable, URL) -> Effect - struct Error: Swift.Error, Equatable {} + struct Error: Swift.Error, Equatable, Hashable {} - enum Action: Equatable { + enum Action: Equatable, Hashable { case response(Data) case updateProgress(Double) } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 9c39a768f020..f21b2d7ccd12 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -60,7 +60,7 @@ enum Mode: Equatable { } } -enum DownloadComponentAction: Equatable { +enum DownloadComponentAction: Equatable, Hashable { case alert(AlertAction) case buttonTapped case downloadClient(Result) @@ -185,7 +185,8 @@ struct DownloadComponent: View { var body: some View { - let tmp = ViewStore(self.store).binding(get: { $0.alert }, send: DownloadComponentAction.alert) + let tmp: Binding> = ViewStore(self.store) + .binding(get: { $0.alert }, send: .alert(.dismiss)) return WithViewStore(self.store) { viewStore in Button(action: { viewStore.send(.buttonTapped) }) { @@ -214,6 +215,15 @@ struct DownloadComponent: View { } } } + + ._alert( + item: viewStore.binding(get: { $0.alert }, send: .alert(.dismiss)), + send: viewStore.send + ) { alert in + alert + } + + // ._alert(item: <#T##Binding>#>, send: <#T##(Hashable) -> Void#>, content: <#T##(AlertState.Alert) -> View#>) // ._alert( From a04fbc4903bc6d811934c58f372cde30443c583d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 21 Jun 2020 12:22:48 -0500 Subject: [PATCH 03/29] wip --- .../SwiftUICaseStudies/00-RootView.swift | 97 -------------- .../DownloadComponent.swift | 65 +-------- .../SwiftUI/Alert.swift | 125 ++++++++++++++++++ .../ComposableCoreLocation/Interface.swift | 2 +- 4 files changed, 133 insertions(+), 156 deletions(-) create mode 100644 Sources/ComposableArchitecture/SwiftUI/Alert.swift diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 3aa468e9db3e..27a8fc892bc0 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -334,100 +334,3 @@ struct RootView_Previews: PreviewProvider { RootView() } } - - -enum AlertState { - case dismissed - case show(Alert) - - struct Alert { - var message: String? - var primaryButton: Button - var secondaryButton: Button? - var title: String - - struct Button { - var action: Action - var label: String - var type: `Type` - - enum `Type` { - case cancel - case `default` - case destructive - } - - func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { - switch self.type { - case .cancel: - return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } - case .default: - return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } - case .destructive: - return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } - } - } - } - - func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert { - let title = Text(self.title) - let message = self.message.map { Text($0) } - - if let secondaryButton = self.secondaryButton { - return SwiftUI.Alert( - title: title, - message: message, - primaryButton: self.primaryButton.toSwiftUI(send: send), - secondaryButton: secondaryButton.toSwiftUI(send: send) - ) - } else { - return SwiftUI.Alert( - title: title, - message: message, - dismissButton: self.primaryButton.toSwiftUI(send: send) - ) - } - } - } -} - -extension AlertState.Alert: Equatable where Action: Equatable {} -extension AlertState.Alert.Button: Equatable where Action: Equatable {} -extension AlertState: Equatable where Action: Equatable {} - -extension AlertState.Alert: Hashable where Action: Hashable {} -extension AlertState.Alert.Button: Hashable where Action: Hashable {} -extension AlertState: Hashable where Action: Hashable {} - -extension AlertState.Alert: Identifiable where Action: Hashable { - var id: Self { self } -} - -extension View { - func _alert( - item: Binding>, - send: @escaping (Action) -> Void, - @ViewBuilder content: @escaping (AlertState.Alert) -> Content - ) -> some View where Action: Hashable { - - self.alert( - item: Binding.Alert?>( - get: { - switch item.wrappedValue { - case .dismissed: - return nil - case let .show(alert): - return alert - } - }, - set: { - if let alert = $0 { - item.wrappedValue = .show(alert) - } else { - item.wrappedValue = .dismissed - } - }), - content: { $0.toSwiftUI(send: send) } - ) - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index f21b2d7ccd12..b5d5873de830 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -2,43 +2,12 @@ import ComposableArchitecture import SwiftUI struct DownloadComponentState: Equatable { - var alert: AlertState = .dismissed // DownloadAlert? + var alert: AlertState = .dismissed let id: ID var mode: Mode let url: URL } -//struct DownloadAlert: Equatable, Identifiable { -// var primaryButton: Button -// var secondaryButton: Button -// var title: String -// -// var id: String { self.title } -// -// struct Button: Equatable { -// var action: DownloadComponentAction -// var label: String -// var type: `Type` -// -// enum `Type` { -// case cancel -// case `default` -// case destructive -// } -// -// func toSwiftUI(action: @escaping (DownloadComponentAction) -> Void) -> Alert.Button { -// switch self.type { -// case .cancel: -// return .cancel(Text(self.label)) { action(self.action) } -// case .default: -// return .default(Text(self.label)) { action(self.action) } -// case .destructive: -// return .destructive(Text(self.label)) { action(self.action) } -// } -// } -// } -//} - enum Mode: Equatable { case downloaded case downloading(progress: Double) @@ -184,11 +153,7 @@ struct DownloadComponent: View { let store: Store, DownloadComponentAction> var body: some View { - - let tmp: Binding> = ViewStore(self.store) - .binding(get: { $0.alert }, send: .alert(.dismiss)) - - return WithViewStore(self.store) { viewStore in + WithViewStore(self.store) { viewStore in Button(action: { viewStore.send(.buttonTapped) }) { if viewStore.mode == .downloaded { Image(systemName: "checkmark.circle") @@ -215,27 +180,11 @@ struct DownloadComponent: View { } } } - - ._alert( - item: viewStore.binding(get: { $0.alert }, send: .alert(.dismiss)), - send: viewStore.send - ) { alert in - alert - } - - -// ._alert(item: <#T##Binding>#>, send: <#T##(Hashable) -> Void#>, content: <#T##(AlertState.Alert) -> View#>) - -// ._alert( -// item: viewStore.binding(get: { $0.alert }, send: { $0 }) -// ) { alert in -// alert.toSwiftUI(send: viewStore.send) -//// Alert( -//// title: Text(alert.title), -//// primaryButton: alert.primaryButton.toSwiftUI(action: viewStore.send), -//// secondaryButton: alert.secondaryButton.toSwiftUI(action: viewStore.send) -//// ) -// } + .alert( + viewStore.alert, + send: viewStore.send, + dismissal: .alert(.dismiss) + ) } } } diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift new file mode 100644 index 000000000000..a9bd9315e80f --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -0,0 +1,125 @@ +import SwiftUI + +/// A data type that describes the state of an alert that can be shown to the user. +public enum AlertState: Hashable { + case dismissed + case show(Alert) + + public struct Alert { + public var message: String? + public var primaryButton: Button + public var secondaryButton: Button? + public var title: String + + public init( + message: String?, + primaryButton: Button, + secondaryButton: Button?, + title: String + ) { + self.message = message + self.primaryButton = primaryButton + self.secondaryButton = secondaryButton + self.title = title + } + + public struct Button { + public var action: Action + public var label: String + public var type: `Type` + + public init( + action: Action, + label: String, + type: `Type` + ) { + self.action = action + self.label = label + self.type = type + } + + public enum `Type` { + case cancel + case `default` + case destructive + } + } + } +} + +extension AlertState.Alert: Hashable where Action: Hashable {} +extension AlertState.Alert.Button: Equatable where Action: Equatable {} +extension AlertState.Alert.Button: Hashable where Action: Hashable {} + +extension AlertState.Alert: Identifiable where Action: Hashable { + public var id: Self { self } +} + +extension View { + /// Displays an alert when `state` is in the `.show` state. + /// + /// - Parameters: + /// - state: A value that describes if the alert is shown or dismissed. + /// - send: A reference to the view store's `send` method for which actions from this alert + /// should be sent to. + /// - dismissal: An action to send when the alert is dismissed through non-user actions, such + /// as when an alert is automatically dismissed by the system. + public func alert( + _ state: AlertState, + send: @escaping (Action) -> Void, + dismissal: Action + ) -> some View where Action: Hashable { + + self.alert( + item: Binding.Alert?>( + get: { + switch state { + case .dismissed: + return nil + case let .show(alert): + return alert + } + }, + set: { + guard $0 == nil else { return } + send(dismissal) + }), + content: { $0.toSwiftUI(send: send) } + ) + } +} + +extension AlertState.Alert.Button { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + switch self.type { + case .cancel: + return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } + case .default: + return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } + case .destructive: + return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } + } + } +} + +extension AlertState.Alert { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert { + let title = Text(self.title) + let message = self.message.map { Text($0) } + + if let secondaryButton = self.secondaryButton { + return SwiftUI.Alert( + title: title, + message: message, + primaryButton: self.primaryButton.toSwiftUI(send: send), + secondaryButton: secondaryButton.toSwiftUI(send: send) + ) + } else { + return SwiftUI.Alert( + title: title, + message: message, + dismissButton: self.primaryButton.toSwiftUI(send: send) + ) + } + } +} diff --git a/Sources/ComposableCoreLocation/Interface.swift b/Sources/ComposableCoreLocation/Interface.swift index 98de142bb6bb..49fa48eae396 100644 --- a/Sources/ComposableCoreLocation/Interface.swift +++ b/Sources/ComposableCoreLocation/Interface.swift @@ -175,7 +175,7 @@ public struct LocationManager { /// Actions that correspond to `CLLocationManagerDelegate` methods. /// /// See `CLLocationManagerDelegate` for more information. - public enum Action: Equatable { + public enum Action: Equatable, Hashable { case didChangeAuthorization(CLAuthorizationStatus) @available(tvOS, unavailable) From ea326df89d76240b6a82c33e68e4a1b9095312d1 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 21 Jun 2020 12:43:56 -0500 Subject: [PATCH 04/29] wip --- .../DownloadClient.swift | 4 ++-- .../DownloadComponent.swift | 18 +++++++++--------- .../ReusableComponents-Download.swift | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift index 0abbc0ced54a..09eb3175e9a4 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift @@ -6,9 +6,9 @@ struct DownloadClient { var cancel: (AnyHashable) -> Effect var download: (AnyHashable, URL) -> Effect - struct Error: Swift.Error, Equatable, Hashable {} + struct Error: Swift.Error, Equatable {} - enum Action: Equatable, Hashable { + enum Action: Equatable { case response(Data) case updateProgress(Double) } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index b5d5873de830..8a74502a26c8 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -2,7 +2,7 @@ import ComposableArchitecture import SwiftUI struct DownloadComponentState: Equatable { - var alert: AlertState = .dismissed + var alert: AlertState = .dismissed let id: ID var mode: Mode let url: URL @@ -29,12 +29,12 @@ enum Mode: Equatable { } } -enum DownloadComponentAction: Equatable, Hashable { +enum DownloadComponentAction: Equatable { case alert(AlertAction) case buttonTapped case downloadClient(Result) - enum AlertAction: Equatable { + enum AlertAction: Equatable, Hashable { case cancelButtonTapped case deleteButtonTapped case dismiss @@ -121,7 +121,7 @@ private let deleteAlert = AlertState.show( .init( message: nil, primaryButton: .init( - action: .alert(.deleteButtonTapped), + action: .deleteButtonTapped, label: "Delete", type: .destructive ), @@ -134,7 +134,7 @@ private let cancelAlert = AlertState.show( .init( message: nil, primaryButton: .init( - action: .alert(.cancelButtonTapped), + action: .cancelButtonTapped, label: "Cancel", type: .destructive ), @@ -143,8 +143,8 @@ private let cancelAlert = AlertState.show( ) ) -let nevermindButton = AlertState.Alert.Button( - action: .alert(.nevermindButtonTapped), +let nevermindButton = AlertState.Alert.Button( + action: .nevermindButtonTapped, label: "Nevermind", type: .default ) @@ -182,8 +182,8 @@ struct DownloadComponent: View { } .alert( viewStore.alert, - send: viewStore.send, - dismissal: .alert(.dismiss) + send: { viewStore.send(.alert($0)) }, + dismissal: .dismiss ) } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift index fdb8620e6380..4c3723a76e9e 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift @@ -23,7 +23,7 @@ struct CityMap: Equatable, Identifiable { } struct CityMapState: Equatable, Identifiable { - var downloadAlert: AlertState = .dismissed + var downloadAlert: AlertState = .dismissed var downloadMode: Mode var cityMap: CityMap From 23a5aaaa8f43e68946cf227f65ba10f82be4b17d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 21 Jun 2020 12:49:24 -0500 Subject: [PATCH 05/29] clean up --- .../DownloadComponent.swift | 2 -- Sources/ComposableArchitecture/SwiftUI/Alert.swift | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 8a74502a26c8..fd46d0340fe9 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -119,7 +119,6 @@ extension Reducer { private let deleteAlert = AlertState.show( .init( - message: nil, primaryButton: .init( action: .deleteButtonTapped, label: "Delete", @@ -132,7 +131,6 @@ private let deleteAlert = AlertState.show( private let cancelAlert = AlertState.show( .init( - message: nil, primaryButton: .init( action: .cancelButtonTapped, label: "Cancel", diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index a9bd9315e80f..f27e06816e1d 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -1,7 +1,7 @@ import SwiftUI /// A data type that describes the state of an alert that can be shown to the user. -public enum AlertState: Hashable { +public enum AlertState { case dismissed case show(Alert) @@ -12,9 +12,9 @@ public enum AlertState: Hashable { public var title: String public init( - message: String?, + message: String? = nil, primaryButton: Button, - secondaryButton: Button?, + secondaryButton: Button? = nil, title: String ) { self.message = message @@ -47,6 +47,9 @@ public enum AlertState: Hashable { } } +extension AlertState: Equatable where Action: Equatable {} +extension AlertState: Hashable where Action: Hashable {} +extension AlertState.Alert: Equatable where Action: Equatable {} extension AlertState.Alert: Hashable where Action: Hashable {} extension AlertState.Alert.Button: Equatable where Action: Equatable {} extension AlertState.Alert.Button: Hashable where Action: Hashable {} From c78ba1ea76b4cefb21b75f37b2ed9a649cfd9d22 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 21 Jun 2020 12:55:12 -0500 Subject: [PATCH 06/29] wip --- ...ducers-ResuableOfflineDownloadsTests.swift | 77 +++++++++++-------- .../ComposableCoreLocation/Interface.swift | 2 +- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift index d881479b4d05..abb99c4cc068 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift @@ -20,7 +20,6 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { func testDownloadFlow() { let store = TestStore( initialState: DownloadComponentState( - alert: nil, id: 1, mode: .notDownloaded, url: URL(string: "https://www.pointfree.co")! @@ -57,7 +56,6 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { func testDownloadThrottling() { let store = TestStore( initialState: DownloadComponentState( - alert: nil, id: 1, mode: .notDownloaded, url: URL(string: "https://www.pointfree.co")! @@ -99,7 +97,6 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { func testCancelDownloadFlow() { let store = TestStore( initialState: DownloadComponentState( - alert: nil, id: 1, mode: .notDownloaded, url: URL(string: "https://www.pointfree.co")! @@ -120,19 +117,25 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.buttonTapped) { - $0.alert = DownloadAlert( - primaryButton: .init( - action: .alert(.cancelButtonTapped), label: "Cancel", type: .destructive - ), - secondaryButton: .init( - action: .alert(.nevermindButtonTapped), label: "Nevermind", type: .default - ), - title: "Do you want to cancel downloading this map?" + $0.alert = .show( + .init( + primaryButton: .init( + action: .cancelButtonTapped, + label: "Cancel", + type: .destructive + ), + secondaryButton: .init( + action: .nevermindButtonTapped, + label: "Nevermind", + type: .default + ), + title: "Do you want to cancel downloading this map?" + ) ) }, .send(.alert(.cancelButtonTapped)) { - $0.alert = nil + $0.alert = .dismissed $0.mode = .notDownloaded }, @@ -143,7 +146,6 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { func testDownloadFinishesWhileTryingToCancel() { let store = TestStore( initialState: DownloadComponentState( - alert: nil, id: 1, mode: .notDownloaded, url: URL(string: "https://www.pointfree.co")! @@ -164,14 +166,20 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.buttonTapped) { - $0.alert = DownloadAlert( - primaryButton: .init( - action: .alert(.cancelButtonTapped), label: "Cancel", type: .destructive - ), - secondaryButton: .init( - action: .alert(.nevermindButtonTapped), label: "Nevermind", type: .default - ), - title: "Do you want to cancel downloading this map?" + $0.alert = .show( + .init( + primaryButton: .init( + action: .cancelButtonTapped, + label: "Cancel", + type: .destructive + ), + secondaryButton: .init( + action: .nevermindButtonTapped, + label: "Nevermind", + type: .default + ), + title: "Do you want to cancel downloading this map?" + ) ) }, @@ -179,7 +187,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .do { self.downloadSubject.send(completion: .finished) }, .do { self.scheduler.advance(by: 1) }, .receive(.downloadClient(.success(.response(Data())))) { - $0.alert = nil + $0.alert = .dismissed $0.mode = .downloaded } ) @@ -188,7 +196,6 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { func testDeleteDownloadFlow() { let store = TestStore( initialState: DownloadComponentState( - alert: nil, id: 1, mode: .downloaded, url: URL(string: "https://www.pointfree.co")! @@ -205,19 +212,25 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { store.assert( .send(.buttonTapped) { - $0.alert = DownloadAlert( - primaryButton: .init( - action: .alert(.deleteButtonTapped), label: "Delete", type: .destructive - ), - secondaryButton: .init( - action: .alert(.nevermindButtonTapped), label: "Nevermind", type: .default - ), - title: "Do you want to delete this map from your offline storage?" + $0.alert = .show( + .init( + primaryButton: .init( + action: .deleteButtonTapped, + label: "Delete", + type: .destructive + ), + secondaryButton: .init( + action: .nevermindButtonTapped, + label: "Nevermind", + type: .default + ), + title: "Do you want to delete this map from your offline storage?" + ) ) }, .send(.alert(.deleteButtonTapped)) { - $0.alert = nil + $0.alert = .dismissed $0.mode = .notDownloaded } ) diff --git a/Sources/ComposableCoreLocation/Interface.swift b/Sources/ComposableCoreLocation/Interface.swift index 49fa48eae396..98de142bb6bb 100644 --- a/Sources/ComposableCoreLocation/Interface.swift +++ b/Sources/ComposableCoreLocation/Interface.swift @@ -175,7 +175,7 @@ public struct LocationManager { /// Actions that correspond to `CLLocationManagerDelegate` methods. /// /// See `CLLocationManagerDelegate` for more information. - public enum Action: Equatable, Hashable { + public enum Action: Equatable { case didChangeAuthorization(CLAuthorizationStatus) @available(tvOS, unavailable) From 6acc1a44fd630fa6b87b9a3a2bf5037650c93e66 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 27 Jun 2020 17:29:22 -0500 Subject: [PATCH 07/29] wip --- .../CaseStudies.xcodeproj/project.pbxproj | 4 + .../SwiftUICaseStudies/00-RootView.swift | 11 ++ ...GettingStarted-AlertsAndActionSheets.swift | 146 ++++++++++++++++++ .../SwiftUI/ActionSheet.swift | 115 ++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index 8940c7474653..a44bc0b45f20 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */; }; CAA9ADCA2446605B0003A984 /* 02-Effects-LongLiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */; }; CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */; }; + CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */; }; DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */; }; DC072322244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */; }; DC13940E2469E25C00EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC13940D2469E25C00EE1157 /* ComposableArchitecture */; }; @@ -140,6 +141,7 @@ CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-CancellationTests.swift"; sourceTree = ""; }; CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLiving.swift"; sourceTree = ""; }; CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLivingTests.swift"; sourceTree = ""; }; + CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheets.swift"; sourceTree = ""; }; DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-TimersTests.swift"; sourceTree = ""; }; DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Sheet-LoadThenPresent.swift"; sourceTree = ""; }; DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfLetStoreController.swift; sourceTree = ""; }; @@ -310,6 +312,7 @@ CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */, DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */, DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */, + CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */, DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */, CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */, CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */, @@ -586,6 +589,7 @@ DC89C41B24460F95006900B9 /* 00-RootView.swift in Sources */, DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */, DCC68EAB244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift in Sources */, + CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift in Sources */, CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */, DC88D8A6245341EC0077F427 /* 01-GettingStarted-Animations.swift in Sources */, DC89C44D244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift in Sources */, diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 27a8fc892bc0..489dc1a24b30 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -62,6 +62,17 @@ struct RootView: View { ) ) + NavigationLink( + "Alerts and Action Sheets", + destination: SharedStateView( + store: Store( + initialState: SharedState(), + reducer: sharedStateReducer, + environment: () + ) + ) + ) + NavigationLink( "Animations", destination: AnimationsView( diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift new file mode 100644 index 000000000000..49b28f45fa2b --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -0,0 +1,146 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This demonstrates how to best handle alerts and action sheets in the Composable Architecture. + + Because the library demands that all data flow through the application in a single direction, \ + we cannot leverage SwiftUI's two-way bindings since they allow making changes to state without \ + going through a reducer. This means we can't directly use the standard API for display alerts \ + and sheets. + + However, the library comes with two types, `AlertState` and `ActionSheetState`, which can be \ + constructed from reducers that control whether or not an alert or action sheet is displayed. \ + Further, it automatically handles sending actions when you tap their buttons, which allows you \ + to properly handle their functionality in the reducer rather than in two-way bindings and \ + action closures. + + The benefit of doing this is that you can get full test coverage on how a user interacts with \ + with alerts and action sheets in your application + """ + +struct AlertsAndActionSheetsState: Equatable { + var actionSheet = ActionSheetState.dismissed + var alert = AlertState.dismissed + var count = 0 +} + +enum AlertsAndActionSheetsAction: Equatable { + case actionSheetButtonTapped + case actionSheetCancelTapped + case alertButtonTapped + case alertCancelTapped + case decrementButtonTapped + case incrementButtonTapped +} + +struct AlertsAndActionSheetsEnvironment {} + +let alertsAndActionSheetsReducer = Reducer { state, action, _ in + + switch action { + case .actionSheetButtonTapped: + state.actionSheet = .show( + .init( + buttons: [ + .init( + action: .actionSheetCancelTapped, + label: "Cancel", + type: .cancel + ), + .init( + action: .incrementButtonTapped, + label: "Increment", + type: .default + ), + .init( + action: .decrementButtonTapped, + label: "Decrement", + type: .default + ), + ], + message: "This is an action sheet.", + title: "Action sheet" + ) + ) + return .none + + case .actionSheetCancelTapped: + state.actionSheet = .dismissed + return .none + + case .alertButtonTapped: + state.alert = .show( + .init( + message: "This is an alert", + primaryButton: .init( + action: .alertCancelTapped, + label: "Cancel", + type: .cancel + ), + secondaryButton: .init( + action: .incrementButtonTapped, + label: "Increment", + type: .default + ), + title: "Aert!" + ) + ) + return .none + + case .alertCancelTapped: + state.alert = .dismissed + return .none + + case .decrementButtonTapped: + state.count -= 1 + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + } +} + +struct AlertsAndActionSheetsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + Form { + Section(header: Text(template: readMe, .caption).textCase(.none)) { + Text("Count: \(viewStore.count)") + + Button("Alert") { viewStore.send(.alertButtonTapped) } + .alert( + viewStore.alert, + send: viewStore.send, + dismissal: .alertCancelTapped + ) + + Button("Action sheet") { viewStore.send(.actionSheetButtonTapped) } + .actionSheet( + viewStore.actionSheet, + send: viewStore.send, + dismissal: .actionSheetCancelTapped + ) + } + } + } + .navigationTitle("Alerts & Action Sheets") + } +} + +struct AlertsAndActionSheets_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AlertsAndActionSheetsView( + store: .init( + initialState: .init(), + reducer: alertsAndActionSheetsReducer, + environment: .init() + ) + ) + } + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift new file mode 100644 index 000000000000..c68bdec28c4a --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -0,0 +1,115 @@ +import SwiftUI + +public enum ActionSheetState { + case dismissed + case show(ActionSheet) + + public struct ActionSheet { + public var buttons: [Button] + public var message: String? + public var title: String + + public init( + buttons: [Button], + message: String?, + title: String + ) { + self.buttons = buttons + self.message = message + self.title = title + } + } + + public struct Button { + public var action: Action + public var label: String + public var type: `Type` + + public init( + action: Action, + label: String, + type: `Type` + ) { + self.action = action + self.label = label + self.type = type + } + + public enum `Type` { + case cancel + case `default` + case destructive + } + } +} + +extension ActionSheetState: Equatable where Action: Equatable {} +extension ActionSheetState: Hashable where Action: Hashable {} +extension ActionSheetState.ActionSheet: Equatable where Action: Equatable {} +extension ActionSheetState.ActionSheet: Hashable where Action: Hashable {} +extension ActionSheetState.Button: Equatable where Action: Equatable {} +extension ActionSheetState.Button: Hashable where Action: Hashable {} + +extension ActionSheetState.ActionSheet: Identifiable where Action: Hashable { + public var id: Self { self } +} + +extension View { + /// Displays an alert when `state` is in the `.show` state. + /// + /// - Parameters: + /// - state: A value that describes if the alert is shown or dismissed. + /// - send: A reference to the view store's `send` method for which actions from this alert + /// should be sent to. + /// - dismissal: An action to send when the alert is dismissed through non-user actions, such + /// as when an alert is automatically dismissed by the system. + public func actionSheet( + _ state: ActionSheetState, + send: @escaping (Action) -> Void, + dismissal: Action + ) -> some View where Action: Hashable { + + self.actionSheet( + item: Binding.ActionSheet?>( + get: { + switch state { + case .dismissed: + return nil + case let .show(alert): + return alert + } + }, + set: { + guard $0 == nil else { return } + send(dismissal) + }), + content: { $0.toSwiftUI(send: send) } + ) + } +} + +extension ActionSheetState.Button { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + switch self.type { + case .cancel: + return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } + case .default: + return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } + case .destructive: + return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } + } + } +} + +extension ActionSheetState.ActionSheet { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { + + SwiftUI.ActionSheet( + title: Text(self.title), + message: self.message.map { Text($0) }, + buttons: self.buttons.map { + $0.toSwiftUI(send: send) + } + ) + } +} From 6fba2452ac98468366cffd5d072d20b280353b27 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 27 Jun 2020 17:30:17 -0500 Subject: [PATCH 08/29] wip --- .../CaseStudies/SwiftUICaseStudies/00-RootView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 489dc1a24b30..4c70917d5732 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -64,11 +64,11 @@ struct RootView: View { NavigationLink( "Alerts and Action Sheets", - destination: SharedStateView( - store: Store( - initialState: SharedState(), - reducer: sharedStateReducer, - environment: () + destination: AlertsAndActionSheetsView( + store: .init( + initialState: .init(), + reducer: alertsAndActionSheetsReducer, + environment: .init() ) ) ) From 6b06e5a3a03fb591f0ae25a559c87c953d566a6f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 27 Jun 2020 18:18:23 -0500 Subject: [PATCH 09/29] wip --- .../SwiftUI/Alert.swift | 132 ++++++++++++++++-- 1 file changed, 120 insertions(+), 12 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index f27e06816e1d..90274299a7a0 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -1,6 +1,114 @@ import SwiftUI -/// A data type that describes the state of an alert that can be shown to the user. +/// A data type that describes the state of an alert that can be shown to the user. The `Action` +/// generic is the type of actions that can be sent from tapping on a button in the alert. +/// +/// This type can be used in your application's state in order to control the presentation or +/// dismissal of alerts. It is preferrable to use this API instead of the default SwiftUI API +/// for alerts because SwiftUI uses 2-way bindings in order to control the showing and dismissal +/// of alerts, and that does not play nicely with the Composable Architecture. The library requires +/// that all state mutations happen by sending an action so that a reducer can handle that logic, +/// which greatly simplifies how data flows through your application, and gives you instant +/// testability on all parts of your application. +/// +/// To use this API, you model all the alert actions in your domain's action enum: +/// +/// enum Action { +/// case cancelTapped +/// case confirmTapped +/// case deleteTapped +/// +/// // Your other actions +/// } +/// +/// And you model the state for showing the alert in your domain's state, and it can start off in +/// the `.dismissed` state: +/// +/// struct AppState { +/// var alert = AlertState.dismissed +/// // Your other state +/// } +/// +/// Then, in the reducer you can construct an `AlertState` value to represent the alert you want +/// to show to the user: +/// +/// let appReducer = Reducer { state, action, env in +/// switch action +/// case .cancelTapped: +/// state.alert = .dismissed +/// return .none +/// +/// case .confirmTapped: +/// state.alert = .dismissed +/// // Do deletion logic... +/// +/// case .deleteTapped: +/// state.alert = .show( +/// .init( +/// message: "Are you sure you want to delete this? It cannot be undone.", +/// primaryButton: .init( +/// action: .confirmTapped, +/// label: "Confirm", +/// type: .default +/// ), +/// secondaryButton: .init( +/// action: .cancelTapped, +/// label: "Cancel", +/// type: .cancel +/// ), +/// title: "Delete" +/// ) +/// ) +/// } +/// } +/// +/// And then, in your view you can use the new `.sheet(_:send:dismiss:)` method on `View` in order +/// to present the alert in a way that works best with the Composable Architecture: +/// +/// Button("Delete") { viewStore.send(.deleteTapped) } +/// .alert( +/// viewStore.alert, +/// send: viewStore.send, +/// dismiss: .cancelTapped +/// ) +/// +/// This makes your reducer in complete control of when the alert is shown or dismissed, and makes +/// it so that any choice made in the alert is automatically fed back into the reducer so that you +/// can handle its logic. +/// +/// Even better, you can instantly write tests that your alert-behavior works as expected: +/// +/// let store = TestStore( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: .mock +/// ) +/// +/// store.assert( +/// .send(.deleteTapped) { +/// $0.alert = .show( +/// .init( +/// message: "Are you sure you want to delete this? It cannot be undone.", +/// primaryButton: .init( +/// action: .confirmTapped, +/// label: "Confirm", +/// type: .default +/// ), +/// secondaryButton: .init( +/// action: .cancelTapped, +/// label: "Cancel", +/// type: .cancel +/// ), +/// title: "Delete" +/// ) +/// ) +/// }, +/// .send(.deleteTapped) { +/// $0.alert = .dismissed +/// // Also verify that delete logic executed correctly +/// } +/// ) +/// public enum AlertState { case dismissed case show(Alert) @@ -47,17 +155,6 @@ public enum AlertState { } } -extension AlertState: Equatable where Action: Equatable {} -extension AlertState: Hashable where Action: Hashable {} -extension AlertState.Alert: Equatable where Action: Equatable {} -extension AlertState.Alert: Hashable where Action: Hashable {} -extension AlertState.Alert.Button: Equatable where Action: Equatable {} -extension AlertState.Alert.Button: Hashable where Action: Hashable {} - -extension AlertState.Alert: Identifiable where Action: Hashable { - public var id: Self { self } -} - extension View { /// Displays an alert when `state` is in the `.show` state. /// @@ -92,6 +189,17 @@ extension View { } } +extension AlertState: Equatable where Action: Equatable {} +extension AlertState: Hashable where Action: Hashable {} +extension AlertState.Alert: Equatable where Action: Equatable {} +extension AlertState.Alert: Hashable where Action: Hashable {} +extension AlertState.Alert.Button: Equatable where Action: Equatable {} +extension AlertState.Alert.Button: Hashable where Action: Hashable {} + +extension AlertState.Alert: Identifiable where Action: Hashable { + public var id: Self { self } +} + extension AlertState.Alert.Button { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { switch self.type { From 3b89bdb6c9837e453604b5bed84ac5e95f22aa5f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 28 Jun 2020 10:07:28 -0500 Subject: [PATCH 10/29] format --- ...01-GettingStarted-AlertsAndActionSheets.swift | 16 +++++++++------- .../MotionManager/MotionManagerView.swift | 4 ++-- .../MotionManagerTests/MotionTests.swift | 2 +- Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift | 2 +- Makefile | 3 ++- .../SwiftUI/ActionSheet.swift | 4 ++-- .../ComposableArchitecture/SwiftUI/Alert.swift | 4 ++-- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 49b28f45fa2b..f3cd246953f2 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -36,7 +36,9 @@ enum AlertsAndActionSheetsAction: Equatable { struct AlertsAndActionSheetsEnvironment {} -let alertsAndActionSheetsReducer = Reducer { state, action, _ in +let alertsAndActionSheetsReducer = Reducer< + AlertsAndActionSheetsState, AlertsAndActionSheetsAction, AlertsAndActionSheetsEnvironment +> { state, action, _ in switch action { case .actionSheetButtonTapped: @@ -134,13 +136,13 @@ struct AlertsAndActionSheetsView: View { struct AlertsAndActionSheets_Previews: PreviewProvider { static var previews: some View { NavigationView { - AlertsAndActionSheetsView( - store: .init( - initialState: .init(), - reducer: alertsAndActionSheetsReducer, - environment: .init() + AlertsAndActionSheetsView( + store: .init( + initialState: .init(), + reducer: alertsAndActionSheetsReducer, + environment: .init() + ) ) - ) } } } diff --git a/Examples/MotionManager/MotionManager/MotionManagerView.swift b/Examples/MotionManager/MotionManager/MotionManagerView.swift index c24a4d7fb3f6..e034600fc5b0 100644 --- a/Examples/MotionManager/MotionManager/MotionManagerView.swift +++ b/Examples/MotionManager/MotionManager/MotionManagerView.swift @@ -178,8 +178,8 @@ struct AppView_Previews: PreviewProvider { // sends a bunch of data on some sine curves. var isStarted = false let mockMotionManager = MotionManager.mock( - create: { _ in .fireAndForget { } }, - destroy: { _ in .fireAndForget { } }, + create: { _ in .fireAndForget {} }, + destroy: { _ in .fireAndForget {} }, deviceMotion: { _ in nil }, startDeviceMotionUpdates: { _, _, _ in isStarted = true diff --git a/Examples/MotionManager/MotionManagerTests/MotionTests.swift b/Examples/MotionManager/MotionManagerTests/MotionTests.swift index b37d7a176859..c3d81dc668ed 100644 --- a/Examples/MotionManager/MotionManagerTests/MotionTests.swift +++ b/Examples/MotionManager/MotionManagerTests/MotionTests.swift @@ -106,7 +106,7 @@ class MotionTests: XCTestCase { $0.z = [0, 0] $0.facingDirection = .backward }, - + .send(.recordingButtonTapped) { $0.facingDirection = nil $0.initialAttitude = nil diff --git a/Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift b/Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift index fd118a1498ed..0caac04de224 100644 --- a/Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift +++ b/Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift @@ -54,7 +54,7 @@ let voiceMemosReducer = Reducer Date: Sun, 28 Jun 2020 10:29:58 -0500 Subject: [PATCH 11/29] clean up --- .../SwiftUICaseStudies/00-RootView.swift | 4 +- ...GettingStarted-AlertsAndActionSheets.swift | 28 ++--- .../SwiftUI/ActionSheet.swift | 103 ++++++++++++++---- .../SwiftUI/Alert.swift | 2 +- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 4c70917d5732..e4fb28111896 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -64,10 +64,10 @@ struct RootView: View { NavigationLink( "Alerts and Action Sheets", - destination: AlertsAndActionSheetsView( + destination: AlertAndSheetView( store: .init( initialState: .init(), - reducer: alertsAndActionSheetsReducer, + reducer: AlertAndSheetReducer, environment: .init() ) ) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index f3cd246953f2..3677327bb6df 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -19,13 +19,13 @@ private let readMe = """ with alerts and action sheets in your application """ -struct AlertsAndActionSheetsState: Equatable { - var actionSheet = ActionSheetState.dismissed - var alert = AlertState.dismissed +struct AlertAndSheetState: Equatable { + var actionSheet = ActionSheetState.dismissed + var alert = AlertState.dismissed var count = 0 } -enum AlertsAndActionSheetsAction: Equatable { +enum AlertAndSheetAction: Equatable { case actionSheetButtonTapped case actionSheetCancelTapped case alertButtonTapped @@ -34,10 +34,10 @@ enum AlertsAndActionSheetsAction: Equatable { case incrementButtonTapped } -struct AlertsAndActionSheetsEnvironment {} +struct AlertAndSheetEnvironment {} -let alertsAndActionSheetsReducer = Reducer< - AlertsAndActionSheetsState, AlertsAndActionSheetsAction, AlertsAndActionSheetsEnvironment +let AlertAndSheetReducer = Reducer< + AlertAndSheetState, AlertAndSheetAction, AlertAndSheetEnvironment > { state, action, _ in switch action { @@ -104,13 +104,13 @@ let alertsAndActionSheetsReducer = Reducer< } } -struct AlertsAndActionSheetsView: View { - let store: Store +struct AlertAndSheetView: View { + let store: Store var body: some View { WithViewStore(self.store) { viewStore in Form { - Section(header: Text(template: readMe, .caption).textCase(.none)) { + Section(header: Text(template: readMe, .caption)) { Text("Count: \(viewStore.count)") Button("Alert") { viewStore.send(.alertButtonTapped) } @@ -129,17 +129,17 @@ struct AlertsAndActionSheetsView: View { } } } - .navigationTitle("Alerts & Action Sheets") + .navigationBarTitle("Alerts & Action Sheets") } } -struct AlertsAndActionSheets_Previews: PreviewProvider { +struct AlertAndSheet_Previews: PreviewProvider { static var previews: some View { NavigationView { - AlertsAndActionSheetsView( + AlertAndSheetView( store: .init( initialState: .init(), - reducer: alertsAndActionSheetsReducer, + reducer: AlertAndSheetReducer, environment: .init() ) ) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 6c2b16c8786f..c6436355d77c 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -1,5 +1,11 @@ import SwiftUI +/// +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) public enum ActionSheetState { case dismissed case show(ActionSheet) @@ -18,38 +24,78 @@ public enum ActionSheetState { self.message = message self.title = title } - } - public struct Button { - public var action: Action - public var label: String - public var type: `Type` + public struct Button { + public var action: Action + public var label: String + public var type: `Type` - public init( - action: Action, - label: String, - type: `Type` - ) { - self.action = action - self.label = label - self.type = type - } + public init( + action: Action, + label: String, + type: `Type` + ) { + self.action = action + self.label = label + self.type = type + } - public enum `Type` { - case cancel - case `default` - case destructive + public enum `Type` { + case cancel + case `default` + case destructive + } } } } +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState: Equatable where Action: Equatable {} + +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState: Hashable where Action: Hashable {} + +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState.ActionSheet: Equatable where Action: Equatable {} + +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState.ActionSheet: Hashable where Action: Hashable {} -extension ActionSheetState.Button: Equatable where Action: Equatable {} -extension ActionSheetState.Button: Hashable where Action: Hashable {} +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ActionSheetState.ActionSheet.Button: Equatable where Action: Equatable {} + +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ActionSheetState.ActionSheet.Button: Hashable where Action: Hashable {} + +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState.ActionSheet: Identifiable where Action: Hashable { public var id: Self { self } } @@ -63,6 +109,11 @@ extension View { /// should be sent to. /// - dismissal: An action to send when the alert is dismissed through non-user actions, such /// as when an alert is automatically dismissed by the system. + @available(iOS 13, *) + @available(macCatalyst 13, *) + @available(macOS, unavailable) + @available(tvOS 13, *) + @available(watchOS 6, *) public func actionSheet( _ state: ActionSheetState, send: @escaping (Action) -> Void, @@ -88,7 +139,12 @@ extension View { } } -extension ActionSheetState.Button { +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ActionSheetState.ActionSheet.Button { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { switch self.type { case .cancel: @@ -101,6 +157,11 @@ extension ActionSheetState.Button { } } +@available(iOS 13, *) +@available(macCatalyst 13, *) +@available(macOS, unavailable) +@available(tvOS 13, *) +@available(watchOS 6, *) extension ActionSheetState.ActionSheet { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index c7c0d095eabb..47249f474edc 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -62,7 +62,7 @@ import SwiftUI /// } /// } /// -/// And then, in your view you can use the new `.sheet(_:send:dismiss:)` method on `View` in order +/// And then, in your view you can use the `.alert(_:send:dismiss:)` method on `View` in order /// to present the alert in a way that works best with the Composable Architecture: /// /// Button("Delete") { viewStore.send(.deleteTapped) } From a2ed3a655effc79d91ed37400841fcf7dccbfe65 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 28 Jun 2020 10:36:55 -0500 Subject: [PATCH 12/29] clean up --- .../01-GettingStarted-AlertsAndActionSheets.swift | 9 +++------ .../DownloadComponent.swift | 3 +-- ...gherOrderReducers-ResuableOfflineDownloadsTests.swift | 9 +++------ Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift | 4 ++-- Sources/ComposableArchitecture/SwiftUI/Alert.swift | 8 +++----- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 3677327bb6df..54d42263e960 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -52,13 +52,11 @@ let AlertAndSheetReducer = Reducer< ), .init( action: .incrementButtonTapped, - label: "Increment", - type: .default + label: "Increment" ), .init( action: .decrementButtonTapped, - label: "Decrement", - type: .default + label: "Decrement" ), ], message: "This is an action sheet.", @@ -82,8 +80,7 @@ let AlertAndSheetReducer = Reducer< ), secondaryButton: .init( action: .incrementButtonTapped, - label: "Increment", - type: .default + label: "Increment" ), title: "Aert!" ) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index fd46d0340fe9..d7b9c0ae4050 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -143,8 +143,7 @@ private let cancelAlert = AlertState.show( let nevermindButton = AlertState.Alert.Button( action: .nevermindButtonTapped, - label: "Nevermind", - type: .default + label: "Nevermind" ) struct DownloadComponent: View { diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift index abb99c4cc068..0d2dc77a2191 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift @@ -126,8 +126,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { ), secondaryButton: .init( action: .nevermindButtonTapped, - label: "Nevermind", - type: .default + label: "Nevermind" ), title: "Do you want to cancel downloading this map?" ) @@ -175,8 +174,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { ), secondaryButton: .init( action: .nevermindButtonTapped, - label: "Nevermind", - type: .default + label: "Nevermind" ), title: "Do you want to cancel downloading this map?" ) @@ -221,8 +219,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { ), secondaryButton: .init( action: .nevermindButtonTapped, - label: "Nevermind", - type: .default + label: "Nevermind" ), title: "Do you want to delete this map from your offline storage?" ) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index c6436355d77c..7dee74d30c61 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -17,7 +17,7 @@ public enum ActionSheetState { public init( buttons: [Button], - message: String?, + message: String? = nil, title: String ) { self.buttons = buttons @@ -33,7 +33,7 @@ public enum ActionSheetState { public init( action: Action, label: String, - type: `Type` + type: `Type` = .default ) { self.action = action self.label = label diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 47249f474edc..7546100c9264 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -48,8 +48,7 @@ import SwiftUI /// message: "Are you sure you want to delete this? It cannot be undone.", /// primaryButton: .init( /// action: .confirmTapped, -/// label: "Confirm", -/// type: .default +/// label: "Confirm" /// ), /// secondaryButton: .init( /// action: .cancelTapped, @@ -91,8 +90,7 @@ import SwiftUI /// message: "Are you sure you want to delete this? It cannot be undone.", /// primaryButton: .init( /// action: .confirmTapped, -/// label: "Confirm", -/// type: .default +/// label: "Confirm" /// ), /// secondaryButton: .init( /// action: .cancelTapped, @@ -139,7 +137,7 @@ public enum AlertState { public init( action: Action, label: String, - type: `Type` + type: `Type` = .default ) { self.action = action self.label = label From daf0c24c03c495713f9353942bbe288462eebada Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 28 Jun 2020 10:51:10 -0500 Subject: [PATCH 13/29] docs --- .../SwiftUI/ActionSheet.swift | 148 ++++++++++++++++-- .../SwiftUI/Alert.swift | 13 +- 2 files changed, 142 insertions(+), 19 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 7dee74d30c61..3c44761fdbd0 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -1,6 +1,128 @@ import SwiftUI -/// +/// A data type that describes the state of an action sheet that can be shown to the user. The +/// `Action` generic is the type of actions that can be sent from tapping on a button in the sheet. +/// +/// This type can be used in your application's state in order to control the presentation or +/// dismissal of action sheets. It is preferrable to use this API instead of the default SwiftUI API +/// for action sheets because SwiftUI uses 2-way bindings in order to control the showing and +/// dismissal of sheets, and that does not play nicely with the Composable Architecture. The library +/// requires that all state mutations happen by sending an action so that a reducer can handle that +/// logic, which greatly simplifies how data flows through your application, and gives you instant +/// testability on all parts of your application. +/// +/// To use this API, you model all the action sheet actions in your domain's action enum: +/// +/// enum Action { +/// case cancelTapped +/// case deleteTapped +/// case favoriteTapped +/// case infoTapped +/// +/// // Your other actions +/// } +/// +/// And you model the state for showing the action sheet in your domain's state, and it can start +/// off in the `.dismissed` state: +/// +/// struct AppState { +/// var actionSheet = ActionSheetState.dismissed +/// // Your other state +/// } +/// +/// Then, in the reducer you can construct an `ActionSheetState` value to represent the action +/// sheet you want to show to the user: +/// +/// let appReducer = Reducer { state, action, env in +/// switch action +/// case .cancelTapped: +/// state.actionSheet = .dismissed +/// return .none +/// +/// case .deleteTapped: +/// state.actionSheet = .dismissed +/// // Do deletion logic... +/// +/// case .favoriteTapped: +/// state.actionSheet = .dismissed +/// // Do favoriting logic +/// +/// case .infoTapped: +/// state.actionSheet = .show( +/// .init( +/// buttons: [ +/// .init( +/// action: .favoriteTapped, +/// label: "Favorite" +/// ), +/// .init( +/// action: .deleteTapped, +/// label: "Delete" +/// ), +/// .init( +/// action: .cancelTapped, +/// label: "Cancel", +/// type: .cancel +/// ) +/// ], +/// title: "What would you like to do?" +/// ) +/// ) +/// return .none +/// } +/// } +/// +/// And then, in your view you can use the `.actionSheet(_:send:dismiss:)` method on `View` in order +/// to present the action sheet in a way that works best with the Composable Architecture: +/// +/// Button("Info") { viewStore.send(.infoTapped) } +/// .actionSheet( +/// viewStore.actionSheet, +/// send: viewStore.send, +/// dismiss: .cancelTapped +/// ) +/// +/// This makes your reducer in complete control of when the action sheet is shown or dismissed, and +/// makes it so that any choice made in the action sheet is automatically fed back into the reducer +/// so that you can handle its logic. +/// +/// Even better, you can instantly write tests that your action sheet behavior works as expected: +/// +/// let store = TestStore( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: .mock +/// ) +/// +/// store.assert( +/// .send(.infoTapped) { +/// $0.actionSheet = .show( +/// .init( +/// buttons: [ +/// .init( +/// action: .favoriteTapped, +/// label: "Favorite" +/// ), +/// .init( +/// action: .deleteTapped, +/// label: "Delete" +/// ), +/// .init( +/// action: .cancelTapped, +/// label: "Cancel", +/// type: .cancel +/// ) +/// ], +/// title: "What would you like to do?" +/// ) +/// ) +/// }, +/// .send(.favoriteTapped) { +/// $0.actionSheet = .dismissed +/// // Also verify that favoriting logic executed correctly +/// } +/// ) +/// @available(iOS 13, *) @available(macCatalyst 13, *) @available(macOS, unavailable) @@ -101,14 +223,14 @@ extension ActionSheetState.ActionSheet: Identifiable where Action: Hashable { } extension View { - /// Displays an alert when `state` is in the `.show` state. + /// Displays an action sheet when `state` is in the `.show` state. /// /// - Parameters: - /// - state: A value that describes if the alert is shown or dismissed. - /// - send: A reference to the view store's `send` method for which actions from this alert - /// should be sent to. - /// - dismissal: An action to send when the alert is dismissed through non-user actions, such - /// as when an alert is automatically dismissed by the system. + /// - state: A value that describes if the action sheet is shown or dismissed. + /// - send: A reference to the view store's `send` method for which actions from this action + /// sheet should be sent to. + /// - dismissal: An action to send when the action sheet is dismissed through non-user actions, + /// such as when an action sheet is automatically dismissed by the system. @available(iOS 13, *) @available(macCatalyst 13, *) @available(macOS, unavailable) @@ -126,8 +248,8 @@ extension View { switch state { case .dismissed: return nil - case let .show(alert): - return alert + case let .show(actionSheet): + return actionSheet } }, set: { @@ -145,14 +267,14 @@ extension View { @available(tvOS 13, *) @available(watchOS 6, *) extension ActionSheetState.ActionSheet.Button { - fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet.Button { switch self.type { case .cancel: - return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } + return .cancel(Text(self.label)) { send(self.action) } case .default: - return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } + return .default(Text(self.label)) { send(self.action) } case .destructive: - return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } + return .destructive(Text(self.label)) { send(self.action) } } } } diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 7546100c9264..bee5eccbf57a 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -39,8 +39,8 @@ import SwiftUI /// return .none /// /// case .confirmTapped: -/// state.alert = .dismissed -/// // Do deletion logic... +/// state.alert = .dismissed +/// // Do deletion logic... /// /// case .deleteTapped: /// state.alert = .show( @@ -58,6 +58,7 @@ import SwiftUI /// title: "Delete" /// ) /// ) +/// return .none /// } /// } /// @@ -75,7 +76,7 @@ import SwiftUI /// it so that any choice made in the alert is automatically fed back into the reducer so that you /// can handle its logic. /// -/// Even better, you can instantly write tests that your alert-behavior works as expected: +/// Even better, you can instantly write tests that your alert behavior works as expected: /// /// let store = TestStore( /// initialState: AppState(), @@ -202,11 +203,11 @@ extension AlertState.Alert.Button { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { switch self.type { case .cancel: - return SwiftUI.Alert.Button.cancel(Text(self.label)) { send(self.action) } + return .cancel(Text(self.label)) { send(self.action) } case .default: - return SwiftUI.Alert.Button.default(Text(self.label)) { send(self.action) } + return .default(Text(self.label)) { send(self.action) } case .destructive: - return SwiftUI.Alert.Button.destructive(Text(self.label)) { send(self.action) } + return .destructive(Text(self.label)) { send(self.action) } } } } From cda140f60097790198652780994365216f5f2fde Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 28 Jun 2020 10:51:49 -0500 Subject: [PATCH 14/29] wip --- Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 3c44761fdbd0..bdd8331eadba 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -286,7 +286,6 @@ extension ActionSheetState.ActionSheet.Button { @available(watchOS 6, *) extension ActionSheetState.ActionSheet { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { - SwiftUI.ActionSheet( title: Text(self.title), message: self.message.map { Text($0) }, From 4cde44730fce148bf171323397caa3b9d954bac2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 28 Jun 2020 11:51:40 -0500 Subject: [PATCH 15/29] tests --- .../CaseStudies.xcodeproj/project.pbxproj | 8 +- ...GettingStarted-AlertsAndActionSheets.swift | 5 +- ...ingStarts-AlertsAndActionSheetsTests.swift | 78 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index a44bc0b45f20..32fe108cdc3f 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ CA27C0B7245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */; }; CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; }; CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; }; + CA50BE6024A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */; }; CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */; }; CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */; }; CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */; }; @@ -130,6 +131,7 @@ CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-SystemEnvironment.swift"; sourceTree = ""; }; CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = ""; }; CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = ""; }; + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarts-AlertsAndActionSheetsTests.swift"; sourceTree = ""; }; CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReusableComponents-Download.swift"; sourceTree = ""; }; CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = ""; }; @@ -342,13 +344,14 @@ DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */ = { isa = PBXGroup; children = ( + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */, + CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */, CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */, CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */, - CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */, DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */, CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */, - DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */, CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift */, + DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */, ); path = SwiftUICaseStudiesTests; sourceTree = ""; @@ -613,6 +616,7 @@ CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */, CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */, DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */, + CA50BE6024A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift in Sources */, CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */, CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */, ); diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 54d42263e960..cefba6575e33 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -82,7 +82,7 @@ let AlertAndSheetReducer = Reducer< action: .incrementButtonTapped, label: "Increment" ), - title: "Aert!" + title: "Alert!" ) ) return .none @@ -92,10 +92,13 @@ let AlertAndSheetReducer = Reducer< return .none case .decrementButtonTapped: + state.actionSheet = .dismissed state.count -= 1 return .none case .incrementButtonTapped: + state.actionSheet = .dismissed + state.alert = .dismissed state.count += 1 return .none } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift new file mode 100644 index 000000000000..c61deabee3ae --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift @@ -0,0 +1,78 @@ +import Combine +import ComposableArchitecture +import SwiftUI +import XCTest + +@testable import SwiftUICaseStudies + +class AlertsAndActionSheetsTests: XCTestCase { + func testAlert() { + let store = TestStore( + initialState: AlertAndSheetState(), + reducer: AlertAndSheetReducer, + environment: AlertAndSheetEnvironment() + ) + + store.assert( + .send(.alertButtonTapped) { + $0.alert = .show( + .init( + message: "This is an alert", + primaryButton: .init( + action: .alertCancelTapped, + label: "Cancel", + type: .cancel + ), + secondaryButton: .init( + action: .incrementButtonTapped, + label: "Increment" + ), + title: "Alert!" + ) + ) + }, + .send(.incrementButtonTapped) { + $0.alert = .dismissed + $0.count = 1 + } + ) + } + + func testActionSheet() { + let store = TestStore( + initialState: AlertAndSheetState(), + reducer: AlertAndSheetReducer, + environment: AlertAndSheetEnvironment() + ) + + store.assert( + .send(.actionSheetButtonTapped) { + $0.actionSheet = .show( + .init( + buttons: [ + .init( + action: .actionSheetCancelTapped, + label: "Cancel", + type: .cancel + ), + .init( + action: .incrementButtonTapped, + label: "Increment" + ), + .init( + action: .decrementButtonTapped, + label: "Decrement" + ), + ], + message: "This is an action sheet.", + title: "Action sheet" + ) + ) + }, + .send(.incrementButtonTapped) { + $0.actionSheet = .dismissed + $0.count = 1 + } + ) + } +} From 9e3a2bd01d6a1f5c757c06c08e7da88dd554c654 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 10:17:45 -0400 Subject: [PATCH 16/29] API tweaks --- ...GettingStarted-AlertsAndActionSheets.swift | 22 ++++++++---------- .../DownloadComponent.swift | 5 ++-- .../SwiftUI/ActionSheet.swift | 23 ++++++++----------- .../SwiftUI/Alert.swift | 20 ++++++++-------- 4 files changed, 31 insertions(+), 39 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index cefba6575e33..8327e5af6eb9 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -5,15 +5,15 @@ private let readMe = """ This demonstrates how to best handle alerts and action sheets in the Composable Architecture. Because the library demands that all data flow through the application in a single direction, \ - we cannot leverage SwiftUI's two-way bindings since they allow making changes to state without \ - going through a reducer. This means we can't directly use the standard API for display alerts \ - and sheets. + we cannot leverage SwiftUI's two-way bindings because they can make changes to state without \ + going through a reducer. This means we can't directly use the standard API to display alerts and \ + sheets. However, the library comes with two types, `AlertState` and `ActionSheetState`, which can be \ - constructed from reducers that control whether or not an alert or action sheet is displayed. \ + constructed from reducers and control whether or not an alert or action sheet is displayed. \ Further, it automatically handles sending actions when you tap their buttons, which allows you \ - to properly handle their functionality in the reducer rather than in two-way bindings and \ - action closures. + to properly handle their functionality in the reducer rather than in two-way bindings and action \ + closures. The benefit of doing this is that you can get full test coverage on how a user interacts with \ with alerts and action sheets in your application @@ -115,16 +115,14 @@ struct AlertAndSheetView: View { Button("Alert") { viewStore.send(.alertButtonTapped) } .alert( - viewStore.alert, - send: viewStore.send, - dismissal: .alertCancelTapped + self.store.scope(state: { $0.alert }), + dismiss: .alertCancelTapped ) Button("Action sheet") { viewStore.send(.actionSheetButtonTapped) } .actionSheet( - viewStore.actionSheet, - send: viewStore.send, - dismissal: .actionSheetCancelTapped + self.store.scope(state: { $0.actionSheet }), + dismiss: .actionSheetCancelTapped ) } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index d7b9c0ae4050..a85500c856ed 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -178,9 +178,8 @@ struct DownloadComponent: View { } } .alert( - viewStore.alert, - send: { viewStore.send(.alert($0)) }, - dismissal: .dismiss + self.store.scope(state: { $0.alert }, action: DownloadComponentAction.alert), + dismiss: .dismiss ) } } diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index bdd8331eadba..1ca8deb904b4 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -77,8 +77,7 @@ import SwiftUI /// /// Button("Info") { viewStore.send(.infoTapped) } /// .actionSheet( -/// viewStore.actionSheet, -/// send: viewStore.send, +/// self.store.scope(state: { $0.actionSheet }), /// dismiss: .cancelTapped /// ) /// @@ -226,26 +225,24 @@ extension View { /// Displays an action sheet when `state` is in the `.show` state. /// /// - Parameters: - /// - state: A value that describes if the action sheet is shown or dismissed. - /// - send: A reference to the view store's `send` method for which actions from this action - /// sheet should be sent to. + /// - store: A store that describes if the action sheet is shown or dismissed. /// - dismissal: An action to send when the action sheet is dismissed through non-user actions, - /// such as when an action sheet is automatically dismissed by the system. + /// such as when an action sheet is automatically dismissed by the system. @available(iOS 13, *) @available(macCatalyst 13, *) @available(macOS, unavailable) @available(tvOS 13, *) @available(watchOS 6, *) public func actionSheet( - _ state: ActionSheetState, - send: @escaping (Action) -> Void, - dismissal: Action + _ store: Store, Action>, + dismiss: Action ) -> some View where Action: Hashable { - self.actionSheet( + let viewStore = ViewStore(store) + return self.actionSheet( item: Binding.ActionSheet?>( get: { - switch state { + switch viewStore.state { case .dismissed: return nil case let .show(actionSheet): @@ -254,9 +251,9 @@ extension View { }, set: { guard $0 == nil else { return } - send(dismissal) + viewStore.send(dismiss) }), - content: { $0.toSwiftUI(send: send) } + content: { $0.toSwiftUI(send: viewStore.send) } ) } } diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index bee5eccbf57a..ef73fa258771 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -158,21 +158,19 @@ extension View { /// Displays an alert when `state` is in the `.show` state. /// /// - Parameters: - /// - state: A value that describes if the alert is shown or dismissed. - /// - send: A reference to the view store's `send` method for which actions from this alert - /// should be sent to. + /// - store: A store that describes if the alert is shown or dismissed. /// - dismissal: An action to send when the alert is dismissed through non-user actions, such - /// as when an alert is automatically dismissed by the system. + /// as when an alert is automatically dismissed by the system. public func alert( - _ state: AlertState, - send: @escaping (Action) -> Void, - dismissal: Action + _ store: Store, Action>, + dismiss: Action ) -> some View where Action: Hashable { - self.alert( + let viewStore = ViewStore(store) + return self.alert( item: Binding.Alert?>( get: { - switch state { + switch viewStore.state { case .dismissed: return nil case let .show(alert): @@ -181,9 +179,9 @@ extension View { }, set: { guard $0 == nil else { return } - send(dismissal) + viewStore.send(dismiss) }), - content: { $0.toSwiftUI(send: send) } + content: { $0.toSwiftUI(send: viewStore.send) } ) } } From 64b833f30c69add939d17a0aee56a7e5bf306723 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 10:20:44 -0400 Subject: [PATCH 17/29] Fix --- .github/workflows/format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 80e808bc6cfe..487f11fb35a9 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -18,6 +18,6 @@ jobs: - uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Run swift-format - branch: 'master' + branch: 'main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a78bbed61b3967ad76ba6a2766e46589ebe86523 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 11:34:57 -0400 Subject: [PATCH 18/29] More API changes --- .../CaseStudies.xcodeproj/project.pbxproj | 18 +++---- ...GettingStarted-AlertsAndActionSheets.swift | 17 +++---- .../DownloadComponent.swift | 28 +++++------ ...gStarted-AlertsAndActionSheetsTests.swift} | 0 ...ucers-ReusableOfflineDownloadsTests.swift} | 0 .../SwiftUI/Alert.swift | 49 +++++++++++++------ 6 files changed, 65 insertions(+), 47 deletions(-) rename Examples/CaseStudies/SwiftUICaseStudiesTests/{01-GettingStarts-AlertsAndActionSheetsTests.swift => 01-GettingStarted-AlertsAndActionSheetsTests.swift} (100%) rename Examples/CaseStudies/SwiftUICaseStudiesTests/{04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift => 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift} (100%) diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj index d3b820e3d14b..091e1b681c67 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -7,13 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift */; }; + CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */; }; CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */; }; CA27C0B7245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */; }; CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */; }; CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; }; CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; }; - CA50BE6024A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */; }; + CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */; }; CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */; }; CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */; }; CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */; }; @@ -127,13 +127,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift"; sourceTree = ""; }; + CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift"; sourceTree = ""; }; CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Basics.swift"; sourceTree = ""; }; CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-SystemEnvironment.swift"; sourceTree = ""; }; CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AnimationsTests.swift"; sourceTree = ""; }; CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = ""; }; CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = ""; }; - CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarts-AlertsAndActionSheetsTests.swift"; sourceTree = ""; }; + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheetsTests.swift"; sourceTree = ""; }; CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReusableComponents-Download.swift"; sourceTree = ""; }; CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = ""; }; @@ -312,11 +312,11 @@ children = ( DC89C42424460F96006900B9 /* Info.plist */, DC89C41A24460F95006900B9 /* 00-RootView.swift */, + CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */, DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */, CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */, DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */, DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */, - CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */, DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */, CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */, CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */, @@ -346,15 +346,15 @@ DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */ = { isa = PBXGroup; children = ( + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */, CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */, - CA50BE5F24A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift */, CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */, CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */, CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */, DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */, CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */, - CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift */, DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */, + CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */, ); path = SwiftUICaseStudiesTests; sourceTree = ""; @@ -614,13 +614,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift in Sources */, + CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */, DC634B252448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift in Sources */, CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */, CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */, CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */, DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */, - CA50BE6024A8F46500FE7DBA /* 01-GettingStarts-AlertsAndActionSheetsTests.swift in Sources */, + CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */, CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */, CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */, ); diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 8327e5af6eb9..af7faa4fb1d0 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -72,17 +72,16 @@ let AlertAndSheetReducer = Reducer< case .alertButtonTapped: state.alert = .show( .init( + title: "Alert!", message: "This is an alert", - primaryButton: .init( - action: .alertCancelTapped, - label: "Cancel", - type: .cancel + primaryButton: .cancel( + "Cancel", + send: .alertCancelTapped ), - secondaryButton: .init( - action: .incrementButtonTapped, - label: "Increment" - ), - title: "Alert!" + secondaryButton: .default( + "Increment", + send: .incrementButtonTapped + ) ) ) return .none diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index a85500c856ed..7b0c73d84328 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -119,31 +119,29 @@ extension Reducer { private let deleteAlert = AlertState.show( .init( - primaryButton: .init( - action: .deleteButtonTapped, - label: "Delete", - type: .destructive + title: "Do you want to delete this map from your offline storage?", + primaryButton: .destructive( + "Delete", + send: .deleteButtonTapped ), - secondaryButton: nevermindButton, - title: "Do you want to delete this map from your offline storage?" + secondaryButton: nevermindButton ) ) private let cancelAlert = AlertState.show( .init( - primaryButton: .init( - action: .cancelButtonTapped, - label: "Cancel", - type: .destructive + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel( + "Cancel", + send: .cancelButtonTapped ), - secondaryButton: nevermindButton, - title: "Do you want to cancel downloading this map?" + secondaryButton: nevermindButton ) ) -let nevermindButton = AlertState.Alert.Button( - action: .nevermindButtonTapped, - label: "Nevermind" +let nevermindButton = AlertState.Alert.Button.default( + "Nevermind", + send: .nevermindButtonTapped ) struct DownloadComponent: View { diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift similarity index 100% rename from Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarts-AlertsAndActionSheetsTests.swift rename to Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift similarity index 100% rename from Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift rename to Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index ef73fa258771..200aba5187cc 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -114,15 +114,25 @@ public enum AlertState { public struct Alert { public var message: String? - public var primaryButton: Button + public var primaryButton: Button? public var secondaryButton: Button? public var title: String public init( + title: String, + message: String? = nil, + dismissButton: Button? = nil + ) { + self.message = message + self.primaryButton = dismissButton + self.title = title + } + + public init( + title: String, message: String? = nil, primaryButton: Button, - secondaryButton: Button? = nil, - title: String + secondaryButton: Button ) { self.message = message self.primaryButton = primaryButton @@ -135,14 +145,25 @@ public enum AlertState { public var label: String public var type: `Type` - public init( - action: Action, - label: String, - type: `Type` = .default - ) { - self.action = action - self.label = label - self.type = type + public static func cancel( + _ label: String, + send action: Action + ) -> Self { + Self(action: action, label: label, type: .cancel) + } + + public static func `default`( + _ label: String, + send action: Action + ) -> Self { + Self(action: action, label: label, type: .default) + } + + public static func destructive( + _ label: String, + send action: Action + ) -> Self { + Self(action: action, label: label, type: .destructive) } public enum `Type` { @@ -215,18 +236,18 @@ extension AlertState.Alert { let title = Text(self.title) let message = self.message.map { Text($0) } - if let secondaryButton = self.secondaryButton { + if let primaryButton = self.primaryButton, let secondaryButton = self.secondaryButton { return SwiftUI.Alert( title: title, message: message, - primaryButton: self.primaryButton.toSwiftUI(send: send), + primaryButton: primaryButton.toSwiftUI(send: send), secondaryButton: secondaryButton.toSwiftUI(send: send) ) } else { return SwiftUI.Alert( title: title, message: message, - dismissButton: self.primaryButton.toSwiftUI(send: send) + dismissButton: self.primaryButton?.toSwiftUI(send: send) ) } } From 0a1643373a55651ad71144ea37fc4c4020e998c5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 12:10:08 -0400 Subject: [PATCH 19/29] More API changes --- ...GettingStarted-AlertsAndActionSheets.swift | 32 ++------ .../DownloadComponent.swift | 16 +--- ...ngStarted-AlertsAndActionSheetsTests.swift | 35 +++------ ...ducers-ReusableOfflineDownloadsTests.swift | 39 +++------- .../SwiftUI/ActionSheet.swift | 74 ++---------------- .../SwiftUI/Alert.swift | 75 +++++++++---------- 6 files changed, 70 insertions(+), 201 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index af7faa4fb1d0..8f399bf5c51f 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -44,23 +44,13 @@ let AlertAndSheetReducer = Reducer< case .actionSheetButtonTapped: state.actionSheet = .show( .init( - buttons: [ - .init( - action: .actionSheetCancelTapped, - label: "Cancel", - type: .cancel - ), - .init( - action: .incrementButtonTapped, - label: "Increment" - ), - .init( - action: .decrementButtonTapped, - label: "Decrement" - ), - ], + title: "Action sheet", message: "This is an action sheet.", - title: "Action sheet" + buttons: [ + .cancel(), + .default("Increment", send: .incrementButtonTapped), + .default("Decrement", send: .decrementButtonTapped), + ] ) ) return .none @@ -74,14 +64,8 @@ let AlertAndSheetReducer = Reducer< .init( title: "Alert!", message: "This is an alert", - primaryButton: .cancel( - "Cancel", - send: .alertCancelTapped - ), - secondaryButton: .default( - "Increment", - send: .incrementButtonTapped - ) + primaryButton: .cancel(), + secondaryButton: .default("Increment", send: .incrementButtonTapped) ) ) return .none diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 7b0c73d84328..0340a923fc63 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -120,10 +120,7 @@ extension Reducer { private let deleteAlert = AlertState.show( .init( title: "Do you want to delete this map from your offline storage?", - primaryButton: .destructive( - "Delete", - send: .deleteButtonTapped - ), + primaryButton: .destructive("Delete", send: .deleteButtonTapped), secondaryButton: nevermindButton ) ) @@ -131,18 +128,13 @@ private let deleteAlert = AlertState.show( private let cancelAlert = AlertState.show( .init( title: "Do you want to cancel downloading this map?", - primaryButton: .cancel( - "Cancel", - send: .cancelButtonTapped - ), + primaryButton: .cancel(send: .cancelButtonTapped), secondaryButton: nevermindButton ) ) -let nevermindButton = AlertState.Alert.Button.default( - "Nevermind", - send: .nevermindButtonTapped -) +let nevermindButton = AlertState.Alert.Button + .default("Nevermind", send: .nevermindButtonTapped) struct DownloadComponent: View { let store: Store, DownloadComponentAction> diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift index c61deabee3ae..ca9c49bb9a0a 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift @@ -17,17 +17,10 @@ class AlertsAndActionSheetsTests: XCTestCase { .send(.alertButtonTapped) { $0.alert = .show( .init( + title: "Alert!", message: "This is an alert", - primaryButton: .init( - action: .alertCancelTapped, - label: "Cancel", - type: .cancel - ), - secondaryButton: .init( - action: .incrementButtonTapped, - label: "Increment" - ), - title: "Alert!" + primaryButton: .cancel(), + secondaryButton: .default("Increment", send: .incrementButtonTapped) ) ) }, @@ -49,23 +42,13 @@ class AlertsAndActionSheetsTests: XCTestCase { .send(.actionSheetButtonTapped) { $0.actionSheet = .show( .init( - buttons: [ - .init( - action: .actionSheetCancelTapped, - label: "Cancel", - type: .cancel - ), - .init( - action: .incrementButtonTapped, - label: "Increment" - ), - .init( - action: .decrementButtonTapped, - label: "Decrement" - ), - ], + title: "Action sheet", message: "This is an action sheet.", - title: "Action sheet" + buttons: [ + .cancel(), + .default("Increment", send: .incrementButtonTapped), + .default("Decrement", send: .decrementButtonTapped), + ] ) ) }, diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift index 0d2dc77a2191..c84ed7671119 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift @@ -119,16 +119,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .send(.buttonTapped) { $0.alert = .show( .init( - primaryButton: .init( - action: .cancelButtonTapped, - label: "Cancel", - type: .destructive - ), - secondaryButton: .init( - action: .nevermindButtonTapped, - label: "Nevermind" - ), - title: "Do you want to cancel downloading this map?" + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel(send: .cancelButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) ) }, @@ -167,16 +160,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .send(.buttonTapped) { $0.alert = .show( .init( - primaryButton: .init( - action: .cancelButtonTapped, - label: "Cancel", - type: .destructive - ), - secondaryButton: .init( - action: .nevermindButtonTapped, - label: "Nevermind" - ), - title: "Do you want to cancel downloading this map?" + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel(send: .cancelButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) ) }, @@ -212,16 +198,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .send(.buttonTapped) { $0.alert = .show( .init( - primaryButton: .init( - action: .deleteButtonTapped, - label: "Delete", - type: .destructive - ), - secondaryButton: .init( - action: .nevermindButtonTapped, - label: "Nevermind" - ), - title: "Do you want to delete this map from your offline storage?" + title: "Do you want to delete this map from your offline storage?", + primaryButton: .destructive("Delete", send: .deleteButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) ) }, diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 1ca8deb904b4..335fc20b9a71 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -51,19 +51,9 @@ import SwiftUI /// state.actionSheet = .show( /// .init( /// buttons: [ -/// .init( -/// action: .favoriteTapped, -/// label: "Favorite" -/// ), -/// .init( -/// action: .deleteTapped, -/// label: "Delete" -/// ), -/// .init( -/// action: .cancelTapped, -/// label: "Cancel", -/// type: .cancel -/// ) +/// .default("Favorite", send: .favoriteTapped), +/// .destructive("Delete", send: .deleteTapped), +/// .cancel(send: .cancelTapped), /// ], /// title: "What would you like to do?" /// ) @@ -137,36 +127,16 @@ public enum ActionSheetState { public var title: String public init( - buttons: [Button], + title: String, message: String? = nil, - title: String + buttons: [Button] ) { self.buttons = buttons self.message = message self.title = title } - public struct Button { - public var action: Action - public var label: String - public var type: `Type` - - public init( - action: Action, - label: String, - type: `Type` = .default - ) { - self.action = action - self.label = label - self.type = type - } - - public enum `Type` { - case cancel - case `default` - case destructive - } - } + public typealias Button = AlertState.Alert.Button } } @@ -198,20 +168,6 @@ extension ActionSheetState.ActionSheet: Equatable where Action: Equatable {} @available(watchOS 6, *) extension ActionSheetState.ActionSheet: Hashable where Action: Hashable {} -@available(iOS 13, *) -@available(macCatalyst 13, *) -@available(macOS, unavailable) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ActionSheetState.ActionSheet.Button: Equatable where Action: Equatable {} - -@available(iOS 13, *) -@available(macCatalyst 13, *) -@available(macOS, unavailable) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ActionSheetState.ActionSheet.Button: Hashable where Action: Hashable {} - @available(iOS 13, *) @available(macCatalyst 13, *) @available(macOS, unavailable) @@ -258,24 +214,6 @@ extension View { } } -@available(iOS 13, *) -@available(macCatalyst 13, *) -@available(macOS, unavailable) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ActionSheetState.ActionSheet.Button { - fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet.Button { - switch self.type { - case .cancel: - return .cancel(Text(self.label)) { send(self.action) } - case .default: - return .default(Text(self.label)) { send(self.action) } - case .destructive: - return .destructive(Text(self.label)) { send(self.action) } - } - } -} - @available(iOS 13, *) @available(macCatalyst 13, *) @available(macOS, unavailable) diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 200aba5187cc..c27b26cfdd92 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -45,17 +45,10 @@ import SwiftUI /// case .deleteTapped: /// state.alert = .show( /// .init( +/// title: "Delete", /// message: "Are you sure you want to delete this? It cannot be undone.", -/// primaryButton: .init( -/// action: .confirmTapped, -/// label: "Confirm" -/// ), -/// secondaryButton: .init( -/// action: .cancelTapped, -/// label: "Cancel", -/// type: .cancel -/// ), -/// title: "Delete" +/// primaryButton: .default("Confirm", send: .confirmTapped), +/// secondaryButton: .cancel(send: .cancelTapped) /// ) /// ) /// return .none @@ -67,8 +60,7 @@ import SwiftUI /// /// Button("Delete") { viewStore.send(.deleteTapped) } /// .alert( -/// viewStore.alert, -/// send: viewStore.send, +/// viewStore.scope(state: \.alert), /// dismiss: .cancelTapped /// ) /// @@ -88,17 +80,10 @@ import SwiftUI /// .send(.deleteTapped) { /// $0.alert = .show( /// .init( +/// title: "Delete", /// message: "Are you sure you want to delete this? It cannot be undone.", -/// primaryButton: .init( -/// action: .confirmTapped, -/// label: "Confirm" -/// ), -/// secondaryButton: .init( -/// action: .cancelTapped, -/// label: "Cancel", -/// type: .cancel -/// ), -/// title: "Delete" +/// primaryButton: .default("Confirm", send: .confirmTapped), +/// secondaryButton: .cancel(send: .cancelTapped) /// ) /// ) /// }, @@ -141,35 +126,40 @@ public enum AlertState { } public struct Button { - public var action: Action - public var label: String + public var action: Action? public var type: `Type` public static func cancel( _ label: String, - send action: Action + send action: Action? = nil ) -> Self { - Self(action: action, label: label, type: .cancel) + Self(action: action, type: .cancel(label: label)) + } + + public static func cancel( + send action: Action? = nil + ) -> Self { + Self(action: action, type: .cancel(label: nil)) } public static func `default`( _ label: String, - send action: Action + send action: Action? = nil ) -> Self { - Self(action: action, label: label, type: .default) + Self(action: action, type: .default(label: label)) } public static func destructive( _ label: String, - send action: Action + send action: Action? = nil ) -> Self { - Self(action: action, label: label, type: .destructive) + Self(action: action, type: .destructive(label: label)) } - public enum `Type` { - case cancel - case `default` - case destructive + public enum `Type`: Hashable { + case cancel(label: String?) + case `default`(label: String) + case destructive(label: String) } } } @@ -219,14 +209,17 @@ extension AlertState.Alert: Identifiable where Action: Hashable { } extension AlertState.Alert.Button { - fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + let action = { if let action = self.action { send(action) } } switch self.type { - case .cancel: - return .cancel(Text(self.label)) { send(self.action) } - case .default: - return .default(Text(self.label)) { send(self.action) } - case .destructive: - return .destructive(Text(self.label)) { send(self.action) } + case let .cancel(.some(label)): + return .cancel(Text(label), action: action) + case .cancel(.none): + return .cancel(action) + case let .default(label): + return .default(Text(label), action: action) + case let .destructive(label): + return .destructive(Text(label), action: action) } } } From e5685df9abd479d57fa1136075f63e546ce0f864 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 13:45:57 -0400 Subject: [PATCH 20/29] More --- ...GettingStarted-AlertsAndActionSheets.swift | 33 +++++------ .../01-GettingStarted-SharedState.swift | 30 ++++------ .../02-Effects-WebSocket.swift | 30 ++++------ .../03-Effects-SystemEnvironment.swift | 23 +++----- ...gherOrderReducers-ReusableFavoriting.swift | 37 ++++++------ ...ngStarted-AlertsAndActionSheetsTests.swift | 26 ++++----- .../02-Effects-WebSocketTests.swift | 2 +- ...rderReducers-ReusableFavoritingTests.swift | 8 ++- ...ducers-ReusableOfflineDownloadsTests.swift | 24 +++----- .../SwiftUI/ActionSheet.swift | 58 ++++++++++--------- .../SwiftUI/Alert.swift | 50 ++++++++++++---- 11 files changed, 156 insertions(+), 165 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 8f399bf5c51f..3cd76d336429 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -4,10 +4,9 @@ import SwiftUI private let readMe = """ This demonstrates how to best handle alerts and action sheets in the Composable Architecture. - Because the library demands that all data flow through the application in a single direction, \ - we cannot leverage SwiftUI's two-way bindings because they can make changes to state without \ - going through a reducer. This means we can't directly use the standard API to display alerts and \ - sheets. + Because the library demands that all data flow through the application in a single direction, we \ + cannot leverage SwiftUI's two-way bindings because they can make changes to state without going \ + through a reducer. This means we can't directly use the standard API to display alerts and sheets. However, the library comes with two types, `AlertState` and `ActionSheetState`, which can be \ constructed from reducers and control whether or not an alert or action sheet is displayed. \ @@ -43,15 +42,13 @@ let AlertAndSheetReducer = Reducer< switch action { case .actionSheetButtonTapped: state.actionSheet = .show( - .init( - title: "Action sheet", - message: "This is an action sheet.", - buttons: [ - .cancel(), - .default("Increment", send: .incrementButtonTapped), - .default("Decrement", send: .decrementButtonTapped), - ] - ) + title: "Action sheet", + message: "This is an action sheet.", + buttons: [ + .cancel(), + .default("Increment", send: .incrementButtonTapped), + .default("Decrement", send: .decrementButtonTapped), + ] ) return .none @@ -61,12 +58,10 @@ let AlertAndSheetReducer = Reducer< case .alertButtonTapped: state.alert = .show( - .init( - title: "Alert!", - message: "This is an alert", - primaryButton: .cancel(), - secondaryButton: .default("Increment", send: .incrementButtonTapped) - ) + title: "Alert!", + message: "This is an alert", + primaryButton: .cancel(), + secondaryButton: .default("Increment", send: .incrementButtonTapped) ) return .none diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift index 1717547f024a..75ed61950b0d 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift @@ -21,7 +21,7 @@ struct SharedState: Equatable { enum Tab { case counter, profile } struct CounterState: Equatable { - var alert: String? + var alert = AlertState.dismissed var count = 0 var maxCount = 0 var minCount = 0 @@ -72,7 +72,7 @@ enum SharedStateAction { case profile(ProfileAction) case selectTab(SharedState.Tab) - enum CounterAction { + enum CounterAction: Hashable { case alertDismissed case decrementButtonTapped case incrementButtonTapped @@ -89,7 +89,7 @@ let sharedStateCounterReducer = Reducer< > { state, action, _ in switch action { case .alertDismissed: - state.alert = nil + state.alert = .dismissed return .none case .decrementButtonTapped: @@ -105,10 +105,11 @@ let sharedStateCounterReducer = Reducer< return .none case .isPrimeButtonTapped: - state.alert = - isPrime(state.count) - ? "👍 The number \(state.count) is prime!" - : "👎 The number \(state.count) is not prime :(" + state.alert = .show( + title: isPrime(state.count) + ? "👍 The number \(state.count) is prime!" + : "👎 The number \(state.count) is not prime :(" + ) return .none } } @@ -205,13 +206,9 @@ struct SharedStateCounterView: View { .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top) .navigationBarTitle("Shared State Demo") .alert( - item: viewStore.binding( - get: { $0.alert.map(PrimeAlert.init(title:)) }, - send: .alertDismissed - ) - ) { alert in - SwiftUI.Alert(title: Text(alert.title)) - } + self.store.scope(state: \.alert), + dismiss: .alertDismissed + ) } } } @@ -249,11 +246,6 @@ struct SharedStateProfileView: View { } } -private struct PrimeAlert: Equatable, Identifiable { - let title: String - var id: String { self.title } -} - // MARK: - SwiftUI previews struct SharedState_Previews: PreviewProvider { diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift index 445ab78c6889..83e2220909e4 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift @@ -11,7 +11,7 @@ private let readMe = """ """ struct WebSocketState: Equatable { - var alert: String? + var alert = AlertState.dismissed var connectivityState = ConnectivityState.disconnected var messageToSend = "" var receivedMessages: [String] = [] @@ -23,7 +23,7 @@ struct WebSocketState: Equatable { } } -enum WebSocketAction: Equatable { +enum WebSocketAction: Hashable { case alertDismissed case connectButtonTapped case messageToSendChanged(String) @@ -60,7 +60,7 @@ let webSocketReducer = Reducer.dismissed var dateString: String? var fetchedNumberString: String? var isFetchInFlight = false var uuidString: String? } -enum MultipleDependenciesAction { +enum MultipleDependenciesAction: Hashable { case alertButtonTapped case alertDelayReceived case alertDismissed @@ -50,11 +50,11 @@ let multipleDependenciesReducer = Reducer< .eraseToEffect() case .alertDelayReceived: - state.alertTitle = "Here's an alert after a delay!" + state.alert = .show(title: "Here's an alert after a delay!") return .none case .alertDismissed: - state.alertTitle = nil + state.alert = .dismissed return .none case .dateButtonTapped: @@ -107,13 +107,9 @@ struct MultipleDependenciesView: View { Button("Delayed Alert") { viewStore.send(.alertButtonTapped) } .alert( - item: viewStore.binding( - get: { $0.alertTitle.map(Alert.init(title:)) }, - send: { _ in .alertDismissed } - ) - ) { - SwiftUI.Alert(title: Text($0.title)) - } + self.store.scope(state: { $0.alert }), + dismiss: .alertDismissed + ) } Section( @@ -139,11 +135,6 @@ struct MultipleDependenciesView: View { } .navigationBarTitle("System Environment") } - - struct Alert: Identifiable { - var title: String - var id: String { self.title } - } } struct MultipleDependenciesView_Previews: PreviewProvider { diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift index 52f41272ec9c..1ace2265d479 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift @@ -21,14 +21,14 @@ private let readMe = """ // MARK: - Favorite domain struct FavoriteState: Equatable, Identifiable where ID: Hashable { + var alert = AlertState.dismissed let id: ID var isFavorite: Bool - var error: FavoriteError? } -enum FavoriteAction: Equatable { +enum FavoriteAction: Hashable { + case alertDismissed case buttonTapped - case errorDismissed case response(Result) } @@ -43,8 +43,8 @@ struct FavoriteCancelId: Hashable where ID: Hashable { } /// A wrapper for errors that occur when favoriting. -struct FavoriteError: Equatable, Error, Identifiable { - let error: Error +struct FavoriteError: Error, Hashable, Identifiable { + let error: NSError var localizedDescription: String { self.error.localizedDescription } var id: String { self.error.localizedDescription } static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } @@ -62,23 +62,23 @@ extension Reducer { Reducer, FavoriteAction, FavoriteEnvironment> { state, action, environment in switch action { + case .alertDismissed: + state.alert = .dismissed + state.isFavorite.toggle() + return .none + case .buttonTapped: state.isFavorite.toggle() return environment.request(state.id, state.isFavorite) .receive(on: environment.mainQueue) - .mapError(FavoriteError.init(error:)) + .mapError { FavoriteError(error: $0 as NSError) } .catchToEffect() .map(FavoriteAction.response) .cancellable(id: FavoriteCancelId(id: state.id), cancelInFlight: true) - case .errorDismissed: - state.error = nil - state.isFavorite.toggle() - return .none - case let .response(.failure(error)): - state.error = error + state.alert = .show(title: error.localizedDescription) return .none case let .response(.success(isFavorite)): @@ -99,9 +99,10 @@ struct FavoriteButton: View where ID: Hashable { Button(action: { viewStore.send(.buttonTapped) }) { Image(systemName: viewStore.isFavorite ? "heart.fill" : "heart") } - .alert(item: viewStore.binding(get: { $0.error }, send: .errorDismissed)) { - Alert(title: Text($0.localizedDescription)) - } + .alert( + self.store.scope(state: { $0.alert }), + dismiss: .alertDismissed + ) } } } @@ -109,14 +110,14 @@ struct FavoriteButton: View where ID: Hashable { // MARK: Feature domain - struct EpisodeState: Equatable, Identifiable { - var error: FavoriteError? + var alert = AlertState.dismissed let id: UUID var isFavorite: Bool let title: String var favorite: FavoriteState { - get { .init(id: self.id, isFavorite: self.isFavorite, error: self.error) } - set { (self.isFavorite, self.error) = (newValue.isFavorite, newValue.error) } + get { .init(alert: self.alert, id: self.id, isFavorite: self.isFavorite) } + set { (self.alert, self.isFavorite) = (newValue.alert, newValue.isFavorite) } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift index ca9c49bb9a0a..ca8f3e58b596 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift @@ -16,12 +16,10 @@ class AlertsAndActionSheetsTests: XCTestCase { store.assert( .send(.alertButtonTapped) { $0.alert = .show( - .init( - title: "Alert!", - message: "This is an alert", - primaryButton: .cancel(), - secondaryButton: .default("Increment", send: .incrementButtonTapped) - ) + title: "Alert!", + message: "This is an alert", + primaryButton: .cancel(), + secondaryButton: .default("Increment", send: .incrementButtonTapped) ) }, .send(.incrementButtonTapped) { @@ -41,15 +39,13 @@ class AlertsAndActionSheetsTests: XCTestCase { store.assert( .send(.actionSheetButtonTapped) { $0.actionSheet = .show( - .init( - title: "Action sheet", - message: "This is an action sheet.", - buttons: [ - .cancel(), - .default("Increment", send: .incrementButtonTapped), - .default("Decrement", send: .decrementButtonTapped), - ] - ) + title: "Action sheet", + message: "This is an action sheet.", + buttons: [ + .cancel(), + .default("Increment", send: .incrementButtonTapped), + .default("Decrement", send: .decrementButtonTapped), + ] ) }, .send(.incrementButtonTapped) { diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift index 7ef3b38ca1e5..60ee7dd2bf45 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift @@ -96,7 +96,7 @@ class WebSocketTests: XCTestCase { $0.messageToSend = "" }, .receive(.sendResponse(NSError(domain: "", code: 1))) { - $0.alert = "Could not send socket message. Try again." + $0.alert = .show(title: "Could not send socket message. Try again.") }, // Disconnect from the socket diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift index be6dc6144c2f..577e67ab07b1 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift @@ -65,11 +65,13 @@ class ReusableComponentsFavoritingTests: XCTestCase { .receive( .episode(index: 2, action: .favorite(.response(.failure(FavoriteError(error: error))))) ) { - $0.episodes[2].error = FavoriteError(error: error) + $0.episodes[2].alert = .show( + title: "The operation couldn’t be completed. (co.pointfree error -1.)" + ) }, - .send(.episode(index: 2, action: .favorite(.errorDismissed))) { - $0.episodes[2].error = nil + .send(.episode(index: 2, action: .favorite(.alertDismissed))) { + $0.episodes[2].alert = .dismissed $0.episodes[2].isFavorite = false } ) diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift index c84ed7671119..2ac4a8ec25e4 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift @@ -118,11 +118,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .send(.buttonTapped) { $0.alert = .show( - .init( - title: "Do you want to cancel downloading this map?", - primaryButton: .cancel(send: .cancelButtonTapped), - secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) - ) + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel(send: .cancelButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) }, @@ -159,11 +157,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .send(.buttonTapped) { $0.alert = .show( - .init( - title: "Do you want to cancel downloading this map?", - primaryButton: .cancel(send: .cancelButtonTapped), - secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) - ) + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel(send: .cancelButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) }, @@ -197,11 +193,9 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { store.assert( .send(.buttonTapped) { $0.alert = .show( - .init( - title: "Do you want to delete this map from your offline storage?", - primaryButton: .destructive("Delete", send: .deleteButtonTapped), - secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) - ) + title: "Do you want to delete this map from your offline storage?", + primaryButton: .destructive("Delete", send: .deleteButtonTapped), + secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) ) }, diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 335fc20b9a71..4a53527830dd 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -49,14 +49,12 @@ import SwiftUI /// /// case .infoTapped: /// state.actionSheet = .show( -/// .init( -/// buttons: [ -/// .default("Favorite", send: .favoriteTapped), -/// .destructive("Delete", send: .deleteTapped), -/// .cancel(send: .cancelTapped), -/// ], -/// title: "What would you like to do?" -/// ) +/// title: "What would you like to do?", +/// buttons: [ +/// .default("Favorite", send: .favoriteTapped), +/// .destructive("Delete", send: .deleteTapped), +/// .cancel(send: .cancelTapped), +/// ] /// ) /// return .none /// } @@ -67,7 +65,7 @@ import SwiftUI /// /// Button("Info") { viewStore.send(.infoTapped) } /// .actionSheet( -/// self.store.scope(state: { $0.actionSheet }), +/// self.store.scope(state: \.actionSheet), /// dismiss: .cancelTapped /// ) /// @@ -86,24 +84,22 @@ import SwiftUI /// store.assert( /// .send(.infoTapped) { /// $0.actionSheet = .show( -/// .init( -/// buttons: [ -/// .init( -/// action: .favoriteTapped, -/// label: "Favorite" -/// ), -/// .init( -/// action: .deleteTapped, -/// label: "Delete" -/// ), -/// .init( -/// action: .cancelTapped, -/// label: "Cancel", -/// type: .cancel -/// ) -/// ], -/// title: "What would you like to do?" -/// ) +/// title: "What would you like to do?", +/// buttons: [ +/// .init( +/// action: .favoriteTapped, +/// label: "Favorite" +/// ), +/// .init( +/// action: .deleteTapped, +/// label: "Delete" +/// ), +/// .init( +/// action: .cancelTapped, +/// label: "Cancel", +/// type: .cancel +/// ) +/// ] /// ) /// }, /// .send(.favoriteTapped) { @@ -121,6 +117,14 @@ public enum ActionSheetState { case dismissed case show(ActionSheet) + public static func show( + title: String, + message: String? = nil, + buttons: [ActionSheet.Button] + ) -> Self { + self.show(.init(title: title, message: message, buttons: buttons)) + } + public struct ActionSheet { public var buttons: [Button] public var message: String? diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index c27b26cfdd92..726172ebbe6c 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -44,12 +44,10 @@ import SwiftUI /// /// case .deleteTapped: /// state.alert = .show( -/// .init( -/// title: "Delete", -/// message: "Are you sure you want to delete this? It cannot be undone.", -/// primaryButton: .default("Confirm", send: .confirmTapped), -/// secondaryButton: .cancel(send: .cancelTapped) -/// ) +/// title: "Delete", +/// message: "Are you sure you want to delete this? It cannot be undone.", +/// primaryButton: .default("Confirm", send: .confirmTapped), +/// secondaryButton: .cancel(send: .cancelTapped) /// ) /// return .none /// } @@ -79,12 +77,10 @@ import SwiftUI /// store.assert( /// .send(.deleteTapped) { /// $0.alert = .show( -/// .init( -/// title: "Delete", -/// message: "Are you sure you want to delete this? It cannot be undone.", -/// primaryButton: .default("Confirm", send: .confirmTapped), -/// secondaryButton: .cancel(send: .cancelTapped) -/// ) +/// title: "Delete", +/// message: "Are you sure you want to delete this? It cannot be undone.", +/// primaryButton: .default("Confirm", send: .confirmTapped), +/// secondaryButton: .cancel(send: .cancelTapped) /// ) /// }, /// .send(.deleteTapped) { @@ -97,6 +93,36 @@ public enum AlertState { case dismissed case show(Alert) + public static func show( + title: String, + message: String? = nil, + dismissButton: Alert.Button? = nil + ) -> Self { + return .show( + .init( + title: title, + message: message, + dismissButton: dismissButton + ) + ) + } + + public static func show( + title: String, + message: String? = nil, + primaryButton: Alert.Button, + secondaryButton: Alert.Button + ) -> Self { + return .show( + .init( + title: title, + message: message, + primaryButton: primaryButton, + secondaryButton: secondaryButton + ) + ) + } + public struct Alert { public var message: String? public var primaryButton: Button? From 58f0672864f19351e747063e1f4d2272d4b2ff88 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 14:04:43 -0400 Subject: [PATCH 21/29] Fix --- Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift | 4 ++-- Sources/ComposableArchitecture/SwiftUI/Alert.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 4a53527830dd..aaabd87888b3 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -13,7 +13,7 @@ import SwiftUI /// /// To use this API, you model all the action sheet actions in your domain's action enum: /// -/// enum Action { +/// enum AppAction: Hashable { /// case cancelTapped /// case deleteTapped /// case favoriteTapped @@ -53,7 +53,7 @@ import SwiftUI /// buttons: [ /// .default("Favorite", send: .favoriteTapped), /// .destructive("Delete", send: .deleteTapped), -/// .cancel(send: .cancelTapped), +/// .cancel(), /// ] /// ) /// return .none diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 726172ebbe6c..6992fd3491ae 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -13,7 +13,7 @@ import SwiftUI /// /// To use this API, you model all the alert actions in your domain's action enum: /// -/// enum Action { +/// enum AppAction: Hashable { /// case cancelTapped /// case confirmTapped /// case deleteTapped @@ -47,7 +47,7 @@ import SwiftUI /// title: "Delete", /// message: "Are you sure you want to delete this? It cannot be undone.", /// primaryButton: .default("Confirm", send: .confirmTapped), -/// secondaryButton: .cancel(send: .cancelTapped) +/// secondaryButton: .cancel() /// ) /// return .none /// } From 31d839f7f372675f5f1136cdc27c23ca48529aef Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 29 Jun 2020 14:49:15 -0400 Subject: [PATCH 22/29] Fix docs --- .../SwiftUI/ActionSheet.swift | 17 ++++------------- .../ComposableArchitecture/SwiftUI/Alert.swift | 1 + 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index aaabd87888b3..361825bd1569 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -27,6 +27,7 @@ import SwiftUI /// /// struct AppState { /// var actionSheet = ActionSheetState.dismissed +/// /// // Your other state /// } /// @@ -86,19 +87,9 @@ import SwiftUI /// $0.actionSheet = .show( /// title: "What would you like to do?", /// buttons: [ -/// .init( -/// action: .favoriteTapped, -/// label: "Favorite" -/// ), -/// .init( -/// action: .deleteTapped, -/// label: "Delete" -/// ), -/// .init( -/// action: .cancelTapped, -/// label: "Cancel", -/// type: .cancel -/// ) +/// .default("Favorite", send: .favoriteTapped), +/// .destructive("Delete", send: .deleteTapped), +/// .cancel(), /// ] /// ) /// }, diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 6992fd3491ae..045ab48a6300 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -26,6 +26,7 @@ import SwiftUI /// /// struct AppState { /// var alert = AlertState.dismissed +/// /// // Your other state /// } /// From 987f160d1b3e485c1696cc9e0f4258f86b14a225 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 30 Jun 2020 09:11:30 -0400 Subject: [PATCH 23/29] Generic alerts optionality (#202) * Use Optional to model generic alerts * Xcode 12 * Refinement * update docs --- .../xcschemes/ComposableArchitecture.xcscheme | 2 +- .../xcschemes/ComposableCoreLocation.xcscheme | 2 +- .../xcschemes/ComposableCoreMotion.xcscheme | 2 +- .../CaseStudies.xcodeproj/project.pbxproj | 2 +- .../xcschemes/CaseStudies (SwiftUI).xcscheme | 2 +- .../xcschemes/CaseStudies (UIKit).xcscheme | 2 +- ...GettingStarted-AlertsAndActionSheets.swift | 18 +- .../01-GettingStarted-SharedState.swift | 13 +- .../02-Effects-WebSocket.swift | 19 +- .../03-Effects-SystemEnvironment.swift | 13 +- .../DownloadComponent.swift | 36 ++-- .../ReusableComponents-Download.swift | 2 +- ...gherOrderReducers-ReusableFavoriting.swift | 17 +- ...ngStarted-AlertsAndActionSheetsTests.swift | 8 +- .../02-Effects-WebSocketTests.swift | 2 +- ...rderReducers-ReusableFavoritingTests.swift | 4 +- ...ducers-ReusableOfflineDownloadsTests.swift | 12 +- Examples/LocationManager/Common/AppCore.swift | 16 +- .../Desktop/LocationManagerView.swift | 9 +- .../xcschemes/LocationManagerDesktop.xcscheme | 2 +- .../xcschemes/LocationManagerMobile.xcscheme | 2 +- .../Mobile/LocationManagerView.swift | 9 +- .../xcschemes/MotionManager.xcscheme | 2 +- .../MotionManager/MotionManagerView.swift | 17 +- .../xcshareddata/xcschemes/Search.xcscheme | 2 +- .../xcschemes/SpeechRecognition.xcscheme | 2 +- .../SpeechRecognition/SpeechRecognition.swift | 25 +-- .../TicTacToe/Sources/Common/AlertData.swift | 9 - Examples/TicTacToe/Sources/Core/AppCore.swift | 2 +- .../TicTacToe/Sources/Core/GameCore.swift | 4 +- .../TicTacToe/Sources/Core/LoginCore.swift | 8 +- .../TicTacToe/Sources/Core/NewGameCore.swift | 2 +- .../Sources/Core/TwoFactorCore.swift | 6 +- .../Views-SwiftUI/LoginSwiftView.swift | 16 +- .../Views-SwiftUI/TwoFactorSwiftView.swift | 14 +- .../Views-UIKit/LoginViewController.swift | 12 +- .../Views-UIKit/TwoFactorViewController.swift | 12 +- .../TicTacToe/Tests/LoginSwiftUITests.swift | 4 +- .../Tests/TwoFactorSwiftUITests.swift | 4 +- .../TicTacToe.xcodeproj/project.pbxproj | 4 - .../xcshareddata/xcschemes/AppCore.xcscheme | 2 +- .../xcschemes/AppSwiftUI.xcscheme | 2 +- .../xcschemes/AuthenticationClient.xcscheme | 2 +- .../xcshareddata/xcschemes/GameCore.xcscheme | 2 +- .../xcschemes/GameSwiftUI.xcscheme | 2 +- .../LiveAuthenticationClient.xcscheme | 2 +- .../xcshareddata/xcschemes/LoginCore.xcscheme | 2 +- .../xcschemes/LoginSwiftUI.xcscheme | 2 +- .../xcschemes/NewGameCore.xcscheme | 2 +- .../xcschemes/NewGameSwiftUI.xcscheme | 2 +- .../xcshareddata/xcschemes/TicTacToe.xcscheme | 2 +- .../xcschemes/TicTacToeCommon.xcscheme | 2 +- .../xcschemes/TwoFactorCore.xcscheme | 2 +- .../xcschemes/TwoFactorSwiftUI.xcscheme | 2 +- .../xcshareddata/xcschemes/Todos.xcscheme | 2 +- .../AudioPlayerClient/AudioPlayerClient.swift | 2 +- .../AudioRecorderClient.swift | 2 +- Examples/VoiceMemos/VoiceMemos/Helpers.swift | 5 - .../VoiceMemos/VoiceMemos/VoiceMemos.swift | 22 +-- .../SwiftUI/ActionSheet.swift | 94 +++------ .../SwiftUI/Alert.swift | 182 +++++++----------- 61 files changed, 263 insertions(+), 413 deletions(-) delete mode 100644 Examples/TicTacToe/Sources/Common/AlertData.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme index 66b18f63a331..cc236a07ea96 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme @@ -1,6 +1,6 @@ .dismissed - var alert = AlertState.dismissed + var actionSheet: ActionSheetState? + var alert: AlertState? var count = 0 } @@ -41,7 +41,7 @@ let AlertAndSheetReducer = Reducer< switch action { case .actionSheetButtonTapped: - state.actionSheet = .show( + state.actionSheet = .init( title: "Action sheet", message: "This is an action sheet.", buttons: [ @@ -53,11 +53,11 @@ let AlertAndSheetReducer = Reducer< return .none case .actionSheetCancelTapped: - state.actionSheet = .dismissed + state.actionSheet = nil return .none case .alertButtonTapped: - state.alert = .show( + state.alert = .init( title: "Alert!", message: "This is an alert", primaryButton: .cancel(), @@ -66,17 +66,17 @@ let AlertAndSheetReducer = Reducer< return .none case .alertCancelTapped: - state.alert = .dismissed + state.alert = nil return .none case .decrementButtonTapped: - state.actionSheet = .dismissed + state.actionSheet = nil state.count -= 1 return .none case .incrementButtonTapped: - state.actionSheet = .dismissed - state.alert = .dismissed + state.actionSheet = nil + state.alert = nil state.count += 1 return .none } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift index 75ed61950b0d..b93cb27b4dee 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift @@ -21,7 +21,7 @@ struct SharedState: Equatable { enum Tab { case counter, profile } struct CounterState: Equatable { - var alert = AlertState.dismissed + var alert: AlertState? var count = 0 var maxCount = 0 var minCount = 0 @@ -72,7 +72,7 @@ enum SharedStateAction { case profile(ProfileAction) case selectTab(SharedState.Tab) - enum CounterAction: Hashable { + enum CounterAction { case alertDismissed case decrementButtonTapped case incrementButtonTapped @@ -89,7 +89,7 @@ let sharedStateCounterReducer = Reducer< > { state, action, _ in switch action { case .alertDismissed: - state.alert = .dismissed + state.alert = nil return .none case .decrementButtonTapped: @@ -105,7 +105,7 @@ let sharedStateCounterReducer = Reducer< return .none case .isPrimeButtonTapped: - state.alert = .show( + state.alert = .init( title: isPrime(state.count) ? "👍 The number \(state.count) is prime!" : "👎 The number \(state.count) is not prime :(" @@ -205,10 +205,7 @@ struct SharedStateCounterView: View { .padding(16) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top) .navigationBarTitle("Shared State Demo") - .alert( - self.store.scope(state: \.alert), - dismiss: .alertDismissed - ) + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } } } diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift index 83e2220909e4..9be4a72cd38a 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift @@ -11,7 +11,7 @@ private let readMe = """ """ struct WebSocketState: Equatable { - var alert = AlertState.dismissed + var alert: AlertState? var connectivityState = ConnectivityState.disconnected var messageToSend = "" var receivedMessages: [String] = [] @@ -23,7 +23,7 @@ struct WebSocketState: Equatable { } } -enum WebSocketAction: Hashable { +enum WebSocketAction: Equatable { case alertDismissed case connectButtonTapped case messageToSendChanged(String) @@ -60,7 +60,7 @@ let webSocketReducer = Reducer.dismissed + var alert: AlertState? var dateString: String? var fetchedNumberString: String? var isFetchInFlight = false var uuidString: String? } -enum MultipleDependenciesAction: Hashable { +enum MultipleDependenciesAction: Equatable { case alertButtonTapped case alertDelayReceived case alertDismissed @@ -50,11 +50,11 @@ let multipleDependenciesReducer = Reducer< .eraseToEffect() case .alertDelayReceived: - state.alert = .show(title: "Here's an alert after a delay!") + state.alert = .init(title: "Here's an alert after a delay!") return .none case .alertDismissed: - state.alert = .dismissed + state.alert = nil return .none case .dateButtonTapped: @@ -106,10 +106,7 @@ struct MultipleDependenciesView: View { } Button("Delayed Alert") { viewStore.send(.alertButtonTapped) } - .alert( - self.store.scope(state: { $0.alert }), - dismiss: .alertDismissed - ) + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } Section( diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift index 0340a923fc63..e8cdd76ea546 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -2,7 +2,7 @@ import ComposableArchitecture import SwiftUI struct DownloadComponentState: Equatable { - var alert: AlertState = .dismissed + var alert: AlertState? let id: ID var mode: Mode let url: URL @@ -34,7 +34,7 @@ enum DownloadComponentAction: Equatable { case buttonTapped case downloadClient(Result) - enum AlertAction: Equatable, Hashable { + enum AlertAction: Equatable { case cancelButtonTapped case deleteButtonTapped case dismiss @@ -59,18 +59,18 @@ extension Reducer { switch action { case .alert(.cancelButtonTapped): state.mode = .notDownloaded - state.alert = .dismissed + state.alert = nil return environment.downloadClient.cancel(state.id) .fireAndForget() case .alert(.deleteButtonTapped): - state.alert = .dismissed + state.alert = nil state.mode = .notDownloaded return .none case .alert(.nevermindButtonTapped), .alert(.dismiss): - state.alert = .dismissed + state.alert = nil return .none case .buttonTapped: @@ -98,7 +98,7 @@ extension Reducer { case .downloadClient(.success(.response)): state.mode = .downloaded - state.alert = .dismissed + state.alert = nil return .none case let .downloadClient(.success(.updateProgress(progress))): @@ -107,7 +107,7 @@ extension Reducer { case .downloadClient(.failure): state.mode = .notDownloaded - state.alert = .dismissed + state.alert = nil return .none } } @@ -117,23 +117,19 @@ extension Reducer { } } -private let deleteAlert = AlertState.show( - .init( - title: "Do you want to delete this map from your offline storage?", - primaryButton: .destructive("Delete", send: .deleteButtonTapped), - secondaryButton: nevermindButton - ) +private let deleteAlert = AlertState( + title: "Do you want to delete this map from your offline storage?", + primaryButton: .destructive("Delete", send: .deleteButtonTapped), + secondaryButton: nevermindButton ) -private let cancelAlert = AlertState.show( - .init( - title: "Do you want to cancel downloading this map?", - primaryButton: .cancel(send: .cancelButtonTapped), - secondaryButton: nevermindButton - ) +private let cancelAlert = AlertState( + title: "Do you want to cancel downloading this map?", + primaryButton: .cancel(send: .cancelButtonTapped), + secondaryButton: nevermindButton ) -let nevermindButton = AlertState.Alert.Button +let nevermindButton = AlertState.Button .default("Nevermind", send: .nevermindButtonTapped) struct DownloadComponent: View { diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift index 4c3723a76e9e..ffb2a9a10d0c 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift @@ -23,7 +23,7 @@ struct CityMap: Equatable, Identifiable { } struct CityMapState: Equatable, Identifiable { - var downloadAlert: AlertState = .dismissed + var downloadAlert: AlertState? var downloadMode: Mode var cityMap: CityMap diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift index 1ace2265d479..09c40c1274ad 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift @@ -21,12 +21,12 @@ private let readMe = """ // MARK: - Favorite domain struct FavoriteState: Equatable, Identifiable where ID: Hashable { - var alert = AlertState.dismissed + var alert: AlertState? let id: ID var isFavorite: Bool } -enum FavoriteAction: Hashable { +enum FavoriteAction: Equatable { case alertDismissed case buttonTapped case response(Result) @@ -43,7 +43,7 @@ struct FavoriteCancelId: Hashable where ID: Hashable { } /// A wrapper for errors that occur when favoriting. -struct FavoriteError: Error, Hashable, Identifiable { +struct FavoriteError: Equatable, Error, Identifiable { let error: NSError var localizedDescription: String { self.error.localizedDescription } var id: String { self.error.localizedDescription } @@ -63,7 +63,7 @@ extension Reducer { state, action, environment in switch action { case .alertDismissed: - state.alert = .dismissed + state.alert = nil state.isFavorite.toggle() return .none @@ -78,7 +78,7 @@ extension Reducer { .cancellable(id: FavoriteCancelId(id: state.id), cancelInFlight: true) case let .response(.failure(error)): - state.alert = .show(title: error.localizedDescription) + state.alert = .init(title: error.localizedDescription) return .none case let .response(.success(isFavorite)): @@ -99,10 +99,7 @@ struct FavoriteButton: View where ID: Hashable { Button(action: { viewStore.send(.buttonTapped) }) { Image(systemName: viewStore.isFavorite ? "heart.fill" : "heart") } - .alert( - self.store.scope(state: { $0.alert }), - dismiss: .alertDismissed - ) + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } } } @@ -110,7 +107,7 @@ struct FavoriteButton: View where ID: Hashable { // MARK: Feature domain - struct EpisodeState: Equatable, Identifiable { - var alert = AlertState.dismissed + var alert: AlertState? let id: UUID var isFavorite: Bool let title: String diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift index ca8f3e58b596..b1fe9b8d56c6 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift @@ -15,7 +15,7 @@ class AlertsAndActionSheetsTests: XCTestCase { store.assert( .send(.alertButtonTapped) { - $0.alert = .show( + $0.alert = .init( title: "Alert!", message: "This is an alert", primaryButton: .cancel(), @@ -23,7 +23,7 @@ class AlertsAndActionSheetsTests: XCTestCase { ) }, .send(.incrementButtonTapped) { - $0.alert = .dismissed + $0.alert = nil $0.count = 1 } ) @@ -38,7 +38,7 @@ class AlertsAndActionSheetsTests: XCTestCase { store.assert( .send(.actionSheetButtonTapped) { - $0.actionSheet = .show( + $0.actionSheet = .init( title: "Action sheet", message: "This is an action sheet.", buttons: [ @@ -49,7 +49,7 @@ class AlertsAndActionSheetsTests: XCTestCase { ) }, .send(.incrementButtonTapped) { - $0.actionSheet = .dismissed + $0.actionSheet = nil $0.count = 1 } ) diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift index 60ee7dd2bf45..a10b015d8eaa 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift @@ -96,7 +96,7 @@ class WebSocketTests: XCTestCase { $0.messageToSend = "" }, .receive(.sendResponse(NSError(domain: "", code: 1))) { - $0.alert = .show(title: "Could not send socket message. Try again.") + $0.alert = .init(title: "Could not send socket message. Try again.") }, // Disconnect from the socket diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift index 577e67ab07b1..2a819462a2d2 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift @@ -65,13 +65,13 @@ class ReusableComponentsFavoritingTests: XCTestCase { .receive( .episode(index: 2, action: .favorite(.response(.failure(FavoriteError(error: error))))) ) { - $0.episodes[2].alert = .show( + $0.episodes[2].alert = .init( title: "The operation couldn’t be completed. (co.pointfree error -1.)" ) }, .send(.episode(index: 2, action: .favorite(.alertDismissed))) { - $0.episodes[2].alert = .dismissed + $0.episodes[2].alert = nil $0.episodes[2].isFavorite = false } ) diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift index 2ac4a8ec25e4..87b8f18416a4 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift @@ -117,7 +117,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.buttonTapped) { - $0.alert = .show( + $0.alert = .init( title: "Do you want to cancel downloading this map?", primaryButton: .cancel(send: .cancelButtonTapped), secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) @@ -125,7 +125,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.alert(.cancelButtonTapped)) { - $0.alert = .dismissed + $0.alert = nil $0.mode = .notDownloaded }, @@ -156,7 +156,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.buttonTapped) { - $0.alert = .show( + $0.alert = .init( title: "Do you want to cancel downloading this map?", primaryButton: .cancel(send: .cancelButtonTapped), secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) @@ -167,7 +167,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { .do { self.downloadSubject.send(completion: .finished) }, .do { self.scheduler.advance(by: 1) }, .receive(.downloadClient(.success(.response(Data())))) { - $0.alert = .dismissed + $0.alert = nil $0.mode = .downloaded } ) @@ -192,7 +192,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { store.assert( .send(.buttonTapped) { - $0.alert = .show( + $0.alert = .init( title: "Do you want to delete this map from your offline storage?", primaryButton: .destructive("Delete", send: .deleteButtonTapped), secondaryButton: .default("Nevermind", send: .nevermindButtonTapped) @@ -200,7 +200,7 @@ class ReusableComponentsDownloadComponentTests: XCTestCase { }, .send(.alert(.deleteButtonTapped)) { - $0.alert = .dismissed + $0.alert = nil $0.mode = .notDownloaded } ) diff --git a/Examples/LocationManager/Common/AppCore.swift b/Examples/LocationManager/Common/AppCore.swift index 08082767d353..0230ea69c5f4 100644 --- a/Examples/LocationManager/Common/AppCore.swift +++ b/Examples/LocationManager/Common/AppCore.swift @@ -19,14 +19,14 @@ public struct PointOfInterest: Equatable, Hashable { } public struct AppState: Equatable { - public var alert: String? + public var alert: AlertState? public var isRequestingCurrentLocation = false public var pointOfInterestCategory: MKPointOfInterestCategory? public var pointsOfInterest: [PointOfInterest] = [] public var region: CoordinateRegion? public init( - alert: String? = nil, + alert: AlertState? = nil, isRequestingCurrentLocation: Bool = false, pointOfInterestCategory: MKPointOfInterestCategory? = nil, pointsOfInterest: [PointOfInterest] = [], @@ -99,7 +99,7 @@ public let appReducer = Reducer { state, ac case .currentLocationButtonTapped: guard environment.locationManager.locationServicesEnabled() else { - state.alert = "Location services are turned off." + state.alert = .init(title: "Location services are turned off.") return .none } @@ -117,11 +117,11 @@ public let appReducer = Reducer { state, ac #endif case .restricted: - state.alert = "Please give us access to your location in settings." + state.alert = .init(title: "Please give us access to your location in settings.") return .none case .denied: - state.alert = "Please give us access to your location in settings." + state.alert = .init(title: "Please give us access to your location in settings.") return .none case .authorizedAlways, .authorizedWhenInUse: @@ -148,7 +148,7 @@ public let appReducer = Reducer { state, ac return .none case .localSearchResponse(.failure): - state.alert = "Could not perform search. Please try again." + state.alert = .init(title: "Could not perform search. Please try again.") return .none case .locationManager: @@ -204,7 +204,9 @@ private let locationManagerReducer = Reducer ? var facingDirection: Direction? var initialAttitude: Attitude? var isRecording = false @@ -45,14 +45,14 @@ let appReducer = Reducer { state, action, e switch action { case .alertDismissed: - state.alertTitle = nil + state.alert = nil return .none case .motionUpdate(.failure): - state.alertTitle = """ + state.alert = .init(title: """ We encountered a problem with the motion manager. Make sure you run this demo on a real \ device, not the simulator. - """ + """) state.isRecording = false return .none @@ -143,14 +143,7 @@ struct AppView: View { } .padding() .background(viewStore.facingDirection == .backward ? Color.green : Color.clear) - .alert( - item: viewStore.binding( - get: { $0.alertTitle.map(AppAlert.init(title:)) }, - send: .alertDismissed - ) - ) { alert in - Alert(title: Text(alert.title)) - } + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } } } diff --git a/Examples/Search/Search.xcodeproj/xcshareddata/xcschemes/Search.xcscheme b/Examples/Search/Search.xcodeproj/xcshareddata/xcschemes/Search.xcscheme index c0c81c8deef5..9b28e4d35cba 100644 --- a/Examples/Search/Search.xcodeproj/xcshareddata/xcschemes/Search.xcscheme +++ b/Examples/Search/Search.xcodeproj/xcshareddata/xcschemes/Search.xcscheme @@ -1,6 +1,6 @@ ? var isRecording = false var speechRecognizerAuthorizationStatus = SFSpeechRecognizerAuthorizationStatus.notDetermined var transcribedText = "" @@ -33,12 +33,12 @@ let appReducer = Reducer { state, action, e switch action { case .dismissAuthorizationStateAlert: - state.authorizationStateAlert = nil + state.alert = nil return .none case .speech(.failure(.couldntConfigureAudioSession)), .speech(.failure(.couldntStartAudioEngine)): - state.authorizationStateAlert = "Problem with audio device. Please try again." + state.alert = .init(title: "Problem with audio device. Please try again.") return .none case .recordButtonTapped: @@ -66,7 +66,7 @@ let appReducer = Reducer { state, action, e } case let .speech(.failure(error)): - state.authorizationStateAlert = "An error occured while transcribing. Please try again." + state.alert = .init(title: "An error occured while transcribing. Please try again.") return environment.speechClient.finishTask(SpeechRecognitionId()) .fireAndForget() @@ -76,17 +76,17 @@ let appReducer = Reducer { state, action, e switch status { case .notDetermined: - state.authorizationStateAlert = "Try again." + state.alert = .init(title: "Try again.") return .none case .denied: - state.authorizationStateAlert = """ + state.alert = .init(title: """ You denied access to speech recognition. This app needs access to transcribe your speech. - """ + """) return .none case .restricted: - state.authorizationStateAlert = "Your device does not allow speech recognition." + state.alert = .init(title: "Your device does not allow speech recognition.") return .none case .authorized: @@ -144,14 +144,7 @@ struct SpeechRecognitionView: View { } } .padding() - .alert( - item: viewStore.binding( - get: { $0.authorizationStateAlert.map(AuthorizationStateAlert.init(title:)) }, - send: .dismissAuthorizationStateAlert - ) - ) { alert in - Alert(title: Text(alert.title)) - } + .alert(self.store.scope(state: { $0.alert }), dismiss: .dismissAuthorizationStateAlert) } } } diff --git a/Examples/TicTacToe/Sources/Common/AlertData.swift b/Examples/TicTacToe/Sources/Common/AlertData.swift deleted file mode 100644 index c77639c2508d..000000000000 --- a/Examples/TicTacToe/Sources/Common/AlertData.swift +++ /dev/null @@ -1,9 +0,0 @@ -public struct AlertData: Equatable, Identifiable { - public let title: String - - public init(title: String) { - self.title = title - } - - public var id: String { self.title } -} diff --git a/Examples/TicTacToe/Sources/Core/AppCore.swift b/Examples/TicTacToe/Sources/Core/AppCore.swift index 6920316c17a0..42c1651f449d 100644 --- a/Examples/TicTacToe/Sources/Core/AppCore.swift +++ b/Examples/TicTacToe/Sources/Core/AppCore.swift @@ -6,7 +6,7 @@ import NewGameCore public struct AppState: Equatable { public var login: LoginState? = LoginState() - public var newGame: NewGameState? = nil + public var newGame: NewGameState? public init() {} } diff --git a/Examples/TicTacToe/Sources/Core/GameCore.swift b/Examples/TicTacToe/Sources/Core/GameCore.swift index dfe8344f5293..06c384d7040b 100644 --- a/Examples/TicTacToe/Sources/Core/GameCore.swift +++ b/Examples/TicTacToe/Sources/Core/GameCore.swift @@ -93,9 +93,9 @@ extension Array where Element == [Player?] { ] for condition in winConditions { - let matchCount = - condition + let matches = condition .map { self[$0 % 3][$0 / 3] } + let matchCount = matches .filter { $0 == player } .count diff --git a/Examples/TicTacToe/Sources/Core/LoginCore.swift b/Examples/TicTacToe/Sources/Core/LoginCore.swift index 0fb58b6dc203..4cad4db43eee 100644 --- a/Examples/TicTacToe/Sources/Core/LoginCore.swift +++ b/Examples/TicTacToe/Sources/Core/LoginCore.swift @@ -5,12 +5,12 @@ import TicTacToeCommon import TwoFactorCore public struct LoginState: Equatable { - public var alertData: AlertData? = nil + public var alert: AlertState? public var email = "" public var isFormValid = false public var isLoginRequestInFlight = false public var password = "" - public var twoFactor: TwoFactorState? = nil + public var twoFactor: TwoFactorState? public init() {} } @@ -55,7 +55,7 @@ public let loginReducer = twoFactorReducer state, action, environment in switch action { case .alertDismissed: - state.alertData = nil + state.alert = nil return .none case let .emailChanged(email): @@ -71,7 +71,7 @@ public let loginReducer = twoFactorReducer return .none case let .loginResponse(.failure(error)): - state.alertData = AlertData(title: error.localizedDescription) + state.alert = .init(title: error.localizedDescription) state.isLoginRequestInFlight = false return .none diff --git a/Examples/TicTacToe/Sources/Core/NewGameCore.swift b/Examples/TicTacToe/Sources/Core/NewGameCore.swift index bacf5f5b3e7e..f791f526b8ce 100644 --- a/Examples/TicTacToe/Sources/Core/NewGameCore.swift +++ b/Examples/TicTacToe/Sources/Core/NewGameCore.swift @@ -3,7 +3,7 @@ import GameCore import TicTacToeCommon public struct NewGameState: Equatable { - public var game: GameState? = nil + public var game: GameState? public var oPlayerName = "" public var xPlayerName = "" diff --git a/Examples/TicTacToe/Sources/Core/TwoFactorCore.swift b/Examples/TicTacToe/Sources/Core/TwoFactorCore.swift index 2343a6b5da5d..0f157dee9dd9 100644 --- a/Examples/TicTacToe/Sources/Core/TwoFactorCore.swift +++ b/Examples/TicTacToe/Sources/Core/TwoFactorCore.swift @@ -5,7 +5,7 @@ import Dispatch import TicTacToeCommon public struct TwoFactorState: Equatable { - public var alertData: AlertData? = nil + public var alert: AlertState? public var code = "" public var isFormValid = false public var isTwoFactorRequestInFlight = false @@ -41,7 +41,7 @@ public let twoFactorReducer = Reducer? var email: String var isActivityIndicatorVisible: Bool var isFormDisabled: Bool @@ -82,22 +82,18 @@ public struct LoginView: View { } .disabled(viewStore.isFormDisabled) } - .navigationBarTitle("Login") - // NB: This is necessary to clear the bar items from the game. - .navigationBarItems(trailing: EmptyView()) - .alert( - item: viewStore.binding(get: { $0.alertData }, send: .alertDismissed) - ) { alertData in - Alert(title: Text(alertData.title)) - } + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } + .navigationBarTitle("Login") + // NB: This is necessary to clear the bar items from the game. + .navigationBarItems(trailing: EmptyView()) } } extension LoginState { var view: LoginView.ViewState { LoginView.ViewState( - alertData: self.alertData, + alert: self.alert, email: self.email, isActivityIndicatorVisible: self.isLoginRequestInFlight, isFormDisabled: self.isLoginRequestInFlight, diff --git a/Examples/TicTacToe/Sources/Views-SwiftUI/TwoFactorSwiftView.swift b/Examples/TicTacToe/Sources/Views-SwiftUI/TwoFactorSwiftView.swift index e737750c7ceb..c147370941c6 100644 --- a/Examples/TicTacToe/Sources/Views-SwiftUI/TwoFactorSwiftView.swift +++ b/Examples/TicTacToe/Sources/Views-SwiftUI/TwoFactorSwiftView.swift @@ -6,14 +6,14 @@ import TwoFactorCore public struct TwoFactorView: View { struct ViewState: Equatable { - var alertData: AlertData? + var alert: AlertState? var code: String var isActivityIndicatorVisible: Bool var isFormDisabled: Bool var isSubmitButtonDisabled: Bool } - enum ViewAction { + enum ViewAction: Equatable { case alertDismissed case codeChanged(String) case submitButtonTapped @@ -56,20 +56,16 @@ public struct TwoFactorView: View { } } .disabled(viewStore.isFormDisabled) - .navigationBarTitle("Two Factor Confirmation") - .alert( - item: viewStore.binding(get: { $0.alertData }, send: .alertDismissed) - ) { alertData in - Alert(title: Text(alertData.title)) - } + .alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed) } + .navigationBarTitle("Two Factor Confirmation") } } extension TwoFactorState { var view: TwoFactorView.ViewState { TwoFactorView.ViewState( - alertData: self.alertData, + alert: self.alert, code: self.code, isActivityIndicatorVisible: self.isTwoFactorRequestInFlight, isFormDisabled: self.isTwoFactorRequestInFlight, diff --git a/Examples/TicTacToe/Sources/Views-UIKit/LoginViewController.swift b/Examples/TicTacToe/Sources/Views-UIKit/LoginViewController.swift index 7fd314eedb39..b46e4df48a80 100644 --- a/Examples/TicTacToe/Sources/Views-UIKit/LoginViewController.swift +++ b/Examples/TicTacToe/Sources/Views-UIKit/LoginViewController.swift @@ -7,7 +7,7 @@ import UIKit class LoginViewController: UIViewController { struct ViewState: Equatable { - let alertData: AlertData? + let alert: AlertState? let email: String? let isActivityIndicatorHidden: Bool let isEmailTextFieldEnabled: Bool @@ -131,13 +131,13 @@ class LoginViewController: UIViewController { .assign(to: \.isHidden, on: activityIndicator) .store(in: &self.cancellables) - self.viewStore.publisher.alertData - .sink { [weak self] alertData in + self.viewStore.publisher.alert + .sink { [weak self] alert in guard let self = self else { return } - guard let alertData = alertData else { return } + guard let alert = alert else { return } let alertController = UIAlertController( - title: alertData.title, message: nil, preferredStyle: .alert) + title: alert.title, message: nil, preferredStyle: .alert) alertController.addAction( UIAlertAction( title: "Ok", style: .default, @@ -189,7 +189,7 @@ class LoginViewController: UIViewController { extension LoginState { var view: LoginViewController.ViewState { .init( - alertData: self.alertData, + alert: self.alert, email: self.email, isActivityIndicatorHidden: !self.isLoginRequestInFlight, isEmailTextFieldEnabled: !self.isLoginRequestInFlight, diff --git a/Examples/TicTacToe/Sources/Views-UIKit/TwoFactorViewController.swift b/Examples/TicTacToe/Sources/Views-UIKit/TwoFactorViewController.swift index 82a36be76edf..97269a523050 100644 --- a/Examples/TicTacToe/Sources/Views-UIKit/TwoFactorViewController.swift +++ b/Examples/TicTacToe/Sources/Views-UIKit/TwoFactorViewController.swift @@ -6,7 +6,7 @@ import UIKit public final class TwoFactorViewController: UIViewController { struct ViewState: Equatable { - let alertData: AlertData? + let alert: AlertState? let code: String? let isActivityIndicatorHidden: Bool let isLoginButtonEnabled: Bool @@ -87,13 +87,13 @@ public final class TwoFactorViewController: UIViewController { .assign(to: \.isEnabled, on: loginButton) .store(in: &self.cancellables) - self.viewStore.publisher.alertData - .sink { [weak self] alertData in + self.viewStore.publisher.alert + .sink { [weak self] alert in guard let self = self else { return } - guard let alertData = alertData else { return } + guard let alert = alert else { return } let alertController = UIAlertController( - title: alertData.title, message: nil, preferredStyle: .alert) + title: alert.title, message: nil, preferredStyle: .alert) alertController.addAction( UIAlertAction( title: "Ok", style: .default, @@ -117,7 +117,7 @@ public final class TwoFactorViewController: UIViewController { extension TwoFactorState { var view: TwoFactorViewController.ViewState { .init( - alertData: self.alertData, + alert: self.alert, code: self.code, isActivityIndicatorHidden: !self.isTwoFactorRequestInFlight, isLoginButtonEnabled: self.isFormValid && !self.isTwoFactorRequestInFlight diff --git a/Examples/TicTacToe/Tests/LoginSwiftUITests.swift b/Examples/TicTacToe/Tests/LoginSwiftUITests.swift index 0c8cdb1acf1c..39acf88d919a 100644 --- a/Examples/TicTacToe/Tests/LoginSwiftUITests.swift +++ b/Examples/TicTacToe/Tests/LoginSwiftUITests.swift @@ -119,13 +119,13 @@ class LoginSwiftUITests: XCTestCase { self.scheduler.advance() }, .receive(.loginResponse(.failure(.invalidUserPassword))) { - $0.alertData = AlertData( + $0.alert = .init( title: AuthenticationError.invalidUserPassword.localizedDescription) $0.isActivityIndicatorVisible = false $0.isFormDisabled = false }, .send(.alertDismissed) { - $0.alertData = nil + $0.alert = nil } ) } diff --git a/Examples/TicTacToe/Tests/TwoFactorSwiftUITests.swift b/Examples/TicTacToe/Tests/TwoFactorSwiftUITests.swift index 1f6bde8e4c13..401ae128eaf7 100644 --- a/Examples/TicTacToe/Tests/TwoFactorSwiftUITests.swift +++ b/Examples/TicTacToe/Tests/TwoFactorSwiftUITests.swift @@ -88,12 +88,12 @@ class TwoFactorSwiftUITests: XCTestCase { self.scheduler.advance() }, .receive(.twoFactorResponse(.failure(.invalidTwoFactor))) { - $0.alertData = AlertData(title: AuthenticationError.invalidTwoFactor.localizedDescription) + $0.alert = .init(title: AuthenticationError.invalidTwoFactor.localizedDescription) $0.isActivityIndicatorVisible = false $0.isFormDisabled = false }, .send(.alertDismissed) { - $0.alertData = nil + $0.alert = nil } ) } diff --git a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj index 3a9ca3b4e98f..6a153b17dd2a 100644 --- a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj +++ b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj @@ -88,7 +88,6 @@ DC9195A324201B3B00A5BE1F /* LoginSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9195A124201B3B00A5BE1F /* LoginSwiftUITests.swift */; }; DCAF10D1242027D400483288 /* TwoFactorSwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCAF10C8242027D400483288 /* TwoFactorSwiftUI.framework */; }; DCAF10D8242027D400483288 /* TwoFactorSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAF10D7242027D400483288 /* TwoFactorSwiftUITests.swift */; }; - DCAF114E2420294700483288 /* AlertData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAF10E52420285A00483288 /* AlertData.swift */; }; DCAF1152242029EE00483288 /* TwoFactorSwiftView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAF1151242029EE00483288 /* TwoFactorSwiftView.swift */; }; DCAF115D24202A5700483288 /* AuthenticationClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC919425242012D200A5BE1F /* AuthenticationClient.framework */; }; DCAF115E24202A5C00483288 /* TicTacToeCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCAF11312420293300483288 /* TicTacToeCommon.framework */; }; @@ -600,7 +599,6 @@ DCAF10C8242027D400483288 /* TwoFactorSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TwoFactorSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCAF10D0242027D400483288 /* TwoFactorSwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TwoFactorSwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DCAF10D7242027D400483288 /* TwoFactorSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorSwiftUITests.swift; sourceTree = ""; }; - DCAF10E52420285A00483288 /* AlertData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertData.swift; sourceTree = ""; }; DCAF10EC242028DE00483288 /* AppCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCAF110E242028E600483288 /* AppSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCAF11312420293300483288 /* TicTacToeCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TicTacToeCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -934,7 +932,6 @@ isa = PBXGroup; children = ( DCAF11C72420335500483288 /* ActivityIndicator.swift */, - DCAF10E52420285A00483288 /* AlertData.swift */, ); path = Common; sourceTree = ""; @@ -1965,7 +1962,6 @@ buildActionMask = 2147483647; files = ( DCAF11CA2420335500483288 /* ActivityIndicator.swift in Sources */, - DCAF114E2420294700483288 /* AlertData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Examples/TicTacToe/TicTacToe.xcodeproj/xcshareddata/xcschemes/AppCore.xcscheme b/Examples/TicTacToe/TicTacToe.xcodeproj/xcshareddata/xcschemes/AppCore.xcscheme index bca2876739d9..33699a02bb62 100644 --- a/Examples/TicTacToe/TicTacToe.xcodeproj/xcshareddata/xcschemes/AppCore.xcscheme +++ b/Examples/TicTacToe/TicTacToe.xcodeproj/xcshareddata/xcschemes/AppCore.xcscheme @@ -1,6 +1,6 @@ ? var audioRecorderPermission = RecorderPermission.undetermined var currentRecording: CurrentRecording? var voiceMemos: [VoiceMemo] = [] @@ -78,7 +78,7 @@ let voiceMemosReducer = Reducer.dismissed +/// struct AppState: Equatable { +/// var actionSheet: ActionSheetState? /// /// // Your other state /// } @@ -37,19 +37,19 @@ import SwiftUI /// let appReducer = Reducer { state, action, env in /// switch action /// case .cancelTapped: -/// state.actionSheet = .dismissed +/// state.actionSheet = nil /// return .none /// /// case .deleteTapped: -/// state.actionSheet = .dismissed +/// state.actionSheet = nil /// // Do deletion logic... /// /// case .favoriteTapped: -/// state.actionSheet = .dismissed +/// state.actionSheet = nil /// // Do favoriting logic /// /// case .infoTapped: -/// state.actionSheet = .show( +/// state.actionSheet = .init( /// title: "What would you like to do?", /// buttons: [ /// .default("Favorite", send: .favoriteTapped), @@ -84,7 +84,7 @@ import SwiftUI /// /// store.assert( /// .send(.infoTapped) { -/// $0.actionSheet = .show( +/// $0.actionSheet = .init( /// title: "What would you like to do?", /// buttons: [ /// .default("Favorite", send: .favoriteTapped), @@ -94,7 +94,7 @@ import SwiftUI /// ) /// }, /// .send(.favoriteTapped) { -/// $0.actionSheet = .dismissed +/// $0.actionSheet = nil /// // Also verify that favoriting logic executed correctly /// } /// ) @@ -104,35 +104,22 @@ import SwiftUI @available(macOS, unavailable) @available(tvOS 13, *) @available(watchOS 6, *) -public enum ActionSheetState { - case dismissed - case show(ActionSheet) +public struct ActionSheetState { + public var buttons: [Button] + public var message: String? + public var title: String - public static func show( + public init( title: String, message: String? = nil, - buttons: [ActionSheet.Button] - ) -> Self { - self.show(.init(title: title, message: message, buttons: buttons)) + buttons: [Button] + ) { + self.buttons = buttons + self.message = message + self.title = title } - public struct ActionSheet { - public var buttons: [Button] - public var message: String? - public var title: String - - public init( - title: String, - message: String? = nil, - buttons: [Button] - ) { - self.buttons = buttons - self.message = message - self.title = title - } - - public typealias Button = AlertState.Alert.Button - } + public typealias Button = AlertState.Button } @available(iOS 13, *) @@ -154,21 +141,7 @@ extension ActionSheetState: Hashable where Action: Hashable {} @available(macOS, unavailable) @available(tvOS 13, *) @available(watchOS 6, *) -extension ActionSheetState.ActionSheet: Equatable where Action: Equatable {} - -@available(iOS 13, *) -@available(macCatalyst 13, *) -@available(macOS, unavailable) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ActionSheetState.ActionSheet: Hashable where Action: Hashable {} - -@available(iOS 13, *) -@available(macCatalyst 13, *) -@available(macOS, unavailable) -@available(tvOS 13, *) -@available(watchOS 6, *) -extension ActionSheetState.ActionSheet: Identifiable where Action: Hashable { +extension ActionSheetState: Identifiable where Action: Hashable { public var id: Self { self } } @@ -185,26 +158,19 @@ extension View { @available(tvOS 13, *) @available(watchOS 6, *) public func actionSheet( - _ store: Store, Action>, + _ store: Store?, Action>, dismiss: Action - ) -> some View where Action: Hashable { + ) -> some View { - let viewStore = ViewStore(store) + let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) }) return self.actionSheet( - item: Binding.ActionSheet?>( - get: { - switch viewStore.state { - case .dismissed: - return nil - case let .show(actionSheet): - return actionSheet - } - }, + isPresented: Binding( + get: { viewStore.state != nil }, set: { - guard $0 == nil else { return } + guard !$0 else { return } viewStore.send(dismiss) }), - content: { $0.toSwiftUI(send: viewStore.send) } + content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? ActionSheet(title: Text("")) } ) } } @@ -214,7 +180,7 @@ extension View { @available(macOS, unavailable) @available(tvOS 13, *) @available(watchOS 6, *) -extension ActionSheetState.ActionSheet { +extension ActionSheetState { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { SwiftUI.ActionSheet( title: Text(self.title), diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 045ab48a6300..532c7dc7a45d 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -13,7 +13,7 @@ import SwiftUI /// /// To use this API, you model all the alert actions in your domain's action enum: /// -/// enum AppAction: Hashable { +/// enum AppAction: Equatable { /// case cancelTapped /// case confirmTapped /// case deleteTapped @@ -21,11 +21,11 @@ import SwiftUI /// // Your other actions /// } /// -/// And you model the state for showing the alert in your domain's state, and it can start off in -/// the `.dismissed` state: +/// And you model the state for showing the alert in your domain's state, and it can start off +/// `nil`: /// -/// struct AppState { -/// var alert = AlertState.dismissed +/// struct AppState: Equatable { +/// var alert = AlertState? /// /// // Your other state /// } @@ -36,15 +36,15 @@ import SwiftUI /// let appReducer = Reducer { state, action, env in /// switch action /// case .cancelTapped: -/// state.alert = .dismissed +/// state.alert = nil /// return .none /// /// case .confirmTapped: -/// state.alert = .dismissed +/// state.alert = nil /// // Do deletion logic... /// /// case .deleteTapped: -/// state.alert = .show( +/// state.alert = .init( /// title: "Delete", /// message: "Are you sure you want to delete this? It cannot be undone.", /// primaryButton: .default("Confirm", send: .confirmTapped), @@ -77,7 +77,7 @@ import SwiftUI /// /// store.assert( /// .send(.deleteTapped) { -/// $0.alert = .show( +/// $0.alert = .init( /// title: "Delete", /// message: "Are you sure you want to delete this? It cannot be undone.", /// primaryButton: .default("Confirm", send: .confirmTapped), @@ -85,109 +85,74 @@ import SwiftUI /// ) /// }, /// .send(.deleteTapped) { -/// $0.alert = .dismissed +/// $0.alert = nil /// // Also verify that delete logic executed correctly /// } /// ) /// -public enum AlertState { - case dismissed - case show(Alert) +public struct AlertState { + public var message: String? + public var primaryButton: Button? + public var secondaryButton: Button? + public var title: String - public static func show( + public init( title: String, message: String? = nil, - dismissButton: Alert.Button? = nil - ) -> Self { - return .show( - .init( - title: title, - message: message, - dismissButton: dismissButton - ) - ) + dismissButton: Button? = nil + ) { + self.title = title + self.message = message + self.primaryButton = dismissButton } - public static func show( + public init( title: String, message: String? = nil, - primaryButton: Alert.Button, - secondaryButton: Alert.Button - ) -> Self { - return .show( - .init( - title: title, - message: message, - primaryButton: primaryButton, - secondaryButton: secondaryButton - ) - ) + primaryButton: Button, + secondaryButton: Button + ) { + self.title = title + self.message = message + self.primaryButton = primaryButton + self.secondaryButton = secondaryButton } - public struct Alert { - public var message: String? - public var primaryButton: Button? - public var secondaryButton: Button? - public var title: String + public struct Button { + public var action: Action? + public var type: `Type` - public init( - title: String, - message: String? = nil, - dismissButton: Button? = nil - ) { - self.message = message - self.primaryButton = dismissButton - self.title = title + public static func cancel( + _ label: String, + send action: Action? = nil + ) -> Self { + Self(action: action, type: .cancel(label: label)) } - public init( - title: String, - message: String? = nil, - primaryButton: Button, - secondaryButton: Button - ) { - self.message = message - self.primaryButton = primaryButton - self.secondaryButton = secondaryButton - self.title = title + public static func cancel( + send action: Action? = nil + ) -> Self { + Self(action: action, type: .cancel(label: nil)) } - public struct Button { - public var action: Action? - public var type: `Type` - - public static func cancel( - _ label: String, - send action: Action? = nil - ) -> Self { - Self(action: action, type: .cancel(label: label)) - } - - public static func cancel( - send action: Action? = nil - ) -> Self { - Self(action: action, type: .cancel(label: nil)) - } - - public static func `default`( - _ label: String, - send action: Action? = nil - ) -> Self { - Self(action: action, type: .default(label: label)) - } + public static func `default`( + _ label: String, + send action: Action? = nil + ) -> Self { + Self(action: action, type: .default(label: label)) + } - public static func destructive( - _ label: String, - send action: Action? = nil - ) -> Self { - Self(action: action, type: .destructive(label: label)) - } + public static func destructive( + _ label: String, + send action: Action? = nil + ) -> Self { + Self(action: action, type: .destructive(label: label)) + } - public enum `Type`: Hashable { - case cancel(label: String?) - case `default`(label: String) - case destructive(label: String) - } + public enum `Type`: Hashable { + case cancel(label: String?) + case `default`(label: String) + case destructive(label: String) } } } @@ -200,42 +165,33 @@ extension View { /// - dismissal: An action to send when the alert is dismissed through non-user actions, such /// as when an alert is automatically dismissed by the system. public func alert( - _ store: Store, Action>, + _ store: Store?, Action>, dismiss: Action - ) -> some View where Action: Hashable { + ) -> some View { - let viewStore = ViewStore(store) + let viewStore = ViewStore(store, removeDuplicates: { ($0 == nil) != ($1 == nil) }) return self.alert( - item: Binding.Alert?>( - get: { - switch viewStore.state { - case .dismissed: - return nil - case let .show(alert): - return alert - } - }, + isPresented: Binding( + get: { viewStore.state != nil }, set: { - guard $0 == nil else { return } + guard !$0 else { return } viewStore.send(dismiss) }), - content: { $0.toSwiftUI(send: viewStore.send) } + content: { viewStore.state?.toSwiftUI(send: viewStore.send) ?? Alert(title: Text("")) } ) } } extension AlertState: Equatable where Action: Equatable {} extension AlertState: Hashable where Action: Hashable {} -extension AlertState.Alert: Equatable where Action: Equatable {} -extension AlertState.Alert: Hashable where Action: Hashable {} -extension AlertState.Alert.Button: Equatable where Action: Equatable {} -extension AlertState.Alert.Button: Hashable where Action: Hashable {} +extension AlertState.Button: Equatable where Action: Equatable {} +extension AlertState.Button: Hashable where Action: Hashable {} -extension AlertState.Alert: Identifiable where Action: Hashable { +extension AlertState: Identifiable where Action: Hashable { public var id: Self { self } } -extension AlertState.Alert.Button { +extension AlertState.Button { func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { let action = { if let action = self.action { send(action) } } switch self.type { @@ -251,7 +207,7 @@ extension AlertState.Alert.Button { } } -extension AlertState.Alert { +extension AlertState { fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert { let title = Text(self.title) let message = self.message.map { Text($0) } From ddcaa66919802113e3e4dc3f27d228acf44dc1f7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 30 Jun 2020 09:38:31 -0400 Subject: [PATCH 24/29] Fix --- Examples/LocationManager/CommonTests/CommonTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Examples/LocationManager/CommonTests/CommonTests.swift b/Examples/LocationManager/CommonTests/CommonTests.swift index d5842a89a6ab..01b8404c7042 100644 --- a/Examples/LocationManager/CommonTests/CommonTests.swift +++ b/Examples/LocationManager/CommonTests/CommonTests.swift @@ -154,7 +154,9 @@ class LocationManagerTests: XCTestCase { locationManagerSubject.send(.didChangeAuthorization(.denied)) }, .receive(.locationManager(.didChangeAuthorization(.denied))) { - $0.alert = "Location makes this app better. Please consider giving us access." + $0.alert = .init( + title: "Location makes this app better. Please consider giving us access." + ) $0.isRequestingCurrentLocation = false }, From 20bf1becd524ddb194828b316db7cbba58deba7c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 30 Jun 2020 09:47:24 -0400 Subject: [PATCH 25/29] Fix --- Examples/LocationManager/Desktop/LocationManagerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/LocationManager/Desktop/LocationManagerView.swift b/Examples/LocationManager/Desktop/LocationManagerView.swift index dd270766046f..7f1446fec73a 100644 --- a/Examples/LocationManager/Desktop/LocationManagerView.swift +++ b/Examples/LocationManager/Desktop/LocationManagerView.swift @@ -49,7 +49,7 @@ struct LocationManagerView: View { .padding([.bottom], 16) } } - .alert(self.store(scope: { $0.alert }), dismiss: .dismissAlertButtonTapped) + .alert(self.store.scope(state: { $0.alert }), dismiss: .dismissAlertButtonTapped) .onAppear { viewStore.send(.onAppear) } } } From 629d649379021e4195109133031def13e0be6adf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 08:47:13 -0500 Subject: [PATCH 26/29] doc fixes --- Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift | 3 ++- Sources/ComposableArchitecture/SwiftUI/Alert.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift index 17a872c928aa..68ad40f7d0c9 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -146,7 +146,8 @@ extension ActionSheetState: Identifiable where Action: Hashable { } extension View { - /// Displays an action sheet when `state` is in the `.show` state. + /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it + /// becomes `nil`. /// /// - Parameters: /// - store: A store that describes if the action sheet is shown or dismissed. diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift index 532c7dc7a45d..1fbbe756a828 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -158,7 +158,8 @@ public struct AlertState { } extension View { - /// Displays an alert when `state` is in the `.show` state. + /// Displays an alert when then store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. /// /// - Parameters: /// - store: A store that describes if the alert is shown or dismissed. From f767f9f4400ce0cee5ef09c5db1a98b4cf2624f7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 08:51:20 -0500 Subject: [PATCH 27/29] rename --- Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift | 2 +- .../01-GettingStarted-AlertsAndActionSheets.swift | 4 ++-- .../01-GettingStarted-AlertsAndActionSheetsTests.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift index 0656d30aed24..ab83378b0b6f 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -67,7 +67,7 @@ struct RootView: View { destination: AlertAndSheetView( store: .init( initialState: .init(), - reducer: AlertAndSheetReducer, + reducer: alertAndSheetReducer, environment: .init() ) ) diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift index 51fe1cb2c7b9..1e2cc0169c16 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndActionSheets.swift @@ -35,7 +35,7 @@ enum AlertAndSheetAction: Equatable { struct AlertAndSheetEnvironment {} -let AlertAndSheetReducer = Reducer< +let alertAndSheetReducer = Reducer< AlertAndSheetState, AlertAndSheetAction, AlertAndSheetEnvironment > { state, action, _ in @@ -115,7 +115,7 @@ struct AlertAndSheet_Previews: PreviewProvider { AlertAndSheetView( store: .init( initialState: .init(), - reducer: AlertAndSheetReducer, + reducer: alertAndSheetReducer, environment: .init() ) ) diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift index b1fe9b8d56c6..2ebbea9fcb83 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndActionSheetsTests.swift @@ -9,7 +9,7 @@ class AlertsAndActionSheetsTests: XCTestCase { func testAlert() { let store = TestStore( initialState: AlertAndSheetState(), - reducer: AlertAndSheetReducer, + reducer: alertAndSheetReducer, environment: AlertAndSheetEnvironment() ) @@ -32,7 +32,7 @@ class AlertsAndActionSheetsTests: XCTestCase { func testActionSheet() { let store = TestStore( initialState: AlertAndSheetState(), - reducer: AlertAndSheetReducer, + reducer: alertAndSheetReducer, environment: AlertAndSheetEnvironment() ) From 8490cf383146a0d15ad245ecee922e4fe483738c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 09:10:22 -0500 Subject: [PATCH 28/29] fixes --- .../SpeechRecognitionTests.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift b/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift index 320f39f03fac..20c45be18d2c 100644 --- a/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift +++ b/Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift @@ -26,8 +26,11 @@ class SpeechRecognitionTests: XCTestCase { }, .do { self.scheduler.advance() }, .receive(.speechRecognizerAuthorizationStatusResponse(.denied)) { - $0.authorizationStateAlert = - "You denied access to speech recognition. This app needs access to transcribe your speech." + $0.alert = .init( + title: """ + You denied access to speech recognition. This app needs access to transcribe your speech. + """ + ) $0.isRecording = false $0.speechRecognizerAuthorizationStatus = .denied } @@ -52,7 +55,7 @@ class SpeechRecognitionTests: XCTestCase { }, .do { self.scheduler.advance() }, .receive(.speechRecognizerAuthorizationStatusResponse(.restricted)) { - $0.authorizationStateAlert = "Your device does not allow speech recognition." + $0.alert = .init(title: "Your device does not allow speech recognition.") $0.isRecording = false $0.speechRecognizerAuthorizationStatus = .restricted } @@ -136,7 +139,7 @@ class SpeechRecognitionTests: XCTestCase { .do { self.recognitionTaskSubject.send(completion: .failure(.couldntConfigureAudioSession)) }, .receive(.speech(.failure(.couldntConfigureAudioSession))) { - $0.authorizationStateAlert = "Problem with audio device. Please try again." + $0.alert = .init(title: "Problem with audio device. Please try again.") }, .do { self.recognitionTaskSubject.send(completion: .finished) } @@ -168,7 +171,7 @@ class SpeechRecognitionTests: XCTestCase { .do { self.recognitionTaskSubject.send(completion: .failure(.couldntStartAudioEngine)) }, .receive(.speech(.failure(.couldntStartAudioEngine))) { - $0.authorizationStateAlert = "Problem with audio device. Please try again." + $0.alert = .init(title: "Problem with audio device. Please try again.") }, .do { self.recognitionTaskSubject.send(completion: .finished) } From abfe5b5c537e30cf9c85600cc04d2987ab15b338 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 30 Jun 2020 09:11:54 -0500 Subject: [PATCH 29/29] fixes --- Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift b/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift index ef3907444c9f..2a508a81a8bf 100644 --- a/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift +++ b/Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift @@ -97,11 +97,11 @@ class VoiceMemosTests: XCTestCase { .send(.recordButtonTapped), .do { self.scheduler.advance() }, .receive(.recordPermissionBlockCalled(false)) { - $0.alertMessage = "Permission is required to record voice memos." + $0.alert = .init(title: "Permission is required to record voice memos.") $0.audioRecorderPermission = .denied }, .send(.alertDismissed) { - $0.alertMessage = nil + $0.alert = nil }, .send(.openSettingsButtonTapped), .do { XCTAssert(didOpenSettings) } @@ -142,7 +142,7 @@ class VoiceMemosTests: XCTestCase { }, .do { audioRecorderSubject.send(completion: .failure(.couldntActivateAudioSession)) }, .receive(.audioRecorderClient(.failure(.couldntActivateAudioSession))) { - $0.alertMessage = "Voice memo recording failed." + $0.alert = .init(title: "Voice memo recording failed.") $0.currentRecording = nil } ) @@ -223,7 +223,7 @@ class VoiceMemosTests: XCTestCase { $0.voiceMemos[0].mode = .playing(progress: 0) }, .receive(.voiceMemo(index: 0, action: .audioPlayerClient(.failure(.decodeErrorDidOccur)))) { - $0.alertMessage = "Voice memo playback failed." + $0.alert = .init(title: "Voice memo playback failed.") $0.voiceMemos[0].mode = .notPlaying } )