Hallo vrienden en vijanden, Leo hier. Today we will explore something important while migrating to SwiftUI with a previous UIKit code base, which is how to send data from UIKit to SwiftUI.

The tale is old as time. A new shiny framework is launched by Apple and we all get excited about the new opportunities and the freshness of everything.

For the ones that are fortunate enough to work with only the latest technologies, this is pretty easy, because we can start with already everything that you need. You can start with a SwiftUI app with only the latest iOS version supported and that’s amazing. The problem is that is as amazing as unlikely to happen to most of us that work for some established app.

The majority of us will have to support older iOS versions, and we have to wait years to enjoy new technologies that Apple brings every year in WWDC. Generally, the rule of thumb used for older iOS versions is the current version minus two, for example, if the current iOS is 16 we support iOS14 at a minimum.

The advantages of using the most recent iOS are vast. In the new iOS 16, you can have new accordion view initializers to make your life easier or even use the new map kit configurations for a stunning view of the city! I’m sure you know that supporting the latest iOS is a breeze and it is not something that we all can enjoy.

No more talking let’s code! But first…

 

Painting of the Day

The featured painting is called Anunciation, an 1443 art piece by Filippo Lippi.

Giovanni Lippi, also known as Fra Filippo Lippi, was a prominent artist and monk of the 15th century. He was born in Florence in 1406 to Tommaso, a butcher, and his wife. Unfortunately, his parents passed away during his childhood, and as a result, he was sent to live with his aunt Mona Lapaccia. Despite her efforts, she was unable to provide for him financially, and thus, he was placed in the neighboring Carmelite convent at the age of eight.

This marked the beginning of his education and spiritual journey within the Carmelite Order. In 1420, he was formally admitted into the community of Carmelite friars at the Priory of Our Lady of Mount Carmel in Florence, taking his religious vows the following year at the age of sixteen. He was ordained as a priest around 1425 and remained a resident of that priory until 1432.

I chose this painting because the angels are delivering messages to Mary, like in this article that UIKit will transfer data to SwiftUI.

 

The Problem – Get Data From UIKit

You want to update SwiftUI views and the data is coming from UIKit.

In this article, we will check five ways to transfer data from UIKit to SwiftUI. In four of them, the data can update itself and you don’t need to recreate the view, only one that is not possible and you will have to recreate the view yourself.

The five ways to pass data to SwiftUI from UIKit that I will show today are:

  1. View Initializer
  2. Observable Objects
  3. User Defaults
  4. Environment Object
  5. Notification Center

 

Searching for this article I’ve learned a lot about how SwiftUI is made for use with data that update itself. SwiftUI has several tools to get an update that is outside of its context.

Let’s start with the simple one, get information in SwiftUI from UIKit using the View initializer. Every example below will be a full running example, so you can copy and paste it into a brand new UIKit project and that will work fine.

The design of this view is irrelevant for this article but should look like this: 

receive data in SwiftUI from UIKit tutorial

1 – Using SwiftUI’s View Initializer to Dispatch Data from UIKit to SwiftUI

The most simple example that we can have is when you instantiate a new SwiftUI view in your old UIKit code, you just pass the data using the SwiftUI View initializer.

Check the example below:

import UIKit
import SwiftUI

struct User {
    var id: String = UUID().uuidString
}

class ViewController: UIViewController {
    private var usersViewModel = UserViewModel()
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
                configureUserView()
        
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak usersViewModel] _ in
            usersViewModel?.generateUser()
        }) // this timer will simulate an update in the usersViewModel, this could be anything async such as Network calls or heavy background tasks that take long time.
    }
    
        private func configureUserView() {
            usersViewModel.generateUser()
            let userViewController = UIHostingController(rootView: UserView(users: usersViewModel.users)) //the problem is that we always have to update the view manually        
            addChild(userViewController)
            userViewController.view.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(userViewController.view)
            userViewController.didMove(toParent: self)
    
            NSLayoutConstraint.activate([
                userViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                userViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])
        }
}

class UserViewModel {
    private(set) var users: [User] = []

    @discardableResult
    func generateUser() -> User {
        let newUser = User()
        users.append(newUser)
        return newUser
    }
}

struct UserView: View {

    let users: [User]

    var body: some View {
        VStack {
            Text("User name: \(users.first?.id ?? "")")
                .minimumScaleFactor(0.5)
                .lineLimit(2)
            Text("User Count: \(users.count)")
        }
    }
}

As you can see in the example above, you just need to wrap the SwiftUI View in an UIHostingController and pass the data as parameters to the View initializer. I’m using the timer to

The drawback with that example is that every time you want the data or information in the SwiftUI View update, you will need to re-instantiate the whole SwiftUI View.

In the next examples, this doesn’t happen because we keep a reference to the object outside of the SwiftUI and we can update from outside reflecting inside the SwiftUI View.

