SwipeActions

Latest release

contact: @lexkraev Telegram Group

corner radius

用于为任何 SwiftUI View 创建完全可定制的滑动操作的库,类似于 Apple 的 swipeActions(edge:allowsFullSwipe:content:),该操作从 iOS 15 开始可用,并且仅在 List 中可用 🤷🏼‍♂️。你可以在以 iOS 13 为目标的任何视图(例如 TextVStack)的项目中使用 SwipeActions

👨🏻‍💻 欢迎订阅 telegram 上的 SwiftUI dev 频道。

要求

比较表

功能支持 此 SPM 其他
SwiftUI 🟢 🟢
SPM 🟢 🟢
MacOS 🟢 🔴
iOS 13.0 🟢 🔴
RTL 语言 🟢 🔴
完全滑动模式 🟢 🔴
灵活性 🟢 🟡
触感反馈 🟢 🟡
易用性 🟢 🔴

安装

Swift Package Manager

要使用 SwiftPM 将 SwipeActions 集成到您的项目中,请将以下内容添加到您的 Package.swift

dependencies: [
    .package(url: "https://github.com/c-villain/SwipeActions", from: "0.1.0"),
],

或者通过 XcodeGen 插入到您的 project.yml

name: YourProjectName
options:
  deploymentTarget:
    iOS: 13.0
packages:
  SwipeActions:
    url: https://github.com/c-villain/SwipeActions
    from: 0.1.0
targets:
  YourTarget:
    type: application
    ...
    dependencies:
       - package: SwipeActions

类型

不同类型的菜单

Example for .swiped and .slided menu

两种类型都可以升级为完全滑动

Example of full swipe with non-destructive role

快速开始

添加前导和尾随滑动操作

Example with leading and trailing swipes

.addSwipeAction { ... } 修饰符内部使用 Leading { ... }Trailing { ... } 闭包

import SwipeActions

struct YourView: View {
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1...100, id: \.self) { cell in
                    Text("Cell \(cell)")
                        .frame(height: 50, alignment: .center)
                        .frame(maxWidth: .infinity)
                        .contentShape(Rectangle())
                        .addSwipeAction {
                            Leading { //<= HERE 
                                Button {
                                    print("edit \(cell)")
                                } label: {
                                    Image(systemName: "pencil")
                                        .foregroundColor(.white)
                                }
                                .frame(width: 60, height: 50, alignment: .center)
                                .contentShape(Rectangle())
                                .background(Color.green)

                            }
                            Trailing { //<= HERE 
                                HStack(spacing: 0) {
                                    Button {
                                        print("remove \(cell)")
                                    } label: {
                                        Image(systemName: "trash")
                                            .foregroundColor(.white)
                                    }
                                    .frame(width: 60, height: 50, alignment: .center)
                                    .contentShape(Rectangle())
                                    .background(Color.red)
    
                                    Button {
                                        print("Inform \(cell)")
                                    } label: {
                                        Image(systemName: "bell.slash.fill")
                                            .foregroundColor(.white)
                                    }
                                    .frame(width: 60, height: 50, alignment: .center)
                                    .background(Color.blue)
                                }
                            }
                        }
                }
            }
        }
    }
}
在侧面添加多个操作

不要忘记您的操作通常是子视图,特别是按钮或其他东西。请安排它们

YourView()
.addSwipeAction(edge: .trailing) {
    HStack(spacing: 0) { // <= 👀 Look here 
        Rectangle()
            .fill(Color.green.opacity(0.8))
            .frame(width: 8.0, height: 80)
        
        Button {
        } label: {
            Image(systemName: "message")
                .foregroundColor(.white)
                .frame(width: 60, height: 80)
                .contentShape(Rectangle())
        }
        .background(Color.blue)
    }
}
向视图的一侧添加滑动操作

Example with trailing swipe menu

使用 .addSwipeAction(edge: ) { ... } 修饰符,edge - HorizontalAlignment 值输入参数 - 带有使用 .leading.trailing 的两种情况

import SwipeActions

