Skip to content

Instantly share code, notes, and snippets.

@dziobaczy
Last active February 7, 2022 09:30
Show Gist options
  • Save dziobaczy/ea0d3b9dee775cd719012ae56c2fb128 to your computer and use it in GitHub Desktop.
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!
/*:
# 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