Skip to content

Commit

Permalink
Reuseable Navigation Actions (#2)
Browse files Browse the repository at this point in the history
* Reuseable Navigation Actions

- changed `ParentCoordinated` to `AnyParentCoordinated` and let ParentCoordinated adapt parent as Coordinator
- added basic unit tests
- added reusable navigation actions to `AppStep` for taskDetail

* cleanup

* fix
  • Loading branch information
laubengaier committed Oct 19, 2021
1 parent de736b4 commit 16e0447
Show file tree
Hide file tree
Showing 19 changed files with 346 additions and 123 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

Swordinator is a minimal, lightweight and easy customizable navigation framework for iOS applications.

[![Tests](https://github.com/laubengaier/Swordinator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/laubengaier/Swordinator/actions/workflows/ci.yml)

[![Tests](https://github.com/laubengaier/Swordinator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/laubengaier/Swordinator/actions/workflows/ci.yml) ![SPM Compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen)
## Requirements
iOS 14.0+, Swift 5.0+

Expand Down
18 changes: 17 additions & 1 deletion Sources/Swordinator/Swordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,36 @@ public protocol Coordinator: AnyObject
var childCoordinators: [Coordinator] { get set }
func start()
func handle(step: Step)
func releaseChild<T: Coordinator>(type: T.Type)
}

public extension Coordinator {
func handle(step: Step) {
print("⚠️ step handler is not implemented for \(String(describing: Self.self))")
}
func releaseChild<T: Coordinator>(type: T.Type) {
childCoordinators.removeAll { $0 is T }
}
}

public protocol ParentCoordinated: AnyObject
public protocol AnyParentCoordinated: AnyObject
{
associatedtype Parent
var parent: Parent? { get set }
}

public protocol ParentCoordinated: AnyParentCoordinated
{
var parent: Coordinator? { get set }
func releaseFromParent()
}

public extension ParentCoordinated {
func releaseFromParent() {
parent?.childCoordinators.removeAll { $0 is Self }
}
}

public protocol Coordinated: AnyObject
{
associatedtype Coordinator
Expand Down
22 changes: 20 additions & 2 deletions SwordinatorDemo/SwordinatorDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
A186D22A27104B3C0047BA45 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A186D21B27104B3C0047BA45 /* LoginViewController.swift */; };
A186D22B27104B3C0047BA45 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A186D21D27104B3C0047BA45 /* Task.swift */; };
A186D22E27104BF40047BA45 /* Swordinator in Frameworks */ = {isa = PBXBuildFile; productRef = A186D22D27104BF40047BA45 /* Swordinator */; };
F718EFD4271E773C009D2E33 /* NavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F718EFD3271E773C009D2E33 /* NavCoordinator.swift */; };
F718EFD6271E7774009D2E33 /* AppDeeplinkStep+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F718EFD5271E7774009D2E33 /* AppDeeplinkStep+Convert.swift */; };
F738C5BD2713F89E00EA5812 /* TaskDetailNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738C5BC2713F89E00EA5812 /* TaskDetailNameCell.swift */; };
F738C5BF2713FB1500EA5812 /* TaskDetailSelectableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738C5BE2713FB1500EA5812 /* TaskDetailSelectableCell.swift */; };
F738C5C2271400E900EA5812 /* TaskDetailReminderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738C5C1271400E900EA5812 /* TaskDetailReminderViewController.swift */; };
Expand Down Expand Up @@ -68,6 +70,8 @@
A186D21A27104B3C0047BA45 /* LoginCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = "<group>"; };
A186D21B27104B3C0047BA45 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
A186D21D27104B3C0047BA45 /* Task.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
F718EFD3271E773C009D2E33 /* NavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavCoordinator.swift; sourceTree = "<group>"; };
F718EFD5271E7774009D2E33 /* AppDeeplinkStep+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDeeplinkStep+Convert.swift"; sourceTree = "<group>"; };
F738C5BC2713F89E00EA5812 /* TaskDetailNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailNameCell.swift; sourceTree = "<group>"; };
F738C5BE2713FB1500EA5812 /* TaskDetailSelectableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailSelectableCell.swift; sourceTree = "<group>"; };
F738C5C1271400E900EA5812 /* TaskDetailReminderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailReminderViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -147,6 +151,7 @@
children = (
A186D21827104B3C0047BA45 /* AppStep.swift */,
A186D20A27104B3C0047BA45 /* AppCoordinator.swift */,
F718EFD2271E772E009D2E33 /* Helper */,
A186D21927104B3C0047BA45 /* Login */,
F7D3056A2716A267008B2552 /* Sync */,
A186D20F27104B3C0047BA45 /* Dashboard */,
Expand Down Expand Up @@ -225,6 +230,15 @@
name = Frameworks;
sourceTree = "<group>";
};
F718EFD2271E772E009D2E33 /* Helper */ = {
isa = PBXGroup;
children = (
F718EFD3271E773C009D2E33 /* NavCoordinator.swift */,
F718EFD5271E7774009D2E33 /* AppDeeplinkStep+Convert.swift */,
);
path = Helper;
sourceTree = "<group>";
};
F738C5BB2713F87A00EA5812 /* Cells */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -399,6 +413,8 @@
A186D1EE27104AF90047BA45 /* AppDelegate.swift in Sources */,
A186D1F027104AF90047BA45 /* SceneDelegate.swift in Sources */,
F7D3057627194B0D008B2552 /* ProfileSettingsViewModel.swift in Sources */,
F718EFD4271E773C009D2E33 /* NavCoordinator.swift in Sources */,
F718EFD6271E7774009D2E33 /* AppDeeplinkStep+Convert.swift in Sources */,
F74BDC0A2710B26600B3F68A /* TaskListCell.swift in Sources */,
A186D22727104B3C0047BA45 /* ProfileViewModel.swift in Sources */,
A186D22A27104B3C0047BA45 /* LoginViewController.swift in Sources */,
Expand Down Expand Up @@ -554,13 +570,14 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2255UBWM66;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SwordinatorDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -581,13 +598,14 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2255UBWM66;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SwordinatorDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
53 changes: 14 additions & 39 deletions SwordinatorDemo/SwordinatorDemo/Features/AppStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@

