diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample.xcodeproj/project.pbxproj b/Examples/SUICoordinatorExample/SUICoordinatorExample.xcodeproj/project.pbxproj index fc917b3..ed090cc 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample.xcodeproj/project.pbxproj +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample.xcodeproj/project.pbxproj @@ -539,7 +539,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -577,7 +577,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/CustomTabbar/CustomTabbarView.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/CustomTabbar/CustomTabbarView.swift index 8e7c5dc..1214e52 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/CustomTabbar/CustomTabbarView.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/CustomTabbar/CustomTabbarView.swift @@ -43,7 +43,7 @@ struct CustomTabbarView: View where DataSourc @StateObject private var viewModel: DataSource @State private var currentPage: Page - @State private var pages: [Page] + @State private var pages: [Page] = [] @State var badges = [BadgeItem]() let widthIcon: CGFloat = 22 @@ -57,8 +57,6 @@ struct CustomTabbarView: View where DataSourc init(viewModel: DataSource) { self._viewModel = .init(wrappedValue: viewModel) currentPage = viewModel.currentPage - pages = viewModel.pages - badges = viewModel.pages.map { (nil, $0) } } @@ -94,8 +92,10 @@ struct CustomTabbarView: View where DataSourc guard let index = getBadgeIndex(page: page) else { return } badges[index].value = value } - .onAppear { - badges = pages.map { (nil, $0) } + .task { + currentPage = viewModel.currentPage + pages = viewModel.pages + badges = viewModel.pages.map { (nil, $0) } } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/ActionList/ActionListViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/ActionList/ActionListViewModel.swift index 15810c6..c8ac975 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/ActionList/ActionListViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/ActionList/ActionListViewModel.swift @@ -32,31 +32,31 @@ class ActionListViewModel: ObservableObject { self.coordinator = coordinator } - func navigateToFirstView() async { + @MainActor func navigateToFirstView() async { await coordinator.navigateToPushView() } - func presentSheet() async { + @MainActor func presentSheet() async { await coordinator.presentSheet() } - func presentFullscreen() async { + @MainActor func presentFullscreen() async { await coordinator.presentFullscreen() } - func presentDetents() async { + @MainActor func presentDetents() async { await coordinator.presentDetents() } - func presentTabbarCoordinator() async { + @MainActor func presentTabbarCoordinator() async { await coordinator.presentTabbarCoordinator() } - func finish() async { + @MainActor func finish() async { await coordinator.finish() } - func showFinishButton() -> Bool { + @MainActor func showFinishButton() -> Bool { !(coordinator.parent is HomeCoordinator) } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/DetentsView/DetentsViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/DetentsView/DetentsViewModel.swift index ff201fb..82ac30a 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/DetentsView/DetentsViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/DetentsView/DetentsViewModel.swift @@ -32,15 +32,15 @@ class DetentsViewModel: ObservableObject { self.coordinator = coordinator } - func navigateToNextView() async { + @MainActor func navigateToNextView() async { await coordinator.presentTabbarCoordinator() } - func close() async { + @MainActor func close() async { await coordinator.close() } - func finishFlow() async { + @MainActor func finishFlow() async { await coordinator.finish() } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/FullscreenView/FullscreenViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/FullscreenView/FullscreenViewModel.swift index 1fb95e1..8b806f1 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/FullscreenView/FullscreenViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/FullscreenView/FullscreenViewModel.swift @@ -32,11 +32,11 @@ class FullscreenViewModel: ObservableObject { self.coordinator = coordinator } - func navigateToNextView() async { + @MainActor func navigateToNextView() async { await coordinator.presentDetents() } - func close() async { + @MainActor func close() async { await coordinator.close() } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/PushView/PushViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/PushView/PushViewModel.swift index c3e39dd..1ce8d76 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/PushView/PushViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/PushView/PushViewModel.swift @@ -32,11 +32,11 @@ class PushViewModel: ObservableObject { self.coordinator = coordinator } - func navigateToNextView() async { + @MainActor func navigateToNextView() async { await coordinator.presentSheet() } - func close() async { + @MainActor func close() async { await coordinator.close() } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetView.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetView.swift index 12c3b8e..f01adf3 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetView.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetView.swift @@ -41,11 +41,11 @@ struct SheetView: View { VStack { Button("Presents FullscreenView") { - Task { await viewModel.navigateToNextView() } + Task { await viewModel.navigateToNextView() } }.buttonStyle(.borderedProminent) Button("Close view") { - Task { await viewModel.close() } + Task { await viewModel.close() } }.buttonStyle(.borderedProminent) } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetViewModel.swift index 36899ea..fd95d2c 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/Home/SheetView/SheetViewModel.swift @@ -32,11 +32,11 @@ class SheetViewModel: ObservableObject { self.coordinator = coordinator } - func navigateToNextView() async { + @MainActor func navigateToNextView() async { await coordinator.presentFullscreen() } - func close() async { + @MainActor func close() async { await coordinator.close() } } diff --git a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/TabbarFlow/TabbarActionList/TabbarActionListViewModel.swift b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/TabbarFlow/TabbarActionList/TabbarActionListViewModel.swift index 410f60e..ad132b8 100644 --- a/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/TabbarFlow/TabbarActionList/TabbarActionListViewModel.swift +++ b/Examples/SUICoordinatorExample/SUICoordinatorExample/Modules/TabbarFlow/TabbarActionList/TabbarActionListViewModel.swift @@ -32,15 +32,15 @@ class TabbarActionListViewModel: ObservableObject { self.coordinator = coordinator } - func presentDefaultTabbarCoordinator() async { + @MainActor func presentDefaultTabbarCoordinator() async { await coordinator.presentDefaultTabbarCoordinator() } - func presentCustomTabbarCoordinator() async { + @MainActor func presentCustomTabbarCoordinator() async { await coordinator.presentCustomTabbarCoordinator() } - func finsh() async { + @MainActor func finsh() async { await coordinator.finishFlow() } } diff --git a/Package.swift b/Package.swift index 6b8b285..e4ecde6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Helpers.swift b/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Helpers.swift index 58bf811..251d972 100644 --- a/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Helpers.swift +++ b/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Helpers.swift @@ -96,7 +96,7 @@ extension CoordinatorType { guard let first = children.first else { return } if let parent = first.parent as? (any TabbarCoordinatable) { - await parent.setCurrentPage(with: first) + parent.setCurrentPage(with: first) } await first.emptyCoordinator(animated: animated) @@ -150,26 +150,25 @@ extension CoordinatorType { /// - withDismiss: A boolean value indicating whether to dismiss the coordinator. /// - Returns: An asynchronous void task representing the finish process. func finish(animated: Bool = true, withDismiss: Bool = true) async -> Void { - let handleFinish = { (coordinator: TCoordinatorType) async -> Void in - await coordinator.emptyCoordinator(animated: animated) - } - guard let parent, withDismiss else { - return await handleFinish(self) + return await emptyCoordinator(animated: animated) } if parent is (any TabbarCoordinatable) { await parent.parent?.closeLastSheet(animated: animated) - return await handleFinish(parent) + return await parent.emptyCoordinator(animated: animated) } await parent.closeLastSheet(animated: animated) - await handleFinish(self) + await emptyCoordinator(animated: animated) } /// Cleans up the coordinator. - func swipedAway() async { + func swipedAway() { guard !isEmptyCoordinator else { return } - await finish(animated: false, withDismiss: false) + + Task(priority: .low) { [weak self] in + await self?.finish(animated: false, withDismiss: false) + } } } diff --git a/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Navigation.swift b/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Navigation.swift index 9839b57..c2b38fc 100644 --- a/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Navigation.swift +++ b/Sources/SUICoordinator/CoordinatorType/CoordinatorType+Navigation.swift @@ -52,12 +52,10 @@ public extension CoordinatorType { animated: animated, presentationStyle: (presentationStyle != .push) ? presentationStyle : .sheet, view: { [weak coordinator] in coordinator?.getView() }, - onFinish: { Task(priority: .low) { - @MainActor [weak coordinator] in await coordinator?.swipedAway() - }} + onFinish: {[weak coordinator] in coordinator?.swipedAway()} ) - await router.presentSheet(item: item) + router.presentSheet(item: item) } /// Finishes the flow of the coordinator. diff --git a/Sources/SUICoordinator/CoordinatorType/CoordinatorType.swift b/Sources/SUICoordinator/CoordinatorType/CoordinatorType.swift index 7b09fb2..fa0afc2 100644 --- a/Sources/SUICoordinator/CoordinatorType/CoordinatorType.swift +++ b/Sources/SUICoordinator/CoordinatorType/CoordinatorType.swift @@ -30,6 +30,7 @@ import Foundation /// of a specific module or feature in an application. /// /// - Important: Adopt this protocol in your custom coordinator implementations. +@MainActor public protocol CoordinatorType: SCHashable, ObservableObject { // --------------------------------------------------------- diff --git a/Sources/SUICoordinator/Router/RouteType.swift b/Sources/SUICoordinator/Router/RouteType.swift index f377caa..d9d10ab 100644 --- a/Sources/SUICoordinator/Router/RouteType.swift +++ b/Sources/SUICoordinator/Router/RouteType.swift @@ -48,5 +48,5 @@ public protocol RouteType: SCHashable { var presentationStyle: TransitionPresentationStyle { get } /// The body of the route, conforming to the View protocol. - @ViewBuilder var view: Body { get } + @ViewBuilder @MainActor var view: Body { get } } diff --git a/Sources/SUICoordinator/Router/Router.swift b/Sources/SUICoordinator/Router/Router.swift index ab31101..b639236 100644 --- a/Sources/SUICoordinator/Router/Router.swift +++ b/Sources/SUICoordinator/Router/Router.swift @@ -185,6 +185,7 @@ public class Router: ObservableObject, RouterType { @MainActor public func clean(animated: Bool, withMainView: Bool = true) async -> Void { await popToRoot(animated: false) items.removeAll() + await sheetCoordinator.clean() sheetCoordinator = .init() if withMainView { diff --git a/Sources/SUICoordinator/Router/RouterType.swift b/Sources/SUICoordinator/Router/RouterType.swift index 299c060..9d40157 100644 --- a/Sources/SUICoordinator/Router/RouterType.swift +++ b/Sources/SUICoordinator/Router/RouterType.swift @@ -65,7 +65,7 @@ public protocol RouterType: ObservableObject { /// - route: The route to navigate to. /// - presentationStyle: The transition presentation style for the navigation. /// - animated: A boolean value indicating whether to animate the navigation. - func navigate(to route: Route, presentationStyle: TransitionPresentationStyle?, animated: Bool) async + @MainActor func navigate(to route: Route, presentationStyle: TransitionPresentationStyle?, animated: Bool) async /// Presents a view or coordinator with optional presentation style and animation. /// @@ -73,19 +73,19 @@ public protocol RouterType: ObservableObject { /// - view: The view or coordinator to present. /// - presentationStyle: The transition presentation style for the presentation. /// - animated: A boolean value indicating whether to animate the presentation. - func present(_ view: Route, presentationStyle: TransitionPresentationStyle?, animated: Bool) async + @MainActor func present(_ view: Route, presentationStyle: TransitionPresentationStyle?, animated: Bool) async /// Pops the top view or coordinator from the navigation stack. /// /// - Parameters: /// - animated: A boolean value indicating whether to animate the pop action. - func pop(animated: Bool) async + @MainActor func pop(animated: Bool) async /// Pops to the root of the navigation stack. /// /// - Parameters: /// - animated: A boolean value indicating whether to animate the pop action. - func popToRoot(animated: Bool) async + @MainActor func popToRoot(animated: Bool) async /// Pops to a specific view or coordinator in the navigation stack. /// @@ -93,13 +93,13 @@ public protocol RouterType: ObservableObject { /// - view: The target view or coordinator to pop to. /// - animated: A boolean value indicating whether to animate the pop action. /// - Returns: A boolean value indicating whether the pop action was successful. - func popToView(_ view: T, animated: Bool) async -> Bool + @MainActor func popToView(_ view: T, animated: Bool) async -> Bool /// Dismisses the currently presented view or coordinator. /// /// - Parameters: /// - animated: A boolean value indicating whether to animate the dismissal. - func dismiss(animated: Bool) async + @MainActor func dismiss(animated: Bool) async /// Cleans up the current view or coordinator, optionally preserving the main view. /// @@ -113,7 +113,7 @@ public protocol RouterType: ObservableObject { /// - Parameters: /// - animated: A boolean value indicating whether to animate the closing action. /// - finishFlow: A boolean value indicating whether to finish the associated flow. - func close(animated: Bool, finishFlow: Bool) async -> Void + @MainActor func close(animated: Bool, finishFlow: Bool) async -> Void /// Restarts the current view or coordinator, optionally animating the restart. /// diff --git a/Sources/SUICoordinator/Router/TransitionPresentationStyle.swift b/Sources/SUICoordinator/Router/TransitionPresentationStyle.swift index 23924e5..b4c7ec6 100644 --- a/Sources/SUICoordinator/Router/TransitionPresentationStyle.swift +++ b/Sources/SUICoordinator/Router/TransitionPresentationStyle.swift @@ -29,7 +29,7 @@ import SwiftUI TransitionPresentationStyle enumerates the different styles used for transitioning between views or presenting views within an application. */ -public enum TransitionPresentationStyle: SCEquatable { +public enum TransitionPresentationStyle: SCEquatable, Sendable { /// A push transition style, commonly used in navigation controllers. case push diff --git a/Sources/SUICoordinator/Shared/Protocols/SheetItemType.swift b/Sources/SUICoordinator/Shared/Protocols/SheetItemType.swift index 3b7160f..82c48bc 100644 --- a/Sources/SUICoordinator/Shared/Protocols/SheetItemType.swift +++ b/Sources/SUICoordinator/Shared/Protocols/SheetItemType.swift @@ -26,8 +26,8 @@ import Foundation protocol SheetItemType: SCIdentifiable { /// A boolean value indicating whether to animate the presentation. - var animated: Bool { get set } + func isAnimated() -> Bool /// The transition presentation style for presenting the sheet item. - var presentationStyle: TransitionPresentationStyle { get set } + func getPresentationStyle() -> TransitionPresentationStyle } diff --git a/Sources/SUICoordinator/SheetCoordinator/SheetItem.swift b/Sources/SUICoordinator/SheetCoordinator/SheetItem.swift index 7072d80..bf767bf 100644 --- a/Sources/SUICoordinator/SheetCoordinator/SheetItem.swift +++ b/Sources/SUICoordinator/SheetCoordinator/SheetItem.swift @@ -27,25 +27,25 @@ import Foundation /// A class representing a sheet item for presenting views or coordinators in a coordinator-based architecture. /// /// Sheet items encapsulate information about the view, animation, and presentation style. -final public class SheetItem: SCHashable, SheetItemType { +public struct SheetItem:SCHashable, SheetItemType { // --------------------------------------------------------- // MARK: Properties // --------------------------------------------------------- /// The unique identifier for the sheet item. - public var id: String + public let id: String /// The view or coordinator associated with the sheet item. let view: () -> T? /// A boolean value indicating whether to animate the presentation. - var animated: Bool + let animated: Bool /// The transition presentation style for presenting the sheet item. - var presentationStyle: TransitionPresentationStyle + let presentationStyle: TransitionPresentationStyle - var onFinish: (() -> Void)? + private var itemDeallocate: SheetItemDeallocator? // --------------------------------------------------------- // MARK: Constructor @@ -69,13 +69,41 @@ final public class SheetItem: SCHashable, SheetItemType { self.animated = animated self.presentationStyle = presentationStyle self.id = id - self.onFinish = onFinish + + itemDeallocate = .init(onFinish: onFinish) } // --------------------------------------------------------- // MARK: Deinitializer // --------------------------------------------------------- + + func getPresentationStyle() -> TransitionPresentationStyle { + presentationStyle + } + + func isAnimated() -> Bool { + animated + } +} + + +/// A utility class to handle deallocation of sheet items, providing a callback +/// that is executed upon deinitialization of the object. +class SheetItemDeallocator { + + /// A closure that is invoked when the instance is deallocated. + var onFinish: (() -> Void)? + + /// Initializes a new `SheetItemDeallocator` instance. + /// + /// - Parameter onFinish: A closure to be executed when the instance is deallocated. Defaults to `nil`. + init(onFinish: (() -> Void)? = nil ) { + self.onFinish = onFinish + } + + /// Deinitializes the instance, invoking the `onFinish` closure if set, and + /// cleans up the closure reference to avoid potential memory leaks. deinit { onFinish?() onFinish = nil diff --git a/Sources/SUICoordinator/SheetCoordinator/SheetView.swift b/Sources/SUICoordinator/SheetCoordinator/SheetView.swift index c35450a..3ca3ff1 100644 --- a/Sources/SUICoordinator/SheetCoordinator/SheetView.swift +++ b/Sources/SUICoordinator/SheetCoordinator/SheetView.swift @@ -128,6 +128,6 @@ struct SheetView: View { return transitionStyle } - return items[index]?.presentationStyle ?? transitionStyle + return items[index]?.getPresentationStyle() ?? transitionStyle } } diff --git a/Sources/SUICoordinator/Tabbar/TabbarCoordinator.swift b/Sources/SUICoordinator/Tabbar/TabbarCoordinator.swift index ae1af8f..5d3abff 100644 --- a/Sources/SUICoordinator/Tabbar/TabbarCoordinator.swift +++ b/Sources/SUICoordinator/Tabbar/TabbarCoordinator.swift @@ -70,7 +70,7 @@ open class TabbarCoordinator: TabbarCoordinatable { public var setBadge: PassthroughSubject<(String?, Page), Never> = .init() /// A custom view associated with the tabbar coordinator. - public var customView: (() -> (Page.View))? + public var customView: (() -> (Page.View?))? // --------------------------------------------------------- // MARK: Constructor @@ -87,7 +87,7 @@ open class TabbarCoordinator: TabbarCoordinatable { pages: [Page], currentPage: Page, presentationStyle: TransitionPresentationStyle = .sheet, - customView: (() -> Page.View)? = nil + customView: (() -> Page.View?)? = nil ) { self.router = .init() self.uuid = "\(NSStringFromClass(type(of: self))) - \(UUID().uuidString)" @@ -108,7 +108,7 @@ open class TabbarCoordinator: TabbarCoordinatable { /// - Parameters: /// - animated: A boolean value indicating whether to animate the start process. open func start(animated: Bool = true) async { - await setupPages(pages, currentPage: currentPage) + setupPages(pages, currentPage: currentPage) let cView = customView?() ?? TabbarCoordinatorView(dataSource: self, currentPage: currentPage) @@ -139,4 +139,11 @@ open class TabbarCoordinator: TabbarCoordinatable { else { throw TabbarCoordinatorError.coordinatorSelected } return children[index] } + + @MainActor public func clean() async { + await setPages([], currentPage: nil) + await router.clean(animated: false) + router = .init() + customView = nil + } } diff --git a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType+TabbarCoordinatable.swift b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType+TabbarCoordinatable.swift index 9f5d4ef..775d5bb 100644 --- a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType+TabbarCoordinatable.swift +++ b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType+TabbarCoordinatable.swift @@ -24,13 +24,6 @@ extension TabbarCoordinatorType where Self : TabbarCoordinatable { - /// Cleans the coordinator. - @MainActor func clean() async { - pages.removeAll() - await router.clean(animated: false) - customView = nil - } - /// Sets the array of pages for the tabbar coordinator. /// /// - Parameters: @@ -38,14 +31,14 @@ extension TabbarCoordinatorType where Self : TabbarCoordinatable { /// - currentPage: The optional current page to set. public func setPages(_ values: [Page], currentPage: Page? = nil) async { await removeChildren() - await setupPages(values, currentPage: currentPage) + setupPages(values, currentPage: currentPage) } /// Sets up the pages for the tabbar coordinator. /// /// - Parameters: /// - value: The array of pages to set up. - @MainActor func setupPages(_ value: [Page], currentPage: Page? = nil) { + func setupPages(_ value: [Page], currentPage: Page? = nil) { for page in value { let item = page.coordinator() startChildCoordinator(item) diff --git a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType.swift b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType.swift index a75ad0d..8e6a82a 100644 --- a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType.swift +++ b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorType.swift @@ -28,6 +28,7 @@ import Combine /// A protocol representing a type for managing and coordinating a tabbar-based navigation. /// /// Tabbar coordinator types define the interface for handling the selected page and badge updates. +@MainActor public protocol TabbarCoordinatorType: ObservableObject { // --------------------------------------------------------- @@ -70,7 +71,7 @@ public protocol TabbarCoordinatorType: ObservableObject { /// /// This closure provides a SwiftUI view for customization, which can be associated with a specific /// `Page`. The view is optional and can be left `nil` if no custom view is needed. - var customView: (() -> (Page.View))? { get set } + var customView: (() -> (Page.View?))? { get set } // --------------------------------------------------------- // MARK: Functions @@ -87,6 +88,8 @@ public protocol TabbarCoordinatorType: ObservableObject { /// - Returns: The coordinator that corresponds to the selected tab. /// - Throws: An error if the selected coordinator cannot be determined. func getCoordinatorSelected() throws -> (any CoordinatorType) + + @MainActor func clean() async } /// A type alias representing a coordinator that conforms to both `CoordinatorType` and `TabbarCoordinatorType`. diff --git a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorView.swift b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorView.swift index 148d451..ae21cbd 100644 --- a/Sources/SUICoordinator/Tabbar/TabbarCoordinatorView.swift +++ b/Sources/SUICoordinator/Tabbar/TabbarCoordinatorView.swift @@ -30,7 +30,6 @@ struct TabbarCoordinatorView: View where Data typealias Page = DataSource.Page typealias BadgeItem = DataSource.BadgeItem - // --------------------------------------------------------------------- // MARK: Properties // --------------------------------------------------------------------- @@ -40,12 +39,17 @@ struct TabbarCoordinatorView: View where Data @State var pages = [Page]() @State var currentPage: Page + init(dataSource: DataSource, currentPage: Page) { + self._dataSource = .init(wrappedValue: dataSource) + self.currentPage = dataSource.currentPage + } + // --------------------------------------------------------------------- // MARK: View // --------------------------------------------------------------------- public var body: some View { - TabView(selection: tabSelection()){ + TabView(selection: $dataSource.currentPage){ ForEach(pages, id: \.id, content: tabBarItem) } .onChange(of: dataSource.pages) { pages in @@ -59,6 +63,10 @@ struct TabbarCoordinatorView: View where Data guard let index = getBadgeIndex(page: page) else { return } badges[index].value = value } + .task { + pages = dataSource.pages + badges = pages.map { (nil, $0) } + } } // --------------------------------------------------------------------- @@ -90,18 +98,3 @@ struct TabbarCoordinatorView: View where Data badges.firstIndex(where: { $0.1 == page }) } } - - -extension TabbarCoordinatorView { - - private func tabSelection() -> Binding { - Binding { - currentPage - } set: { [weak dataSource] tappedTab in - if tappedTab == currentPage { - Task(priority: .high) { await dataSource?.popToRoot() } - } - dataSource?.currentPage = tappedTab - } - } -} diff --git a/Sources/SUICoordinator/Tabbar/TabbarNavigationRouter.swift b/Sources/SUICoordinator/Tabbar/TabbarNavigationRouter.swift index e5b701e..e0b30ac 100644 --- a/Sources/SUICoordinator/Tabbar/TabbarNavigationRouter.swift +++ b/Sources/SUICoordinator/Tabbar/TabbarNavigationRouter.swift @@ -25,7 +25,7 @@ import Foundation /// A protocol representing a type for managing and providing a coordinator for tabbar navigation. -public protocol TabbarNavigationRouter { +public protocol TabbarNavigationRouter: Sendable { // --------------------------------------------------------- // MARK: Functions @@ -34,5 +34,5 @@ public protocol TabbarNavigationRouter { /// Retrieves a coordinator associated with tabbar navigation. /// /// - Returns: The coordinator associated with tabbar navigation. - func coordinator() -> (any CoordinatorType) + @MainActor func coordinator() -> (any CoordinatorType) } diff --git a/Tests/SUICoordinatorTests/CoordinatorTests.swift b/Tests/SUICoordinatorTests/CoordinatorTests.swift index f42ac92..59e996b 100644 --- a/Tests/SUICoordinatorTests/CoordinatorTests.swift +++ b/Tests/SUICoordinatorTests/CoordinatorTests.swift @@ -30,7 +30,7 @@ final class CoordinatorTests: XCTestCase { private let animated: Bool = false - func test_finshFlow() async throws { + @MainActor func test_finshFlow() async throws { let sut = makeSUT() await sut.router.navigate(to: .pushStep2, animated: animated ) @@ -41,7 +41,7 @@ final class CoordinatorTests: XCTestCase { XCTAssertEqual(sut.router.sheetCoordinator.items.count, 0) } - func test_finshFlow_mainCoordinator() async throws { + @MainActor func test_finshFlow_mainCoordinator() async throws { let sut = AnyCoordinator() let coordinator = OtherCoordinator() @@ -55,7 +55,7 @@ final class CoordinatorTests: XCTestCase { XCTAssertEqual(sut.router.sheetCoordinator.items.count, 0) } - func test_starFlow() async throws { + @MainActor func test_starFlow() async throws { let sut = makeSUT() let route = AnyEnumRoute.fullScreenStep @@ -68,7 +68,7 @@ final class CoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_parentCoordinator_not_nil() async throws { + @MainActor func test_parentCoordinator_not_nil() async throws { let sut = makeSUT() let coordinator = OtherCoordinator() @@ -78,7 +78,7 @@ final class CoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_navigateToCoordinator() async throws { + @MainActor func test_navigateToCoordinator() async throws { let sut = makeSUT() let coordinator = OtherCoordinator() @@ -89,7 +89,7 @@ final class CoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_getTopmostCoordinator() async throws { + @MainActor func test_getTopmostCoordinator() async throws { let sut = makeSUT() let coordinator1 = OtherCoordinator() let coordinator2 = AnyCoordinator() @@ -101,7 +101,7 @@ final class CoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_force_to_present_coordinator() async throws { + @MainActor func test_force_to_present_coordinator() async throws { let sut = makeSUT() let coordinator1 = OtherCoordinator() let coordinator2 = AnyCoordinator() @@ -117,7 +117,7 @@ final class CoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_finishCoordinatorWhichHasChildren() async throws { + @MainActor func test_finishCoordinatorWhichHasChildren() async throws { let sut = makeSUT() let coordinator1 = OtherCoordinator() let coordinator2 = AnyCoordinator() @@ -141,7 +141,7 @@ final class CoordinatorTests: XCTestCase { // MARK: Helpers // -------------------------------------------------------------------- - private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> AnyCoordinator { + @MainActor private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> AnyCoordinator { let coordinator = AnyCoordinator() trackForMemoryLeaks(coordinator, file: file, line: line) return coordinator diff --git a/Tests/SUICoordinatorTests/Helpers/Tests+Helpers.swift b/Tests/SUICoordinatorTests/Helpers/Tests+Helpers.swift index f07ec08..84fd998 100644 --- a/Tests/SUICoordinatorTests/Helpers/Tests+Helpers.swift +++ b/Tests/SUICoordinatorTests/Helpers/Tests+Helpers.swift @@ -28,13 +28,13 @@ import Combine extension XCTestCase { - func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) { - addTeardownBlock { [weak instance] in + @MainActor func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) { + addTeardownBlock { @MainActor [weak instance] in XCTAssertNil(instance, "Potential memory leak.", file: file, line: line) } } - func navigateToCoordinator( + @MainActor func navigateToCoordinator( _ nextCoordinator: (any CoordinatorType), in coordinator: (any CoordinatorType), animated: Bool = false @@ -43,10 +43,11 @@ extension XCTestCase { to: nextCoordinator, presentationStyle: .fullScreenCover, animated: animated) + await nextCoordinator.start(animated: animated) } - func finishFlow(sut: (any CoordinatorType), animated: Bool = false) async { + @MainActor func finishFlow(sut: (any CoordinatorType), animated: Bool = false) async { await sut.finishFlow(animated: animated) } diff --git a/Tests/SUICoordinatorTests/RouterTests.swift b/Tests/SUICoordinatorTests/RouterTests.swift index d43e8d5..72bccc0 100644 --- a/Tests/SUICoordinatorTests/RouterTests.swift +++ b/Tests/SUICoordinatorTests/RouterTests.swift @@ -28,7 +28,7 @@ import XCTest final class RouterTests: XCTestCase { - func test_navigationStack_pushToRoute() async throws { + @MainActor func test_navigationStack_pushToRoute() async throws { let sut = makeSUT() let route = AnyEnumRoute.pushStep @@ -37,7 +37,7 @@ final class RouterTests: XCTestCase { XCTAssertEqual(sut.items.last?.id, route.id) } - func test_navigationStack_pop() async throws { + @MainActor func test_navigationStack_pop() async throws { let sut = makeSUT() await sut.navigate(to: .pushStep, animated: false) @@ -46,7 +46,7 @@ final class RouterTests: XCTestCase { XCTAssertNil(sut.items.last ?? nil) } - func test_navigationStack_popToRoot() async throws { + @MainActor func test_navigationStack_popToRoot() async throws { let sut = makeSUT() await sut.navigate(to: .pushStep, animated: false) @@ -58,7 +58,7 @@ final class RouterTests: XCTestCase { XCTAssertNil(sut.items.last ?? nil) } - func test_closeRoute() async throws { + @MainActor func test_closeRoute() async throws { let sut = makeSUT() await sut.navigate(to: .pushStep, animated: false) @@ -67,11 +67,11 @@ final class RouterTests: XCTestCase { await sut.navigate(to: .sheetStep, animated: false) await sut.close(animated: false) - await sut.sheetCoordinator.removeAllNilItems() + sut.sheetCoordinator.removeAllNilItems() XCTAssertEqual(sut.sheetCoordinator.items.count, 0) } - func test_cleanRouter() async throws { + @MainActor func test_cleanRouter() async throws { let sut = makeSUT() await sut.navigate(to: .pushStep, animated: false) @@ -85,7 +85,7 @@ final class RouterTests: XCTestCase { XCTAssertEqual(sut.sheetCoordinator.items.count, 0) } - func test_navigationStack_popToView() async throws { + @MainActor func test_navigationStack_popToView() async throws { let sut = makeSUT() let view = PushStepView.self @@ -127,7 +127,7 @@ final class RouterTests: XCTestCase { } } - func test_navigationStack_popToViewFail() async throws { + @MainActor func test_navigationStack_popToViewFail() async throws { let sut = makeSUT() let route = AnyEnumRoute.fullScreenStep @@ -145,14 +145,14 @@ final class RouterTests: XCTestCase { // MARK: Helpers // -------------------------------------------------------------------- - private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> Router { + @MainActor private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> Router { let router = Router() router.mainView = .pushStep trackForMemoryLeaks(router, file: file, line: line) return router } - private func makeSheetItem(_ item: any RouteType, animated: Bool = true) -> SheetItem { + @MainActor private func makeSheetItem(_ item: any RouteType, animated: Bool = true) -> SheetItem { .init(id: UUID().uuidString, animated: animated, presentationStyle: item.presentationStyle, view: { item.view }) } } diff --git a/Tests/SUICoordinatorTests/SheetCoordinatorTests.swift b/Tests/SUICoordinatorTests/SheetCoordinatorTests.swift index 239b62f..0d1f628 100644 --- a/Tests/SUICoordinatorTests/SheetCoordinatorTests.swift +++ b/Tests/SUICoordinatorTests/SheetCoordinatorTests.swift @@ -27,17 +27,17 @@ import XCTest final class SheetCoordinatorTests: XCTestCase { - func test_presentRoute() async throws { + @MainActor func test_presentRoute() async throws { let sut = makeSUT() let item = makeSheetItem("Custom Item") - await sut.presentSheet(item) + sut.presentSheet(item) XCTAssertFalse(sut.items.isEmpty) XCTAssertEqual(sut.items.last??.view(), item.view()) } - func test_presentRouteTwice() async throws { + @MainActor func test_presentRouteTwice() async throws { let sut = makeSUT() let finalRoute = makeSheetItem("Final Item") @@ -48,7 +48,7 @@ final class SheetCoordinatorTests: XCTestCase { XCTAssertEqual(sut.items.last??.id, finalRoute.id) } - func test_dismiss_lastRoute() async throws { + @MainActor func test_dismiss_lastRoute() async throws { let sut = makeSUT() let item = makeSheetItem("Custom Item") @@ -56,25 +56,25 @@ final class SheetCoordinatorTests: XCTestCase { XCTAssertEqual(sut.items.count, 1) await sut.removeLastSheet(animated: false) - await sut.removeAllNilItems() + sut.removeAllNilItems() XCTAssertEqual(sut.items.count, 0) } - func test_dismiss_route_atPositon() async throws { + @MainActor func test_dismiss_route_atPositon() async throws { let sut = makeSUT() await presentSheet(makeSheetItem("First Item"), with: sut) await presentSheet(makeSheetItem("Second Item"), with: sut) await presentSheet(makeSheetItem("Third Item"), with: sut) - await sut.remove(at: 1) + sut.remove(at: 1) XCTAssertEqual(sut.items.count, 2) XCTAssertEqual(sut.items.last??.view(), "Third Item") } - func test_cleanCoordinator() async throws { + @MainActor func test_cleanCoordinator() async throws { let sut = makeSUT() await presentSheet(makeSheetItem("First Item"), with: sut) @@ -90,7 +90,7 @@ final class SheetCoordinatorTests: XCTestCase { // MARK: Helpers // -------------------------------------------------------------------- - private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> SheetCoordinator { + @MainActor private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> SheetCoordinator { let coordinator = SheetCoordinator() trackForMemoryLeaks(coordinator, file: file, line: line) return coordinator @@ -104,8 +104,8 @@ final class SheetCoordinatorTests: XCTestCase { .init(id: UUID().uuidString, animated: animated, presentationStyle: presentationStyle, view: { item }) } - private func presentSheet( _ item: SheetItem, with sut: SheetCoordinator) async { - await sut.presentSheet(item) - await sut.removeAllNilItems() + @MainActor private func presentSheet( _ item: SheetItem, with sut: SheetCoordinator) async { + sut.presentSheet(item) + sut.removeAllNilItems() } } diff --git a/Tests/SUICoordinatorTests/TabbarCoordinatorTests.swift b/Tests/SUICoordinatorTests/TabbarCoordinatorTests.swift index cb2c130..cc9594d 100644 --- a/Tests/SUICoordinatorTests/TabbarCoordinatorTests.swift +++ b/Tests/SUICoordinatorTests/TabbarCoordinatorTests.swift @@ -31,7 +31,7 @@ final class TabbarCoordinatorTests: XCTestCase { private let animated: Bool = false - func test_setPages() async throws { + @MainActor func test_setPages() async throws { let sut = makeSUT() let pages = [AnyEnumTabbarRoute.tab2] @@ -42,7 +42,7 @@ final class TabbarCoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_changeTab() async throws { + @MainActor func test_changeTab() async throws { let sut = makeSUT(currentPage: .tab1) XCTAssertEqual(sut.currentPage, .tab1) @@ -51,7 +51,7 @@ final class TabbarCoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_get_coordinator_selected_fail() async { + @MainActor func test_get_coordinator_selected_fail() async { let sut = makeSUT(currentPage: .tab1) await sut.start(animated: animated) @@ -66,7 +66,7 @@ final class TabbarCoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_navigateToCoordinator() async throws { + @MainActor func test_navigateToCoordinator() async throws { let sut = makeSUT(currentPage: .tab1) let coordinator = AnyCoordinator() @@ -78,7 +78,7 @@ final class TabbarCoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_popToRoot_in_tab() async throws { + @MainActor func test_popToRoot_in_tab() async throws { let sut = makeSUT(currentPage: .tab1) await sut.start(animated: animated) @@ -93,13 +93,13 @@ final class TabbarCoordinatorTests: XCTestCase { await finishFlow(sut: sut) } - func test_siTabbarCoordinator() async throws { + @MainActor func test_siTabbarCoordinator() async throws { let sut = makeSUT(currentPage: .tab1) XCTAssertTrue(sut.isTabbarCoordinable) await finishFlow(sut: sut) } - func test_finshCoordinator() async throws { + @MainActor func test_finshCoordinator() async throws { let sut = makeSUT() let coordinator1 = OtherCoordinator() let coordinator2 = AnyCoordinator() @@ -111,7 +111,7 @@ final class TabbarCoordinatorTests: XCTestCase { XCTAssertTrue(sut.isEmptyCoordinator) } - func test_force_to_present_coordinator() async throws { + @MainActor func test_force_to_present_coordinator() async throws { let sut = makeSUT(currentPage: .tab1) let coordinator = AnyCoordinator() @@ -133,7 +133,7 @@ final class TabbarCoordinatorTests: XCTestCase { // MARK: Helpers // -------------------------------------------------------------------- - private func makeSUT( + @MainActor private func makeSUT( currentPage: AnyEnumTabbarRoute = AnyEnumTabbarRoute.tab1, file: StaticString = #filePath, line: UInt = #line