The next example is learning how to use Observable Objects to update a SwiftUI view with live data from UIKit.

 

2 – Using Observable Objects to Send Data from UIKit to SwiftUI

Now we start to use one of the true powers of SwiftUI, updating data seamlessly. Using Observable Objects you can not just insert data from UIKit but also live update data in SwiftUI that comes from UIKit.

Observe the full example below:

import UIKit
import SwiftUI

struct User {
    var id: String = UUID().uuidString
}

class ViewController: UIViewController {
    private var usersViewModel = UserViewModel()
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUserViewToSendDataByInitializer()
        
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak usersViewModel] _ in                   
            usersViewModel?.generateUser()
        })
    }

    private func configureUserViewToSendDataByInitializer() {
        let userViewController = UIHostingController(rootView: UserView(userViewModel: usersViewModel))
        addChild(userViewController)
        userViewController.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(userViewController.view)
        userViewController.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            userViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            userViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            userViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
    }
}

class UserViewModel: ObservableObject { // add conformance to ObservableObject
    @Published private(set) var users: [User] = [] // add published to this var

    @discardableResult
    func generateUser() -> User {
        let newUser = User()
        print(newUser)
        users.append(newUser)
        return newUser
    }
}

struct UserView: View {
    @ObservedObject var userViewModel: UserViewModel // annotate with ObservedObject

    var body: some View {
        VStack {
            Text("User name: \(userViewModel.users.first?.id ?? "")")
                .minimumScaleFactor(0.5)
                .lineLimit(2)
            Text("User Count: \(userViewModel.users.count)")
        }
    }
}

To this example works properly, we had to add three things.

  1. In the SwiftUI View, we updated the userViewModel variable to be annotated with @ObservedObject
  2. Add the protocol ObservableObject to the ViewModel.
  3. Add annotation @Published to the user’s var inside the view model.

 

With just those three changes, now the SwiftUI View updates itself! Cool, right? So simple changes have a great effect on the final product features. I would recommend first trying to use this way before going with other alternatives.

 

3 – Using User Defaults to Send Data from UIKit to SwiftUI

You can use other tools in your toolbox to send data to SwiftUI Views. You don’t even need to use initializers if you want that a SwiftUI View reacts to changes in your app because it can listen to the User Defaults.

Check the example below:

import UIKit
import SwiftUI

struct User {
    var id: String = UUID().uuidString
}

class ViewController: UIViewController {
    private var usersViewModel = UserViewModel()
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureUserViewToUpdateWithUserDefaults()
        
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak usersViewModel] _ in                     
            usersViewModel?.generateUser()
        })
    }
    
    private func configureUserViewToUpdateWithUserDefaults() {
        let userViewController = UIHostingController(rootView: UserView())
        addChild(userViewController)
        userViewController.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(userViewController.view)
        userViewController.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            userViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            userViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            userViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
        
    }
}

// using User Defaults to update values in the SwiftUI view
class UserViewModel: ObservableObject {
    private(set) var users: [User] = []

    @discardableResult
    func generateUser() -> User {
        let newUser = User()
        print(newUser)
        users.append(newUser)

        UserDefaults.standard.set(newUser.id, forKey: "userKeyID")

        return newUser
    }
}

struct UserView: View {
    @AppStorage("userKeyID") var user = ""

    var body: some View {
        VStack {
            Text("User name: \(user)")
                .minimumScaleFactor(0.5)
                .lineLimit(2)
        }
    }
}

The drawbacks of using this are the same as every approach using User Defaults.

UserDefaults can only store property list types such as String, Array, Dictionary, Data, and a few others. This can be a limitation if you need to store more complex data structures. However, you can use workarounds to make it work with complex data types but I always find it suspicious when I have to use workarounds on any technology to make it work.

Another thing about User Defaults is that it isn’t thread-safe. Be careful with data consistency or even crashes if you are doing too many operations with it.

 

4 – Using Environment Object to Transfer Data from UIKit to SwiftUI

Going back to SwiftUI capabilities you don’t necessarily have to use the initializer to pass data to your SwiftUI Views. There’s another function called Environment Object that you can use to inject objects that will be available through the hierarchy of the SwiftUI views they are injected.

And a plus with this solution is that it invalidates the view, in other words, forces a view refresh, every time the Environment Object changes.

Let’s have a look at the code below:

import UIKit
import SwiftUI

struct User {
    var id: String = UUID().uuidString
}

class ViewController: UIViewController {
    private var usersViewModel = UserViewModel()
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUserViewToUpdateWithEnvironmentObject()
        
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak usersViewModel] _ in
            usersViewModel?.generateUser()
        })
    }
    
    private func configureUserViewToUpdateWithEnvironmentObject() {
        let userView = UserView().environmentObject(usersViewModel) // injecting here the environment object.                      
        
        let userViewController = UIHostingController(rootView: userView)
        addChild(userViewController)
        userViewController.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(userViewController.view)
        userViewController.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            userViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            userViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            userViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])   
    }
}

