SwiftUI - 自定义列表中的滑动操作

29

我如何在SwiftUI中使用自定义滑动操作?

我尝试使用UIKit框架在SwiftUI中实现这些操作,但是不起作用。

import SwiftUI
import UIKit



    init() {
        override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
            let important = importantAction(at: indexPath)
            return UISwipeActionsConfiguration(actions: [important])
        }
        func importantAction(at indexPath: IndexPath) -> UIContextualAction {
            let action = UIContextualAction(style: .normal, title: "Important") { (action, view, completion) in
                print("HI")
            }
            action.backgroundColor = UIColor(hue: 0.0861, saturation: 0.76, brightness: 0.94, alpha: 1.0) /* #f19938 */
            action.image = UIImage(named: "pencil")
            return action
        }
    }






struct TestView: View {

      NavigationView {
               List {
                    ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in Row_Appointments(appointment: appointment)
                }.onDelete(perform: delete)
            }
        }
    }
}


你的代码,就像现在这样,不会构建成功。init 是某个东西的一部分,但是它属于什么呢?而且,你究竟想做什么,"that doesn't work for me" 的意思是什么?你是说 UIViewControllerRepresentable 不起作用吗?还是说 UIKit 中的一些东西无法使用?或许是 onDelete 拦截了滑动操作?请提供更多细节,我们可以帮助你。 - user7014451
1
看起来Max想要为他的列表项添加一个尾随滑动操作,以将该项标记为“重要”。由于tableView(_:trailingSwipeActionsConfigurationForRow:)是一个UITableViewDelegate方法,并且SwiftUI不允许您为其创建的UITableView设置委托作为实现细节,因此Max的尝试很可能不会成功。 - rob mayoff
在iOS 15中,我们终于可以使用本地滑动操作 - 参见此答案 - pawello2222
7个回答

23

如果您的部署目标是iOS 15(或更高版本),那么您可以使用swipeActions修饰符来自定义列表项的滑动操作。

这也适用于watchOS 8和macOS 12。

这些操作系统将在2021年底发布。

在SwiftUI的2021年底版本之前,没有支持List项目的自定义滑动操作。

如果您需要针对旧版本进行目标设置,最好采用其他用户界面实现,例如将切换按钮作为列表项的子视图添加,或者将上下文菜单添加到列表项中。


9

iOS 15+

iOS 15中,我们终于可以使用本地的 滑动操作(Swipe Actions)

func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, content: () -> T) -> some View where T : View

它们可以像 onMoveonDelete 一样附加到 ForEach 容器上:

List {
    ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in
        Row_Appointments(appointment: appointment)
    }
    .swipeActions(edge: .trailing) {
        Button {
            print("Hi")
        } label: {
            Label("Important", systemImage: "pencil")
        }
    }
}

有没有办法更改滑动操作标签的字体? - JAHelia

5

基于 Michał Ziobro答案,使用Introspect来简化表视图代理设置。

请注意,这将覆盖表视图委托,并可能破坏某些现有的表视图行为。虽然像 header 高度这样的东西可以通过自己添加方法到自定义委托中来解决,但其他问题可能无法解决。

struct ListSwipeActions: ViewModifier {

    @ObservedObject var coordinator = Coordinator()

    func body(content: Content) -> some View {

        return content
            .introspectTableView { tableView in
                tableView.delegate = self.coordinator
            }
    }

    class Coordinator: NSObject, ObservableObject, UITableViewDelegate {

        func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
            return .delete
        }

        func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

            let archiveAction = UIContextualAction(style: .normal, title: "Title") { action, view, completionHandler in
                // update data source
                completionHandler(true)
            }
            archiveAction.image = UIImage(systemName: "archivebox")!
            archiveAction.backgroundColor = .systemYellow

            let configuration = UISwipeActionsConfiguration(actions: [archiveAction])

            return configuration
        }
    }
}

extension List {
    func swipeActions() -> some View {
        return self.modifier(ListSwipeActions())
    }
}

@AntonShevtsov 真的吗?你测试过了吗? - LetsGoBrandon
1
这似乎适用于常规的willDisplay回调,但会削弱任何返回值的方法。像部分标题之类的东西将会消失(因为UIKit依赖于委托来托管这些内容)。 - AverageHelper
2
虽然这对滑动操作有效,但它实际上破坏了与 SwiftUI 表的其余交互。显然,被覆盖的委托处理所有这些问题,但是重新委托方法给原始委托并不会有所帮助。 - Koraktor
@Koraktor,哪个具体的功能无法修复? - Ivan Rep
我在一个列表中使用了NavigationLinks。但是,一旦我使用了你的修饰符,它们就停止工作了。这可能是因为新的委托没有处理单元格上的点击事件。 - Koraktor

