Onboarding TCA intro

This commit is contained in:
adam 2021-10-12 06:25:34 -05:00
parent f39c458669
commit 0169164472
6 changed files with 331 additions and 2 deletions

View File

@ -1,4 +1,4 @@
# Architecture
Our App architecture is based on the [SwiftUI Router Pattern](https://github.com/roboheadz/swiftui-router) by @roboheadz
Our App architecture is based on [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture) by pointfreeco

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
@ -53,6 +53,10 @@
660558E9270C7A54009D6954 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 660558E8270C7A54009D6954 /* Colors.xcassets */; };
660558F7270C862F009D6954 /* Fonts+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F5270C862F009D6954 /* Fonts+Generated.swift */; };
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F6270C862F009D6954 /* XCAssets+Generated.swift */; };
6654C73A2715A38000901167 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 6654C7392715A38000901167 /* ComposableArchitecture */; };
6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6654C73D2715A41300901167 /* OnboardingStore.swift */; };
6654C7412715A47300901167 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6654C7402715A47300901167 /* Onboarding.swift */; };
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -125,6 +129,9 @@
660558E8270C7A54009D6954 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
660558F5270C862F009D6954 /* Fonts+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fonts+Generated.swift"; sourceTree = "<group>"; };
660558F6270C862F009D6954 /* XCAssets+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCAssets+Generated.swift"; sourceTree = "<group>"; };
6654C73D2715A41300901167 /* OnboardingStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStore.swift; sourceTree = "<group>"; };
6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -132,6 +139,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6654C73A2715A38000901167 /* ComposableArchitecture in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -292,6 +300,7 @@
0D4E7A0726B364170058B01E /* secant */ = {
isa = PBXGroup;
children = (
6654C73B2715A3F000901167 /* Features */,
660558F4270C85F7009D6954 /* Generated */,
0D5D16F326E24CB900AD33D1 /* App Errors */,
0D2ACE7E26C2C65E00D62E3C /* Fonts */,
@ -323,6 +332,7 @@
0D4E7A1926B364180058B01E /* secantTests */ = {
isa = PBXGroup;
children = (
6654C7422715A48E00901167 /* OnboardingTests */,
0D4E7A1A26B364180058B01E /* secantTests.swift */,
0D4E7A1C26B364180058B01E /* Info.plist */,
0D864A0426E1546000A61879 /* LoadingScreenTests.swift */,
@ -426,6 +436,39 @@
path = Generated;
sourceTree = "<group>";
};
6654C73B2715A3F000901167 /* Features */ = {
isa = PBXGroup;
children = (
6654C73C2715A3FA00901167 /* Onboarding */,
);
path = Features;
sourceTree = "<group>";
};
6654C73C2715A3FA00901167 /* Onboarding */ = {
isa = PBXGroup;
children = (
6654C73D2715A41300901167 /* OnboardingStore.swift */,
6654C73F2715A45900901167 /* Views */,
);
path = Onboarding;
sourceTree = "<group>";
};
6654C73F2715A45900901167 /* Views */ = {
isa = PBXGroup;
children = (
6654C7402715A47300901167 /* Onboarding.swift */,
);
path = Views;
sourceTree = "<group>";
};
6654C7422715A48E00901167 /* OnboardingTests */ = {
isa = PBXGroup;
children = (
6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */,
);
path = OnboardingTests;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -444,6 +487,9 @@
dependencies = (
);
name = "secant-testnet";
packageProductDependencies = (
6654C7392715A38000901167 /* ComposableArchitecture */,
);
productName = secant;
productReference = 0D4E7A0526B364170058B01E /* secant-testnet.app */;
productType = "com.apple.product-type.application";
@ -515,6 +561,9 @@
Base,
);
mainGroup = 0D4E79FC26B364170058B01E;
packageReferences = (
6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */,
);
productRefGroup = 0D4E7A0626B364170058B01E /* Products */;
projectDirPath = "";
projectRoot = "";
@ -609,6 +658,7 @@
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */,
0DA13C9D26C1942100E3B610 /* BackupWalletScreenViewModel.swift in Sources */,
0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */,
6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */,
0D32281E26C5867D00262533 /* ScanQrScreen.swift in Sources */,
0D864A0E26E1583000A61879 /* LoadingScreen.swift in Sources */,
0DA13C9C26C1942100E3B610 /* BackupWalletScreen.swift in Sources */,
@ -639,6 +689,7 @@
0DA13CA226C1955600E3B610 /* HomeScreenViewModel.swift in Sources */,
0D32282926C586E000262533 /* RequestZcashScreenViewModel.swift in Sources */,
0D32281926C5864B00262533 /* ProfileScreen.swift in Sources */,
6654C7412715A47300901167 /* Onboarding.swift in Sources */,
0D32282426C586A800262533 /* HistoryScreenViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -648,6 +699,7 @@
buildActionMask = 2147483647;
files = (
0D864A0526E1546000A61879 /* LoadingScreenTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -960,6 +1012,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture";
requirement = {
kind = exactVersion;
version = 0.9.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
6654C7392715A38000901167 /* ComposableArchitecture */ = {
isa = XCSwiftPackageProductDependency;
package = 6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */;
productName = ComposableArchitecture;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 0D4E79FD26B364170058B01E /* Project object */;
}

View File

@ -0,0 +1,43 @@
{
"object": {
"pins": [
{
"package": "combine-schedulers",
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
"state": {
"branch": null,
"revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b",
"version": "0.5.3"
}
},
{
"package": "swift-case-paths",
"repositoryURL": "https://github.com/pointfreeco/swift-case-paths",
"state": {
"branch": null,
"revision": "d226d167bd4a68b51e352af5655c92bce8ee0463",
"version": "0.7.0"
}
},
{
"package": "swift-composable-architecture",
"repositoryURL": "https://github.com/pointfreeco/swift-composable-architecture",
"state": {
"branch": null,
"revision": "b67569f69813140cd9c984db33ee959d9711a008",
"version": "0.9.0"
}
},
{
"package": "xctest-dynamic-overlay",
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state": {
"branch": null,
"revision": "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version": "0.2.1"
}
}
]
},
"version": 1
}

View File

@ -0,0 +1,48 @@
//
// Onboarding.swift
// OnboardingTCA
//
// Created by Adam Stener on 10/10/21.
//
import Foundation
import SwiftUI
import ComposableArchitecture
struct OnboardingStep: Equatable, Identifiable {
let id: UUID
let description: String
let imageName: String
}
struct OnboardingState: Equatable {
var steps: IdentifiedArrayOf<OnboardingStep>
var index = 0
var offset: CGFloat = .zero
var currentStep: OnboardingStep { steps[index] }
var nextButtonDisabled: Bool { steps.count == index + 1 }
var backButtonDisabled: Bool { index == 0 }
var progress: Int {
((index + 1) * 100) / (steps.count)
}
}
enum OnboardingAction: Equatable {
case nextPressed
case backPressed
}
let onboardingReducer = Reducer<OnboardingState, OnboardingAction, Void> { state, action, _ in
switch action {
case .backPressed:
state.index -= 1
state.offset += 20.0
return .none
case .nextPressed:
state.index += 1
state.offset -= 20.0
return .none
}
}

View File

@ -0,0 +1,95 @@
//
// Onboarding.swift
// secant-testnet
//
// Created by Adam Stener on 10/12/21.
//
import SwiftUI
import ComposableArchitecture
struct OnboardingView: View {
let store: Store<OnboardingState, OnboardingAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack(spacing: 50) {
HStack(spacing: 50) {
Button(
action: { viewStore.send(.backPressed) },
label: { Text("Previous") }
)
.disabled(viewStore.backButtonDisabled)
Spacer()
Button(
action: { viewStore.send(.nextPressed) },
label: { Text("Next") }
)
.disabled(viewStore.nextButtonDisabled)
}
.frame(height: 100)
.padding(.horizontal, 50)
Spacer()
Text(viewStore.currentStep.imageName)
.frame(maxWidth: .infinity)
.offset(y: viewStore.offset)
.animation(.easeOut(duration: 0.4))
Spacer()
VStack {
Text(viewStore.currentStep.description)
ProgressView(
"Progress \(viewStore.progress)%",
value: Double(viewStore.index + 1),
total: Double(viewStore.steps.count)
)
.padding(.horizontal, 25)
.padding(.vertical, 50)
}
.animation(.easeOut(duration: 0.2))
}
}
}
}
extension OnboardingState {
static let steps = IdentifiedArray(
uniqueElements: [
OnboardingStep(
id: UUID(),
description: "This is the description of the first onboarding step, please read it carefully.",
imageName: "Image"
),
OnboardingStep(
id: UUID(),
description: "The second step is even more important, have to pay attention to the details here.",
imageName: "Image"
),
OnboardingStep(
id: UUID(),
description: "Congratulations you made it all the way through to the end, you can use the app now!",
imageName: "Image"
)
]
)
}
struct Onboarding_Previews: PreviewProvider {
static var previews: some View {
Group {
OnboardingView(
store: Store(
initialState: OnboardingState(steps: OnboardingState.steps),
reducer: onboardingReducer,
environment: ()
)
)
}
}
}

