Search code examples
swiftcocoa-touchuikitswiftuiswiftui-form

How do I reuse a UIViewRepresentable UIKit component?


Due to the limitations of SwiftUI, I usually need to jump back into to UIKit and take advantage of UIKit components. This is all good, but I'm finding that I usually need to keep creating separate structs to hold the functionality I need.

For example. Let's say I have a form with 4 fields, e.g. first name, last name, email, and password. I usually need to create separate UIViewRepresentable UIKit components for each.

I'm currently working on a project where I have 2 simple sliders that update 2 different labels when they are moved. Reusing the same UIViewRepresentable UISlider component in SwiftUI treats both sliders as 1, because they both reference the same view model property, hence me needing to create separate components rather than reusing them.

I figured I might as well just see if there is a more efficient way to do this as I've been duplicating code for quite some time now, and there usually isn't much difference in the code apart from things like what kind of keypad to show, colours etc.

Below, is an example of how I tap into UIKit from SwiftUI. I'm creating a screen that will allow bet odds and a stake to be selected using sliders.

I track when the slider's value is changed and store the value in a @Published var from my view model.

self.bViewModel.odds

I access this view model through an environment variable in the view where the slider is used, and use it to update a Text label.

VStack {
    HStack(spacing: 0) {
        Text("\(self.bViewModel.odds)")
             .font(Font.system(size: 15, weight: .semibold, design: .default))
             .foregroundColor(Color("CustomPurple"))
            .frame(minHeight: 0)
            .padding(.top, 3)
            .padding(.bottom, 3)

         Spacer()
    }

    SliderView()
        .frame(minHeight: 25, maxHeight: 25)

}
.padding(.bottom, 30)

The SliderView struct looks like this:

struct SliderView: UIViewRepresentable {

    final class Coordinator: NSObject {

        @EnvironmentObject var bViewModel: BViewModel

        init(bViewModel: EnvironmentObject<BViewModel>)
        {
            self._bViewModel = bViewModel
        }

        // Create a valueChanged(_:) action
        @objc func valueChanged(_ sender: UISlider) {

            self.bViewModel.odds = Double(sender.value)
        }

    }

    func makeCoordinator() -> SliderView.Coordinator {
        Coordinator(bViewModel: self._bViewModel)
    }

    @EnvironmentObject var bViewModel: BViewModel

    func makeUIView(context: Context) -> UISlider {
        let slider = UISlider(frame: .zero)
        slider.minimumValue = 0.00
        slider.maximumValue = 100.00

        slider.addTarget(
             context.coordinator,
             action: #selector(Coordinator.valueChanged(_:)),
             for: .valueChanged
           )

        return slider
    }

    func updateUIView(_ uiView: UISlider, context: Context) {

    }
}

Now, I need a slider for the stake selection, but I can't reuse the same slider from above without writing a bunch of code to make it treat it as a different slider. It's just easier to duplicate the code and change the line where I access my view model's odds variable so that it access the stakes variable instead, e.g.

    @objc func valueChanged(_ sender: UISlider) {

        self.bViewModel.stake = Double(sender.value)
    }

This is how I've been working for months, but it has always felt wrong. However, I can't seem to think of another way to approach this.

Any suggestions? How do you usually handle this kind of situation?

Thanks in advance.


