Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.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-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */; };
CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */; };
CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5ECF91267A79F0002067FF /* FactClient.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 */; };
Expand All @@ -32,7 +32,7 @@
CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */; };
CABC4F3926AEE00C00D5FA2C /* 02-Effects-Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3826AEE00C00D5FA2C /* 02-Effects-Refreshable.swift */; };
CABC4F3B26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */; };
CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */; };
CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */; };
CAF069D024ACC5AF00A1AAEF /* 00-Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */; };
CAF88E7324B8E26D00539345 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E7224B8E26D00539345 /* AppDelegate.swift */; };
CAF88E7524B8E26D00539345 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E7424B8E26D00539345 /* RootView.swift */; };
Expand Down Expand Up @@ -160,7 +160,7 @@
CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-Lifecycle.swift"; sourceTree = "<group>"; };
CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = "<group>"; };
CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = "<group>"; };
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheetsTests.swift"; sourceTree = "<group>"; };
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndConfirmationDialogsTests.swift"; sourceTree = "<group>"; };
CA5ECF91267A79F0002067FF /* FactClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactClient.swift; sourceTree = "<group>"; };
CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReusableComponents-Download.swift"; sourceTree = "<group>"; };
CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
Expand All @@ -175,7 +175,7 @@
CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLivingTests.swift"; sourceTree = "<group>"; };
CABC4F3826AEE00C00D5FA2C /* 02-Effects-Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Refreshable.swift"; sourceTree = "<group>"; };
CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-RefreshableTests.swift"; sourceTree = "<group>"; };
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheets.swift"; sourceTree = "<group>"; };
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndConfirmationDialogs.swift"; sourceTree = "<group>"; };
CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "00-Core.swift"; sourceTree = "<group>"; };
CAF88E7024B8E26D00539345 /* tvOSCaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tvOSCaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; };
CAF88E7224B8E26D00539345 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -393,7 +393,7 @@
DC89C42424460F96006900B9 /* Info.plist */,
CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */,
DC89C41A24460F95006900B9 /* 00-RootView.swift */,
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */,
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */,
DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */,
CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */,
DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */,
Expand Down Expand Up @@ -432,7 +432,7 @@
DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */ = {
isa = PBXGroup;
children = (
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */,
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */,
CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */,
DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */,
4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */,
Expand Down Expand Up @@ -763,7 +763,7 @@
DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */,
CABC4F3926AEE00C00D5FA2C /* 02-Effects-Refreshable.swift in Sources */,
DCC68EAB244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift in Sources */,
CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift in Sources */,
CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.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 */,
Expand Down Expand Up @@ -791,7 +791,7 @@
CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */,
CA0C0C4724B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift in Sources */,
DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */,
CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */,
CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */,
CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */,
4F5AC11F24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift in Sources */,
CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */,
Expand Down
10 changes: 5 additions & 5 deletions Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import UIKit
import XCTestDynamicOverlay

struct RootState {
var alertAndActionSheet = AlertAndSheetState()
var alertAndConfirmationDialog = AlertAndConfirmationDialogState()
var animation = AnimationsState()
var bindingBasics = BindingBasicsState()
#if compiler(>=5.4)
Expand Down Expand Up @@ -40,7 +40,7 @@ struct RootState {
}

enum RootAction {
case alertAndActionSheet(AlertAndSheetAction)
case alertAndConfirmationDialog(AlertAndConfirmationDialogAction)
case animation(AnimationsAction)
case bindingBasics(BindingBasicsAction)
#if compiler(>=5.4)
Expand Down Expand Up @@ -110,10 +110,10 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
return .none
}
},
alertAndSheetReducer
alertAndConfirmationDialogReducer
.pullback(
state: \.alertAndActionSheet,
action: /RootAction.alertAndActionSheet,
state: \.alertAndConfirmationDialog,
action: /RootAction.alertAndConfirmationDialog,
environment: { _ in .init() }
),
animationsReducer
Expand Down
8 changes: 4 additions & 4 deletions Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ struct RootView: View {
)

NavigationLink(
"Alerts and Action Sheets",
destination: AlertAndSheetView(
"Alerts and Confirmation Dialogs",
destination: AlertAndConfirmationDialogView(
store: self.store.scope(
state: \.alertAndActionSheet,
action: RootAction.alertAndActionSheet
state: \.alertAndConfirmationDialog,
action: RootAction.alertAndConfirmationDialog
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,50 @@ import ComposableArchitecture
import SwiftUI

private let readMe = """
This demonstrates how to best handle alerts and action sheets in the Composable Architecture.
This demonstrates how to best handle alerts and confirmation dialogs 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.

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. \
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.
However, the library comes with two types, `AlertState` and `ConfirmationDialogState`, which can \
be constructed from reducers and control whether or not an alert or confirmation dialog 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 \
alerts and action sheets in your application
alerts and dialogs in your application
"""

struct AlertAndSheetState: Equatable {
var actionSheet: ActionSheetState<AlertAndSheetAction>?
var alert: AlertState<AlertAndSheetAction>?
struct AlertAndConfirmationDialogState: Equatable {
var alert: AlertState<AlertAndConfirmationDialogAction>?
var confirmationDialog: ConfirmationDialogState<AlertAndConfirmationDialogAction>?
var count = 0
}

enum AlertAndSheetAction: Equatable {
case actionSheetButtonTapped
case actionSheetDismissed
enum AlertAndConfirmationDialogAction: Equatable {
case alertButtonTapped
case alertDismissed
case confirmationDialogButtonTapped
case confirmationDialogDismissed
case decrementButtonTapped
case incrementButtonTapped
}

struct AlertAndSheetEnvironment {}
struct AlertAndConfirmationDialogEnvironment {}

let alertAndSheetReducer = Reducer<
AlertAndSheetState, AlertAndSheetAction, AlertAndSheetEnvironment
let alertAndConfirmationDialogReducer = Reducer<
AlertAndConfirmationDialogState, AlertAndConfirmationDialogAction, AlertAndConfirmationDialogEnvironment
> { state, action, _ in

switch action {
case .actionSheetButtonTapped:
state.actionSheet = .init(
title: .init("Action sheet"),
message: .init("This is an action sheet."),
buttons: [
.cancel(),
.default(.init("Increment"), action: .send(.incrementButtonTapped)),
.default(.init("Decrement"), action: .send(.decrementButtonTapped)),
]
)
return .none

case .actionSheetDismissed:
state.actionSheet = nil
return .none

case .alertButtonTapped:
state.alert = .init(
title: .init("Alert!"),
message: .init("This is an alert"),
primaryButton: .cancel(),
primaryButton: .cancel(.init("Cancel")),
secondaryButton: .default(.init("Increment"), action: .send(.incrementButtonTapped))
)
return .none
Expand All @@ -69,6 +54,22 @@ let alertAndSheetReducer = Reducer<
state.alert = nil
return .none

case .confirmationDialogButtonTapped:
state.confirmationDialog = .init(
title: .init("Confirmation dialog"),
message: .init("This is a confirmation dialog."),
buttons: [
.cancel(.init("Cancel")),
.default(.init("Increment"), action: .send(.incrementButtonTapped)),
.default(.init("Decrement"), action: .send(.decrementButtonTapped)),
]
)
return .none

case .confirmationDialogDismissed:
state.confirmationDialog = nil
return .none

case .decrementButtonTapped:
state.alert = .init(title: .init("Decremented!"))
state.count -= 1
Expand All @@ -81,8 +82,8 @@ let alertAndSheetReducer = Reducer<
}
}

struct AlertAndSheetView: View {
let store: Store<AlertAndSheetState, AlertAndSheetAction>
struct AlertAndConfirmationDialogView: View {
let store: Store<AlertAndConfirmationDialogState, AlertAndConfirmationDialogAction>

var body: some View {
WithViewStore(self.store) { viewStore in
Expand All @@ -96,25 +97,25 @@ struct AlertAndSheetView: View {
dismiss: .alertDismissed
)

Button("Action sheet") { viewStore.send(.actionSheetButtonTapped) }
.actionSheet(
self.store.scope(state: \.actionSheet),
dismiss: .actionSheetDismissed
Button("Confirmation Dialog") { viewStore.send(.confirmationDialogButtonTapped) }
.confirmationDialog(
self.store.scope(state: \.confirmationDialog),
dismiss: .confirmationDialogDismissed
)
}
}
}
.navigationBarTitle("Alerts & Action Sheets")
.navigationBarTitle("Alerts & Confirmation Dialogs")
}
}

struct AlertAndSheet_Previews: PreviewProvider {
struct AlertAndConfirmationDialog_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AlertAndSheetView(
AlertAndConfirmationDialogView(
store: .init(
initialState: .init(),
reducer: alertAndSheetReducer,
reducer: alertAndConfirmationDialogReducer,
environment: .init()
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ let animationsReducer = Reducer<AnimationsState, AnimationsAction, AnimationsEnv
.init("Reset"),
action: .send(.resetConfirmationButtonTapped, animation: .default)
),
secondaryButton: .cancel()
secondaryButton: .cancel(.init("Cancel"))
)
return .none

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import XCTest

@testable import SwiftUICaseStudies

class AlertsAndActionSheetsTests: XCTestCase {
class AlertsAndConfirmationDialogsTests: XCTestCase {
func testAlert() {
let store = TestStore(
initialState: AlertAndSheetState(),
reducer: alertAndSheetReducer,
environment: AlertAndSheetEnvironment()
initialState: AlertAndConfirmationDialogState(),
reducer: alertAndConfirmationDialogReducer,
environment: AlertAndConfirmationDialogEnvironment()
)

store.send(.alertButtonTapped) {
$0.alert = .init(
title: .init("Alert!"),
message: .init("This is an alert"),
primaryButton: .cancel(),
primaryButton: .cancel(.init("Cancel")),
secondaryButton: .default(.init("Increment"), action: .send(.incrementButtonTapped))
)
}
Expand All @@ -30,19 +30,19 @@ class AlertsAndActionSheetsTests: XCTestCase {
}
}

func testActionSheet() {
func testConfirmationDialog() {
let store = TestStore(
initialState: AlertAndSheetState(),
reducer: alertAndSheetReducer,
environment: AlertAndSheetEnvironment()
initialState: AlertAndConfirmationDialogState(),
reducer: alertAndConfirmationDialogReducer,
environment: AlertAndConfirmationDialogEnvironment()
)

store.send(.actionSheetButtonTapped) {
$0.actionSheet = .init(
title: .init("Action sheet"),
message: .init("This is an action sheet."),
store.send(.confirmationDialogButtonTapped) {
$0.confirmationDialog = .init(
title: .init("Confirmation dialog"),
message: .init("This is a confirmation dialog."),
buttons: [
.cancel(),
.cancel(.init("Cancel")),
.default(.init("Increment"), action: .send(.incrementButtonTapped)),
.default(.init("Decrement"), action: .send(.decrementButtonTapped)),
]
Expand All @@ -52,8 +52,8 @@ class AlertsAndActionSheetsTests: XCTestCase {
$0.alert = .init(title: .init("Incremented!"))
$0.count = 1
}
store.send(.actionSheetDismissed) {
$0.actionSheet = nil
store.send(.confirmationDialogDismissed) {
$0.confirmationDialog = nil
}
}
}
13 changes: 13 additions & 0 deletions Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SwiftUI

extension Binding {
func isPresent<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
.init(
get: { self.wrappedValue != nil },
set: { isPresent, transaction in
guard !isPresent else { return }
self.transaction(transaction).wrappedValue = nil
}
)
}
}
Loading