4

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()
            }
        }
    }
}
Timmy
  • 166
  • 1
  • 9
  • You must have more going on than what you listed here, because the code you've provided doesn't lead to that result (and, for that matter, doesn't even compile since you've listed generic requirements on `MenuViewController` that don't get used anywhere). Can you test what you have in a [mre] that we can run? – jnpdx Apr 23 '21 at 18:03
  • Your `ViewControllerRepresentable` is good as per UI. Post `EntityViewItem` and `Entity` code so that I can run you code without errors – udbhateja Apr 23 '21 at 19:48
  • @jnpdx - I changed the existing code and added some more. You should be able to compile with just the code there. – Timmy Apr 24 '21 at 02:36
  • @udbhateja read the comment above. – Timmy Apr 24 '21 at 02:36
  • 1
    @Timmy it's a function of being inside of the `ScrollView`. Without the `ScrollView`, the UIViewControllerRepresentable takes up all available space (much like GeometryReader does). So, a couple of questions before I answer: 1) Are you able to provide an explicit size via `frame` or do you not know what size it should be? 2) Is there a reason that you *have* to use a `UIViewController` and not sure `UIView*? – jnpdx Apr 24 '21 at 02:50
  • @jnpdx I really don't want to provide a fixed frame, as I will be hosting SwiftUI views with dynamic heights inside the `UIViewController`. I could probably calculate the frame and set it dynamically, but that would cause all sorts of problems. And yes - I do need to use a `UIViewController`. My real goal is to use a `UIMenuController` to provide actions like copy. I tried using the `UIMenuController` with a `UIView`, and that didn't work. I tried using my custom view controller with a `UIViewRepresentable`, and that didn't work either (same result as with the `UIViewControllerRepresentable`). – Timmy Apr 24 '21 at 15:40
  • @Timmy I think you're in for an uphill battle. A UIViewController doesn't have a defined frame size, so you'd need to figure out what the size of the elements inside are and then constrain the frame to that size. – jnpdx Apr 24 '21 at 16:18
  • @jnpdx thanks for the help. That's what I did in my answer I posted below. UIKit is still a little unknown to me, so this was very confusing. – Timmy Apr 25 '21 at 00:17

4 Answers4

6

If anyone else is trying to find an easier solution, that takes any view controller and resizes to fit its content:

struct ViewControllerContainer: UIViewControllerRepresentable {
    let content: UIViewController
    
    init(_ content: UIViewController) {
        self.content = content
    }
        
    func makeUIViewController(context: Context) -> UIViewController {
        let size = content.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        content.preferredContentSize = size
        return content
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}

And then, when you use it in SwiftUI, make sure to call .fixedSize():

struct MainView: View {
    var body: some View {
        VStack(spacing: 0) {
 
            ViewControllerContainer(MenuViewController())
                .fixedSize()
 
        }
    }
}
Casper Zandbergen
  • 3,419
  • 2
  • 25
  • 49
Edudjr
  • 1,676
  • 18
  • 28
  • bonkers. I thought `preferredContentSize` was specific for popover layout. but this fixed our layout issues! Thanks – markturnip Oct 18 '22 at 00:02
4

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

Timmy
  • 166
  • 1
  • 9
3

As @jnpdx mentioned, you need to provide explicit size via frame for the representable to be visible as it's nested in VStack with other View.

If you have a specific reason to use UIViewController, then do provide explicit frame or else create a 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)
            .frame(height: 100.0)
        
        EntityViewItem(copyValue: "copy value", label: {
            Text("Route")
        }, content: {
            HStack(alignment: .center) {
                Text("Random route name")
            }
        })
        .border(Color.blue)
    }
  }
}
udbhateja
  • 948
  • 6
  • 21
-2

For anyone looking for the simplest possible solution, it's a couple of lines in @Edudjr's answer:

let size = content.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
content.preferredContentSize = size

Just add that inside your makeUIViewController!