AdvancedList

Swift 5.3 Platforms Current version Build status Code coverage License

本软件包提供了一个围绕 SwiftUI List view 的包装视图,增加了分页(通过我的 ListPagination 软件包)以及错误加载状态,包括相应的视图。

📦 安装

在 Xcode 中使用其 Github 仓库 URL 添加此 Swift 软件包。(File > Swift Packages > Add Package Dependency...)

🚀 如何使用

AdvancedList 视图类似于 ListForEach 视图。您必须将数据 (RandomAccessCollection) 和视图提供者 ((Data.Element) -> some View) 传递给初始化器。除了 List 视图之外,AdvancedList 还需要一个列表状态和相应的视图。随时修改您的数据,或者如果您愿意,可以通过内容块隐藏项目。该视图会自动更新 🎉。

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    Text(error.localizedDescription)
        .lineLimit(nil)
}, loadingStateView: {
    Text("Loading ...")
})

🆕 自定义列表视图

从版本 6.0.0 开始,您可以使用自定义列表视图代替底层使用的 SwiftUI List。 例如,如果需要,您现在可以轻松使用 iOS 14 中引入的 LazyVStack

从版本 5.0.0 升级**不会破坏任何内容**。 升级后只需添加 listView 参数

AdvancedList(yourData, listView: { rows in
    if #available(iOS 14, macOS 11, *) {
        ScrollView {
            LazyVStack(alignment: .leading, content: rows)
                .padding()
        }
    } else {
        List(content: rows)
    }
}, content: { item in
    Text("Item")
}, listState: listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    Text(error.localizedDescription)
        .lineLimit(nil)
}, loadingStateView: {
    Text("Loading ...")
})

🆕 自定义内容视图

从版本 8.0.0 开始,您可以完全自由地控制在 AdvancedListitems 状态下呈现的内容视图。 使用 SwiftUI List自定义视图

从版本 7.0.0 升级**不会破坏任何内容**并使用新的 API

AdvancedList(listState: yourListState, content: {
    VStack {
        Text("Row 1")
        Text("Row 2")
        Text("Row 3")
    }
}, errorStateView: { error in
    VStack(alignment: .leading) {
        Text("Error").foregroundColor(.primary)
        Text(error.localizedDescription).foregroundColor(.secondary)
    }
}, loadingStateView: ProgressView.init)

📄 分页

Pagination 功能现在 (>= 5.0.0) 作为 modifier 实现。 它有三种不同的状态:erroridleloading。 如果 Paginationstate 发生变化,AdvancedList 会显示由指定分页对象 (AdvancedListPagination) 的视图构建器创建的视图。 通过创建类型为 AdvancedListPaginationState 的局部状态变量 (@State) 来跟踪当前分页状态。 在分页配置对象的 content ViewBuilder 中使用此状态变量来确定列表中应显示哪个视图(请参见下面的示例)。

如果您想使用分页,则可以在 lastItemPaginationthresholdItemPagination 之间进行选择。 这两个概念都在 这里 描述。 只需在将 .pagination 修饰符添加到 AdvancedList 时指定分页的类型。

只有在列表的最后一个项目出现后,分页配置对象的 content ViewBuilder 创建的视图才会在列表下方可见! 这样,只有在需要时才会中断用户。

示例

@State private var paginationState: AdvancedListPaginationState = .idle

AdvancedList(...)
.pagination(.init(type: .lastItem, shouldLoadNextPage: {
    paginationState = .loading
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        items.append(contentsOf: moreItems)
        paginationState = .idle
    }
}) {
    switch paginationState {
    case .idle:
        EmptyView()
    case .loading:
        if #available(iOS 14, *) {
            ProgressView()
        } else {
            Text("Loading ...")
        }
    case let .error(error):
        Text(error.localizedDescription)
    }
})

📁 移动和 🗑️ 删除项目

