Rotate view to always face mouse pointer in SwiftUI - swift

Background: I am trying to have a view rotate to face the mouse at all times.
Details of issue: Moving the view based solely on the mouse moving left or right or on up or down works fine (see version 1). But using both at the same time (non-linear or arc motion with the mouse) fails to work as soon as the mouse arcs down (as y goes negative) and around (as x goes negative) and the position of the view regresses.
For this I am attempting to dynamically switch rotation values from negative to positive depending on what side of the screen the mouse is on (see version 2). But this feels like a poor implementation and is quite buggy.
Question: How can I better do this?
I am basing my cursor fetching code off of here.
Version 1-
Problem: moving the view based solely on the mouse moving left or right or on up or down works fine but this issue occurs when trying to move the mouse in a non-linear way:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.y > 60) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x) / 2)
)
.rotationEffect(
(self.position.x > 320) ?
.degrees(Double(self.position.y * -1))
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Version 2- kind of works but is buggy:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x * -1) / 2)
)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.y * -1) / 2)
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

check this out:
few remarks:
1) i changed it to ios (should be easy to change back to macos because the difference is just the getting of the position
2) i made a gradient to the rectangle so that is clear, in which direction the rectangle rotates
3) i have no idea what you are calculating, but in my opinion you can only do it correctly with trigonometric functions
struct ContentView: View {
#State var position : CGPoint = .zero
#State var offset : CGSize = .zero
#State var translation : CGSize = .zero
#State var angle : Double = 0
func calcAngle(_ geometry: GeometryProxy) -> Angle {
let rectPosition = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
// tan alpha = dx / dy
var alpha : Double
if rectPosition.y == position.y {
alpha = 0
} else {
var dx = (position.x - rectPosition.x)
let dy = (position.y - rectPosition.y)
if dy > 0 {
dx = -dx
}
let r = sqrt(abs(dx * dx) + abs(dy * dy))
alpha = Double(asin(dx / r))
if dy > 0 {
alpha = alpha + Double.pi
}
}
return Angle(radians: alpha)
}
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color.red, Color.yellow]), startPoint: UnitPoint.top, endPoint: UnitPoint.bottom))
.frame(width: 200, height: 200)
.position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
.rotationEffect(self.calcAngle(geometry), anchor: UnitPoint(x: 0.5, y: 0.5))
Circle().fill(Color.blue).frame(width:20,height:20).position(self.position)
Circle().fill(Color.green).frame(width:20,height:20).position(self.position)
}.frame(width: geometry.size.width, height: geometry.size.height)
.gesture(
DragGesture()
.onChanged { gesture in
self.position = gesture.location
self.translation = gesture.translation
}
.onEnded { _ in
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

Related

SwiftUI animate opacity in gradient mask

I want to animate a mountain line chart (see screenshot) so that the line appears smoothly from left to right. I tried animating the endPoint of a LinearGradient in a mask. This works as it should, but leaves the right end of the line with an opacity value < 1. I also tried animating the opacity value itself, but that doesn't animate anything. This is the View:
import SwiftUI
struct ContentView: View {
private var datapoints: [CGFloat] = [100, 250, 200, 220, 200, 250, 350, 300, 325, 250]
#State private var pct: Double = 0.0
var body: some View {
GeometryReader { geometry in
ZStack {
Path() { path in
var x = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
}
.stroke()
Path() { path in
var x: Double = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height))
}
.fill(LinearGradient(colors: [.black.opacity(0.25), .black.opacity(0)], startPoint: .top, endPoint: .bottom))
}
.mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(pct), location: 1)], startPoint: .leading, endPoint: .trailing))
.onAppear() {
withAnimation(.linear(duration: 3)) {
pct = 1.0
}
}
// .mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(0), location: 1)], startPoint: .leading, endPoint: pct == 1 ? .trailing : .leading))
// .onAppear() {
// withAnimation(.linear(duration: 3)) {
// pct = 1.0
// }
// }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the result if uncomment the commented out lines:

Custom Slider Taking Too Much Space

