shaky view when changing offset through a drag gesture - swift

edit: it looks even worse on physical device, and simulator vs gif
I have a grid of rectangles. These are nested in HStacks and VStacks. This grid is put into a ZStack. I attatch the gesture to the nested Grid and see what happens when I drag with my implementation
import SwiftUI
import UIKit
struct ContentView: View {
#State var numCell: CGFloat = 20
#State var cellSize: CGFloat = 50
var body: some View {
return ZStack() {
Grid(numCell: $numCell, cellSize: $cellSize)
}.position(x: 0, y: 0).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).background(Color.blue)
}
}
struct Grid: View {
#Binding var numCell: CGFloat
#Binding var cellSize: CGFloat
#State var gridDraggedX: CGFloat = 0
#State var gridDraggedY: CGFloat = 0
#State var accumulatedGridDraggedX: CGFloat = 0
#State var accumulatedGridDraggedY: CGFloat = 0
var body: some View {
let drag = DragGesture().onChanged({ value in
self.gridDraggedX = value.translation.width + self.accumulatedGridDraggedX
self.gridDraggedY = value.translation.height + self.accumulatedGridDraggedY
}).onEnded({ value in
self.gridDraggedX = value.translation.width + self.accumulatedGridDraggedX
self.gridDraggedY = value.translation.height + self.accumulatedGridDraggedY
self.accumulatedGridDraggedX = self.gridDraggedX
self.accumulatedGridDraggedY = self.gridDraggedY
})
return HStack(spacing: 2) {
ForEach(0..<Int(numCell) - 1) { _ in
VStack(spacing: 2) {
ForEach(0..<Int(self.numCell) - 1) { _ in
Rectangle()
.fill(Color.red)
.frame(width: self.cellSize,
height: self.cellSize)
}
}
}
}.gesture(drag).background(Color.green).offset(x: gridDraggedX, y: gridDraggedY)
}
}
via GIPHY

DragGesture(coordinateSpace: .global)

Related

Determine UIScreen half in SwiftUI

I have a gesture applied to my text. Right now, it is sitting at the bottom of the screen and if I drag it to anywhere and let go, it goes back to the bottom. But if I drag it to the top half of my device screen, it should stay at the top.
The feature is almost ready to go, it just needs the correct function to split using UIScreen.main.bounds.
struct SwiftUIView: View {
#State var offset: CGSize = .zero
#State var isOnTop = false
var body: some View {
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isOnTop == true ? .top : .bottom)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.spring()) {
offset = value.translation
}
}
.onEnded { value in
withAnimation(.spring()) {
offset = .zero
// if dragged to the top half of the screen, set true
//isOnTop = true
}
}
)
}
}
This is how I will do, I use GeometryReader to have the size of my screen and I will apply logic to my offset when my gesture is ended
struct SwiftUIView: View {
#State private var yOffset: CGFloat = 0.0
#State private var xOffset: CGFloat = 0.0
#State private var height: CGFloat = 0
#State private var lastXOffset: CGFloat = 0.0
#State private var lastYOffset : CGFloat = 0.0
var body: some View {
GeometryReader{ geo in
VStack{
Spacer()
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.offset(x: xOffset, y: yOffset >= 0 ? 0: yOffset)
.gesture(DragGesture().onChanged({ gesture in
self.xOffset = gesture.translation.width + lastXOffset
self.yOffset = gesture.translation.height + lastYOffset
}).onEnded({ _ in
withAnimation{
lastXOffset = xOffset
if yOffset <= -height/2 {
yOffset = -height + geo.safeAreaInsets.top
///save last offset
lastYOffset = yOffset
} else {
yOffset = 0
///save last offset
lastYOffset = yOffset
}
}
}))
}
.onAppear{
height = geo.size.height
}
}
}
}

Is there a way to add a flip animation when I tap on my text?

