Adjust Drag Gesture on Rotated View - ios

I have an image on a view.
added rotation to rotate the view
added drag gesture to pan the image
drag gesture works fine when image is not rotated
once the view is rotated to certain angle the drag gesture gets disturbed, since view is rotated.
So, how to adjust the dragOffset and position based on the angle of rotation?
Code:
struct ImageView: View {
#State private var dragOffset: CGSize = .zero
#State private var position: CGSize = .zero
#State private var currentRotation: Angle = .zero
#GestureState private var twistAngle: Angle = .zero
public var body: some View {
let rotationGesture = RotationGesture(minimumAngleDelta: .degrees(10))
.updating($twistAngle, body: { (value, state, _) in
state = value
})
.onEnded{ self.currentRotation += $0 }
let dragGesture = DragGesture()
.onChanged({ (value) in
self.dragOffset = value.translation
})
.onEnded({ (value) in
self.position.width += value.translation.width
self.position.height += value.translation.height
self.dragOffset = .zero
})
let gestures = rotationGesture
.simultaneously(with: dragGesture)
Image.placeholder320x192
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.rotationEffect(currentRotation + twistAngle)
.gesture(gestures, including: .gesture)
}
}

The order of the modifiers matter. You currently have the offset before the rotation - therefore you are applying the offset then rotating. This makes the offset appear at an angle. Instead, you want to rotate and then offset.
Change:
Image.placeholder320x192
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.rotationEffect(currentRotation + twistAngle)
.gesture(gestures, including: .gesture)
To this:
Image.placeholder320x192
.rotationEffect(currentRotation + twistAngle)
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.gesture(gestures, including: .gesture)

Related

Problem with offset and buttons in SwiftUI?

I have a group of buttons that i show with a clockwise rotation, but I cannot click them properly:
I think there is a problem with the offset but I don't know how to solve it, any suggestions?
This is the code:
struct CategoryView: View {
// Circle Radius
#State private var radius: Double = 150
let circleSize: Double = 350
// Degree of circle
#State private var degree = -90.0
let cards = ["John", "Mark", "Alex", "Kevin", "Jimmy"]
var body: some View {
ZStack {
let anglePerCount = Double.pi * 2.0 / Double(cards.count)
ForEach(0..<cards.count, id: \.self) { index in
let angle = Double(index) * anglePerCount
let xOffset = CGFloat(radius * cos(angle))
let yOffset = CGFloat(radius * sin(angle))
Button {
} label: {
Text(cards[index])
.font(.title)
.fontWeight(.bold)
.rotationEffect(Angle(radians: angle + Double.pi/2))
.offset(x: xOffset, y: yOffset)
}
}
}
.rotationEffect(Angle(degrees: degree))
.onAppear() {
radius = circleSize/2 - 47 // 47 is for padding
}
}
}
This is a simple mistake, that all SwiftUI devs have made countless times. You're modifying the label, but not the actual button itself. Simply move the modifier to the Button.
Button (action: {}, label: {
Text(cards[index])
.font(.title)
.fontWeight(.bold)
})
.rotationEffect(Angle(radians: angle + Double.pi/2))
.offset(x: xOffset, y: yOffset)
In SwiftUI nearly everything is a View and can be treated as such. A button, also a view, can have most of the same modifiers that a Text can have. In your question, you made a change to the inner view of the button, and not the actual button itself. That's why when you were clicking in the middle, it appeared to not be positioned right, but in fact it did exactly what you told it too. That is an important thing to remember because you can actually push, pull, and offset things outside of the view which makes for some interesting layouts; Layouts such as side-menus, or modals.

How can I change views' rotation based on user's scroll in SwiftUI?

I've implemented this wheel picker:
but I don't know how to change numbers' angulation according to wheel rotation, what should I do?
I would like to achieve this:
Numbers' rotation should be based on user's scroll(for example, every number should be in normal position when reaches the top)
This is the full code:
struct myVal : Equatable {
let id = UUID()
let val : String
}
enum Direction {
case left
case right
}
struct WheelView: View {
// Circle Radius
#State var radius : Double = 150
// Direction of swipe
#State var direction = Direction.left
// index of the number at the bottom of the circle
#State var chosenIndex = 0
// degree of circle and hue
#Binding var degree : Double
// #State var degree = 90.0
let array : [myVal]
let circleSize : Double
var body: some View {
ZStack {
let anglePerCount = Double.pi * 2.0 / Double(array.count)
let drag = DragGesture()
.onEnded { value in
if value.startLocation.x > value.location.x + 10 {
direction = .left
} else if value.startLocation.x < value.location.x - 10 {
direction = .right
}
//here I call the function to move the wheel
}
// MARK: WHEEL STACK - BEGINNING
ZStack {
ForEach(0 ..< array.count) { index in
let angle = Double(index) * anglePerCount
let xOffset = CGFloat(radius * cos(angle))
let yOffset = CGFloat(radius * sin(angle))
Text("\(array[index].val)")
.rotationEffect(Angle(degrees: -degree))
.offset(x: xOffset, y: yOffset )
}
}
.rotationEffect(Angle(degrees: degree))
.gesture(drag)
.onAppear() {
radius = circleSize/2 - 30 // 30 is for padding
}
// MARK: WHEEL STACK - END
}
}
}
You are calculating angle in radian. Set radians instead of degrees and add Double.pi/2.
Text("\(array[index].val)")
.rotationEffect(Angle(radians: angle + Double.pi/2)) // <== Here

SwiftUI withAnimation inside conditional not working