Solution

  • new Answer, now with EnvironmentObject

    import SwiftUI
    import Combine
    
    class ViewModel : ObservableObject {
    
        @Published var sliderValue1 : Double = 3.14
        @Published var sliderValue2 : Double = 42.0
        @Published var sliderValue3 : Double = 3.14
        @Published var sliderValue4 : Double = 42.0
    }
    
    struct SliderView: UIViewRepresentable {
    
        @Binding var value : Double
    
        class Coordinator: NSObject {
            var sliderView: SliderView
    
            init(_ sliderView: SliderView) {
                self.sliderView = sliderView
            }
    
            @objc func valueChanged(_ slider: UISlider) {
                sliderView.value = Double(slider.value)
            }
        }
    
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        func makeUIView(context: Context) -> UISlider {
            let slider = UISlider(frame: .zero)
            slider.minimumValue = 0.00
            slider.maximumValue = 100.00
    
            slider.addTarget(
                 context.coordinator,
                 action: #selector(Coordinator.valueChanged(_:)),
                 for: .valueChanged
               )
    
            return slider
        }
    
        func updateUIView(_ uiView: UISlider, context: Context) {
    
        }
    }
    
    struct ContentView : View {
    
        @EnvironmentObject var viewModel : ViewModel
    
        var body: some View {
            VStack {
                Text("\(self.viewModel.sliderValue1)")
                Slider(value: Binding<Double>(get:
                    { return self.viewModel.sliderValue1 },
                                              set: {
                                                print($0)
                                                self.viewModel.sliderValue1 = $0
                }))
    
                Text("\(self.viewModel.sliderValue2)")
                Slider(value: Binding<Double>(get:
                    { return self.viewModel.sliderValue2 },
                                              set: {
                                                print($0)
                                                self.viewModel.sliderValue2 = $0
                }))
    
    
                Text("\(viewModel.sliderValue3)")
                SliderView(value: Binding<Double>(get:
                    { return self.viewModel.sliderValue3 },
                                              set: {
                                                print($0)
                                                self.viewModel.sliderValue3 = $0
                }))
                Text("\(viewModel.sliderValue4)")
                SliderView(value: Binding<Double>(get:
                    { return self.viewModel.sliderValue4 },
                                              set: {
                                                print($0)
                                                self.viewModel.sliderValue4 = $0
                }))
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
    
            var value1 : Double = 0.4
            var value2 : Double = 0.4
            var value3 : Double = 0.4
            var value4 : Double = 0.4
    
            return ContentView().environmentObject(ViewModel())
        }
    }
    

    ok, because you are so polite and really answer quickly i tried it and hopefully now i got, what you needed ;)

    i did let the "old sliders" in it, but now the lower two sliders are from my UIViewRepresentable.

    NEW ANSWER:

    struct SliderView: UIViewRepresentable {
    
        @Binding var value : Double
    
        class Coordinator: NSObject {
            var sliderView: SliderView
    
            init(_ sliderView: SliderView) {
                self.sliderView = sliderView
            }
    
            @objc func valueChanged(_ slider: UISlider) {
                sliderView.value = Double(slider.value)
            }
        }
    
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        func makeUIView(context: Context) -> UISlider {
            let slider = UISlider(frame: .zero)
            slider.minimumValue = 0.00
            slider.maximumValue = 100.00
    
            slider.addTarget(
                 context.coordinator,
                 action: #selector(Coordinator.valueChanged(_:)),
                 for: .valueChanged
               )
    
            return slider
        }
    
        func updateUIView(_ uiView: UISlider, context: Context) {
    
        }
    }
    
    struct ContentView : View {
    
        @State var sliderValue1 : Double
        @State var sliderValue2 : Double
    
        @State var sliderValue3 : Double
        @State var sliderValue4 : Double
    
        var body: some View {
            VStack {
                Text("\(sliderValue1)")
                Slider(value: Binding<Double>(get:
                    { return self.sliderValue1 },
                                              set: {
                                                print($0)
                                                self.sliderValue1 = $0
                }))
    
                Text("\(sliderValue2)")
                Slider(value: Binding<Double>(get:
                    { return self.sliderValue2 },
                                              set: {
                                                print($0)
                                                self.sliderValue2 = $0
                }))
    
    
                Text("\(sliderValue3)")
                SliderView(value: Binding<Double>(get:
                    { return self.sliderValue3 },
                                              set: {
                                                print($0)
                                                self.sliderValue3 = $0
                }))
                Text("\(sliderValue4)")
                SliderView(value: Binding<Double>(get:
                    { return self.sliderValue4 },
                                              set: {
                                                print($0)
                                                self.sliderValue4 = $0
                }))
            }
        }
    }
    

    OLD ANSWER:

    ok, i figured it out how you can do it without UIViewRepresentable.

    See this example.

    All you have to do is extend your viewModel with your sliderVariables (sliderValue1 or whatever name you like) and

    struct ContentView : View {
    
        @State var sliderValue1 : Double
        @State var sliderValue2 : Double
    
        var body: some View {
            VStack {
                Text("\(sliderValue1)")
                Slider(value: Binding<Double>(get:
                    { return self.sliderValue1 },
                                              set: {
                                                print($0)
                                                self.sliderValue1 = $0
                }))
    
                Text("\(sliderValue2)")
                Slider(value: Binding<Double>(get:
                    { return self.sliderValue2 },
                                              set: {
                                                print($0)
                                                self.sliderValue2 = $0
                }))
            }
        }
    }