I used a code from here to create my own variation of a custom slider. However, the slider has too much extra space at the bottom, and I would like to remove that. Is there any way to do so?
Here is the code of the slider:
struct CustomSlider: View {
#Binding var value: CGFloat
#State var lastOffset: CGFloat = 0
var range: ClosedRange<CGFloat>
var leadingOffset: CGFloat = 5
var trailingOffset: CGFloat = 5
var knobSize: CGSize = CGSize(width: 25, height: 25)
let trackGradient = LinearGradient(gradient: Gradient(colors: [.pink, .yellow]), startPoint: .leading, endPoint: .trailing)
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
ZStack(alignment: .leading) {
Capsule()
.fill(Color(hue: 0.172, saturation: 0.275, brightness: 1.0))
.frame(height: 30)
Capsule()
.fill(Color(hue: 0.55, saturation: 0.326, brightness: 1.0))
.frame(width: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))+30, height: 30)
}
HStack {
RoundedRectangle(cornerRadius: 50)
.frame(width: self.knobSize.width, height: self.knobSize.height)
.foregroundColor(.white)
.offset(x: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset)))
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if abs(value.translation.width) < 0.1 {
self.lastOffset = self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))
}
let sliderPos = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geometry.size.width - self.knobSize.width - self.trailingOffset))
let sliderVal = sliderPos.map(from: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset), to: self.range)
self.value = sliderVal
}
)
}
}
}
}
}
I have noticed that when I select the empty space, the code cased in geometry reader is automatically selected. (I couldn't embed the image in this post, so here's the link)
When you use GeometryReader, it automatically takes up all available space. If you'd like to constrain it, use a frame(height: ) modifier to give it a certain height.
struct CustomSlider: View {
//all of the properties
var body: some View {
GeometryReader { geometry in
// slider code
}.frame(height: 60) //<-- Here
}
}

Fill circle with wave animation in SwiftUI

