Skip to content

Commit

Permalink
Support ScrollViewReader
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Jul 24, 2024
1 parent cb5d796 commit f42f9ea
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 156 deletions.
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,11 @@ Support levels:
<td>🟡</td>
<td>
<details>
<summary><code>GeometryReader</code></summary>
<summary><code>GeometryProxy</code></summary>
<ul>
<li>See <code>GeometryProxy</code></li>
<li><code>var size: CGSize</code></li>
<li><code>func frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRect</code></li>
<li>Only <code>.local</code> and <code>.global</code> coordinate spaces are supported</li>
</ul>
</details>
</td>
Expand All @@ -480,11 +482,9 @@ Support levels:
<td>🟡</td>
<td>
<details>
<summary><code>GeometryProxy</code></summary>
<summary><code>GeometryReader</code></summary>
<ul>
<li><code>var size: CGSize</code></li>
<li><code>func frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRect</code></li>
<li>Only <code>.local</code> and <code>.global</code> coordinate spaces are supported</li>
<li>See <code>GeometryProxy</code></li>
</ul>
</details>
</td>
Expand Down Expand Up @@ -681,6 +681,29 @@ Support levels:
<td>✅</td>
<td><code>ScrollView</code> (<a href="https://skip.tools/docs/components/frame/">example</a>)</td>
</tr>
<tr>
<td>🟡</td>
<td>
<details>
<summary><code>ScrollViewProxy</code></summary>
<ul>
<li>Works only for <code>List</code> and lazy containers: <code>LazyVStack</code>, <code>LazyHStack</code>, <code>LazyVGrid</code>, <code>LazyHGrid</code></li>
<li><code>UnitRect</code> parameter is ignored</li>
</ul>
</details>
</td>
</tr>
<tr>
<td>🟡</td>
<td>
<details>
<summary><code>ScrollViewReader</code></summary>
<ul>
<li>See <code>ScrollViewProxy</code></li>
</ul>
</details>
</td>
</tr>
<tr>
<td>🟢</td>
<td>
Expand Down
7 changes: 7 additions & 0 deletions Sources/SkipUI/SkipUI/Animation/Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result>(_ 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,
Expand Down
18 changes: 14 additions & 4 deletions Sources/SkipUI/SkipUI/Containers/ForEach.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<objectsBinding.wrappedValue.count {
let contentViews = collectViews(from: objectsBindingContent!(objectsBinding, i), context: appendingContext)
let objects = objectsBinding.wrappedValue
for i in 0..<objects.count {
var contentViews = collectViews(from: objectsBindingContent!(objectsBinding, i), 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(objects[i]), context: appendingContext)
}
contentViews.forEach { $0.Compose(appendingContext) }
}
}
Expand Down
31 changes: 26 additions & 5 deletions Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.LazyHorizontalGrid
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
Expand Down Expand Up @@ -44,10 +49,26 @@ public struct LazyHGrid: 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
LazyHorizontalGrid(modifier: modifier, rows: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, userScrollEnabled: isScrollEnabled) {
factoryContext.initialize(
// Integrate with our scroll-to-top and ScrollViewReader
let gridState = rememberLazyGridState()
let coroutineScope = rememberCoroutineScope()
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)

LazyHorizontalGrid(state: gridState, modifier: modifier, rows: gridCells, horizontalArrangement: horizontalArrangement, verticalArrangement: verticalArrangement, userScrollEnabled: isScrollEnabled) {
factoryContext.value.initialize(
startItemIndex: 0,
item: { view in
item {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
31 changes: 26 additions & 5 deletions Sources/SkipUI/SkipUI/Containers/LazyHStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
73 changes: 57 additions & 16 deletions Sources/SkipUI/SkipUI/Containers/LazySupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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<Any>)?.remove(at: fromIndex - itemIndex) {
(binding.wrappedValue as? RangeReplaceableCollection<Any>)?.insert(element, at: toIndex - itemIndex)
Expand Down Expand Up @@ -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
}
Expand All @@ -241,9 +282,9 @@ public final class LazyItemFactoryContext {
}

private enum Content {
case items(Int, ((IndexSet, Int) -> Void)?)
case objectItems(RandomAccessCollection<Any>, ((IndexSet, Int) -> Void)?)
case objectBindingItems(Binding<RandomAccessCollection<Any>>, ((IndexSet, Int) -> Void)?)
case items(Int, Int, ((Int) -> AnyHashable?)?, ((IndexSet, Int) -> Void)?)
case objectItems(RandomAccessCollection<Any>, (Any) -> AnyHashable?, ((IndexSet, Int) -> Void)?)
case objectBindingItems(Binding<RandomAccessCollection<Any>>, (Any) -> AnyHashable?, ((IndexSet, Int) -> Void)?)
case sectionHeader
case sectionFooter
}
Expand Down
Loading

0 comments on commit f42f9ea

Please sign in to comment.