diff --git a/README.md b/README.md index dc4a6af..e0a7825 100644 --- a/README.md +++ b/README.md @@ -56,24 +56,29 @@ Added an example project, with **iOS** target: https://github.com/mroffmix/Swipe ### Create array of actions ```swift -var leftActions = [ - Action(title: "Note", iconName: "pencil", bgColor: .note, action: {}), - Action(title: "Edit doc", iconName: "doc.text", bgColor: .edit, action: {}), - Action(title: "New doc", iconName: "doc.text.fill", bgColor: .done, action: {}) - ] -var rightActions = [Action(title: "Delete", iconName: "trash", bgColor: .delete, action: {})] +let left = [ + Action(title: "Note", iconName: "pencil", bgColor: .red, action: {}), + Action(title: "Edit doc", iconName: "doc.text", bgColor: .yellow, action: {}), + Action(title: "New doc", iconName: "doc.text.fill", bgColor: .green, action: {}) +] + +let right = [ + Action(title: "Note", iconName: "pencil", bgColor: .blue, action: {}), + Action(title: "Edit doc", iconName: "doc.text", bgColor: .yellow, action: {}) +] ``` ### Create SwipeableView ```swift SwipeableView(content: { - - // your view content here - - }, - leftActions: Example.leftActions, - rightActions: Example.rightActions, - rounded: false) - .frame(height: 90) + GroupBox { + Text("View content") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +}, +leftActions: left, +rightActions: right, +rounded: true +).frame(height: 90) ``` ![Swipeable View](https://github.com/mroffmix/SwipebleView/blob/main/Resources/IndependedView.gif) @@ -94,7 +99,7 @@ leftActions: Example.leftActions, rightActions: Example.rightActions, rounded: true, container: container) -.frame(height: 140) +.frame(height: 100) ``` Views behaviour in a container diff --git a/Resources/IndependedView.gif b/Resources/IndependedView.gif index 2dc98dc..39febf3 100644 Binary files a/Resources/IndependedView.gif and b/Resources/IndependedView.gif differ diff --git a/Resources/ViewsInAContainer.gif b/Resources/ViewsInAContainer.gif index 702f016..c8513e2 100644 Binary files a/Resources/ViewsInAContainer.gif and b/Resources/ViewsInAContainer.gif differ diff --git a/Resources/WholeScreen.gif b/Resources/WholeScreen.gif index 8b2cf9b..24034a5 100644 Binary files a/Resources/WholeScreen.gif and b/Resources/WholeScreen.gif differ diff --git a/Resources/sample.gif b/Resources/sample.gif index 930e36b..7da3276 100644 Binary files a/Resources/sample.gif and b/Resources/sample.gif differ diff --git a/Sources/SwipeableView/View/EditActions.swift b/Sources/SwipeableView/View/EditActions.swift index 1e75244..f67ae3f 100644 --- a/Sources/SwipeableView/View/EditActions.swift +++ b/Sources/SwipeableView/View/EditActions.swift @@ -31,13 +31,17 @@ public struct EditActions: View { .padding(.bottom, 8) #endif #if os(iOS) - Image(systemName: action.iconName) - .font(.system(size: 20)) - .padding(.bottom, 8) + if getWidth() > 35 { + Image(systemName: action.iconName) + .font(.system(size: 20)) + .padding(.bottom, 8) + .opacity(getWidth() < 30 ? 0.1 : 1 ) + } + #endif if viewModel.actions.count < 4 && height > 50 { - Text(action.title) + Text(getWidth() > 70 ? action.title : "") .font(.system(size: 10, weight: .semibold)) .multilineTextAlignment(.center) .lineLimit(3) @@ -46,17 +50,36 @@ public struct EditActions: View { } .padding() .frame(width: getWidth(), height: height) - .background(action.bgColor.value.saturation(0.8)) + .background(action.bgColor.opacity(getWidth() < 30 ? 0.1 : 1 )) .cornerRadius(rounded ? 10 : 0) } private func getWidth() -> CGFloat { - let width = CGFloat(abs(offset.width) / CGFloat(viewModel.actions.count)) - if width < 80 { - return 80 + let width = CGFloat(offset.width / CGFloat(viewModel.actions.count)) + // - left / + right + switch side { + case .left: + if width < 0 { + return addPaddingsIfNeeded(width: abs(width)) + } else { + return 0 + } + case .right: + if width > 0 { + return addPaddingsIfNeeded(width: abs(width)) + } else { + return 0 + } + } + + } + + private func addPaddingsIfNeeded(width:CGFloat) -> CGFloat { + if rounded { + return width - 5 > 0 ? width - 5 : 0 } else { - return rounded ? width - 5 : width + return width } } @@ -117,27 +140,27 @@ public struct EditActions: View { struct EditActions_Previews: PreviewProvider { static var actions = [ - Action(title: "No interest", iconName: "trash", bgColor: .delete, action: {}), - Action(title: "Request offer", iconName: "doc.text", bgColor: .edit, action: {}), - Action(title: "Order", iconName: "doc.text.fill", bgColor: .delete, action: {}), - Action(title: "Order provided", iconName: "car", bgColor: .done, action: {}), + Action(title: "No interest", iconName: "trash", bgColor: .red, action: {}), + Action(title: "Request offer", iconName: "doc.text", bgColor: .yellow, action: {}), + Action(title: "Order", iconName: "doc.text.fill", bgColor: .red, action: {}), + Action(title: "Order provided", iconName: "car", bgColor: .green, action: {}), ] static var previews: some View { Group { - EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(.zero), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .right, rounded: false) + EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(CGSize.init(width: 300, height: 10)), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .right, rounded: false) .previewLayout(.fixed(width: 450, height: 400)) - EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(.zero), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: false) + EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(CGSize.init(width: -300, height: 10)), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: false) .previewLayout(.fixed(width: 450, height: 100)) - EditActions(viewModel: EditActionsVM(actions, maxActions: 2), offset: .constant(.zero), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: false) + EditActions(viewModel: EditActionsVM(actions, maxActions: 2), offset: .constant(CGSize.init(width: -300, height: 10)), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: false) .previewLayout(.fixed(width: 450, height: 150)) - EditActions(viewModel: EditActionsVM(actions, maxActions: 3), offset: .constant(.zero), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .right, rounded: true) + EditActions(viewModel: EditActionsVM(actions, maxActions: 3), offset: .constant(CGSize.init(width: 300, height: 10)), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .right, rounded: true) .previewLayout(.fixed(width: 450, height: 100)) - EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(.zero), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: true) + EditActions(viewModel: EditActionsVM(actions, maxActions: 4), offset: .constant(CGSize.init(width: -300, height: 10)), state: .constant(.center), onChangeSwipe: .constant(.noChange), side: .left, rounded: true) .previewLayout(.fixed(width: 550, height: 180)) diff --git a/Sources/SwipeableView/View/SwipeableView.swift b/Sources/SwipeableView/View/SwipeableView.swift index a7e3721..8715137 100644 --- a/Sources/SwipeableView/View/SwipeableView.swift +++ b/Sources/SwipeableView/View/SwipeableView.swift @@ -18,18 +18,18 @@ enum OnChangeSwipe { case rightStarted case noChange } + public struct SwipeableView: View { @Environment(\.colorScheme) var colorScheme - + @ObservedObject var viewModel: SWViewModel + var container: SwManager? var rounded: Bool var leftActions: EditActionsVM - var rightActions: EditActionsVM - @ObservedObject var viewModel: SWViewModel - - //@State private var onChangeSwipe: OnChangeSwipe = .noChange - + var rightActions: EditActionsVM let content: Content + @State var finishedOffset: CGSize = .zero + public init(@ViewBuilder content: () -> Content, leftActions: [Action], rightActions: [Action], rounded: Bool = false, container: SwManager? = nil ) { self.content = content() @@ -38,86 +38,100 @@ public struct SwipeableView: View { self.rounded = rounded viewModel = SWViewModel(state: .center, size: .zero) + self.container = container + container?.addView(viewModel) + } private func makeView(_ geometry: GeometryProxy) -> some View { return content } - private func makeActions() -> AnyView { - switch viewModel.onChangeSwipe { - case .leftStarted: - - return AnyView(EditActions(viewModel: leftActions, - offset: .init(get: {self.viewModel.dragOffset}, set: {self.viewModel.dragOffset = $0}), - state: .init(get: {self.viewModel.state}, set: {self.viewModel.state = $0}), - onChangeSwipe: .init(get: {self.viewModel.onChangeSwipe}, set: {self.viewModel.onChangeSwipe = $0}), - side: .left, - rounded: rounded) - .transition(.slide)) - - - case .rightStarted : - - return AnyView(EditActions(viewModel: rightActions, - offset: .init(get: {self.viewModel.dragOffset}, set: {self.viewModel.dragOffset = $0}), - state: .init(get: {self.viewModel.state}, set: {self.viewModel.state = $0}), - onChangeSwipe: .init(get: {self.viewModel.onChangeSwipe}, set: {self.viewModel.onChangeSwipe = $0}), - side: .right, - rounded: rounded) - .transition(.slide)) - - case .noChange: - return AnyView(EmptyView()) - + public var body: some View { + + let dragGesture = DragGesture(minimumDistance: 1.0, coordinateSpace: .global) + .onChanged(self.onChanged(value:)) + .onEnded(self.onEnded(value:)) + + return GeometryReader { reader in + self.makeLeftActions() + self.makeView(reader) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .offset(x: self.viewModel.dragOffset.width) + .zIndex(100) + .onTapGesture(count: 1, perform: { self.toCenterWithAnimation()}) + .highPriorityGesture( dragGesture ) + self.makeRightActions() } } + private func makeRightActions() -> AnyView { + + return AnyView(EditActions(viewModel: rightActions, + offset: .init(get: {self.viewModel.dragOffset}, set: {self.viewModel.dragOffset = $0}), + state: .init(get: {self.viewModel.state}, set: {self.viewModel.state = $0}), + onChangeSwipe: .init(get: {self.viewModel.onChangeSwipe}, set: {self.viewModel.onChangeSwipe = $0}), + side: .right, + rounded: rounded) + .animation(.easeInOut)) + } + + private func makeLeftActions() -> AnyView { + + return AnyView(EditActions(viewModel: leftActions, + offset: .init(get: {self.viewModel.dragOffset}, set: {self.viewModel.dragOffset = $0}), + state: .init(get: {self.viewModel.state}, set: {self.viewModel.state = $0}), + onChangeSwipe: .init(get: {self.viewModel.onChangeSwipe}, set: {self.viewModel.onChangeSwipe = $0}), + side: .left, + rounded: rounded) + .animation(.easeInOut)) + } + private func toCenterWithAnimation() { withAnimation(.easeOut) { self.viewModel.dragOffset = CGSize.zero self.viewModel.state = .center - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - withAnimation(Animation.easeOut) { - if self.viewModel.state == .center { - self.viewModel.onChangeSwipe = .noChange - } - } + self.viewModel.onChangeSwipe = .noChange + self.viewModel.otherTapped() } } - private func onChanged(value: DragGesture.Value){ - + private func onChanged(value: DragGesture.Value) { + if self.viewModel.state == .center { - if value.translation.width < 0 && value.translation.height > -30 && value.translation.height < 30 { + + if value.translation.width <= 0 { + //&& value.translation.height > -60 && value.translation.height < 60 self.viewModel.onChangeSwipe = .leftStarted self.viewModel.dragOffset.width = value.translation.width - } else if self.viewModel.dragOffset.width >= 0 && value.translation.height > -30 && value.translation.height < 30{ + + } else if self.viewModel.dragOffset.width >= 0 { + //&& value.translation.height > -60 && value.translation.height < 60 self.viewModel.onChangeSwipe = .rightStarted self.viewModel.dragOffset.width = value.translation.width } } else { - - self.viewModel.dragOffset.width = self.viewModel.dragOffset.width + ((value.translation.width < 0) ? -2 : 2) + // print(value.translation.width) + if self.viewModel.dragOffset.width != .zero { + self.viewModel.dragOffset.width = finishedOffset.width + value.translation.width + // print(self.viewModel.dragOffset.width) + } else { + self.viewModel.onChangeSwipe = .noChange + self.viewModel.state = .center + } } - } - - - private func onEnded(value: DragGesture.Value){ + private func onEnded(value: DragGesture.Value) { - #if DEBUG - // print(viewModel.dragOffset) - #endif + finishedOffset = value.translation - if self.viewModel.dragOffset.width < 0 { + if self.viewModel.dragOffset.width <= 0 { // left - if self.viewModel.state == .center && value.translation.width < -50 { + if self.viewModel.state == .center && value.translation.width <= -50 { var offset = (CGFloat(min(4, self.leftActions.actions.count)) * -80) @@ -131,10 +145,11 @@ public struct SwipeableView: View { } else { self.toCenterWithAnimation() + finishedOffset = .zero } - } else if self.viewModel.dragOffset.width > 0 { + } else if self.viewModel.dragOffset.width >= 0 { // right if self.viewModel.state == .center && value.translation.width > 50{ @@ -149,54 +164,77 @@ public struct SwipeableView: View { } else { self.toCenterWithAnimation() } - } - } - public var body: some View { - - ZStack { - makeActions() - GeometryReader { reader in - self.makeView(reader) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .offset(x: self.viewModel.dragOffset.width) - - .onTapGesture(count: 1, perform: { self.toCenterWithAnimation()}) - .gesture( DragGesture(minimumDistance: 10.0, coordinateSpace: .local).onChanged(self.onChanged(value:)).onEnded(self.onEnded(value:))) - } - - } - } + } @available(iOS 14.0, *) struct SwipebleView_Previews: PreviewProvider { + @ObservedObject static var container = SwManager() static var previews: some View { - GeometryReader { reader in + + let left = [ + Action(title: "Note", iconName: "pencil", bgColor: .red, action: {}), + Action(title: "Edit doc", iconName: "doc.text", bgColor: .yellow, action: {}), + Action(title: "New doc", iconName: "doc.text.fill", bgColor: .green, action: {}) + ] + + let right = [ + Action(title: "Note", iconName: "pencil", bgColor: .blue, action: {}), + Action(title: "Edit doc", iconName: "doc.text", bgColor: .yellow, action: {}) + ] + + return GeometryReader { reader in VStack { Spacer() + HStack { + Text("Independed view:") + .bold() + Spacer() + } SwipeableView(content: { GroupBox { Text("View content") .frame(maxWidth: .infinity, maxHeight: .infinity) } }, - leftActions:[ - Action(title: "Note", iconName: "pencil", bgColor: .note, action: {}), - Action(title: "Edit doc", iconName: "doc.text", bgColor: .edit, action: {}), - Action(title: "New doc", iconName: "doc.text.fill", bgColor: .done, action: {}) - ], - rightActions: [ - Action(title: "Note", iconName: "pencil", bgColor: .note, action: {}), - Action(title: "Edit doc", iconName: "doc.text", bgColor: .edit, action: {}) - ], + leftActions: left, + rightActions: right, rounded: true ).frame(height: 90) + HStack { + Text("Container:") + .bold() + Spacer() + } + + + SwipeableView(content: { + Text("View content") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.blue.opacity(0.5)) + }, + leftActions: left, + rightActions: right, + rounded: false, + container: container + ).frame(height: 90) + + SwipeableView(content: { + Text("View content") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.blue.opacity(0.5)) + }, + leftActions: left, + rightActions: right, + rounded: false, + container: container + ).frame(height: 90) Spacer() - } + }.padding() } } diff --git a/Sources/SwipeableView/ViewModel/EditActionsVM.swift b/Sources/SwipeableView/ViewModel/EditActionsVM.swift index a9ca16e..0b97501 100644 --- a/Sources/SwipeableView/ViewModel/EditActionsVM.swift +++ b/Sources/SwipeableView/ViewModel/EditActionsVM.swift @@ -7,28 +7,9 @@ import SwiftUI -public enum EditActionColor { - case edit - case delete - case done - case note -} - -extension EditActionColor { - var value: Color { - get { - switch self { - case .delete: return .red - case .done: return .green - case .edit: return .yellow - case .note: return .blue - } - } - } -} public struct Action: Identifiable { - public init(title: String, iconName: String, bgColor: EditActionColor, action: @escaping () -> ()?) { + public init(title: String, iconName: String, bgColor: Color, action: @escaping () -> ()?) { self.title = title self.iconName = iconName self.bgColor = bgColor @@ -38,7 +19,7 @@ public struct Action: Identifiable { public let id: UUID = UUID.init() let title: String let iconName: String - let bgColor: EditActionColor + let bgColor: Color let action: () -> ()? } diff --git a/Sources/SwipeableView/ViewModel/SWViewModel.swift b/Sources/SwipeableView/ViewModel/SWViewModel.swift index 2aab494..d0b2a39 100644 --- a/Sources/SwipeableView/ViewModel/SWViewModel.swift +++ b/Sources/SwipeableView/ViewModel/SWViewModel.swift @@ -5,8 +5,8 @@ // Created by Ilya on 14.10.20. // -import Combine import SwiftUI +import Combine public class SWViewModel: ObservableObject { let id: UUID = UUID.init() @@ -22,27 +22,20 @@ public class SWViewModel: ObservableObject { @Published var dragOffset: CGSize let stateDidChange = PassthroughSubject() + let otherActionTapped = PassthroughSubject() init(state: ViewState, size: CGSize) { self.state = state self.dragOffset = size } + public func otherTapped(){ + self.otherActionTapped.send(true) + } + public func goToCenter(){ - DispatchQueue.main.async { - - withAnimation { - - self.dragOffset = .zero - } - - withAnimation { - self.state = .center - self.onChangeSwipe = .noChange - - } - } - - + self.dragOffset = .zero + self.state = .center + self.onChangeSwipe = .noChange } } diff --git a/Sources/SwipeableView/ViewModel/SwManager.swift b/Sources/SwipeableView/ViewModel/SwManager.swift index bb1e555..b49a579 100644 --- a/Sources/SwipeableView/ViewModel/SwManager.swift +++ b/Sources/SwipeableView/ViewModel/SwManager.swift @@ -23,12 +23,8 @@ public class SwManager: ObservableObject { public func addView(_ view: SWViewModel) { views.append(view) - view.stateDidChange.sink(receiveValue: { vm in if self.views.count != 0 { - #if DEBUG - //print("swiped = \(vm.id.uuidString)") - #endif self.views.forEach { if vm.id != $0.id && $0.state != .center{ $0.goToCenter() @@ -36,5 +32,13 @@ public class SwManager: ObservableObject { } } }).store(in: &subscriptions) + + view.otherActionTapped.sink(receiveValue: { _ in + if self.views.count != 0 { + self.views.forEach { + $0.goToCenter() + } + } + }).store(in: &subscriptions) } } diff --git a/Tests/SwipeableViewTests/SwipeableViewTests.swift b/Tests/SwipeableViewTests/SwipeableViewTests.swift index 3f27fb4..aecc6f2 100644 --- a/Tests/SwipeableViewTests/SwipeableViewTests.swift +++ b/Tests/SwipeableViewTests/SwipeableViewTests.swift @@ -8,7 +8,7 @@ final class swipableviewTests: XCTestCase { var subscriptions = Set() func testAction() { - let action = Action(title: "Foo", iconName: "trash", bgColor: .delete, action: { print("Foo") }) + let action = Action(title: "Foo", iconName: "trash", bgColor: .red, action: { print("Foo") }) XCTAssertNotNil(action) @@ -16,7 +16,7 @@ final class swipableviewTests: XCTestCase { XCTAssert(action.title == "Foo") XCTAssert(action.iconName == "trash") - XCTAssert(action.bgColor == .delete) + XCTAssert(action.bgColor == .red) } @@ -36,7 +36,8 @@ final class swipableviewTests: XCTestCase { model.state = .right XCTAssert(model.state == .right) - // model.goToCenter() + model.goToCenter() + XCTAssert(model.state == .center) }