Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

4. Integrating SwiftUI
Written by Audrey Tam

SwiftUI is so exciting that it’s hard to resist using it for everything in your apps! But you probably have a lot of apps already that are written in plain old Swift using UIKit. There’s no way you have time to rewrite them all in SwiftUI. What to do?

No need to fear. Apple has your back. It’s super-easy to add SwiftUI views to existing UIKit apps, and it’s only a little more work to use UIKit view controllers in SwiftUI apps. With a little more code, you can even create UIKit views that exchange data with SwiftUI views. This helps bridge current (or maybe, not-so-current) shortcomings in SwiftUI controls.

Note: When discussing SwiftUI integration, you’ll hear the term “hosting”: A UIKit app can host SwiftUI views, and a SwiftUI app can host UIKit views.

In this chapter, you’ll learn how to do the following:

  • Host a SwiftUI view in a UIKit project.
  • Host a view controller in a SwiftUI project.
  • Host a UIKit view with data dependencies in a SwiftUI project.

Time to get started!

Getting started

First, duplicate (using Command-D) the BullsEye starter project in the chapter materials. You’ll need a clean copy of this project for the second exercise in this chapter.

Now open the BullsEye starter project, and build and run:

UIKit BullsEye starter app
UIKit BullsEye starter app

This UIKit app displays a random target value between 1 and 100. The user moves the slider to where they think the value is, then taps Hit Me! to see their score.

Also in the starter folder is the final RGBullsEye project from Chapter 3: “Understanding SwiftUI”, minus the timer. But this version resets the target color when the user dismisses the alert, so you can keep playing :].

You’re about to integrate this SwiftUI view into the UIKit BullsEye app!

Targeting iOS 13

SwiftUI requires iOS 13, so check that your UIKit app’s deployment target is iOS 13 or higher:

UIKit BullsEye deployment target is iOS 13.
UIKit BullsEye deployment target is iOS 13.

Hosting a SwiftUI view in a UIKit project

The absolute easiest integration to perform is to host a SwiftUI view in an existing UIKit app. All you have to do is:

  1. Add the SwiftUI view file to your project.
  2. Add a button to play RGBullsEye.
  3. Drag a Hosting Controller onto your storyboard and create a segue to it.
  4. Connect the segue to an @IBSegueAction in your view controller code and set the hosting controller’s rootView to an instance of your SwiftUI view.

So to start, open the project navigator and drag ContentView.swift from Finder into the BullsEye project. Check the Destination box Copy items if needed.

Add ContentView.swift to BullsEye project.
Add ContentView.swift to BullsEye project.

Next, in the storyboard, open the Library, add a button, and change its title to Play RGBullsEye. Also, set constraints to pin the button’s bottom edge to the view’s bottom margin, and center it horizontally in the view:

Add Play RGBullsEye button.
Add Play RGBullsEye button.

That was step 2. Now for the good part: From the Library, drag a Hosting View Controller onto the storyboard, then Control-drag to it from the Play RGBullsEye button, and select Show:

Create segue from button to Hosting Controller.
Create segue from button to Hosting Controller.

A UIHostingController is a UIViewController whose Content is a SwiftUI View. You’ve already seen it in SceneDelegate when you load ContentView into the window:

window.rootViewController = UIHostingController(
  rootView: ContentView(...))

Double-click in the hosting controller’s navigation bar and set its title to RGBullsEye. And that’s step 3 done!

Now, for step 4.

In the storyboard, select View Controller, and open the assistant editor (Control-Option-Command-Return). Add this statement to the top of ViewController.swift:

import SwiftUI

Control-drag from the segue in the storyboard into ViewController to create an @IBSegueAction. Name it openRGBullsEye:

Create @IBSegueAction in ViewController.
Create @IBSegueAction in ViewController.

@IBSegueAction is new in Xcode 11. You can use it in UIKit apps instead of prepare(for:sender:). It’s especially useful if you want to set properties in the destination view controller when you create it. And because it’s connected directly to a segue, you don’t even need a segue identifier!

Finally, replace the return prompt in openRGBullsEye(_:) with this code:

UIHostingController(coder: coder, rootView:
  ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5))

Note: As of Swift 5.1, you don’t need the return keyword if there’s only one line of code.

Build and run, then tap Play RGBullsEye. Hey presto! To prove it works, move the sliders, then tap Hit Me!:

Hosting SwiftUI RGBullsEye in UIKit BullsEye.
Hosting SwiftUI RGBullsEye in UIKit BullsEye.

How easy was that!? Now you can go wild adding SwiftUI views to your existing apps!

Hosting a view controller in a SwiftUI project

