Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ii 29 scroll to top progress #3

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
27C8B4552BC0038900A62E0B /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C8B4542BC0038900A62E0B /* CircularProgressView.swift */; };
A51D6A4F2B83DE8000E44E19 /* PullToRefreshSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51D6A4E2B83DE8000E44E19 /* PullToRefreshSampleApp.swift */; };
A51D6A612B83E0E800E44E19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A51D6A5E2B83E0E800E44E19 /* Assets.xcassets */; };
A51D6A622B83E0E800E44E19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A51D6A602B83E0E800E44E19 /* Preview Assets.xcassets */; };
Expand All @@ -23,6 +24,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
27C8B4542BC0038900A62E0B /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
A51D6A4B2B83DE8000E44E19 /* PullToRefreshSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PullToRefreshSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
A51D6A4E2B83DE8000E44E19 /* PullToRefreshSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToRefreshSampleApp.swift; sourceTree = "<group>"; };
A51D6A5C2B83DE9B00E44E19 /* PullToRefreshSwiftUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PullToRefreshSwiftUI; path = ..; sourceTree = "<group>"; };
Expand Down Expand Up @@ -115,6 +117,7 @@
A5BA6BAE2B870D0100DAE39C /* ListContentView.swift */,
A5288C5F2BA6B45A0033D0AE /* CircleAnimationWithProgressView.swift */,
A5288C612BA6B4820033D0AE /* CircleAnimationWithRepeatView.swift */,
27C8B4542BC0038900A62E0B /* CircularProgressView.swift */,
);
path = UI;
sourceTree = "<group>";
Expand Down Expand Up @@ -212,6 +215,7 @@
A5BA6BAF2B870D0100DAE39C /* ListContentView.swift in Sources */,
A51D6A4F2B83DE8000E44E19 /* PullToRefreshSampleApp.swift in Sources */,
A5288C602BA6B45A0033D0AE /* CircleAnimationWithProgressView.swift in Sources */,
27C8B4552BC0038900A62E0B /* CircularProgressView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import SwiftUI

struct CircularProgressView: View {

private let tintColor: Color
private let dashesesCount: Int
private let size: CGFloat
private let totalDashesCount = 8

init(progress: CGFloat, tintColor: Color = .secondary, size: CGFloat = 20.0) {
self.dashesesCount = Int(CGFloat(totalDashesCount) * progress)
self.tintColor = tintColor
self.size = size
}

var body: some View {
ZStack {
ForEach(0..<dashesesCount, id: \.self, content: { dashNumber in
Capsule()
.fill(tintColor)
.frame(width: size / CGFloat(totalDashesCount), height: size / 3.0)
.transformEffect(CGAffineTransform(translationX: 0, y: size / -3.0))
.rotationEffect(Angle(radians: CGFloat.pi * 2 * CGFloat(dashNumber) / CGFloat(totalDashesCount)))
})
}
.frame(width: size, height: size)
.opacity(0.7)
}

}

#Preview {
CircularProgressView(progress: 0.8)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ struct ContentViewWithSplitView: View {
} detail: {
switch selectedOption {
case .list:
ListContentView()
NavigationStack {
ListContentView()
}
case .scroll:
ScrollContentView()
case .none:
Expand Down
127 changes: 71 additions & 56 deletions PullToRefreshSample/PullToRefreshSampleiOS/UI/ListContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct ListContentView: View {

@State private var isRefreshing: Bool = false
@State private var animationType: AnimationType = .native
@State private var showRect: Bool = false

@State private var items: [ListItem] = [
ListItem(title: "Item 1"),
Expand All @@ -54,68 +55,82 @@ struct ListContentView: View {
]

var body: some View {
PullToRefreshListView(
options: PullToRefreshListViewOptions(pullToRefreshAnimationHeight: 100,
animationDuration: 0.3,
animatePullingViewPresentation: true,
animateRefreshingViewPresentation: true),
isRefreshing: $isRefreshing,
onRefresh: {
debugPrint("Refreshing")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(5), execute: {
isRefreshing = false
items.shuffle()
})
},
animationViewBuilder: { (state) in
switch state {
case .idle:
Color.clear
case .pulling(let progress):
switch animationType {
case .native:
CircleAnimationWithProgressView(progress: progress)
case .progressView:
ProgressView(value: progress, total: 1)
.progressViewStyle(.linear)
case .lottie:
LottieView(animation: .named("animation-pulling-shakuro_logo"))
.playbackMode(.paused(at: .progress(progress)))
VStack(spacing: 0, content: {
Rectangle()
.fill(Color.black.opacity(0.4))
.frame(height: showRect ? 200 : 0)
PullToRefreshListView(
options: PullToRefreshListViewOptions(pullToRefreshAnimationHeight: 100,
animationDuration: 0.3,
animatePullingViewPresentation: true,
animateRefreshingViewPresentation: true),
isRefreshing: $isRefreshing,
onRefresh: {
debugPrint("Refreshing")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(5), execute: {
isRefreshing = false
items.shuffle()
})
},
animationViewBuilder: { (state) in
switch state {
case .idle:
Color.clear
case .pulling(let progress):
switch animationType {
case .native:
CircleAnimationWithProgressView(progress: progress)
case .progressView:
CircularProgressView(progress: progress)
case .lottie:
LottieView(animation: .named("animation-pulling-shakuro_logo"))
.playbackMode(.paused(at: .progress(progress)))
}
case .refreshing:
switch animationType {
case .native:
CircleAnimationWithRepeatView()
case .progressView:
ProgressView()
.progressViewStyle(.circular)
case .lottie:
LottieView(animation: .named("animation-refreshing-shakuro_logo"))
.playbackMode(.playing(.fromProgress(0, toProgress: 1, loopMode: .loop)))
}
}
case .refreshing:
switch animationType {
case .native:
CircleAnimationWithRepeatView()
case .progressView:
ProgressView()
.progressViewStyle(.circular)
case .lottie:
LottieView(animation: .named("animation-refreshing-shakuro_logo"))
.playbackMode(.playing(.fromProgress(0, toProgress: 1, loopMode: .loop)))
},
contentViewBuilder: { _ in

Button(action: {
showRect.toggle()
}, label: {
Label(showRect ? "Hide top rect" : "Show top rect", systemImage: "eye")
})
.padding(.all, 10)
.border(Color.blue, width: 2)
.frame(maxWidth: .infinity)
Picker("Current animation type", selection: $animationType) {
Text("Native").tag(AnimationType.native)
Text("ProgressView").tag(AnimationType.progressView)
Text("Lottie").tag(AnimationType.lottie)
}
}
},
contentViewBuilder: { _ in
Picker("Current animation type", selection: $animationType) {
Text("Native").tag(AnimationType.native)
Text("ProgressView").tag(AnimationType.progressView)
Text("Lottie").tag(AnimationType.lottie)
}
.pickerStyle(.segmented)
.listRowSeparator(.hidden, edges: .top)
ForEach(items, content: { (item) in
ListContentItemView(listItem: item)
})
.onDelete(perform: { (indexSet) in
items.remove(atOffsets: indexSet)
.pickerStyle(.segmented)
.listRowSeparator(.hidden, edges: .top)
ForEach(items, content: { (item) in
ListContentItemView(listItem: item)
})
.onDelete(perform: { (indexSet) in
items.remove(atOffsets: indexSet)
})
.onMove(perform: { (indices, newOffset) in
items.move(fromOffsets: indices, toOffset: newOffset)
})
})
.onMove(perform: { (indices, newOffset) in
items.move(fromOffsets: indices, toOffset: newOffset)
})
})
})
.navigationTitle("Items")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: EditButton())

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ struct ScrollContentView: View {
case .native:
CircleAnimationWithProgressView(progress: progress)
case .progressView:
ProgressView(value: progress, total: 1)
.progressViewStyle(.linear)
CircularProgressView(progress: progress)
case .lottie:
LottieView(animation: .named("animation-pulling-shakuro_logo"))
.playbackMode(.paused(at: .progress(progress)))
Expand Down
21 changes: 16 additions & 5 deletions Sources/PullToRefreshSwiftUI/PullToRefreshListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,21 @@ public struct PullToRefreshListView<AnimationViewType: View, ContentViewType: Vi
Color.clear
})
.opacity(isPullToRefreshEnabled ? 1 : 0)
// List content

GeometryReader(content: { (geometryProxy) in
VStack(spacing: 0, content: {
VStack(spacing: -options.pullToRefreshAnimationHeight, content: {
// view to show pull to refresh animations
// List inset is calculated as safeAreaTopInset + this view height
Color.clear
.frame(height: options.pullToRefreshAnimationHeight * scrollViewState.progress)
// List content
List(content: {
// view to save top List inset when scrolled down, equals -spacing of Vstack
Color.clear
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.frame(height: options.pullToRefreshAnimationHeight)
.listRowInsets(EdgeInsets())
// view for offset calculation
Color.clear
.listRowSeparator(.hidden)
Expand All @@ -89,6 +96,7 @@ public struct PullToRefreshListView<AnimationViewType: View, ContentViewType: Vi
.listRowInsets(EdgeInsets())
.readLayoutData(coordinateSpace: .global, onChange: { (data) in
let offsetConclusive = data.frameInCoordinateSpace.minY - topOffset
debugPrint("fake cell layout", Date(), offsetConclusive)
scrollViewState.contentOffset = offsetConclusive
updateProgressIfNeeded()
stopIfNeeded()
Expand All @@ -101,12 +109,15 @@ public struct PullToRefreshListView<AnimationViewType: View, ContentViewType: Vi
.environment(\.defaultMinListRowHeight, 0)
.listStyle(PlainListStyle())
})
.clipped()
.readLayoutData(coordinateSpace: .global, onChange: { (data) in
topOffset = data.frameInCoordinateSpace.minY
debugPrint("VStack layout", Date(), topOffset)
})
.animation(scrollViewState.isDragging ? nil : defaultAnimation, value: scrollViewState.progress)
})
})
.readLayoutData(coordinateSpace: .global, onChange: { (data) in
topOffset = data.frameInCoordinateSpace.minY
})

.onAppear(perform: {
scrollViewState.addGestureRecognizer()
})
Expand Down