Rotate rectangle by dragging icon - swift

I have a rectangle, that I need to rotate about itself, by dragging the icon in the top right corner. Now I have 2 versions of code, both of this doesn't work almost as it should, and I can't figure out what's wrong with second version. The question is, how I should change code to make a rectangle rotate with an icon on it?
struct rotateTest: View {
#State var angle: Angle = .zero
#State var recSize: CGSize = CGSize(width: 150, height: 80)
var body: some View {
Rectangle()
.foregroundColor(.blue)
.frame(width: recSize.width, height: recSize.height)
// .rotationEffect(angle) //v1 if rotation effect applyed here, rectangle rotate as it should, but rotation icon dont move with it (pic. 1)
.overlay(
Image(systemName: "arrow.counterclockwise.circle.fill")
.offset(x: recSize.width / 2, y: -recSize.height / 2)
.gesture(DragGesture()
.onChanged{ value in
angle = calcRotationAngle(value.translation)
})
)
.rotationEffect(angle) //v2 if rotation effect applyed here, icon move with rectangle, but rotation become unpredictable (pic. 2)
}
func calcRotationAngle(_ translation: CGSize) -> Angle {
Angle(radians: Double(
atan2(translation.height - recSize.height / 2,
translation.width + recSize.width / 2 )))
}
}
pic. 1:
pic. 2:

Related

How to implement the same iOS 16 lock screen circular widget myself?

