Skip to content
Open
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
35 changes: 31 additions & 4 deletions Sources/PullToRefreshSwiftUI/PullToRefreshListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ public enum PullToRefreshListViewConstant { // TODO: implement
public static let offset: CGFloat = 0
}

public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: View, ContentViewType: View>: View {
public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: View, ContentViewType: View, Style: ListStyle>: View {

private let options: PullToRefreshListViewOptions
private let refreshViewHeight: CGFloat
private let showsIndicators: Bool
private let isPullToRefreshEnabled: Bool
private let isRefreshing: Binding<Bool>
private let listStyle: Style
private let listTopPadding: CGFloat
private let onRefresh: () -> Void
private let pullingViewBuilder: (_ progress: CGFloat) -> PullingViewType
private let refreshingViewBuilder: (_ isTriggered: Bool) -> RefreshingViewType
Expand All @@ -31,6 +33,7 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
@StateObject private var scrollViewState: ScrollViewState = ScrollViewState()

@State private var safeAreaTopInset: CGFloat = 0
@State private var offsetValue: CGFloat = 0

// MARK: - Initialization

Expand All @@ -39,6 +42,8 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
showsIndicators: Bool = true,
isPullToRefreshEnabled: Bool = true,
isRefreshing: Binding<Bool>,
listStyle: Style = .automatic,
listTopPadding: CGFloat = 0,
onRefresh: @escaping () -> Void,
@ViewBuilder pullingViewBuilder: @escaping (_ progress: CGFloat) -> PullingViewType,
@ViewBuilder refreshingViewBuilder: @escaping (_ isTriggered: Bool) -> RefreshingViewType,
Expand All @@ -48,6 +53,8 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
self.showsIndicators = showsIndicators
self.isPullToRefreshEnabled = isPullToRefreshEnabled
self.isRefreshing = isRefreshing
self.listStyle = listStyle
self.listTopPadding = listTopPadding
self.onRefresh = onRefresh
self.pullingViewBuilder = pullingViewBuilder
self.refreshingViewBuilder = refreshingViewBuilder
Expand Down Expand Up @@ -80,17 +87,19 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
// view to show pull to refresh animations
// List inset is calculated as safeAreaTopInset + this view height
Color.clear
.frame(height: refreshViewHeight * scrollViewState.progress)
.frame(height: offsetValue)
List(content: {
// view for offset calculation
Color.clear
.listRowSeparator(.hidden, edges: .top)
.frame(height: 0)
.listRowInsets(EdgeInsets())
.offset(coordinateSpace: PullToRefreshListViewConstant.coordinateSpace, offset: { (offset) in
let offsetConclusive = offset - safeAreaTopInset
let offsetConclusive = offset - safeAreaTopInset - listTopPadding
let offsetOld = scrollViewState.contentOffset
scrollViewState.contentOffset = offsetConclusive
updateProgressIfNeeded()
updateOffsetValueIfNeeded(oldContentOffset: offsetOld)
stopIfNeeded()
resetReadyToTriggerIfNeeded()
startIfNeeded()
Expand All @@ -100,7 +109,9 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
})
.environment(\.defaultMinListRowHeight, 0)
.coordinateSpace(name: PullToRefreshListViewConstant.coordinateSpace)
.listStyle(PlainListStyle())
.listStyle(listStyle)
.listBackport.scrollContentBackground(.hidden)
.listBackport.contentMargins(.vertical, listTopPadding)
})
.animation(scrollViewState.isDragging ? nil : defaultAnimation, value: scrollViewState.progress)
})
Expand All @@ -125,6 +136,9 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
scrollViewState.isRefreshing = false
stopIfNeeded()
resetReadyToTriggerIfNeeded()
withAnimation {
offsetValue = 0
}
}
})
.onChange(of: scrollViewState.isDragging, perform: { (_) in
Expand Down Expand Up @@ -162,6 +176,19 @@ public struct PullToRefreshListView<PullingViewType: View, RefreshingViewType: V
}
}

private func updateOffsetValueIfNeeded(oldContentOffset: CGFloat) {
// when user touched up in triggered state and List is moving up,
// we wait for the moment when top offset of List becomes equal to refreshViewHeight
// then set offsetValue to refreshViewHeight to make space above the List
if !scrollViewState.isDragging,
scrollViewState.isTriggered,
scrollViewState.contentOffset <= refreshViewHeight,
oldContentOffset > scrollViewState.contentOffset,
offsetValue == 0 {
offsetValue = refreshViewHeight
}
}

private func resetReadyToTriggerIfNeeded() {
if scrollViewState.contentOffset <= 1 &&
!scrollViewState.isReadyToTrigger &&
Expand Down
36 changes: 36 additions & 0 deletions Sources/PullToRefreshSwiftUI/View+ListBackport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import SwiftUI

struct ListBackport<Content> {

let content: Content

init(_ content: Content) {
self.content = content
}

}

extension View {
var listBackport: ListBackport<Self> { ListBackport(self) }
}

extension ListBackport where Content: View {

@ViewBuilder func scrollContentBackground(_ visibility: Visibility) -> some View {
if #available(iOS 16, *) {
content.scrollContentBackground(visibility)
} else {
content
}
}

@ViewBuilder func contentMargins(_ edges: Edge.Set = .all, _ length: CGFloat?) -> some View {
if #available(iOS 17, *) {
content.contentMargins(edges, length)
} else {
content
}
}

}