├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── SwiftUINavigatorTests │ └── SwiftUINavigatorTests.swift ├── Sources └── SwiftUINavigator │ ├── UIViewController+ │ ├── UIViewController+indentifier.swift │ ├── UIViewController+functional.swift │ └── UIViewController+.swift │ ├── Wrappable.swift │ ├── WrapperViewController.swift │ └── Navigator.swift ├── Package.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/SwiftUINavigatorTests/SwiftUINavigatorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftUINavigator 3 | 4 | final class SwiftUINavigatorTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/UIViewController+/UIViewController+indentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+indentifier.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/15/23. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public extension UIViewController { 12 | 13 | private struct AssociatedKeys { 14 | static var identifier = "Identifier" 15 | } 16 | 17 | var identifier: String? { 18 | get { 19 | objc_getAssociatedObject(self, &AssociatedKeys.identifier) as? String 20 | } 21 | set { 22 | objc_setAssociatedObject(self, &AssociatedKeys.identifier, newValue, .OBJC_ASSOCIATION_RETAIN) 23 | } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/UIViewController+/UIViewController+functional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+functional.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/15/23. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public extension UIViewController { 12 | 13 | func title(_ title: String?) -> Self { 14 | self.title = title 15 | return self 16 | } 17 | 18 | func hidesBottomBarWhenPushed(_ state: Bool) -> Self { 19 | self.hidesBottomBarWhenPushed = state 20 | return self 21 | } 22 | 23 | func backgroundColor(_ color: UIColor?) -> Self { 24 | self.view.backgroundColor = color 25 | return self 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/UIViewController+/UIViewController+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/12/23. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public extension UIViewController { 12 | 13 | func addChildAndSubView(_ controller: UIViewController) { 14 | self.addChild(controller) 15 | controller.view.frame = self.view.frame 16 | self.view.addSubview(controller.view) 17 | controller.didMove(toParent: self) 18 | } 19 | 20 | func removeFromParentAndSuperView() { 21 | self.willMove(toParent: nil) 22 | self.view.removeFromSuperview() 23 | self.removeFromParent() 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/Wrappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Wrappable.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/12/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | public protocol Wrappable: View { 12 | 13 | var navigator: Navigator? { get set } 14 | } 15 | 16 | public extension Wrappable { 17 | 18 | var identifier: String { "\(Self.self)" } 19 | 20 | func asViewController( 21 | backgroundColor: UIColor? = nil, 22 | hidesBottomBarWhenPushed: Bool = true 23 | ) -> WrapperViewController { 24 | WrapperViewController( 25 | content: self, 26 | indentifier: identifier, 27 | backgroundColor: backgroundColor 28 | ) 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUINavigator", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, making them visible to other packages. 13 | .library( 14 | name: "SwiftUINavigator", 15 | targets: ["SwiftUINavigator"]), 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package, defining a module or a test suite. 19 | // Targets can depend on other targets in this package and products from dependencies. 20 | .target( 21 | name: "SwiftUINavigator"), 22 | .testTarget( 23 | name: "SwiftUINavigatorTests", 24 | dependencies: ["SwiftUINavigator"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 insub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/WrapperViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapperViewController.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/12/23. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | public class WrapperViewController: UIViewController { 12 | 13 | var hController = UIHostingController(rootView: nil) 14 | var content: Content 15 | let navigator: Navigator 16 | 17 | init( 18 | content: Content, 19 | indentifier: String, 20 | backgroundColor: UIColor? = nil 21 | ) { 22 | self.navigator = Navigator() 23 | self.content = content 24 | self.content.navigator = self.navigator 25 | super.init(nibName: nil, bundle: nil) 26 | 27 | self.identifier = indentifier 28 | self.view.backgroundColor = backgroundColor ?? .systemBackground 29 | 30 | setHController() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | public override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | self.navigator.set(self) 40 | } 41 | } 42 | 43 | extension WrapperViewController { 44 | 45 | func setHController() { 46 | hController = .init(rootView: content) 47 | hController.view.backgroundColor = self.view.backgroundColor 48 | addChildAndSubView(hController) 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧭 SwiftUINavigator 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | ![Static Badge](https://img.shields.io/badge/iOS-v13-blue) 5 | ![Static Badge](https://img.shields.io/badge/Swift-5.4-orange) 6 | 7 | > Use SwiftUI View with UIKit Project easily. 8 | 9 | ## 🤔 Why do you need SwiftUINavigator? 10 | > With UIHostingController, I faced bugs that were hard to predict and could not be resolved. That's why I made SwiftUINavigator to solve those problems. 11 | 12 | ## ⚒️ How it works 13 | 스크린샷 2023-10-15 오후 2 20 50 14 | 15 | 16 | ## ✔️ Simple Examples 17 | ```swift 18 | let viewController = HomeView() // View 19 | .asViewController() // WrapperViewController 20 | .title("HomeView") // UIViewController 21 | .backgroundColor(.gray) // UIViewController 22 | .hidesBottomBarWhenPushed(true) // UIViewController 23 | ``` 24 | ```swift 25 | weak var navigator: Navigator? 26 | let controller = DestinationView().asViewController() 27 | navigator.push(controller) 28 | ``` 29 | ```swift 30 | weak var navigator: Navigator? 31 | let controller = DestinationView().asViewController() 32 | navigator?.present(controller, .popover) 33 | ``` 34 | ```swift 35 | weak var navigator: Navigator? 36 | navigator.pop(to: "HomeView") 37 | ``` 38 | ```swift 39 | weak var navigator: Navigator? 40 | navigator.pop() 41 | ``` 42 | ```swift 43 | weak var navigator: Navigator? 44 | navigator.dismiss() 45 | ``` 46 | 47 | ## ✔️ Project Example 48 | ### UIKit side 49 | ```swift 50 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 51 | 52 | guard let scene = (scene as? UIWindowScene) else { return } 53 | window = UIWindow(frame: scene.coordinateSpace.bounds) 54 | window?.windowScene = scene 55 | 56 | let controller = RootView().asViewController() 57 | let navigationController = UINavigationController(rootViewController: controller) 58 | 59 | window?.rootViewController = navigationController 60 | window?.makeKeyAndVisible() 61 | } 62 | ``` 63 | 64 | ### SwiftUI side 65 | ```swift 66 | struct RootView: Wrappable { 67 | 68 | weak var navigator: Navigator? 69 | 70 | var body: some View { 71 | Button("Button") { 72 | let controller = DestinationView().asViewController() 73 | navigator?.push(controller) 74 | } 75 | } 76 | } 77 | 78 | struct DestinationView: Wrappable { 79 | 80 | weak var navigator: Navigator? 81 | 82 | var body: some View { 83 | Button("Button") { 84 | navigator?.pop() 85 | } 86 | } 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /Sources/SwiftUINavigator/Navigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigator.swift 3 | // 4 | // 5 | // Created by 김인섭 on 10/12/23. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public class Navigator { 12 | 13 | public weak var viewController: UIViewController? 14 | 15 | private var navigationController: UINavigationController? { 16 | viewController?.navigationController 17 | } 18 | 19 | public func set(_ viewController: UIViewController?) { 20 | self.viewController = viewController 21 | } 22 | } 23 | 24 | // Presentation 25 | public extension Navigator { 26 | 27 | func push(_ destination: UIViewController, animated: Bool = true) { 28 | self.viewController?.navigationController?.pushViewController(destination, animated: animated) 29 | } 30 | 31 | func present(_ destination: UIViewController, style: UIModalPresentationStyle = .fullScreen, animated: Bool = true) { 32 | destination.modalPresentationStyle = style 33 | navigationController?.topViewController?.present(destination, animated: animated) 34 | } 35 | } 36 | 37 | // Dismiss, Dismiss 38 | public extension Navigator { 39 | 40 | func popToRootViewController(_ animated: Bool = true) { 41 | navigationController?.popToRootViewController(animated: animated) 42 | } 43 | 44 | func pop(animated: Bool = true) { 45 | navigationController?.popViewController(animated: animated) 46 | } 47 | 48 | func pop(to controller: AnyClass, animated: Bool = true) { 49 | 50 | let target = self.navigationController? 51 | .viewControllers 52 | .filter { $0.isMember(of: controller) } 53 | .first 54 | 55 | guard let target = target else { return } 56 | popToViewController(target, animated: animated) 57 | } 58 | 59 | func pop(to identifier: String, animated: Bool = true) { 60 | let target = self.navigationController? 61 | .viewControllers 62 | .filter { $0.identifier == identifier } 63 | .first 64 | 65 | guard let target = target else { return } 66 | popToViewController(target, animated: animated) 67 | } 68 | 69 | func pop(toOneOf controllers: AnyClass..., animated: Bool = true) { 70 | 71 | guard let viewControllers = navigationController?.viewControllers 72 | else { return } 73 | 74 | for controller in viewControllers { 75 | let shouldPop = controllers.contains(where: { controller.isMember(of: $0) }) 76 | guard shouldPop else { continue } 77 | popToViewController(controller, animated: animated) 78 | } 79 | } 80 | 81 | func pop(toOneOf identifiers: String..., animated: Bool = true) { 82 | 83 | guard let viewControllers = self 84 | .viewController? 85 | .navigationController? 86 | .viewControllers 87 | else { return } 88 | 89 | let target = viewControllers 90 | .filter { identifiers.contains($0.identifier ?? "") } 91 | .first 92 | 93 | guard let target = target else { return } 94 | popToViewController(target, animated: animated) 95 | } 96 | 97 | func popToViewController(_ controller: UIViewController, animated: Bool = true) { 98 | navigationController?.popToViewController(controller, animated: animated) 99 | } 100 | 101 | 102 | func dismiss(animated: Bool = true) { 103 | viewController?.dismiss(animated: animated) 104 | } 105 | 106 | func removeFromParentAndSuperView() { 107 | viewController? 108 | .navigationController? 109 | .topViewController? 110 | .removeFromParentAndSuperView() 111 | } 112 | } 113 | #endif 114 | --------------------------------------------------------------------------------