├── .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 | [](https://opensource.org/licenses/MIT)
4 | 
5 | 
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 |
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 |
--------------------------------------------------------------------------------