I want to rotate a stackedImageView (which contains image and shape) based on the DragGesture's value. The rotation should rotate with the center of the stackedImageView as anchorPoint. It seems to rotate based on the centre of the parent view.
The rotation works fine when placed in the middle of the screen (location = CGPoint(x: 0, y: 0); however, the rotation went unpredictable when placed near to the edges of the parent view (eg CGPoint(x: -80, y: -100)).
I think this is due to the anchorPoint in .rotateEffect(). How can I compute the UnitPoint to the centre of my rotating stackedImageView and use it as anchorPoint for the rotateEffect? Is there a way to compute it from the DragGesture's startLocation?
Thanks!
struct ContentView: View {
@State private var angle: CGFloat = 0
@State private var lastAngle: CGFloat = 0
@State private var length : CGFloat = 100
@State private var location: CGPoint = CGPoint(x: -80, y: -100)
let systemImage = UIImage(systemName: "camera")!
var body: some View {
var body: some View {
VStack {
Text("Rotating View Example")
.frame(height: 200)
rotationView
.frame(width: 300, height: 300)
.border(.blue, width: 2)
}
}
private var rotationView: some View {
let imageSize = newSize(100)
return ZStack {
Image(uiImage: systemImage)
.resizable()
.aspectRatio(contentMode: .fit)
.offset(x: location.x, y: location.y)
Circle()
.foregroundColor(Color.accentColor)
.frame(width: 20, height: 20)
.position(x: location.x + imageSize.width, y: location.y + imageSize.height)
}
.rotationEffect(.degrees(Double(self.angle)))
.gesture(DragGesture()
.onChanged{ value in
let centerPt = CGPoint(x: value.startLocation.x - imageSize.width / 2, y: value.startLocation.y - imageSize.height / 2)
let theta = (atan2(value.location.y - centerPt.y, value.location.x - centerPt.x) - atan2(value.startLocation.y - centerPt.y, value.startLocation.x - centerPt.x)) * 180 / .pi
angle = theta + lastAngle
print("angle: \(angle), centerPt: (\(centerPt.x), \(centerPt.y)), start: (\(value.startLocation.x), \(value.startLocation.y), end: (\(value.location.x), \(value.location.y)")
}
.onEnded { v in
self.lastAngle = self.angle
}
)
.animation(.none)
.frame(width: imageSize.width, height: imageSize.height)
}
func newSize(_ maxDimension: CGFloat) -> CGSize {
let imageSize = systemImage.size
if imageSize.width > imageSize.height {
return recalculateSize(imageSize: systemImage.size, width: maxDimension)
} else {
return recalculateSize(imageSize: systemImage.size, height: maxDimension)
}
}
func recalculateSize(imageSize: CGSize, width: CGFloat? = nil, height: CGFloat? = nil) -> CGSize {
if let width = width, height == nil {
let newHeight = imageSize.height / (imageSize.width / width)
return CGSize(width: width, height: newHeight)
} else if let height = height, width == nil {
let newWidth = imageSize.width / (imageSize.height / height)
return CGSize(width: newWidth, height: height)
}
return imageSize
}
}
The issue that you are having is actually simple, but it comes from a fundamental misunderstanding of what
.offset()does..offset()does not actually move the view that is being offset, rather, it moves where the view is displayed from the actual location of the view. What happened in yourvar rotationViewis you were offsetting the view BEFORE applying the rotation. The rotation is ALWAYS going to use the counterpoint of the view as the center for rotation. Remember, regardless of.offset(), this is the initial center which is in the center of theZStack. So, while it looked like it was using theZStack'scenter, it was actually using the center ofrotationViewwhich also happened to be the center of theZStack. If you removed the offset, and changed theZStackalignment, you will see how this changes the actual center of therotationView.The fix is actually pretty simple: put the offset for the
Imageon therotationView. That allows the rotation to happen before it is offset, and it looks like you were expecting.Moving the
.offsetdoes affect the placement of theCircle(). For that, you used.position()which does move the actual view to the absolute position on the screen. That now gets changed to an.offset()as well, as you want to position theCircle()relatively in theZStack, and have it rotate around the same center.Last thing.
.animation()is deprecated and should be removed. All of your animations should be explicit now, not implicit, and this view won't animate unless you expressly make it.