我使用了List
来简单列出数据。我想添加下拉菜单以刷新功能,但是我不确定哪种方法是最好的。
仅当用户尝试像从UITableView
到UIRefreshControl
的{{1}}一样从第一个索引下拉时,下拉刷新视图才可见
这是在UIKit
中列出数据的简单代码。
SwiftUI
答案 0 :(得分:6)
对于正在玩的应用程序,我也需要同样的东西,而且看来SwiftUI API目前不包含针对ScrollView
的刷新控制功能。
随着时间的流逝,API将开发并纠正此类情况,但是由于SwiftUI中缺少功能而导致的一般回退将始终是实现实现UIViewRepresentable
的结构。这是UIScrollView
的又脏又臭的方法,带有刷新控件。
struct LegacyScrollView : UIViewRepresentable {
// any data state, if needed
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action:
#selector(Coordinator.handleRefreshControl),
for: .valueChanged)
// Simply to give some content to see in the app
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
label.text = "Scroll View Content"
control.addSubview(label)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// code to update scroll view from view state, if needed
}
class Coordinator: NSObject {
var control: LegacyScrollView
init(_ control: LegacyScrollView) {
self.control = control
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
// handle the refresh event
sender.endRefreshing()
}
}
}
但是,当然,您不能在滚动视图中使用任何SwiftUI组件,除非将它们包装在UIHostingController
中并将其放入makeUIView
中,而不是将它们放入LegacyScrollView() { // views here }
中
答案 1 :(得分:5)
来自 iOS 15+
NavigationView {
List(1..<100) { row in
Text("Row \(row)")
}
.refreshable {
print("write your pull to refresh logic here")
}
}
了解更多详情:Apple Doc
答案 2 :(得分:3)
我尝试了许多不同的解决方案,但对于我的案例来说,没有一种效果足够好。
对于复杂布局,基于 GeometryReader
的解决方案性能不佳。
这是一个看起来运行良好的纯 SwiftUI 2.0 视图,不会因不断更新状态而降低滚动性能,也不会使用任何 UIKit 技巧:
import SwiftUI
struct PullToRefreshView: View
{
private static let minRefreshTimeInterval = TimeInterval(0.2)
private static let triggerHeight = CGFloat(100)
private static let indicatorHeight = CGFloat(100)
private static let fullHeight = triggerHeight + indicatorHeight
let backgroundColor: Color
let foregroundColor: Color
let isEnabled: Bool
let onRefresh: () -> Void
@State private var isRefreshIndicatorVisible = false
@State private var refreshStartTime: Date? = nil
init(bg: Color = .white, fg: Color = .black, isEnabled: Bool = true, onRefresh: @escaping () -> Void)
{
self.backgroundColor = bg
self.foregroundColor = fg
self.isEnabled = isEnabled
self.onRefresh = onRefresh
}
var body: some View
{
VStack(spacing: 0)
{
LazyVStack(spacing: 0)
{
Color.clear
.frame(height: Self.triggerHeight)
.onAppear
{
if isEnabled
{
withAnimation
{
isRefreshIndicatorVisible = true
}
refreshStartTime = Date()
}
}
.onDisappear
{
if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
{
onRefresh()
}
withAnimation
{
isRefreshIndicatorVisible = false
}
refreshStartTime = nil
}
}
.frame(height: Self.triggerHeight)
indicator
.frame(height: Self.indicatorHeight)
}
.background(backgroundColor)
.ignoresSafeArea(edges: .all)
.frame(height: Self.fullHeight)
.padding(.top, -Self.fullHeight)
}
private var indicator: some View
{
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
.opacity(isRefreshIndicatorVisible ? 1 : 0)
}
}
当它进入或离开屏幕边界时,它使用带有负填充的 LazyVStack
在触发器视图 onAppear
上调用 onDisappear
和 Color.clear
。
如果触发器视图出现和消失之间的时间大于 minRefreshTimeInterval
以允许 ScrollView
弹跳而不触发刷新,则会触发刷新。
要使用它,请将 PullToRefreshView
添加到 ScrollView
的顶部:
import SwiftUI
struct RefreshableScrollableContent: View
{
var body: some View
{
ScrollView
{
VStack(spacing: 0)
{
PullToRefreshView { print("refreshing") }
// ScrollView content
}
}
}
}
要点:https://gist.github.com/tkashkin/e5f6b65b255b25269d718350c024f550
答案 3 :(得分:2)
老实说,没有一个最受好评的答案真的适合我的场景。该场景在 ScrollView
和自定义 LoadingView
之间切换。每次我从 LoadingView
切换到使用旧版 ScrollView
使用 UIScrollView
创建的 UIViewRepresentable
时,contentSize
都会搞砸。
因此,作为解决方案,我创建了一个库,以便这对所有试图为如此简单的问题寻找解决方案的开发人员有用。我从互联网上收集了很多资料,浏览了许多网站,最后调整了解决方案,最终为我提供了最佳解决方案。
步骤
SPM
https://github.com/bibinjacobpulickal/BBRefreshableScrollView 添加到您的项目中。import BBRefreshableScrollView
到所需文件。View
的 body
。struct CategoryHome: View {
...
var body: some View {
NavigationView {
BBRefreshableScrollView { completion in
// do refreshing stuff here
} content: {
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
更多详情,您可以关注Readme。
答案 4 :(得分:1)
您好,请查看我制作的这个库:https://github.com/AppPear/SwiftUI-PullToRefresh
您可以通过一行代码来实现它:
struct CategoryHome: View {
var categories: [String: [Landmark]] {
.init(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var body: some View {
RefreshableNavigationView(title: "Featured", action:{
// your refresh action
}){
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
Divider() // !!! this is important to add cell separation
}
}
}
}
}
答案 5 :(得分:1)
这是一个实现视图层次结构并为SwiftUI列表的表视图添加适当的UIRefreshControl
的实现:https://github.com/timbersoftware/SwiftUIRefresh
可以在这里找到大量的自省逻辑:https://github.com/timbersoftware/SwiftUIRefresh/blob/15d9deed3fec66e2c0f6fd1fd4fe966142a891db/Sources/PullToRefresh.swift#L39-L73
答案 6 :(得分:1)
masift尚不支持swiftui-introspects,因此,如果要构建适用于iOS和macOS的UI,请考虑Samu Andras库。
我分叉了他的代码,添加了一些增强功能,并增加了在没有NavigationView的情况下使用的功能
这是示例代码。
RefreshableList(showRefreshView: $showRefreshView, action:{
// Your refresh action
// Remember to set the showRefreshView to false
self.showRefreshView = false
}){
ForEach(self.numbers, id: \.self){ number in
VStack(alignment: .leading){
Text("\(number)")
Divider()
}
}
}
有关更多详细信息,您可以访问下面的链接。 https://github.com/phuhuynh2411/SwiftUI-PullToRefresh
答案 7 :(得分:1)
这是我制作的简单,小型和纯 SwiftUI解决方案,旨在为ScrollView添加拉动刷新功能。
struct PullToRefresh: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
} else {
Text("⬇️")
}
Spacer()
}
}.padding(.top, -50)
}
}
要使用它很简单,只需将其添加到ScrollView的顶部,并为其指定ScrollView的坐标空间:
ScrollView {
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
// do your stuff when pulled
}
Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
答案 8 :(得分:1)
iOS 15 中的新功能,SwiftUI 的 refreshable()
修饰符可让您将功能附加到列表,以便在用户向下拖动足够远时触发。只要您的代码完成运行,iOS 就会自动显示活动指示器。
struct CallItem: Decodable, Identifiable {
let id: Int
let title: String
let strap: String
}
struct ContentView: View {
@State private var calls = [
CallItem(id: 0, title: "Want the latest news?", strap: "Pull to refresh!")
]
var body: some View {
NavigationView {
List(calls) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.strap)
.foregroundColor(.secondary)
}
}
.refreshable {
do {
// Fetch and decode JSON into news items
let url = URL(string: "https://www.someCoolsite.com/samples/calls.json")!
let (data, _) = try await URLSession.shared.data(from: url)
calls = try JSONDecoder().decode([CallItem].self, from: data)
} catch {
// Something went wrong; clear the news
calls = []
}
}
}
}
}
答案 9 :(得分:0)
这是一种使用 ScrollView、GeometryReader 和 PreferenceKey 的纯 SwiftUI 方法 我能够读取 ScrollView 中的滚动偏移量,一旦它高于阈值,我就可以执行操作
import SwiftUI
struct RefreshableView<Content:View>: View {
init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.refreshAction = action
}
var body: some View {
GeometryReader { geometry in
ScrollView {
content()
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[$0].y
}
}
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
if offset > threshold {
refreshAction()
}
}
}
}
private var content: () -> Content
private var refreshAction: () -> Void
private let threshold:CGFloat = 50.0
}
fileprivate struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
这是 RefreshableView 的使用示例。 进度指示器不包含在 RefreshableView 中,它只提供一个 GeometryReader 和一个 ScrollView,包括您要刷新的内容。您需要提供一个 ProgressView 或其他视图来显示正在加载。 它不适用于 List,但您可以改用 ForEach,由于 ScrollView
,内容将滚动RefreshableView(action: {
viewModel.refreshFeed(forceReload: true)
}) {
if viewModel.showProgressView {
VStack {
ProgressView()
Text("reloading feed...")
.font(Font.caption2)
}
}
ForEach(viewModel.feed.entries) { entry in
viewForEntry(entry)
}
}
完整示例可在 GitHub
上找到