From f42f9ea7de5cd8010053d74eaaa6409db41c34f1 Mon Sep 17 00:00:00 2001 From: Abe White Date: Tue, 23 Jul 2024 23:20:25 -0500 Subject: [PATCH] Support ScrollViewReader --- README.md | 35 +++- .../SkipUI/SkipUI/Animation/Animation.swift | 7 + .../SkipUI/SkipUI/Containers/ForEach.swift | 18 +- .../SkipUI/SkipUI/Containers/LazyHGrid.swift | 31 +++- .../SkipUI/SkipUI/Containers/LazyHStack.swift | 31 +++- .../SkipUI/Containers/LazySupport.swift | 73 ++++++-- .../SkipUI/SkipUI/Containers/LazyVGrid.swift | 36 +++- .../SkipUI/SkipUI/Containers/LazyVStack.swift | 36 +++- Sources/SkipUI/SkipUI/Containers/List.swift | 18 +- .../SkipUI/SkipUI/Containers/ScrollView.swift | 157 ++++++------------ 10 files changed, 286 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index ecfc516f..7646ab22 100644 --- a/README.md +++ b/README.md @@ -469,9 +469,11 @@ Support levels: 🟡
- GeometryReader + GeometryProxy
@@ -480,11 +482,9 @@ Support levels: 🟡
- GeometryProxy + GeometryReader
@@ -681,6 +681,29 @@ Support levels: ✅ ScrollView (example) + + 🟡 + +
+ ScrollViewProxy + +
+ + + + 🟡 + +
+ ScrollViewReader + +
+ + 🟢 diff --git a/Sources/SkipUI/SkipUI/Animation/Animation.swift b/Sources/SkipUI/SkipUI/Animation/Animation.swift index e7583169..e9a77c3a 100644 --- a/Sources/SkipUI/SkipUI/Animation/Animation.swift +++ b/Sources/SkipUI/SkipUI/Animation/Animation.swift @@ -118,6 +118,13 @@ public struct Animation : Hashable, Sendable { return isAnimating ? rememberedAnimation : nil } + /// Whether we're in a `withAnimation` block. + static var isInWithAnimation: Bool { + synchronized (withAnimationLock) { + return _withAnimation != nil + } + } + /// Internal implementation of global `withAnimation` SwiftUI function. static func withAnimation(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result { // SwiftUI's withAnimation works as if by snapshotting the view tree at the beginning of the block, diff --git a/Sources/SkipUI/SkipUI/Containers/ForEach.swift b/Sources/SkipUI/SkipUI/Containers/ForEach.swift index 75eaae12..58c376aa 100644 --- a/Sources/SkipUI/SkipUI/Containers/ForEach.swift +++ b/Sources/SkipUI/SkipUI/Containers/ForEach.swift @@ -105,35 +105,45 @@ public final class ForEach : View, LazyItemFactory { var isFirstView = true if let indexRange { for index in indexRange { - let contentViews = collectViews(from: indexedContent!(index), context: appendingContext) + var contentViews = collectViews(from: indexedContent!(index), context: appendingContext) if !isUnrollRequired(contentViews: contentViews, isFirstView: isFirstView, context: appendingContext) { views.add(self) return ComposeResult.ok } else { isFirstView = false } + if let identifier { + contentViews = taggedViews(for: contentViews, defaultTag: index, context: appendingContext) + } contentViews.forEach { $0.Compose(appendingContext) } } } else if let objects { for object in objects { - let contentViews = collectViews(from: objectContent!(object), context: appendingContext) + var contentViews = collectViews(from: objectContent!(object), context: appendingContext) if !isUnrollRequired(contentViews: contentViews, isFirstView: isFirstView, context: appendingContext) { views.add(self) return ComposeResult.ok } else { isFirstView = false } + if let identifier { + contentViews = taggedViews(for: contentViews, defaultTag: identifier(object), context: appendingContext) + } contentViews.forEach { $0.Compose(appendingContext) } } } else if let objectsBinding { - for i in 0.. Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + gridState.animateScrollToItem(itemIndex) + } else { + gridState.scrollToItem(itemIndex) + } + } + } + } + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + + LazyHorizontalGrid(state: gridState, modifier: modifier, rows: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, userScrollEnabled: isScrollEnabled) { + factoryContext.value.initialize( startItemIndex: 0, item: { view in item { @@ -98,9 +119,9 @@ public struct LazyHGrid: View { ) for view in collectingComposer.views { if let factory = view as? LazyItemFactory { - factory.composeLazyItems(context: factoryContext) + factory.composeLazyItems(context: factoryContext.value) } else { - factoryContext.item(view) + factoryContext.value.item(view) } } } diff --git a/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift b/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift index c7f4344a..faa3843f 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyHStack.swift @@ -7,8 +7,13 @@ #if SKIP import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch #else import struct CoreGraphics.CGFloat #endif @@ -38,10 +43,26 @@ public struct LazyHStack : View { content.Compose(context: viewsCollector) let itemContext = context.content() - let factoryContext = LazyItemFactoryContext() + let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } ComposeContainer(axis: .horizontal, modifier: context.modifier, fillWidth: true, fillHeight: false) { modifier in - LazyRow(modifier: modifier, horizontalArrangement: rowArrangement, verticalAlignment: rowAlignment, userScrollEnabled: isScrollEnabled) { - factoryContext.initialize( + // Integrate with ScrollViewReader + let listState = rememberLazyListState() + let coroutineScope = rememberCoroutineScope() + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + listState.animateScrollToItem(itemIndex) + } else { + listState.scrollToItem(itemIndex) + } + } + } + } + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + + LazyRow(state: listState, modifier: modifier, horizontalArrangement: rowArrangement, verticalAlignment: rowAlignment, userScrollEnabled: isScrollEnabled) { + factoryContext.value.initialize( startItemIndex: 0, item: { view in item { @@ -80,9 +101,9 @@ public struct LazyHStack : View { ) for view in collectingComposer.views { if let factory = view as? LazyItemFactory { - factory.composeLazyItems(context: factoryContext) + factory.composeLazyItems(context: factoryContext.value) } else { - factoryContext.item(view) + factoryContext.value.item(view) } } } diff --git a/Sources/SkipUI/SkipUI/Containers/LazySupport.swift b/Sources/SkipUI/SkipUI/Containers/LazySupport.swift index 69d0972e..fd58460e 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazySupport.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazySupport.swift @@ -54,28 +54,29 @@ public final class LazyItemFactoryContext { self.sectionHeader(EmptyView()) } item(view) - content.append(.items(1, nil)) + let id = TagModifierView.strip(from: view, role: .id)?.value + content.append(.items(0, 1, { _ in id }, nil)) } self.indexedItems = { range, identifier, onDelete, onMove, factory in if case .sectionFooter = content.last { self.sectionHeader(EmptyView()) } indexedItems(range, identifier, count, onDelete, onMove, factory) - content.append(.items(range.endExclusive - range.start, onMove)) + content.append(.items(range.start, range.endExclusive - range.start, identifier, onMove)) } self.objectItems = { objects, identifier, onDelete, onMove, factory in if case .sectionFooter = content.last { self.sectionHeader(EmptyView()) } objectItems(objects, identifier, count, onDelete, onMove, factory) - content.append(.objectItems(objects, onMove)) + content.append(.objectItems(objects, identifier, onMove)) } self.objectBindingItems = { binding, identifier, editActions, onDelete, onMove, factory in if case .sectionFooter = content.last { self.sectionHeader(EmptyView()) } objectBindingItems(binding, identifier, count, editActions, onDelete, onMove, factory) - content.append(.objectBindingItems(binding, onMove)) + content.append(.objectBindingItems(binding, identifier, onMove)) } self.sectionHeader = { view in // If this is a header after an item, add a section footer before it @@ -99,15 +100,55 @@ public final class LazyItemFactoryContext { var itemCount = 0 for content in self.content { switch content { - case .items(let count, _): itemCount += count - case .objectItems(let objects, _): itemCount += objects.count - case .objectBindingItems(let binding, _): itemCount += binding.wrappedValue.count + case .items(_, let count, _, _): itemCount += count + case .objectItems(let objects, _, _): itemCount += objects.count + case .objectBindingItems(let binding, _, _): itemCount += binding.wrappedValue.count case .sectionHeader, .sectionFooter: itemCount += 1 } } return itemCount } + /// Return the list index for the given item ID, or nil. + func index(for id: Any) -> Int? { + var index = startItemIndex + for content in self.content { + switch content { + case .items(let start, let count, let idMap, _): + for i in start..<(start + count) { + let itemID: Any? + if let idMap { + itemID = idMap(i) + } else { + itemID = i + } + if itemID == id { + return index + } + index += 1 + } + case .objectItems(let objects, let idMap, _): + for object in objects { + let itemID = idMap(object) + if itemID == id { + return index + } + index += 1 + } + case .objectBindingItems(let binding, let idMap, _): + for object in binding.wrappedValue { + let itemID = idMap(object) + if itemID == id { + return index + } + index += 1 + } + case .sectionHeader, .sectionFooter: index += 1 + } + } + return nil + } + private var moving: (fromIndex: Int, toIndex: Int)? private var moveTrigger = 0 @@ -164,15 +205,15 @@ public final class LazyItemFactoryContext { var itemIndex = startItemIndex for content in self.content { switch content { - case .items(let count, let onMove): + case .items(_, let count, _, let onMove): if performMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: count, onMove: onMove) { return } - case .objectItems(let objects, let onMove): + case .objectItems(let objects, _, let onMove): if performMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: objects.count, onMove: onMove) { return } - case .objectBindingItems(let binding, let onMove): + case .objectBindingItems(let binding, _, let onMove): if performMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: binding.wrappedValue.count, onMove: onMove, customMove: { if let element = (binding.wrappedValue as? RangeReplaceableCollection)?.remove(at: fromIndex - itemIndex) { (binding.wrappedValue as? RangeReplaceableCollection)?.insert(element, at: toIndex - itemIndex) @@ -210,15 +251,15 @@ public final class LazyItemFactoryContext { var itemIndex = startItemIndex for content in self.content { switch content { - case .items(let count, _): + case .items(_, let count, _, _): if let ret = canMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: count) { return ret } - case .objectItems(let objects, _): + case .objectItems(let objects, _, _): if let ret = canMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: objects.count) { return ret } - case .objectBindingItems(let binding, _): + case .objectBindingItems(let binding, _, _): if let ret = canMove(fromIndex: fromIndex, toIndex: toIndex, itemIndex: &itemIndex, count: binding.wrappedValue.count) { return ret } @@ -241,9 +282,9 @@ public final class LazyItemFactoryContext { } private enum Content { - case items(Int, ((IndexSet, Int) -> Void)?) - case objectItems(RandomAccessCollection, ((IndexSet, Int) -> Void)?) - case objectBindingItems(Binding>, ((IndexSet, Int) -> Void)?) + case items(Int, Int, ((Int) -> AnyHashable?)?, ((IndexSet, Int) -> Void)?) + case objectItems(RandomAccessCollection, (Any) -> AnyHashable?, ((IndexSet, Int) -> Void)?) + case objectBindingItems(Binding>, (Any) -> AnyHashable?, ((IndexSet, Int) -> Void)?) case sectionHeader case sectionFooter } diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift index 0b11a03b..1cc50438 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift @@ -9,8 +9,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch #else import struct CoreGraphics.CGFloat #endif @@ -44,10 +49,31 @@ public struct LazyVGrid: View { content.Compose(context: viewsCollector) let itemContext = context.content() - let factoryContext = LazyItemFactoryContext() + let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } ComposeContainer(axis: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in - LazyVerticalGrid(modifier: modifier, columns: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, userScrollEnabled: isScrollEnabled) { - factoryContext.initialize( + // Integrate with our scroll-to-top and ScrollViewReader + let gridState = rememberLazyGridState() + let coroutineScope = rememberCoroutineScope() + PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { + coroutineScope.launch { + gridState.animateScrollToItem(0) + } + }) + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + gridState.animateScrollToItem(itemIndex) + } else { + gridState.scrollToItem(itemIndex) + } + } + } + } + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + + LazyVerticalGrid(state: gridState, modifier: modifier, columns: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, userScrollEnabled: isScrollEnabled) { + factoryContext.value.initialize( startItemIndex: 0, item: { view in item { @@ -98,9 +124,9 @@ public struct LazyVGrid: View { ) for view in collectingComposer.views { if let factory = view as? LazyItemFactory { - factory.composeLazyItems(context: factoryContext) + factory.composeLazyItems(context: factoryContext.value) } else { - factoryContext.item(view) + factoryContext.value.item(view) } } } diff --git a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift index e414b116..34733778 100644 --- a/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/LazyVStack.swift @@ -7,8 +7,13 @@ #if SKIP import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch #else import struct CoreGraphics.CGFloat #endif @@ -38,10 +43,31 @@ public struct LazyVStack : View { content.Compose(context: viewsCollector) let itemContext = context.content() - let factoryContext = LazyItemFactoryContext() + let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) } ComposeContainer(axis: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in - LazyColumn(modifier: modifier, verticalArrangement: columnArrangement, horizontalAlignment: columnAlignment, userScrollEnabled: isScrollEnabled) { - factoryContext.initialize( + // Integrate with our scroll-to-top and ScrollViewReader + let listState = rememberLazyListState() + let coroutineScope = rememberCoroutineScope() + PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { + coroutineScope.launch { + listState.animateScrollToItem(0) + } + }) + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + listState.animateScrollToItem(itemIndex) + } else { + listState.scrollToItem(itemIndex) + } + } + } + } + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) + + LazyColumn(state: listState, modifier: modifier, verticalArrangement: columnArrangement, horizontalAlignment: columnAlignment, userScrollEnabled: isScrollEnabled) { + factoryContext.value.initialize( startItemIndex: 0, item: { view in item { @@ -80,9 +106,9 @@ public struct LazyVStack : View { ) for view in collectingComposer.views { if let factory = view as? LazyItemFactory { - factory.composeLazyItems(context: factoryContext) + factory.composeLazyItems(context: factoryContext.value) } else { - factoryContext.item(view) + factoryContext.value.item(view) } } } diff --git a/Sources/SkipUI/SkipUI/Containers/List.swift b/Sources/SkipUI/SkipUI/Containers/List.swift index 72724d0f..1601e31a 100644 --- a/Sources/SkipUI/SkipUI/Containers/List.swift +++ b/Sources/SkipUI/SkipUI/Containers/List.swift @@ -180,13 +180,25 @@ public final class List : View { }) modifier = modifier.reorderable(reorderableState) - // Integrate with our scroll-to-top navigation bar taps + // Integrate with our scroll-to-top and ScrollViewReader let coroutineScope = rememberCoroutineScope() PreferenceValues.shared.contribute(context: context, key: ScrollToTopPreferenceKey.self, value: { coroutineScope.launch { reorderableState.listState.animateScrollToItem(0) } }) + let scrollToID: (Any) -> Void = { id in + if let itemIndex = factoryContext.value.index(for: id) { + coroutineScope.launch { + if Animation.isInWithAnimation { + reorderableState.listState.animateScrollToItem(itemIndex) + } else { + reorderableState.listState.scrollToItem(itemIndex) + } + } + } + } + PreferenceValues.shared.contribute(context: context, key: ScrollToIDPreferenceKey.self, value: scrollToID) LazyColumn(state: reorderableState.listState, modifier: modifier) { let sectionHeaderContext = context.content(composer: RenderingComposer { view, context in ComposeSectionHeader(view: view, context: context(false), styling: styling, isTop: false) @@ -231,9 +243,9 @@ public final class List : View { }, indexedItems: { range, identifier, offset, onDelete, onMove, factory in let count = range.endExclusive - range.start - let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!(factoryContext.value.remapIndex($0, from: offset))) } + let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!(range.start + factoryContext.value.remapIndex($0, from: offset))) } items(count: count, key: key) { index in - let keyValue = key?(index + range.start) // Key closure already remaps index + let keyValue = key?(index) // Key closure already remaps index let index = factoryContext.value.remapIndex(index, from: offset) let itemModifier: Modifier = shouldAnimateItems() ? Modifier.animateItemPlacement() : Modifier let editableItemContext = context.content(composer: RenderingComposer { view, context in diff --git a/Sources/SkipUI/SkipUI/Containers/ScrollView.swift b/Sources/SkipUI/SkipUI/Containers/ScrollView.swift index f23600fa..40cd9f64 100644 --- a/Sources/SkipUI/SkipUI/Containers/ScrollView.swift +++ b/Sources/SkipUI/SkipUI/Containers/ScrollView.swift @@ -19,6 +19,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import kotlinx.coroutines.launch #else @@ -94,6 +96,42 @@ public struct ScrollView : View { #endif } +public struct ScrollViewProxy { + #if SKIP + let scrollToID: (Any) -> Void + #endif + + public func scrollTo(_ id: Any, anchor: UnitPoint? = nil) { + #if SKIP + // Warning: anchor is currently ignored + scrollToID(id) + #endif + } +} + +public struct ScrollViewReader : View where Content : View { + public let content: (ScrollViewProxy) -> Content + + public init(@ViewBuilder content: @escaping (ScrollViewProxy) -> Content) { + self.content = content + } + + #if SKIP + @Composable public override func ComposeContent(context: ComposeContext) { + let scrollToID = rememberSaveable(stateSaver: context.stateSaver as! Saver Void>, Any>) { mutableStateOf(Preference<(Any) -> Void>(key: ScrollToIDPreferenceKey.self)) } + let scrollToIDCollector = PreferenceCollector<(Any) -> Void>(key: ScrollToIDPreferenceKey.self, state: scrollToID) + let scrollProxy = ScrollViewProxy(scrollToID: scrollToID.value.reduced) + PreferenceValues.shared.collectPreferences([scrollToIDCollector]) { + content(scrollProxy).Compose(context) + } + } + #else + public var body: some View { + stubView() + } + #endif +} + #if SKIP struct ScrollToTopPreferenceKey: PreferenceKey { typealias Value = () -> Void @@ -106,6 +144,18 @@ struct ScrollToTopPreferenceKey: PreferenceKey { } } } + +struct ScrollToIDPreferenceKey: PreferenceKey { + typealias Value = (Any) -> Void + + // SKIP DECLARE: companion object: PreferenceKeyCompanion<(Any) -> Unit> + final class Companion: PreferenceKeyCompanion { + let defaultValue: (Any) -> Void = { _ in } + func reduce(value: inout (Any) -> Void, nextValue: () -> (Any) -> Void) { + value = nextValue() + } + } +} #endif public enum ScrollBounceBehavior : Sendable { @@ -664,113 +714,6 @@ extension ScrollTransitionPhase : Equatable { extension ScrollTransitionPhase : Hashable { } -/// A proxy value that supports programmatic scrolling of the scrollable -/// views within a view hierarchy. -/// -/// You don't create instances of `ScrollViewProxy` directly. Instead, your -/// ``ScrollViewReader`` receives an instance of `ScrollViewProxy` in its -/// `content` view builder. You use actions within this view builder, such -/// as button and gesture handlers or the ``View/onChange(of:perform:)`` -/// method, to call the proxy's ``ScrollViewProxy/scrollTo(_:anchor:)`` method. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) -public struct ScrollViewProxy { - - /// Scans all scroll views contained by the proxy for the first - /// with a child view with identifier `id`, and then scrolls to - /// that view. - /// - /// If `anchor` is `nil`, this method finds the container of the identified - /// view, and scrolls the minimum amount to make the identified view - /// wholly visible. - /// - /// If `anchor` is non-`nil`, it defines the points in the identified - /// view and the scroll view to align. For example, setting `anchor` to - /// ``UnitPoint/top`` aligns the top of the identified view to the top of - /// the scroll view. Similarly, setting `anchor` to ``UnitPoint/bottom`` - /// aligns the bottom of the identified view to the bottom of the scroll - /// view, and so on. - /// - /// - Parameters: - /// - id: The identifier of a child view to scroll to. - /// - anchor: The alignment behavior of the scroll action. - public func scrollTo(_ id: ID, anchor: UnitPoint? = nil) where ID : Hashable { fatalError() } -} - -/// A view that provides programmatic scrolling, by working with a proxy -/// to scroll to known child views. -/// -/// The scroll view reader's content view builder receives a ``ScrollViewProxy`` -/// instance; you use the proxy's ``ScrollViewProxy/scrollTo(_:anchor:)`` to -/// perform scrolling. -/// -/// The following example creates a ``ScrollView`` containing 100 views that -/// together display a color gradient. It also contains two buttons, one each -/// at the top and bottom. The top button tells the ``ScrollViewProxy`` to -/// scroll to the bottom button, and vice versa. -/// -/// @Namespace var topID -/// @Namespace var bottomID -/// -/// var body: some View { -/// ScrollViewReader { proxy in -/// ScrollView { -/// Button("Scroll to Bottom") { -/// withAnimation { -/// proxy.scrollTo(bottomID) -/// } -/// } -/// .id(topID) -/// -/// VStack(spacing: 0) { -/// ForEach(0..<100) { i in -/// color(fraction: Double(i) / 100) -/// .frame(height: 32) -/// } -/// } -/// -/// Button("Top") { -/// withAnimation { -/// proxy.scrollTo(topID) -/// } -/// } -/// .id(bottomID) -/// } -/// } -/// } -/// -/// func color(fraction: Double) -> Color { -/// Color(red: fraction, green: 1 - fraction, blue: 0.5) -/// } -/// -/// ![A scroll view, with a button labeled "Scroll to Bottom" at top. -/// Below this, a series of vertically aligned rows, each filled with a -/// color, that are progressing from green to -/// red.](SkipUI-ScrollViewReader-scroll-to-bottom-button.png) -/// -/// > Important: You may not use the ``ScrollViewProxy`` -/// during execution of the `content` view builder; doing so results in a -/// runtime error. Instead, only actions created within `content` can call -/// the proxy, such as gesture handlers or a view's `onChange(of:perform:)` -/// method. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) -@frozen public struct ScrollViewReader : View where Content : View { - - /// The view builder that creates the reader's content. - public var content: (ScrollViewProxy) -> Content { get { fatalError() } } - - /// Creates an instance that can perform programmatic scrolling of its - /// child scroll views. - /// - /// - Parameter content: The reader's content, containing one or more - /// scroll views. This view builder receives a ``ScrollViewProxy`` - /// instance that you use to perform scrolling. - @inlinable public init(@ViewBuilder content: @escaping (ScrollViewProxy) -> Content) { fatalError() } - - @MainActor public var body: some View { get { return stubView() } } - -// public typealias Body = some View -} - /// The scroll behavior that aligns scroll targets to container-based geometry. /// /// In the following example, every view in the lazy stack is flexible