Skip to content

Commit

Permalink
Merge pull request #1 from c-villain/fix-drag-and-lazy
Browse files Browse the repository at this point in the history
0.2.0
  • Loading branch information
c-villain authored Dec 30, 2021
2 parents 4736809 + 4fe6eae commit 240f80e
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 55 deletions.
178 changes: 128 additions & 50 deletions Sources/SwipeActions/SwipeActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import SwiftUI
public typealias Leading<V> = Group<V> where V:View
public typealias Trailing<V> = Group<V> where V:View

public enum MenuType {
case slided /// hstacked
case swiped /// zstacked
}

public struct SwipeAction<V1: View, V2: View>: ViewModifier {

enum VisibleButton {
Expand All @@ -22,23 +27,39 @@ public struct SwipeAction<V1: View, V2: View>: ViewModifier {
@State private var oldOffset: CGFloat = 0
@State private var visibleButton: VisibleButton = .none

/**
To detect if drag gesture is ended because of known issue that drag gesture onEnded not called:
https://stackoverflow.com/questions/58807357/detect-draggesture-cancelation-in-swiftui
*/
@GestureState private var dragGestureActive: Bool = false

@State private var maxLeadingOffset: CGFloat = .zero
@State private var minTrailingOffset: CGFloat = .zero
@State private var contentHeight: CGFloat = .zero

/**
For lazy views: because of measuring size occurred every onAppear
*/
@State private var maxLeadingOffsetIsCounted: Bool = false
@State private var minTrailingOffsetIsCounted: Bool = false

private let menuTyped: MenuType
private let leadingSwipeView: Group<V1>?
private let trailingSwipeView: Group<V2>?

init(@ViewBuilder _ content: @escaping () -> TupleView<(Leading<V1>, Trailing<V2>)>) {
init(menu: MenuType, @ViewBuilder _ content: @escaping () -> TupleView<(Leading<V1>, Trailing<V2>)>) {
menuTyped = menu
leadingSwipeView = content().value.0
trailingSwipeView = content().value.1
}

init(@ViewBuilder leading: @escaping () -> V1) {
init(menu: MenuType, @ViewBuilder leading: @escaping () -> V1) {
menuTyped = menu
leadingSwipeView = Group { leading() }
trailingSwipeView = nil
}

init(@ViewBuilder trailing: @escaping () -> V2) {
init(menu: MenuType, @ViewBuilder trailing: @escaping () -> V2) {
menuTyped = menu
trailingSwipeView = Group { trailing() }
leadingSwipeView = nil
}
Expand All @@ -49,59 +70,116 @@ public struct SwipeAction<V1: View, V2: View>: ViewModifier {
oldOffset = 0
}

public func body(content: Content) -> some View {
ZStack {
content
.measureSize {
contentHeight = $0.height
var leadingView: some View {
leadingSwipeView
.measureSize {
if !maxLeadingOffsetIsCounted {
maxLeadingOffset = maxLeadingOffset + $0.width
}
.contentShape(Rectangle()) ///otherwise swipe won't work in vacant area
.offset(x: offset)
.gesture(DragGesture(minimumDistance: 15, coordinateSpace: .local)
.onChanged({ (value) in
let totalSlide = value.translation.width + oldOffset
if (0...Int(maxLeadingOffset) ~= Int(totalSlide)) || (Int(minTrailingOffset)...0 ~= Int(totalSlide)) { //left to right slide
withAnimation{
offset = totalSlide
}
}
.onAppear {
/**
maxLeadingOffsetIsCounted for of lazy views
*/
maxLeadingOffsetIsCounted = true
}
}

var trailingView: some View {
trailingSwipeView
.measureSize {
if !minTrailingOffsetIsCounted {
minTrailingOffset = (abs(minTrailingOffset) + $0.width) * -1
}
}
.onAppear {
/**
maxLeadingOffsetIsCounted for of lazy views
*/
minTrailingOffsetIsCounted = true
}
}

var swipedMenu: some View {
HStack(spacing: 0) {
leadingView
Spacer()
trailingView
}
}

var slidedMenu: some View {
HStack(spacing: 0) {
leadingView
.offset(x: (-1 * maxLeadingOffset) + offset)
Spacer()
trailingView
.offset(x: (-1 * minTrailingOffset) + offset)
}
}

func gesturedContent(content: Content) -> some View {
content
.contentShape(Rectangle()) ///otherwise swipe won't work in vacant area
.offset(x: offset)
.gesture(
DragGesture(minimumDistance: 15, coordinateSpace: .local)
.updating($dragGestureActive) { value, state, transaction in
state = true
}
///can update this logic to set single button action with filled single button background if scrolled more then buttons width
})
.onEnded({ value in
withAnimation {
if visibleButton == .left && value.translation.width < -20 { ///user dismisses left buttons
reset()
} else if visibleButton == .right && value.translation.width > 20 { ///user dismisses right buttons
reset()
} else if offset > 25 || offset < -25 { ///scroller more then 50% show button
if offset > 0 {
visibleButton = .left
offset = maxLeadingOffset
.onChanged { value in
let totalSlide = value.translation.width + oldOffset

if (0...Int(maxLeadingOffset) ~= Int(totalSlide)) || (Int(minTrailingOffset)...0 ~= Int(totalSlide)) {
withAnimation {
offset = totalSlide
}
}
}.onEnded { value in
print("gesture is ended!")
withAnimation {
if visibleButton == .left && value.translation.width < -20 { ///user dismisses left buttons
reset()
} else if visibleButton == .right && value.translation.width > 20 { ///user dismisses right buttons
reset()
} else if offset > 20 || offset < -20 { ///scroller more then 50% show button
if offset > 0 {
visibleButton = .left
offset = maxLeadingOffset
} else {
visibleButton = .right
offset = minTrailingOffset
}
oldOffset = offset
///Bonus Handling -> set action if user swipe more then x px
} else {
visibleButton = .right
offset = minTrailingOffset
reset()
}
}
})
.valueChanged(of: dragGestureActive) { newIsActiveValue in
if newIsActiveValue == false {
withAnimation {
if visibleButton == .none {
reset()
}
oldOffset = offset
///Bonus Handling -> set action if user swipe more then x px
} else {
reset()
}
}
}))
HStack(alignment: .center, spacing: 0) {
leadingSwipeView
.measureSize {
maxLeadingOffset = maxLeadingOffset + $0.width
}
.offset(x: (-1 * maxLeadingOffset) + offset)
Spacer()
trailingSwipeView
.measureSize {
minTrailingOffset = (abs(minTrailingOffset) + $0.width) * -1
}
.offset(x: (-1 * minTrailingOffset) + offset)
}
}

public func body(content: Content) -> some View {
switch menuTyped {
case .slided:
ZStack {
gesturedContent(content: content)
slidedMenu
}
case .swiped:
ZStack {
swipedMenu
gesturedContent(content: content)
}
}
.frame(height: contentHeight)
}
}
10 changes: 5 additions & 5 deletions Sources/SwipeActions/ViewModifiers/SwipeActionModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import SwiftUI
public extension View {

@ViewBuilder
func addSwipeAction<V1: View, V2: View>(@ViewBuilder _ content: @escaping () -> TupleView<(Leading<V1>, Trailing<V2>)>) -> some View {
self.modifier(SwipeAction.init(content))
func addSwipeAction<V1: View, V2: View>(menu: MenuType = .slided, @ViewBuilder _ content: @escaping () -> TupleView<(Leading<V1>, Trailing<V2>)>) -> some View {
self.modifier(SwipeAction.init(menu: menu, content))
}

@ViewBuilder
func addSwipeAction<V1: View>(edge: HorizontalAlignment, @ViewBuilder _ content: @escaping () -> V1) -> some View {
func addSwipeAction<V1: View>(menu: MenuType = .slided, edge: HorizontalAlignment, @ViewBuilder _ content: @escaping () -> V1) -> some View {
switch edge {
case .leading:
self.modifier(SwipeAction<V1, EmptyView>.init(leading: content))
self.modifier(SwipeAction<V1, EmptyView>.init(menu: menu, leading: content))
default:
self.modifier(SwipeAction<EmptyView, V1>.init(trailing: content))
self.modifier(SwipeAction<EmptyView, V1>.init(menu: menu, trailing: content))
}
}
}
22 changes: 22 additions & 0 deletions Sources/SwipeActions/ViewModifiers/ValueChangedModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ValueChangedModifier.swift
//
//
// Created by c-villain on 28.12.2021.
//

import SwiftUI
import Combine

extension View {
/// A backwards compatible wrapper for iOS 14 `onChange`
@ViewBuilder func valueChanged<T: Equatable>(of value: T, onChange: @escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: onChange)
} else {
self.onReceive(Just(value)) { (value) in
onChange(value)
}
}
}
}

0 comments on commit 240f80e

Please sign in to comment.