要启用移动或删除功能,只需使用相关的 onMoveonDelete 视图修饰符。 如果您不添加视图修饰符,默认情况下这些功能将被禁用。

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    Text(error.localizedDescription)
        .lineLimit(nil)
}, loadingStateView: {
    Text("Loading ...")
})
.onMove { (indexSet, index) in
    // move me
}
.onDelete { indexSet in
    // delete me
}

🎛️ 过滤

您可以通过内容块隐藏列表中的项目。 仅当满足特定条件时才在内容块中返回视图。

🎁 示例

以下代码显示了该视图的易用性

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
})

有关更多示例,请查看 Example 目录。

迁移

迁移 2.x -> 3.0

AdvancedList 得到了极大的简化,现在更像是 ListForEach SwiftUI 视图。

  1. 删除您的列表服务实例并直接将您的数据传递给列表初始化器
  2. 通过内容块(初始化器参数)创建您的视图,而不是直接使您的项目符合 View(删除类型擦除包装器 AnyListItem
  3. 将列表状态绑定传递给初始化器(之前: ListService 管理列表状态)
  4. 移动和删除: 不要在您的列表服务上设置 AdvancedListActions,只需将 onMoveAction 和/或 onDeleteAction 块传递给初始化器

之前

import AdvancedList

let listService = ListService()
listService.supportedListActions = .moveAndDelete(onMove: { (indexSet, index) in
    // please move me
}, onDelete: { indexSet in
    // please delete me
})
listService.listState = .loading

AdvancedList(listService: listService, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)

listService.listState = .loading
// fetch your items ...
listService.appendItems(yourItems)
listService.listState = .items

之后

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, onMoveAction: { (indexSet, index) in
    // move me
}, onDeleteAction: { indexSet in
    // delete me
}, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)
迁移 3.0 -> 4.0

感谢 @SpectralDragon 的提示,我可以将 onMoveonDelete 功能重构为视图修饰符。

之前

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, onMoveAction: { (indexSet, index) in
    // move me
}, onDeleteAction: { indexSet in
    // delete me
}, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)

之后

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)
.onMove { (indexSet, index) in
    // move me
}
.onDelete { indexSet in
    // delete me
}
迁移 4.0 -> 5.0

Pagination 现在作为 modifier 实现 💪 最后但并非最不重要的一点是,代码文档已到达 😀

之前

private lazy var pagination: AdvancedListPagination<AnyView, AnyView> = {
    .thresholdItemPagination(errorView: { error in
        AnyView(
            VStack {
                Text(error.localizedDescription)
                    .lineLimit(nil)
                    .multilineTextAlignment(.center)

                Button(action: {
                    // load current page again
                }) {
                    Text("Retry")
                }.padding()
            }
        )
    }, loadingView: {
        AnyView(
            VStack {
                Divider()
                Text("Loading...")
            }
        )
    }, offset: 25, shouldLoadNextPage: {
        // load next page
    }, state: .idle)
}()

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: pagination)

之后

@State private var listState: ListState = .items
@State private var paginationState: AdvancedListPaginationState = .idle

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
})
.pagination(.init(type: .lastItem, shouldLoadNextPage: {
    paginationState = .loading
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        items.append(contentsOf: moreItems)
        paginationState = .idle
    }
}) {
    switch paginationState {
    case .idle:
        EmptyView()
    case .loading:
        if #available(iOS 14, *) {
            ProgressView()
        } else {
            Text("Loading ...")
        }
    case let .error(error):
        Text(error.localizedDescription)
    }
})
迁移 6.0 -> 7.0

我替换了不必要的 listState Binding 并将其替换为简单的 value 参数。

之前

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: $listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)

之后

import AdvancedList

@State private var listState: ListState = .items

AdvancedList(yourData, content: { item in
    Text("Item")
}, listState: listState, emptyStateView: {
    Text("No data")
}, errorStateView: { error in
    VStack {
        Text(error.localizedDescription)
            .lineLimit(nil)
        
        Button(action: {
            // do something
        }) {
            Text("Retry")
        }
    }
}, loadingStateView: {
    Text("Loading ...")
}, pagination: .noPagination)
迁移 7.0 -> 8.0没事可做 🎉