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 */; };
0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF2DC50272344E400FA31E2 /* EmptyChip.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 */; };
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 */; };
@ -190,6 +194,10 @@
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>"; };
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>"; };
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>"; };
@ -201,6 +209,7 @@
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>"; };
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>"; };
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>"; };
@ -274,6 +283,7 @@
0D1922EB26BDD9A500052649 /* Screens */ = {
isa = PBXGroup;
children = (
2E5C037F2738C55F008BFFD3 /* Onboarding */,
0D864A0B26E1580700A61879 /* Loading */,
0D864A0626E154D100A61879 /* Error */,
0D32282F26C5874B00262533 /* Balance */,
@ -370,6 +380,7 @@
0D4E7A1926B364180058B01E /* secantTests */,
0D4E7A2426B364180058B01E /* secantUITests */,
0D4E7A0626B364170058B01E /* Products */,
2EB660DF2747EA6000A06A07 /* Recovered References */,
);
sourceTree = "<group>";
};
@ -581,6 +592,25 @@
path = Extensions;
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 */ = {
isa = PBXGroup;
children = (
@ -887,7 +917,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
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 */
@ -896,6 +926,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2EB660E02747EAB900A06A07 /* OnboardingScreen.swift in Sources */,
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */,
0D32281F26C5867D00262533 /* ScanQrScreenViewModel.swift in Sources */,
@ -929,6 +960,7 @@
0D32282326C586A800262533 /* HistoryScreen.swift in Sources */,
0D864A0A26E154FD00A61879 /* InitFailedScreenViewModel.swift in Sources */,
0DA13CA526C1963000E3B610 /* Balance.swift in Sources */,
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */,
0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */,
@ -949,8 +981,10 @@
0D32281A26C5864B00262533 /* ProfileScreenViewModel.swift in Sources */,
0D185819272723FF0046B928 /* BlueChip.swift in Sources */,
0D864A0F26E1583000A61879 /* LoadingScreenViewModel.swift in Sources */,
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
0DA13CA126C1955600E3B610 /* HomeScreen.swift in Sources */,
0DA13C9026C15D1D00E3B610 /* WelcomeScreenViewModel.swift in Sources */,
2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */,
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
663FAB9E271D875700E495F8 /* CreateButton.swift in Sources */,
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */,

View File

@ -26,7 +26,10 @@ struct HomeView: View {
HStack {
VStack(alignment: .leading) {
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)
.frame(maxWidth: .infinity, alignment: .leading)

View File

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

View File

@ -16,13 +16,13 @@ struct OnboardingView: View {
VStack(spacing: 50) {
HStack(spacing: 50) {
Button("Back") { viewStore.send(.back) }
.disabled(viewStore.backButtonDisabled)
.disabled(viewStore.isInitialStep)
Spacer()
Button("Next") { viewStore.send(.next) }
Button("Skip") { viewStore.send(.skip) }
.disabled(viewStore.skipButtonDisabled)
.disabled(viewStore.isFinalStep)
}
.frame(height: 100)
.padding(.horizontal, 50)
@ -57,25 +57,33 @@ struct OnboardingView: View {
extension OnboardingState {
static let onboardingSteps = IdentifiedArray(
uniqueElements: [
OnboardingStep(
Step(
id: UUID(),
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(),
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(),
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(),
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(
store: Store(
initialState: OnboardingState(),
reducer: onboardingReducer,
reducer: .default,
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 {
var body: some View {
GeometryReader { proxy in
let lineWidth = proxy.size.width * 0.07
let lineWidth = proxy.size.width * 0.05
Circle()
.stroke(lineWidth: lineWidth)

View File

@ -6,9 +6,37 @@
//
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
func body(content: Content) -> some View {
ZStack {
image
@ -25,6 +53,10 @@ extension CircularFrame {
func backgroundImage(_ image: Image) -> some View {
modifier(CircularFrameBackgroundImage(image: image))
}
func backgroundImages(_ store: Store<CircularFrameBackgroundImages.ViewState, Never>) -> some View {
modifier(CircularFrameBackgroundImages(store: store))
}
}
struct CircularFrameBackground_Previews: PreviewProvider {

View File

@ -6,23 +6,70 @@
//
import SwiftUI
import ComposableArchitecture
struct BadgeIcon: ViewModifier {
enum Badge: Equatable {
case shield
case list
case person
enum Badge: Equatable {
case shield
case list
case person
var image: Image {
switch self {
case .shield: return Asset.Assets.Icons.shield.image
case .list: return Asset.Assets.Icons.list.image
case .person: return Asset.Assets.Icons.profile.image
}
var image: Image {
switch self {
case .shield: return Asset.Assets.Icons.shield.image
case .list: return Asset.Assets.Icons.list.image
case .person: return Asset.Assets.Icons.profile.image
}
}
}
let badge: Badge
struct BadgesOverlay: Animatable, ViewModifier {
struct ViewState: Equatable {
let index: Int
let badges: [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 {
content
@ -37,14 +84,16 @@ struct BadgeIcon: ViewModifier {
badge.image
.resizable()
.frame(
width: proxy.size.width * 0.5,
height: proxy.size.height * 0.5,
width: proxy.size.width * 0.35,
height: proxy.size.height * 0.35,
alignment: .center
)
.offset(
x: 0.0,
y: proxy.size.height * 0.21
x: 4.0,
y: proxy.size.height * 0.15
)
.transition(.scale(scale: 2))
.transition(.opacity)
Spacer()
}
}
@ -54,8 +103,12 @@ struct BadgeIcon: ViewModifier {
}
extension View {
func badgeIcon(_ badge: BadgeIcon.Badge) -> some View {
modifier(BadgeIcon(badge: badge))
func badgeIcon(_ badge: Badge) -> some View {
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 {
static var previews: some View {
NavigationView {
// swiftlint:disable:next large_tuple
StateContainer(initialState: (false, false, false)) { (binding: Binding<(Bool, Bool, Bool)>) in
List {
NavigationLink(
@ -46,7 +47,7 @@ struct WithStateBinding_Previews: PreviewProvider {
),
content: {
NavigationLink(
isActive:$0,
isActive: $0,
destination: { Text("Wrapped Custom Binding") },
label: { Text("Wrapped Custom Binding") }
)

View File

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