Skip to content

Commit

Permalink
Generic alerts and action sheets (#201)
Browse files Browse the repository at this point in the history
* alerts

* wip

* wip

* wip

* clean up

* wip

* wip

* wip

* wip

* format

* clean up

* clean up

* docs

* wip

* tests

* API tweaks

* Fix

* More API changes

* More API changes

* More

* Fix

* Fix docs

* Generic alerts optionality (#202)

* Use Optional to model generic alerts

* Xcode 12

* Refinement

* update docs

* Fix

* Fix

* doc fixes

* rename

* fixes

* fixes

Co-authored-by: Stephen Celis <stephen@stephencelis.com>
  • Loading branch information
mbrandonw and stephencelis authored Jun 30, 2020
1 parent 2ce84cc commit a905fbf
Show file tree
Hide file tree
Showing 68 changed files with 814 additions and 339 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1150"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
18 changes: 13 additions & 5 deletions Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +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-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 */; };
Expand All @@ -24,6 +25,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 */; };
Expand Down Expand Up @@ -125,12 +127,13 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ResuableOfflineDownloadsTests.swift"; sourceTree = "<group>"; };
CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift"; sourceTree = "<group>"; };
CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Basics.swift"; sourceTree = "<group>"; };
CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-SystemEnvironment.swift"; sourceTree = "<group>"; };
CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AnimationsTests.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>"; };
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>"; };
CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = "<group>"; };
Expand All @@ -142,6 +145,7 @@
CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-CancellationTests.swift"; sourceTree = "<group>"; };
CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLiving.swift"; sourceTree = "<group>"; };
CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLivingTests.swift"; sourceTree = "<group>"; };
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheets.swift"; sourceTree = "<group>"; };
DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-TimersTests.swift"; sourceTree = "<group>"; };
DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Sheet-LoadThenPresent.swift"; sourceTree = "<group>"; };
DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfLetStoreController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -308,6 +312,7 @@
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 */,
Expand Down Expand Up @@ -341,14 +346,15 @@
DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */ = {
isa = PBXGroup;
children = (
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */,
CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.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 = "<group>";
Expand Down Expand Up @@ -465,7 +471,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1140;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "Point-Free";
TargetAttributes = {
DC4C6EA62450DD380066A05D = {
Expand Down Expand Up @@ -589,6 +595,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 */,
Expand All @@ -607,12 +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-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */,
CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */,
CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
11 changes: 11 additions & 0 deletions Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ struct RootView: View {
)
)

NavigationLink(
"Alerts and Action Sheets",
destination: AlertAndSheetView(
store: .init(
initialState: .init(),
reducer: alertAndSheetReducer,
environment: .init()
)
)
)

NavigationLink(
"Animations",
destination: AnimationsView(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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 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.
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 AlertAndSheetState: Equatable {
var actionSheet: ActionSheetState<AlertAndSheetAction>?
var alert: AlertState<AlertAndSheetAction>?
var count = 0
}

enum AlertAndSheetAction: Equatable {
case actionSheetButtonTapped
case actionSheetCancelTapped
case alertButtonTapped
case alertCancelTapped
case decrementButtonTapped
case incrementButtonTapped
}

struct AlertAndSheetEnvironment {}

let alertAndSheetReducer = Reducer<
AlertAndSheetState, AlertAndSheetAction, AlertAndSheetEnvironment
> { state, action, _ in

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

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

case .alertButtonTapped:
state.alert = .init(
title: "Alert!",
message: "This is an alert",
primaryButton: .cancel(),
secondaryButton: .default("Increment", send: .incrementButtonTapped)
)
return .none

case .alertCancelTapped:
state.alert = nil
return .none

case .decrementButtonTapped:
state.actionSheet = nil
state.count -= 1
return .none

case .incrementButtonTapped:
state.actionSheet = nil
state.alert = nil
state.count += 1
return .none
}
}

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

var body: some View {
WithViewStore(self.store) { viewStore in
Form {
Section(header: Text(template: readMe, .caption)) {
Text("Count: \(viewStore.count)")

Button("Alert") { viewStore.send(.alertButtonTapped) }
.alert(
self.store.scope(state: { $0.alert }),
dismiss: .alertCancelTapped
)

Button("Action sheet") { viewStore.send(.actionSheetButtonTapped) }
.actionSheet(
self.store.scope(state: { $0.actionSheet }),
dismiss: .actionSheetCancelTapped
)
}
}
}
.navigationBarTitle("Alerts & Action Sheets")
}
}

struct AlertAndSheet_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AlertAndSheetView(
store: .init(
initialState: .init(),
reducer: alertAndSheetReducer,
environment: .init()
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct SharedState: Equatable {
enum Tab { case counter, profile }

struct CounterState: Equatable {
var alert: String?
var alert: AlertState<SharedStateAction.CounterAction>?
var count = 0
var maxCount = 0
var minCount = 0
Expand Down Expand Up @@ -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 = .init(
title: isPrime(state.count)
? "👍 The number \(state.count) is prime!"
: "👎 The number \(state.count) is not prime :("
)
return .none
}
}
Expand Down Expand Up @@ -204,14 +205,7 @@ struct SharedStateCounterView: View {
.padding(16)
.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))
}
.alert(self.store.scope(state: { $0.alert }), dismiss: .alertDismissed)
}
}
}
Expand Down Expand Up @@ -249,11 +243,6 @@ struct SharedStateProfileView: View {
}
}

private struct PrimeAlert: Equatable, Identifiable {
let title: String
var id: String { self.title }
}

// MARK: - SwiftUI previews

struct SharedState_Previews: PreviewProvider {
Expand Down
Loading

0 comments on commit a905fbf

Please sign in to comment.