I'm trying to implement lock screen widget myself
Widget I currently implement
I want to implement this ios16 lock screen widget
I've made almost everything, but I haven't been able to implement the small circle's transparent border.
I couldn't find a way to make even the background of the ring behind it transparent.
My code
struct RingTipShape: Shape { // small circle
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
let angle = CGFloat((240 * currentPercentage) * .pi / 180)
let controlRadius: CGFloat = rect.width / 2 - thickness / 2
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let x = center.x + controlRadius * cos(angle)
let y = center.y + controlRadius * sin(angle)
let pointCenter = CGPoint(x: x, y: y)
path.addEllipse(in:
CGRect(
x: pointCenter.x - thickness / 2,
y: pointCenter.y - thickness / 2,
width: thickness,
height: thickness
)
)
return path
}
var animatableData: Double {
get { return currentPercentage }
set { currentPercentage = newValue }
}
}
struct RingShape: Shape {
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.width / 2, y: rect.height / 2), radius: rect.width / 2 - (thickness / 2), startAngle: Angle(degrees: 0), endAngle: Angle(degrees: currentPercentage * 240), clockwise: false)
return path.strokedPath(.init(lineWidth: thickness, lineCap: .round, lineJoin: .round))
}
var animatableData: Double {
get { return currentPercentage}
set { currentPercentage = newValue}
}
}
struct CircularWidgetView: View { // My customizing widget view
#State var percentage: Double = 1.0
var body: some View {
GeometryReader { geo in
ZStack {
RingBackgroundShape(thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.21))
RingShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.385))
RingTipShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white)
/*
I want to make RingTipShape completely
transparent. Ignoring even the RingShape behind it
*/
VStack(spacing: 4) {
Image(systemName: "scooter")
.resizable()
.frame(width: 24, height: 24)
Text("hello")
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
}
}
}
How can I make a transparent border that also ignores the background of the view behind it?
This is a great exercise. The missing piece is a mask.
Note: Despite the fact that there are numerous ways to improve the existing code, I will try to stick to the original solution since the point is to gain experience through practice (based on the comments). However I will share some tips at the end.
So we can think of it in two steps:
We need some way to make another RingTipShape at the same (centered) position as our existing but a bit larger.
We need to find a way to create a mask that removes only that shape from other content (in our case the track rings)
The first point is an easy one, we just need to define the outer thickness in order to place the ellipse on top of the track at the correct location:
struct RingTipShape: Shape { // small circle
//...
let outerThickness: CGFloat
//...
let controlRadius: CGFloat = rect.width / 2 - outerThickness / 2
//...
}
then our existing code changes to:
RingTipShape(currentPercentage: percentage, thickness: 5.5, outerThickness: 5.5)
now for the second part we need something to create a larger circle, which is easy:
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
ok so now for the final part, we are going to use this (larger) shape to create a kind of inverted mask:
private var thumbMask: some View {
ZStack {
Color.white // This part will be transparent
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
.fill(Color.black) // This will be masked out
.rotationEffect(Angle(degrees: 150))
}
.compositingGroup() // Rasterize the shape
.luminanceToAlpha() // Map luminance to alpha values
}
and we apply the mask like this:
RingShape(currentPercentage: percentage, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.foregroundColor(.white.opacity(0.385))
.mask(thumbMask)
which results to this:
Some observations/tips:
You don't need the GeometryReader (and all the frame modifiers) in your CircularWidgetView, the ZStack will offer all available space to views.
You can add .aspectRatio(contentMode: .fit) to your image in order to avoid stretching.
You could take advantage of existing apis for making your track shapes.
For example:
struct MyGauge: View {
let value: Double = 0.5
let range = 0.1...0.9
var body: some View {
ZStack {
// Backing track
track().opacity(0.2)
// Value track
track(showsProgress: true)
}
}
private var mappedValue: Double {
(range.upperBound + range.lowerBound) * value
}
private func track(showsProgress: Bool = false) -> some View {
Circle()
.trim(from: range.lowerBound, to: showsProgress ? mappedValue : range.upperBound)
.stroke(.white, style: .init(lineWidth: 5.5, lineCap: .round))
.rotationEffect(.radians(Double.pi / 2))
}
}
would result to:
which simplifies things a bit by utilizing the trim modifier.
I hope that this makes sense.

How to adapt frame of half circle to its size

I am making an app where I need to use half circle. But I can't make its frame match the real size of the object. Right now I am trimming the circle but I don't really know you to actually crop the view.
Here is how I trimming the circle:
Circle()
.trim(from: 0, to: 0.5)
.fill(Color.blue)
.background(Color.red)
And I am getting colour red as background twice bigger the my new object.
It's probably better to make your own HalfCircle that conforms to Shape instead of trying to bash Circle into the shape you need. Let's go the extra mile and make it conform to InsettableShape, in case you want to use it to stroke a border.
import SwiftUI
struct HalfCircle: InsettableShape {
var _inset: CGFloat = 0
func inset(by amount: CGFloat) -> Self {
var copy = self
copy._inset += amount
return copy
}
func path(in rect: CGRect) -> Path {
var path = Path()
// This is a half-circle centered at the origin with radius 1.
path.addArc(
center: .zero,
radius: 1,
startAngle: .zero,
endAngle: .radians(.pi),
clockwise: false
)
path.closeSubpath()
// Since it's the bottom half of a circle, we only want
// to inset the left, right, and bottom edges of rect.
let rect = rect
.insetBy(dx: _inset, dy: 0.5 * _inset)
.offsetBy(dx: 0, dy: -(0.5 * _inset))
// This transforms bounding box of the path to fill rect.
let transform = CGAffineTransform.identity
.translatedBy(x: rect.origin.x + 0.5 * rect.size.width, y: 0)
.scaledBy(x: rect.width / 2, y: rect.height)
return path.applying(transform)
}
}
We can use it to draw this:
using this playground:
import PlaygroundSupport
PlaygroundPage.current.setLiveView(HalfCircle()
.inset(by: 20)
.fill(.black)
.background(.gray)
.aspectRatio(2, contentMode: .fit)
.frame(width: 200)
.padding()
)
You can mask it differently using a GeometryReader:
GeometryReader { proxy in
Circle()
.fill(.blue)
.offset(y: -proxy.size.height) // <- Shows bottom part
.frame(height: proxy.size.width) // <- Makes the circle match frame
.background(.red) // <- Just for testing
}
.aspectRatio(2, contentMode: .fit) // <- Makes the frame ration 2 : 1 (landscape rectangle)
.clipped() // <- Removes out of bound drawings

Brightness Saturation circular pad in Swift

Preamble
I'm trying to create a rounded Brightness Saturation pad using SwiftUI both for indication (from a given color, show the cursor position) and use (by moving the cursor, obtain the resulting color).
I apologize for my bad English 😅.
Presentation code
Firstly I draw the Circle using two Circle elements, filled with a LinearGradient element and the luminosity bland mode.
public var body: some View {
ZStack(alignment: .top) {
//MARK: - Inner Color circle
Group {
Circle()
.fill(
LinearGradient(
colors: [self.minSaturatedColor, self.maxSaturatedColor],
startPoint: .leading,
endPoint: .trailing
)
)
Circle()
.fill(
LinearGradient(
colors: [.white, .black],
startPoint: .top,
endPoint: .bottom
)
)
.blendMode(.luminosity)
}
.frame(width: self.diameter, height: self.diameter, alignment: .center)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged(self.updateCursorPosition)
.onEnded({ _ in
self.startIndication = nil
self.changed(self.finalColor)
})
)
//MARK: - Indicator
Circle()
.stroke(Color.black, lineWidth: 2)
.foregroundColor(Color.clear)
.frame(width: self.cursorDiameter, height: self.cursorDiameter, alignment: .center)
.position(cursorPosition)
}
.frame(width: self.diameter, height: self.diameter, alignment: .center)
}
Theory
Here's the theory above the mixtures of the color saturation with the brightness.
I'm not sure that this is the correct approach, but it seems so because it produces the following circle having (probably) all the shades:
Problem 1
The simplified code that I used to determine the Cursor position (from a given color) is:
public var cursorPosition: CGPoint {
let hsb = self.finalColor.hsba
let x: CGFloat = hsb.saturation * self.diameter
let y: CGFloat = hsb.brightness * self.diameter
// Adjust the Apple reversed ordinate
return CGPoint(x: x, y: self.diameter - y)
}
Unfortunately, this it seems not correct because by giving a certain color (I.e. #bd4d22) it makes the pointer placed in a wrong position.
Problem 2
There's also another problem: the component should allow to move the cursor and so update the finalColor State property with the correct amount of brightness and saturation.
private func updateCursorPosition(_ value: DragGesture.Value) -> Void {
let hsb = self.baseColor.hsba
let saturation = value.location.x / self.diameter
let brightness = (0 - value.location.y + self.diameter) / self.diameter
self.finalColor = Color(
hue: hsb.hue,
saturation: saturation,
brightness: brightness
)
}
yet the result is not what expected because, event tough the cursor follows the point (meaning that the computed value is correct), the resulting color is not what rendered behind the pointer!
Example pt. 1
Here, as you can see, the full saturated color (that should theoretically be 2π of the circumference) is not correct.
Example pt. 2
Here, as you can see, there's an issue by going outside of the circle.
Question
Can anyone explain where am I failing?
Can anyone help me blocking the updateCursorPosition resulting color value, if the cursor is dragged out of the circle?
Am I doing something else in the wrong way?
Thank you for the patience 🖖🏻!
Ps. To accomplish this, I take clue form what done by Procreate and what asked here Circular saturation-brightness gradient for color wheel
Okay, this took a while as it wasn't one thing. I ended up pulling your code apart and rebuilding it a little more simply, as well as refactoring the indicator from the color view. To fix the indicator, I took inspiration from this answer. Give it a bump.
As to the color wheel itself, I simplified it into a Circle() with an overlay of the second Circle(). Getting the colors straight took a lot more time as I am not a designer, so I had to work through dealing with the colors less intuitively. I had a two realizations while dealing with this.
To handle the maximum and minimum saturations, the only variable that could be used was the hue. The brightness and saturation were fixed, either 0 or 1 for saturation and 1 for brightness, otherwise the base color gradient was off.
That caused me to have to deal with the brightness gradient. The color was already at maximum brightness, so putting a gradient from white to black rendered incorrectly, yet again. Another realization I had was that the brightness gradient should go from clear to black. That resolved the color issues. The code is below and commented.
The Color Wheel:
struct ColorWheelView: View {
#State private var position: CGPoint
#Binding public var selectedColor: Color
private var saturation: CGFloat {
position.x / diameter
}
private var brightness: CGFloat {
(diameter - position.y) / diameter
}
private let diameter: CGFloat
private let cursorDiameter: CGFloat
private let baseHSB: (hue: Double, saturation: Double, brightness: Double, alpha: Double)
private let minSaturatedColor: Color
private let maxSaturatedColor: Color
public init(
//there needs to be an intitial fixed color to set the hue from
color: Color,
//this returns the selected color. It can start as anything as it is set to
//color in .onAppear()
selectedColor: Binding<Color>,
diameter: CGFloat = 200,
cursorDiameter: CGFloat = 20
) {
let hsb = color.hsba
// Because the view uses the different parts of color, it made sense to just have
// an hsba here. I would consider making this its own Type just for readability.
// The only thing that this entire variable is used for is setting the initial selected
// color. Otherwise I could have just kept the hue.
baseHSB = Color(hue: color.hsba.hue, saturation: color.hsba.saturation, brightness: color.hsba.brightness).hsba
// This sets the initial indicator position to be at the correct place on the wheel as
//The initial color sent in, including the initial saturation and brightness.
_position = State(initialValue: CGPoint(x: hsb.saturation * diameter, y: diameter - (hsb.brightness * diameter)))
// This is the return color.
_selectedColor = selectedColor
// self is only needed to avoid confusion for the compiler.
self.diameter = diameter
self.cursorDiameter = cursorDiameter
// Note these are set to maximum brightness.
minSaturatedColor = Color(hue: baseHSB.hue, saturation: 0, brightness: 1)
maxSaturatedColor = Color(hue: baseHSB.hue, saturation: 1, brightness: 1)
}
var body: some View {
VStack {
Circle()
.fill(
LinearGradient(
colors: [self.minSaturatedColor, self.maxSaturatedColor],
startPoint: .leading,
endPoint: .trailing
)
)
.overlay(
Circle()
.fill(
LinearGradient(
// Instead of a gradient of white to black, this is clear to black
// as the saturation gradients are at maximum brightness already.
colors: [.clear, .black],
startPoint: .top,
endPoint: .bottom
)
)
.blendMode(.luminosity)
)
.overlay(
IndicatorView(position: $position, diameter: diameter, cursorDiameter: cursorDiameter)
)
.frame(width: self.diameter, height: self.diameter)
.onAppear {
// Sets the bound color to the original color.
selectedColor = Color(hue: baseHSB.hue, saturation: baseHSB.saturation, brightness: baseHSB.brightness, opacity: baseHSB.alpha)
}
.onChange(of: position) { _ in
// When position changes, this changes the saturation and brightness.
selectedColor = Color(
hue: baseHSB.hue,
saturation: saturation,
brightness: brightness
)
}
}
}
}
The Indicator:
struct IndicatorView: View {
#Binding var position: CGPoint
private let diameter: CGFloat
private let cursorDiameter: CGFloat
private let radius: CGFloat
private let center: CGPoint
init(position: Binding<CGPoint>, diameter: CGFloat, cursorDiameter: CGFloat) {
_position = position
self.diameter = diameter
self.cursorDiameter = cursorDiameter
self.radius = diameter / 2
// the center of the circle is the center of the frame which is CGPoint(radius, radius)
self.center = CGPoint(x: radius, y: radius)
}
var body: some View {
VStack {
Circle()
// This circle is simply to keep the indicator aligned with the ColorWheelView
.fill(Color.clear)
.overlay(
Circle()
.stroke(Color.black, lineWidth: 2)
.foregroundColor(Color.clear)
.frame(width: cursorDiameter, height: self.cursorDiameter)
.position(position)
.gesture(DragGesture()
.onChanged { value in
updatePosition(value.location)
}
)
)
.frame(width: diameter, height: diameter)
}
.onAppear {
updatePosition(position)
}
}
private func updatePosition(_ point: CGPoint) {
let currentLocation = point
let center = CGPoint(x: radius, y: radius)
let distance = center.distance(to:currentLocation)
// This triggers if the drag goes outside of the circle.
if distance > radius {
let overDrag = radius / distance
//These coordinates are guaranteed inside of the circle.
let newLocationX = (currentLocation.x - center.x) * overDrag + center.x
let newLocationY = (currentLocation.y - center.y) * overDrag + center.y
self.position = CGPoint(x: newLocationX, y: newLocationY)
}else{
self.position = point
}
}
}
extension CGPoint {
//This is simply the Pythagorean theorem. Distance is the hypotenuse.
func distance(to point: CGPoint) -> CGFloat {
return sqrt(pow((point.x - x), 2) + pow((point.y - y), 2))
}
}
Also, as a side note, most of the variables you had were constants and should be lets and not vars. Anything internal to the struct should be private. #State variables are always private.

How to draw a radial slice of a circle in SwiftUI ? (ie. pie chart like)

I am trying to create a slice of a circle in SwiftUI.
I am playing around with trim but it behaves oddly.
It picks the amount starting from the outmost point of the area.
Such that 0.5 is half a circle ok. But 0.25 of a circle is not a quarter slice (as a naive programming newbie would guess) but rather the shape filled in a non logical way. There also doesnt seem a way to modify this.
It would be sensible to define a Circle by saying Circle(radius: x, angle: y) instead such that angle: 360 would be a full circle filled.
Does one need to program this oneself in SwiftUI?
import SwiftUI
struct CircleView: View {
var body: some View {
let trimValue: CGFloat = 0.25
Circle()
.trim(from: 0, to: trimValue)
.fill(Color.red)
}
}
struct CircleView_Previews: PreviewProvider {
static var previews: some View {
CircleView()
}
}
Using a Path with an arc is the normal way to do this:
struct PieSegment: Shape {
var start: Angle
var end: Angle
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: center)
path.addArc(center: center, radius: rect.midX, startAngle: start, endAngle: end, clockwise: false)
return path
}
}
struct ContentView: View {
#State var fileData: Data?
var body: some View {
PieSegment(start: .zero, end: .degrees(45))
}
}

