Skip to content

Commit

Permalink
New infinite scroll iOS18
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcHidalgo5 committed Sep 29, 2024
1 parent 9193cad commit 573dfda
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import SwiftUI
@MainActor
open class InfiniteScrollingDataSource<ListItem: Identifiable & Sendable>: ObservableObject {

@Published public private(set) var items = [ListItem]()
@Published public var items = [ListItem]()
@Published public private(set) var state: State
@Published public var paginationError: Error?
private var itemFetcher: ItemFetcher
Expand Down Expand Up @@ -66,17 +66,6 @@ open class InfiniteScrollingDataSource<ListItem: Identifiable & Sendable>: Obser
try await loadMoreContent()
}

/// This is a workaround for paging glitches found on iOS 17 and above.
/// As all workarouds, it's an indicator of a poor design that must be fixed ASAP.
public func ___update(items: [ListItem]? = nil, state: State? = nil) {
if let items {
self.items = items
}
if let state {
self.state = state
}
}

/// MARK: Private

@MainActor
Expand Down
108 changes: 64 additions & 44 deletions Sources/BSWInterfaceKit/SwiftUI/Views/InfiniteVerticalScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import SwiftUI
@State
var items: [Item] = Item.createItems()

@Previewable
@State
var pleaseScrollTo: Item.ID? = nil

NavigationStack {
InfiniteVerticalScrollView(
direction: .downwards,
items: $items,
pleaseScrollTo: $pleaseScrollTo,
nextPageFetcher: { _ in
try await Task.sleep(for: .seconds(2))
return (Item.createItems(), true)
Expand All @@ -35,29 +40,30 @@ import SwiftUI
}
}

@available(iOS 17, macOS 14, *)
@available(iOS 18, macOS 14, *)
public struct InfiniteVerticalScrollView<Item: Identifiable & Sendable, ItemView: View>: View where Item.ID : Sendable {

public init(direction: Direction = .downwards,
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
pinnedViews: PinnedScrollableViews = .init(),
items: Binding<[Item]>,
nextPageFetcher: @escaping NextPageFetcher,
@ViewBuilder itemViewBuilder: @escaping ItemViewBuilder) {
self.alignment = alignment
self.spacing = spacing
self.pinnedViews = pinnedViews
self.direction = direction
self._items = items
self.nextPageFetcher = nextPageFetcher
self.itemViewBuilder = itemViewBuilder
public init(
direction: Direction = .downwards,
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
pinnedViews: PinnedScrollableViews = .init(),
items: Binding<[Item]>,
pleaseScrollTo: Binding<Item.ID?>,
nextPageFetcher: @escaping NextPageFetcher,
@ViewBuilder itemViewBuilder: @escaping ItemViewBuilder) {
self.alignment = alignment
self.spacing = spacing
self.pinnedViews = pinnedViews
self.direction = direction
self._items = items
self._scrollPositionItemID = pleaseScrollTo
self.nextPageFetcher = nextPageFetcher
self.itemViewBuilder = itemViewBuilder
}

public enum Direction {
case downwards

@available(iOS 18, macOS 15, *)
case upwards
}

Expand All @@ -77,12 +83,15 @@ public struct InfiniteVerticalScrollView<Item: Identifiable & Sendable, ItemView
@State
private var phase: Phase = .idle

@State
@Binding
private var scrollPositionItemID: Item.ID?

@State
private var error: Swift.Error?

@State
private var pleaseScrollTo: Item.ID?

enum Phase: Equatable {
case idle
case noMorePages
Expand All @@ -99,30 +108,38 @@ public struct InfiniteVerticalScrollView<Item: Identifiable & Sendable, ItemView
}

public var body: some View {
ScrollView(.vertical) {

if #available(iOS 18, macOS 15, *), direction == .upwards, phase.isPaging {
ProgressView()
}
ScrollViewReader { proxy in
ScrollView(.vertical) {
if direction == .upwards, phase.isPaging {
ProgressView()
}

LazyVStack(alignment: alignment, spacing: spacing, pinnedViews: pinnedViews) {
ForEach(items) { item in
itemViewBuilder(item)
.id(item.id)
LazyVStack(alignment: alignment, spacing: spacing, pinnedViews: pinnedViews) {
ForEach(items) { item in
itemViewBuilder(item)
.id(item.id)
}
}
.scrollTargetLayout()

if direction == .downwards, phase.isPaging {
ProgressView()
}
}
.scrollTargetLayout()

if direction == .downwards, phase.isPaging {
ProgressView()
.onChange(of: pleaseScrollTo) { oldValue, newValue in
if let newValue {
proxy.scrollTo(newValue, anchor: direction == .upwards ? .top : .bottom)
}
self.pleaseScrollTo = nil
}
.scrollPosition(
id: $scrollPositionItemID,
anchor: (direction == .downwards) ? .bottom : .top
)
.defaultScrollAnchor((direction == .downwards) ? .top : .bottom)
.scrollDismissesKeyboard(.interactively)
}
.scrollPosition(
id: $scrollPositionItemID,
anchor: (direction == .downwards) ? .bottom : .top
)
.defaultScrollAnchor((direction == .downwards) ? .top : .bottom)
.onChange(of: scrollPositionItemID) { _, newValue in
.onChange(of: scrollPositionItemID) { oldValue, newValue in
if let newValue, newValue == anchorItemID, phase == .idle {
self.phase = .paging(fromItem: newValue)
}
Expand All @@ -133,19 +150,22 @@ public struct InfiniteVerticalScrollView<Item: Identifiable & Sendable, ItemView
}
do {
let (newItems, areThereMorePages) = try await nextPageFetcher(itemID)
switch direction {
case .downwards:
self.items.append(contentsOf: newItems)
case .upwards:
self.items.insert(contentsOf: newItems, at: 0)
withAnimation {
self.phase = areThereMorePages ? .idle : .noMorePages
} completion: {
switch direction {
case .downwards:
self.items.append(contentsOf: newItems)
case .upwards:
self.items.insert(contentsOf: newItems, at: 0)
}
self.pleaseScrollTo = itemID
}
self.phase = areThereMorePages ? .idle : .noMorePages
} catch {
self.phase = .idle
self.error = error
}
}
.errorAlert(error: $error)
}

private var anchorItemID: Item.ID? {
Expand Down

0 comments on commit 573dfda

Please sign in to comment.