struct YourView: View {
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1...100, id: \.self) { cell in
                    Text("Cell \(cell)")
                        .frame(height: 50, alignment: .center)
                        .frame(maxWidth: .infinity)
                        .contentShape(Rectangle())
                        .addSwipeAction(edge: .trailing) { // <= choose here .trailing or .leading
                            HStack(spacing: 0) {
                                Button {
                                    print("remove \(cell)")
                                } label: {
                                    Image(systemName: "trash")
                                        .foregroundColor(.white)
                                }
                                .frame(width: 60, height: 50, alignment: .center)
                                .contentShape(Rectangle())
                                .background(Color.red)
                                
                                Button {
                                    print("Inform \(cell)")
                                } label: {
                                    Image(systemName: "bell.slash.fill")
                                        .foregroundColor(.white)
                                }
                                .frame(width: 60, height: 50, alignment: .center)
                                .background(Color.blue)
                            }
                        }
                }
            }
        }
    }
}
对于在滑动期间自动关闭其他打开的操作

Example with auto closing swipe actions

SwipeState 变量添加到您的 View,并将其作为 binding 传递到 .addSwipeAction(state:)

struct YourView: View {  
   @State var state: SwipeState = .untouched // <= HERE

   var body: some View {
        ScrollView {
             VStack(spacing: 2) {
                 ForEach(1 ... 30, id: \.self) { cell in
                     Text("Cell \(cell)")
                         .addSwipeAction(state: $state) { // <= HERE
                            ....
                         }
                  }
             }
        }
   }
}

完全滑动操作

对于完全滑动,请使用修饰符 .addFullSwipeAction(menu:swipeColor:swipeRole:state:content:action:)

基本上,完全滑动操作有两个主要的 SwipeRole.destructive(默认)和其他一个。

.destructive(破坏性)

此角色用于关闭/隐藏/移除单元格。

Example of full swipe with destructive role

struct YourView: View {  
   
   @State var range: [Int] = [1,2,3,4,5,6,7,8,9,10]

   var body: some View {
        ScrollView {
             VStack(spacing: 2) {
                 ForEach(range, id: \.self) { cell in
                     Text("Cell \(cell)")
                         .addFullSwipeAction(
                             menu: .slided,
                             swipeColor: .red) { // <= Color is the same as last button in Trailing for full effect 
                                  Leading { 
                                      ...
                                  }
                                  Trailing {
                                      ...
                                      
                                      Button {
                                          withAnimation { 
                                              if let index = range.firstIndex(of: cell) {
                                                  range.remove(at: index)
                                              }
                                          }
                                      } label: {
                                          Image(systemName: "trash")
                                              .foregroundColor(.white)
                                      }
                                      .contentShape(Rectangle())
                                      .frame(width: 60)
                                      .frame(maxHeight: .infinity)
                                      .background(Color.red) // <=== Look here
                                  }
                              } action: { // <= action for full swiping
                                  withAnimation {
                                      if let index = range.firstIndex(of: cell) {
                                          range.remove(at: index)
                                      }
                                  }
                              }
                  }
             }
        }
   }
}
.default(默认)

此角色用于对单元格执行某些操作。

Example of full swipe with non-destructive role

struct YourView: View {  ]

   var body: some View {
        ScrollView {
             VStack(spacing: 2) {
                 ForEach(1...10, id: \.self) { cell in
                     Text("Cell \(cell)")
                         .addFullSwipeAction(menu: .slided,
                                             swipeColor: .green, // <=== Color is the same as last button in Trailing for full effect 
                                             swipeRole: .defaults) {  // <=== Add this parameter
                                  Leading { 
                                      ...
                                  }
                                  Trailing {
                                      HStack(spacing: 0) {
                                          ...
                                          
                                          Button {
          
                                          } label: {
                                              Image(systemName: "trash")
                                                  .foregroundColor(.white)
                                          }
                                          .contentShape(Rectangle())
                                          .frame(width: 60)
                                          .frame(maxHeight: .infinity)
                                          .background(Color.green) // <=== Look here
                                      }
                                  }
                              } action: { // <=== action for full swiping
                                  ...
                              }
                  }
             }
        }
   }
}

使用建议

对于动态高度内容。

使用 .frame(maxHeight: .infinity)

YourView()
    .addSwipeAction(menu: .slided, edge: .trailing) {
        Button {
            ...
        } label: {
            Image("trash")
                .font(.system(size: 20.0))
                .foregroundColor(.white)
                .frame(width: 68, alignment: .center)
                .frame(maxHeight: .infinity) // <= Look here
                .background(.red)
        }
    }
对于透明彩色视图。

对于与 .slided 类型一起使用,没有 限制或任何建议!

