I'm attempting to do a simple pop-out animation with some text and an image in SwiftUI. In the preview on the simulator it looks fine, but when using a physical device to preview or when building and running on a physical device the characters in the text of the animation shake and it looks awful. Interestingly, the image moves fine.
I'm still new to SwiftUI so any advice or input would be very appreciated.
struct SplashScreenContent: View {
#State private var blurRadius: CGFloat = 10
#State private var movingShadowX: CGFloat = 4
#State private var movingShadowY: CGFloat = 4
#State private var imageSize: CGFloat = 50
#State private var fontSize: CGFloat = 55
#State private var bottomInset: CGFloat = -5
#State private var trailingInset: CGFloat = -3
var body: some View {
VStack {
Button("Animate", action: buttonPress)
Spacer()
VStack(spacing: 45) {
Image(systemName: "house")
.resizable()
.foregroundColor(.white)
.scaledToFill()
.frame(width: imageSize, height: imageSize)
.shadow(color: .black, radius: 1.5)
.shadow(color: .yellow, radius: 5)
.shadow(color: .black, radius: 3, x: movingShadowX, y: movingShadowY)
Text("I'm so\nShakey")
.font(.custom("NewYork-regular", size: fontSize))
.fontWeight(.semibold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 70.0)
.shadow(color: .black, radius: 1.5)
.shadow(color: .yellow, radius: 5)
.shadow(color: .black, radius: 3, x: movingShadowX, y: movingShadowY)
}
.padding(.init(top: 0, leading: 0, bottom: bottomInset, trailing: trailingInset))
.blur(radius: blurRadius)
.onAppear(perform: animate)
Spacer()
}
}
}
extension SplashScreenContent {
func buttonPress() {
blurRadius = 10
movingShadowX = 4
movingShadowY = 4
imageSize = 50
fontSize = 55
bottomInset = -5
trailingInset = -3
animate()
}
func animate() {
withAnimation(.easeIn(duration: 0.4).delay(0.8)) {
blurRadius = 0
withAnimation(.easeIn(duration: 4).delay(0.3)) {
movingShadowX = 6
movingShadowY = 9.5
imageSize = 53
fontSize = 58
bottomInset = 13
trailingInset = 3
withAnimation(.easeIn(duration: 0.5).delay(4.3)) {
blurRadius = 20
}
}
}
}
}
I tried removing the shadows, changing how much everything was animating, and so on. The text remains to be shakey.
Related
This is the desired outcome
This is what I have now
Can anyone help? I'm new to SwiftUI and I've been struggling for two days
The thin line and the rotation works well, but how can I keep the label horizontal at any rotation?
I have tried using a VSTack and that causes undesired behavior. And when I set the rotation only to the rectangle (thin line) I can't figure out how to correctly postion the label dynamically.
This is my code so far, and the piece at TodayLabel is where this is done
struct SingleRingProgressView: View {
let startAngle: Double = 270
let progress: Float // 0 - 1
let ringWidth: CGFloat
let size: CGFloat
let trackColor: Color
let ringColor: Color
let centerText: AttributedText?
let centerTextSubtitle: AttributedText?
let todayLabel: CircleGraph.Label?
private let maxProgress: Float = 2 // allows the ring show a progress up to 200%
private let shadowOffsetMultiplier: CGFloat = 4
private var absolutePercentageAngle: Float {
percentToAngle(percent: (progress * 100), startAngle: 0)
}
private var relativePercentageAngle: Float {
// Take into account the startAngle
absolutePercentageAngle + Float(startAngle)
}
#State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
VStack {
Spacer()
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, maxProgress)))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
if shouldShowShadow(frame: proxy.size) {
Circle()
.fill(ringColor)
.frame(width: ringWidth, height: ringWidth, alignment: .center)
.offset(y: -(size/2))
.rotationEffect(Angle.degrees(360 * Double(progress)))
.shadow(
color: Color.white,
radius: 2,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
.shadow(
color: Color.black.opacity(0.5),
radius: 1,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
}
// Today label
if let todayLabel = self.todayLabel {
ZStack {
StyledText(todayLabel.label)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.color(token: .hint))
.cornerRadius(2)
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}.rotationEffect(Angle.degrees(Double(todayLabel.degrees)))
}
VStack(spacing: 4) {
if let text = centerText {
StyledText(text)
}
if let subtitle = centerTextSubtitle {
StyledText(subtitle)
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
Spacer()
}
Spacer()
}
}
}
private func percentToAngle(percent: Float, startAngle: Float) -> Float {
(percent / 100 * 360) + startAngle
}
private func endCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + Float(startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = CGFloat(relativeXOffset) * shadowOffsetMultiplier
let yOffset = CGFloat(relativeYOffset) * shadowOffsetMultiplier
return (xOffset, yOffset)
}
private func shouldShowShadow(frame: CGSize) -> Bool {
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = CGFloat((360 - absolutePercentageAngle).toRadians())
if (progress * 100) >= 100 {
return true
} else if circleRadius * remainingAngleInRadians <= ringWidth {
return true
}
return false
}
}
just turn the inner text label back by -angle:
struct ContentView: View {
let startAngle: Double = 270
let progress: Float = 0.2 // 0 - 1
let ringWidth: CGFloat = 30
let size: CGFloat = 200
let trackColor: Color = .gray
let ringColor: Color = .blue
let todayLabeldegrees = 120.0
#State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(progress))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
// Today label
ZStack {
Text("todayLabel")
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.white)
.cornerRadius(5)
.shadow(radius: 2)
.rotationEffect(Angle.degrees(-todayLabeldegrees)) // << turn back
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}
.rotationEffect(Angle.degrees(todayLabeldegrees))
VStack(spacing: 4) {
Text("Test").font(.title)
Text("subtitle")
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
}
I have tried to find the Before-After slider effect for images in SwiftUI. But I cannot find any libraries or solutions. And finally I came up with below solution myself.
Hope this help someone!!
I have added background color for images for to make differ if there is no images.
import SwiftUI
struct ContentView: View {
#State private var location: CGPoint = CGPoint(x: 0, y: 0)
#State private var maskWidth: CGFloat = 0.0
#State var startPoint: CGFloat = 0
#State var endPoint: CGFloat = 0
#State var yPoint: CGFloat = 0
var sliderWidth: CGFloat = 30
var containerWidth: CGFloat = 400
var containerHeight: CGFloat = 300
var body: some View {
ZStack {
ZStack() {
Image("before_img")
.resizable()
.frame(width: containerWidth, height: containerHeight)
.background(Color.red)
.clipped()
Image("after_img")
.resizable()
.frame(width: containerWidth, height: containerHeight)
.clipped()
.background(Color.green)
.mask(mask)
}
.clipped()
Slider
}
.clipped()
.frame(width: containerWidth, height: containerHeight)
.onAppear {
yPoint = containerHeight/2
location = CGPoint(x: containerWidth/2, y: yPoint)
maskWidth = containerWidth/2
endPoint = containerWidth
}
}
var dragAction: some Gesture {
DragGesture()
.onChanged { value in
updateDragView(point: value.location)
updateMaskView(point: value.translation)
}
.onEnded { value in
setInitialPosition()
}
}
var mask: some View {
HStack {
Spacer()
Rectangle()
.mask(Color.black)
.frame(width: maskWidth, height: containerHeight)
}
}
var Slider: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.white)
.frame(width: 4)
Image(systemName: "circle.circle.fill")
.foregroundColor(.white)
.frame(width: sliderWidth, height: sliderWidth)
.font(.system(size: sliderWidth))
Rectangle()
.fill(Color.white)
.frame(width: 4)
}
.position(location)
.gesture(dragAction)
.shadow(radius: 4)
}
func updateDragView(point: CGPoint) {
let locX = point.x
if locX > startPoint && locX < endPoint {
self.location = CGPoint(x: point.x, y: yPoint)
}
}
func updateMaskView(point: CGSize) {
let width = -(point.width)
let newWidth = ((containerWidth/2)+width)
if newWidth > 0 {
maskWidth = ((containerWidth/2)+width)
} else {
setInitialPosition()
}
}
func setInitialPosition() {
withAnimation {
location = CGPoint(x: containerWidth/2, y: yPoint)
maskWidth = containerWidth/2
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
For example: I have a joystick that can be moved around freely, how can I make the dragging slower with distance? The further I drag the joystick, the slower the drag is.
Thanks in advance.
My Joystick code, dragging works but has no bounds and isn’t slowed down if you drag it to the edges:
import SwiftUI
struct ContentView: View {
#State var isDragging = false
#State var dragValue = CGSize.zero
var body: some View {
VStack {
Text("width: \(dragValue.width)")
Text("height: \(dragValue.height)")
VStack (spacing: 16) {
HStack(spacing: 35) {
Image(systemName: "chevron.left")
.foregroundColor(.gray)
VStack (spacing: 80) {
Image(systemName: "chevron.up")
.foregroundColor(.gray)
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
}
.offset(x: dragValue.width * 0.05, y: dragValue.height * 0.05)
.frame(width: 150, height: 150)
.background(LinearGradient(gradient: Gradient(colors: [Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)), Color(#colorLiteral(red: 0.8705882353, green: 0.8941176471, blue: 0.9450980392, alpha: 1))]), startPoint: .top, endPoint: .bottom))
.clipShape(RoundedRectangle(cornerRadius: isDragging ? (55 - abs(dragValue.height) / 10) : 55, style: .continuous))
.offset(x: dragValue.width, y: dragValue.height)
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 20)
.padding(.horizontal, 30)
.gesture(
DragGesture().onChanged { value in
self.dragValue = value.translation
self.isDragging = true
}
.onEnded { value in
self.dragValue = .zero
self.isDragging = false
}
)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is possible solution based on asymptotic curve (if somebody find it helpful).
Tested with Xcode 11.4 / iOS 13.4
Update: retested with Xcode 13.4 / iOS 15.5
Changed part
DragGesture().onChanged { value in
let limit: CGFloat = 200 // the less the faster resistance
let xOff = value.translation.width
let yOff = value.translation.height
let dist = sqrt(xOff*xOff + yOff*yOff);
let factor = 1 / (dist / limit + 1)
self.dragValue = CGSize(width: value.translation.width * factor,
height: value.translation.height * factor)
self.isDragging = true
}
This is how it works right now
I want the magnifying glass to pop up when the user is dragging, and show a zoomed in view of the photo to help the user drag the line to the right spot. I can't get the X and Y coordinates right, so it just goes wherever.
This is the ZStack that contains the magnifying glass...
ZStack{
Image("demo")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(50)
.frame(width: 500, height: 500, alignment: .center)
.position(x: CGFloat(self.magnifyX), y: CGFloat(self.magnifyY))
}
.frame(width: 100, height: 100, alignment: .center)
.clipShape(Circle())
.overlay(
ZStack{
Circle().fill(Color.black).frame(width: 5, height: 5, alignment: .center)
Circle().stroke(Color.black, lineWidth: 4)
}
)
.position(x: CGFloat(self.magnifyX), y: CGFloat(self.magnifyY-75))
I'd be open to other ways of doing it as well, if this is not the correct way to do it.
Thanks for the help!
Not an answer, but I can't comment yet... Are you able to share your code for magnifyX and magnifyY?
Looking at the screen capture you shared, the magnification seems to be following the line, it's obviously just higher, but it's also the opposite of/mirroring your movements, i.e. as you move left to right, it's moving right to left.
Have a look at the code below, the only thing I am still unable to fix/implement is to prevent the image moving off-screen:
import SwiftUI
struct ContentDetailPhoto : View {
//Properties
#State var scale: CGFloat = 1.0
#State var touchScale: CGFloat = 2
#State var isTouchingScreen = false
#State var isZoomedIn = false
#State var pointTouchedOnScreen: CGPoint = CGPoint.zero
#State var panSize: CGSize = CGSize.zero
#State var fingerState: String = "Finger is not touching the image"
#State var prevSize: CGSize = CGSize.zero
//#State var counter = 0
#State var touching = false
#State var xPos = 0
#State var yPos = 0
//let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var cakeName: String
var body: some View {
ZStack {
Image("\(self.cakeName)")
.grayscale(0.999)
.opacity(0.5)
GeometryReader { reader in
Image("\(self.cakeName)")
.resizable()
.cornerRadius(10)
.offset(x: self.panSize.width, y: self.panSize.height)
.scaleEffect(self.isTouchingScreen ? self.touchScale : 1, anchor: UnitPoint(x: self.pointTouchedOnScreen.x / reader.frame(in: .global).maxX, y: self.pointTouchedOnScreen.y / reader.frame(in: .global).maxY))
.aspectRatio(contentMode: self.isZoomedIn ? .fill : .fit)
.frame(maxWidth: UIScreen.main.bounds.size.width - 10, maxHeight: UIScreen.main.bounds.size.height * 0.7, alignment: .center)
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { (value) in
self.fingerState = "Finger is touching the image" // for debug purpose only
self.isZoomedIn = true
self.isTouchingScreen = true
self.pointTouchedOnScreen = value.startLocation
self.scale = self.touchScale
self.panSize = CGSize(width: (value.translation.width * self.touchScale/2), height: (value.translation.height * self.touchScale/2))
}
.onEnded { _ in
self.fingerState = "Finger is not touching the image" // for debug purpose only
self.isZoomedIn = false
self.isTouchingScreen = false
self.panSize = CGSize.zero
self.prevSize = CGSize.zero
})
.cornerRadius(10)
.animation(.easeInOut(duration: 1))
//.offset(x: 0, y: -50)
}.edgesIgnoringSafeArea(.top)
// Add a watermark on top of all the layers
// Image("logo_small")
// .resizable()
// .scaledToFit()
// .opacity(0.05)
// .frame(width: UIScreen.main.bounds.width - 10, height: UIScreen.main.bounds.height - 10, alignment: .center)
// .clipShape(Circle())
}
}
}
#if DEBUG
struct MapView01_Previews: PreviewProvider {
static var previews: some View {
Group {
// iPhone SE
ContentDetailPhoto(cakeName: "cake_0000")
.previewDevice("iPhone SE")
// iPhone X
ContentDetailPhoto(cakeName: "cake_0000")
.previewDevice("iPhone X")
}
}
}
#endif
It looks like you're SwiftUI in an iPhone app, using the iOS simulator. The simulator simulates touches. There is no actual API on the iPhone that supports that because you're finger can't magically turn into a magnifying glass (there is no other pointer input).
macOS and iPadOS is a bit of a different story. On macOS you could use the Hover API to natively change the pointer using NSCursor, or make use of the new UIPointerInteraction on iPadOS.
I want a view in which the user is shown an image. By dragging the corner points I want him to be capable of choosing the crop rectangle.
Because the input image can be of bigger dimensions than the screen, I want to use aspect fit as content mode of the image.
The problem I have is that I don't know how to take the displacement caused by the content mode into account when determining the measurements of the crop rectangle relative to the original size of the image.
This might be easier to explain in a video:
As you can see, the context for the positions of the circles is the whole view. I want to use the coordinate system of the resized image instead. For this transformation I would need the difference between the size of the outer view and the resized image.
So the question is: How can I get the correct measurements of the user-chosen rectangle with respect to the resized image?
import SwiftUI
struct CropImageViewTest: View {
var currentImage: Image
#State private var currentPositionTopLeft: CGPoint = .zero
#State private var newPositionTopLeft: CGPoint = .zero
#State private var currentPositionTopRight: CGPoint = .zero
#State private var newPositionTopRight: CGPoint = .zero
#State private var currentPositionBottomLeft: CGPoint = .zero
#State private var newPositionBottomLeft: CGPoint = .zero
#State private var currentPositionBottomRight: CGPoint = .zero
#State private var newPositionBottomRight: CGPoint = .zero
var body: some View {
ZStack {
VStack {
Text("Top left: \(currentPositionTopLeft.x) | \(currentPositionTopLeft.y)")
Text("Top right: \(currentPositionTopRight.x) | \(currentPositionTopRight.y)")
Text("Bottom left: \(currentPositionBottomLeft.x) | \(currentPositionBottomLeft.y)")
Text("Bottom right: \(currentPositionBottomRight.x) | \(currentPositionBottomRight.y)")
Spacer()
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.background(Color.red)
Spacer()
Group {
Button(action: {
// TODO: Crop it
}) {
Image(systemName: "checkmark").resizable().frame(width: 24, height: 24)
.padding(20)
.background(Color(Colors.getColor(Colors.colorSboBlue)))
.foregroundColor(Color.white)
}.clipShape(Circle())
.shadow(radius: 4)
}
}
getCorners()
}
}
private func getCorners() -> some View{
return
HStack {
VStack {
ZStack {
GeometryReader { geometry in
Path { path in
path.move(to: self.currentPositionTopLeft)
path.addLine(
to: .init(
x: self.currentPositionTopRight.x + geometry.size.width,
y: self.currentPositionTopRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomRight.x + geometry.size.width,
y: self.currentPositionBottomRight.y + geometry.size.height
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomLeft.x,
y: self.currentPositionBottomLeft.y + geometry.size.height
)
)
path.addLine(
to: .init(
x: self.currentPositionTopLeft.x,
y: self.currentPositionTopLeft.y
)
)
}
.stroke(Color.blue, lineWidth: CGFloat(1))
}
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionTopLeft.x, y: self.currentPositionTopLeft.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionTopLeft = CGPoint(x: value.translation.width + self.newPositionTopLeft.x, y: value.translation.height + self.newPositionTopLeft.y)
}
.onEnded { value in
self.currentPositionTopLeft = CGPoint(x: value.translation.width + self.newPositionTopLeft.x, y: value.translation.height + self.newPositionTopLeft.y)
self.newPositionTopLeft = self.currentPositionTopLeft
print(self.currentPositionTopLeft)
print(self.newPositionTopLeft)
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: 0))
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionTopRight.x, y: self.currentPositionTopRight.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionTopRight = CGPoint(x: value.translation.width + self.newPositionTopRight.x, y: value.translation.height + self.newPositionTopRight.y)
}
.onEnded { value in
self.currentPositionTopRight = CGPoint(x: value.translation.width + self.newPositionTopRight.x, y: value.translation.height + self.newPositionTopRight.y)
self.newPositionTopRight = self.currentPositionTopRight
print(self.currentPositionTopRight)
print(self.newPositionTopRight)
}
)
.opacity(0.5)
.position(CGPoint(x: geometry.size.width, y: 0))
}
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionBottomLeft.x, y: self.currentPositionBottomLeft.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionBottomLeft = CGPoint(x: value.translation.width + self.newPositionBottomLeft.x, y: value.translation.height + self.newPositionBottomLeft.y)
}
.onEnded { value in
self.currentPositionBottomLeft = CGPoint(x: value.translation.width + self.newPositionBottomLeft.x, y: value.translation.height + self.newPositionBottomLeft.y)
self.newPositionBottomLeft = self.currentPositionBottomLeft
print(self.currentPositionBottomLeft)
print(self.newPositionBottomLeft)
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: geometry.size.height))
}
GeometryReader { geometry in
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPositionBottomRight.x, y: self.currentPositionBottomRight.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPositionBottomRight = CGPoint(x: value.translation.width + self.newPositionBottomRight.x, y: value.translation.height + self.newPositionBottomRight.y)
}
.onEnded { value in
self.currentPositionBottomRight = CGPoint(x: value.translation.width + self.newPositionBottomRight.x, y: value.translation.height + self.newPositionBottomRight.y)
self.newPositionBottomRight = self.currentPositionBottomRight
print(self.currentPositionBottomRight)
print(self.newPositionBottomRight)
}
)
.opacity(0.5)
.position(CGPoint(x: geometry.size.width, y: geometry.size.height))
}
}
Spacer()
}
Spacer()
}
}
}
If you don't have a sample image with you, you can just call the View this way:
CropImageViewTest(currentImage: Image(systemName: "camera.fill"))
I added a red background so that you can see the constraints of the image.
I am also open to completely different approaches if the current way is not the "swiftiest" way to go.
Thank you in advance!
Edit:
I have the UIImage available the (SwiftUI) Image originates from. If this is of any help in determining the correct measurements.
Update:
If I use the crop rectangle as an overlay of the image like so:
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.overlay(getCorners())
it's actually working. Still, there is the problem that every corner defines its starting position is (0|0). I would like the position to be defined relative to the upper left corner of the image.
Okay, finally solved it.
1.) I used the view with the rectangle and the draggable corners as an overlay of the Image. This way, the origin of the rectangle and the corners is the image, not the surrounding view. Got the inspiration for that from here: https://swiftui-lab.com/geometryreader-to-the-rescue/
2.) There was still the problem that every corner defined it origin (0|0) as where it was initially positioned. I got around that by using
.position(CGPoint(x: 0, y: 0))
and using onAppear to place displace the coordinates.
This leads to the application correctly calculating the coordinates relative to the resized image:
I also encapsulated the rectangle and the corners in custom views resulting in this code:
The root view:
import SwiftUI
struct CropImageViewTest: View {
var currentImage: Image
#State private var currentPositionTopLeft: CGPoint = .zero
#State private var newPositionTopLeft: CGPoint = .zero
#State private var currentPositionTopRight: CGPoint = .zero
#State private var newPositionTopRight: CGPoint = .zero
#State private var currentPositionBottomLeft: CGPoint = .zero
#State private var newPositionBottomLeft: CGPoint = .zero
#State private var currentPositionBottomRight: CGPoint = .zero
#State private var newPositionBottomRight: CGPoint = .zero
var body: some View {
ZStack {
VStack {
Text("Top left: \(currentPositionTopLeft.x) | \(currentPositionTopLeft.y)")
Text("Top right: \(currentPositionTopRight.x) | \(currentPositionTopRight.y)")
Text("Bottom left: \(currentPositionBottomLeft.x) | \(currentPositionBottomLeft.y)")
Text("Bottom right: \(currentPositionBottomRight.x) | \(currentPositionBottomRight.y)")
Spacer()
currentImage
.resizable()
.aspectRatio(1 , contentMode: .fit)
.overlay(getCorners())
Spacer()
Group {
Button(action: {
// TODO: Crop it
}) {
Image(systemName: "checkmark").resizable().frame(width: 24, height: 24)
.padding(20)
.background(Color(Colors.getColor(Colors.colorSboBlue)))
.foregroundColor(Color.white)
}.clipShape(Circle())
.shadow(radius: 4)
}
}
}
}
private func getCorners() -> some View{
return
HStack {
VStack {
ZStack {
CropImageViewRectangle(
currentPositionTopLeft: self.$currentPositionTopLeft,
currentPositionTopRight: self.$currentPositionTopRight,
currentPositionBottomLeft: self.$currentPositionBottomLeft,
currentPositionBottomRight: self.$currentPositionBottomRight
)
GeometryReader { geometry in
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionTopLeft,
newPosition: self.$newPositionTopLeft,
displacementX: 0,
displacementY: 0
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionTopRight,
newPosition: self.$newPositionTopRight,
displacementX: geometry.size.width,
displacementY: 0
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionBottomLeft,
newPosition: self.$newPositionBottomLeft,
displacementX: 0,
displacementY: geometry.size.height
)
CropImageViewRectangleCorner(
currentPosition: self.$currentPositionBottomRight,
newPosition: self.$newPositionBottomRight,
displacementX: geometry.size.width,
displacementY: geometry.size.height
)
}
}
Spacer()
}
Spacer()
}
}
}
The rectangle:
import SwiftUI
struct CropImageViewRectangle: View {
#Binding var currentPositionTopLeft: CGPoint
#Binding var currentPositionTopRight: CGPoint
#Binding var currentPositionBottomLeft: CGPoint
#Binding var currentPositionBottomRight: CGPoint
var body: some View {
GeometryReader { geometry in
Path { path in
path.move(to: self.currentPositionTopLeft)
path.addLine(
to: .init(
x: self.currentPositionTopRight.x,
y: self.currentPositionTopRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomRight.x,
y: self.currentPositionBottomRight.y
)
)
path.addLine(
to: .init(
x: self.currentPositionBottomLeft.x,
y: self.currentPositionBottomLeft.y
)
)
path.addLine(
to: .init(
x: self.currentPositionTopLeft.x,
y: self.currentPositionTopLeft.y
)
)
}
.stroke(Color.blue, lineWidth: CGFloat(1))
}
}
}
The corner:
import SwiftUI
struct CropImageViewRectangleCorner: View {
#Binding var currentPosition: CGPoint
#Binding var newPosition: CGPoint
var displacementX: CGFloat
var displacementY: CGFloat
var body: some View {
Circle().foregroundColor(Color.blue).frame(width: 24, height: 24)
.offset(x: self.currentPosition.x, y: self.currentPosition.y)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGPoint(x: value.translation.width + self.newPosition.x, y: value.translation.height + self.newPosition.y)
}
.onEnded { value in
self.currentPosition = CGPoint(x: value.translation.width + self.newPosition.x, y: value.translation.height + self.newPosition.y)
self.newPosition = self.currentPosition
}
)
.opacity(0.5)
.position(CGPoint(x: 0, y: 0))
.onAppear() {
if self.displacementX > 0 || self.displacementY > 0 {
self.currentPosition = CGPoint(x: self.displacementX, y: self.displacementY)
self.newPosition = self.currentPosition
}
}
}
}