import Foundation
import Swordinator
import UIKit
import MBProgressHUD

enum AppStep: Step {

// task
case taskDetail(task: Task, completion: (() -> Void)?)
case taskDetailLazy(id: Int)
case taskDetailReminder(task: Task)
case taskDetailPriority(task: Task)
case lazyTaskDetail(id: Int)
case taskDetailClose
case taskDetailCompleted

// auth
case authWithSIWA
case authCompleted
Expand All @@ -28,7 +31,7 @@ enum AppStep: Step {
// profile
case profile
case profileSettings
case closeProfileSettings
case profileSettingsCompleted

// navigation
case close
Expand All @@ -38,45 +41,17 @@ enum AppStep: Step {
}

enum AppDeeplinkStep: DeeplinkStep {

// task
case taskDetail(task: Task)
case lazyTaskDetail(id: Int)
case taskDetailLazy(id: Int)

// tabbar
case tasks
case profile

// profile
case profileSettings
case logout
}

extension AppDeeplinkStep {
static func convert(url: URL) -> AppDeeplinkStep? {
guard
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return nil
}

guard
let host = components.host,
let path = components.path
//let params = components.queryItems
else {
return nil
}
print("host = \(host)")
print("path = \(path)")

if url.absoluteString.starts(with: "swordinator://newTask") {
return .taskDetail(task: Task(id: 10, name: "Test1"))
} else if host == "tasks", let path = Int(url.pathComponents[1]) {
return .lazyTaskDetail(id: path)
} else if host == "tasks" {
return .tasks
} else if host == "profile" {
return .profile
} else if host == "logout" {
return .logout
} else if host == "settings" {
return .profileSettings
}
return nil
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// AppDeeplinkStep+Convert.swift
// SwordinatorDemo
//
// Created by Timotheus Laubengaier on 2021/10/19.
//

import Foundation

extension AppDeeplinkStep {
static func convert(url: URL) -> AppDeeplinkStep? {
guard
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true)
else {
return nil
}

guard
let host = components.host,
let path = components.path
//let params = components.queryItems
else {
return nil
}
print("host = \(host)")
print("path = \(path)")

if host == "newTask" {
return .taskDetail(task: Task(id: 10, name: "Test1"))
} else if host == "tasks", let path = Int(url.pathComponents[1]) {
return .taskDetailLazy(id: path)
} else if host == "tasks" {
return .tasks
} else if host == "profile" {
return .profile
} else if host == "logout" {
return .logout
} else if host == "settings" {
return .profileSettings
}
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// NavCoordinator.swift
// SwordinatorDemo
//
// Created by Timotheus Laubengaier on 2021/10/19.
//

import Foundation
import MBProgressHUD
import Swordinator

protocol NavCoordinator: NavigationControllerCoordinator, ParentCoordinated, HasServices where Parent == Coordinator {}

// MARK: Task Actions
extension NavCoordinator {

func navigateToTask(id: Int) {
MBProgressHUD.showAdded(to: navigationController.view, animated: true)
services.lazyTask(id: id) { task in
MBProgressHUD.hide(for: self.navigationController.view, animated: true)
guard let task = task else { return }
self.navigateToTask(task: task)
}
}

func navigateToTask(task: Task) {
let nvc = UINavigationController()
let coordinator = TaskDetailCoordinator(navigationController: nvc, services: services, task: task)
coordinator.parent = self
navigationController.present(nvc, animated: true, completion: nil)
childCoordinators.append(coordinator)
}

func endNavigateToTask(animated: Bool, shouldDismiss: Bool = false, completion: (() -> Void)? = nil) {
parent?.handle(step: AppStep.taskDetailCompleted)
if shouldDismiss {
navigationController.dismiss(animated: animated, completion: completion)
}
}

func releaseTaskDetail() {
releaseChild(type: TaskDetailCoordinator.self)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protocol NoStepCoordinatorHandling: AnyObject {
}


class NoStepCoordinator: NavigationControllerCoordinator, ParentCoordinated
class NoStepCoordinator: NavigationControllerCoordinator, AnyParentCoordinated
{
enum Event {
case something
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ class ProfileSettingsCoordinator: NavigationControllerCoordinator, ParentCoordin
var navigationController: UINavigationController
var childCoordinators: [Coordinator] = []

let services: AppServices
let services: Services

enum Event {
case close
}

init(navigationController: UINavigationController, services: AppServices) {
init(navigationController: UINavigationController, services: Services) {
self.navigationController = navigationController
self.services = services
start()
Expand Down Expand Up @@ -73,11 +73,11 @@ extension ProfileSettingsCoordinator {
}

private func dismiss() {
parent?.handle(step: AppStep.closeProfileSettings)
parent?.handle(step: AppStep.profileSettingsCompleted)
navigationController.dismiss(animated: true, completion: nil)
}

private func close() {
parent?.handle(step: AppStep.closeProfileSettings)
parent?.handle(step: AppStep.profileSettingsCompleted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

class ProfileSettingsViewModel {

let services: AppServices
let services: Services

var sections: [TableSection] = [
TableSection(name: nil, items: [
Expand All @@ -19,7 +19,7 @@ class ProfileSettingsViewModel {
])
]

init(services: AppServices) {
init(services: Services) {
self.services = services
}
}
Expand Down

0 comments on commit 16e0447

Please sign in to comment.