Im trying to make a flash cards program and I can't seem to figure out how to make a flip animation to reveal the corresponding answer. Here is my code:
Text( "\(text)" + " \(X)" + " \(text1)")
.frame(width: 120, height: 300, alignment: .center)
.onTapGesture {
if selection == "Multiply" { text = num1 * num2 clear() }
if selection == "Addition" { text = num1 + num2 clear() }
if selection == "Subtraction" { text = num1 - num2 clear() }
if selection == "Division" { divAnswer = Double(num1 / num2) text = num1 / num2 clear() }
}
(im very new to swifui)
Based on the tutorial from the link in the comment, I set up the following. It created just one card with one exercise, but it shows how it could work. You would have to adjust it to your needs:
//
// CardFlip.swift
// CardFlip
//
// Created by Sebastian Fox on 26.08.22.
//
import SwiftUI
struct ContentView: View {
// For testing, you can setup your exercise here
#State var task: Task = Task(operator1: 5, operator2: 10, operation: .addition)
//MARK: Variables
#State var backDegree = 0.0
#State var frontDegree = -90.0
#State var isFlipped = false
let width : CGFloat = 200
let height : CGFloat = 250
let durationAndDelay : CGFloat = 0.3
//MARK: Flip Card Function
func flipCard () {
isFlipped = !isFlipped
if isFlipped {
withAnimation(.linear(duration: durationAndDelay)) {
backDegree = 90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
frontDegree = 0
}
} else {
withAnimation(.linear(duration: durationAndDelay)) {
frontDegree = -90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
backDegree = 0
}
}
}
//MARK: View Body
var body: some View {
ZStack {
CardBack(width: width, height: height, degree: $backDegree, task: $task)
CardFront(width: width, height: height, degree: $frontDegree, task: $task)
}.onTapGesture {
flipCard ()
}
}
}
struct CardFront : View {
let width : CGFloat
let height : CGFloat
#Binding var degree : Double
#Binding var task: Task
func calcResult() -> Double {
var result: Double!
switch task.operation {
case .addition: result = task.operator1 + task.operator2
case .substraction: result = task.operator1 - task.operator2
case .mulitply: result = task.operator1 * task.operator2
case .division: result = task.operator1 / task.operator2
}
return result
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(.white)
.frame(width: width, height: height)
.shadow(color: .gray, radius: 2, x: 0, y: 0)
.overlay(
Text("\(calcResult(), specifier: "%.2f")"))
}.rotation3DEffect(Angle(degrees: degree), axis: (x: 0, y: 1, z: 0))
}
}
struct CardBack : View {
let width : CGFloat
let height : CGFloat
#Binding var degree : Double
#Binding var task: Task
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(.white)
.frame(width: width, height: height)
.shadow(color: .gray, radius: 2, x: 0, y: 0)
.overlay(
Text("\(String(task.operator1)) \(task.operation.rawValue) \(String(task.operator2))")
)
}.rotation3DEffect(Angle(degrees: degree), axis: (x: 0, y: 1, z: 0))
}
}
struct Task {
var operator1: Double
var operator2: Double
var operation: Operations
}
enum Operations: String, CaseIterable {
case addition = "+"
case division = "/"
case mulitply = "x"
case substraction = "-"
}
Here a gif of how it looks like in the simulator:

SwiftUI: Adjust bounds of ScrollView

I am trying to scale a view horizontally using scaleEffect and MagnificationGesture.
It is almost working as I want with the exception that the ScrollView does not resize when its child view resizes.
Is there a fix for this? Any solution would be greatly appreciated.
To reproduce:
run the code below
scale up the image horizontally with the pinch gesture
notice that the ScrollView scrolls as if the image still has the same size.
struct ContentView: View {
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
ScrollView(.horizontal) {
Image(systemName: "star")
.resizable()
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
)
}
.frame(maxHeight: 300)
}
}
This view does it.
struct HorizontalScaleView<Content: View>: View {
#ViewBuilder var content: Content
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
GeometryReader { geo in
ScrollView(.horizontal) {
content
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.frame(width: (finalAmount + currentAmount) * geo.size.width, height: geo.size.width)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { _ in
if self.finalAmount + self.currentAmount >= 1 {
self.finalAmount += self.currentAmount
} else {
self.finalAmount = 1
}
self.currentAmount = 0
}
)
}
}
}
}

How do I add a zoomable SVG to SwiftUI?

I have a map that I want to put into my app, and I am currently learning SwiftUI. I want to put in an SVG where I can scroll, zoom, set initial x & y coordinates / initial zoom, max zoom, and tap on an element to open a page. I know I can do this with UIKit, but how do I do this with SwiftUI? If I am not able to do this with an SVG, how can I do this with a raster image that is interactive?
edit: here is my code:
struct ContentView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var scale: CGFloat = 1.0
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
Image("SVG")
.frame(width: 1100, height: 1100)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
print(self.newPosition.width)
print(self.newPosition.height)
self.newPosition = self.currentPosition
}
)
.scaleEffect(finalAmount + currentAmount)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to drag a working slider using SwiftUI

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()
}
}