iosswiftuiuikituiviewcontrollerrepresentable

UIViewControllerRepresentable not correctly taking up space


I am trying to use a custom UIViewController in a SwiftUI view. I set up a UIViewControllerRepresentable class which creates the UIViewController in the makeUIViewController method. This creates the UIViewController and displays the button, however, the UIViewControllerRepresentable does not take up any space.

I tried using a UIImagePickerController instead of my custom controller, and that sizes correctly. The only way I got my controller to take up space was by setting a fixed frame on the UIViewControllerRepresentable in my SwiftUI view, which I absolutely don't want to do.

Note: I do need to use a UIViewController because I am trying to implement a UIMenuController in SwiftUI. I got all of it to work besides this problem I am having with it not sizing correctly.

Here is my code:

struct ViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> MenuViewController {
        let controller = MenuViewController()
        return controller
    }
    
    func updateUIViewController(_ uiViewController: MenuViewController, context: Context) {
        
    }
}

class MenuViewController: UIViewController {
    override func viewDidLoad() {
        let button = UIButton()
        button.setTitle("Test button", for: .normal)
        button.setTitleColor(.red, for: .normal)
        
        self.view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        button.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        button.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
    }
}

My SwiftUI view:

struct ClientView: View {
    var body: some View {
        VStack(spacing: 0) {
            EntityViewItem(copyValue: "copy value", label: {
                Text("Name")
            }, content: {
                Text("Random name")
            })
            .border(Color.green)
            
            ViewControllerRepresentable()
                .border(Color.red)
            
            EntityViewItem(copyValue: "copy value", label: {
                Text("Route")
            }, content: {
                HStack(alignment: .center) {
                    Text("Random route name")
                }
            })
            .border(Color.blue)
        }
    }
}

Screenshot:

screenshot of question

I do not have much experience with UIKit - my only experience is writing UIKit views to use in SwiftUI. The problem could very possibly be related to my lack of UIKit knowledge.

Thanks in advance!

Edit:

Here is the code for EntityViewItem. I will also provide the container view that ClientView is in - EntityView.

I also cleaned up the rest of the code and replaced references to Entity with hardcoded values.

struct EntityViewItem<Label: View, Content: View>: View {
    var copyValue: String
    var label: Label
    var content: Content
    var action: (() -> Void)?
    
    init(copyValue: String, @ViewBuilder label: () -> Label, @ViewBuilder content: () -> Content, action: (() -> Void)? = nil) {
        self.copyValue = copyValue
        self.label = label()
        self.content = content()
        self.action = action
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            label
                .opacity(0.6)
            content
                .onTapGesture {
                    guard let unwrappedAction = action else {
                        return
                    }
                    unwrappedAction()
                }
                .contextMenu {
                    Button(action: {
                        UIPasteboard.general.string = copyValue
                    }) {
                        Text("Copy to clipboard")
                        Image(systemName: "doc.on.doc")
                    }
                }
        }
        .padding([.top, .leading, .trailing])
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

The container of ClientView:

struct EntityView: View {
    let headerHeight: CGFloat = 56
    
    var body: some View {
        ZStack {
            ScrollView(showsIndicators: false) {
                VStack(spacing: 0) {
                    Color.clear.frame(
                        height: headerHeight
                    )
                    ClientView()
                }
            }
            VStack(spacing: 0) {
                HStack {
                    Button(action: {
                    }, label: {
                        Text("Back")
                    })
                    Spacer()
                    Text("An entity name")
                        .lineLimit(1)
                        .minimumScaleFactor(0.5)
                    
                    Spacer()
                    Color.clear
                        .frame(width: 24, height: 0)
                }
                .frame(height: headerHeight)
                .padding(.leading)
                .padding(.trailing)
                .background(
                    Color.white
                        .ignoresSafeArea()
                        .opacity(0.95)
                )
                Spacer()
            }
        }
    }
}

Solution

  • Thanks so much to @udbhateja and @jnpdx for the help. That makes a lot of sense why the UIViewControllerRepresentable compresses its frame when inside a ScrollView. I did end up figuring out a solution to my problem which involved setting a fixed height on the UIViewControllerRepresentable. Basically, I used a PreferenceKey to find the height of the SwiftUI view, and set the frame of the UIViewControllerRepresentable to match it.

    In case anyone has this same problem, here is my code:

    struct EntityViewItem<Label: View, Content: View>: View {
        var copyValue: String
        var label: Label
        var content: Content
        var action: (() -> Void)?
        
        @State var height: CGFloat = 0
        
        init(copyValue: String, @ViewBuilder label: () -> Label, @ViewBuilder content: () -> Content, action: (() -> Void)? = nil) {
            self.copyValue = copyValue
            self.label = label()
            self.content = content()
            self.action = action
        }
        
        var body: some View {
            ViewControllerRepresentable(copyValue: copyValue) {
                SizingView(height: $height) { // This calculates the height of the SwiftUI view and sets the binding
                    VStack(alignment: .leading, spacing: 2) {
                        // Content
                    }
                    .padding([.leading, .trailing])
                    .padding(.top, 10)
                    .padding(.bottom, 10)
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
            }
            .frame(height: height) // Here I set the height to the value returned from the SizingView
        }
    }
    

    And the code for SizingView:

    struct SizingView<T: View>: View {
        
        let view: T
        @Binding var height: CGFloat
        
        init(height: Binding<CGFloat>, @ViewBuilder view: () -> T) {
            self.view = view()
            self._height = height
        }
        var body: some View {
            view.background(
                GeometryReader { proxy in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: proxy.size)
                }
            )
            .onPreferenceChange(SizePreferenceKey.self) { preferences in
                height = preferences.height
            }
    
        }
        
        func size(with view: T, geometry: GeometryProxy) -> T {
            height = geometry.size.height
            return view
        }
    }
    
    struct SizePreferenceKey: PreferenceKey {
        static var defaultValue: CGSize = .zero
    
        static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
            value = nextValue()
        }
    }
    

    With this finished, my UIMenuController is fully functional. It was a lot of code (if this functionality existed in SwiftUI, I probably would have had to write like 5 lines of code), but it works great. If anyone would like the code, please comment and I will share.

    Here is an image of the final product: enter image description here