5 May, 2024

Sneak peek into embedding UIKit into SwiftUI

As a companion for the more in-depth article on bridging SwiftUI into UIKit (check it out here!), we will dive into the main concepts that you will need if you are fortunate enough of having to go the other way round: needing to close gaps in SwiftUI functionality by integrating UIKit views and view controllers into a SwiftUI-centric app.

This is relatively common when dealing with UIKit APIs that are yet to have its counterpart in SwiftUI (as the example we will be following here with web views) or when needing extra customization that is not yet available (e.g., dealing with SwiftUI TextFields is, at the moment, way more restrictive that its UIKit counterpart).

We will follow the example of wrapping UIKit's WKWebView for displaying web views in SwiftUI, a very common business problem that has yet to have its native bridge in SwiftUI.

Hands-on: Intro on UIViewRepresentable

SwiftUI provides a protocol called UIViewRepresentable which we can use to wrap UIKit views and incorporate them into Views. For it we need to implement two methods: one for creating the view and setting its initial state, and one to update the view on state changes if needed.

Lets see how we would implement this for our WebView:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable { 

    let urlToOpen: URL

    // (1) Implement the method makeUIView, which creates our WKWebView and sets its initial state:
    func makeUIView(context: Context) -> WKWebView { 
        WKWebView()
    }

    // (2) Implement the method updateUIView, which we will use to make changes in the view:
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: urlToOpen)
        webView.load(request)
    }
}

Pretty straight-forward! We can now fully use it in any SwiftUI context:

import SwiftUI

struct ContentView: View {
    var body: some View {
        WebView(urlToOpen: URL(string: "https://varanios.com")!)
    }
}

And what if I need a View Controller?

For View Controllers we just have conform to the analogue UIViewControllerRepresentable, which works in pretty much the same way with the following changes:

  • - makeUIView() is now makeUIViewController(), with the responsibility of creating our View Controller instance and its initial state.
  • - updateUIView() is now updateUIViewController(), with the responsibility of updating our View Controller’s state.

Meet Coordinators

Coordinators act as intermediaries between SwiftUI views and UIKit objects, facilitating communication for state updates and handling delegate callbacks. To use them, we need to implement the optional makeCoordinator() method, available in both UIViewRepresentable and UIViewControllerRepresentable.

Following the previous example, we will expand our WebView to print the web page loaded when it finishes, adding a nested Coordinator inside the struct that conforms to the WKNavigationDelegate:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable { 

    let urlToOpen: URL
    
    // (1) Create our Coordinator class to conform to the WKNavigationDelegate:
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: WebView

        init(_ parent: WebView) {
            self.parent = parent
        }

        // This method is called when the web view finishes loading, and here we will print the URL:
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            if let urlString = webView.url?.absoluteString {
                print("Web page finished loading with URL: \(urlString)")
            }
        }
    }
    
    // (2) Create an instance of our Coordinator:
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // (3) Assign our Coordinator object as the navigation delegate of our web view before creating it:
    func makeUIView(context: Context) -> WKWebView { 
        webView.navigationDelegate = context.coordinator
        WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: urlToOpen)
        webView.load(request)
    }
}

Now, whenever you initialize a WebView, the coordinator will handle printing out the URL of the web page that finished loading.

I hope you found this article helpful. For any questions, comments, or feedback, don't hesitate to reach out: connect with me any of the social links below, or drop me an e-mail at marcos@varanios.com.

Thank you for reading! 🥰