Skip to content

Commit

Permalink
Add Wizard Coordinator Pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonjrr committed Nov 18, 2021
1 parent 9114759 commit 62918f6
Show file tree
Hide file tree
Showing 16 changed files with 545 additions and 0 deletions.
68 changes: 68 additions & 0 deletions MVVM.Demo.SwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
objects = {

/* Begin PBXBuildFile section */
9665D9552745C1D60055F1F6 /* ColorWizardCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */; };
9665D9572745C1F10055F1F6 /* ColorWizardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */; };
9665D95A2745C2540055F1F6 /* ColorWizardPageCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9592745C2540055F1F6 /* ColorWizardPageCoordinatorView.swift */; };
9665D95C2745C26F0055F1F6 /* ColorWizardPageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D95B2745C26F0055F1F6 /* ColorWizardPageCoordinator.swift */; };
9665D95F2745C2E00055F1F6 /* ColorWizardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */; };
9665D9612745C2FA0055F1F6 /* ColorWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */; };
9665D9632745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */; };
9665D9652745C3700055F1F6 /* ColorWizardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */; };
9665D9682745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */; };
96B4E06527432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E06427432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift */; };
96B4E06927432D6200EC88B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96B4E06827432D6200EC88B3 /* Assets.xcassets */; };
96B4E06C27432D6200EC88B3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96B4E06B27432D6200EC88B3 /* Preview Assets.xcassets */; };
Expand Down Expand Up @@ -70,6 +79,15 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardCoordinatorView.swift; sourceTree = "<group>"; };
9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardCoordinator.swift; sourceTree = "<group>"; };
9665D9592745C2540055F1F6 /* ColorWizardPageCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardPageCoordinatorView.swift; sourceTree = "<group>"; };
9665D95B2745C26F0055F1F6 /* ColorWizardPageCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardPageCoordinator.swift; sourceTree = "<group>"; };
9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardContentView.swift; sourceTree = "<group>"; };
9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardContentViewModel.swift; sourceTree = "<group>"; };
9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardConfigurationViewModel.swift; sourceTree = "<group>"; };
9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardConfiguration.swift; sourceTree = "<group>"; };
9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColorWizardConfiguration+Mock.swift"; sourceTree = "<group>"; };
96B4E06127432D6100EC88B3 /* MVVM.Demo.SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MVVM.Demo.SwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
96B4E06427432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVVM_Demo_SwiftUIApp.swift; sourceTree = "<group>"; };
96B4E06827432D6200EC88B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -143,6 +161,46 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
9665D9532745C1B50055F1F6 /* ColorWizard */ = {
isa = PBXGroup;
children = (
9665D9692745CCB70055F1F6 /* Configuration */,
9665D95D2745C2C80055F1F6 /* Content */,
9665D9582745C2210055F1F6 /* PageCoordinator */,
9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */,
9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */,
);
path = ColorWizard;
sourceTree = "<group>";
};
9665D9582745C2210055F1F6 /* PageCoordinator */ = {
isa = PBXGroup;
children = (
9665D9592745C2540055F1F6 /* ColorWizardPageCoordinatorView.swift */,
9665D95B2745C26F0055F1F6 /* ColorWizardPageCoordinator.swift */,
);
path = PageCoordinator;
sourceTree = "<group>";
};
9665D95D2745C2C80055F1F6 /* Content */ = {
isa = PBXGroup;
children = (
9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */,
9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */,
);
path = Content;
sourceTree = "<group>";
};
9665D9692745CCB70055F1F6 /* Configuration */ = {
isa = PBXGroup;
children = (
9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */,
9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */,
9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */,
);
path = Configuration;
sourceTree = "<group>";
};
96B4E05827432D6100EC88B3 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -264,6 +322,7 @@
96B4E0C52743312600EC88B3 /* UI */ = {
isa = PBXGroup;
children = (
9665D9532745C1B50055F1F6 /* ColorWizard */,
96B4E0C62743312F00EC88B3 /* AppRootCoordinator */,
96B4E0D22743381500EC88B3 /* Landing */,
96B4E0EC274424E800EC88B3 /* Pulse */,
Expand Down Expand Up @@ -493,17 +552,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9665D9682745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift in Sources */,
96B4E0E427437CBA00EC88B3 /* SignInViewModel.swift in Sources */,
96B4E0AC27432F8900EC88B3 /* String+Extensions.swift in Sources */,
96B4E0AE27432FAB00EC88B3 /* UIApplication+EndEditing.swift in Sources */,
96B4E0DA2743390E00EC88B3 /* BrightBorderedButtonStyle.swift in Sources */,
96B4E0EB274386FA00EC88B3 /* Color+SystemColors.swift in Sources */,
9665D95F2745C2E00055F1F6 /* ColorWizardContentView.swift in Sources */,
9665D9632745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift in Sources */,
96B4E0C4274330E600EC88B3 /* ViewModelAssembly.swift in Sources */,
96B4E09327432E4000EC88B3 /* HapticFeedbackProvider.swift in Sources */,
96B4E0C82743313900EC88B3 /* AppRootCoordinatorView.swift in Sources */,
96B4E09627432E6800EC88B3 /* CancelBag.swift in Sources */,
96B4E0D42743382B00EC88B3 /* LandingView.swift in Sources */,
96B4E0CF274335A900EC88B3 /* ColorService.swift in Sources */,
9665D95A2745C2540055F1F6 /* ColorWizardPageCoordinatorView.swift in Sources */,
96B4E0CA2743314700EC88B3 /* AppRootCoordinator.swift in Sources */,
96B4E0B92743303F00EC88B3 /* View+OnReceive.swift in Sources */,
96B4E0D62743384F00EC88B3 /* LandingViewModel.swift in Sources */,
Expand All @@ -516,19 +579,24 @@
96B4E06527432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift in Sources */,
96B4E0A727432F1C00EC88B3 /* PassthroughSubject+Extensions.swift in Sources */,
96B4E09827432E7D00EC88B3 /* Just+Void.swift in Sources */,
9665D9552745C1D60055F1F6 /* ColorWizardCoordinatorView.swift in Sources */,
96B4E0B72743302100EC88B3 /* View+Navigation.swift in Sources */,
9665D9612745C2FA0055F1F6 /* ColorWizardContentViewModel.swift in Sources */,
96B4E0D1274336EE00EC88B3 /* AlertService.swift in Sources */,
96B4E0BE2743309B00EC88B3 /* AppAssembler.swift in Sources */,
96B4E09C27432EB100EC88B3 /* Publisher+Extensions.swift in Sources */,
96B4E0F0274424FE00EC88B3 /* PulseViewModel.swift in Sources */,
96B4E0E227437CA200EC88B3 /* SignInView.swift in Sources */,
96B4E0DF2743396600EC88B3 /* ButtonTextStyle.swift in Sources */,
9665D95C2745C26F0055F1F6 /* ColorWizardPageCoordinator.swift in Sources */,
96B4E0DD2743393E00EC88B3 /* TextStyle.swift in Sources */,
96B4E0B327432FEB00EC88B3 /* EdgeInsets+Extensions.swift in Sources */,
96B4E09A27432E9300EC88B3 /* Publisher+DefinedScheduler.swift in Sources */,
96B4E0EE274424F600EC88B3 /* PulseView.swift in Sources */,
96B4E0E927437D5300EC88B3 /* CardView.swift in Sources */,
9665D9652745C3700055F1F6 /* ColorWizardConfiguration.swift in Sources */,
96B4E0CD274331EE00EC88B3 /* AuthenticationService.swift in Sources */,
9665D9572745C1F10055F1F6 /* ColorWizardCoordinator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
8 changes: 8 additions & 0 deletions MVVM.Demo.SwiftUI/Architecture/CoordinatorAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,13 @@ class CoordinatorAssembly: Assembly {
container.register(AppRootCoordinator.self) { r in
AppRootCoordinator(resolver: r)
}.inObjectScope(.container)

container.register(ColorWizardCoordinator.self) { r in
ColorWizardCoordinator(resolver: r)
}.inObjectScope(.transient)

container.register(ColorWizardPageCoordinator.self) { r in
ColorWizardPageCoordinator(resolver: r)
}.inObjectScope(.transient)
}
}
4 changes: 4 additions & 0 deletions MVVM.Demo.SwiftUI/Architecture/ViewModelAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Swinject

class ViewModelAssembly: Assembly {
func assemble(container: Container) {
container.register(ColorWizardContentViewModel.self) { r in
ColorWizardContentViewModel()
}.inObjectScope(.transient)

container.register(LandingViewModel.self) { r in
LandingViewModel(
alertService: r.resolve(AlertServiceProtocol.self)!,
Expand Down
13 changes: 13 additions & 0 deletions MVVM.Demo.SwiftUI/UI/AppRootCoordinator/AppRootCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class AppRootCoordinator: ViewModel {

@Published var signInViewModel: SignInViewModel?
@Published var pulseViewModel: PulseViewModel?
@Published var colorWizardCoordinator: ColorWizardCoordinator?
@Published var alert: AlertService.Alert?

init(resolver: Resolver) {
Expand All @@ -38,6 +39,11 @@ extension AppRootCoordinator: LandingViewModelDelegate {
.setup(delegate: self)
}

func landingViewModelDidTapColorWizard(_ source: LandingViewModel) {
self.colorWizardCoordinator = self.resolver.resolve(ColorWizardCoordinator.self)!
.setup(configuration: ColorWizardConfiguration.mock(), delegate: self)
}

func landingViewModel(_ source: LandingViewModel, didAlertWith alert: AlertService.Alert) {
DispatchQueue.main.async {
self.alert = alert
Expand All @@ -60,3 +66,10 @@ extension AppRootCoordinator: SignInViewModelDelegate {
extension AppRootCoordinator: PulseViewModelDelegate {
// Nothing yet
}

// MARK: ColorWizardCoordinatorDelegate
extension AppRootCoordinator: ColorWizardCoordinatorDelegate {
func colorWizardCoordinatorDidComplete(_ source: ColorWizardCoordinator) {
self.colorWizardCoordinator = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ struct AppRootCoordinatorView: View {
.navigation(item: self.$coordinator.pulseViewModel) {
PulseView(viewModel: $0)
}
.fullScreenCover(item: self.$coordinator.colorWizardCoordinator) { coordinator in
NavigationView {
ColorWizardCoordinatorView(coordiantor: coordinator)
}
}

if let viewModel = self.signInViewModel {
SignInView(viewModel: viewModel)
Expand Down
74 changes: 74 additions & 0 deletions MVVM.Demo.SwiftUI/UI/ColorWizard/ColorWizardCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// ColorWizardCoordinator.swift
// MVVM.Demo.SwiftUI
//
// Created by Jason Lew-Rapai on 11/17/21.
//

import Foundation
import Combine
import Swinject

protocol ColorWizardCoordinatorDelegate: AnyObject {
func colorWizardCoordinatorDidComplete(_ source: ColorWizardCoordinator)
}

class ColorWizardCoordinator: ViewModel {
private let resolver: Resolver

private weak var delegate: ColorWizardCoordinatorDelegate?
private var configurationViewModel: ColorWizardConfigurationViewModel!

@Published var colorWizardPageCoordinator: ColorWizardPageCoordinator!

private var cancelBag = CancelBag()

This comment has been minimized.

Copy link
@aoverfield

aoverfield Nov 18, 2021

Doesn't seem to be used?


init(resolver: Resolver) {
self.resolver = resolver
}

func setup(configuration: ColorWizardConfiguration, delegate: ColorWizardCoordinatorDelegate) -> Self {
self.delegate = delegate
self.configurationViewModel = ColorWizardConfigurationViewModel(configuration: configuration)

if let firstPageViewModel = self.configurationViewModel.pages.first {
self.colorWizardPageCoordinator = self.resolver.resolve(ColorWizardPageCoordinator.self)!
.setup(currentPageViewModel: firstPageViewModel, delegate: self)
} else {
fatalError()
}

return self
}
}

// MARK: ColorWizardPageCoordinatorDelegate
extension ColorWizardCoordinator: ColorWizardPageCoordinatorDelegate {
func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, canMoveBackFromIndex index: Int) -> Bool {
return index != 0
}

func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, canMoveForwardFromIndex index: Int) -> Bool {
return self.configurationViewModel.pages.count > index + 1
}

func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, canCompleteFromIndex index: Int) -> Bool {
return self.configurationViewModel.pages.count == index + 1
}

func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, didMoveBackFromIndex index: Int) {
fatalError("A back command should never reach this coordinator.")
}

func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, nextPageAfterIndex index: Int) -> ColorWizardConfigurationViewModel.PageViewModel? {
let newIndex = index + 1
guard newIndex < self.configurationViewModel.pages.count else {
return nil
}
return self.configurationViewModel.pages[newIndex]
}

func colorWizardPageCoordinator(_ source: ColorWizardPageCoordinator, didCompleteFromIndex index: Int) {
self.delegate?.colorWizardCoordinatorDidComplete(self)
}
}
16 changes: 16 additions & 0 deletions MVVM.Demo.SwiftUI/UI/ColorWizard/ColorWizardCoordinatorView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// ColorWizardCoordinatorView.swift
// MVVM.Demo.SwiftUI
//
// Created by Jason Lew-Rapai on 11/17/21.
//

import SwiftUI

struct ColorWizardCoordinatorView: View {
@ObservedObject var coordiantor: ColorWizardCoordinator

var body: some View {
ColorWizardPageCoordinatorView(coordinator: self.coordiantor.colorWizardPageCoordinator)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// ColorWizardConfiguration+Mock.swift
// MVVM.Demo.SwiftUI
//
// Created by Jason Lew-Rapai on 11/17/21.
//

import Foundation

extension ColorWizardConfiguration {
static func mock() -> ColorWizardConfiguration {
ColorWizardConfiguration(pages: [
.page("First Color", color: .green),
.page("Second Color", color: .orange),
.page("Third Color", color: .systemIndigo),
.page("Fourth Color", color: .pink),
.page("Fifth Color", color: .purple),
.page("Summary", colors: [
.green,
.orange,
.systemIndigo,
.pink,
.purple,
]),
])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ColorWizardConfiguration.swift
// MVVM.Demo.SwiftUI
//
// Created by Jason Lew-Rapai on 11/17/21.
//

import SwiftUI

struct ColorWizardConfiguration {
let pages: [Page]
}

extension ColorWizardConfiguration {
struct Page {
let title: String
let color: Color?
let colors: [Color]

static func page(_ title: String, color: Color? = nil, colors: [Color] = []) -> Page {
Page(title: title, color: color, colors: colors)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ColorWizardConfigurationViewModel.swift
// MVVM.Demo.SwiftUI
//
// Created by Jason Lew-Rapai on 11/17/21.
//

import Foundation
import Combine
import SwiftUI

class ColorWizardConfigurationViewModel: ViewModel {
let pages: [PageViewModel]

init(configuration: ColorWizardConfiguration) {
var pageViewModels: [PageViewModel] = []
for (index, page) in configuration.pages.enumerated() {
pageViewModels.append(PageViewModel(page: page, index: index))
}
self.pages = pageViewModels
}
}

extension ColorWizardConfigurationViewModel {
class PageViewModel: ViewModel {
let index: Int
let title: String
let color: Color?
let colors: [ColorViewModel]

This comment has been minimized.

Copy link
@aoverfield

aoverfield Nov 18, 2021

What's the purpose of having 2 properties for colors?
Can the single color be transformed into a single item in the colors array?


init(page: ColorWizardConfiguration.Page, index: Int) {
self.index = index
self.title = page.title
self.color = page.color
self.colors = page.colors.map { ColorViewModel(color: $0) }
}
}

class ColorViewModel: ViewModel {
let id: String = UUID().uuidString
let color: Color

init(color: Color) {
self.color = color
}
}
}
Loading

0 comments on commit 62918f6

Please sign in to comment.