3

很高兴看到iOS 15在SwiftUI中为List引入了期待已久的.swipeActions视图修饰符,使用起来非常简单。

List {
    ForEach(store.messages) { message in
        MessageCell(message: message)
            .swipeActions(edge: .leading) {
                Button { store.toggleUnread(message) } label: {
                    if message.isUnread {
                        Label("Read", systemImage: "envelope.open")
                    } else {
                        Label("Unread", systemImage: "envelope.badge")
                    }
                }
            }
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    store.delete(message)
                } label: {
                    Label("Delete", systemImage: "trash")
                }
                Button { store.flag(message) } label: {
                    Label("Flag", systemImage: "flag")
                }
            }
        }
    }
}

按照列出的顺序,动作从起始边开始向内部工作。

上面的示例产生:

swipe actions

请注意,swipeActions 会覆盖 ForEach 上提供的 onDelete 处理程序(如果有)。

在 Apple 的开发者文档中阅读更多信息


3

可以用类似以下的方式来实现:

           List {
                ForEach(items) { (item) in

                    Text("\(item.title)")
                }
                .onDelete(perform: self.delete)
            }.swipeActions()

然后您需要添加这个swipeActions()修饰符

struct ListSwipeActions: ViewModifier {

    @ObservedObject var coordinator = Coordinator()

    func body(content: Content) -> some View {

        return content
            .background(TableViewConfigurator(configure: { tableView in
                delay {
                    tableView.delegate = self.coordinator
                }
            }))
    }

    class Coordinator: NSObject, ObservableObject, UITableViewDelegate {

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            print("Scrolling ....!!!")
        }

        func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
            return .delete
        }

        func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

            let isArchived = false
            let title = isArchived ? NSLocalizedString("Unarchive", comment: "Unarchive") : NSLocalizedString("Archive", comment: "Archive")

            let archiveAction = UIContextualAction(style: .normal, title: title, handler: {
                (action, view, completionHandler) in

                // update data source
                completionHandler(true)
            })
            archiveAction.title = title
            archiveAction.image = UIImage(systemName: "archivebox")!
            archiveAction.backgroundColor = .systemYellow

            let configuration = UISwipeActionsConfiguration(actions: [archiveAction])

            return configuration
        }
    }
}

extension List {

    func swipeActions() -> some View {
        return self.modifier(ListSwipeActions())
    }
}

还有一个TableViewConfigurator,它在List后面搜索表视图

struct TableViewConfigurator: UIViewControllerRepresentable {

    var configure: (UITableView) -> Void = { _ in }

    func makeUIViewController(context: Context) -> UIViewController {

        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {


        let tableViews = UIApplication.nonModalTopViewController()?.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()

        for tableView in tableViews {
            self.configure(tableView)
        }
    }
}

3
struct ListSwipeActions: ViewModifier { func body(content: Content) -> some View {中,delay被用于什么目的? - tbergmen
只需使用以下代码:DispatchQueue.main.asyncAfter(deadline: .now() + interval) { } - Michał Ziobro
3
nonModalTopViewController是什么?XCode找不到它。 - PRSHL
3
  1. 未解决的标识符使用“delay”
  2. 类型“UIApplication”没有成员“nonModalTopViewController”
- LetsGoBrandon

0

现在有了 IOS 15 和 Swift 5.5,我们可以像这样添加滑动操作

struct ContentView: View {
    @State private var total = 0

