SwiftUI:滚动视图中的顶部锚定可调整大小视图

Posted

技术标签:

【中文标题】SwiftUI:滚动视图中的顶部锚定可调整大小视图【英文标题】:SwiftUI: top-anchoring resizable views in a scroll view 【发布时间】:2019-10-04 20:24:55 【问题描述】:

如何将滚动视图中调整大小的视图锚定到顶部引导点,以便它们在增长时向下增长?

上下文:我正在尝试制作视图的滚动视图,我可以通过上下拖动底部的手柄来调整高度。我的问题是,当它调整大小时,它的大小同样会向上和向下调整。我希望顶部保持不动,只调整它向下的距离。

我认为问题不在于滚动视图,因为如果我用 VStack 替换它,行为是相同的。但是,在滚动视图的上下文中,它向上调整大小使用户无法向上滚动足够远以看到视图的顶部。

完整的示例代码如下截图。问题出在 iPad 和 iPhone 模拟器上

在以下屏幕截图中,滚动视图都滚动到顶部。第一个屏幕截图显示了调整最顶部项目大小之前的开始状态

第二个屏幕截图显示了调整最顶部项目的大小后的状态 - 最顶部的项目现在超出了列表,因此我们无法看到向上滚动以查看顶部

下面是完整的代码,可以用 Xcode 11.0 运行,以显示问题

struct ScaleItem: View 

    static let defaultHeight: CGFloat = 240.0

    @State var heightDiff: CGFloat = 0.0
    @State var currentHeight: CGFloat = ScaleItem.defaultHeight

    var resizingButton: some View 
        VStack 
            VStack 
                Spacer(minLength: 15)
                HStack 
                    Spacer()
                    Image(systemName: "arrow.up.and.down.square")
                        .background(Color.white)
                    Spacer()
                
            
            Spacer()
                .frame(height: 11)
        
        .background(Color.clear)
    

    var body: some View 

        ZStack 
            VStack 
                Spacer()
                HStack 
                    Spacer()
                    Text("Sample")
                    Spacer()
                
                Spacer()
            
            .background(Color.red)
            .overlay(
                RoundedRectangle(cornerRadius: 5.0)
                    .strokeBorder(Color.black, lineWidth: 1.0)
                    .shadow(radius: 3.0)
            )
            .padding()
            .frame(
                minHeight: self.currentHeight + heightDiff,
                idealHeight: self.currentHeight + heightDiff,
                maxHeight: self.currentHeight + heightDiff,
                alignment: .top
            )

            resizingButton
                .gesture(
                    DragGesture()
                        .onChanged( gesture in
                            print("Changed")
                            let location = gesture.location
                            let startLocation = gesture.startLocation
                            let deltaY = location.y - startLocation.y
                            self.heightDiff = deltaY
                            print(deltaY)
                        )
                    .onEnded  gesture in
                        print("Ended")
                        let location = gesture.location
                        let startLocation = gesture.startLocation
                        let deltaY = location.y - startLocation.y
                        self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
                        self.heightDiff = 0
                        print(deltaY)
                        print(String(describing: gesture))
                    )

        

    


struct ScaleDemoView: View 

    var body: some View 

        ScrollView 
            ForEach(0..<3)  _ in
                ScaleItem()
            
        
    


【问题讨论】:

【参考方案1】:

解决此问题的一种方法是在拖动过程中将 ScrollView 重新绘制到其原始位置。创建一个观察者对象,当您的 DeltaY 发生更改时,将通知父视图并相应地更新视图。

首先,创建一个 ObservableObject final class DeltaHeight: ObservableObject 并传递给子视图。 将.offset(x: 0, y: 0) 添加到您的ScrollView

这里是测试代码:

import SwiftUI

struct ScaleDemoView_Previews: PreviewProvider 
    static var previews: some View 
        ScaleDemoView()
    


struct ScaleItem: View 
    @ObservedObject var delta: DeltaHeight

    static let defaultHeight: CGFloat = 200.0

    @State var heightDiff: CGFloat = 0.0
    @State var currentHeight: CGFloat = ScaleItem.defaultHeight

    var resizingButton: some View 
        VStack 
            VStack 
                Spacer(minLength: 15)
                HStack 
                    Spacer()
                    Image(systemName: "arrow.up.and.down.square")
                        .background(Color.white)
                    Spacer()
                
            
            Spacer()
                .frame(height: 11)
        
        .background(Color.clear)
    

    var body: some View 

        ZStack 
            VStack 
                Spacer()
                HStack 
                    Spacer()
                    Text("Sample")
                    Spacer()
                
                Spacer()
            
            .background(Color.red)
            .overlay(
                RoundedRectangle(cornerRadius: 5.0)
                    .strokeBorder(Color.black, lineWidth: 1.0)
                    .shadow(radius: 3.0)
            )
            .padding()
            .frame(
                minHeight: self.currentHeight + heightDiff,
                idealHeight: self.currentHeight + heightDiff,
                maxHeight: self.currentHeight + heightDiff,
                alignment: .top
            )

            resizingButton
                .gesture(
                    DragGesture()
                        .onChanged( gesture in
                            print("Changed")
                            let location = gesture.location
                            let startLocation = gesture.startLocation
                            let deltaY = location.y - startLocation.y
                            self.heightDiff = deltaY
                            print("deltaY: ", deltaY)
                            self.delta.delta = deltaY
                        )
                    .onEnded  gesture in
                        print("Ended")
                        let location = gesture.location
                        let startLocation = gesture.startLocation
                        let deltaY = location.y - startLocation.y
                        self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
                        self.heightDiff = 0
                        print(deltaY)
                        print(String(describing: gesture))
                    )

        

    


struct ScaleDemoView: View 
    @ObservedObject var delta = DeltaHeight()
    var body: some View 
        ScrollView(.vertical) 
            ForEach(0..<3)  _ in
                ScaleItem(delta: self.delta)
            
        .offset(x: 0, y: 0)
            .background(Color.green)
    



final class DeltaHeight: ObservableObject 
    @Published  var delta: CGFloat = 0.0

【讨论】:

谢谢! :-) 这样可行。 :-) 我唯一需要添加的就是在 onEnded 中更新 self.delta.delta,否则当它被调整为小于 ScaleItem.defaultHeight 时会出现类似的跳跃。非常感谢你的帮助! :-) 我猜想在 scrollView 后面,ScrollView 就像 reloadRows tableViewCell 一样工作,当你改变高度时,视图会根据用户的最佳可见性而增长。

以上是关于SwiftUI:滚动视图中的顶部锚定可调整大小视图的主要内容,如果未能解决你的问题,请参考以下文章

如何向 swiftUI ScrollView 指示内容大小已更改?

自动布局和可调整大小视图的问题

如何在滚动时更改 UIView 大小并延迟

如何锚定 UIScrollView 的子视图,如移动 Safari 中的 URL 栏?

可调整大小视图的圆角

如何使节标题锚定到多节表格视图的顶部?