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

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的布局约束不稳定,需要做以下修改:

  1. 禁用UIHostingController的安全区域适配
    创建UIHostingController时,直接设置其ignoresSafeArea为true,阻止它自动调整内容偏移来适配安全区域:

    let hostingController = UIHostingController(rootView: content)
    hostingController.ignoresSafeArea = true
    let hostedView = hostingController.view!
    
  2. 替换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内容宽度与自身一致,避免横向滚动或布局错乱。

  3. 配合设置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 11:22:49