Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuseable Navigation Actions #2

Merged
merged 3 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
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
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
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
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
}

}
@@ -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
}
}
@@ -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)
}

}
Expand Up @@ -14,7 +14,7 @@ protocol NoStepCoordinatorHandling: AnyObject {
}


class NoStepCoordinator: NavigationControllerCoordinator, ParentCoordinated
class NoStepCoordinator: NavigationControllerCoordinator, AnyParentCoordinated
{
enum Event {
case something
Expand Down
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)
}
}
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