View File

@ -0,0 +1,72 @@
//
// OnboardingStoreTests.swift
// OnboardingStoreTests
//
// Created by Adam Stener on 10/10/21.
//
import XCTest
import ComposableArchitecture
@testable import secant_testnet
class OnboardingStoreTests: XCTestCase {
func testIncrementingOnboarding() {
let store = TestStore(
initialState: OnboardingState(steps: OnboardingState.steps),
reducer: onboardingReducer,
environment: ()
)
store.send(.nextPressed) {
$0.index += 1
$0.offset -= 20.0
XCTAssertFalse($0.nextButtonDisabled)
XCTAssertFalse($0.backButtonDisabled)
XCTAssertEqual($0.currentStep, OnboardingState.steps[1])
XCTAssertEqual($0.progress, 66)
}
store.send(.nextPressed) {
$0.index += 1
$0.offset -= 20.0
XCTAssertTrue($0.nextButtonDisabled)
XCTAssertFalse($0.backButtonDisabled)
XCTAssertEqual($0.currentStep, OnboardingState.steps[2])
XCTAssertEqual($0.progress, 100)
}
}
func testDecrementingOnboarding() {
let store = TestStore(
initialState: OnboardingState(
steps: OnboardingState.steps,
index: 2,
offset: .zero - 20.0 - 20.0
),
reducer: onboardingReducer,
environment: ()
)
store.send(.backPressed) {
$0.index -= 1
$0.offset += 20.0
XCTAssertFalse($0.nextButtonDisabled)
XCTAssertFalse($0.backButtonDisabled)
XCTAssertEqual($0.currentStep, OnboardingState.steps[1])
XCTAssertEqual($0.progress, 66)
}
store.send(.backPressed) {
$0.index -= 1
$0.offset += 20.0
XCTAssertFalse($0.nextButtonDisabled)
XCTAssertTrue($0.backButtonDisabled)
XCTAssertEqual($0.currentStep, OnboardingState.steps[0])
XCTAssertEqual($0.progress, 33)
}
}
}