求助:SwiftUI视图包裹进UIKit视图时保持布局一致的问题(含ScrollView与宽高比场景适配)
我最近在做一个需求:把任意SwiftUI视图包装成UIKit的UIView实例,同时要保证两者的尺寸完全匹配。我一开始尝试用GeometryReader和Anchor Preferences来实现,下面是我最初的代码:
/// PreferenceKey to capture the bounds of the content struct BoundsPreferenceKey: PreferenceKey { typealias Value = Anchor<CGRect>? static var defaultValue: Anchor<CGRect>? = nil static func reduce(value: inout Anchor<CGRect>?, nextValue: () -> Anchor<CGRect>?) { value = value ?? nextValue() } } /// Wraps any SwiftUI `Content` in a `UIHostingController` /// and calls you back with its `.view` so you can use it in UIKit. struct ViewProvider<Content: View>: UIViewRepresentable { let content: Content @Binding var uiView: UIView let bounds: CGRect func makeUIView(context: Context) -> UIView { let host = UIHostingController(rootView: content) host.view.backgroundColor = .clear host.view.bounds = bounds DispatchQueue.main.async { uiView = host.view } return host.view } func updateUIView(_ uiView: UIView, context: Context) { uiView.bounds = bounds // doesn't have any effect } } struct UIKitWrapperModifier: ViewModifier { @Binding var uiView: UIView @State var bounds = CGRect.zero func body(content: Content) -> some View { ViewProvider( content: contentMeasured(content: content), uiView: $uiView, bounds: bounds ) } @ViewBuilder func contentMeasured(content: Content) -> some View { content .anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { $0 } .backgroundPreferenceValue(BoundsPreferenceKey.self) { anchor in GeometryReader { geometry in if let anchor { print(geometry[anchor]) // prints correct bounds bounds = geometry[anchor] } return Color.clear } } } } extension View { func uiKitWrapped(_ uiView: Binding<UIView>) -> some View { modifier(UIKitWrapperModifier(uiView: uiView)) } } // 测试用的ContentView struct ContentView: View { @State private var sourceUIView = UIView() var body: some View { Color.blue .frame(width: 200, height: 150) .uiKitWrapped($sourceUIView) .background(Color.yellow) } }
但实际运行这个ContentView的时候,发现UIKit的wrapper并没有贴合内部的SwiftUI蓝色视图——黄色背景的wrapper范围比蓝色视图大很多,完全没有匹配子视图的真实尺寸。有没有大佬能帮忙看看问题出在哪?我核心需求就是让UIKit wrapper和内部的SwiftUI子视图尺寸完全一致。
Update 1:后来用了Sweeper的方案,基本能正常工作了(万分感谢!),但又遇到了新的场景问题:我的目标视图是放在ScrollView里面的,而且还加了aspectRatio修饰符,这时候视图的宽度变得特别小(只有10pt,应该是SwiftUI默认的固有尺寸)。调试后发现sizeThatFits方法收到的proposal参数是nil。以下是复现问题的简化视图结构:
struct ContentView: View { @State private var sourceUIView: UIView? var body: some View { ScrollView { Rectangle() .fill(Color.blue) .aspectRatio(2, contentMode: .fit) .uiKitWrapped($sourceUIView) } } }
补充说明:如果我把uiKitWrapped修饰符移到aspectRatio上面,布局就正常了——视图能撑满整个屏幕宽度,但这不符合我的实际代码结构:这个带aspectRatio的视图是嵌套在其他视图里面的,我必须从它的某个父视图上获取被包裹后的UIView实例。
Update 2:最后我修改了Sweeper原方案里的sizeThatFits方法,终于完全解决了问题!修改后的代码如下:
func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIHostingController<Content>, context: Context) -> CGSize? { if let size { return size } else { return proposal.replacingUnspecifiedDimensions(by: CGSize(width: .max, height: .max)) } }
这个修改的逻辑是:如果还没有确定的size,就把proposal里未指定的维度替换成最大可用尺寸。当然这里有个小注意点:如果视图本身没有设置aspectRatio或者其他尺寸约束,可能会导致视图无限拉伸变高,但我的场景里是有aspectRatio限制的,所以完全没问题,现在ScrollView里的带比例视图也能正常撑满宽度,同时UIKit wrapper的尺寸也和SwiftUI视图完全匹配了。
内容来源于stack exchange