    var body: some View {
        NavigationView {
            List {
                ForEach(1..<100) { i in
                    Text("\(i)")
                        .swipeActions(edge: .leading) {
                            Button {
                                total += i
                            } label: {
                                Label("Add \(i)", systemImage: "plus.circle")
                            }
                            .tint(.indigo)
                        }
                        .swipeActions(edge: .trailing) {
                            Button {
                                total -= i
                            } label: {
                                Label("Subtract \(i)", systemImage: "minus.circle")
                            }
                        }
                }
            }
            .navigationTitle("Total: \(total)")
        }
    }
}

0

我想要相同的功能,现在有以下实现方式。

SwipeController 检查何时执行滑动操作并执行 SwipeAction,现在您可以在 executeAction 函数的打印行下添加您的滑动操作。但最好将其制作成抽象类。

然后,在 SwipeLeftRightContainer 结构中,我们在 DragGesture 中拥有大部分逻辑。它的作用是在拖动时更改偏移量,然后调用 SwipeController 来查看是否达到向左或向右滑动的阈值。然后当您完成拖动时,它将进入 DragGesture 的 onEnded 回调。在这里,我们将重置偏移量,并让 SwipeController 决定执行操作。

请记住,视图中的许多变量对于 iPhone X 是静态的,因此您应该将它们更改为最合适的内容。

此外,这将创建左右滑动的操作,但您当然可以根据自己的使用进行调整。

import SwiftUI

/** executeRight: checks if it should execute the swipeRight action
    execute Left: checks if it should execute the swipeLeft action
    submitThreshold: the threshold of the x offset when it should start executing the action
*/
class SwipeController {
    var executeRight = false
    var executeLeft = false
    let submitThreshold: CGFloat = 200
    
    func checkExecutionRight(offsetX: CGFloat) {
        if offsetX > submitThreshold && self.executeRight == false {
            Utils.HapticSuccess()
            self.executeRight = true
        } else if offsetX < submitThreshold {
            self.executeRight = false
        }
    }
    
    func checkExecutionLeft(offsetX: CGFloat) {
        if offsetX < -submitThreshold && self.executeLeft == false {
            Utils.HapticSuccess()
            self.executeLeft = true
        } else if offsetX > -submitThreshold {
            self.executeLeft = false
        }
    }
    
    func excuteAction() {
        if executeRight {
            print("executed right")
        } else if executeLeft {
            print("executed left")
        }
        
        self.executeLeft = false
        self.executeRight = false
    }
}

struct SwipeLeftRightContainer: View {
    
    var swipeController: SwipeController = SwipeController()
    
    @State var offsetX: CGFloat = 0
    
    let maxWidth: CGFloat = 335
    let maxHeight: CGFloat = 125
    let swipeObjectsOffset: CGFloat = 350
    let swipeObjectsWidth: CGFloat = 400
    
    @State var rowAnimationOpacity: Double = 0
    var body: some View {
        ZStack {
            Group {
                HStack {
                    Text("Sample row")
                    Spacer()
                }
            }.padding(10)
            .zIndex(1.0)
            .frame(width: maxWidth, height: maxHeight)
            .cornerRadius(5)
            .background(RoundedRectangle(cornerRadius: 10).fill(Color.gray))
            .padding(10)
            .offset(x: offsetX)
            .gesture(DragGesture(minimumDistance: 5).onChanged { gesture in
                withAnimation(Animation.linear(duration: 0.1)) {
                    offsetX = gesture.translation.width
                }
                swipeController.checkExecutionLeft(offsetX: offsetX)
                swipeController.checkExecutionRight(offsetX: offsetX)
            }.onEnded { _ in
                withAnimation(Animation.linear(duration: 0.1)) {
                    offsetX = 0
                    swipeController.prevLocX = 0
                    swipeController.prevLocXDiff = 0
                    self.swipeController.excuteAction()
                }
            })
            Group {
                ZStack {
                    Rectangle().fill(Color.red).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityDelete)
                    Image(systemName: "multiply").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.trailing, 150)
                }
            }.zIndex(0.9).offset(x: swipeObjectsOffset + offsetX)
            Group {
                ZStack {
                    Rectangle().fill(Color.green).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityLike)
                    Image(systemName: "heart").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.leading, 150)
                }
            }.zIndex(0.9).offset(x: -swipeObjectsOffset + offsetX)
        }
    }
    
    var opacityDelete: Double {
        if offsetX < 0 {
            return Double(abs(offsetX) / 50)
        }
        return 0
    }
    
    var opacityLike: Double {
        if offsetX > 0 {
            return Double(offsetX / 50)
        }
        return 0
    }
}

struct SwipeListView: View {
    
    var body: some View {
        ScrollView {
            ForEach(0..<10) { index in
                SwipeLeftRightContainer().listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
            }
        }
    }
    
}

struct SwipeLeftRight_Previews: PreviewProvider {
    static var previews: some View {
        SwipeListView()
    }
}

1
提供完整答案。 - Arvind Patel
我尝试了一下,也许我们可以得到一些好的结果,但说实话,使用滚动视图很困难,而且它远远不能与我使用UITableView进行本地实现时所得到的相比。你在此期间改进了你的代码吗? - Marc T.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接