在 SwiftUI 中从屏幕底部进行过渡时,包装的 UIScrollView 中的内容偏移问题。

huangapple go评论80阅读模式
英文:

Content offset issue in wrapped UIScrollView during transition from the bottom of the screen in SwiftUI

问题

Description

我正在使用一个自定义的 UIScrollView,在从屏幕底部过渡的弹出视图中包含自定义内容。然而,每次执行动画时,在过渡期间,UIScrollView 中的内容似乎会偏移其位置,当它到达屏幕的顶部或底部安全区域时。

问题发生在动画开始时,当 UIView 从屏幕底部过渡时,正好在设备的安全区域区域。因此,我认为问题与 UIView 的内容如何与设备的垂直安全区域互动有关。

您可以在以下图片中观察到这个问题:

在 SwiftUI 中从屏幕底部进行过渡时,包装的 UIScrollView 中的内容偏移问题。
在 SwiftUI 中从屏幕底部进行过渡时,包装的 UIScrollView 中的内容偏移问题。

复制问题

问题在下面的示例中得到了简化。为了尽可能保持代码简单,我在下面的示例中没有使用任何协调器或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("切换 UIView", action: {
                withAnimation(.linear(duration: 4)){
                    self.isUIViewShowed.toggle()
                }
            })
            .frame(height: 50)
            
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.gray.opacity(0.4))
        
        // UIView 弹出视图
        .overlay(alignment: .top, content: {
            if isUIViewShowed {
                myUIView
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .transition(.move(edge: .bottom))
                    // 没有帮助
                    // .ignoresSafeArea()
            }
        })
    }
    
    private var myUIView: some View {
        MyUIView(content: {
            Text("这里有一些内容")
            // 没有帮助
            // .edgesIgnoringSafeArea(.vertical)
            // 没有帮助
            // .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
        
        // 设置特定的frame大小也没有帮助
        // hostedView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        
        // 也没有帮助
        // scrollView.contentInsetAdjustmentBehavior = .never
        
        // 也没有帮助
        // scrollView.insetsLayoutMarginsFromSafeArea = false
        
        scrollView.addSubview(hostedView)
        
        return scrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {}
    
    // 我在这里不使用协调器,以尽可能保持代码简单
    func makeCoordinator() -> Coordinator {}
    
}

我在这里错过了什么?是否有更好的方法来定义SwiftUI中的自定义UIView,以防止这种行为?

英文:

Description

I am using a custom UIScrollView with custom content in a popup view that transitions from the bottom of the screen. However, each time it animates, the content within the UIScrollView appears to offset its position when reaching the top or bottom safe area of the screen during the transition.

The issue occurs at the beginning of the animation when the UIView transitions from the bottom of the screen, right in the area of the device's safe area. Therefore, I assume the problem is related to how the UIView's content interacts with the vertical safe areas of the device.

You can observe the issue in these images:

在 SwiftUI 中从屏幕底部进行过渡时,包装的 UIScrollView 中的内容偏移问题。
在 SwiftUI 中从屏幕底部进行过渡时,包装的 UIScrollView 中的内容偏移问题。

Replication of the problem

The problem is simplified in the example below. To keep the code as simple as possible, I am not using any coordinators or additional settings for the UIScrollView in the example below.

I tried different approaches like setting the ignoreSafeArea() modifiers on UIView, setting the fixed height of the content of the UIScrollView or setting the contentInsetAdjustmentBehavior to .never for the UIScrollView but nothing solves the issue.

Testd on: XCode Version 14.2, iOS Simulator iPhone 13 Pro with iOS 16.2

struct ContentView: View {
    
    @State private var isUIViewShowed: Bool = true
    
    var body: some View {
        VStack {
            Button(&quot;Toggle the UIView&quot;, 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&#39;t help
                    // .ignoresSafeArea()
            }
        })
    }
    
    private var myUIView: some View {
        MyUIView(content: {
            Text(&quot;Some content here&quot;)
            // doesn&#39;t help
            // .edgesIgnoringSafeArea(.vertical)
            // doesn&#39;t help
            // .ignoresSafeArea()
        })
        .frame(height: 100, alignment: .center)
    }
    
}


fileprivate struct MyUIView&lt;Content: View&gt; : UIViewRepresentable {
    
    private let content: Content
    
    init( @ViewBuilder content: @escaping ()-&gt;Content) {
        self.content = content()
    }
    
    func makeUIView(context: Context) -&gt; 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&#39;t help
        // hostedView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        
        // doesn&#39;t help either
        // scrollView.contentInsetAdjustmentBehavior = .never
        
        // doesn&#39;t help either
        // scrollView.insetsLayoutMarginsFromSafeArea = false
        
        scrollView.addSubview(hostedView)
        
        return scrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {}
    
    // I don&#39;t use coordinator here to keep the code as simple as possible
    func makeCoordinator() -&gt; Coordinator {}
    
}

What am I missing here? Is there a better approach to defining a custom UIView in SwiftUI that prevents such a behavior?

答案1

得分: 0

I came up with a solution by defining constraints for the UIView, thanks to this post: https://stackoverflow.com/a/62392498/19954370 .

The entire fixed code looks like this. But please note that this is the bare minimum to demonstrate the problem I faced, and for the completely working UIScrollView, you need to add a custom coordinator and define the contentSize of the scrollView in MakeUIView method.

英文:

I came up with a solution by defining constraints for the UIView, thanks to this post: https://stackoverflow.com/a/62392498/19954370 .

        scrollView.contentInsetAdjustmentBehavior = .never
        hostedView.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            hostedView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostedView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostedView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            hostedView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            hostedView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
        ]
        scrollView.addConstraints(constraints)

The entire fixed code looks like this. But please note that this is the bare minimum to demonstrate the problem I faced, and for the completely working UIScrollView, you need to add a custom coordinator and define the contentSize of the scrollView in MakeUIView method.

struct ContentView: View {
    
    @State private var isUIViewShowed: Bool = true
    
    var body: some View {
        VStack {
            Button(&quot;Toggle the UIView&quot;, 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))
            }
        })
    }
    
    private var myUIView: some View {
        MyUIView(content: {
            Text(&quot;Some content here&quot;)
                // added fixed height to the content of the UIView
                .frame(height: 100, alignment: .center)
        })
        .frame(height: 100, alignment: .center)
    }
    
}



fileprivate struct MyUIView&lt;Content: View&gt; : UIViewRepresentable {
    
    private let content: Content
    
    init( @ViewBuilder content: @escaping ()-&gt;Content) {
        self.content = content()
    }
    
    func makeUIView(context: Context) -&gt; UIScrollView {
        
        let scrollView = UIScrollView()
        let hostedView = UIHostingController(rootView: content).view!

        scrollView.addSubview(hostedView)
        
        // adding contraints fixed the shifting behaviour
        scrollView.contentInsetAdjustmentBehavior = .never
        hostedView.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            hostedView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostedView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostedView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            hostedView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            hostedView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
        ]
        scrollView.addConstraints(constraints)
        
        return scrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {}
    
    // I don&#39;t use coordinator here to keep the code as simple as possible
    func makeCoordinator() -&gt; Coordinator {}
    
}

I would appreciate any corrections or better approaches to the answer.

huangapple
  • 本文由 发表于 2023年3月8日 18:50:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/75672074.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定