6

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")
        }
    }
}

enter image description here

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

1

3 Answers 3

6

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 ?? "")
        }
    }
Sign up to request clarification or add additional context in comments.

6 Comments

Thank you. This is exactly what I was looking for. I need to understand the differences between State and Binding more.
Quick follow-up. It appears my LoginView SwiftUI preview fails rendering. And the culprit seems to be this line ButtonView(title: "Proceed", action: observable.onProceedAction). Strange thing is the ButtonView's preview works just fine. Any idea why that might be?
use this inside the ButtonView_Previews. ButtonView(title: "Title", action: {})
use this for InputView_Previews : InputView(title: "Title", text: .constant(""))
How did you get observable.onLoginAction in viewDidLoad?
|
2

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.

Comments

0

First for passing data on action(Tap on List) using NavigationLink and UIViewControllerRepresentable from SwiftUI to UIKit.

struct LessonsListView: View {
    @StateObject var viewModel = LessonsViewModel()
    var body: some View {
        NavigationView {
            List {
                ForEach(self.viewModel.lessonList,id: \.self) { lesson in
                    ZStack(alignment: .leading) {
                        NavigationLink(destination: LessonVideoPlayerRepresentable(thumbnail: lesson.thumbnail ?? "", videoURL: lesson.video_url ?? "", lessonDesc: lesson.description ?? "", name: lesson.name ?? "")) {
                            EmptyView()
                        }
                        LessonView(lesson: lesson)
                    }.listRowBackground(Color.black)
                    
                }
                .padding([.top, .leading, .bottom], 0.0)
                .foregroundColor(.clear).navigationBarTitleDisplayMode(.large).navigationTitle("Lessons").edgesIgnoringSafeArea([.all])
            }
            .listStyle(.plain).scrollContentBackground(.hidden).navigationBarBackButtonHidden()
            
        }.background(Color.black).onAppear {
            self.viewModel.getLessonsList{ success in
                print(success)
            }
        }.edgesIgnoringSafeArea(.all)
        
    }
}
struct LessonsListView_Previews: PreviewProvider {
    static var previews: some View {
        LessonsListView()
    }
}
struct LessonView: View {
    
    var lesson: LessonsModel?
    @StateObject var viewModel = LessonsViewModel()
    var body: some View {
        HStack(spacing: 10) {
            
            LessonImageView(
                url: URL(string: lesson?.thumbnail ?? "")!,
                placeholder: { Text("Loading ...") }
            ).frame(width: 100, height: 60).aspectRatio(contentMode: .fit)
            Text(lesson?.name ?? "").font(.headline).foregroundColor(.white)
            Spacer()
            Image("Forward")
                .renderingMode(.original)
                .padding(/*@START_MENU_TOKEN@*/.all, 10.0/*@END_MENU_TOKEN@*/)
        }
    }
}
struct LessonView_Previews: PreviewProvider {
    static var previews: some View {
        LessonView()
    }
}

struct LessonImageView<Placeholder: View>: View {
    @StateObject private var loader: ImageLoader
    private let placeholder: Placeholder
    private let image: (UIImage) -> Image
    
    init(
        url: URL,
        @ViewBuilder placeholder: () -> Placeholder,
        @ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
    ) {
        self.placeholder = placeholder()
        self.image = image
        _loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
    }
    
    var body: some View {
        content
            .onAppear(perform: loader.load)
    }
    
    private var content: some View {
        Group {
            if loader.image != nil {
                image(loader.image!).resizable()
            } else {
                placeholder
            }
        }
    }
}

Design will look like screenshot.

enter image description here

Second you need to add UIViewControllerRepresentable in SwiftUI View.

struct LessonVideoPlayerRepresentable: UIViewControllerRepresentable {
    var thumbnail: String
    var videoURL: String
    var lessonDesc: String
    var name: String
    func makeUIViewController(context: UIViewControllerRepresentableContext<LessonVideoPlayerRepresentable>) -> LessonVideoPlayer {
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let detailsVC = storyboard.instantiateViewController(withIdentifier: "LessonVideoPlayer") as! LessonVideoPlayer
        detailsVC.name = name
        detailsVC.thumbnail = thumbnail
        detailsVC.videoURL = videoURL
        detailsVC.lessonDescription = lessonDesc
        detailsVC.view.backgroundColor = .black
        return detailsVC
    }
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext<LessonVideoPlayerRepresentable>) {
        uiViewController.view.backgroundColor = .black
        uiViewController.navigationItem.largeTitleDisplayMode = .never
    }
}

Now make a UIKit ViewController and you will get data from SwiftUi to UIKit Class.

class LessonVideoPlayer: UIViewController {
    
    var thumbnail: String = ""
    var videoURL: String = ""
    var name: String = ""
    var lessonDescription: String = "" 

override func viewDidLoad() {
        super.viewDidLoad()
    
        
        print("Name:\(name)")
        print("Thumbnail:\(thumbnail)")
        print("Video URL:\(videoURL)")
        print("Description:\(lessonDescription)")
         
}

}

2 Comments

stackoverflow.com/questions/69942854/… You can also have a shared source of truth so you don’t have to pass everything individually
@loremipsum it will be easy for developers if they want to pass individual.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.