Now, to do the opposite — host the BullsEye view controller in RGBullsEye — here’s what you’ll do:

  1. Add ViewController.swift and Main.storyboard to RGBullsEye.
  2. In the storyboard’s identity inspector, set the Storyboard ID for ViewController.
  3. Create a representation struct for ViewController.
  4. Add a NavigationLink to ContentView.

Seems straightforward enough. Let’s get started.

Open the RGBullsEye starter project, then open the project navigator.

From the clean copy of the starter BullsEye project in Finder, drag ViewController.swift and Main.storyboard into the RGBullsEye project.

Check the Destination box Copy items if needed.

Add ViewController and storyboard to RGBullsEye project.
Add ViewController and storyboard to RGBullsEye project.

Next, in the storyboard, select View Controller and, in the identity inspector, set its Storyboard ID to ViewController:

Set View Controller’s Storyboard ID.
Set View Controller’s Storyboard ID.

You’ll use this stringly-typed ID in the next step.

Note: The Storyboard ID doesn’t have to match the name of your view controller, but it’s one less thing to think about.

Conforming to UIViewControllerRepresentable

This is where the magic happens.

At the top of ViewController.swift, add the following statement:

import SwiftUI

Below the ViewController class, create ViewControllerRepresentation:

struct ViewControllerRepresentation: UIViewControllerRepresentable {

  func makeUIViewController(
    context: UIViewControllerRepresentableContext
    <ViewControllerRepresentation>) -> ViewController {
    UIStoryboard(name: "Main", bundle: nil)
      .instantiateViewController(
        withIdentifier: "ViewController") as! ViewController
  }

  func updateUIViewController(
    _ uiViewController: ViewController,
    context: UIViewControllerRepresentableContext
    <ViewControllerRepresentation>) {

  }
}

Note: This struct is completely outside your view controller class. The first part of its name doesn’t have to match the name of your view controller. The value of the withIdentifier parameter is the Storyboard ID you set for your view controller in the previous step.

The UIViewControllerRepresentable protocol requires a make method and an update method. The makeUIViewController(context:) method instantiates a ViewController from Main.storyboard — that’s why you had to give it a Storyboard ID.

Note: If your view controller doesn’t use the storyboard, or you just want an empty view controller, creating it is even easier — simply use its default constructor.

You’ll leave the other required method updateUIViewController(_:context:) empty, as ViewController doesn’t depend on your SwiftUI view for any data. If Xcode tries to get you to implement a Coordinator, ignore it: ViewControllerRepresentation doesn’t need one, because your SwiftUI view doesn’t depend on ViewController for any data. But there’s no fear of missing out. The next section needs both of these!

Navigating to the view controller

Almost finally, add this code at the bottom of the highest-level VStack in ContentView, just below the padding modifier of the VStack of ColorSliders:

NavigationLink(destination: ViewControllerRepresentation()) {
  Text("Play BullsEye")
}
.padding(.bottom)

You’ve added a “button” below the color sliders. Tapping it pushes the BullsEye view onto the navigation stack.

Note: You’ll learn all about navigation in Chapter 11: “Lists and Navigation”.

And finally finally, embed the top-level VStack in a NavigationView:

// 1
NavigationView {
  VStack {
    ...
  }
  // 2
  .navigationBarTitle("RGBullsEye")
  .background(Color(.systemBackground))
}
// 3
.navigationViewStyle(StackNavigationViewStyle())
  1. You wrap the top-level VStack in a NavigationView so its NavigationLink works.
  2. You modify the top-level VStack to set its navigation bar title.
  3. You modify NavigationView to override the default split-view navigation view style of an iPhone 11 Pro Max in landscape mode.

Build and run, then tap Play BullsEye. Move the slider, then tap Hit Me!:

Hosting UIKit BullsEye in SwiftUI RGBullsEye.
Hosting UIKit BullsEye in SwiftUI RGBullsEye.

It works! But the BullsEye view’s navigation bar title is missing. Although SwiftUI uses a navigation controller under the covers, there’s a wrapper view controller between the SwiftUI navigation controller and the BullsEye view controller. This wrapper view controller doesn’t have any title information for the navigation controller.

So add this line to viewWillAppear(_:) in ViewController.swift:

parent?.navigationItem.title = "BullsEye"

The BullsEye view controller tells its parent what its title is. When this parent is the SwiftUI wrapper view controller, it can now pass this information on to the SwiftUI navigation controller. Thanks to @josephap on StackOverflow (bit.ly/2Xzkm8E) for this fix.

Build and run, then tap Play BullsEye to see the navigation bar title:

Navbar title for UIKit BullsEye in SwiftUI RGBullsEye.
Navbar title for UIKit BullsEye in SwiftUI RGBullsEye.

Previewing UIKit views

So that didn’t take long to do. But wait, there’s more! Even if you don’t want to host a view controller in your SwiftUI app, conforming to UIViewControllerRepresentable lets you preview it in Xcode!

