我想在SwiftUI中实现自定义的类似List的视图,该视图必须具有比SwiftUI中的标准本机List视图更多的功能。我想添加拖放(尽管onMove())在List中不存在。
我已经通过以下方式实现了此列表:
import SwiftUI
import MobileCoreServices
final class ReorderIndexPath: NSIndexPath {
}
extension ReorderIndexPath : NSItemProviderWriting {
public static var writableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
progress.completedUnitCount = 100
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return progress
}
}
extension ReorderIndexPath : NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypeData as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ReorderIndexPath {
do {
return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! ReorderIndexPath
} catch {
fatalError(error.localizedDescription)
}
}
}
struct ReorderableList: UIViewControllerRepresentable {
struct Model {
private(set) var items : [AnyView]
init(items: [AnyView]) {
self.items = items
}
mutating func addItem(_ item: AnyView, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at sourceIndex: Int, to destinationIndex: Int) {
guard sourceIndex != destinationIndex else { return }
let item = items[sourceIndex]
items.remove(at: sourceIndex)
items.insert(item, at: destinationIndex)
}
func canHandle(_ session: UIDropSession) -> Bool {
return session.canLoadObjects(ofClass: ReorderIndexPath.self)
}
func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
//let item = items[indexPath.row]
//let data = item.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerObject(ReorderIndexPath(row: indexPath.row, section: indexPath.section), visibility: .all)
return [
UIDragItem(itemProvider: itemProvider)
]
}
}
@State private var model : Model
// MARK: - Actions
let onReorder : (Int, Int) -> Void
let onDelete : ((Int) -> Bool)?
// MARK: - Init
public init<Data, RowContent>(onReorder: @escaping (Int, Int) -> Void = { _, _ in }, onDelete: ((Int) -> Bool)? = nil, _ content: @escaping () -> ForEach<Data, Data.Element.ID, RowContent>) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, RowContent>(onReorder: @escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { element in HStack { rowContent(element) } }
}
}
public init<Data, ID, RowContent>(onReorder: @escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: @escaping () -> ForEach<Data, ID, RowContent>) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { element in
let item = content.content(element)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<Data, ID, RowContent>(onReorder: @escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Data, id: KeyPath<Data.Element, ID>, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Data : RandomAccessCollection, ID : Hashable, RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data, id: id) { element in HStack { rowContent(element) } }
}
}
public init<RowContent>(onReorder: @escaping (Int, Int) -> Void = { _,_ in }, onDelete: ((Int) -> Bool)? = nil, _ content: @escaping () -> ForEach<Range<Int>, Int, RowContent>) where RowContent : View {
let content = content()
var items = [AnyView]()
content.data.forEach { i in
let item = content.content(i)
items.append(AnyView(item))
}
self.onReorder = onReorder
self.onDelete = onDelete
self._model = State(initialValue: Model(items: items))
}
public init<RowContent>(onReorder: @escaping (Int, Int) -> Void = {_,_ in }, onDelete: ((Int) -> Bool)? = nil, _ data: Range<Int>, @ViewBuilder rowContent: @escaping (Int) -> RowContent) where RowContent : View {
self.init(onReorder: onReorder, onDelete: onDelete) {
ForEach(data) { i in
HStack { rowContent(i) }
}
}
}
func makeUIViewController(context: Context) -> UITableViewController {
let tableView = UITableViewController()
tableView.tableView.delegate = context.coordinator
tableView.tableView.dataSource = context.coordinator
tableView.tableView.dragInteractionEnabled = true
tableView.tableView.dragDelegate = context.coordinator
tableView.tableView.dropDelegate = context.coordinator
tableView.tableView.register(HostingTableViewCell<AnyView>.self, forCellReuseIdentifier: "HostingCell")
context.coordinator.controller = tableView
return tableView
}
func updateUIViewController(_ uiView: UITableViewController, context: Context) {
//print("Reorderable list update")
//uiView.tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource, UITableViewDragDelegate, UITableViewDropDelegate {
let parent: ReorderableList
weak var controller : UITableViewController?
// MARK: - Init
init(_ parent: ReorderableList) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell<AnyView>
let item = parent.model.items[indexPath.row]
cell.host(item, parent: controller!)
return cell
}
// MARK: - Delegate
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return parent.onDelete != nil ? .delete : .none
}
func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
return false
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if parent.onDelete?(indexPath.row) ?? false {
tableView.beginUpdates()
parent.model.removeItem(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
} else if editingStyle == .insert {
}
}
/*
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let object = parent.model.items[sourceIndexPath.row]
parent.model.items.remove(at: sourceIndexPath.row)
parent.model.items.insert(object, at: destinationIndexPath.row)
}
*/
// MARK: - Drag Delegate
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
return parent.model.dragItems(for: indexPath)
}
// MARK: - Drop Delegate
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return parent.model.canHandle(session)
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
if tableView.hasActiveDrag {
if session.items.count > 1 {
return UITableViewDropProposal(operation: .cancel)
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
}
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// Get last index path of table view.
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: ReorderIndexPath.self) { items in
// Consume drag items.
let indexPaths = items as! [IndexPath]
for (index, sourceIndexPath) in indexPaths.enumerated() {
let destinationIndexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
self.parent.model.moveItem(at: sourceIndexPath.row, to: destinationIndexPath.row)
tableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
self.parent.onReorder(sourceIndexPath.row, destinationIndexPath.row)
}
}
}
}
}
这是使用它的客户端代码
struct ContentView: View {
@State private var items: [String] = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
ReorderableList(onReorder: reorder, onDelete: delete) {
ForEach(self.items, id: \.self) { item in
Text("\(item)")
}
}
.navigationBarTitle("Reorderable List", displayMode: .inline)
.navigationBarItems(trailing: Button(action: add, label: {
Image(systemName: "plus")
}))
}
}
func reorder(from source: Int, to destination: Int) {
items.move(fromOffsets: IndexSet([source]), toOffset: destination)
}
func delete(_ idx: Int) -> Bool {
items.remove(at: idx)
return true
}
func add() {
items.append("Item \(items.count)")
}
}
问题在于它没有自然的List刷新行为,因此在导航栏中点按+按钮并添加项目不会刷新ReorderableList
更新
由于上面的代码有点长,我还简化了示例来测试此单元格的刷新。
struct ReorderableList2<T, Content>: UIViewRepresentable where Content : View {
@Binding private var items: [T]
let content: (T) -> Content
init(_ items: Binding<[T]>, @ViewBuilder content: @escaping (T) -> Content) {
self.content = content
self._items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
uiView.reloadData()
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}
即使绑定项目中添加了新项目,此简化版本也不起作用。
每次我添加新项目时, tableView:numberOfRowsInSection:
都会被正确调用,但是parent.items.count
是错误的旧数字
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 3 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- content : (Function)
即使在构造函数中或在updateUIView()中检查相同项目,绑定也会提供正确的更新项目列表。
▿ ReorderableList2<String, Text>
▿ _items : Binding<Array<String>>
▿ transaction : Transaction
▿ plist : []
- elements : nil
▿ location : <LocationBox<ScopedLocation>: 0x6000016a4bd0>
▿ _value : 5 elements
- 0 : "Item 1"
- 1 : "Item 2"
- 2 : "Item 3"
- 3 : "Item 3"
- 4 : "Item 4"
- content : (Function)
答案 0 :(得分:0)
我找到了这种技巧,但是不喜欢DispatchQueue.main.async { }
中的updateUIView()
突变状态。如果有人对如何解决此问题有更好的主意,请在注释中留下其他解决方案。
我发现:
视图是结构,因此每次调用其初始化程序时,都会创建对项目/模型属性的新引用,尽管它们是类或结构
makeCoordinator()仅被调用一次,因此在重绘时,会有带有旧引用的旧Coordinator
@State被保留在视图重绘之间,因为它具有一些与之相关的基础存储,因此在每个重绘的View模型中,引用都是从同一基础存储中读取的(不同的指针)。因此,在updateUIView()中更新时,此@State在所有引用路径(包括Coordinator通过不可变的父级View引用保留的路径)上刷新此状态。
导入SwiftUI
扩展名ReorderableList2 {
struct Model<T> {
private(set) var items: [T]
init(items: [T]) {
self.items = items
}
mutating func addItem(_ item: T, at index: Int) {
items.insert(item, at: index)
}
mutating func removeItem(at index: Int) {
items.remove(at: index)
}
mutating func moveItem(at source: Int, to destination: Int) {
guard source != destination else { return }
let item = items[source]
items.remove(at: source)
items.insert(item, at: destination)
}
mutating func replaceItems(_ items: [T]) {
self.items = items
}
}
}
struct ReorderableList2:UIViewRepresentable,其中Content:视图{
// MARK: - State
@State private(set) var model = Model<T>(items: [])
// MARK: - Properties
private let items: [T]
private let content: (T) -> Content
init(_ items: [T], @ViewBuilder content: @escaping (T) -> Content) {
self.content = content
self.items = items
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "HostingCell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
DispatchQueue.main.async {
self.model.replaceItems(self.items)
uiView.reloadData()
}
}
class Coordinator : NSObject, UITableViewDataSource, UITableViewDelegate {
private let parent: ReorderableList2
// MARK: - Init
init(_ parent: ReorderableList2) {
self.parent = parent
}
// MARK: - Data Source
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.model.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HostingCell") as! HostingTableViewCell
let item = parent.model.items[indexPath.row]
let rootView = parent.content(item)
cell.host(rootView: rootView)
return cell
}
// MARK: - Delegate
}
}