I have created a circle in swiftUI, and I want to fill it with sine wave animation for the water wave effect/animation. I wanted to fill it with a similar look:
Below is my code:
import SwiftUI
struct CircleWaveView: View {
var body: some View {
Circle()
.stroke(Color.blue, lineWidth: 10)
.frame(width: 300, height: 300)
}
}
struct CircleWaveView_Previews: PreviewProvider {
static var previews: some View {
CircleWaveView()
}
}
I want to mostly implement it on SwiftUI so that I can support dark mode! Thanks for the help!
Here's a complete standalone example. It features a slider which allows you to change the percentage:
import SwiftUI
struct ContentView: View {
#State private var percent = 50.0
var body: some View {
VStack {
CircleWaveView(percent: Int(self.percent))
Slider(value: self.$percent, in: 0...100)
}
.padding(.all)
}
}
struct Wave: Shape {
var offset: Angle
var percent: Double
var animatableData: Double {
get { offset.degrees }
set { offset = Angle(degrees: newValue) }
}
func path(in rect: CGRect) -> Path {
var p = Path()
// empirically determined values for wave to be seen
// at 0 and 100 percent
let lowfudge = 0.02
let highfudge = 0.98
let newpercent = lowfudge + (highfudge - lowfudge) * percent
let waveHeight = 0.015 * rect.height
let yoffset = CGFloat(1 - newpercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
let startAngle = offset
let endAngle = offset + Angle(degrees: 360)
p.move(to: CGPoint(x: 0, y: yoffset + waveHeight * CGFloat(sin(offset.radians))))
for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
p.addLine(to: CGPoint(x: x, y: yoffset + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))))
}
p.addLine(to: CGPoint(x: rect.width, y: rect.height))
p.addLine(to: CGPoint(x: 0, y: rect.height))
p.closeSubpath()
return p
}
}
struct CircleWaveView: View {
#State private var waveOffset = Angle(degrees: 0)
let percent: Int
var body: some View {
GeometryReader { geo in
ZStack {
Text("\(self.percent)%")
.foregroundColor(.black)
.font(Font.system(size: 0.25 * min(geo.size.width, geo.size.height) ))
Circle()
.stroke(Color.blue, lineWidth: 0.025 * min(geo.size.width, geo.size.height))
.overlay(
Wave(offset: Angle(degrees: self.waveOffset.degrees), percent: Double(percent)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.clipShape(Circle().scale(0.92))
)
}
}
.aspectRatio(1, contentMode: .fit)
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.waveOffset = Angle(degrees: 360)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
CircleWaveView(percent: 58)
}
}

Crop image according to rectangle in SwiftUI

I have an Image that I can drag around using DragGesture(). I want to crop the image visible inside the Rectangle area. Here is my code...
struct CropImage: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
var body: some View {
VStack {
ZStack {
Image("test_pic")
.resizable()
.scaledToFit()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.screenWidth * 0.7 , height: UIScreen.screenHeight/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.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)
self.newPosition = self.currentPosition
})
Button ( action : {
// how to crop the image according to rectangle area
} ) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
For easier understanding...
Here is possible approach using .clipShape. Tested with Xcode 11.4 / iOS 13.4
struct CropFrame: Shape {
let isActive: Bool
func path(in rect: CGRect) -> Path {
guard isActive else { return Path(rect) } // full rect for non active
let size = CGSize(width: UIScreen.screenWidth * 0.7, height: UIScreen.screenHeight/5)
let origin = CGPoint(x: rect.midX - size.width / 2, y: rect.midY - size.height / 2)
return Path(CGRect(origin: origin, size: size).integral)
}
}
struct CropImage: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var clipped = false
var body: some View {
VStack {
ZStack {
Image("test_pic")
.resizable()
.scaledToFit()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.screenWidth * 0.7 , height: UIScreen.screenHeight/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.clipShape(
CropFrame(isActive: clipped)
)
.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)
self.newPosition = self.currentPosition
})
Button (action : { self.clipped.toggle() }) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
Thanks to Asperi's answer, I have implement a lightweight swiftUI library to crop image.Here is the library and demo. Demo
The magic is below:
public var body: some View {
GeometryReader { proxy in
// ...
Button(action: {
// how to crop the image according to rectangle area
if self.tempResult == nil {
self.cropTheImageWithImageViewSize(proxy.size)
}
self.resultImage = self.tempResult
}) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
func cropTheImageWithImageViewSize(_ size: CGSize) {
let imsize = inputImage.size
let scale = max(inputImage.size.width / size.width,
inputImage.size.height / size.height)
let zoomScale = self.scale
let currentPositionWidth = self.dragAmount.width * scale
let currentPositionHeight = self.dragAmount.height * scale
let croppedImsize = CGSize(width: (self.cropSize.width * scale) / zoomScale, height: (self.cropSize.height * scale) / zoomScale)
let xOffset = (( imsize.width - croppedImsize.width) / 2.0) - (currentPositionWidth / zoomScale)
let yOffset = (( imsize.height - croppedImsize.height) / 2.0) - (currentPositionHeight / zoomScale)
let croppedImrect: CGRect = CGRect(x: xOffset, y: yOffset, width: croppedImsize.width, height: croppedImsize.height)
if let cropped = inputImage.cgImage?.cropping(to: croppedImrect) {
//uiimage here can write to data in png or jpeg
let croppedIm = UIImage(cgImage: cropped)
tempResult = croppedIm
result = Image(uiImage: croppedIm)
}
}
Hear is code
// Created by Deepak Gautam on 15/02/22.
import SwiftUI
struct ImageCropView: View {
#Environment(\.presentationMode) var pm
#State var imageWidth:CGFloat = 0
#State var imageHeight:CGFloat = 0
#Binding var image : UIImage
#State var dotSize:CGFloat = 13
var dotColor = Color.init(white: 1).opacity(0.9)
#State var center:CGFloat = 0
#State var activeOffset:CGSize = CGSize(width: 0, height: 0)
#State var finalOffset:CGSize = CGSize(width: 0, height: 0)
#State var rectActiveOffset:CGSize = CGSize(width: 0, height: 0)
#State var rectFinalOffset:CGSize = CGSize(width: 0, height: 0)
#State var activeRectSize : CGSize = CGSize(width: 200, height: 200)
#State var finalRectSize : CGSize = CGSize(width: 200, height: 200)
var body: some View {
ZStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.overlay(GeometryReader{geo -> AnyView in
DispatchQueue.main.async{
self.imageWidth = geo.size.width
self.imageHeight = geo.size.height
}
return AnyView(EmptyView())
})
Text("Crop")
.padding(6)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.offset(y: -250)
.onTapGesture {
let cgImage: CGImage = image.cgImage!
let scaler = CGFloat(cgImage.width)/imageWidth
if let cImage = cgImage.cropping(to: CGRect(x: getCropStartCord().x * scaler, y: getCropStartCord().y * scaler, width: activeRectSize.width * scaler, height: activeRectSize.height * scaler)){
image = UIImage(cgImage: cImage)
}
pm.wrappedValue.dismiss()
}
Rectangle()
.stroke(lineWidth: 1)
.foregroundColor(.white)
.offset(x: rectActiveOffset.width, y: rectActiveOffset.height)
.frame(width: activeRectSize.width, height: activeRectSize.height)
Rectangle()
.stroke(lineWidth: 1)
.foregroundColor(.white)
.background(Color.green.opacity(0.3))
.offset(x: rectActiveOffset.width, y: rectActiveOffset.height)
.frame(width: activeRectSize.width, height: activeRectSize.height)
.gesture(
DragGesture()
.onChanged{drag in
let workingOffset = CGSize(
width: rectFinalOffset.width + drag.translation.width,
height: rectFinalOffset.height + drag.translation.height
)
self.rectActiveOffset.width = workingOffset.width
self.rectActiveOffset.height = workingOffset.height
activeOffset.width = rectActiveOffset.width - activeRectSize.width / 2
activeOffset.height = rectActiveOffset.height - activeRectSize.height / 2
}
.onEnded{drag in
self.rectFinalOffset = rectActiveOffset
self.finalOffset = activeOffset
}
)
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 12))
.background(Circle().frame(width: 20, height: 20).foregroundColor(dotColor))
.frame(width: dotSize, height: dotSize)
.foregroundColor(.black)
.offset(x: activeOffset.width, y: activeOffset.height)
.gesture(
DragGesture()
.onChanged{drag in
let workingOffset = CGSize(
width: finalOffset.width + drag.translation.width,
height: finalOffset.height + drag.translation.height
)
let changeInXOffset = finalOffset.width - workingOffset.width
let changeInYOffset = finalOffset.height - workingOffset.height
if finalRectSize.width + changeInXOffset > 40 && finalRectSize.height + changeInYOffset > 40{
self.activeOffset.width = workingOffset.width
self.activeOffset.height = workingOffset.height
activeRectSize.width = finalRectSize.width + changeInXOffset
activeRectSize.height = finalRectSize.height + changeInYOffset
rectActiveOffset.width = rectFinalOffset.width - changeInXOffset / 2
rectActiveOffset.height = rectFinalOffset.height - changeInYOffset / 2
}
}
.onEnded{drag in
self.finalOffset = activeOffset
finalRectSize = activeRectSize
rectFinalOffset = rectActiveOffset
}
)
}
.onAppear {
activeOffset.width = rectActiveOffset.width - activeRectSize.width / 2
activeOffset.height = rectActiveOffset.height - activeRectSize.height / 2
finalOffset = activeOffset
}
}
func getCropStartCord() -> CGPoint{
var cropPoint : CGPoint = CGPoint(x: 0, y: 0)
cropPoint.x = imageWidth / 2 - (activeRectSize.width / 2 - rectActiveOffset.width )
cropPoint.y = imageHeight / 2 - (activeRectSize.height / 2 - rectActiveOffset.height )
return cropPoint
}
}
struct TestCrop : View{
#State var imageWidth:CGFloat = 0
#State var imageHeight:CGFloat = 0
#State var image:UIImage
#State var showCropper : Bool = false
var body: some View{
VStack{
Text("Open Cropper")
.font(.system(size: 17, weight: .medium))
.padding(.horizontal, 15)
.padding(.vertical, 10)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.onTapGesture {
showCropper = true
}
Image(uiImage: image)
.resizable()
.scaledToFit()
}
.sheet(isPresented: $showCropper) {
//
} content: {
ImageCropView(image: $image)
}
}
}
struct TestViewFinder_Previews: PreviewProvider {
static var originalImage = UIImage(named: "food")
static var previews: some View {
TestCrop(image: originalImage ?? UIImage())
}
}

