SwiftUI DragGesture only in one direction - swiftui

I'd like SwiftUI DragGesture to start only when the gesture is in a specific direction (horizontal/vertical). Is it possible?

Yes, it is, by applying one of the two components (either horizontal or vertical) of the gesture translation to the view offset.
Here's such behaviour implemented as a ViewModifier.
struct DraggableModifier : ViewModifier {
enum Direction {
case vertical
case horizontal
}
let direction: Direction
#State private var draggedOffset: CGSize = .zero
func body(content: Content) -> some View {
content
.offset(
CGSize(width: direction == .vertical ? 0 : draggedOffset.width,
height: direction == .horizontal ? 0 : draggedOffset.height)
)
.gesture(
DragGesture()
.onChanged { value in
self.draggedOffset = value.translation
}
.onEnded { value in
self.draggedOffset = .zero
}
)
}
}
Demo:
struct ContentView: View {
var body: some View {
VStack {
Spacer(minLength: 100)
HStack {
Rectangle()
.foregroundColor(.green)
.frame(width: 100, height: 100)
.modifier(DraggableModifier(direction: .vertical))
Text("Vertical")
}
Spacer(minLength: 100)
Rectangle()
.foregroundColor(.red)
.frame(width: 100, height: 100)
.modifier(DraggableModifier(direction: .horizontal))
Text(verbatim: "Horizontal")
Spacer(minLength: 100)
}
}
}
Result:

