Onboarding Screen

This commit is contained in:
adam 2021-11-09 05:59:03 -06:00
parent 883ce52011
commit cc32e881ba
13 changed files with 579 additions and 94 deletions

View File

@ -72,6 +72,10 @@
0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; }; 0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */; };
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; }; 0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */; };
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; }; 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */; };
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */; };
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */; };
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA11F5C27467F7700709571 /* OnboardingContentView.swift */; };
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */; };
660558E9270C7A54009D6954 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 660558E8270C7A54009D6954 /* Colors.xcassets */; }; 660558E9270C7A54009D6954 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 660558E8270C7A54009D6954 /* Colors.xcassets */; };
660558F7270C862F009D6954 /* Fonts+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F5270C862F009D6954 /* Fonts+Generated.swift */; }; 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 */; }; 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F6270C862F009D6954 /* XCAssets+Generated.swift */; };
@ -190,6 +194,10 @@
0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; }; 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignGuide.swift; sourceTree = "<group>"; };
0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; }; 0DF2DC50272344E400FA31E2 /* EmptyChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyChip.swift; sourceTree = "<group>"; };
0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; }; 0DF2DC5327235E3E00FA31E2 /* View+InnerShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InnerShadow.swift"; sourceTree = "<group>"; };
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeaderView.swift; sourceTree = "<group>"; };
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = "<group>"; };
2EA11F5C27467F7700709571 /* OnboardingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContentView.swift; sourceTree = "<group>"; };
660558E8270C7A54009D6954 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 660558F6270C862F009D6954 /* XCAssets+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCAssets+Generated.swift"; sourceTree = "<group>"; };
@ -201,6 +209,7 @@
6654C7402715A47300901167 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.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>"; }; 6654C7432715A4AC00901167 /* OnboardingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStoreTests.swift; sourceTree = "<group>"; };
665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; }; 665C963E272C26E600BC04FB /* CircularFrameBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBackground.swift; sourceTree = "<group>"; };
66779071273AAC26003A1540 /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; }; 669FDAE8272C23B3007B9422 /* CircularFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrame.swift; sourceTree = "<group>"; };
669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; }; 669FDAEA272C23C2007B9422 /* CircularFrameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularFrameBadge.swift; sourceTree = "<group>"; };
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; }; 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
@ -274,6 +283,7 @@
0D1922EB26BDD9A500052649 /* Screens */ = { 0D1922EB26BDD9A500052649 /* Screens */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2E5C037F2738C55F008BFFD3 /* Onboarding */,
0D864A0B26E1580700A61879 /* Loading */, 0D864A0B26E1580700A61879 /* Loading */,
0D864A0626E154D100A61879 /* Error */, 0D864A0626E154D100A61879 /* Error */,
0D32282F26C5874B00262533 /* Balance */, 0D32282F26C5874B00262533 /* Balance */,
@ -370,6 +380,7 @@
0D4E7A1926B364180058B01E /* secantTests */, 0D4E7A1926B364180058B01E /* secantTests */,
0D4E7A2426B364180058B01E /* secantUITests */, 0D4E7A2426B364180058B01E /* secantUITests */,
0D4E7A0626B364170058B01E /* Products */, 0D4E7A0626B364170058B01E /* Products */,
2EB660DF2747EA6000A06A07 /* Recovered References */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -581,6 +592,25 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2E5C037F2738C55F008BFFD3 /* Onboarding */ = {
isa = PBXGroup;
children = (
2E5C03802738C570008BFFD3 /* OnboardingScreen.swift */,
2E58E73A274679F000B2B84B /* OnboardingHeaderView.swift */,
2EA11F5C27467F7700709571 /* OnboardingContentView.swift */,
2EA11F5A27467EF800709571 /* OnboardingFooterView.swift */,
);
path = Onboarding;
sourceTree = "<group>";
};
2EB660DF2747EA6000A06A07 /* Recovered References */ = {
isa = PBXGroup;
children = (
66779071273AAC26003A1540 /* OnboardingScreen.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
};
660558F4270C85F7009D6954 /* Generated */ = { 660558F4270C85F7009D6954 /* Generated */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -887,7 +917,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "swiftlint_version=0.44.0\n\nif which swiftlint >/dev/null; then\n if [ $(swiftlint version) = $swiftlint_version ]; then\n swiftlint\n else\n echo \"warning: Compatible SwiftLint version not installed, download version $swiftlint_version from https://github.com/realm/SwiftLint. Currently installed version is $(swiftlint version)\"\n fi \nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; shellScript = "swiftlint_version=0.45.0\n\nif which swiftlint >/dev/null; then\n if [ $(swiftlint version) = $swiftlint_version ]; then\n swiftlint\n else\n echo \"warning: Compatible SwiftLint version not installed, download version $swiftlint_version from https://github.com/realm/SwiftLint. Currently installed version is $(swiftlint version)\"\n fi \nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@ -896,6 +926,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */,
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */, 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */, F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
0D32281F26C5867D00262533 /* ScanQrScreenViewModel.swift in Sources */, 0D32281F26C5867D00262533 /* ScanQrScreenViewModel.swift in Sources */,
@ -929,6 +960,7 @@
0D32282326C586A800262533 /* HistoryScreen.swift in Sources */, 0D32282326C586A800262533 /* HistoryScreen.swift in Sources */,
0D864A0A26E154FD00A61879 /* InitFailedScreenViewModel.swift in Sources */, 0D864A0A26E154FD00A61879 /* InitFailedScreenViewModel.swift in Sources */,
0DA13CA526C1963000E3B610 /* Balance.swift in Sources */, 0DA13CA526C1963000E3B610 /* Balance.swift in Sources */,
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */, 66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */, 0D1922F826BDEB3500052649 /* MockServices.swift in Sources */,
0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */, 0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */,
@ -949,8 +981,10 @@
0D32281A26C5864B00262533 /* ProfileScreenViewModel.swift in Sources */, 0D32281A26C5864B00262533 /* ProfileScreenViewModel.swift in Sources */,
0D185819272723FF0046B928 /* BlueChip.swift in Sources */, 0D185819272723FF0046B928 /* BlueChip.swift in Sources */,
0D864A0F26E1583000A61879 /* LoadingScreenViewModel.swift in Sources */, 0D864A0F26E1583000A61879 /* LoadingScreenViewModel.swift in Sources */,
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
0DA13CA126C1955600E3B610 /* HomeScreen.swift in Sources */, 0DA13CA126C1955600E3B610 /* HomeScreen.swift in Sources */,
0DA13C9026C15D1D00E3B610 /* WelcomeScreenViewModel.swift in Sources */, 0DA13C9026C15D1D00E3B610 /* WelcomeScreenViewModel.swift in Sources */,
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */, 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
663FAB9E271D875700E495F8 /* CreateButton.swift in Sources */, 663FAB9E271D875700E495F8 /* CreateButton.swift in Sources */,
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */, 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */,

View File

@ -26,7 +26,10 @@ struct HomeView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Route: \(String(dumping: viewStore.route))") Text("Route: \(String(dumping: viewStore.route))")
Text("SelectedTransaction: \(String(dumping: viewStore.transactionHistoryState.route.map(/TransactionHistoryState.Route.showTransaction)))") Text(
// swiftlint:disable:next line_length
"SelectedTransaction: \(String(dumping: viewStore.transactionHistoryState.route.map(/TransactionHistoryState.Route.showTransaction)))"
)
} }
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)

View File

@ -9,20 +9,22 @@ import Foundation
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct OnboardingStep: Equatable, Identifiable { struct OnboardingState: Equatable {
struct Step: Equatable, Identifiable {
let id: UUID let id: UUID
let title: String let title: String
let description: String let description: String
} let background: Image
let badge: Badge
}
struct OnboardingState: Equatable { var steps: IdentifiedArrayOf<Step> = Self.onboardingSteps
var steps: IdentifiedArrayOf<OnboardingStep> = Self.onboardingSteps
var index = 0 var index = 0
var skippedAtindex: Int? var skippedAtindex: Int?
var currentStep: OnboardingStep { steps[index] } var currentStep: Step { steps[index] }
var skipButtonDisabled: Bool { steps.count == index + 1 } var isFinalStep: Bool { steps.count == index + 1 }
var backButtonDisabled: Bool { index == 0 } var isInitialStep: Bool { index == 0 }
var progress: Int { ((index + 1) * 100) / (steps.count) } var progress: Int { ((index + 1) * 100) / (steps.count) }
var offset: CGFloat { var offset: CGFloat {
let maxOffset = CGFloat(-60) let maxOffset = CGFloat(-60)
@ -39,7 +41,10 @@ enum OnboardingAction: Equatable {
case createNewWallet case createNewWallet
} }
let onboardingReducer = Reducer<OnboardingState, OnboardingAction, Void> { state, action, _ in typealias OnboardingReducer = Reducer<OnboardingState, OnboardingAction, Void>
extension OnboardingReducer {
static let `default` = Reducer<OnboardingState, OnboardingAction, Void> { state, action, _ in
switch action { switch action {
case .back: case .back:
guard state.index > 0 else { return .none } guard state.index > 0 else { return .none }
@ -57,6 +62,7 @@ let onboardingReducer = Reducer<OnboardingState, OnboardingAction, Void> { state
return .none return .none
case .skip: case .skip:
guard state.skippedAtindex == nil else { return .none }
state.skippedAtindex = state.index state.skippedAtindex = state.index
state.index = state.steps.count - 1 state.index = state.steps.count - 1
return .none return .none
@ -64,4 +70,5 @@ let onboardingReducer = Reducer<OnboardingState, OnboardingAction, Void> { state
case .createNewWallet: case .createNewWallet:
return .none return .none
} }
}
} }

View File

@ -16,13 +16,13 @@ struct OnboardingView: View {
VStack(spacing: 50) { VStack(spacing: 50) {
HStack(spacing: 50) { HStack(spacing: 50) {
Button("Back") { viewStore.send(.back) } Button("Back") { viewStore.send(.back) }
.disabled(viewStore.backButtonDisabled) .disabled(viewStore.isInitialStep)
Spacer() Spacer()
Button("Next") { viewStore.send(.next) } Button("Next") { viewStore.send(.next) }
Button("Skip") { viewStore.send(.skip) } Button("Skip") { viewStore.send(.skip) }
.disabled(viewStore.skipButtonDisabled) .disabled(viewStore.isFinalStep)
} }
.frame(height: 100) .frame(height: 100)
.padding(.horizontal, 50) .padding(.horizontal, 50)
@ -57,25 +57,33 @@ struct OnboardingView: View {
extension OnboardingState { extension OnboardingState {
static let onboardingSteps = IdentifiedArray( static let onboardingSteps = IdentifiedArray(
uniqueElements: [ uniqueElements: [
OnboardingStep( Step(
id: UUID(), id: UUID(),
title: "Shielded by Default", title: "Shielded by Default",
description: "Tired of worrying about which wallet you used last? US TOO! Now you don't have to, as all funds will automatically be moved to your shielded wallet (and migrated for you)." description: "Tired of worrying about which wallet you used last? US TOO! Now you don't have to, as all funds will automatically be moved to your shielded wallet (and migrated for you).",
background: Asset.Assets.Backgrounds.callout1.image,
badge: .shield
), ),
OnboardingStep( Step(
id: UUID(), id: UUID(),
title: "Unified Addresses", title: "Unified Addresses",
description: "Tired of worrying about which wallet you used last? US TOO! Now you don't have to, as all funds will automatically be moved to your shielded wallet (and migrated for you)." description: "Tired of worrying about which wallet you used last? US TOO! Now you don't have to, as all funds will automatically be moved to your shielded wallet (and migrated for you).",
background: Asset.Assets.Backgrounds.callout2.image,
badge: .person
), ),
OnboardingStep( Step(
id: UUID(), id: UUID(),
title: "And so much more...", title: "And so much more...",
description: "Faster reverse syncing (yes it's a thing). Liberated Payments, Social Payments, Address Books, in-line ZEC requests, wrapped Bitcoin, fractionalize NFTs, you providing liquidity for anything you want, getting that Defi, and going to Mexico." description: "Faster reverse syncing (yes it's a thing). Liberated Payments, Social Payments, Address Books, in-line ZEC requests, wrapped Bitcoin, fractionalize NFTs, you providing liquidity for anything you want, getting that Defi, and going to Mexico.",
background: Asset.Assets.Backgrounds.callout3.image,
badge: .list
), ),
OnboardingStep( Step(
id: UUID(), id: UUID(),
title: "Ready for the Future", title: "Ready for the Future",
description: "Lets get you set up!" description: "Lets get you set up!",
background: Asset.Assets.Backgrounds.callout4.image,
badge: .shield
) )
] ]
) )
@ -87,7 +95,7 @@ struct Onboarding_Previews: PreviewProvider {
OnboardingView( OnboardingView(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(),
reducer: onboardingReducer, reducer: .default,
environment: () environment: ()
) )
) )

View File

@ -0,0 +1,121 @@
//
// OnboardingContentView.swift
// secant-testnet
//
// Created by Adam Stener on 11/18/21.
//
import SwiftUI
import ComposableArchitecture
struct OnboardingContentView: View {
let store: Store<OnboardingState, OnboardingAction>
let width: Double
let height: Double
var body: some View {
WithViewStore(self.store) { viewStore in
ZStack {
if viewStore.isFinalStep {
VStack {
Asset.Assets.Backgrounds.callout4.image
.resizable()
.frame(
width: width,
height: height * 0.6
)
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
Spacer()
}
.transition(.opacity)
} else {
CircularFrame()
.backgroundImages(
store.actionless.scope(
state: { state in
CircularFrameBackgroundImages.ViewState(
index: state.index,
images: state.steps.map { $0.background }
)
}
)
)
.frame(width: width * 0.85, height: width * 0.85)
.badgeIcons(
store.actionless.scope(
state: { state in
BadgesOverlay.ViewState(
index: state.index,
badges: state.steps.map { $0.badge }
)
}
)
)
.offset(y: viewStore.offset - height / 7)
.transition(.scale(scale: 2).combined(with: .opacity))
}
}
ZStack {
ForEach(0..<viewStore.steps.count) { stepIndex in
VStack(spacing: viewStore.isFinalStep ? 50 : 15) {
HStack {
Text(viewStore.steps[stepIndex].title)
.font(.custom(FontFamily.Roboto.bold.name, size: 30))
.fontWeight(.regular)
if !viewStore.isFinalStep {
Spacer()
}
}
Text(viewStore.steps[stepIndex].description)
.font(.custom(FontFamily.Roboto.regular.name, size: 15))
.lineSpacing(5)
}
.opacity(stepIndex == viewStore.index ? 1: 0)
.padding(.horizontal, 35)
.frame(width: width, height: height)
}
}
.offset(y: viewStore.isFinalStep ? width / 2.3 : viewStore.offset + width / 2.3)
}
}
}
struct OnboardingContentView_Previews: PreviewProvider {
static var previews: some View {
let store = Store(
initialState: OnboardingState(index: 2),
reducer: OnboardingReducer.default,
environment: ()
)
GeometryReader { proxy in
ZStack {
OnboardingHeaderView(
store: store.scope(
state: { state in
OnboardingHeaderView.ViewState(
isInitialStep: state.isInitialStep,
isFinalStep: state.isFinalStep
)
},
action: { action in
switch action {
case .back: return .back
case .skip: return .skip
}
}
)
)
.zIndex(1)
OnboardingContentView(
store: store,
width: proxy.size.width,
height: proxy.size.height
)
.preferredColorScheme(.light)
}
}
}
}

View File

@ -0,0 +1,75 @@
//
// OnboardingFooterView.swift
// secant-testnet
//
// Created by Adam Stener on 11/18/21.
//
import SwiftUI
import ComposableArchitecture
struct OnboardingFooterView: View {
let store: Store<OnboardingState, OnboardingAction>
let animationDuration: CGFloat = 0.8
var body: some View {
GeometryReader { proxy in
WithViewStore(self.store) { viewStore in
VStack(spacing: 5) {
Spacer()
if viewStore.isFinalStep {
Button("Create New Wallet") {
withAnimation(.easeInOut(duration: animationDuration)) {
viewStore.send(.createNewWallet)
}
}
.primaryButtonStyle
.frame(height: proxy.size.height / 12)
.padding(.horizontal, 15)
.transition(.opacity)
} else {
Button("Next") {
withAnimation(.easeInOut(duration: animationDuration)) {
viewStore.send(.next)
}
}
.primaryButtonStyle
.frame(height: proxy.size.height / 12)
.padding(.horizontal, 15)
.transition(.opacity)
}
ProgressView(
"\(viewStore.index + 1)/\(viewStore.steps.count)",
value: Double(viewStore.index + 1),
total: Double(viewStore.steps.count)
)
.onboardingProgressStyle
.padding(.horizontal, 30)
.padding([.vertical], 20)
}
}
}
}
}
struct OnboardingFooterView_Previews: PreviewProvider {
static var previews: some View {
let store = Store<OnboardingState, OnboardingAction>(
initialState: OnboardingState(index: 0),
reducer: OnboardingReducer.default,
environment: ()
)
Group {
OnboardingFooterView(store: store)
.preferredColorScheme(.dark)
.previewDevice("iPhone 13 Pro Max")
OnboardingFooterView(store: store)
.preferredColorScheme(.dark)
.previewDevice("iPhone 13 mini")
}
}
}

View File

@ -0,0 +1,89 @@
//
// OnboardingNavigationButtons.swift
// secant-testnet
//
// Created by Adam Stener on 11/18/21.
//
import SwiftUI
import ComposableArchitecture
struct OnboardingHeaderView: View {
struct ViewState: Equatable {
let isInitialStep: Bool
let isFinalStep: Bool
}
enum ViewAction {
case back
case skip
}
let store: Store<ViewState, ViewAction>
let animationDuration: CGFloat = 0.8
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
if !viewStore.isInitialStep {
Button("Back") {
withAnimation(.easeInOut(duration: animationDuration)) {
viewStore.send(.back)
}
}
.navigationButtonStyle
.disabled(viewStore.isInitialStep)
.frame(width: 75)
}
Spacer()
if !viewStore.isFinalStep {
Button("Skip") {
withAnimation(.easeInOut(duration: animationDuration)) {
viewStore.send(.skip)
}
}
.navigationButtonStyle
.disabled(viewStore.isFinalStep)
.frame(width: 75)
}
}
.padding(.horizontal, 30)
.frame(height: 40)
Spacer()
}
.padding(.top, 20)
}
}
}
struct OnboardingHeaderView_Previews: PreviewProvider {
static var previews: some View {
let store = Store<OnboardingState, OnboardingAction>(
initialState: OnboardingState(index: 0),
reducer: OnboardingReducer.default,
environment: ()
)
OnboardingHeaderView(
store: store.scope(
state: { state in
OnboardingHeaderView.ViewState(
isInitialStep: state.isInitialStep,
isFinalStep: state.isFinalStep
)
},
action: { action in
switch action {
case .back: return .back
case .skip: return .skip
}
}
)
)
.preferredColorScheme(.light)
}
}

View File

@ -0,0 +1,62 @@
//
// OnboardingScreen.swift
// secant-testnet
//
// Created by Adam Stener on 11/7/21.
//
import SwiftUI
import ComposableArchitecture
struct OnboardingScreen: View {
let store: Store<OnboardingState, OnboardingAction>
let animationDuration: CGFloat = 0.8
var body: some View {
GeometryReader { proxy in
ZStack {
ScreenBackground()
.edgesIgnoringSafeArea(.all)
OnboardingHeaderView(
store: store.scope(
state: { state in
OnboardingHeaderView.ViewState(
isInitialStep: state.isInitialStep,
isFinalStep: state.isFinalStep
)
},
action: { action in
switch action {
case .back: return .back
case .skip: return .skip
}
}
)
)
.zIndex(1)
OnboardingContentView(
store: store,
width: proxy.size.width,
height: proxy.size.height
)
OnboardingFooterView(store: store)
}
}
}
}
struct OnboardingScreen_Previews: PreviewProvider {
static var previews: some View {
OnboardingScreen(
store: Store(
initialState: OnboardingState(),
reducer: OnboardingReducer.default,
environment: ()
)
)
.preferredColorScheme(.light)
}
}

View File

@ -10,7 +10,7 @@ import SwiftUI
struct CircularFrame: View { struct CircularFrame: View {
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
let lineWidth = proxy.size.width * 0.07 let lineWidth = proxy.size.width * 0.05
Circle() Circle()
.stroke(lineWidth: lineWidth) .stroke(lineWidth: lineWidth)

View File

@ -6,9 +6,37 @@
// //
import SwiftUI import SwiftUI
import ComposableArchitecture
struct CircularFrameBackgroundImage: ViewModifier { struct CircularFrameBackgroundImages: Animatable, ViewModifier {
struct ViewState: Equatable {
let index: Int
let images: [Image]
}
let store: Store<ViewState, Never>
func body(content: Content) -> some View {
WithViewStore(self.store) { viewStore in
ZStack {
ForEach(0..<viewStore.images.count - 1) { imageIndex in
viewStore.images[imageIndex]
.resizable()
.aspectRatio(1.3, contentMode: .fill)
.opacity(imageIndex == viewStore.index ? 1 : 0)
.offset(x: imageIndex <= viewStore.index ? 0 : 25)
.mask(Circle())
}
content
}
}
}
}
struct CircularFrameBackgroundImage: Animatable, ViewModifier {
let image: Image let image: Image
func body(content: Content) -> some View { func body(content: Content) -> some View {
ZStack { ZStack {
image image
@ -25,6 +53,10 @@ extension CircularFrame {
func backgroundImage(_ image: Image) -> some View { func backgroundImage(_ image: Image) -> some View {
modifier(CircularFrameBackgroundImage(image: image)) modifier(CircularFrameBackgroundImage(image: image))
} }
func backgroundImages(_ store: Store<CircularFrameBackgroundImages.ViewState, Never>) -> some View {
modifier(CircularFrameBackgroundImages(store: store))
}
} }
struct CircularFrameBackground_Previews: PreviewProvider { struct CircularFrameBackground_Previews: PreviewProvider {

View File

@ -6,9 +6,9 @@
// //
import SwiftUI import SwiftUI
import ComposableArchitecture
struct BadgeIcon: ViewModifier { enum Badge: Equatable {
enum Badge: Equatable {
case shield case shield
case list case list
case person case person
@ -20,9 +20,56 @@ struct BadgeIcon: ViewModifier {
case .person: return Asset.Assets.Icons.profile.image case .person: return Asset.Assets.Icons.profile.image
} }
} }
}
struct BadgesOverlay: Animatable, ViewModifier {
struct ViewState: Equatable {
let index: Int
let badges: [Badge]
} }
let badge: Badge let store: Store<ViewState, Never>
func body(content: Content) -> some View {
WithViewStore(self.store) { viewStore in
content
.overlay(
GeometryReader { proxy in
VStack {
Spacer()
HStack {
Spacer()
ZStack {
ForEach(0..<viewStore.badges.count) { badgeIndex in
viewStore.badges[viewStore.index].image
.resizable()
.renderingMode(.none)
.frame(
width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35,
alignment: .center
)
.offset(
x: 4.0,
y: proxy.size.height * 0.15
)
.opacity(badgeIndex == viewStore.index ? 1 : 0)
}
}
Spacer()
}
}
}
)
}
}
}
struct BadgeOverlay: Animatable, ViewModifier {
var badge: Badge
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
@ -37,14 +84,16 @@ struct BadgeIcon: ViewModifier {
badge.image badge.image
.resizable() .resizable()
.frame( .frame(
width: proxy.size.width * 0.5, width: proxy.size.width * 0.35,
height: proxy.size.height * 0.5, height: proxy.size.height * 0.35,
alignment: .center alignment: .center
) )
.offset( .offset(
x: 0.0, x: 4.0,
y: proxy.size.height * 0.21 y: proxy.size.height * 0.15
) )
.transition(.scale(scale: 2))
.transition(.opacity)
Spacer() Spacer()
} }
} }
@ -54,8 +103,12 @@ struct BadgeIcon: ViewModifier {
} }
extension View { extension View {
func badgeIcon(_ badge: BadgeIcon.Badge) -> some View { func badgeIcon(_ badge: Badge) -> some View {
modifier(BadgeIcon(badge: badge)) modifier(BadgeOverlay(badge: badge))
}
func badgeIcons(_ store: Store<BadgesOverlay.ViewState, Never>) -> some View {
modifier(BadgesOverlay(store: store))
} }
} }

View File

@ -22,6 +22,7 @@ struct WithStateBinding<T: Equatable, Content: View>: View {
struct WithStateBinding_Previews: PreviewProvider { struct WithStateBinding_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NavigationView { NavigationView {
// swiftlint:disable:next large_tuple
StateContainer(initialState: (false, false, false)) { (binding: Binding<(Bool, Bool, Bool)>) in StateContainer(initialState: (false, false, false)) { (binding: Binding<(Bool, Bool, Bool)>) in
List { List {
NavigationLink( NavigationLink(
@ -46,7 +47,7 @@ struct WithStateBinding_Previews: PreviewProvider {
), ),
content: { content: {
NavigationLink( NavigationLink(
isActive:$0, isActive: $0,
destination: { Text("Wrapped Custom Binding") }, destination: { Text("Wrapped Custom Binding") },
label: { Text("Wrapped Custom Binding") } label: { Text("Wrapped Custom Binding") }
) )

View File

@ -13,15 +13,15 @@ class OnboardingStoreTests: XCTestCase {
func testIncrementingOnboarding() { func testIncrementingOnboarding() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(), initialState: OnboardingState(),
reducer: onboardingReducer, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
store.send(.next) { store.send(.next) {
$0.index += 1 $0.index += 1
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[1]) XCTAssertEqual($0.currentStep, $0.steps[1])
XCTAssertEqual($0.offset, -20.0) XCTAssertEqual($0.offset, -20.0)
XCTAssertEqual($0.progress, 50) XCTAssertEqual($0.progress, 50)
@ -30,8 +30,8 @@ class OnboardingStoreTests: XCTestCase {
store.send(.next) { store.send(.next) {
$0.index += 1 $0.index += 1
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[2]) XCTAssertEqual($0.currentStep, $0.steps[2])
XCTAssertEqual($0.offset, -40.0) XCTAssertEqual($0.offset, -40.0)
XCTAssertEqual($0.progress, 75) XCTAssertEqual($0.progress, 75)
@ -40,8 +40,8 @@ class OnboardingStoreTests: XCTestCase {
store.send(.next) { store.send(.next) {
$0.index += 1 $0.index += 1
XCTAssertTrue($0.skipButtonDisabled) XCTAssertTrue($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[3]) XCTAssertEqual($0.currentStep, $0.steps[3])
XCTAssertEqual($0.offset, -60.0) XCTAssertEqual($0.offset, -60.0)
XCTAssertEqual($0.progress, 100) XCTAssertEqual($0.progress, 100)
@ -51,21 +51,21 @@ class OnboardingStoreTests: XCTestCase {
func testIncrementingPastTotalStepsDoesNothing() { func testIncrementingPastTotalStepsDoesNothing() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: 3), initialState: OnboardingState(index: 3),
reducer: onboardingReducer, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
store.send(.next) { store.send(.next) {
XCTAssertTrue($0.skipButtonDisabled) XCTAssertTrue($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[3]) XCTAssertEqual($0.currentStep, $0.steps[3])
XCTAssertEqual($0.offset, -60.0) XCTAssertEqual($0.offset, -60.0)
XCTAssertEqual($0.progress, 100) XCTAssertEqual($0.progress, 100)
} }
store.send(.next) { store.send(.next) {
XCTAssertTrue($0.skipButtonDisabled) XCTAssertTrue($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[3]) XCTAssertEqual($0.currentStep, $0.steps[3])
XCTAssertEqual($0.offset, -60.0) XCTAssertEqual($0.offset, -60.0)
XCTAssertEqual($0.progress, 100) XCTAssertEqual($0.progress, 100)
@ -75,15 +75,15 @@ class OnboardingStoreTests: XCTestCase {
func testDecrementingOnboarding() { func testDecrementingOnboarding() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: 2), initialState: OnboardingState(index: 2),
reducer: onboardingReducer, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
store.send(.back) { store.send(.back) {
$0.index -= 1 $0.index -= 1
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[1]) XCTAssertEqual($0.currentStep, $0.steps[1])
XCTAssertEqual($0.offset, -20.0) XCTAssertEqual($0.offset, -20.0)
XCTAssertEqual($0.progress, 50) XCTAssertEqual($0.progress, 50)
@ -92,8 +92,8 @@ class OnboardingStoreTests: XCTestCase {
store.send(.back) { store.send(.back) {
$0.index -= 1 $0.index -= 1
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertTrue($0.backButtonDisabled) XCTAssertTrue($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[0]) XCTAssertEqual($0.currentStep, $0.steps[0])
XCTAssertEqual($0.offset, 0.0) XCTAssertEqual($0.offset, 0.0)
XCTAssertEqual($0.progress, 25) XCTAssertEqual($0.progress, 25)
@ -103,21 +103,21 @@ class OnboardingStoreTests: XCTestCase {
func testDecrementingPastFirstStepDoesNothing() { func testDecrementingPastFirstStepDoesNothing() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(), initialState: OnboardingState(),
reducer: onboardingReducer, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
store.send(.back) { store.send(.back) {
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertTrue($0.backButtonDisabled) XCTAssertTrue($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[0]) XCTAssertEqual($0.currentStep, $0.steps[0])
XCTAssertEqual($0.offset, 0.0) XCTAssertEqual($0.offset, 0.0)
XCTAssertEqual($0.progress, 25) XCTAssertEqual($0.progress, 25)
} }
store.send(.back) { store.send(.back) {
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertTrue($0.backButtonDisabled) XCTAssertTrue($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[0]) XCTAssertEqual($0.currentStep, $0.steps[0])
XCTAssertEqual($0.offset, 0.0) XCTAssertEqual($0.offset, 0.0)
XCTAssertEqual($0.progress, 25) XCTAssertEqual($0.progress, 25)
@ -129,7 +129,7 @@ class OnboardingStoreTests: XCTestCase {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: initialIndex), initialState: OnboardingState(index: initialIndex),
reducer: onboardingReducer, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -137,8 +137,8 @@ class OnboardingStoreTests: XCTestCase {
$0.index = $0.steps.count - 1 $0.index = $0.steps.count - 1
$0.skippedAtindex = initialIndex $0.skippedAtindex = initialIndex
XCTAssertTrue($0.skipButtonDisabled) XCTAssertTrue($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[3]) XCTAssertEqual($0.currentStep, $0.steps[3])
XCTAssertEqual($0.offset, -60.0) XCTAssertEqual($0.offset, -60.0)
XCTAssertEqual($0.progress, 100) XCTAssertEqual($0.progress, 100)
@ -148,8 +148,8 @@ class OnboardingStoreTests: XCTestCase {
$0.skippedAtindex = nil $0.skippedAtindex = nil
$0.index = initialIndex $0.index = initialIndex
XCTAssertFalse($0.skipButtonDisabled) XCTAssertFalse($0.isFinalStep)
XCTAssertFalse($0.backButtonDisabled) XCTAssertFalse($0.isInitialStep)
XCTAssertEqual($0.currentStep, $0.steps[1]) XCTAssertEqual($0.currentStep, $0.steps[1])
XCTAssertEqual($0.offset, -20.0) XCTAssertEqual($0.offset, -20.0)
XCTAssertEqual($0.progress, 50) XCTAssertEqual($0.progress, 50)