Last active
February 7, 2022 09:30
-
-
Save dziobaczy/ea0d3b9dee775cd719012ae56c2fb128 to your computer and use it in GitHub Desktop.
This gist shows idea behind adding SwiftUI to the UIKit app in a clean way that allows easy fallback to UIKit if needed. Please save that file with `playground` and open in Xcode so that you can benefit from documentation rendering for better reading experience!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*: | |
# SwiftUI Views in UIKit World | |
(If you see this as a comment instead rendering, open the pane on the right and select `Render Documentation` in `Playground Settings`) | |
This is a simple concept of how you can use SwiftUI for creating your views, at the | |
same time protecting your app from having SwiftUI everywhere. | |
Imagine you use coordinators in your app and inject into them view controller factories. This way you can easily compose and test your app. Now for the new feature you would like to try SwiftUI. What can you do? | |
*/ | |
/*: | |
## SwiftUI Views | |
Let's start simple with a simple view and it's data model. We create it to protect the view from outside world changes. There is no need for the `ProfileView` to know that username also has `phoneNumber` for example. | |
*/ | |
import UIKit | |
import SwiftUI | |
import PlaygroundSupport | |
struct ProfileView: View { | |
let data: ProfileViewData | |
var body: some View { | |
VStack(alignment: .center, spacing: 12) { | |
Text(data.username) | |
Text(data.location) | |
} | |
} | |
} | |
struct ProfileViewData { | |
let username: String | |
let location: String | |
} | |
/*: | |
## Views factory | |
Let's say the `ProfileView` is part of the settings part of the app, we are making factory to centralise the initialisation of those views in case of future `init` changes or their shared dependencies. | |
*/ | |
struct SettingsViewFactory { | |
func makeProfileView(with data: ProfileViewData) -> some View { | |
ProfileView(data: data) | |
} | |
// Other needed views ... | |
} | |
/*: | |
## User Business Model | |
Even tho our view needs just some concrete informations, the user in our app might be much more complex structure, at the same time there is no need to couple the `ProfileView` with the `User` model itself, that's why we created small and dedicated `ProfileViewData`. This will make testing much easier, and help make the view more reusable. | |
You can also see that the `contactDetails` is optional, again thanks to us making the model require providing `String` for location into the `ProfileView` compiler will help us ensure that there is value provided and we do not handle the nil value in the view which as you usually hear should just obey. | |
*/ | |
struct User { | |
let username: String | |
let age: UInt | |
let contactDetails: ContactDetails? | |
} | |
struct ContactDetails { | |
let location: String? | |
let phoneNumber: String? | |
} | |
/*: | |
## View Controllers Factory | |
In here we declare protocol so that the implementation might change for the coordinator, we can switch between SwiftUI and UIKit when needed easily. | |
*/ | |
protocol SettingsViewControllerFactory { | |
func makeProfileViewController(with profileViewData: ProfileViewData) -> UIViewController | |
// Other needed controllers ... | |
} | |
final class SwiftUISettingsViewControllerFactory: SettingsViewControllerFactory { | |
private let viewFactory: SettingsViewFactory | |
init(viewFactory: SettingsViewFactory = SettingsViewFactory()) { | |
self.viewFactory = viewFactory | |
} | |
func makeProfileViewController(with profileViewData: ProfileViewData) -> UIViewController { | |
UIHostingController(rootView: viewFactory.makeProfileView(with: profileViewData)) | |
} | |
} | |
/*: | |
## Coordinator and mapping | |
Take a look how we just need to extend the `User` model to match the `makeProfileViewController` view needs. This way we centralise the mapping and ensure future updates to either view or business model will be easy to make. | |
*/ | |
final class SettingsCoordinator { | |
private(set) var navigationController: UINavigationController | |
private let viewControllersFactory: SettingsViewControllerFactory | |
private let user: User | |
init(navigationController: UINavigationController, | |
viewControllersFactory: SettingsViewControllerFactory, | |
user: User | |
) { | |
self.navigationController = navigationController | |
self.viewControllersFactory = viewControllersFactory | |
self.user = user | |
} | |
func showProfile() { | |
navigationController.pushViewController(viewControllersFactory.makeProfileViewController(with: user.asProfileViewData), animated: true) | |
} | |
} | |
extension User { | |
private var defaultLocation: String { "Somewhere in the universe" } | |
var asProfileViewData: ProfileViewData { | |
ProfileViewData(username: username, location: contactDetails?.location ?? defaultLocation) | |
} | |
} | |
let coordinator = SettingsCoordinator(navigationController: UINavigationController(), | |
viewControllersFactory: SwiftUISettingsViewControllerFactory(), | |
user: User(username: "Bogdan", age: 23, contactDetails: .none)) | |
coordinator.showProfile() | |
// Normally because of the `window` `rootViewController` which automatically sizes to the screen dimensions you wouldn't need it. Here I just add for fully sizing inside the playground | |
coordinator.navigationController.visibleViewController?.view.frame.size = CGSize(width: 375, height: 667) | |
PlaygroundPage.current.liveView = coordinator.navigationController.visibleViewController | |
//: If you want to learn more checkout [this series](https://www.youtube.com/watch?v=_tjDTevsQ-I) by essential developer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment