Skip to content

Commit

Permalink
Improves the way tabbar coordinators are created (#40)
Browse files Browse the repository at this point in the history
* Improves the way to create TabbarCoordinators (#39)

* Code refactor - removes unnecessary properties from CoordinatorView

* The `CoordinatorView` works with generic data-source

* Moves functions from `TabbarCoordinator` class to `TabbarCoordinatorType` protocol

* Renames  `PAGE` associatedtype to `Page`

* Changes data-source of TabbarCoordinatorView and CustomTabbarView to generic type

* Updates Readme

* Code refactor - removes warnings
  • Loading branch information
felilo authored Dec 14, 2024
1 parent 20cac01 commit 53cfe21
Show file tree
Hide file tree
Showing 21 changed files with 381 additions and 200 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
//
// AppDelegate.swift
// SUICoordinatorDemo
//
// Created by Andres Lozano on 24/01/24.
// Copyright (c) Andres F. Lozano
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import SwiftUI
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
//
// SUICoordinatorExampleApp.swift
// SUICoordinatorExample
//
// Created by Andres Lozano on 26/01/24.
// Copyright (c) Andres F. Lozano
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import SwiftUI
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
//
// ContentView.swift
// SUICoordinatorExample
//
// Created by Andres Lozano on 26/01/24.
// Copyright (c) Andres F. Lozano
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import SwiftUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class CustomTabbarCoordinator: TabbarCoordinator<MyTabbarPage> {
presentationStyle: TransitionPresentationStyle = .sheet
) {
super.init(
pages: PAGE.allCases,
pages: Page.allCases,
currentPage: currentPage
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,52 @@
import SwiftUI
import SUICoordinator

struct CustomTabbarView: View {
struct CustomTabbarView<DataSource: TabbarCoordinatorType>: View where DataSource.Page: TabbarPage{

// ---------------------------------------------------------------------
// MARK: Typealias
// ---------------------------------------------------------------------

typealias Page = MyTabbarPage
typealias BadgeItem = (value: String?, page: Page)

typealias Page = DataSource.Page
typealias BadgeItem = DataSource.BadgeItem


// ---------------------------------------------------------------------
// MARK: Properties
// ---------------------------------------------------------------------

@StateObject private var viewModel: TabbarCoordinator<Page>

@StateObject private var viewModel: DataSource
@State private var currentPage: Page
@State private var pages: [Page]
@State var badges = [BadgeItem]()

let widthIcon: CGFloat = 22


// ---------------------------------------------------------------------
// MARK: Init
// ---------------------------------------------------------------------

init(viewModel: TabbarCoordinator<Page>) {

init(viewModel: DataSource) {
self._viewModel = .init(wrappedValue: viewModel)
currentPage = viewModel.currentPage
pages = viewModel.pages
badges = viewModel.pages.map { (nil, $0) }
}


// ---------------------------------------------------------------------
// MARK: View
// ---------------------------------------------------------------------


var body: some View {
ZStack(alignment: .bottomLeading) {
TabView(selection: $currentPage) {
ForEach(pages, id: \.id, content: makeTabView)
ForEach(pages, id: \.id, content: tabBarItem)
}

VStack() {
Expand All @@ -71,20 +79,24 @@ struct CustomTabbarView: View {
}.background(
.gray.opacity(0.5),
in: RoundedRectangle(cornerRadius: 0, style: .continuous)
).frame(maxWidth: .infinity, maxHeight: 60)
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.padding(.horizontal, 10)
)
.frame(maxWidth: .infinity, maxHeight: 60)
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.padding(.horizontal, 10)
}
.ignoresSafeArea(.all, edges: [.leading, .trailing, .top])
.onReceive(viewModel.$currentPage) { currentPage = $0 }
.onReceive(viewModel.$pages) { pages in
.onChange(of: viewModel.currentPage) { currentPage = $0 }
.onChange(of: viewModel.pages) { pages in
self.pages = pages
badges = pages.map { (nil, $0) }
}
.onReceive(viewModel.setBadge) { (value, page) in
guard let index = getBadgeIndex(page: page) else { return }
badges[index].value = value
}
.onAppear {
badges = pages.map { (nil, $0) }
}
}


Expand All @@ -94,7 +106,7 @@ struct CustomTabbarView: View {


@ViewBuilder
func makeTabView(page: Page) -> some View {
func tabBarItem(page: Page) -> some View {
if let item = viewModel.getCoordinator(with: page.position) {
AnyView( item.getView() )
.toolbar(.hidden)
Expand All @@ -103,7 +115,7 @@ struct CustomTabbarView: View {
}


private func tabbarButton(@State page: Page, size: CGRect) -> some View {
private func customTabBarItem(@State page: Page, size: CGRect) -> some View {
Button {
viewModel.setCurrentPage(page)
} label: {
Expand All @@ -115,34 +127,34 @@ struct CustomTabbarView: View {
}
}
.frame(width: getWidthButton(with: size))
.overlay( buildCustomBadge(with: page) )
.overlay( customBadge(with: page) )
}


@ViewBuilder
func buildCustomBadge(with page: Page) -> some View {
HStack {
if let value = getBadge(page: page)?.value {
HStack(alignment: .top) {
Text(value)
.font(.footnote)
.foregroundStyle(.white)
.padding(5)

}
.frame(height: 40 / 2 )
.background(.red)
.clipShape(Capsule())
.offset(x: 15, y: -15)
func customBadge(with page: Page) -> some View {
if let value = getBadge(page: page)?.value {
HStack(alignment: .top) {
Text(value)
.font(.footnote)
.foregroundStyle(.white)
.padding(5)

}
.frame(height: 40 / 2 )
.background(.red)
.clipShape(Capsule())
.offset(x: 15, y: -15)
}
}


@ViewBuilder
func customTabbar() -> some View {
GeometryReader { proxy in
HStack(spacing: 0) {
ForEach(pages, id: \.id) {
tabbarButton(
customTabBarItem(
page: $0,
size: proxy.frame(in: .global)
)
Expand All @@ -151,21 +163,25 @@ struct CustomTabbarView: View {
}
}


private func getBadge(page: Page) -> BadgeItem? {
guard let index = getBadgeIndex(page: page) else {
return nil
}
return badges[index]
}


private func getBadgeIndex(page: Page) -> Int? {
badges.firstIndex(where: { $0.1 == page })
}


// ---------------------------------------------------------------------
// MARK: Helper funcs
// ---------------------------------------------------------------------


private func getWidthButton(with size: CGRect) -> CGFloat {
size.width / CGFloat(pages.count)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DefaultTabbarCoordinator: TabbarCoordinator<MyTabbarPage> {
// ---------------------------------------------------------------------

init() {
super.init(pages: PAGE.allCases, currentPage: .second)
super.init(pages: Page.allCases, currentPage: .second)

/// Set badge of a tap
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
Expand Down
9 changes: 0 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,11 @@ let package = Package(
name: "SUICoordinator",
platforms: [.iOS(.v16)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
// .executable(name: "SUICoordinator", targets: ["SUICoordinator"]),
.library(
name: "SUICoordinator",
targets: ["SUICoordinator"]),
],
// dependencies: [
// .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
//
// ],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// .executableTarget(name: "SUICoordinator")
.target(
name: "SUICoordinator"),
.testTarget(
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class HomeCoordinator: Coordinator<HomeRoute> {

func presentTabbarCoordinator() async {
let coordinator = CustomTabbarCoordinator()
await navigate(to: coordinator, presentationStyle: .sheet, animated: animated)
await navigate(to: coordinator, presentationStyle: .sheet)
}

func close() async {
Expand Down Expand Up @@ -208,7 +208,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
let coordinator = CustomTabbarCoordinator()
try? await coordinator.forcePresentation(
presentationStyle: .fullScreenCover,
mainCoordinator: self?.mainCoodinator
mainCoordinator: self?.shee
)
}
}
Expand Down Expand Up @@ -476,6 +476,11 @@ It works the same as Coordinator but has the following additional features:
</td>
<td>Variable that allows set the badge of a tab</td>
</tr>
<tr>
<td><code style="color: blue;">customView</code></td>
<td></td>
<td>Is a closure that receives a <code style="color: #ec6b6f;">View</code> as parameter</td>
</tr>
</tbody>
</table>

Expand Down
2 changes: 1 addition & 1 deletion Sources/SUICoordinator/Coordinator/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ open class Coordinator<Route: RouteType>: ObservableObject, CoordinatorType {
self.router = .init()
self.uuid = "\(NSStringFromClass(type(of: self))) - \(UUID().uuidString)"

router.isTabbarCoordinable = isTabbarCoordinable
router.isTabbarCoordinable = false
}

// --------------------------------------------------------------------
Expand Down
25 changes: 6 additions & 19 deletions Sources/SUICoordinator/Coordinator/CoordinatorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,30 @@
import SwiftUI


public struct CoordinatorView<Route: RouteType>: View {
public struct CoordinatorView<DataSource: CoordinatorType>: View {

// --------------------------------------------------------------------
// MARK: Wrapper properties
// --------------------------------------------------------------------

@StateObject var viewModel: Coordinator<Route>

// --------------------------------------------------------------------
// MARK: Properties
// --------------------------------------------------------------------

var onClean: (() async -> Void)?
var onSetTag: ((String) -> Void)?
@StateObject var dataSource: DataSource

// --------------------------------------------------------------------
// MARK: Constructor
// --------------------------------------------------------------------

init(
viewModel: Coordinator<Route>,
onClean: (() async -> Void)? = nil,
onSetTag: ((String) -> Void)? = nil
) {
self._viewModel = .init(wrappedValue: viewModel)
self.onClean = onClean
self.onSetTag = onSetTag
init(dataSource: DataSource) {
self._dataSource = .init(wrappedValue: dataSource)
}

// --------------------------------------------------------------------
// MARK: View
// --------------------------------------------------------------------

public var body: some View {
RouterView(viewModel: viewModel.router)
RouterView(viewModel: dataSource.router)
.onViewDidLoad {
Task(priority: .high) { await viewModel.start() }
Task(priority: .high) { await dataSource.start(animated: true) }
}
}
}
Loading

0 comments on commit 53cfe21

Please sign in to comment.