Rather old question but didn't find the answer elsewhere.
You can easily check wether it was a left- or a right-swipe, because Gestures return their end state, which contains the starting and final position.
Text("Hello")
.gesture(
DragGesture(minimumDistance: 100)
.onEnded { endedGesture in
if (endedGesture.location.x - endedGesture.startLocation.x) > 0 {
print("Right")
} else {
print("Left")
}
}

This will only move the view if the gesture moves move horizontally than vertically. The isDragging boolean makes sure after the first condition is met, the view will move vertically too. It's not necessary, but it helps for smoother dragging.
#State var dragOffset: CGSize = .zero
#State var isDragging = false
.offset(x: offset.width, y: offset.height)
.gesture(DragGesture()
.onChanged { if abs($0.translation.width) > abs($0.translation.height) || isDragging {
self.offset = $0.translation
self.isDragging = true
}
}
.onEnded {
isDragging = false
})

An example in which, at the start of the movement, a vertical or horizontal movement is determined after which the object moves only in this direction
struct Test: View {
#State private var offsetX = CGFloat.zero
#State private var offsetY = CGFloat.zero
var body: some View {
ZStack {
Divider()
Divider().rotationEffect(.degrees(90))
Divider().rotationEffect(.degrees(45))
Divider().rotationEffect(.degrees(-45))
VStack {
Spacer()
Text("offsetX: \(offsetX) offsetY: \(offsetY)")
.foregroundColor(.black.opacity(0.5))
.frame(maxWidth: .infinity)
.padding(.bottom, 18.0)
}
Rectangle()
.fill(.green)
.frame(width: 300.0, height: 200.0)
.cornerRadius(13.0)
.offset(x: offsetX, y: offsetY)
.gesture(drag)
}
}
#State private var horizontal = true
#State private var vertical = true
var drag: some Gesture {
DragGesture()
.onChanged { dg in
var x = dg.translation.width
var y = dg.translation.height
if ((x < 0 && y < 0 && x < y) || (x < 0 && y > 0 && -x > y) || (x > 0 && y < 0 && x > -y) || (x > 0 && y > 0 && x > y)) && self.horizontal && self.vertical {
self.horizontal = true
self.vertical = false
} else if vertical && horizontal {
self.horizontal = false
self.vertical = true
}
if self.horizontal {
self.offsetX = x
} else {
self.offsetY = y
}
}
.onEnded { _ in
withAnimation {
self.offsetX = .zero
self.offsetY = .zero
}
self.horizontal = true
self.vertical = true
}
}
}
Demo: https://i.stack.imgur.com/Vdbkx.gif

Related

CarouselView inside ScrollView is possible without Drag gesture?

I have created a carousel cards in SwiftUI, it is working on the DragGesture
I want to achieve same experience using scrollview i.e. same design and functionalities using scrollview instead of Drag-gesture
I have created Sample using scrollview but it has some limitation
Here is ScreenShot the upper carousel is using scrollview and lower one using Drag Gesture
import SwiftUI
struct Item: Identifiable {
var id: Int
var title: String
var color: Color
var isSelected: Bool
}
class Store: ObservableObject {
#Published var items: [Item]
let colors: [Color] = [.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo,.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo]
init() {
items = []
for i in 0...15 {
let new = Item(id: i, title: "Item \(i)", color: colors[i], isSelected: false)
items.append(new)
}
}
}
struct ContentView: View {
#StateObject var store = Store()
#State private var draggingItem = 0.0
#State var activeIndex: Int = 0
#State var selectedIndex: Int = 0
#State private var snappedItem = 0.0
let gridItems = [
GridItem(.flexible())
]
var body: some View {
VStack {
Spacer()
Text("Selected Index: \(store.items[selectedIndex].id)")
.fontWeight(.bold)
.padding()
Text("Acticted Index: \(activeIndex)")
.fontWeight(.bold)
.padding()
Spacer()
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollview in
LazyHGrid(rows: gridItems, alignment: .center, spacing: 25) {
ForEach(0..<store.items.count, id: \.self) { index in
GeometryReader { proxy in
let scale = getScale(proxy: proxy)
ZStack() {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.font(.body)
.fontWeight(.light)
}.frame(width: 70, height: 70)
.onTapGesture {
withAnimation {
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
draggingItem = Double(selectedIndex)
activeIndex = selectedIndex
}
}
.overlay(Circle()
.stroke(selectedIndex == index ? .black : .clear, lineWidth: selectedIndex == index ? 2 : 0))
.scrollSnappingAnchor(.bounds)
.scaleEffect(.init(width: (scale * 1.2) , height: (scale * 1.2)))
.animation(.easeOut(duration: 0.2), value: 0)
.padding(.vertical)
.onChange(of: selectedIndex) { newValue in
withAnimation {
scrollview.scrollTo(selectedIndex, anchor: .center)
}
}
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
} //End Geometry
.frame(width: 70, height: 150)
} //End ForEach
} //End Grid
}
}
ZStack {
ForEach(0..<store.items.count, id: \.self) { index in
ZStack {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.padding()
}
.frame(width: 100, height: 100)
.onTapGesture { loc in
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
withAnimation(.linear) {
draggingItem = Double(store.items[index].id)
activeIndex = index
}
}
.overlay(Circle()
.stroke(activeIndex == index ? .white : .clear, lineWidth: activeIndex == index ? 2 : 0))
.scaleEffect(1.0 - abs(distance(store.items[index].id)) * 0.15 )
.offset(x: myXOffset(store.items[index].id), y: 0)
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
}
}
.gesture(
DragGesture()
.onChanged { value in
draggingItem = (snappedItem) + value.translation.width / 100
}
.onEnded { value in
withAnimation {
snappedItem = draggingItem
draggingItem = round(draggingItem).remainder(dividingBy: Double(store.items.count))
//Get the active Item index
self.activeIndex = store.items.count + Int(draggingItem)
if self.activeIndex > store.items.count || Int(draggingItem) >= 0 {
self.activeIndex = Int(draggingItem)
}
}
}
)
}
}
func distance(_ item: Int) -> Double {
return (draggingItem - Double(item)).remainder(dividingBy: Double(store.items.count))
}
func myXOffset(_ item: Int) -> Double {
let angle = Double.pi * 2 / Double(store.items.count) * distance(item)
return sin(angle) * 200
}
func getScale(proxy: GeometryProxy) -> CGFloat {
let midPoint: CGFloat = 200
let viewFrame = proxy.frame(in: CoordinateSpace.global)
var scale: CGFloat = 1.0
let deltaXAnimationThreshold: CGFloat = 70
let diffFromCenter = abs(midPoint - viewFrame.origin.x - deltaXAnimationThreshold / 2)
if diffFromCenter < deltaXAnimationThreshold {
scale = 1 + (deltaXAnimationThreshold - diffFromCenter) / 300
}
return scale
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI - Misaligned Rectangles when Animating Size of ZStack

I'm creating a custom "Disclosure Group" in SwiftUI, that implements a "swipe to delete" kind of function. It works by having two rectangles stacked on top of each other, and the one on the bottom is the red "delete" button you see when you swipe. I also have a boolean value that flags whether or not the component is "expanded," i.e, having larger size. Here is a video of what this looks like:
As you can see, the component expands in size when tapped, and shows a red "delete" button when dragged. But, when unexpanding the component, you can see that part of the delete rectangle shows on the bottom. Below is the implementation, and I'm not sure why the two rectangles don't completely stick together. Does anyone know how I can avoid this glitch?
MRE:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 30) {
TestDisclosure()
TestDisclosure()
TestDisclosure()
Spacer()
}
}
}
struct TestDisclosure: View {
#State var expanded: Bool = false
#State var isDeleting: Bool = false
#State var horzdrag: CGFloat = 0 // the horizontal translation of the drag
#State var predictedEnd: CGFloat = 0 // the predicted end translation of the drag
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
label
.clipped()
.offset(x: getOffset(horzdrag: horzdrag))
.animation(.spring(), value: horzdrag)
}
.offset(x: isDeleting ? -400 : 0)
.animation(.spring(), value: isDeleting)
.transition(.move(edge: .leading))
.gesture(
DragGesture()
.onChanged { gesture in
onDragChange(gesture: gesture)
}
.onEnded { _ in
onDragEnd()
}
)
.cornerRadius(15)
.padding(.horizontal)
.onTapGesture {
withAnimation(.spring()) {
expanded.toggle()
}
}
.frame(maxHeight: expanded ? 150 : 85)
.clipped()
}
private var label: some View {
ZStack {
Rectangle()
.foregroundColor(.teal)
VStack {
HStack {
Text("Test")
Spacer()
Text("1 unit")
Text("12 units")
}
.padding(.horizontal)
}
}
}
private func onDragChange(gesture: DragGesture.Value) {
horzdrag = gesture.translation.width
predictedEnd = gesture.predictedEndTranslation.width
}
private func onDragEnd() {
if getOffset(horzdrag: horzdrag) <= -400 {
withAnimation(.spring()) {
isDeleting = true
}
}
horzdrag = .zero
}
// used to calculate how far to move the teal rectangle
private func getOffset(horzdrag: CGFloat) -> CGFloat {
if isDeleting {
return -400
} else if horzdrag < -165 {
return -400
} else if predictedEnd < -60 && horzdrag == 0 {
return -80
} else if predictedEnd < -60 {
return horzdrag
} else if predictedEnd < 50 && horzdrag > 0 && (-80 + horzdrag <= 0) {
return -80 + horzdrag
} else if horzdrag < 0 {
return horzdrag
} else {
return 0
}
}
}
This looks like interference of same animation (spring in this case) applied to different properties, `cause effect is observed when tap (collapse) is applied when drag (offset) is not yet finished.
I'm not sure if this is a bug, but the workaround is to use type of different animations.
Here is a fix - use default instead of spring.
Tested with Xcode 13.3 / iOS 15.4
label
.clipped()
.offset(x: getOffset(horzdrag: horzdrag))
.animation(.default, value: horzdrag) // << here !!

SwiftUI Image Gallery don't pan out of visible area

What I'm trying to do is basically having some images in a TabView and being able to zoom and pan on those images.
Right now I got this quite nicely implemented but I struggle to find a solution so that it is not possible to pan outside the bounds of the image.
This is what I currently have:
I want to try having the image clip to the side of the screen so you don't pan the image out of the visible area.
So far this it what my code looks like:
struct FinalImageSwipeView: View {
#ObservedObject var viewModel: ImageChatViewModel
#State var currentImage: UUID
#State var fullPreview: Bool = false
#GestureState var draggingOffset: CGSize = .zero
var body: some View {
TabView(selection: $currentImage) {
ForEach(viewModel.images) { image in
GeometryReader{ proxy in
let size = proxy.size
PinchAndPanImage(image: UIImage(named: image.imageName)!,
fullPreview: $fullPreview)
.frame(width: size.width, height: size.height)
.contentShape(Rectangle())
}
.tag(image.id)
.ignoresSafeArea()
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.ignoresSafeArea()
// upper navigation bar
.overlay(
ImageSwipeViewNavigationBar(fullPreview: $fullPreview, hideSwipeView: viewModel.hideSwipeView),
alignment: .top
)
// bottom image scrollview
.overlay(
ImageSwipeViewImageSelection(viewModel: viewModel,
currentImage: $currentImage,
fullPreview: $fullPreview),
alignment: .bottom
)
.gesture(DragGesture().updating($draggingOffset, body: { (value, outValue, _) in
if viewModel.imageScale == 0 {
outValue = value.translation
viewModel.onChangeDragGesture(value: draggingOffset)
}}).onEnded({ (value) in
if viewModel.imageScale == 0 {
viewModel.onEnd(value: value)
}
}))
.transition(.offset(y: UIScreen.main.bounds.size.height + 100))
}
}
struct ImageSwipeViewImageSelection: View {
#ObservedObject var viewModel: ImageChatViewModel
#Binding var currentImage: UUID
#Binding var fullPreview: Bool
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 15) {
ForEach(viewModel.images) { image in
Image(image.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 60)
.cornerRadius(12)
.id(image.id)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(image.id == currentImage ? Color.white : Color.clear, lineWidth: 2)
}
.onTapGesture {
currentImage = image.id
}
}
}
.padding()
}
.frame(height: 80)
.background(BlurView(style: .systemUltraThinMaterialDark).ignoresSafeArea(edges: .bottom))
// While current post changing center current image in scrollview
.onAppear(perform: {
proxy.scrollTo(currentImage, anchor: .bottom)
})
.onChange(of: currentImage) { _ in
viewModel.imageScale = 1
withAnimation {
proxy.scrollTo(currentImage, anchor: .bottom)
}
}
}
.offset(y: fullPreview ? 150 : 0)
}
}
struct PinchAndPanImage: View {
let image: UIImage
#Binding var fullPreview: Bool
// Stuff for Pinch and Pan
#State var imageScale: CGFloat = 1
#State var imageCurrentScale: CGFloat = 0
#State var imagePanOffset: CGSize = .zero
#State var currentImagePanOffset: CGSize = .zero
var usedImageScale: CGFloat {
max(1, min(imageScale + imageCurrentScale, 10))
}
var usedImagePan: CGSize {
let width = imagePanOffset.width + currentImagePanOffset.width
let height = imagePanOffset.height + currentImagePanOffset.height
return CGSize(width: width, height: height)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(0)
.offset(usedImagePan)
.scaleEffect(usedImageScale > 1 ? usedImageScale : 1)
.gesture(
// Magnifying Gesture
MagnificationGesture()
.onChanged({ value in
imageCurrentScale = value - 1
})
.onEnded({ value in
imageCurrentScale = 0
imageScale = imageScale + value - 1
withAnimation(.easeInOut) {
if imageScale > 5 {
imageScale = 5
}
}
})
)
.simultaneousGesture(createPanGesture())
.onTapGesture(count: 2) {
withAnimation {
imageScale = 1
imagePanOffset = .zero
}
}
.onTapGesture(count: 1) {
withAnimation {
fullPreview.toggle()
}
}
}
private func createPanGesture() -> _EndedGesture<_ChangedGesture<DragGesture>>? {
let gesture = DragGesture()
.onChanged { value in
let width = value.translation.width / usedImageScale
let height = value.translation.height / usedImageScale
currentImagePanOffset = CGSize(width: width, height: height)
}
.onEnded { value in
currentImagePanOffset = .zero
let scaledWidth = value.translation.width / usedImageScale
let scaledHeight = value.translation.height / usedImageScale
let width = imagePanOffset.width + scaledWidth
let height = imagePanOffset.height + scaledHeight
imagePanOffset = CGSize(width: width, height: height)
}
return imageScale > 1 ? gesture : nil
}
}

"CALayer position contains NaN: [nan nan]" error message caused by custom slider

I have a video player that starts playing a video (from firebase URL) and in some cases (70% of cases) i get this error message (exception) when running on physical device (no issues when launching in simulator though):
"CALayer position contains NaN: [nan nan]"
I found that the error doesn't appear when i comment "VideoPlayerControlsView()", so i'm pretty sure the problem is is my CustomerSlider object located insider of this VideoPlayerControlsView view.
I think it may somehow be caused by loading remote video, as the video is not loaded, the app doesn't know the size/bounds of AVPlayer object and therefore some parent view (maybe CustomerSlider) can't be created..
Building a Minimal Reproducible Example would be a nightmare, i just hope some can find a mistake in my code/logic.. If not - gonna build it of course. No other choice.
struct DetailedPlayerView : View {
// The progress through the video, as a percentage (from 0 to 1)
#State private var videoPos: Double = 0
// The duration of the video in seconds
#State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
#State private var seeking = false
private var player: AVPlayer = AVPlayer()
init(item: ExerciseItem, hVideoURL: URL?) {
if hVideoURL != nil {
player = AVPlayer(url: hVideoURL!)
player.isMuted = true
player.play()
} else {
print("[debug] hVideoURL is nil")
}
}
var body: some View {
ZStack {
//VStack {
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
//timeline: $timeline,
//videoTimeline: videoTimeline,
player: player)
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth)
VStack {
Spacer()
VideoPlayerControlsView(videoPos: $videoPos, **<<-----------------------**
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.frame(width: UIScreen.screenHeight - 2*Constants.scrollPadding, height: 20)
.padding(.bottom, 20)
}
}
.onDisappear {
// When this View isn't being shown anymore stop the player
self.player.replaceCurrentItem(with: nil)
}
}
}
struct VideoPlayerControlsView : View {
#Binding private(set) var videoPos: Double
#Binding private(set) var videoDuration: Double
#Binding private(set) var seeking: Bool
// #Binding private(set) var timeline: [Advice]
#State var shouldStopPlayer: Bool = false
#State var player: AVPlayer
//let player: AVPlayer
#State private var playerPaused = false
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
.foregroundColor(Color.mainSubtitleColor)
.contentShape(Rectangle())
.padding(.trailing, 10)
}
// Current video time
if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
// Slider for seeking / showing video progress
CustomSlider(value: $videoPos, shouldStopPlayer: self.$shouldStopPlayer, range: (0, 1), knobWidth: 4) { modifiers in
ZStack {
Group {
Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5799999833106995))//Color((red: 0.4, green: 0.3, blue: 1)
.opacity(0.4)
.frame(height: 4)
.modifier(modifiers.barRight)
Color.mainSubtitleColor//Color(red: 0.4, green: 0.3, blue: 1)
.frame(height: 4)
.modifier(modifiers.barLeft)
}
.cornerRadius(5)
VStack {
Image(systemName: "arrowtriangle.down.fill") // SF Symbol
.foregroundColor(Color.mainSubtitleColor)
.offset(y: -3)
}
.frame(width: 20, height: 20)
.contentShape(Rectangle())
.modifier(modifiers.knob)
}
}
.onChange(of: shouldStopPlayer) { _ in
if shouldStopPlayer == false {
print("[debug] shouldStopPlayer == false")
sliderEditingChanged(editingStarted: false)
} else {
if seeking == false {
print("[debug] shouldStopPlayer == true")
sliderEditingChanged(editingStarted: true)
}
}
}
.frame(height: 20)
// Video duration
if videoDuration.isCanonical && videoDuration.isFinite {
Text(Utility.formatSecondsToHMS(videoDuration))
.foregroundColor(Color.mainSubtitleColor)
}
}
.padding(.leading, 40)
.padding(.trailing, 40)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.pausePlayer(false)
}
}
}
}
extension Double {
func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
// Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
var value = self
value -= fromRange.0
value /= Double(fromRange.1 - fromRange.0)
value *= toRange.1 - toRange.0
value += toRange.0
return value
}
}
struct CustomSliderComponents {
let barLeft: CustomSliderModifier
let barRight: CustomSliderModifier
let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
enum Name {
case barLeft
case barRight
case knob
}
let name: Name
let size: CGSize
let offset: CGFloat
func body(content: Content) -> some View {
content
.frame(width: (size.width >= 0) ? size.width : 0)
.position(x: size.width*0.5, y: size.height*0.5)
.offset(x: offset)
}
}
struct CustomSlider<Component: View>: View {
#Binding var value: Double
var range: (Double, Double)
var knobWidth: CGFloat?
let viewBuilder: (CustomSliderComponents) -> Component
#Binding var shouldStopPlayer: Bool
init(value: Binding<Double>, shouldStopPlayer: Binding<Bool>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: #escaping (CustomSliderComponents) -> Component
) {
_value = value
_shouldStopPlayer = shouldStopPlayer
self.range = range
self.viewBuilder = viewBuilder
self.knobWidth = knobWidth
}
var body: some View {
return GeometryReader { geometry in
self.view(geometry: geometry) // function below
}
}
private func view(geometry: GeometryProxy) -> some View {
let frame = geometry.frame(in: .global)
let drag = DragGesture(minimumDistance: 0)
.onChanged { drag in
shouldStopPlayer = true
self.onDragChange(drag, frame)
}
.onEnded { drag in
shouldStopPlayer = false
//self.updatedValue = value
print("[debug] slider drag gesture ended, value = \(value)")
}
let offsetX = self.getOffsetX(frame: frame)
let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5), height: frame.height)
let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
let modifiers = CustomSliderComponents(
barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
return ZStack { viewBuilder(modifiers).gesture(drag) }
}
private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
let xrange = (min: Double(0), max: Double(width.view - width.knob))
var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
value -= 0.5*width.knob // offset from center to leading edge of knob
value = value > xrange.max ? xrange.max : value // limit to leading edge
value = value < xrange.min ? xrange.min : value // limit to trailing edge
value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
//print("[debug] slider drag gesture detected, value = \(value)")
self.value = value
}
private func getOffsetX(frame: CGRect) -> CGFloat {
let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
let xrange: (Double, Double) = (0, Double(width.view - width.knob))
let result = self.value.convert(fromRange: range, toRange: xrange)
return CGFloat(result)
}
}
some extra code showing how DetailedPlayerView is triggered:
struct DetailedVideo: View {
var item: ExerciseItem
var url: URL
#Binding var isPaused: Bool
var body: some View {
ZStack {
DetailedPlayerView(item: self.item, hVideoURL: url)
//.frame(width: 500, height: 500) //##UPDATED: Apr 10
HStack {
VStack {
ZStack {
//Rectangle 126
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-135))
//Rectangle 125
RoundedRectangle(cornerRadius: 1)
.fill(Color(#colorLiteral(red: 0.3063802123069763, green: 0.3063802123069763, blue: 0.3063802123069763, alpha: 1)))
.frame(width: 2, height: 20.3)
.rotationEffect(.degrees(-45))
}
.frame(width: 35, height: 35)//14.4
.contentShape(Rectangle())
.onTapGesture {
print("[debugUI] isPaused = false")
self.isPaused = false
}
.offset(x:20, y:20)
Spacer()
}
Spacer()
}
}
.ignoresSafeArea(.all)
}
}
#ViewBuilder
var detailedVideoView: some View {
if self.hVideoURL != nil {
DetailedVideo(item: self.exerciseVM.exerciseItems[self.exerciseVM.currentIndex], url: self.hVideoURL!, isPaused: self.$exerciseVM.isPaused) // when is paused - we are playing detailed video?
.frame(width: UIScreen.screenHeight, height: UIScreen.screenWidth) //UPDATED: Apr 9, 2021
.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
} else {
EmptyView()
}
}

How to fix DragGesture() does not move smoothly when I use onTapGesture together

I'm currently developing an application using SwiftUI and trying to make a swipe component.
When I use DragGesture and onTapGesture together, that swipe motion does not move smoothly...
If I make onTapGesture disable the swipe motion work well.
But I want to use both of them together.
How could I solve this problem?
Here is the code:
import SwiftUI
struct SwipeTest1: View {
#State var offset : CGFloat = 0
#State var isOn = false
#State var isOnTap = false
var body: some View {
VStack{
Text(isOnTap ? "isOnTap: ON" : "isOnTap: OFF")
ZStack{
Capsule()
.fill(Color.primary.opacity(0.1))
VStack{
Spacer()
Circle()
.fill(isOn ? Color.blue : Color.green )
.frame(width: 60, height: 60)
.offset(y:offset)
.onTapGesture {isOnTap.toggle()} // ← If I comment out this line,the swipe motion work well.
.gesture(DragGesture().onChanged(isOn ? onChangedOn(value:) : onChangedOff(value:))
.onEnded(isOn ? onEndOn(value:) : onEndOff(value:))
)
}
}
.frame(width: 60, height: 120)
}
}
func onChangedOff(value: DragGesture.Value){
if value.translation.height < 0 && offset >= -60{
offset = value.translation.height
}
}
func onChangedOn(value: DragGesture.Value){
if offset <= 0{
offset = value.translation.height - 60
}
}
func onEndOff(value: DragGesture.Value){
if offset < -40 {
offset = -60
isOn = true
}else{
offset = 0
}
}
func onEndOn(value: DragGesture.Value){
if offset > -20 {
offset = 0
isOn = false
}else{
offset = -60
}
}
}
Xcode: Version 12.3
iOS: 14.0
You can use simultaneousGesture:
Circle()
.fill(isOn ? Color.blue : Color.green )
.frame(width: 60, height: 60)
.offset(y:offset)
.onTapGesture {isOnTap.toggle()}
.simultaneousGesture(DragGesture().onChanged(isOn ? onChangedOn(value:) : onChangedOff(value:))
.onEnded(isOn ? onEndOn(value:) : onEndOff(value:))
)
Also, you can adjust the minimumDistance of the DragGesture (by default it's 10):
DragGesture(minimumDistance: 10)