• 13 min reading time

3 Aug 2020

Refactor UIKit Views to SwiftUI Using the Adapter Pattern

Reusing all of your app's business and presentation logic


Image by Sharon Kehl Califano


With SwiftUI receiving a lot of support from Apple and Swift community, more companies are starting to use it in production. However, older projects will have a lot of UIKit code and will want to refactor that to SwiftUI. Or they might want to adopt SwiftUI for newer parts of UI and reuse existing businesses logic. Some might even want to support iOS versions before iOS 13 and write SwiftUI counterpart views to their UIKit views so they can drop UIKit when switching to iOS 13 minimum App version.


Adopting SwiftUI is all great, but how do we refactor old UIKit code to SwiftUI without breaking any business logic?
In this article, I will attempt to answer that question.


Sample Project

I created a sample project in the form of a simple Counter App with two architectural implementations, using MVC(simple) and Clean Swift(scalable) design patterns. I would recommend downloading the project from here to follow along.


Firstly if you are doing any business logic, network request, mapping or any other computation inside your UIViewController class, you will have to decouple that into a separate class so that your UIViewController stays plain. The view controller should only display what the presentation layer tells it to display. And notify the business logic layer what actions the user has taken. You can achieve that decoupling with both MVC and Clean Swift example implementations.


UIKit implementation

In this article, I will walk you through Clean Swift implementation, so lets, first of all, see how Clean Swift works with a UIViewController module.



You can see the data flow in a circular fashion View-Interactor-Presenter; sometimes, you will see this called a VIP cycle(which is how I will refer to it below). Clean Swift avoids concrete referencing of VIP classes, and it creates references to classes via interfaces(protocols), so concrete implementations can be swapped for testing or in our case reuse purposes. Referencing via interfaces conforms to Liskov Substitution Principle from SOLID, so it is a good practice when writing clean code.


In practice that would look something like: user taps a button in the UIKit class, which notifies the interactor that button has been tapped, interactor performs any computation needed(stores the data, makes a network request, maps the data etc..) and passes the data to presenter which formats that data for display and gives it to the view controller which displays the outcome.


So let's see an example of this in our counter App;


import UIKit

enum Counter {
    
    enum IncreasedCount {
        struct Response {
            let count: Int
        }
        struct ViewModel {
            let countString: String
        }
    }
}

/// View
protocol CounterDisplayLogic: class {
    func showCurrentCount(viewModel: Counter.IncreasedCount.ViewModel)
}

class CounterViewController: UIViewController, CounterDisplayLogic {

    @IBOutlet var titleLabel: UILabel!
    
    private var interactor: CounterInteractable?
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        let viewController = self
        let interactor = CounterInteractor()
        let presenter = CounterPresenter()
        viewController.interactor = interactor
        interactor.presenter = presenter
        presenter.viewController = viewController
    }
    
    @IBAction func decreaseTapped(_ sender: CustomButtonUIKit) {
        interactor?.decreaseTapped()
    }
    
    @IBAction func increaseTapped(_ sender: CustomButtonUIKit) {
        interactor?.increaseTapped()
    }
    
    func showCurrentCount(viewModel: Counter.IncreasedCount.ViewModel) {
        titleLabel.text = viewModel.countString
    }
}

/// Interactor
protocol CounterInteractable {
    func increaseTapped()
    func decreaseTapped()
}

class CounterInteractor: CounterInteractable {
    var presenter: CounterPresentable?
    
    private var count: Int = 0

    func increaseTapped() {
        count += 1
        presenter?.presentCurrentCount(response: Counter.IncreasedCount.Response(count: count))
    }
    
    func decreaseTapped() {
        count -= 1
        presenter?.presentCurrentCount(response: Counter.IncreasedCount.Response(count: count))
    }
}

/// Presenter
protocol CounterPresentable {
    func presentCurrentCount(response: Counter.IncreasedCount.Response)
}

class CounterPresenter: CounterPresentable {
    
    weak var viewController: CounterDisplayLogic?
    
    func presentCurrentCount(response: Counter.IncreasedCount.Response) {
        let countString = "Current count is \(response.count)"
        viewController?.showCurrentCount(viewModel: Counter.IncreasedCount.ViewModel(countString: countString))
    }
}

/// Custom button class
class CustomButtonUIKit: UIButton {
    override func didMoveToSuperview() {
        backgroundColor = .systemBlue
        titleLabel?.textColor = .systemBackground
        layer.cornerRadius = frame.height / 2
    }
}

When you tap "+" button, the view notifies interactor that the user tapped increment via increaseTapped() method. Interactor then increases the value of the local count variable and gives that new count value to the presenter. The presenter then formats the string saying Current count is: (count) and passes that string to the view controller to display. If you look at the project structure downloaded from the link above, you will see all of the VIP components sit in separate files.



