LoginSignup
0
1

More than 1 year has passed since last update.

SwiftUI 2.0 & UIKit & Storyboard (Part 1)

Last updated at Posted at 2021-05-20

Part 1: SwiftUI 2.0 & Storyboard

The article is broken in three different parts:

The first part is about using storyboard elements in a SwiftUI 2.0 application.

Use a Storyboard view controller in SwiftUI

To be able to use a view controller defined in a XCode storyboard in SwiftUI, It has to be wrapped in a type conforming UIViewControllerRepresentable protocol. This is useful for reusing parts of a legacy app or doing things that are possible in the storyboard and UIKit but not yet in SwiftUI.

XCode Project

Create a new SwiftUI app project
image.png
Name the project
Screen Shot 2021-05-19 at 15.40.59.png
Create a new storyboard file
image.png
Name the storyboard
Screen Shot 2021-05-19 at 15.45.27.png
Then populate the default UIViewController with UI elements and make the initial view controller
Screen Shot 2021-05-19 at 22.50.07.png
Now we have a SwiftUI project containing a storyboard but the default ContentView from the SwiftUI project is still being displayed.
We need to connect them ...

StoryboardViewController: The custom UIViewController class

First, we will add a custom UIViewController class to our storyboard view controller...

Create a Swift file for the StoryboardViewController class, our subclass of UIViewController:

StoryboardViewController.swift
import SwiftUI

class StoryboardViewController: UIViewController {
}

Then connect the class to the UI elements of the storyboard view controller's layout using IBOutlets and IBActions:
- labelText: an IBOutlet connected to the second Label element
- inputText: an IBOutlet connected to the TextField element
- changeText(_:): an IBAction connected to the TouchUpInside event for the "Change Text!" Button
- goBack(_:): an IBAction connected to the TouchUpInside event for the "Go Back!" Button

StoryboardViewController.swift
class StoryboardViewController: UIViewController {
    @IBOutlet weak var labelText: UILabel!
    @IBOutlet weak var inputText: UITextField!
    @IBAction func changeText(_ sender: Any) {
        // ...
    }
    @IBAction func goBack(_ sender: Any) {
        // ...
    }
}

Finally, add a reference to the delegate protocol (that will be defined in the next section) and call its methods in the corresponding IBActions:

StoryboardViewController.swift
import SwiftUI

class StoryboardViewController: UIViewController {
    var delegate: StoryboardViewControllerDelegate = DefaultStoryboardViewControllerDelegate()
    @IBOutlet weak var labelText: UILabel!
    @IBOutlet weak var inputText: UITextField!
    @IBAction func changeText(_ sender: Any) {
        delegate.didPressChangeText(self)
    }
    @IBAction func goBack(_ sender: Any) {
        delegate.didPressGoBack(self)
    }
}

StoryboardViewControllerDelegate

Now, let's create the delegate protocol that will be used to communicate changes occurring within the storyboard view controller to other parts of the SwiftUI interface ...

Create a Swift file containing the 2 following types:

  • StoryboardViewControllerDelegate: the delegate protocol used for StoryboardViewController
  • DefaultStoryboardViewControllerDelegate: a default implementation of StoryboardViewControllerDelegate, which methods do nothing when called
StoryboardViewControllerDelegate.swift
import Foundation

protocol StoryboardViewControllerDelegate: AnyObject {
    func didPressChangeText(_ storyboardVC: StoryboardViewController)
    func didPressGoBack(_ storyboardVC: StoryboardViewController)
}

class DefaultStoryboardViewControllerDelegate: StoryboardViewControllerDelegate {
    func didPressChangeText(_ storyboardVC: StoryboardViewController) {}
    func didPressGoBack(_ storyboardVC: StoryboardViewController) {}
}

The StoryboardViewControllerDelegate protocol has 2 methods:

  • didPressChangeText(_:): the method called when the user pressed the "Change Text!" button
  • didPressGoBack(_:): the method called when the user pressed the "Go Back!" button

For simplicity, both methods have only one parameter: the StoryboardViewController instance whose changes they observe.

StoryboardView: The UIViewControllerRepresentable conforming type

Next, let's create the StoryboardView type to wrap the StoryboardViewController and connect the Storyboard interface to the SwiftUI interface.

We need to implement the 2 requirements of the UIViewControllerRepresentable protocol:

StoryboardView.swift
import SwiftUI

struct StoryboardView: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<StoryboardView>) -> StoryboardViewController {
        // ...
    }
    func updateUIViewController(_ uiViewController: StoryboardViewController, context: UIViewControllerRepresentableContext<StoryboardView>) {
    }
}

Then, we implement the makeUIViewController(context:) method to return an instance of the StoryboardViewController from the Storyboard when called by SwiftUI. SwiftUI will call this method once when it’s ready to display the view, and then it will manage the view controller’s life cycle.

