Skip to content
Open

WIP #65

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Development/Development.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
4BEAFA4C2A3CE48800478C59 /* AsyncMultiplexImage in Frameworks */ = {isa = PBXBuildFile; productRef = 4BEAFA4B2A3CE48800478C59 /* AsyncMultiplexImage */; };
4BEAFA4E2A3CE48800478C59 /* AsyncMultiplexImage-Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 4BEAFA4D2A3CE48800478C59 /* AsyncMultiplexImage-Nuke */; };
4BEBA5682D3EC5A200BDE020 /* BookRerender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEBA5672D3EC59A00BDE020 /* BookRerender.swift */; };
B059CA352E8E36F900879BA6 /* Hopping.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059CA342E8E36F900879BA6 /* Hopping.swift */; };
B0699F292E8863750098A042 /* SelectableForEach in Frameworks */ = {isa = PBXBuildFile; productRef = B0699F282E8863750098A042 /* SelectableForEach */; };
B0699F2B2E8863790098A042 /* StickyHeader in Frameworks */ = {isa = PBXBuildFile; productRef = B0699F2A2E8863790098A042 /* StickyHeader */; };
B0699F2D2E88637D0098A042 /* RefreshControl in Frameworks */ = {isa = PBXBuildFile; productRef = B0699F2C2E88637D0098A042 /* RefreshControl */; };
Expand Down Expand Up @@ -62,6 +63,7 @@
4BD04C182B2C15E100FE41D9 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
4BEAFA482A3CE3B100478C59 /* BookVariadicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookVariadicView.swift; sourceTree = "<group>"; };
4BEBA5672D3EC59A00BDE020 /* BookRerender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookRerender.swift; sourceTree = "<group>"; };
B059CA342E8E36F900879BA6 /* Hopping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hopping.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -105,6 +107,7 @@
4B26A6792A33239500B75FB4 /* Development */ = {
isa = PBXGroup;
children = (
B059CA342E8E36F900879BA6 /* Hopping.swift */,
4BEBA5672D3EC59A00BDE020 /* BookRerender.swift */,
4B9223612D3CE6B5007E20CB /* GlobalCounter.swift */,
4B08E0AA2CF5805200B05999 /* BookScrollView.swift */,
Expand Down Expand Up @@ -235,6 +238,7 @@
4B08E0AB2CF5805500B05999 /* BookScrollView.swift in Sources */,
4BC34FB12CDB1C0C00D22811 /* BookCollectionView.swift in Sources */,
4BD04C152B2C05C600FE41D9 /* BookPlainCollectionView.swift in Sources */,
B059CA352E8E36F900879BA6 /* Hopping.swift in Sources */,
4B910EBC2A77A2F50079D26D /* BookUIKitBasedFlow.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
301 changes: 301 additions & 0 deletions Development/Development/Hopping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import CollectionView
import SwiftUI

@available(iOS 17.0, *)
public struct HoppingLayout<Selection: Hashable>: CollectionViewLayoutType {

private let scrollPosition: Binding<Selection?>?

public init(
selection: Binding<Selection?>
) {
self.scrollPosition = selection
}

public init() where Selection == Never {
self.scrollPosition = nil
}

public func body(content: Content) -> some View {
let view = ScrollView(.vertical, showsIndicators: false) {
// In iOS17, scrollTargetLayout only works if its content is LazyVStack.
UnaryViewReader(readingContent: content) { children in
LazyVStack(spacing: 16) {
ForEach(children, id: \.id) { child in
child
.containerRelativeFrame(.vertical)
}
}
.scrollTargetLayout()
}
}
.scrollTargetBehavior(.viewAligned)
.safeAreaPadding(.vertical, 32)

if let scrollPosition {
ScrollViewReader { proxy in
view
.animation(
.smooth,
body: {
$0.scrollPosition(id: scrollPosition)
}
)
.onAppear {
proxy.scrollTo(scrollPosition.wrappedValue)
}
}
} else {
view
}
}

}

// MARK: workaround

/// https://movingparts.io/variadic-views-in-swiftui
struct UnaryViewReader<ReadingContent: View, Content: View>: View {

let readingContent: ReadingContent
let content: (_VariadicView_Children) -> Content

init(
readingContent: ReadingContent,
@ViewBuilder content: @escaping (_VariadicView_Children) -> Content
) {
self.readingContent = readingContent
self.content = content
}

// MARK: View

var body: some View {
_VariadicView.Tree(_UnaryView(content: content)) {
readingContent
}
}

}

struct MultiViewReader<ReadingContent: View, Content: View>: View {

let readingContent: ReadingContent
let content: (_VariadicView_Children) -> Content

init(
readingContent: ReadingContent,
@ViewBuilder content: @escaping (_VariadicView_Children) -> Content
) {
self.readingContent = readingContent
self.content = content
}

// MARK: View

var body: some View {
_VariadicView.Tree(_MultiView(content: content)) {
readingContent
}
}

}

private struct _UnaryView<Content: View>: _VariadicView_UnaryViewRoot {

let content: (_VariadicView_Children) -> Content

init(@ViewBuilder content: @escaping (_VariadicView_Children) -> Content) {
self.content = content
}

@ViewBuilder
func body(children: _VariadicView.Children) -> some View {
content(children)
}
}

private struct _MultiView<Content: View>: _VariadicView_MultiViewRoot {

let content: (_VariadicView_Children) -> Content

init(@ViewBuilder content: @escaping (_VariadicView_Children) -> Content) {
self.content = content
}

@ViewBuilder
func body(children: _VariadicView.Children) -> some View {
content(children)
}
}

#Preview("Hopping 1") {

@Previewable @State var scrollPosition: Int? = 3

VStack {
HStack {
Button("One") {
scrollPosition = 1
}
Button("Two") {
scrollPosition = 2
}
Button("Three") {
scrollPosition = 3
}
}
.buttonStyle(.bordered)

CollectionView(
layout: HoppingLayout(selection: $scrollPosition)
) {
ForEach(0..<30, id: \.self) { i in
CardItem(
title: "Item \(i)",
color: .purple
)
.id(i)
}
}
}
}

#Preview("Hopping 2") {

@Previewable @State var scrollPosition: Int? = 3

VStack {
HStack {
Button("One") {
scrollPosition = 1
}
Button("Two") {
scrollPosition = 2
}
Button("Three") {
scrollPosition = 3
}
}
.buttonStyle(.bordered)

CollectionView(
layout: HoppingLayout(selection: $scrollPosition)
) {
ForEach(0..<30, id: \.self) { i in
Text("Item \(i)")
.background(.red.opacity(0.3))
.id(i)
.id(i)
}
}
}
}

private struct CardItem: View {
let title: String
let color: Color

var body: some View {
ZStack {
color
Text(title)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 24)
}
}


public struct Hopping<Selection: Hashable, Content: View>: View {

private let content: Content
private let scrollPosition: Binding<Selection?>?

public init(
selection: Binding<Selection?>,
@ViewBuilder content: () -> Content
) {
self.scrollPosition = selection
self.content = content()
}

public init(
@ViewBuilder content: () -> Content
) where Selection == Never {
self.scrollPosition = nil
self.content = content()
}

public var body: some View {


// let view = ScrollView(.vertical, showsIndicators: false) {
// // In iOS17, scrollTargetLayout only works if its content is LazyVStack.
// UnaryViewReader(readingContent: content) { children in
// LazyVStack(spacing: 16) {
// ForEach(children, id: \.id) { child in
// child
// .containerRelativeFrame(.vertical)
// }
// }
// .scrollTargetLayout()
// }
// }
// .scrollTargetBehavior(.viewAligned)
// .safeAreaPadding(.vertical, 32)
//
// if let scrollPosition {
// ScrollViewReader { proxy in
// view
// .animation(
// .smooth,
// body: {
// $0.scrollPosition(id: scrollPosition)
// }
// )
// .onAppear {
// proxy.scrollTo(scrollPosition.wrappedValue)
// }
// }
// } else {
// view
// }
}

}

#Preview("ScrollView") {

@Previewable @State var selection: Int? = 1

VStack {
HStack {
Button("One") {
selection = 0
}
Button("Two") {
selection = 1
}
Button("Three") {
selection = 2
}
Button("Clear") {
selection = nil
}
}
Hopping(selection: $selection) {
ForEach(0..<10, id: \.self) { i in
Text("Item \(i)")
.background(.red.opacity(0.3))
.id(i)
}
}
.onChange(of: selection, initial: true) {
print(selection)
}
}
}