对于 .swiped,使用 非透明 颜色层或与 alfa = 1.0 相同的颜色

ForEach(1 ... 30, id: \.self) { cell in
   Text("Cell \(cell)")
       .padding()
       .frame(height: 80)
       .frame(maxWidth: .infinity)
       //.background(Color.green.opacity(0.2)) // <== ❌ DON'T USE SUCH WAY!
       //.background(Color(red: 0.841, green: 0.956, blue: 0.868)) // <== ✅ USE THIS WAY!
       .background( // <== OR THIS WAY!
           ZStack {
               Color(UIColor.systemBackground) // non-transparent color layer
               Color.green.opacity(0.2)
           }
       )
       .contentShape(Rectangle())
       .listStyle(.plain)
       .addSwipeAction(menu: .swiped, // <=== SWIPED TYPE
                       state: $state) {
           Leading {
           ...
           }
       }
       ...
 }
对于 List。

基本上,如果您的应用程序的最低部署目标是 iOS 15,我建议使用 Apple 的 滑动操作 用于 List。无论如何,您都可以使用这个。

由于使用 List 的一些特性,您应该

List(elements) { e in
    Text(e.name)
        .frame(width: UIScreen.main.bounds.size.width - 32, height: 80) // <= HERE
        .background(Color(UIColor.systemBackground))
        .onTapGesture { // <=== HERE
            print("on cell tap!")
        }
        .addSwipeAction(menu: .swiped,
                        edge: .trailing,
                        state: $state) {
            Button {
                print("remove")
            } label: {
                Image(systemName: "trash")
                    .foregroundColor(.white)
            }
            .frame(width: 60, height: 80, alignment: .center) // <= HERE
            .contentShape(Rectangle())
            .background(Color.red)
        }
                        .listRowInsets(EdgeInsets()) // <=== HERE
}
.padding(16)

在示例中查找代码。

对于没有水平填充的视图。

为了避免在没有水平填充的视图后立即开始显示滑动操作中的内容的效果

Demo without insets

.addSwipeAction { ... } 中,添加一个填充了与根视图相同颜色的 Rectangle

Demo with insets

 YourView()
     .frame(height: 80)
     .frame(maxWidth: .infinity)
     .background(Color.green.opacity(0.8)) // <= Look here
     .addSwipeAction(edge: .trailing) {
        HStack(spacing: 0) {
             Rectangle() // <=== HERE!
                 .fill(Color.green.opacity(0.8)) // <= 💡 Don't forget!
                 .frame(width: 8.0, height: 80)
    
             Button {
             } label: {
                 Image(systemName: "message")
                     .foregroundColor(.white)
             }
             .frame(width: 60, height: 80)
             .contentShape(Rectangle())
             .background(Color.blue)
        }
     }
对于上下文菜单。

Demo without insets

由于 SwiftUI 难以检测滑动视图和打开上下文菜单的手势,我建议您在 .addSwipeAction(或 addFullSwipeAction)之后使用 .contextMenu

 YourView()
     .frame(height: 80)
     .frame(maxWidth: .infinity)
    .contentShape(Rectangle()) 
    .padding()
    .background(Color(UIColor.systemBackground))
    .addFullSwipeAction(...) { ... }  // <=== Look here!
    .contextMenu { ... }

实际上,如果您不使用 .contentShape(Rectangle()),您也可以在 .addSwipeAction(或 addFullSwipeAction)之前添加 .contextMenu

 YourView()
     .frame(height: 80)
     .frame(maxWidth: .infinity)
    //.contentShape(Rectangle()) // <=== Look here!
    .padding()
    .contextMenu { ... } // <=== Look here!
    .background(Color(UIColor.systemBackground))
    .addFullSwipeAction(...) { ... } // <=== Look here!
如何添加滑动提示。

使用修饰符 .swipeHint

应用此修饰符的位置取决于菜单的类型。

对于 .slided 类型的菜单,严格在 .addSwipeAction 之后添加 .swipeHint 👇🏻

ForEach(range, ...) {
    YourCell()
        ...
        .addFullSwipeAction(
            menu: .slided, // <== LOOK HERE
            swipeColor: .red,
            state: $state) {
                Leading {
                    ...
                }
                Trailing {
                    ...
                }
            }
        .swipeHint(cell == range.first, hintOffset: 120.0) // for trailing <== LOOK HERE
        .swipeHint(cell == range[1] , hintOffset: -120.0) // for leading <== LOOK HERE
    ...
}

