Incorporate View created with Swift UI into ViewController

Introduction

Hello. I'm Sugawa, who is involved in the development of the job change navigation iOS app at Livesense. This time, I will introduce how to incorporate a View created with SwiftUI into a view controller, using the job change navigation app as an example. Also, I would like to use SwiftUI's ** PropertyWrappers ** to realize a method to update the screen at no implementation cost when the displayed data changes.

About the screen that incorporates Swift UI

The job change navigation app has a place to display the search conditions and the number of search results when searching for a job, but the existing implementation is created with a .xib file. This time, I would like to introduce SwiftUI by replacing View created with xib with SwiftUI. (The red frame part of the image is the target View) Simulator Screen Shot - iPhone 12 Pro - 2020-12-23 at 21.37.26.png

View requirements

The requirements are as follows.

--Tap the ** blue icon ** on the far right to open the search condition setting screen. --When you set the search conditions and close the setting screen, the new search conditions will be reflected. --Search condition setting items -** Job type ** (Multiple selections possible)

Create screen with Swift UI

Creating a ViewModel

First, create a ViewModel that meets the View requirements. The ViewModel conforms to the ObservableObject protocol so that when a value is updated, the new value is automatically reflected in the View. By declaring the property with @Published, the viewing View will be notified of the change when a value update occurs.

python


class SearchConditionHeaderModel: ObservableObject {
    @Published private(set) var workCount = "0"
    @Published private(set) var occupation = "Desired occupation"
    @Published private(set) var salary = "desired salary"
    @Published private(set) var location = "preferred workplace"

    var buttonAction: (() -> Void)?

    func update(workCount: String, occupation: String, salary: String, location: String) {
        self.workCount = workCount
        self.occupation = occupation
        self.salary = salary
        self.location = location
    }
}

Create View

Next, create the following screen with Swift UI. Declare your ViewModel with @ ObservedObject so that you are notified of updates to the ViewModel's properties.

python


@ObservedObject var viewModel: SearchConditionHeaderModel

スクリーンショット 2020-12-23 22.12.33.png By default, the preview on the right side of the screen is displayed in the screen size of the iPhone, but if you add a modifier like ↓, it will be displayed in the size of the content and you can also add a margin.

python


struct SearchConditionHeaderView_Previews: PreviewProvider {
    static var previews: some View {
        SearchConditionHeaderView(viewModel: SearchConditionHeaderModel())
            //View preview by content size
            .previewLayout(PreviewLayout.sizeThatFits)
            //margin
            .padding()
    }
}

Call SwiftUI from ViewController

The layout of the calling view controller is created with Storyboard. I want to display the View created with SwiftUI in the red frame part of the image, so delete the existing View created with xib and add ContainerView to this part. The added ContainerView is linked to the view controller with IBOutlet. スクリーンショット 2020-12-23 22.45.39.png Next, keep the ViewModel created first in the view controller.

python


private var searchConditionModel = SearchConditionHeaderModel()

Call the following function with viewDidLoad () of ViewController.

python


    private func setupConditionHeaderView() {
        // 1.View of SwiftUI wraps in a class that inherits UIHostingController
        //Pass ↑ ViewModel to SwiftUI
        let hostingVC = SearchConditionHeaderHostingViewController(rootView: SearchConditionHeaderView(viewModel: searchConditionModel))
        hostingVC.view.backgroundColor = UIColor(red: 240 / 255, green: 238 / 255, blue: 235 / 255, alpha: 1)

        // 2.Set hostingVC as child ViewController
        addChild(hostingVC)
        // 3.Added hostingVC view to ContainerView subView
        conditionHeaderView.addSubview(hostingVC.view)
        // 4.Call that a child view controller has been added to the parent view controller
        hostingVC.didMove(toParent: self)

        // 5.Fix view with constraint
        hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
        let bindings = ["view": hostingVC.view]
        conditionHeaderView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
                                                      options: NSLayoutConstraint.FormatOptions(rawValue: 0),
                                                      metrics: nil,
                                                      views: bindings as [String: Any]))
        conditionHeaderView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
                                                      options: NSLayoutConstraint.FormatOptions(rawValue: 0),
                                                      metrics: nil,
                                                      views: bindings as [String: Any]))

        // 6.Action when the View button in Swift UI is tapped
        searchConditionModel.buttonAction = { [weak self] in
            self?.showMatchingCondition()
        }
    }
}

I will explain the code.

  1. If you want to call SwiftUI from ViewController, you need ** UIHostingController **. This time I used a subclass because I wanted to control the behavior of NavigationController with viewWillAppear () of UIHostingController, but if there is no part I want to override, it is OK to wrap SwiftUI with UIHostingController. Also, since the viewModel is required to initialize the SwiftUI, we are passing the viewModel called by the view controller.
  2. Add a UIHostingController that wraps the Swift UI as a childViewController for the calling view controller.
  3. Add the hostingVC view (SwiftUI) to the subView of the ContainerView connected by IBOutlet.
  4. If you add a child view controller to ContainerViewController (in this case a subclass of UIHostingController), it seems that you need to notify the child view controller of the completion of the addition with the .didMove (toParent :) method. There was a detailed explanation in here.
  5. If you do not specify the position to display or to restrict the view, the display will shift, so this time we are restricting it.
  6. Finally, define what to do when the button is tapped in the Swift UI. This time, I want to transition from the view controller that holds SwiftUI to the search condition setting screen by tapping the button, so execute the transition process in the view controller. Since we need to notify the view controller that the button has been tapped in the SwiftUI, we are notifying via the ViewModel closure.

Show Swift UI

When you start the app, the View implemented in Swift UI will be displayed. Simulator Screen Shot - iPhone 12 Pro - 2020-12-24 at 00.42.55のコピー.png

Update value

After that, you can bind the search condition to SwiftUI by executing the update () method defined in ViewModel when the search condition is updated.

SearchConditionHeaderModel.swift


func update(workCount: String, occupation: String, salary: String, location: String) {
        self.workCount = workCount
        self.occupation = occupation
        self.salary = salary
        self.location = location
    }

Before After I changed the font and text color slightly, but I was able to replace the equivalent of View made with xib with SwiftUI.

Before After
xib SwiftUI
Simulator Screen Shot - iPhone 12 Pro - 2020-12-24 at 00.49.02 copy.png SimulatorScreenShot-iPhone12Pro-2020-12-24at00.42.55copies.png

Impressions of adding Swift UI to UIKit-based projects

This time I tried adding SwiftUI experimentally, but when creating a View with SwiftUI, it took time to display the Preview, and it was difficult to check the layout. Every time I update the Preview, a differential build runs, and the update is completed several tens of seconds after the build is completed, so if you want to introduce Swift UI in earnest, you need to seek to improve the Preview speed It seems likely. Also, I was wondering which Property Wrappers to use depending on the situation, so I felt that I needed to understand Property Wrappers a little more. It was easier than I expected, so I'd like to use SwiftUI when adding new screens in the future!

Recommended Posts

Incorporate View created with Swift UI into ViewController
Customize View with View Modifier in Swift UI
Starting with Swift Swift UI
Progress made with Swift UI
[Swift UI] Use Map view
How to load UI created by Xib file into View
(For beginners) Swift UI view element collection
I tried using Realm with Swift UI
Manage View status with enum in Swift
Shiritori map app made with Swift UI
Transition to a view controller with Swift WebKit
[Swift] Create an image selection UI with PhotoKit
Swift UI 100 knocks
Display 3D objects of SceneKit with Swift UI, etc.