I'm new to SwiftUI and I've been experimenting with how to integrate SwiftUI and UIKit together in the same app. I made a simple login screen with SwiftUI.
struct LoginView: View {
var body: some View {
VStack {
LogoView()
InputView(title: "Company Code")
ButtonView(title: "Proceed")
}
}
}
And I made all the components in this view reusable by extracting them to separate views (LogoView, InputView, ButtonView).
struct LogoView: View {
var body: some View {
VStack {
Image("logo")
Text("Inventory App")
.foregroundColor(.blue)
.fontWeight(.bold)
.font(.system(size: 32))
}
}
}
struct InputView: View {
let title: String
@State private var text: String = ""
var body: some View {
VStack(alignment: .leading) {
Text(title)
.foregroundColor(.gray)
.fontWeight(.medium)
.font(.system(size: 18))
TextField("", text: $text)
.frame(height: 54)
.textFieldStyle(PlainTextFieldStyle())
.padding([.leading, .trailing], 10)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray))
}
.padding()
}
}
struct ButtonView: View {
let title: String
var body: some View {
Button(title) {
print(#function)
}
.frame(minWidth: 100, idealWidth: 100, maxWidth: .infinity, minHeight: 60, idealHeight: 60)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.padding([.leading, .trailing])
}
}
And I show the view by embedding it inside a UIHostingController
in the View Controller.
class LoginViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let controller = UIHostingController(rootView: LoginView(observable: observable))
controller.view.translatesAutoresizingMaskIntoConstraints = false
addChild(controller)
view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
My problem is how can I get the text inputted in the InputView and the button tap occurs in the ButtonView, all the way up to the View Controller?
In this tutorial, it uses ObservableObject
to pass the data back to the View Controller. Although in that example, the entire view is in a single SwiftUI file. In my case, I broke down the view to separate components.
So I'm wondering, is ObservableObject
still the way to do it? Since my views are subviews, I feel like creating multiple observable objects to propagate values up the subview chain is not ideal.
Is there a better way to achieve this?
First, use binding to your input view. And for action use closure to get action from SwiftUI to UIKit.
Here is a possible solution.
class LoginViewObservable: ObservableObject {
@Published var code: String = ""
var onLoginAction: (()->Void)! //<-- Button action closure
}
struct LoginView: View {
@ObservedObject var observable: LoginViewObservable
var body: some View {
VStack {
LogoView()
InputView(title: "Company Code", text: $observable.code) //<- Binding text
ButtonView(title: "Proceed", action: observable.onLoginAction) //<- Pass action
}
}
}
struct InputView: View {
let title: String
@Binding var text: String //<- Binding
var body: some View {
VStack(alignment: .leading) {
Text(title)
.foregroundColor(.gray)
.fontWeight(.medium)
.font(.system(size: 18))
TextField("", text: $text)
.frame(height: 54)
.textFieldStyle(PlainTextFieldStyle())
.padding([.leading, .trailing], 10)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray))
}
.padding()
}
}
struct ButtonView: View {
let title: String
var action: () -> Void
var body: some View {
Button(title) {
action() //<- Send action
}
.frame(minWidth: 100, idealWidth: 100, maxWidth: .infinity, minHeight: 60, idealHeight: 60)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.background(Color(hex: "4980F3"))
.cornerRadius(10)
.padding([.leading, .trailing])
}
}
in last, inside the viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// Other code----
observable.onLoginAction = { [weak self] in //<-- Get login action
print(self?.observable.code ?? "")
}
}