Part 1: SwiftUI 2.0 & Storyboard
The article is broken in three different parts:
- Part 1: SwiftUI 2.0 & Storyboard (日本語)
- Part 2: SwiftUI 2.0 & UIKit
- Part 3: Storybard & SwiftUI 2.0
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
Name the project
Create a new storyboard file
Name the storyboard
Then populate the default UIViewController
with UI elements and make the initial view controller
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
:
import SwiftUI
class StoryboardViewController: UIViewController {
}
Then connect the class to the UI elements of the storyboard view controller's layout using IBOutlet
s and IBAction
s:
-
labelText
: anIBOutlet
connected to the secondLabel
element -
inputText
: anIBOutlet
connected to theTextField
element -
changeText(_:)
: anIBAction
connected to theTouchUpInside
event for the "Change Text!"Button
-
goBack(_:)
: anIBAction
connected to theTouchUpInside
event for the "Go Back!"Button
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 IBAction
s:
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 forStoryboardViewController
-
DefaultStoryboardViewControllerDelegate
: a default implementation ofStoryboardViewControllerDelegate
, which methods do nothing when called
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:
- a
makeUIViewController(context:)
method that creates and configure aUIViewController
. - an
updateUIViewController(_:context:)
method that updates the state of the specified view controller with new information from SwiftUI. We will leave it blank as we won't use it for our example.
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").
let storyboard = UIStoryboard(name: "Main", bundle: nil)
- To retrieve the
StoryboardViewController
, simply instantiate the storyboard initial view controller using theinstantiateInitialViewController()
method:
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:
let storyboardVC = storyboard.instantiateViewController(withIdentifier: "storyboardView")
- Set the
Coordinator
delegate object that will handle the event notifications fromStoryboardViewController
inStoryboardView
:
storyboardVC.delegate = context.coordinator
※ We will discuss the Coordinator
delegate object in the next section ...
- And last, return the
StoryboardViewController
:
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:
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
:
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:
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
:
struct StoryboardView: UIViewControllerRepresentable {
// ...
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// ...
}
The StoryboardView Source Code
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:
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:
In the storyboard view, input some new text in the textfield:
Then, click on the "Change Text!" button, and the "Initial Text" in the second Text
element gets replaced with the new text:
Finally, click on the "Go Back!" button, and the sheet displaying the storyboard view gets dismissed:
It works!