How to Create a SwiftUI RoundedStar Shape?

This is a self-answered question which is perfectly acceptable (and even encouraged) on Stack Overflow. The point is to share something useful to others.
SwiftUI has a RoundedRectangle Shape. It would be nice to have a five-pointed star with rounded tips that could be used for filling, clipping, and animation.
This Stack Overflow answer shows how to make a RoundedStar as a custom UIView using UIBezierPath.
How can this code be adapted to SwiftUI as a Shape that can be animated?
Here is the RoundedStar code adapted as an animatable SwiftUI Shape:
// Five-point star with rounded tips
struct RoundedStar: Shape {
var cornerRadius: CGFloat
var animatableData: CGFloat {
get { return cornerRadius }
set { cornerRadius = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let r = rect.width / 2
let rc = cornerRadius
let rn = r * 0.95 - rc
// start angle at -18 degrees so that it points up
var cangle = -18.0
for i in 1 ... 5 {
// compute center point of tip arc
let cc = CGPoint(x: center.x + rn * CGFloat(cos(Angle(degrees: cangle).radians)), y: center.y + rn * CGFloat(sin(Angle(degrees: cangle).radians)))
// compute tangent point along tip arc
let p = CGPoint(x: cc.x + rc * CGFloat(cos(Angle(degrees: cangle - 72).radians)), y: cc.y + rc * CGFloat(sin(Angle(degrees: (cangle - 72)).radians)))
if i == 1 {
path.move(to: p)
} else {
path.addLine(to: p)
}
// add 144 degree arc to draw the corner
path.addArc(center: cc, radius: rc, startAngle: Angle(degrees: cangle - 72), endAngle: Angle(degrees: cangle + 72), clockwise: false)
// Move 144 degrees to the next point in the star
cangle += 144
}
return path
}
}
The code is very similar to the UIBezierPath version except that it uses the new Angle type which provides easy access to both degrees and radians. The code to draw the star rotated was removed because it is easy to add rotation to a SwiftUI shape with the .rotationEffect(angle:) view modifier.
Demonstration:
Here is a demonstration that show the animatable qualities of the cornerRadius setting as well as showing what the various cornerRadius settings look like on a full-screen star.
struct ContentView: View {
#State private var radius: CGFloat = 0.0
var body: some View {
ZStack {
Color.blue.edgesIgnoringSafeArea(.all)
VStack(spacing: 40) {
Spacer()
RoundedStar(cornerRadius: radius)
.aspectRatio(1, contentMode: .fit)
.foregroundColor(.yellow)
.overlay(Text(" cornerRadius: \(Int(self.radius)) ").font(.body))
HStack {
ForEach([0, 10, 20, 40, 80, 200], id: \.self) { value in
Button(String(value)) {
withAnimation(.easeInOut(duration: 0.3)) {
self.radius = CGFloat(value)
}
}
.frame(width: 50, height: 50)
.foregroundColor(.black)
.background(Color.yellow.cornerRadius(8))
}
}
Spacer()
}
}
}
}
Running in Swift Playgrounds on iPad
This runs beautifully on an iPad in the Swift Playgrounds app. Just add:
import PlaygroundSupport
at the top and
PlaygroundPage.current.setLiveView(ContentView())
at the end.
Using the RoundedStar shape to create EU Flag
struct ContentView: View {
static let flagSize: CGFloat = 234 // Change this to resize flag
let flagHeight: CGFloat = flagSize
let flagWidth: CGFloat = flagSize * 1.5
let radius: CGFloat = flagSize / 3
let starWidth: CGFloat = flagSize / 9
let pantoneReflexBlue = UIColor(red: 0, green: 0x33/0xff, blue: 0x99/0xff, alpha: 1)
let pantoneYellow = UIColor(red: 1, green: 0xcc/0xff, blue: 0, alpha: 1)
var body: some View {
ZStack {
Color(pantoneReflexBlue).frame(width: flagWidth, height: flagHeight, alignment: .center)
ForEach(0..<12) { n in
RoundedStar(cornerRadius: 0)
.frame(width: starWidth, height: starWidth)
.offset(x: radius * cos(CGFloat(n) / CGFloat(12) * 2 * .pi), y: radius * sin(CGFloat(n) / CGFloat(12) * 2 * .pi))
.foregroundColor(Color(pantoneYellow))
}
}
}
}