SwiftUI in existing MVVM+C/UIKit project
Before diving into the world of SwiftUI, we should first understand what exactly that is, and why would we even attempt such a task, or on the opposite — avoid it.
What is SwiftUI?
SwiftUI is a new way to build user interfaces for apps on Apple platforms. It allows developers to define the UI using Swift code. The key difference between UIKit and SwiftUI is that the latter defines the user interface declaratively, not imperatively.
Why would you want to use SwiftUI?
- You like to follow trends and want to create the newest functionality available on iOS - Dynamic islands / Live activities / Widgets)
- Less code and easier reusability options
- Subjectively easier to understand and learn - no more Auto-Layouts, Conflicts, etc.
- Real-time UI testing with help of Previews
Why would you would like to avoid SwiftUI?
- Quite strict on the minimal deployment target
- My opinion is that if you can’t increase it to 15, you will stumble with a lot of unpleasant issues to resolve (most common one might be list separators)
- Still new and some of the possibilities are still not ported to SwiftUI
- Highly depends on the first point though, with each new iOS version it gets improved
- Less information on some topics, because the technology is new compared to UIKit, and the resources are limited
How would you incorporate SwiftUI in an existing project?
Alright, you’ve considered the pros and cons, and you’re eager to try this new technology out. But how, you might ask? Well, here are some pointers, and guidelines I stuck to, to successfully rewrite a few screens in the currently fully functional UIKit project.
Setup
-
Increasing the minimal iOS deployment version to 15 will help you avoid headaches, but if it's not possible — SwiftUI is available from iOS 13.
-
If you’re like me and like to have clear color and typography separation another thing you might need to do is create SwiftUI analogs, since in SwiftUI:
- UIColor → Color
- UIFont → Font
- And lastly, remember that everything UI-related is now a struct of the
View
protocol. I’ll describe how to incorporate the flows ofViewControllers
in the next section.
MVVM
You’ve written your first View
, it looks the same as your ViewController
, but wait a minute… What about the ViewModel
and all its properties? How can you set them in the newly created View
?
This is the place where I would like to tell you more about Property Wrappers . As well as which ones you should use to achieve the wanted result.
@ObservedObject
- marks a class, whose properties can trigger UI reload@Published
- marks a property inObservableObject
that should trigger UI reload@State
- marks a property insideView
that shouldn’t be re-created (to help save state), and additionally trigger UI reload on change. Note: Ideally these should be kept private outside view’s scope@Binding
- marks a property that comes from outside scope, but the value should be synced to the owner
With this information, you’re ready to dive in and refactor your ViewModel
. Let me give you some examples to speed up the process.
ViewModel
final class ExampleVM: ObservableObject { @Published var publishedLoading = false ... }
We mark our viewModel
with ObservableObject
protocol so that we can mark it with the aforementioned property wrapper @ObservedObject
and trigger UI updates from it. Additionally, we’ve added the @Published
property which will trigger UI updates on value change.
View
struct ExampleView: View { @ObservedObject var viewModel: ExampleVM ... }
As mentioned above we create our property @ObservedObject
to tell the compiler that this object can change properties and trigger UI updates.
Binding / State
struct RotatingLoadingView: View { @Binding var isLoading: Bool @State private var isRotating = 0.0 ... } struct ExampleView: View { @ObservedObject var viewModel: ExampleVM ... var body: some View { VStack(spacing: 0) { ... RotatingLoadingView(isLoading: $viewModel.publishedLoading) } } }
We will have a view that displays loading while rotating the image inside it. For this purpose, we’ve defined one @Binding
property which can be controlled from the outside scope in our example in viewModel
. Additionally, we have the @State
property which controls rotation angle and won’t be accessed outside the scope. When passing binding property it is important to remember that the $
symbol should be used cause we want to pass reference rather than value.
RxSwift
I use RxSwift
in my daily development so wanted to mention how to handle it as well, but in case you aren’t, more power to you, you can skip this section and move on.
isLoading .subscribe(onNext: { [weak self] in self?.publishedLoading = $0 }) .disposed(by: bag)
To handle RxSwift
→ SwiftUI
I defined additional @Published
property and on isLoading
changes also set that property.
This could be handled in extensions to improve Quality of Life and make it easier, but here I wanted to demonstrate a basic appliance, that can be improved based on your needs.
Animations
This is all cool and all, but it never looks pretty if we’ve wooden clunky appearances/updates, so what about animations?
Important to note that there is no more UIView.animate
when we talking SwiftUI
instead we’re supposed to use the withAnimation
block and change animatable properties.
isLoading .subscribe(onNext: { [weak self] isLoading in withAnimation(.easeInOut(duration: isLoading ? 0.2 : 0)) { self?.publishedLoading = isLoading } }) .disposed(by: bag) RotatingLoadingView(isLoading: $viewModel.publishedLoading) .opacity(viewModel.publishedLoading ? 0.4 : 1)
In the above example, we update the isLoading
property with animation. Additionally, based on the properties value in View
, we change the opacity’s value. As a result, we will have our loading view animate its alpha.
Presentation
We’ve created our view, and updated our viewModel
. But what about showing it to the user? We can’t push a View
into the navigation stack, can we? Well no, but let me show you an example that wraps our view into UIViewController
to present it.
private func navigateToExample() { let vm = ExampleVM() let view = ExampleView(viewModel: vm) let vc = UIHostingController(rootView: view) rootNC.pushViewController(vc, animated: true) }
Here we create our viewModel
and View
, and finally, wrap them in UIHostingController
. That allows us to push/present it the same way we are all used to.
UIKit element usage in SwiftUI
As I’ve mentioned before the more you can increase the minimal iOS version, the fewer of these issues you’ll have to deal with, but if there ever is an UIKit
element you wish to use, but can’t find or analogue is inaccessible you can use UIViewRepresentable
protocol to solve your issue.
struct TextView: UIViewRepresentable { @Binding var attributedText: NSAttributedString func makeUIView(context: Context) -> some UIView { let textView = UITextView() /* Customise to your needs */ return textView } func updateUIView(_ uiView: UIViewType, context: Context) { (uiView as? UITextView)?.attributedText = attributedText } }
Protocol has two methods you need to implement:
makeUIView(context: Context) -> some UIView
- is used to create and customize your element according to your needsupdateUIView(_ uiView: UIViewType, context: Context)
- is used to update your view when UI reload is triggered. In this example, we want to set text from@Binding
property when its value changes
The end?
Well if you’re still reading thank you and hopefully, some of the information above was useful for you in your SwiftUI journey. Obviously, this article doesn’t cover all and each of the issues you can encounter when trying to convert your screens to SwiftUI. Its goal is to demonstrate that it can be done even on the existing projects so you don’t have the mindset “ah maybe in a new project I’ll try”. Additionally, my challenges with extracted examples are displayed, to not bore you with theory but give concrete examples.
To end this article I would just like to encourage you to try because my first impressions were, very skeptical - why would I even do this? I can easily do this with old methods not stepping out of my comfort zone... But towards the end I really enjoyed it, saving lots of code and giving me way easier reusability options. And now I want to push for it as much as possible because I see a lot of potential in it.
Let’s build products together!
Contact us