Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting content offset of List in SwiftUI

I'm trying to create a parallax header effect in SwiftUI like this one https://twitter.com/KhaosT/status/1140814602017464320 , but don't really know how to get content offset of the list while scrolling.

Does anyone have know how to calculate the content offset of the list while scrolling?

like image 582
Peacemoon Avatar asked Jun 23 '19 17:06

Peacemoon


1 Answers

I provided a reusable solution for the ScrollView case on this answer which makes use of View Preferences as a method to notify layout information upstream in the View Hierarchy.

For a detail explanation of how View Preferences work, I will suggest reading this 3 articles series on the topic by kontiki

Unfortunately, such solution does not work for List (perhaps a bug) as the View Preferences get trapped inside the List and are not visible by its ancestors.

The only feasible solution so far is to observe the frame changes on the views inside the list. You could achieve this in two ways:

You can report and listen to layout changes on every view (cell) on the list (and act on it):

struct TestView1: View {
    var body: some View {
        GeometryReader { geometry in
            List(TestEnum.allCases) { listValue in
                Text(listValue.id)
                    .padding(60)
                    .transformAnchorPreference(key: MyKey.self, value: .bounds) {
                        $0.append(MyFrame(id: listValue.id, frame: geometry[$1]))
                    }
                    .onPreferenceChange(MyKey.self) {
                        print($0)
                        // Handle content frame changes here
                    }
            }
        }
    }
}

or, alternatively, report and listen to frame changes on some table header view (or an empty header) if you do not need the frame changes on every cell:

struct TestView2: View {
    var body: some View {
        GeometryReader { geometry in
            List {
                Text("")
                    .transformAnchorPreference(key: MyKey.self, value: .bounds) {
                        $0.append(MyFrame(id: "tableTopCell", frame: geometry[$1]))
                    }
                    .onPreferenceChange(MyKey.self) {
                        print($0)
                        // Handle top view frame changes here. 
                        // This only gets reported as long as this 
                        // top view is part of the content. This could be
                        // removed when not visible by the List internals.
                    }

                ForEach(TestEnum.allCases) {
                    Text($0.rawValue)
                        .padding(60)
                }
            }
        }
    }
}

Find below the supporting code for the solutions above: PreferenceKey conforming struct, an identifiable view frame struct and a test enum as data source:

struct MyFrame : Equatable {
    let id : String
    let frame : CGRect

    static func == (lhs: MyFrame, rhs: MyFrame) -> Bool {
        lhs.id == rhs.id && lhs.frame == rhs.frame
    }
}

struct MyKey : PreferenceKey {
    typealias Value = [MyFrame] // The list of view frame changes in a View tree.

    static var defaultValue: [MyFrame] = []

    /// When traversing the view tree, Swift UI will use this function to collect all view frame changes.
    static func reduce(value: inout [MyFrame], nextValue: () -> [MyFrame]) {
        value.append(contentsOf: nextValue())
    }
}

enum TestEnum : String, CaseIterable, Identifiable {
    case one, two, three, four, five, six, seven, eight, nine, ten

    var id: String {
        rawValue
    }
}
like image 90
alfhern Avatar answered Oct 22 '22 01:10

alfhern