SwiftUI in existing MVVM+C/UIKit project

Profile photo of Sandis Millers
Sandis Millers
Feb 08, 2023
10 min
Categories: Development
Light background with text - SwiftUI in existing MVVM+C/UIKit project
Light background with text - 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

  1. 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.

  2. 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
  1. And lastly, remember that everything UI-related is now a struct of the View protocol. I’ll describe how to incorporate the flows of ViewControllers 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 in ObservableObject that should trigger UI reload
  • @State - marks a property inside View 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 RxSwiftSwiftUI 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 needs
  • updateUIView(_ 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.

Share with friends

Let’s build products together!

Contact us