I would like to drag a slider and of course have it slide as well.
I can do one or the other, but I cannot do both.
How can I drag and have a working slider?
I also tried to find a way to remove a gesture, but I could not find a way to do that.
Also tried the "Sequenced Gesture States" code from Apple "Composing SwiftUI Gestures" docs,
and introduce a flag to turn the dragging on/off with same results, drag or slide not both.
I also tried to put the slider in a Container (VStack) and attach the drag gesture to that,
but that did not work either.
import SwiftUI
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return Slider(value: $value, in: 0...100, step: 1)
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.simultaneousGesture(drag, including: .all) // tried .none .gesture, .subviews
// also tried .gesture(flag ? nil : drag)
}
}
With "simultaneousGesture" I was expecting to have both gestures operating at the same time.
This is working. Basically I needed to set the flag in an outside observable object for it to update the view so that it could take effect. When the value changes the flag is set to false, but then after a tenth of a second it becomes draggable. Working pretty seamlessly.
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
#ObservedObject var model = Model()
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return VStack {
Slider(value: $value, in: 0...100, step: 1) { _ in
self.model.flag = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.flag = true
}
}
}
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.gesture(model.flag == true ? drag : nil)
}
}
class Model: ObservableObject {
#Published var flag = false
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
I'd like to implement a custom slider SwiftUI component and put it on the toolbar area of a SwiftUI Mac app. However the gesture of the control gets ignored as the system's window moving gesture takes priority. This problem does not occur for the system UI controls, like Slider or Button.
How to fix the code below so the slider works in the toolbar area as well, not just inside the window similar to the default SwiftUI components?
struct MySlider: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
struct ContentView: View {
#State var value = 0.5
var body: some View {
MySlider()
.toolbar {
MySlider()
Slider(value: $value).frame(width: 100, height: 20)
}.frame(width: 500, height: 100)
}
}
Looks like design limitation (or not implemented yet feature - Apple does not see such view as user interaction capable item).
A possible workaround is to wrap you active element into button style. The button as a container interpreted as user-interaction-able area but all drawing and handling is in your code.
Tested with Xcode 13.2 / macOS 12.2
Note: no changes in your slider logic
struct MySlider: View {
var body: some View {
Button("") {}.buttonStyle(SliderButtonStyle())
}
struct SliderButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
MySliderContent()
}
struct MySliderContent: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
}
}
I'd like to create some content like this, a blinking green circle, and it works in the single preview mode
But when I put the View inside a List, the Green circle start moving left and right
struct DotView: View {
#State var delay: Double = 0 // 1.
#State var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(Animation.easeInOut(duration: 0.6).repeatForever().delay(delay)) // 2.
.onAppear {
withAnimation {
self.scale = 1
}
}
}
}
Using inside a navigation view
List {
VStack {
HStack {
Text(server.name)
.fontWeight(.bold)
.foregroundColor(Color.primary)
.fontWeight(.light)
.foregroundColor(.gray)
Spacer()
DotView()
}
}
}
As I wrote in comments it is not reproducible for me, but try the following...
struct DotView: View {
var delay: Double = 0 // 1. << don't use state for injectable property
#State private var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale // 2. << link to value
)
.onAppear {
self.scale = 1 // 3. << withAnimation no needed now
}
}
}
I'm having a hard time figuring out this animation thing. The problem is, the .onAppear wobbly animation (which should be forever looped) stops and freezes as soon as the view is dragged (which has another animation so that it smoothly moves towards the drag location).
Is there a way to keep the onAppear animation running independently of the view being dragged around? Important to note is that I do need the position to be dependent on the array (hence drag gesture writes directly to data), and I don't need the wobble (size animation) to be tied to the array at all.
Illustration of the animation freezing on drag (undesirable - would like to circles to still wobble during and after being dragged)
Please see code:
var body: some View {
Circle()
// .resizable()
.frame(width:size.width, height: size.height) //self.size is a state var
.foregroundColor(color)
.position(pos) //pos is a normal var bound to an array row
.gesture(DragGesture()
.onChanged { gesture in
withAnimation(.default) {
ballStorage.balls[numberInArray].position = gesture.location
}
}
.onEnded { gesture in
ballStorage.checkOverlaps(for: numberInArray)
}
) .onAppear {
withAnimation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)) {
size = CGSize(width: size.width+20, height: size.height+20)
}
}
}
}
You need to separate circle into own view with own animation, then internal animation will be independent of external animation.
Here is a demo of solution on somehow replicated code. Prepared with Xcode 12.1 / iOS 14.1.
struct Ball: Identifiable, Hashable {
let id = UUID()
var position = CGPoint.zero
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct BallView: View {
let ball: Ball
var color = Color.blue
#State private var size = CGSize(width: 40, height: 40)
var body: some View {
Circle()
.frame(width:size.width, height: size.height) //self.size is a state var
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true), value: size)
.foregroundColor(color)
.onAppear {
size = CGSize(width: size.width+20, height: size.height+20)
}
}
}
struct ContentView: View {
#State private var balls = [Ball(position: CGPoint(x: 100, y: 100)), Ball(position: CGPoint(x: 100, y: 200))]
#State private var off = CGSize.zero
#State private var selected = -1
var body: some View {
ZStack {
ForEach (Array(balls.enumerated()), id: \.1) { i, ball in
BallView(ball: ball)
.offset(x: i == selected ? off.width : 0, y: i == selected ? off.height : 0)
.position(ball.position)
.gesture(DragGesture()
.onChanged { gesture in
self.selected = i
withAnimation(.default) {
self.off = gesture.translation
}
}
.onEnded { gesture in
let pt = balls[i].position
self.balls[i].position = CGPoint(x: pt.x + gesture.translation.width, y: pt.y + gesture.translation.height)
self.off = .zero
self.selected = -1
// ballStorage.checkOverlaps(for: numberInArray)
}
)
}
}
}
}
This is my first time trying out SwiftUI, and I am trying to create a SwiftUI view that acts as a split view, with an adjustable handle in the center of the two views.
Here's my current code implementation example:
struct ContentView: View {
#State private var gestureTranslation = CGSize.zero
#State private var prevTranslation = CGSize.zero
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
.gesture(DragGesture()
.onChanged({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
})
.onEnded({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
self.prevTranslation = self.gestureTranslation
})
)
Rectangle()
.fill(Color.green)
.frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
}
}
}
How it looks like now:
[
This kinda works, but when dragging the handle, it is very glitchy, and that it seems to require a lot of dragging to reach a certain point.
Please advice me what went wrong. Thank you.
See How to change the height of the object by using DragGesture in SwiftUI? for a simpler solution.
My version of that:
let MIN_HEIGHT = CGFloat(50)
struct DragViewSizeView: View {
#State var height: CGFloat = MIN_HEIGHT
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(width: .infinity, height: height)
HStack {
Spacer()
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 10)
.cornerRadius(10)
.gesture(
DragGesture()
.onChanged { value in
height = max(MIN_HEIGHT, height + value.translation.height)
}
)
Spacer()
}
VStack {
Text("my o my")
Spacer()
Text("hoo hah")
}
}
}
}
struct DragTestView: View {
var body: some View {
VStack {
DragViewSizeView()
Spacer() // If comment this line the result will be as on the bottom GIF example
}
}
}
struct DragTestView_Previews: PreviewProvider {
static var previews: some View {
DragTestView()
}
}
From what I have observed, the issue seems to be coming from the handle being repositioned while being dragged along. To counteract that I have set an inverse offset on the handle, so it stays in place. I have tried to cover up the persistent handle position as best as I can, by hiding it beneath the other views (zIndex).
I hope somebody else got a better solution to this question. For now, this is all that I have got:
import PlaygroundSupport
import SwiftUI
struct SplitView<PrimaryView: View, SecondaryView: View>: View {
// MARK: Props
#GestureState private var offset: CGFloat = 0
#State private var storedOffset: CGFloat = 0
let primaryView: PrimaryView
let secondaryView: SecondaryView
// MARK: Initilization
init(
#ViewBuilder top: #escaping () -> PrimaryView,
#ViewBuilder bottom: #escaping () -> SecondaryView)
{
self.primaryView = top()
self.secondaryView = bottom()
}
// MARK: Body
var body: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
self.primaryView
.frame(height: (proxy.size.height / 2) + self.totalOffset)
.zIndex(1)
self.handle
.gesture(
DragGesture()
.updating(self.$offset, body: { value, state, _ in
state = value.translation.height
})
.onEnded { value in
self.storedOffset += value.translation.height
}
)
.offset(y: -self.offset)
.zIndex(0)
self.secondaryView.zIndex(1)
}
}
}
// MARK: Computed Props
var handle: some View {
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
}
var totalOffset: CGFloat {
storedOffset + offset
}
}
// MARK: - Playground
let splitView = SplitView(top: {
Rectangle().foregroundColor(.red)
}, bottom: {
Rectangle().foregroundColor(.green)
})
PlaygroundPage.current.setLiveView(splitView)
Just paste the code inside XCode Playground / Swift Playgrounds
If you found a way to improve my code please let me know.
I am using offset and gesture modifiers to move a circle around the screen. When I use this code, everything works as expected:
import SwiftUI
struct MovingCircle: View {
#State private var dragged = CGSize.zero
var body: some View {
Circle()
.offset(x: self.dragged.width)
.frame(width: 20, height: 20)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = value.translation
}
.onEnded{ value in
self.dragged = CGSize.zero
}
)
}
}
However, I do not want to have the circle reset to the original position onEnded. I would like it to remain in place and then be moved again on dragging. When I use the following code, I lose the ability to move the circle again upon re-dragging and it remains in place:
import SwiftUI
struct MovingCircle: View {
#State private var dragged = CGSize.zero
var body: some View {
Circle()
.offset(x: self.dragged.width)
.frame(width: 20, height: 20)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = value.translation
}
.onEnded{ value in
self.dragged = value.translation
}
)
}
}
What is the cause of this, have I encountered some bug or have I coded it incorrectly?
First, to understand the problem, add a .border(Color.red) to the .frame() modifier:
.frame(width: 20, height: 20).border(Color.red)
You'll see that when the dot is moved, its frame remains in place. That is why later, it won't respond to gestures. The "tappable" area no longer matches the dot. And because the content area is now empty, it is no longer "tappable".
To make the frame move with the dot, invert the order. The .offset() should come later:
.frame(width: 20, height: 20).border(Color.red)
.offset(x: self.dragged.width)
Finally, you will see that after each .onEnded(), the whole thing resets back. One way to solve it, is by accumulating how much you dragged in previous gestures:
struct MovingCircle: View {
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
var body: some View {
Circle()
.frame(width: 20, height: 20).border(Color.red)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
self.dragged = CGSize(width: value.translation.width + self.accumulated.width, height: value.translation.height + self.accumulated.height)
}
.onEnded{ value in
self.dragged = CGSize(width: value.translation.width + self.accumulated.width, height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
}
}
Swift 5.x iOS 13
A Less elegant solution, if you don't care to see the object dragged; you just want to be able to drag it; maybe makes more sense if you got a lot going on and you're looking to save CPU.
Obviously you need to find a better starting position for it then absolute zero.
import SwiftUI
var intern:CGPoint = CGPoint(x:0,y:0)
enum DragState {
case inactive
case dragging(translation: CGSize)
}
struct ContentView: View {
#State var position:CGPoint = CGPoint(x:0,y:0)
#GestureState private var dragState = DragState.inactive
var body: some View {
ZStack {
Circle()
.frame(width: 24, height: 24, alignment: .center)
}.offset(x: self.dragOffset.width, y: self.dragOffset.height)
.gesture(DragGesture(coordinateSpace: .global)
.updating($dragState, body: { (drag, state, translation)
intern = drag.location
})
.onEnded { ( value ) in
self.position = intern
}
).position(position)
}
}