UIViewControllerRepresentable 未正确占用 space

UIViewControllerRepresentable not correctly taking up space

我正在尝试在 SwiftUI 视图中使用自定义 UIViewController。我设置了一个 UIViewControllerRepresentable class,它在 makeUIViewController 方法中创建了 UIViewController。这将创建 UIViewController 并显示按钮,但是 UIViewControllerRepresentable 不会占用任何 space.

我尝试使用 UIImagePickerController 而不是我的自定义控制器,并且大小正确。我让我的控制器占用 space 的唯一方法是在我的 SwiftUI 视图中的 UIViewControllerRepresentable 上设置一个固定框架,我绝对不想这样做。

注意:我确实需要使用 UIViewController,因为我正尝试在 SwiftUI 中实现 UIMenuController。除了我遇到的大小调整不正确的问题外,我所有这些都可以正常工作。

这是我的代码:

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

我的 SwiftUI 视图:

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

截图:

我对 UIKit 没有太多经验 - 我唯一的经验是编写 UIKit 视图以在 SwiftUI 中使用。这个问题很可能与我缺乏 UIKit 知识有关。

提前致谢!

编辑:

这是 EntityViewItem 的代码。我还将提供 ClientView 所在的容器视图 - EntityView.

我还清理了其余代码并将对 Entity 的引用替换为硬编码值。

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

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

正如@jnpdx 提到的,您需要通过框架提供明确的大小,以便可表示对象可见,因为它与其他视图一起嵌套在 VStack 中。

如果您有使用 UIViewController 的特定原因,请提供明确的框架或创建 SwiftUI 视图。

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 和@jnpdx 的帮助。这就解释了为什么 UIViewControllerRepresentableScrollView 中压缩它的框架。我确实最终找到了解决我的问题的方法,其中涉及在 UIViewControllerRepresentable 上设置固定高度。基本上,我使用 PreferenceKey 找到 SwiftUI 视图的高度,并设置 UIViewControllerRepresentable 的框架来匹配它。

如果有人遇到同样的问题,这是我的代码:

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

以及 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()
    }
}

完成后,我的 UIMenuController 就可以正常使用了。这是很多代码(如果 SwiftUI 中存在此功能,我可能不得不编写 5 行代码),但它运行良好。如果有人想要代码,请评论,我会分享。

这是最终产品的图片:

如果其他人正在尝试寻找更简单的解决方案,那么可以使用任何视图控制器并调整大小以适合其内容:

struct ViewControllerContainer: UIViewControllerRepresentable {
    let content: UIViewController
    
    class Coordinator: NSObject {
        var parent: ViewControllerContainer
        
        init(_ pageViewController: ViewControllerContainer) {
            self.parent = pageViewController
        }
    }

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

然后,当您在 SwiftUI 中使用它时,请务必调用 .fixedSize():

struct MainView: View {
    var body: some View {
        VStack(spacing: 0) {
 
            ViewControllerContainer(MenuViewController())
                .fixedSize()
 
        }
    }
}

对于寻找最简单解决方案的人来说,@Edudjr 的回答中有几行:

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

只需将其添加到您的 makeUIViewController 中即可!