Head back to ViewController.swift and scroll down to ViewControllerRepresentation. Add this code below the struct:

struct ViewControllerPreviews: PreviewProvider {
  static var previews: some View {
    ViewControllerRepresentation()
  }
}

This is just like the usual preview code that you see below every SwiftUI view.

Press Option-Command-Return to open the canvas, then Option-Command-P to refresh the preview. You can even start Live Preview and play the game:

Live previewing ViewControllerRepresentation.
Live previewing ViewControllerRepresentation.

Your representation is as live-previewable as a SwiftUI view!

Hosting a UIKit view with data dependencies

Hosting BullsEye in RGBullsEye was pretty easy, but that’s because there aren’t any data dependencies between the BullsEye view controller and the rest of your SwiftUI app.

In this section, you’ll replace the SwiftUI Slider view with a UISlider. Here’s your motivation: the original UIKit RGBullsEye color-coded the sliders by setting their thumbTintColor property, but the SwiftUI Slider doesn’t have this property. So you need to use UISlider to access this property.

The process is similar to hosting a view controller, with a few more steps:

  1. Create a SwiftUI view that conforms to UIViewRepresentable.
  2. Implement the make method to instantiate the UIKit view.
  3. Implement the update method to update the UIKit view from the SwiftUI view.
  4. Create a Coordinator and implement a target-action method to update the SwiftUI view from the UIKit view.

Conforming to UIViewRepresentable

Start by creating a new iOS ▸ User Interface ▸ SwiftUI View file, and name it ColorUISlider.swift.

Now start creating ColorUISlider. Replace the struct ColorUISlider boilerplate with this code:

struct ColorUISlider: UIViewRepresentable {

  func makeUIView(context: Context) -> UISlider {
    let slider = UISlider(frame: .zero)
    return slider
  }

  func updateUIView(_ uiView: UISlider, context: Context) {

  }

}

ColorUISlider wraps a UIView, not a UIViewController, so it conforms to UIViewRepresentable, not UIViewControllerRepresentable.

This protocol also requires a make method and an update method. Here, you’ve implemented the bare minimum, just creating a UISlider in makeUIView(context:).

Note: To quickly wrap a simple UIView, you can use the generic type from John Sundell’s Inline wrapping of UIKit or AppKit views within SwiftUI.

Updating the UIView from SwiftUI

Next, add these properties to ColorUISlider:

var color: UIColor
@Binding var value: Double

Note: Ignore the error message about missing arguments in previews, while you finish setting up ColorUISlider.

One of the complications you need to handle is the different color types in UIKit and SwiftUI. SwiftUI Color is a view, and UIColor isn’t. Fortunately, you can create a Color from a UIColor value — Color(UIColor.red) — so you declare color as UIColor.

The other complication is the different value types for UISlider (Float) and Slider (Double). It’s easier to create a Float from a Double than the other way around, so you declare value as a Double.

As in ColorSlider, value is a @Binding that references @State variables in ContentView.

Add these lines below let slider = UISlider(...), before the return statement:

slider.thumbTintColor = color
slider.value = Float(value)

OK, now you’ll get rid of the the error message. Down in ColorUISlider_Previews, replace ColorUISlider() with the following:

ColorUISlider(color: .red, value: .constant(0.5))
  .previewLayout(.sizeThatFits)

You’re adding arguments for the color and value properties, and setting the layout size. Without this modifier, you’d see your slider centered in an iPhone window.

Refresh the preview if necessary:

Previewing ColourUISlider.
Previewing ColourUISlider.

And there’s your color-coded slider thumb! Beautiful, isn’t it?

Note: UISlider also has minimumTrackTintColor and maximumTrackTintColor properties, in case you want to dress it up even more.

OK, back to work on ColorUISlider. You’ve completed steps 1 and 2, now move on to step 3.

Coordinating data between UIView and SwiftUI view

Add this line to updateUIView(_:context:):

uiView.value = Float(self.value)

When the UISlider receives a Double value from the SwiftUI view, it updates its Float value.

And that was the entire step 3. The UIKit control gets the SwiftUI value. Now to get the data flowing in the other direction.

Step 4 is the longest: You’ll create a coordinator to keep your SwiftUI view ColorUISlider data in sync with the UIKit control UISlider data.

Start by adding this class inside your ColorUISlider struct:

class Coordinator: NSObject {
  var parent: ColorUISlider
  init(_ parent: ColorUISlider) {
    self.parent = parent
  }
}

Because you’ve created this Coordinator class, the UIViewRepresentable protocol requires a makeCoordinator() method, so you’ll see a does-not-conform error message. Get rid of it by adding this just above makeUIView(context:):

func makeCoordinator() -> ColorUISlider.Coordinator {
  Coordinator(self)
}

