Search code examples
swiftobjective-cxcodeswiftuiinstance

How to access SwiftUI Button instance at particular index in LazyVStack which the rootView of a UIHostingController?


I have a custom SwiftUI containerView which contains a set of SwiftUI buttons.

The SwiftUI button contains a UIKit UIView which I need to be used by the main UIViewConroller. The code looks something like as below:

    public struct ContainerView: View {
        public var body: some View {
           LazyVStack {
           ForEach(viewModel.buttonViewModels, id:\.self) { buttonViewModel in 
           CustomButton(buttonViewModel)
       }
     }
    }
   }

    public class CustomButton: View {
    public var backgroundUIView = BackgroundUIView()
    public var body: some View {
        Button(action: viewModel.tapEvent), label: {
           ZStack {
               backgroundUIView
               Text(viewModel.title)
           } 
        }
    }


    public BackgroundUIView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = .blue
        return view
    }


    public CustomHostingController: UIHostingController<ContainerView> {
        public var containerViewModel: ContainerViewModel

        public init?(containerViewModel: ContainerViewModel) {
           self.containerVieModel = containerViewModel
           super.init(rootView: ContainerView())
        }
   }

In CustomHostingController how can I access CustomButton at particular index I want something like this.

extension CustomHostingController {
    func getButtonAtIndex(index: Int) -> CustomButton {
       rootView.body.subviews[index]
    }
}

I need to find the CustomButton instance at particular index in CustomHostingController.

Tried to find a way to find Swiftui button at particular index in ContainerView. like

    extension CustomHostingController {
        func getButtonAtIndex(index: Int) -> CustomButton {
           rootView.body.*subviews[index]*
        }
    }

In unlike UIKit in SwiftUI there is no way to do this subViews[at index]. How can I access CustomButton instance at particular index?

I need access to CustomButton because CustomHostingController is a childViewController to a UIKit UIViewController, and I want to present a third party provided Popover controller (UIKit implementation) which requires the UIView as the source view for presenting it. The third party popover controller initializer needs a UIView or UIButton as the parameter sourceView so that the arrow pointer of the Popover controller can point to the UIView or UIButton.

So I need to use the customButton instance like as below:

'[[ThirdPartyPopoverController alloc] initWithDescription: @"Test coachmark presented" fromSourceView:customButton.backgroundUIView];`


Solution

  • You'll need to expose the UIView (in this case, the backgroundUIView) to your CustomHostingController. This can be done by passing a callback to the CustomButton, which lets the hosting controller know when a new UIView is created.

    try this:

    public struct BackgroundUIView: UIViewRepresentable {
        var onUIViewCreated: ((UIView) -> Void)?
    
        func makeUIView(context: Context) -> UIView {
            let view = UIView()
            view.backgroundColor = .blue
            onUIViewCreated?(view)
            return view
        }
    
        func updateUIView(_ uiView: UIView, context: Context) {}
    }
    
    public struct CustomButton: View {
        public var viewModel: ButtonViewModel
        public var index: Int
        public var onUIViewCreated: ((Int, UIView) -> Void)?
    
        public var body: some View {
            Button(action: viewModel.tapEvent) {
                ZStack {
                    BackgroundUIView(onUIViewCreated: { view in
                        onUIViewCreated?(index, view)
                    })
                    Text(viewModel.title)
                }
            }
        }
    }
    
    public struct ContainerView: View {
        @ObservedObject var viewModel: ContainerViewModel
        var onUIViewCreated: ((Int, UIView) -> Void)?
    
        public var body: some View {
            LazyVStack {
                ForEach(viewModel.buttonViewModels.indices, id: \.self) { index in
                    CustomButton(viewModel: viewModel.buttonViewModels[index],
                                 index: index,
                                 onUIViewCreated: onUIViewCreated)
                }
            }
        }
    }
    
    public class CustomHostingController: UIHostingController<ContainerView> {
        public var containerViewModel: ContainerViewModel
        private var buttonUIViews: [Int: UIView] = [:]
    
        public init(containerViewModel: ContainerViewModel) {
            self.containerViewModel = containerViewModel
            super.init(rootView: ContainerView(viewModel: containerViewModel, onUIViewCreated: { [weak self] index, uiView in
                self?.buttonUIViews[index] = uiView
            }))
        }
    
        @objc required dynamic init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        public func getButtonAtIndex(index: Int) -> UIView? {
            return buttonUIViews[index]
        }
    }
    

    and use like below:

    let hostingController = CustomHostingController(containerViewModel: yourViewModel)
    if let buttonView = hostingController.getButtonAtIndex(index: 2) {
        let popover = ThirdPartyPopoverController(description: "Test", fromSourceView: buttonView)
        // Present your popover
    }