Clean Swift iOS Architectural Design with SwiftUI

Photo of Patryk Strzemiecki

Patryk Strzemiecki

Updated Jun 2, 2024 • 6 min read
clean-swift-and-swiftui-architecture

Since the release of the SwiftUI framework in September 2019, most iOS app developers have either waited for the others to figure out an approach to using the new UI library in a clean way or try it on their own.

Here, I would like to share the approach I prepared for the commercial project that I work on. I wanted to carry on with the navigation flow of the app in UIKit, keep the high testability for the business logic with Clean Swift clean architecture, and yet try implementing the views with SwiftUI.

Why not switch to pure SwiftUI?

I saw many ports of Redux, MVVM, and other well-known architectural design patterns for UI architecture in Swift used with SwiftUI. Yet none of them convinced me that it can be used in a project that is quite old, relies heavily on the old components in Objective-C (singletons and managers), and what is the most important - there is still need to push or show the old ViewControllers written in UIKit from the new scenes that we add.

Besides that, I try to keep the project testable with the business logic separated, and the other big reason not to switch to pure SwiftUI is that Clean Swift was already used as the major pattern for all the modern modules.

If you wonder what Clean Swift architectural design pattern is, check my previous article before reading this one.

What are the changes?

The major changes required to use SwiftUI with Clean Swift are:

  • SceneViewController will keep a reference of SceneViewModel that is a source of data, states, and delegates of our View.

    protocol WelcomeSceneViewControllerInput: AnyObject {
        var router: WelcomeSceneRoutingLogic? { get set }
        var viewModel: WelcomeSceneViewModel? { get set }
    }
    
  • SceneViewModel is a new class where we can use Combine to make our SwiftUI's View reactive.
    protocol WelcomeSceneViewDelegate: AnyObject {
        func didSelectButton(_ sender: WelcomeSceneViewModel?)
    }
    
    protocol WelcomeSceneViewModel {
        var delegate: WelcomeSceneViewDelegate? { get set }
        var text: String { get }
        var buttonText: String { get }
    }
    
    final class DefaultWelcomeSceneViewModel: WelcomeSceneViewModel {
        var delegate: WelcomeSceneViewDelegate?
        @Published private(set) var text: String
        @Published private(set) var buttonText: String
        
        init(
            text: String,
            buttonText: String
        ) {
            self.text = text
            self.buttonText = buttonText
        }
    }
    
  • SceneViewController inherits from UIHostingController<SceneView>, not from UIViewController.
    final class WelcomeSceneViewController: UIHostingController {
        var interactor: WelcomeSceneViewControllerOutput?
        var router: WelcomeSceneRoutingLogic?
        var viewModel: WelcomeSceneViewModel?
    }
    
    Check an example of use in my sample project here.
  • SceneConfigurator takes the ViewModel and assigns it to the ViewController.
    The method 'configured(...)' contains a lot of boilerplate code but it should not be a problem as long as you use my code templates from this repo with the test cases provided for the Configurator module.
    protocol WelcomeSceneConfigurator {
        func configured(
            with viewModel: WelcomeSceneViewModel
        ) -> WelcomeSceneViewController
    }
    
    final class DefaultWelcomeSceneConfigurator: WelcomeSceneConfigurator {
        func configured(
            with viewModel: WelcomeSceneViewModel
        ) -> WelcomeSceneViewController {
    var viewModel = viewModel let viewController = WelcomeSceneViewController( rootView: WelcomeSceneView(viewModel: viewModel) ) let interactor = WelcomeSceneInteractor() let presenter = WelcomeScenePresenter() let router = WelcomeSceneRouter() router.source = viewController presenter.viewController = viewController interactor.presenter = presenter viewController.interactor = interactor viewController.router = router viewController.viewModel = viewModel viewModel.delegate = viewController return viewController } }
  • The View of our ViewController does not inherit from UIView anymore but from SwiftUI's View.
    struct WelcomeSceneView: View {
        let viewModel: WelcomeSceneViewModel
        
        var body: some View {
            VStack {
                Text(viewModel.text)
                Divider()
                Button(viewModel.buttonText) {
                    viewModel.delegate?.didSelectButton(viewModel)
                }
            }
        }
    }
    

If the snippets above are not enough, please check the example project in my GitHub account.

Summary of Clean Swift + SwiftUI

What does it bring to the project?

  • Enforces modularity with very high testability, reusable components, and extracts business logic.
  • Can be applied to existing projects of any size and age with target deployment above iOS 14.

Disadvantages of Clean Swift with SwiftUI

  • Boilerplate code and need to use templates.

If you feel lost anywhere in the article, it may be because of the fact I assume you already know Clean Swift architectural design pattern for iOS development. If you don't, or just would like to review the basics, please check my previous blogpost Clean Swift (VIP) iOS Architecture Pattern.

My idea is just one of many and it's hard to tell how the approach to mixing UIKit and SwiftUI will change over the years. Take it and modify it the way you need it for your project.

Photo of Patryk Strzemiecki

More posts by this author

Patryk Strzemiecki

Senior iOS Developer
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business