【问题标题】:SwiftUI - Custom Swipe Actions In ListSwiftUI - 列表中的自定义滑动操作
【发布时间】:2019-11-28 10:43:00
【问题描述】:

如何在 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 是某些东西的一部分,但是什么?此外,您究竟想做什么,“这对我不起作用”是什么意思?你是说UIViewControllerRepresentable 不起作用吗? UIKit 中的那些东西不起作用?也许您的onDelete 正在拦截滑动操作?拜托,也许我们可以为您提供更多详细信息。
  • 看起来 Max 想要在他的列表项中添加一个尾随滑动动作以将该项标记为“重要”。由于tableView(_:trailingSwipeActionsConfigurationForRow:) 是一个UITableViewDelegate 方法,并且SwiftUI 不允许您为它作为实现细节创建的UITableView 设置委托,因此Max 的尝试不太可能成功。
  • 在 iOS 15 中,我们终于可以使用原生的 Swipe Actions - 请参阅 this answer

标签: swift list swipe swiftui


【解决方案1】:

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

这也适用于 watchOS 8 和 macOS 12。

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

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

如果您需要针对旧版本,最好实现不同的用户界面,例如将切换按钮添加为列表项的子视图,或将adding a context menu 添加到列表项。

【讨论】:

    【解决方案2】:

    可以通过以下方式完成:

               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())
        }
    }
    

    并有TableViewConfiguratorList 后面搜索表视图

    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)
            }
        }
    }
    

    【讨论】:

    • delaystruct ListSwipeActions: ViewModifier { func body(content: Content) -> some View { 中的作用是什么?
    • 只是 DispatchQueue.main.asyncAfter(deadline: .now() + interval) { }
    • nonModalTopViewController 是什么? XCode 找不到这个
    • 使用未解析的标识符'延迟'和类型'UIApplication'没有成员'nonModalTopViewController'
    【解决方案3】:

    基于Michał Ziobro answer 使用Introspect 来简化表视图委托设置。

    请注意,这将覆盖表视图委托,并可能BREAK一些现有的表视图行为。虽然可以通过自己将方法添加到自定义委托来修复诸如标题高度之类的问题,但其他问题可能无法修复。

    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 真的吗?你测试了吗?
    • 这似乎适用于常规的willDisplay 回调,但会削弱任何返回值的方法。部分标题之类的东西将会消失(因为 UIKit 依赖于委托来托管这些)。
    • 虽然这适用于滑动操作,但它本质上会破坏与 SwiftUI 表格其余部分的任何交互。显然,被覆盖的委托处理了所有这些,但是将方法重新委托给原始委托将无济于事。
    • @Koraktor 哪些特定功能无法修复?
    • 我在列表中有 NavigationLinks。一旦我使用您的修饰符,它们就会停止工作。可能是因为新代表不处理单元格上的点击。
    【解决方案4】:

    我想要相同的,现在有以下实现。

    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()
        }
    }
    

    【讨论】:

    • 提供完整答案,
    • 我试过这个,也许我们可以得到一些好的结果,但老实说,使用滚动视图很困难,如果我有一个使用 UITableView 的本机实现,它不会接近我得到的。您是否同时改进了代码?
    【解决方案5】:

    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")
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

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

      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)")
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        很高兴看到 iOS 15 通过易于使用的 API 将期待已久的 .swipeActions 视图修饰符带到 SwiftUI 中的 List

        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")
                        }
                    }
                }
            }
        }
        

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

        上面的例子产生:

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

        Read more in Apple's developer docs

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-10-10
          • 2022-11-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-07-14
          • 2014-08-07
          • 2020-05-15
          相关资源
          最近更新 更多