1

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
    }
}
user14341201
  • 212
  • 1
  • 9

1 Answers1

2

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 your var rotationView is 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 the ZStack. So, while it looked like it was using the ZStack's center, it was actually using the center of rotationView which also happened to be the center of the ZStack. If you removed the offset, and changed the ZStack alignment, you will see how this changes the actual center of the rotationView.

The fix is actually pretty simple: put the offset for the Image on the rotationView. That allows the rotation to happen before it is offset, and it looks like you were expecting.

Moving the .offset does affect the placement of the Circle(). 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 the Circle() relatively in the ZStack, 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.

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 {
        
        VStack {
            
            Text("Rotating View Example")
                .frame(height: 200)
            
            
            rotationView
                .offset(x: location.x, y: location.y)
                .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)
            
            
            Circle()
                .foregroundColor(Color.accentColor)
                .frame(width: 20, height: 20)
                // This no longer uses .position() which is absolute, but uses .offset
                .offset(x: imageSize.width / 2, y: imageSize.height / 2)
            
            
        }
        .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
            }
        )
        .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
    }
}
Yrb
  • 8,103
  • 2
  • 14
  • 44