I would like to have a view with an animation that is only visible conditionally. When I do this I get unpredictable behavior. In particular, in the following cases I would expect calling a forever repeating animation inside onAppear to always work regardless of where or when it initializes, but in reality it behaves erratically. How should I make sense of this behavior? How should I be animating a value inside a view that conditionally appears?
Case 1: When the example starts, there is no circle (as expected), when the button is clicked the circle then starts as animating (as expected), if clicked off then the label keeps animating (which it shouldn't as the animated value is behind a false if statement), if clicked back on again then the circle is stuck at full size and while the label keeps animating
struct TestButton: View {
#State var radius = 50.0
#State var running = false
let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
var body: some View {
VStack {
Button(running ? "Stop" : "Start") {
running.toggle()
}
if running {
Circle()
.fill(.blue)
.frame(width: radius * 2, height: radius * 2)
.onAppear {
withAnimation(animation) {
self.radius = 100
}
}
}
}
}
}
Case 2: No animation shows up regardless of how many times you click the button.
struct TestButton: View {
#State var radius = 50.0
#State var running = false
let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
var body: some View {
VStack {
Button(running ? "Stop" : "Start") {
running.toggle()
}
if running {
Circle()
.fill(.blue.opacity(0.2))
.frame(width: radius * 2, height: radius * 2)
}
}
// `onAppear` moved from `Circle` to `VStack`.
.onAppear {
withAnimation(animation) {
self.radius = 100
}
}
}
}
Case 3: The animation runs just like after the first button click in Case 1.
struct TestButton: View {
#State var radius = 50.0
#State var running = true // This now starts as `true`
let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
var body: some View {
VStack {
Button(running ? "Stop" : "Start") {
running.toggle()
}
if running {
Circle()
.fill(.blue.opacity(0.2))
.frame(width: radius * 2, height: radius * 2)
}
}
.onAppear {
withAnimation(animation) {
self.radius = 100
}
}
}
}
It is better to join animation with value which you want to animate, in your case it is radius, explicitly on container which holds animatable view.
Here is demo of approach. Tested with Xcode 13.2 / iOS 15.2
struct TestButton: View {
#State var radius = 50.0
#State var running = false
let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
var body: some View {
VStack {
Button(running ? "Stop" : "Start") {
running.toggle()
}
VStack { // responsible for animation of
// conditionally appeared/disappeared view
if running {
Circle()
.fill(.blue)
.frame(width: radius * 2, height: radius * 2)
.onAppear {
self.radius = 100
}
.onDisappear {
self.radius = 50
}
}
}
.animation(animation, value: radius) // << here !!
}
}
}

Using MagnificationGesture; how can I zoom in to where a user's fingers actually “pinch”?

I am facing the same problem as below with SwiftUI.
Using PinchGesture; how can I zoom in to where a user's fingers actually "pinch"?
I think I can solve this problem by giving anchor parameter of scaleEffect the center coordinate of the two fingers.
But I don't know how to get the center.
My Code:
import SwiftUI
struct MagnificationGestureView: View {
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
var body: some View {
Image(
systemName: "photo"
)
.resizable()
.frame(
width: 100,
height: 100
)
.scaleEffect(
self.scale
//, anchor: <---- give: the two fingers center position
)
.gesture(
magnification
)
}
var magnification: some Gesture {
// Can I get the center of the pinch?
MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
}.onEnded { _ in
self.lastScaleValue = 1.0
}
}
}
Reference:
https://stackoverflow.com/a/58468234/1979953

SwiftUI: pinch to zoom on image

I want to allow the user to pinch-to-zoom in on an Image in SwiftUI. I figured the best way to go was to use a MagnificationGesture and, following along with the answer here, I ended up with this code:
// outside of `var body: some View`
#State private var scale: Int = 1.0
#State private var lastScale: Int = 1.0
// Image
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.gesture(MagnificationGesture()
.onChanged { val in
let delta = val / self.lastScale
self.lastScale = val
let newScale = self.scale * delta
self.scale = newScale
}
.onEnded { _ in
self.lastScale = 1.0
}
)
.scaleEffect(scale)
This code handles magnification fine, but does not let the user zoom in on a specific area. Instead, it always zooms in on the middle of the image.
How would I go about handling pinch-to-zoom behavior on an image in SwiftUI?
Thanks in advance!
The code creates a pinch-to-zoom effect by adding a drag gesture in addition to the magnification gesture. Use of viewState allows a changing offset position when using the drag gesture.
struct ContentView: View {
#State private var scale: CGFloat = 1.0
#State private var lastScale: CGFloat = 1.0
#State private var viewState = CGSize.zero
var body: some View {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.animation(.spring())
.offset(x: viewState.width, y: viewState.height)
.gesture(DragGesture()
.onChanged { val in
self.viewState = val.translation
}
)
.gesture(MagnificationGesture()
.onChanged { val in
let delta = val / self.lastScale
self.lastScale = val
if delta > 0.94 { // if statement to minimize jitter
let newScale = self.scale * delta
self.scale = newScale
}
}
.onEnded { _ in
self.lastScale = 1.0
}
)
.scaleEffect(scale)
}
}
The 'if' statement was added to minimize the jitter caused by frequent updates. Nothing is special about the 0.94 value, just set by trial and error.
The .animation(spring()) statement was added for a more natural-looking dragging effect.
I found that the easiest way to achieve is to use PDFKit provided by Apple .
1.Start by creating PDFView
import SwiftUI
import PDFKit
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.document = PDFDocument()
guard let page = PDFPage(image: image) else { return view }
view.document?.insert(page, at: 0)
view.autoScales = true
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
}
}
2.Use in swiftUI view, like this
TabView(selection: $index,
content: {
//this line
PhotoDetailView(image: images[index])
.offset(imageViewerOffset)
})
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))

Resources