SwiftUI | GeometryReader:滚动列表时平滑可调整大小的标题

问题描述

我是 SwiftUI 的新手,我想从通讯录应用重新创建通讯录卡片视图。 在下面的列表中滚动时,我正在努力调整顶部图像的大小。

我曾尝试使用 GeometryReader,但在那里遇到了问题。 例如,当向上滚动时,图片大小会突然跳到我指定的 minimumPictureSize。向上滚动时会发生相反的情况:当我停止滚动时,它会突然停止调整大小。

想要的行为:https://gifyu.com/image/Ai04

当前行为:https://gifyu.com/image/AjIc

 struct SwiftUIView: View {
    @State var startOffset: CGFloat = 0
    @State var offset: CGFloat = 0
    
    var minPictureSize: CGFloat = 100
    var maxPictureSize: CGFloat = 200
    
    var body: some View {
        vstack {
            Image("person")
                .resizable()
                .frame(width: max(minPictureSize,min(maxPictureSize,minPictureSize + offset)),height: max(minPictureSize,minPictureSize + offset)))
                .mask(Circle())
            Text("startOffset: \(startOffset)")
            Text("offset: \(offset)")
            List {
                Section {
                    Text("Top Section")
                }.overlay(
                    GeometryReader(){ geometry -> Color in
                        let rect = geometry.frame(in: .global)
                        
                        if startOffset == 0 {
                            dispatchQueue.main.async {
                                startOffset = rect.minY
                            }
                        }
                        dispatchQueue.main.async {
                            offset = rect.minY - startOffset
                        }
                        return Color.clear
                    }
                )
                ForEach((0..<10)) { row in
                    Section {
                        Text("\(row)")
                    }
                }
            }.listStyle(InsetGroupedListStyle())
        }.navigationBarHidden(true)
    }
}

解决方法

不是一个完美的解决方案,但您可以将标题和 List 分成 ZStack 中的 2 个层:

struct SwiftUIView: View {
    @State var startOffset: CGFloat!
    @State var offset: CGFloat = 0
    
    let minPictureSize: CGFloat = 100
    let maxPictureSize: CGFloat = 200
    
    var body: some View {
        ZStack(alignment: .top) {
            if startOffset != nil {
                List {
                    Section {
                        Text("Top Section")
                    } header: {
                        // Leave extra space for `List` so it won't clip its content
                        Color.clear.frame(height: 100)
                    }
                    .overlay {
                        GeometryReader { geometry -> Color in
                            DispatchQueue.main.async {
                                let frame = geometry.frame(in: .global)
                                offset = frame.minY - startOffset
                            }
                            
                            return Color.clear
                        }
                    }
                    
                    ForEach((0..<10)) { row in
                        Section {
                            Text("\(row)")
                        }
                    }
                }
                .listStyle(InsetGroupedListStyle())
                .padding(.top,startOffset-100)  // Make up extra space
            }
            
            VStack {
                Circle().fill(.secondary)
                    .frame(width: max(minPictureSize,min(maxPictureSize,minPictureSize + offset)),height: max(minPictureSize,minPictureSize + offset)))
                Text("startOffset: \(startOffset ?? -1)")
                Text("offset: \(offset)")
            }
            .frame(maxWidth: .infinity)
            .padding(.bottom,20)
            .background(Color(uiColor: UIColor.systemBackground))
            .overlay {
                if startOffset == nil {
                    GeometryReader { geometry -> Color in
                        DispatchQueue.main.async {
                            let frame = geometry.frame(in: .global)
                            
                            startOffset = frame.maxY +  // Original small one
                                maxPictureSize - minPictureSize -
                                frame.minY // Top safe area height
                        }
                        
                        return Color.clear
                    }
                }
            }
                
        }
        .navigationBarHidden(true)
    }
}

注意 Color.clear.frame(height: 100).padding(.top,startOffset-100) 的目的是为 List 留出额外的空间,以避免被剪裁,这会导致滚动条被剪裁。或者,UIScrollView.appearance().clipsToBounds = true 将起作用。但是,它会使移动到 List 边界之外的元素消失。不知道是不是bug。