简体   繁体   中英

Passing data from SwiftUI to UIKit

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?

Demo project

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM