Merge pull request #42 from adamstener/Onboarding-TCA
Onboarding TCA intro
This commit is contained in:
commit
baea8f30fc
|
@ -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
|
||||
|
||||
|
|
|
@ -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 */;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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: ()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue