SwiftUI ScrollView с закрытием обновления фрейма контента - PullRequest
0 голосов
/ 16 октября 2019

Я хочу иметь ScrollView, где вы можете быть в курсе изменений фрейма контента при прокрутке пользователя (аналогично didScroll делегат в UIKit UIScrollView).

С этим вы можетезатем выполните изменения макета в зависимости от поведения прокрутки.

1 Ответ

0 голосов
/ 16 октября 2019

Мне удалось найти хорошее решение этой проблемы, используя Просмотр предпочтений в качестве метода уведомления о расположении вверх по течению в иерархии просмотра.

Для очень подробного объясненияо том, как Просмотр настроек работает, я предлагаю прочитать эту 3 серию статей по теме kontiki

Для моего решения я реализовал дваViewModifiers: один для создания отчета об изменениях в его макете с использованием предпочтений привязки , а второй - для View для обработки обновлений кадров в представлениях на его поддереве.

Для этого мы сначала определяем Struct для передачи идентифицируемой информации кадра в восходящем направлении:

/// Represents the `frame` of an identifiable view as an `Anchor`
struct ViewFrame: Equatable {

    /// A given identifier for the View to faciliate processing
    /// of frame updates
    let viewId : String


    /// An `Anchor` representation of the View
    let frameAnchor: Anchor<CGRect>

    // Conformace to Equatable is required for supporting
    // view udpates via `PreferenceKey`
    static func == (lhs: ViewFrame, rhs: ViewFrame) -> Bool {
        // Since we can currently not compare `Anchor<CGRect>` values
        // without a Geometry reader, we return here `false` so that on
        // every change on bounds an update is issued.
        return false
    }
}

и определяем Struct в соответствии с протоколом PreferenceKey для хранения изменений предпочтений дерева представлений:

/// A `PreferenceKey` to provide View frame updates in a View tree
struct FramePreferenceKey: PreferenceKey {
    typealias Value = [ViewFrame] // The list of view frame changes in a View tree.

    static var defaultValue: [ViewFrame] = []

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

Теперь мы можем определить ViewModifiers, о котором я говорил:

Сделать отчет об изменениях в своем макете :

Это просто добавляетtransformAnchorPreference модификатор View с ручкойr, который просто создает экземпляр ViewFrame с текущим значением кадра Anchor и добавляет его к текущему значению FramePreferenceKey:

/// Adds an Anchor preference to notify of frame changes
struct ProvideFrameChanges: ViewModifier {
    var viewId : String

    func body(content: Content) -> some View {
        content
            .transformAnchorPreference(key: FramePreferenceKey.self, value: .bounds) {
                $0.append(ViewFrame(viewId: self.viewId, frameAnchor: $1))
            }
    }
}

extension View {

    /// Adds an Anchor preference to notify of frame changes
    /// - Parameter viewId: A `String` identifying the View
    func provideFrameChanges(viewId : String) -> some View {
        ModifiedContent(content: self, modifier: ProvideFrameChanges(viewId: viewId))
    }
}

Предоставляет обработчик обновления для представления кадраизменения в его поддереве:

Это добавляет модификатор onPreferenceChange к представлению, где список изменений привязок кадров преобразуется в кадры (CGRect) в координатном пространстве представления и отображается каксловарь обновлений фреймов, основанный на идентификаторах вида:

typealias ViewTreeFrameChanges = [String : CGRect]

/// Provides a block to handle internal View tree frame changes
/// for views using the `ProvideFrameChanges` in own coordinate space.
struct HandleViewTreeFrameChanges: ViewModifier {
    /// The handler to process Frame changes on this views subtree.
    /// `ViewTreeFrameChanges` is a dictionary where keys are string view ids
    /// and values are the updated view frame (`CGRect`)
    var handler : (ViewTreeFrameChanges)->Void

    func body(content: Content) -> some View {
        GeometryReader { contentGeometry in
            content
                .onPreferenceChange(FramePreferenceKey.self) {
                    self._updateViewTreeLayoutChanges($0, in: contentGeometry)
                }
        }
    }

    private func _updateViewTreeLayoutChanges(_ changes : [ViewFrame], in geometry : GeometryProxy) {
        let pairs = changes.map({ ($0.viewId, geometry[$0.frameAnchor]) })
        handler(Dictionary(uniqueKeysWithValues: pairs))
    }
}

extension View {
    /// Adds an Anchor preference to notify of frame changes
    /// - Parameter viewId: A `String` identifying the View
    func handleViewTreeFrameChanges(_ handler : @escaping (ViewTreeFrameChanges)->Void) -> some View {
        ModifiedContent(content: self, modifier: HandleViewTreeFrameChanges(handler: handler))
    }
}

ДАВАЙТЕ ИСПОЛЬЗОВАТЬ:

Я проиллюстрирую использование на примере:

Здесь я получу уведомления об изменениях кадра Header View внутри ScrollView. Поскольку это представление заголовка находится в верхней части содержимого ScrollView, сообщенные изменения кадра в источнике кадра эквивалентны contentOffset изменениям ScrollView

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

    var id: String {
        rawValue
    }
}

struct TestView: View {
    private let _listHeaderViewId = "testView_ListHeader"

    var body: some View {
        ScrollView {
            // Header View
            Text("This is some Header")
                .provideFrameChanges(viewId: self._listHeaderViewId)

            // List of test values
            ForEach(TestEnum.allCases) {
                Text($0.rawValue)
                    .padding(60)
            }
        }
            .handleViewTreeFrameChanges {
                self._updateViewTreeLayoutChanges($0)
            }
    }

    private func _updateViewTreeLayoutChanges(_ changes : ViewTreeFrameChanges) {
        print(changes)
    }
}
...