You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:SwiftUI视图包裹进UIKit视图时保持布局一致的问题(含ScrollView与宽高比场景适配)

求助: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 08:22:59