class UserViewModel: ObservableObject { // add conformance to ObservableObject
    @Published private(set) var users: [User] = [] // add published to this var
    
    @discardableResult
    func generateUser() -> User {
        let newUser = User()
        print(newUser)
        users.append(newUser)
        return newUser
    }
}

To use this technique you have to:

  1. When instantiating your SwiftUI View in UIKit call the environmentObject function, like in the example above.
  2. Add the protocol ObservableObject to the ViewModel.
  3. Add annotation @Published to the user’s var inside the view model.

Steps 2 and 3 are the same when you try to use the @ObservedObject that we checked in the paragraphs above.

That leads to the final way that I will present in this article on how to send data using UIKit to SwiftUI, this time using Notification center.

 

5 – Using Notification Center to Send Data from UIKit to SwiftUI

Notification Center is something that was heavily used in the past and nowadays I don’t see too many people talking about or discussing using it in the projects. However, you can use it as the source of truth for your SwiftUI data.

I wouldn’t recommend this technique as the first implementation because Notification Center is notably hard to reason with and manage on big projects.

Out of curiosity, the technique is below:

import UIKit
import SwiftUI

struct User {
    var id: String = UUID().uuidString
}

class ViewController: UIViewController {
    private var usersViewModel = UserViewModel()
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUserViewToUpdateWitNotificationCenter()
        
        timer = .scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak usersViewModel] _ in
            usersViewModel?.generateUser()
        })
    }
    
    private func configureUserViewToUpdateWitNotificationCenter() {
        let userView = UserView()
        
        let userViewController = UIHostingController(rootView: userView)
        addChild(userViewController)
        userViewController.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(userViewController.view)
        userViewController.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            userViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            userViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            userViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
    }
}

//notification center example
class UserViewModel: ObservableObject { // add conformance to ObservableObject
    @Published private(set) var users: [User] = [] // add published to this var
    
    @discardableResult
    func generateUser() -> User {
        let newUser = User()
        print(newUser)
        users.append(newUser)
        NotificationCenter.default.post(name: NSNotification.Name("sendUsers"), object: self, userInfo: ["users": users])
        return newUser
    }
}

struct UserView: View {
    let usersPublisher = NotificationCenter.default.publisher(for: NSNotification.Name("sendUsers"))
    
    @State var users = [User]()
    
    var body: some View {
        VStack {
            Text("User name: \(users.first?.id ?? "")")
                .minimumScaleFactor(0.5)
                .lineLimit(2)
            Text("User Count: \(users.count)")
        }.onReceive(usersPublisher) { output in // add on receive of the Notification center publisher                                              
            guard let users = output.userInfo?["users"] as? [User] else {
                print("fail")
                return
            }
            print("success \(users)")
            self.users = users
        }
    }
}

As you can see is a very fragile implementation. I know that you can make it robust by creating a manager/service/etc that centrally deals with Notifications Names, but still, this looks workaroundish to me. But is a valid way.

 

Other Ways to Receive Data on SwiftUI

Core Data is something that you could aim for when researching ways to send data to SwiftUI. We have the annotation called FetchRequest that provides a collection of Core Data managed objects to a SwiftUI view. Very useful when you need Core Data objects.

Background Tasks are also a new way to communicate with SwiftUI because since iOS16 we have a new function called backgroundTask in SwiftUI.

 

Summary – Transfer Data From UIKit to SwiftUI

Sending data between UIKit and SwiftUI can be a bit tricky, but as we’ve seen, there are a few different ways to go about it. The key is to find the method that works best for your project and your needs. Using a shared singleton as User Defaults, initializers, notifications, reactive frameworks, or EnvironmentObjects are all excellent ways to send data from UIKit to SwiftUI. And it’s important to note that it can depend on the complexity and size of your project, you may use one method or a combination of methods.

Also, keep in mind that SwiftUI and UIKit integration is a continuous process and as technology advances shortly new methods may be available. But with these 5 proven ways, you now have the tools you need to build apps that are efficient, responsive, and easy to maintain, which is the most important thing.

Last tip: Always keep experimenting and learning to stay updated with new ways of data flow and you will always be ahead of the game!

Fellow Apple Developers, that’s all. I hope you liked reading this article as much as I enjoyed writing it.  

If you want to support this blog you can Buy Me a Coffee or say hello on Twitter. I’m available on LinkedIn or send me an e-mail through the contact page.

You can likewise sponsor this blog so I can get my blog free of ad networks.

Thanks for the reading and… That’s all folks.

Image credit: Featured Image