How to change anchor of rotation angle to center with CGAffineTransform?

For some raisons, in my app in SwiftUI, I need to use transformEffect modifier with CGAffineTransform and rotationAngle property. But the result is la this:
How can I set the anchor of rotation angle to center of my arrow image? I see in the document that can we use CGPoint?
My code:
import SwiftUI
import CoreLocation
struct Arrow: View {
var locationManager = CLLocationManager()
#ObservedObject var heading: LocationManager = LocationManager()
private var animationArrow: Animation {
Animation
.easeInOut(duration: 0.2)
}
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.overlay(RoundedRectangle(cornerRadius: 0)
.stroke(Color.white, lineWidth: 2))
.animation(animationArrow)
.transformEffect(
CGAffineTransform(rotationAngle: CGFloat(-heading.heading.degreesToRadians))
.translatedBy(x: 0, y: 0)
)
Text(String(heading.heading.degreesToRadians))
.font(.system(size: 30))
.fontWeight(.light)
}
}
}
If you want to do it with a transform matrix, you'll need to translate, rotate, translate. But you can perform a rotationEffect with an anchor point. Note that the default anchor point is center. I am just including it explicity, so it works if you want to rotate anchoring somewhere else.
The anchor point is specified in UnitPoint, which means coordinates are 0 to 1. With 0.5 meaning center.
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: angle), anchor: UnitPoint(x: 0.5, y: 0.5))
Slider(value: $angle, in: 0...360)
}
}
}
The same effect, using matrices, is uglier, but also works:
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.transformEffect(
CGAffineTransform(translationX: -50, y: -50)
.concatenating(CGAffineTransform(rotationAngle: CGFloat(angle * .pi / 180)))
.concatenating(CGAffineTransform(translationX: 50, y: 50)))
Slider(value: $angle, in: 0...360)
}
}
}