You’re just connecting the coordinator with its parent ColorSlider, whose underlying UIView is a UISlider.

The purpose of the coordinator is to pass the UISlider value to the ColorSlider property value. This is a binding that references a @State variable value in ContentView, so the UISlider value effectively updates ContentView.

To get the UISlider value, the coordinator implements an action for the UISlider control event valueChanged. You specify the target-action connection in the parent view’s makeUIView(context:).

So add this action to your Coordinator:

@objc func updateColorUISlider(_ sender: UISlider) {
  parent.value = Double(sender.value)
}

The type of sender.value is Float, so you have to convert it to Double to store it in parent.value.

And add this line to makeUIView(context:), just before the return statement:

slider.addTarget(context.coordinator,
  action: #selector(Coordinator.updateColorUISlider(_:)), 
  for: .valueChanged)

You’re using the standard UIKit addTarget(_:action:for:) method to associate the UISlider control event constant valueChanged with the Coordinator target and its updateColorUISlider(_:) action. This method is called whenever the UIControl event valueChanged occurs, which happens whenever the user moves the slider.

Note: The underlying UIView doesn’t have to be a control. Coordinator can also adopt a delegate protocol, like MKMapViewDelegate and implement delegate methods for the UIView.

This completes the task of connecting ColorUISlider with its Coordinator. You now have two-way communication between UISlider and ColorUISlider!

Making it all happen!

Finally, put your ColorUISlider to work! In ContentView, scroll down to struct ColorSlider, and replace Slider and all its modifiers with this view:

ColorUISlider(color: textColor, value: $value)

Now that you have thumb tint color, you don’t need background color. And without a background color, rounded corners don’t show up.

To fix up the Color vs. UIColor mismatch, change the type of textColor to UIColor:

var textColor: UIColor

Now wrap textColor in Color in the foregroundColor modifiers:

Text("0")
  .foregroundColor(Color(textColor))
...
Text("255")
  .foregroundColor(Color(textColor))

The foregroundColor modifier requires a View argument, and Color(textColor) creates a Color view from the UIColor.

If Xcode complains about the VStack of three ColorSliders, specify the type of the textColor arguments as UIColor:

VStack {
  ColorSlider(value: $rGuess, textColor: UIColor.red)
  ColorSlider(value: $gGuess, textColor: UIColor.green)
  ColorSlider(value: $bGuess, textColor: UIColor.blue)
}.padding(.horizontal)

This isn’t actually necessary — Xcode will eventually catch up with textColor being of type UIColor — but it does no harm. At this point, it’s just to prevent the preview refresh from being held up by an out-of-date error message.

Build and run:

RGBullsEye with UISliders.
RGBullsEye with UISliders.

Oooh, color-coded slider thumbs! And that’s how you overcome the absence of thumb tint color in the SwiftUI Slider!

Challenge

Challenge: Data dependency on a UIKit control

The challenge/starter folder contains a SwiftUI BullsEye app that changes the slider’s background color opacity to provide feedback to the user. Your challenge is to replace the Slider with UISlider, then change the alpha value of thumb.tintColor to provide feedback.

SwiftUI BullsEye with opacity feedback in slider thumb.
SwiftUI BullsEye with opacity feedback in slider thumb.

Hint: Opacity is the same thing as alpha.

The solution is in the challenge/final folder for this chapter.

Key points

To host a SwiftUI view in a UIKit project:

  1. Add the SwiftUI view file to your project.
  2. Add a hosting controller to your storyboard, and create a segue to it.
  3. Connect the segue to an @IBSegueAction in your view controller code.
  4. Set the hosting controller’s rootView to an instance of your SwiftUI view.

To host a view controller in a SwiftUI project:

  1. Add the view controller and storyboard files to the SwiftUI project.
  2. In the storyboard’s identity inspector, set the Storyboard ID for the VC.
  3. Create a representation struct for the view controller, and implement the makeUIViewController(context:) method to instantiate it.
  4. Add a NavigationLink to ContentView, with the view controller representation as the link’s destination.

To host a UIKit view with data dependencies:

  1. Create a SwiftUI view that conforms to UIViewRepresentable.
  2. Implement the make method to instantiate the UIKit view.
  3. Implement the update method to update the UIKit view from the SwiftUI view.
  4. Create a Coordinator and implement action or delegate methods to update the SwiftUI view from the UIKit view.

Where to go from here?

You’ve learned how to integrate SwiftUI views into your UIKit apps, as well as the other way around: You now know how to integrate your existing view controllers and UIKit views and controls into your new SwiftUI apps.

So what are you waiting for? The other chapters in this book have loads of ideas for SwiftUI views to add to your existing apps. Or you might see an opportunity to use one of your existing controls, views or view controllers in one of our sample SwiftUI projects. We can’t wait to see what you create!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.