To do that, we need to retrieve our StoryboardViewController from the storyboard:

  • To retrieve the storyboard, simply instantiate a UIStoryboard object with the name of the storyboard file (here, the name of the storyboard is "Main" as the file is named "Main.storyboard").
Swift
let storyboard = UIStoryboard(name: "Main", bundle: nil)
Swift
let storyboardVC = storyboard.instantiateInitialViewController()

※ Another option would be to set a storyboardID (like "storyboardView") on the view controller in the storyboard and instantiate it using the instantiateViewController(withIdentifier:) method with the view controller's storyboardID as argument:

Swift
let storyboardVC = storyboard.instantiateViewController(withIdentifier: "storyboardView")
  • Set the Coordinator delegate object that will handle the event notifications from StoryboardViewController in StoryboardView:
Swift
    storyboardVC.delegate = context.coordinator

※ We will discuss the Coordinator delegate object in the next section ...

  • And last, return the StoryboardViewController:
Swift
    return storyboardVC

Coordinator: The StoryboardViewController's delegate

To communicate changes occurring within the view controller to other parts of the SwiftUI interface, a custom coordinator object must be provided to the SwiftUI view.

To listen to changes in storyboard components, delegates are used. Delegates handle event notifications before or after the event itself has been handled by the system.

And, to listen to StoryboardViewController changes our Coordinator will have to conform to the StoryboardViewControllerDelegate protocol we declared earlier, as well as inherit from NSObject to conform to the NSObjectProtocol protocol which cannot be declared in Swift.

Let's declare our Coordinator type with a constructor setting a reference to the StoryboardView instance whose changes it observes:

Swift
struct StoryboardView: UIViewControllerRepresentable {
    // ...
    final class Coordinator: NSObject, StoryboardViewControllerDelegate {
        var parent: StoryboardView
        init(_ parent: StoryboardView) {
            self.parent = parent
        }
        // ...
    }
    // ...
}

It's now time to implement the 2 required methods for the StoryboardViewControllerDelegate:

1.didPressChangeText(_:)

Change the content of labelText to the content of inputText in the StoryboardViewController:

Swift
    func didPressChangeText(_ storyboardVC: StoryboardViewController) {
        storyboardVC.labelText.text = storyboardVC.inputText.text
        storyboardVC.inputText.text = ""
    }
2.didPressGoBack(_:)

Simply dismiss the view using the environment property presentationMode from the Coordinator parent:

Swift
    func didPressGoBack(_ storyboardVC: StoryboardViewController) {
        parent.presentationMode.wrappedValue.dismiss()
    }
Custom Coordinator Object

To provide our own coordinator object instead of the default implementation provided, we need to implement the makeCoordinator() method in StoryboardView:

Swift
struct StoryboardView: UIViewControllerRepresentable {
    // ...
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    // ...
}

The StoryboardView Source Code

StoryboardView.swift
import SwiftUI

struct StoryboardView: UIViewControllerRepresentable {
    @Environment(\.presentationMode) private var presentationMode
    final class Coordinator: NSObject, StoryboardViewControllerDelegate {
        var parent: StoryboardView
        init(_ parent: StoryboardView) {
            self.parent = parent
        }
        func didPressChangeText(_ storyboardVC: StoryboardViewController) {
            storyboardVC.labelText.text = storyboardVC.inputText.text
            storyboardVC.inputText.text = ""
        }
        func didPressGoBack(_ storyboardVC: StoryboardViewController) {
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: UIViewControllerRepresentableContext<StoryboardView>) -> StoryboardViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let storyboardVC = storyboard.instantiateInitialViewController() as! StoryboardViewController
        storyboardVC.delegate = context.coordinator
        return storyboardVC
    }
    func updateUIViewController(_ uiViewController: StoryboardViewController, context: UIViewControllerRepresentableContext<StoryboardView>) {
    }
}

The ContentView Source Code

Finally, let's add our StoryboardView to the App ContentView.

We use a Button to present our StoryboardView in a sheet using the sheet(isPresented:onDismiss:content:) method:

ContentView.swift
import Foundation
import UIKit
import SwiftUI

struct ContentView: View {
    @State private var isShowingStoryboardView = false

    var body: some View {
        VStack {
        Text("Launch Storyboard View!")
            .padding()
            Button(action: {
                self.isShowingStoryboardView = true
            }) {
                Image(systemName: "play.fill")
                    .font(.system(size: 40))
            }.sheet(isPresented: self.$isShowingStoryboardView) {
                StoryboardView()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The App

Launch the App, and in the content view, click on the "▶" button to open the storyboard view controller:
image.png
In the storyboard view, input some new text in the textfield:
image.png
Then, click on the "Change Text!" button, and the "Initial Text" in the second Text element gets replaced with the new text:
image.png
Finally, click on the "Go Back!" button, and the sheet displaying the storyboard view gets dismissed:
image.png
It works!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1