0

I am new to SwiftUI and I want to recreate the Contact-Card View from the Contacts App. I am struggling to resize the Image on the top smoothly when scrolling in the List below.

I have tried using GeometryReader, but ran into issues there. When scrolling up for example, the picture size just jumps abruptly to the minimumPictureSize I have specified. The opposite happens when scrolling up: It stops resizing abruptly when I stop scrolling.

Wanted behaviour: https://gifyu.com/image/Ai04

Current behaviour: 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, min(maxPictureSize, 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)
    }
}
de.
  • 7,068
  • 3
  • 40
  • 69

1 Answers1

0

Not a perfect solution, but you could separate the header and List into 2 layers in a ZStack:

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, min(maxPictureSize, 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)
    }
}

Notice that Color.clear.frame(height: 100) and .padding(.top, startOffset-100) are intended to leave extra space for List to avoid being clipped, which will cause the scroll bar get clipped. Alternatively, UIScrollView.appearance().clipsToBounds = true will work. However, it'll make element which moves outside the bounds of List disappear. Don't know if it's a bug.

Toto
  • 570
  • 4
  • 13
  • Unfortunately a lot of your code doesn't compile in my Project. I am using Swift 5 and I have iOS 14.0 as Deployment target. – mathisfoxius Jul 01 '21 at 17:29
  • @mathisfoxius Sorry for the inconvenience. Here is the [iOS 14.0 version](https://gist.github.com/toto-minai/af2a65c3ec88177f723a55059f880b82). `.fill()`, `Color(uiColor:)`, `header` for `Section` and using curly brackets for `.overlay` caused the trouble. – Toto Jul 01 '21 at 18:52
  • Thanks, this compiles:) Unfortunately, it doesn't really solve the issues. When trying out your code, the header got stuck being small at some point. Currently I am thinking that using Gestures might help solve the issues – mathisfoxius Jul 01 '21 at 20:33