Skip to content

Commit

Permalink
Merge pull request #40 from erikdrobne/feature/tabbar
Browse files Browse the repository at this point in the history
Feature: Tab bar coordinator
  • Loading branch information
erikdrobne authored Jun 25, 2024
2 parents f254c73 + b04960f commit 2112615
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
1732CA0029A6607800C2BC1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1732C9FF29A6607800C2BC1F /* Assets.xcassets */; };
1732CA0329A6607800C2BC1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1732CA0229A6607800C2BC1F /* Preview Assets.xcassets */; };
17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17360A402A1275D600DB2296 /* FadeTransition.swift */; };
173FA01F2C18EFDB004EDF24 /* TabsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173FA01E2C18EFDB004EDF24 /* TabsCoordinator.swift */; };
1769698E2AB49FCF00CD6696 /* DeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769698D2AB49FCF00CD6696 /* DeepLinkHandler.swift */; };
176969912AB4AFD200CD6696 /* DependencyContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176969902AB4AFD200CD6696 /* DependencyContainer.swift */; };
176F3CB529B8BF71009C4987 /* ShapeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CB429B8BF71009C4987 /* ShapeListView.swift */; };
Expand All @@ -19,6 +20,7 @@
17AABAED2A6D5CF400AFE8A7 /* SimpleShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAEC2A6D5CF400AFE8A7 /* SimpleShapesAction.swift */; };
17AABAEF2A6D5E1500AFE8A7 /* CustomShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAEE2A6D5E1500AFE8A7 /* CustomShapesAction.swift */; };
17AABAF12A6D5F2100AFE8A7 /* ShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */; };
17B9E71C2C1CE30D00518BAB /* TabsCoordinatorRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B9E71B2C1CE30D00518BAB /* TabsCoordinatorRoute.swift */; };
17C379712ACEDD7800CA4105 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C379702ACEDD7800CA4105 /* AppCoordinator.swift */; };
17C379742ACEE1EA00CA4105 /* CoordinatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C379732ACEE1EA00CA4105 /* CoordinatorFactory.swift */; };
17DF24AF2AFD14C600578CD9 /* SlideTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF24AE2AFD14C600578CD9 /* SlideTransition.swift */; };
Expand All @@ -40,6 +42,7 @@
1732C9FF29A6607800C2BC1F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1732CA0229A6607800C2BC1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
17360A402A1275D600DB2296 /* FadeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeTransition.swift; sourceTree = "<group>"; };
173FA01E2C18EFDB004EDF24 /* TabsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsCoordinator.swift; sourceTree = "<group>"; };
1769698D2AB49FCF00CD6696 /* DeepLinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkHandler.swift; sourceTree = "<group>"; };
1769698F2AB4A25600CD6696 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
176969902AB4AFD200CD6696 /* DependencyContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyContainer.swift; sourceTree = "<group>"; };
Expand All @@ -49,6 +52,7 @@
17AABAEC2A6D5CF400AFE8A7 /* SimpleShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleShapesAction.swift; sourceTree = "<group>"; };
17AABAEE2A6D5E1500AFE8A7 /* CustomShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomShapesAction.swift; sourceTree = "<group>"; };
17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesAction.swift; sourceTree = "<group>"; };
17B9E71B2C1CE30D00518BAB /* TabsCoordinatorRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsCoordinatorRoute.swift; sourceTree = "<group>"; };
17C379702ACEDD7800CA4105 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
17C379732ACEE1EA00CA4105 /* CoordinatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorFactory.swift; sourceTree = "<group>"; };
17DF24AE2AFD14C600578CD9 /* SlideTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTransition.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -129,6 +133,15 @@
path = Transitions;
sourceTree = "<group>";
};
173FA01D2C18EFBC004EDF24 /* Tabs */ = {
isa = PBXGroup;
children = (
173FA01E2C18EFDB004EDF24 /* TabsCoordinator.swift */,
17B9E71B2C1CE30D00518BAB /* TabsCoordinatorRoute.swift */,
);
path = Tabs;
sourceTree = "<group>";
};
1769698C2AB49FB700CD6696 /* DeepLink */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -157,6 +170,7 @@
176F3CB929B8C143009C4987 /* Shapes */,
17F1183929CC6648004755DB /* CustomShapes */,
17F1183829CC662B004755DB /* SimpleShapes */,
173FA01D2C18EFBC004EDF24 /* Tabs */,
);
path = Coordinators;
sourceTree = "<group>";
Expand Down Expand Up @@ -288,6 +302,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
173FA01F2C18EFDB004EDF24 /* TabsCoordinator.swift in Sources */,
1769698E2AB49FCF00CD6696 /* DeepLinkHandler.swift in Sources */,
17AABAEF2A6D5E1500AFE8A7 /* CustomShapesAction.swift in Sources */,
176969912AB4AFD200CD6696 /* DependencyContainer.swift in Sources */,
Expand All @@ -305,6 +320,7 @@
17C379712ACEDD7800CA4105 /* AppCoordinator.swift in Sources */,
17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */,
17F1183D29CC668F004755DB /* SimpleShapesRoute.swift in Sources */,
17B9E71C2C1CE30D00518BAB /* TabsCoordinatorRoute.swift in Sources */,
17F1183529CC63B1004755DB /* SimpleShapesView.swift in Sources */,
176F3CBB29B8C162009C4987 /* ShapesRoute.swift in Sources */,
17AABAED2A6D5CF400AFE8A7 /* SimpleShapesAction.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ enum ShapesAction: CoordinatorAction {
case simpleShapes
case customShapes
case featuredShape(NavigationRoute)
case tabs
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class ShapesCoordinator: Routing {
default:
return
}
case ShapesAction.tabs:
let coordinator = factory.makeTabsCoordinator(parent: self)
coordinator.start()
case Action.done(_):
popToRoot()
childCoordinators.removeAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// TabsCoordinator.swift
// SwiftUICoordinatorExample
//
// Created by Erik Drobne on 11. 6. 24.
//

import Foundation

import SwiftUI
import SwiftUICoordinator

class TabsCoordinator: TabBarRouting {
let navigationController: NavigationController
let tabBarController = UITabBarController()
let tabs: [TabsCoordinatorRoute]

// MARK: - Internal properties

weak var parent: Coordinator?
var childCoordinators = [WeakCoordinator]()

// MARK: - Initialization

init(parent: Coordinator?, navigationController: NavigationController) {
self.parent = parent
self.navigationController = navigationController
self.tabs = [.red, .green, .blue]
}

func handle(_ action: CoordinatorAction) {
parent?.handle(action)
}
}

// MARK: - RouterViewFactory

extension TabsCoordinator: RouterViewFactory {

@ViewBuilder
public func view(for route: TabsCoordinatorRoute) -> some View {
switch route {
case .red:
VStack {
Circle()
.foregroundStyle(.red)
}
.padding(16)
case .green:
VStack {
Circle()
.foregroundStyle(.green)
}
.padding(16)
case .blue:
VStack {
Circle()
.foregroundStyle(.blue)
}
.padding(16)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// TabsCoordinatorRoute.swift
// SwiftUICoordinatorExample
//
// Created by Erik Drobne on 14. 6. 24.
//

import UIKit
import SwiftUICoordinator

enum TabsCoordinatorRoute: TabBarNavigationRoute {
case red
case green
case blue

var tabBarItem: UITabBarItem {
switch self {
case .red:
UITabBarItem(
title: "Red",
image: UIImage(systemName: "pencil.tip"),
tag: 0
)
case .green:
UITabBarItem(
title: "Green",
image: UIImage(systemName: "pencil"),
tag: 1
)
case .blue:
UITabBarItem(
title: "Blue",
image: UIImage(systemName: "pencil.and.scribble"),
tag: 2
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ protocol CoordinatorFactory {
func makeShapesCoordinator(parent: Coordinator) -> ShapesCoordinator
func makeSimpleShapesCoordinator(parent: Coordinator) -> SimpleShapesCoordinator
func makeCustomShapesCoordinator(parent: Coordinator) -> CustomShapesCoordinator
func makeTabsCoordinator(parent: Coordinator) -> TabsCoordinator
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ extension DependencyContainer: CoordinatorFactory {
navigationController: self.navigationController
)
}

func makeTabsCoordinator(parent: Coordinator) -> TabsCoordinator {
return TabsCoordinator(
parent: parent,
navigationController: self.navigationController
)
}
}

extension DependencyContainer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct ShapeListView<Coordinator: Routing>: View {
} label: {
Text("Featured")
}
Button {
viewModel.didTapTabs()
} label: {
Text("Tabs")
}
}
.onAppear {
viewModel.coordinator = coordinator
Expand Down Expand Up @@ -62,6 +67,10 @@ extension ShapeListView {

coordinator?.handle(ShapesAction.featuredShape(route))
}

func didTapTabs() {
coordinator?.handle(ShapesAction.tabs)
}
}
}

Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public protocol Navigator: ObservableObject {
/// The starting route of the navigator.
var startRoute: Route { get }

/// This method should be called to start the flow and to show the view for the `startRoute`.
/// This method should be called to start the flow and to show the view for the `startRoute`.
func start() throws
/// It creates a view for the route and adds it to the navigation stack.
func show(route: Route) throws
Expand All @@ -113,6 +113,30 @@ public protocol Navigator: ObservableObject {
}
```

### TabBarCoordinator

The `TabBarCoordinator` protocol provides a way to manage a tab bar interface in your application.
It defines the necessary properties and methods for handling tab bar navigation.

**Protocol declaration**

```Swift
@MainActor
public protocol TabBarCoordinator: ObservableObject {
associatedtype Route: TabBarNavigationRoute

var navigationController: NavigationController { get }
/// The tab bar controller that manages the tab bar interface.
var tabBarController: UITabBarController { get }
/// The tabs available in the tab bar interface, represented by `Route` types.
var tabs: [Route] { get }
/// This method should be called to show the `tabBarController`.
///
/// - Parameter action:The type of transition can be customized by providing a `TransitionAction`.
func start(with action: TransitionAction)
}
```

## 💿 Installation

### Requirements
Expand Down Expand Up @@ -429,7 +453,6 @@ func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
}
```


## 📒 Example project

For better understanding, I recommend that you take a look at the example project located in the `SwiftUICoordinatorExample` folder.
Expand Down
21 changes: 1 addition & 20 deletions Sources/SwiftUICoordinator/Navigator/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public protocol Navigator: ObservableObject {
/// The starting route of the navigator.
var startRoute: Route { get }

/// This method should be called to start the flow and to show the view for the `startRoute`.
/// This method should be called to start the flow and to show the view for the `startRoute`.
func start() throws
/// It creates a view for the route and adds it to the navigation stack.
func show(route: Route) throws
Expand Down Expand Up @@ -106,25 +106,6 @@ public extension Navigator where Self: RouterViewFactory {
}

// MARK: - Private methods

private func hostingController(for route: Route) -> UIHostingController<some View> {
let view: some View = self.view(for: route)
.ifLet(route.title) { view, value in
view.navigationTitle(value)
}
.if(route.attachCoordinator) { view in
view.environmentObject(self)
}

return RouteHostingController(
rootView: view,
route: route
)
}

private func views(for routes: [Route]) -> [UIHostingController<some View>] {
return routes.map { self.hostingController(for: $0) }
}

private func present(
viewController: UIViewController,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ public extension NavigationRoute {
var attachCoordinator: Bool {
return true
}

var action: TransitionAction? {
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// TabBarNavigationRoute.swift
//
//
// Created by Erik Drobne on 24. 6. 24.
//

import UIKit

@MainActor
public protocol TabBarNavigationRoute: NavigationRoute, Hashable {
var tabBarItem: UITabBarItem { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class RouteHostingController<Content: View>: UIHostingController<Content>
init(rootView: Content, route: NavigationRoute) {
self.route = route
super.init(rootView: rootView)

if let tabBarRoute = route as? any TabBarNavigationRoute {
self.tabBarItem = tabBarRoute.tabBarItem
}
}

@objc required dynamic init?(coder aDecoder: NSCoder) {
Expand Down
21 changes: 21 additions & 0 deletions Sources/SwiftUICoordinator/Routing/RouterViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,24 @@ public protocol RouterViewFactory {
@ViewBuilder
func view(for route: Route) -> V
}

extension RouterViewFactory where Self: ObservableObject {
func hostingController(for route: Route) -> UIHostingController<some View> {
let view: some View = self.view(for: route)
.ifLet(route.title) { view, value in
view.navigationTitle(value)
}
.if(route.attachCoordinator) { view in
view.environmentObject(self)
}

return RouteHostingController(
rootView: view,
route: route
)
}

func views(for routes: [Route]) -> [UIHostingController<some View>] {
return routes.map { self.hostingController(for: $0) }
}
}
Loading

0 comments on commit 2112615

Please sign in to comment.