对于 .swiped 类型的菜单,严格在 .addSwipeAction 之前添加 .swipeHint 👇🏻

ForEach(range, ...) {
    YourCell()
        ...
        .swipeHint(cell == range.first, hintOffset: 120.0) // for trailing <== LOOK HERE
        .swipeHint(cell == range[1], hintOffset: -120.0) // for leading <== LOOK HERE
        .addFullSwipeAction(
            menu: .swiped, // <== Look here
            swipeColor: .red,
            state: $state) {
                Leading {
                    ...
                }
                Trailing {
                    ...
                }
            }
    ...
}
多点触控。

由于 SwiftUI 的理念通常不太可能控制多点触控,特别是拖动多个单元格。无论如何,我们可以使用特殊的视图修饰符禁用多点触控:.allowMultitouching(false) 基于 UIKit。在使用滑动操作之前严格添加此修饰符

...
YourView(...)
      .allowMultitouching(false) // <= Look here
      .addSwipeAction( ...) {
        ...
      }
...

默认情况下,此标志为 true。使用此修饰符将重复 telegram 的行为,即您在多点触控期间只能拖动一个单元格。

实际上,该问题仅存在于完全滑动模式下。在默认模式下,您可以拖动多个单元格,但在结束触摸后,只会打开一个单元格。

但由于此解决方案基于 UIKit,您无法使用 drawingGroup() 优化 YourView 的渲染

...
YourView(...)
      .allowMultitouching(false) // <= look here
      .addSwipeAction( ...) {
        ...
      }
      .drawingGroup() // <=  ❌ DON'T DO THAT
...

你肯定会得到这个

Drawing group after swipeactions

实际上,渲染将由 SwiftUI 引擎优化... 为了完全控制,您可以将视图修饰符 .identifier(your id) 添加到 YourView(...)

优化渲染。

要控制视图的 id 以进行优化,您应该使用修饰符 .identifier(your id)

ForEach(...) { cell in
     YourView(cell)
         .addSwipeAction(...) {}
         .identifier(cell) // <= Look here
 }

实际上,如果您忘记添加标识符,请不要担心,系统会自动添加 id。

滑动灵敏度。

要控制滑动灵敏度(拖动的最小距离),您应该使用修饰符 swipeSensitive

ForEach(...) { cell in
     YourView(cell)
         .addSwipeAction(...) {}
         .swipeSensitive(.medium) // <= Look here. You can control it here 
 }
 .swipeSensitive(.medium) // <= Look here. Or here! 

如果您忘记添加灵敏度,请不要担心,系统将提供其自己的默认 low 值。实际上,这是最舒适的行为。

支持从右到左

此库支持从右到左的语言,如阿拉伯语和希伯来语。

通过将 .environment(\.layoutDirection, .rightToLeft) 添加到带有滑动操作的视图来检查它。

查看示例
struct ContentView: View {
    var body: some View {
        LazyVStack {
            ForEach(0..<5) { index in
                Text("Item \(index)")
                    .swipeActions {
                        Button("Delete") {
                            print("Item deleted")
                        }
                        .tint(.red)
                    }
            }
        }
        .environment(\.layoutDirection, .rightToLeft) // <= Look here
    }
}

触感反馈支持

默认情况下,在完全滑动模式下,操作具有触感反馈。

要禁用它,请使用 .allowFullSwipeHaptics(false)

查看示例
YourView()
    .addFullSwipeAction(...) { ... }
    .allowFullSwipeHaptics(false) // <= Look here

您可以使用 .fullSwipeHapticFeedback(:) 轻松更改此反馈类型。

查看示例
YourView()
    .addFullSwipeAction(...) { ... }
    .fullSwipeHapticFeedback(.medium()) // <= Look here

您可以轻松地将触感反馈添加到滑动操作中的自己的按钮。

查看示例
YourView()
    .addFullSwipeAction(...) { 
        ... 
    Trailing {
        Button {
            HapticsProvider.sendHapticFeedback(.heavy()) // <= Look here
                ...
            } label: { ... }
     }
}

交流

许可证

SwipeActions 包是在 MIT 许可证下发布的。

特别感谢

感谢 Prafulla Singh 通过他的 SwiftUI 教程 提供的灵感。