1

I have a gray view that can be moves around the screen. The gray view has a child view (think of it as a clock hand) that rotates with an animation. The angle at which the child view rotates correlates directly to the translation value which the parent view uses for its .offset() or .position.

Even though I am only trying to animate the .rotationEffect() of the child view, it does not stay at it's anchor position.

In the example below, the gray view can be moved with a drag gesture or buttons at the bottom of the screen, and the child view rotates counter-clockwise if the gray view moves up, and clockwise if it moves down.

Without the .animation() property in the child view, it rotates properly.

Here is a Xcode Playground of the issue with the code below:

import UIKit
import SwiftUI
import PlaygroundSupport

struct Example: View {
    
    @State private var translation: CGSize = .zero
    
    var body: some View {
        ZStack {
            movingView()
                .overlay(rotatingView(), alignment: .top)
                .overlay(anchorPoint())
                .offset(translation)
                .gesture(dragGesture())
            
            controls()
        }
    }
}

private extension Example {
    
    func rotatingView() -> some View {
        Rectangle()
            .fill(Color.red)
            .frame(width: 4.0, height: 50.0)
            .rotationEffect(angle, anchor: .bottom)
            .animation(.interactiveSpring(), value: angle)
    }
    
    var angle: Angle {
        let total = translation.height
        return .degrees(total)
    }
    
}

private extension Example {
    
    func movingView() -> some View {
        Color.gray
            .frame(width: 100.0, height: 100.0)
    }
    
    func dragGesture() -> some Gesture {
        DragGesture()
            .onChanged { value in
                self.translation = value.translation
            }
            .onEnded { value in
                self.translation = .zero
            }
    }
    
}

private extension Example {
    func anchorPoint() -> some View {
        Circle()
            .fill(Color.white)
            .frame(width: 8.0, height: 8.0)
    }
}

private extension Example {
    func controls() -> some View {
        VStack {
            Spacer()
            
            HStack {
                Button("Up") {
                    withAnimation(.spring()) {
                        translation.height -= 100
                    }
                }
                
                Button("Down") {
                    withAnimation(.spring()) {
                        translation.height += 100
                    }
                }
            }
        }
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
    rootView: Example()
        .frame(width: 500.0, height: 800.0)
)

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
iMaddin
  • 982
  • 1
  • 14
  • 23

1 Answers1

1

This can be fixed by adding a drawingGroup() modifier after .overlay(anchorPoint()), e.g.

ZStack {
    movingView()
        .overlay(rotatingView(), alignment: .top)
        .overlay(anchorPoint())
        .drawingGroup()
        .offset(translation)
        .gesture(dragGesture())
    
    controls()
}

enter image description here

This composites the view’s contents into an offscreen image before final display.

https://developer.apple.com/documentation/swiftui/navigationview/drawinggroup(opaque:colormode:)

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160