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 ?? "")
}
}
There certainly isn't one definitively right answer for this. The ObservableObject
solution you mentioned is one. Assuming you just needed uni-directional data flow (which your example seems to have), I might be tempted to use a structure with just a simple delegate function that gets called with actions -- a Redux-esque approach:
enum AppAction {
case buttonPress(value: String)
case otherButtonPress(value: Int)
}
typealias DispatchFunction = (AppAction) -> Void
struct ContentView : View {
var dispatch : DispatchFunction = { action in
print(action)
}
var body: some View {
VStack {
SubView(dispatch: dispatch)
SubView2(dispatch: dispatch)
}
}
}
struct SubView : View {
var dispatch : DispatchFunction
var body: some View {
Button(action: { dispatch(.buttonPress(value: "Test")) }) {
Text("Press me")
}
}
}
struct SubView2 : View {
var dispatch : DispatchFunction
var body: some View {
Button(action: { dispatch(.otherButtonPress(value: 2)) }) {
Text("Press me 2")
}
}
}
class ViewController : UIViewController {
func dispatch(_ action: AppAction) {
print("Action: \(action)")
}
override func viewDidLoad() {
let controller = UIHostingController(rootView: ContentView(dispatch: self.dispatch))
//...
}
}
That way, you're still passing around something to all of your subviews, but it's a pretty simple and light dependency just to pass DispatchFunction
around like this.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.