SwiftUI中底部弹出的封装UIScrollView内容偏移问题
我在从屏幕底部弹出的弹窗视图中使用了封装有自定义内容的UIScrollView,但每次执行过渡动画时,UIScrollView内的内容在进入屏幕顶部或底部安全区域时会出现位置偏移。该问题出现在UIView从屏幕底部开始动画的初始阶段,恰好处于设备安全区域位置,推测与UIView内容和设备垂直安全区域的交互逻辑有关。
可通过以下状态观察问题:
- 正常位置:内容居中显示,无偏移
- 偏移位置:内容向上/向下偏移,偏离预期布局位置
问题复现
以下是简化后的复现代码,为保持简洁未使用协调器或额外UIScrollView设置。我尝试了在UIView上添加ignoreSafeArea()修饰符、固定UIScrollView内容高度、将contentInsetAdjustmentBehavior设为.never等方法,但均无法解决问题。
测试环境:XCode 14.2,iOS模拟器iPhone 13 Pro(iOS 16.2)
struct ContentView: View { @State private var isUIViewShowed: Bool = true var body: some View { VStack { Button("Toggle the UIView", action: { withAnimation(.linear(duration: 4)){ self.isUIViewShowed.toggle() } }) .frame(height: 50) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.gray.opacity(0.4)) // UIView popup .overlay(alignment: .top, content: { if isUIViewShowed { myUIView .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .transition(.move(edge: .bottom)) // doesn't help // .ignoresSafeArea() } }) } private var myUIView: some View { MyUIView(content: { Text("Some content here") // doesn't help // .edgesIgnoringSafeArea(.vertical) // doesn't help // .ignoresSafeArea() }) .frame(height: 100, alignment: .center) } } fileprivate struct MyUIView<Content: View> : UIViewRepresentable { private let content: Content init( @ViewBuilder content: @escaping ()->Content) { self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() let hostedView = UIHostingController(rootView: content).view! hostedView.translatesAutoresizingMaskIntoConstraints = true hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight] hostedView.frame = scrollView.bounds // setting specific frame size doesn't help // hostedView.frame = CGRect(x: 0, y: 0, width: 100, height: 100) // doesn't help either // scrollView.contentInsetAdjustmentBehavior = .never // doesn't help either // scrollView.insetsLayoutMarginsFromSafeArea = false scrollView.addSubview(hostedView) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} // I don't use coordinator here to keep the code as simple as possible func makeCoordinator() -> Coordinator {} }
请问我遗漏了哪些关键设置?有没有更优的SwiftUI自定义UIView实现方式可以避免该问题?
一、关键遗漏的设置
问题核心在于UIHostingController的安全区域自动适配逻辑,以及动画过程中UIScrollView的布局约束不稳定,需要做以下修改:
禁用UIHostingController的安全区域适配
创建UIHostingController时,直接设置其ignoresSafeArea为true,阻止它自动调整内容偏移来适配安全区域:let hostingController = UIHostingController(rootView: content) hostingController.ignoresSafeArea = true let hostedView = hostingController.view!替换AutoResizingMask为明确的Auto Layout约束
你当前使用autoresizingMask的方式在动画过程中容易出现布局偏移,改用Auto Layout约束明确绑定子视图与scrollView的关系:hostedView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ hostedView.topAnchor.constraint(equalTo: scrollView.topAnchor), hostedView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), hostedView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), hostedView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), hostedView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ])绑定宽度是为了确保scrollView内容宽度与自身一致,避免横向滚动或布局错乱。
配合设置UIScrollView忽略安全区域调整
现在再添加contentInsetAdjustmentBehavior = .never,就能彻底关闭scrollView的安全区域自动调整:scrollView.contentInsetAdjustmentBehavior = .never
二、更优的SwiftUI实现方式
在SwiftUI中,完全可以不用封装UIKit的UIScrollView,直接使用原生ScrollView组件,配合过渡动画就能实现相同效果,还能避免UIKit与SwiftUI的布局冲突:
struct ContentView: View { @State private var isUIViewShowed: Bool = true var body: some View { VStack { Button("Toggle the UIView", action: { withAnimation(.linear(duration: 4)){ isUIViewShowed.toggle() } }) .frame(height: 50) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.gray.opacity(0.4)) .overlay(alignment: .top, content: { if isUIViewShowed { myScrollView .frame(height: 100) .transition(.move(edge: .bottom)) } }) } private var myScrollView: some View { ScrollView { Text("Some content here") .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity) .ignoresSafeArea() } }
这种方式基于SwiftUI原生组件,无需处理跨框架的布局适配问题,动画和安全区域逻辑会自动处理,代码更简洁易维护。
内容的提问来源于stack exchange,提问作者Hollycene

