Code Monkey home page Code Monkey logo

swipeactions's Introduction

SwipeActions

Latest release

contact: @lexkraev Telegram Group

corner radius

Library for creating swipe actions for any SwiftUI View, similar to Apple's swipeActions(edge:allowsFullSwipe:content:) that available from iOS 15 and only in List ๐Ÿคท๐Ÿผโ€โ™‚๏ธ. You can use SwipeActions in project targeting iOS 13 with any view (e.g. Text or VStack).

๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Feel free to subscribe to channel SwiftUI dev in telegram.

Requirements

  • iOS 13.0 or macOS 10.15

Installation

Swift Package Manager

To integrate SwipeActions into your project using SwiftPM add the following to your Package.swift:

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

or via XcodeGen insert into your 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

Types

Different types of menu:

  • .swiped
  • .slided

Example for .swiped and .slided menu

Both types can be upgraded with full swiping:

Example of full swipe with non-destructive role

Quick start

Adding both leading and trailing swipe actions:

Example with leading and trailing swipes

Use Leading { ... } and Trailing { ... } closures inside .addSwipeAction { ... } modifier:

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 

                                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)
                            }
                        }
                }
            }
        }
    }
}
Adding swipe actions to one side of view:

Example with trailing swipe menu

Use .addSwipeAction(edge: ) { ... } modifier, edge - a HorizontalAlignment value input parameter - with two cases of using .leading or .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) { // <== HERE! choose .trailing or .leading
                            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)
                            
                        }
                }
            }
        }
    }
}
For automatically closing other opened actions during sliding:

Example with auto closing swipe actions

Add SwipeState var to your View and pass it as a binding in .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
                            ....
                         }
                  }
             }
        }
   }
}

Full swipe action

For full swipe use modifier .addFullSwipeAction(menu:swipeColor:swipeRole:state:content:action:)

Basically there are two main SwipeRole for full swipe action: .destructive (defaults) and other one.

.destructive

This role is used for closing/hiding/removing cell.

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

This role is used for making some action on cell.

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 {
                                      ...
                                      
                                      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
                                  ...
                              }
                  }
             }
        }
   }
}

Recommendations for use

With dynamic height content.

use .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) // <====== HERE
                .background(.red)
        }
    }
With transparent colored views.

There is no restrictions or any recommendations for using with .slided type!

With .swiped use non-tranparent color layer or the same color with 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 {
           ...
           }
       }
       ...
 }
With List.

Basically if you have minimum deployments target for your app is iOS 15 I recommend to use Apple's swipe actions for List. Anyway you may use this.

Due to some features for working with List you should:

  • specify a frame for cell width, e.g. .frame(width: UIScreen.main.bounds.size.width - 32, height: 80) and a frame for buttons on swipe actions, e.g. .frame(width: 60, height: 80). Note that height in frames should be the same!

  • add modifier .onTapGesture { ... } for cell to override tapping on swipe action buttons

  • add modifier .listRowInsets(EdgeInsets()) for cell

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)

Look for code in the example.

With no horizontal padding views.

To avoid effect when content in swipe actions started showing immediately after view with no horizontal padding

Demo without insets

in .addSwipeAction { ... } add Rectangle filled with same color as root view:

Demo with insets

 YourView()
     .frame(height: 80)
     .frame(maxWidth: .infinity)
     .background(Color.green.opacity(0.8)) // <=== Look here!
     .addSwipeAction(edge: .trailing) {
         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)
     }
With context menu.

Demo without insets

Due to some difficulties for SwiftUI to detect gestures for sliding view and opening context menu I recommend you to use .contextMenu after .addSwipeAction (or addFullSwipeAction):

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

Actually if you don't use .contentShape(Rectangle()), you can also add .contextMenu before .addSwipeAction (or addFullSwipeAction):

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

Use modifier .swipeHint:

ForEach(range, ...) {
    YourCell()
        ...
        .addFullSwipeAction(
            menu: .slided,
            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
    ...
}

Communication

  • If you found a bug, open an issue or submit a fix via a pull request.
  • If you have a feature request, open an issue or submit a implementation via a pull request or hit me up on [email protected] or telegram.
  • If you want to contribute, submit a pull request onto the master branch.

License

SwipeActions package is released under an MIT license.

Special thanks

Thx to Prafulla Singh for inspriration with his SwiftUI tutorial.

swipeactions's People

Contributors

andrey-pagin avatar aromanov91 avatar c-villain avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

swipeactions's Issues

Automatic closing does not work with LazyVStack items that are offscreen

Using the state parameter as described in the documentation, we can automatically close swiped items when a new item is swiped, however, this doesn't work in a LazyVStack.

To repro:

  1. embed a LazyVStack in a ScrollView.
  2. Then add enough items to the LazyVStack so that it overflows the screen.
  3. Scroll to the bottom item and open it's swipe menu.
  4. Scroll the bottom item off the screen and swipe a new item
  5. Scroll to the bottom item and notice that its swipe menu is still open (so now there are two items with swipe menus open)

SwipeState should update

Shouldn't the state for SwipeState update when a cell is swiped? It looks like the code is just comparing the tag's with the id so if we add logic to check if a cell is already swiped, the state is still .untouched.

problem with adding async images

Screenshot 2023-04-14 at 17 05 29

Screenshot 2023-04-14 at 17 06 42

Screenshot 2023-04-14 at 17 07 44

I added an async image to my SwiftUI view, but when I try to fullswipe the view, it triggers with minimum drag , and as i found content width in function measureSize() becomes 0. This issue doesn't happen without the image. How can I fix this problem?

Problem with dragging several cells at same time

Hi! Your lib works perfectly for my app, but i noticed strange behavior:

I need to show button only for single cell per time, and SwipeState property works good, but if you swipe multiple cells at same time, then several cells remain open.

RPReplay_Final1673530851.mp4

Is it a bug, or feature?
Thank you!

Adding cocoapods support

Hi, can we add cocoapods support? If I create pull request, will You publish it to spec repo?

Adding @State to trigger indications of swipe

Thanks for the lib, it would be Nice to trigger a UI element for the swipe behavior of any rows by passing a bool state ,
like the EditButton from the original List, this will help as the user for now doesn't know if he can swipe any cell.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.