Inside Counter -> Clean Swift Implementation folder you have two folders, one of which is a folder called Shared Code which holds all of our shared logic and UIKit folder with our CounterViewController implementation. Note also that there is the CounterViewControllerWrapper file inside the UIKit folder which acts as a wrapper of our ViewController class, as I have written root HomeView in SwiftUI. To navigate from SwiftUI view to UIKit view, I needed to wrap that inside a UIViewControllerRepresentable struct.


SwiftUI implementation

A great thing about using this pattern is that now we can reuse our business logic CounterInteractor and presentation logic CounterPresenter in SwiftUI implementation of this counter. For this, we will need an additional Adapter layer which will conform to the same CounterDisplayLogic protocol as previously the CounterViewController class (see how we are "substituting" concrete class implementation). Let's see a little graphic of that new implementation here.



Our new CounterViewAdapter class will also conform to ObservableObject (getting into the good stuff here), so our SwiftUI view will be able to observe the changes in it.


Now the only thing we have to do is create a @Published var currentCountDisplayString inside our CounterViewAdapter class and update it whenever showCurrentCount() method fires. Our SwiftUI view will then subscribe to currentCountDisplayString and automatically update to the latest value. Let's see our new implementation:

import SwiftUI

/// Adapter
class CounterViewAdapter: ObservableObject {
    
    @Published var currentCountDisplayString: String = "Tap to start counting"
    
    var interactor: CounterInteractor?
    
    init() {
        let viewController = self
        let interactor = CounterInteractor()
        let presenter = CounterPresenter()
        viewController.interactor = interactor
        interactor.presenter = presenter
        presenter.viewController = viewController
    }
}

extension CounterViewAdapter: CounterDisplayLogic {
    
    func showCurrentCount(viewModel: Counter.IncreasedCount.ViewModel) {
        currentCountDisplayString = viewModel.countString
    }
}

/// SwiftUI view
struct CounterView: View {
    
    @ObservedObject var adapter = CounterViewAdapter()
    
    var body: some View {
        
        ZStack {
            
            Color(.offWhite)
            
            VStack {
                Spacer()
                Text(adapter.currentCountDisplayString)
                    .font(.title)
                Spacer()
                HStack {
                    Button(action: {
                        self.adapter.interactor?.decreaseTapped()
                    }) {
                        Text("-")
                            .foregroundColor(Color(.darkGray))
                            .font(.largeTitle)
                    }
                    .buttonStyle(NeumorphicButtonStyle(widthAndHeight: 100))
                    Spacer()
                        .frame(width: 50)
                    Button(action: {
                        self.adapter.interactor?.increaseTapped()
                    }) {
                        Text("+")
                            .foregroundColor(Color(.darkGray))
                            .font(.largeTitle)
                    }
                    .buttonStyle(NeumorphicButtonStyle(widthAndHeight: 100))
                }
                Spacer()
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

You can now notice that the CounterView struct now needs an @ObservedObject property to reference our new CounterViewAdapter. Now we can use adapter.currentCountDisplayString within SwiftUI’s Text (line 40) and two buttons with actions calling descreaseTapped()(line 45) and increaseTapped()(line 55) methods via adapter.interactor. The rest is just your good old (or very young 👶) SwiftUI code.


If you expand the folder in your project under Counter -> Clean Swift Implementation -> SwiftUI, you will find these new files in there. Notice that business logic(CounterInteractor) and presentation logic(CounterPresenter) are in the same Counter -> Clean Swift Implementation -> Shared folder completely reused from our early UIKit implementation.



If you run the App, you will see that you can navigate from landing screen to two similar screens. The first screen is UIKit implementation of the counter App, and the second is SwiftUI implementation of the Counter App with shared business and presentation logic.


Now when any business or presentation logic is changed, it will reflect in both view implementations. And if later down the line you decide you don’t need a UIKit implementation anymore you can remove it, and the App should still behave as before with SwiftUI implementation.


And that’s it, we reused most of our code inside the Counter module and written both UIKit and SwiftUI views on top of it!


One more note: if you downloaded the project you might have noticed that there are two targets and two implementations of Adapter pattern, the first one is above described Clean Swift and the second a simple MVC implementation. You can see both design pattern implementations to show how easy it is to reuse business logic code also with a basic architectural pattern as MVC if done correctly. If you run each of the targets, the App will look the same; however, under-the-hood implementation is either MVC or Clean Swift design pattern.


I hope you enjoyed this article and learned something new on the way.


If you have any thoughts on this article, I would love to hear them!
You can always get in touch with me via Twitter or LinkedIn 🙂


Cheers,


Tim