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)
}
}
Related
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:
I would like to make my view scalable. However, when I change the size, the size of the ticks remain. The ticks should also scale down to fit the circle proportionally.
Is there a best practice approach here?
here is my code which I have used:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Circle()
.fill(Color.gray)
ForEach(0..<60*4) { tick in
Ticks.tick(at: tick)
}
}
}.frame(height: 100)
}
}
struct Ticks{
static func tick(at tick: Int) -> some View {
VStack {
Rectangle()
.fill(Color.primary)
.opacity(tick % 20 == 0 ? 1 : 0.4)
.frame(width: 2, height: tick % 4 == 0 ? 15 : 7)
Spacer()
}.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Thanks!
You can pass along a scale factor based on the GeometryReader that you already have in place.
struct ContentView: View {
var body: some View {
TestView()
}
}
struct Hand: Shape {
let inset: CGFloat
let angle: Angle
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var p = Path()
p.move(to: CGPoint(x: rect.midX, y: rect.midY))
p.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return p
}
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPos = rect.midX + (radius * cos(angle))
let yPos = rect.midY + (radius * sin(angle))
return CGPoint(x: xPos, y: yPos)
}
}
struct TickHands: View {
var scale: Double
#State private var dateTime = Date()
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Hand(inset: 105 * scale, angle: dateTime.hourAngle)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
Hand(inset: 70 * scale, angle: dateTime.minuteAngle)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
Hand(inset: 40 * scale, angle: dateTime.secondAngle)
.stroke(Color.orange, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Circle().fill(Color.orange).frame(width: 10)
}
.onReceive(timer) { (input) in
self.dateTime = input
}
}
}
extension Date {
var hourAngle: Angle {
return Angle (degrees: (360 / 12) * (self.hour + self.minutes / 60))
}
var minuteAngle: Angle {
return Angle(degrees: (self.minutes * 360 / 60))
}
var secondAngle: Angle {
return Angle (degrees: (self.seconds * 360 / 60))
}
}
extension Date {
var hour: Double {
return Double(Calendar.current.component(.hour, from: self))
}
var minutes: Double {
return Double(Calendar.current.component(.minute, from: self))
}
var seconds: Double {
return Double(Calendar.current.component(.second, from: self))
}
}
struct TestView: View {
var clockSize: CGFloat = 500
var body: some View {
GeometryReader { geometry in
ZStack {
let scale = geometry.size.height / 200
TickHands(scale: scale)
ForEach(0..<60*4) { tick in
Ticks.tick(at: tick, scale: scale)
}
}
}.frame(width: clockSize, height: clockSize)
}
}
struct Ticks{
static func tick(at tick: Int, scale: CGFloat) -> some View {
VStack {
Rectangle()
.fill(Color.primary)
.opacity(tick % 20 == 0 ? 1 : 0.4)
.frame(width: 2 * scale, height: (tick % 5 == 0 ? 15 : 7) * scale)
Spacer()
}
.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}
Note that you may want to change how the scale effect changes the width vs the height -- for example, maybe you want the width to always be 2. Or, perhaps you want to use something like min(1, 2 * scale) to prevent the ticks from going above or below a certain size. But, the principal will be the same (ie using the scale factor). You can also adjust the 200 that I have to something that fits your ideal scaling algorithm.
I have a circular view with two control knobs and a stroke between the two of them as you can see from the image.
The values are correct but the problem is how could I do display the blue stroke to be the opposite? The user might want the range to be NW, N to NE, and not NE including S to NE but the user needs the ability to pick from both. The code below can be just be pasted in to show the same as the image.
import SwiftUI
struct CircularSliderView: View {
var body: some View {
VStack(){
DirectionView()
Spacer()
}
}
}
struct SwellCircularSliderView_Previews: PreviewProvider {
static var previews: some View {
CircularSliderView()
}
}
struct DirectionView: View {
#State var directionValue: CGFloat = 0.0
#State var secondaryDirectionValue: CGFloat = 0.0
var body: some View {
VStack {
Text("\(directionValue, specifier: "%.0f")° - \(secondaryDirectionValue, specifier: "%.0f")° \(Double().degreesToCompassDirection(degree: Double(directionValue))) - \(Double().degreesToCompassDirection(degree: Double(secondaryDirectionValue)))")
.font(.body)
DirectionControlView(directionValue: $directionValue, secondaryDirectionValue: $secondaryDirectionValue)
.padding(.top, 60)
Spacer()
}//: VSTACK
}
}
struct DirectionControlView: View {
#Binding var directionValue: CGFloat
#State var dirAngleValue: CGFloat = 0.0
#Binding var secondaryDirectionValue: CGFloat
#State var secondaryDirAngleValue: CGFloat = 0.0
let minimumValue: CGFloat = 0
let maximumValue: CGFloat = 360.0
let totalValue: CGFloat = 360.0
let knobRadius: CGFloat = 10.0
let radius: CGFloat = 125.0
private let tickHeight: CGFloat = 8
private let longTickHeight: CGFloat = 14
private let tickWidth: CGFloat = 2
func minimumTrimValue() -> CGFloat{
if directionValue > secondaryDirectionValue {
return secondaryDirectionValue/totalValue
} else {
return directionValue/totalValue
}
}
func maximumTrimValue() -> CGFloat{
if directionValue > secondaryDirectionValue {
return directionValue/totalValue
} else {
return secondaryDirectionValue/totalValue
}
}
var body: some View {
ZStack {
Circle()
.trim(from: minimumTrimValue(), to: maximumTrimValue())
.stroke(
AngularGradient(gradient: Gradient(
colors: [Color.blue.opacity(0.2), Color.blue.opacity(1), Color.blue.opacity(0.2)]),
center: .center,
startAngle: .degrees(Double(secondaryDirectionValue)),
endAngle: .degrees(Double(directionValue))),
style: StrokeStyle(lineWidth: 8, lineCap: .round)
)
.frame(width: radius * 2, height: radius * 2)
.rotationEffect(.degrees(-90))
KnobCircle(radius: knobRadius * 2, padding: 6)
.offset(y: -radius)
.rotationEffect(Angle.degrees(Double(dirAngleValue)))
.shadow(color: Color.black.opacity(0.2), radius: 3, x: -3)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged({ angleValue in
knobChange(location: angleValue.location)
}))
KnobCircle(radius: knobRadius * 2, padding: 6)
.offset(y: -radius)
.rotationEffect(Angle.degrees(Double(secondaryDirectionValue)))
.shadow(color: Color.black.opacity(0.2), radius: 3, x: -3)
.gesture(DragGesture(minimumDistance: 0.0)
.onChanged({ angleValue in
knobSecondaryChange(location: angleValue.location)
}))
CompassView(count: 240,
longDivider: 15,
longTickHeight: self.longTickHeight,
tickHeight: self.tickHeight,
tickWidth: self.tickWidth,
highlightedColorDivider: 30,
highlightedColor: .blue,
normalColor: .black.opacity(0.2))
.frame(width: 350, height: 350)
CompassNumber(numbers: self.getNumbers(count: 16))
.frame(width: 310, height: 310)
}//: ZSTACK
.onAppear(){
updateInitialValue()
}
}
private func getNumbers(count: Int) -> [Float] {
var numbers: [Float] = []
numbers.append(Float(count) * 30)
for index in 1..<count {
numbers.append(Float(index) * 30)
}
return numbers
}
private func updateInitialValue(){
directionValue = minimumValue
dirAngleValue = CGFloat(directionValue/totalValue) * 360
}
private func knobChange(location: CGPoint) {
let vector = CGVector(dx: location.x, dy: location.y)
let angle = atan2(vector.dy - knobRadius, vector.dx - knobRadius) + .pi/2.0
let fixedAngle = angle < 0.0 ? angle + 2.0 * .pi : angle
let value = fixedAngle / (2.0 * .pi) * totalValue
if value > minimumValue && value < maximumValue {
directionValue = value
dirAngleValue = fixedAngle * 180 / .pi
}
}
private func knobSecondaryChange(location: CGPoint) {
let vector = CGVector(dx: location.x, dy: location.y)
let angle = atan2(vector.dy - knobRadius, vector.dx - knobRadius) + .pi/2.0
let fixedAngle = angle < 0.0 ? angle + 2.0 * .pi : angle
let value = fixedAngle / (2.0 * .pi) * totalValue
if value > minimumValue && value < maximumValue {
secondaryDirectionValue = value
secondaryDirAngleValue = fixedAngle * 180 / .pi
}
}
}
struct KnobCircle: View {
let radius: CGFloat
let padding: CGFloat
var body: some View {
ZStack(){
Circle()
.fill(Color.init(white: 0.96))
.frame(width: radius, height: radius)
.shadow(color: Color.black.opacity(0.1), radius: 10, x: -10, y: 8)
Circle()
.fill(Color.white)
.frame(width: radius - padding, height: radius - padding)
}//: ZSTACK
}
}
struct CompassView: View {
let count: Int
let longDivider: Int
let longTickHeight: CGFloat
let tickHeight: CGFloat
let tickWidth: CGFloat
let highlightedColorDivider: Int
let highlightedColor: Color
let normalColor: Color
var body: some View {
ZStack(){
ForEach(0..<self.count) { index in
let height = (index % self.longDivider == 0) ? self.longTickHeight : self.tickHeight
let color = (index % self.highlightedColorDivider == 0) ? self.highlightedColor : self.normalColor
let degree: Double = Double.pi * 2 / Double(self.count)
TickShape(tickHeight: height)
.stroke(lineWidth: self.tickWidth)
.rotationEffect(.radians(degree * Double(index)))
.foregroundColor(color)
}
}//: ZSTACK
}//: VIEW
}
struct TickShape: Shape {
let tickHeight: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY + self.tickHeight))
return path
}
}
struct CompassNumber: View {
let numbers: [Float]
let direction: [String] = ["N","NE","E","SE","S","SW","W","NW"]
var body: some View {
ZStack(){
ForEach(0..<self.direction.count) { index in
let degree: Double = Double.pi * 2 / Double(self.direction.count)
let itemDegree = degree * Double(index)
VStack(){
Text(self.direction[index])
.font(.footnote)
.rotationEffect(.radians(-itemDegree))
.foregroundColor(.blue)
Spacer()
}//: VSTACK
.rotationEffect(.radians(itemDegree))
}
}//: ZSTACK
}
}
extension Double {
func degreesToCompassDirection(degree: Double) -> String {
switch degree {
case 0..<11.25:
return "N"
case 11.25..<33.75:
return "NNE"
case 33.75..<56.25:
return "NE"
case 56.25..<78.75:
return "ENE"
case 78.75..<101.25:
return "E"
case 101.25..<123.75:
return "ESE"
case 123.75..<146.25:
return "SE"
case 146.25..<168.75:
return "SSE"
case 168.75..<191.25:
return "S"
case 191.25..<213.75:
return "SSW"
case 213.75..<236.25:
return "SW"
case 236.25..<258.75:
return "WSW"
case 258.75..<281.25:
return "W"
case 281.25..<303.75:
return "WNW"
case 303.75..<326.25:
return "NW"
case 326.25..<348.75:
return "NNW"
case 348.75..<360:
return "N"
default:
return "ERROR"
}
}
}
Thanks for your help.
I did a ZStack and displayed Background Stroke. In Case I want to display over the 0 Position (North) I just swapped Background and foreground colors.
var body: some View {
ZStack {
ZStack{
Circle() //Background
.stroke((directionValue < secondaryDirectionValue) ? Color.white : Color.blue)
.frame(width: radius * 2, height: radius * 2)
.rotationEffect(.degrees(-90))
Circle() //Foreground
.trim(from: minimumTrimValue(), to: maximumTrimValue())
.stroke((directionValue < secondaryDirectionValue) ? Color.blue : Color.white)
.frame(width: radius * 2, height: radius * 2)
.rotationEffect(.degrees(-90))
}
I'm new to SwiftUI and what I trying to build is a dial view like this.
As you can see it has:
a view indicator which is for displaying the currently selected value from the user.
CircularDialView with all the numbers to chose from.
text view for displaying actual selected number
Maximum what I tried to accomplish is that for now CircularDialView is intractable with a two-finger drag gesture and for the rest, I have no even close idea how to do it.
Please help, because I tried everything I could and have to say it's super hard to do with SwiftUI
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.frame(width: 4, height: 25)
.padding()
CircularDialView()
Text("\(1)")
.font(.title)
.fontWeight(.bold)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ClockText: View {
var numbers: [String]
var angle: CGFloat
private struct IdentifiableNumbers: Identifiable {
var id: Int
var number: String
}
private var dataSource: [IdentifiableNumbers] {
numbers.enumerated().map { IdentifiableNumbers(id: $0, number: $1) }
}
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(dataSource) {
Text("\($0.number)")
.position(position(for: $0.id, in: geometry.frame(in: .local)))
}
}
}
}
private func position(for index: Int, in rect: CGRect) -> CGPoint {
let rect = rect.insetBy(dx: angle, dy: angle)
let angle = ((2 * .pi) / CGFloat(numbers.count) * CGFloat(index)) - .pi / 2
let radius = min(rect.width, rect.height) / 2
return CGPoint(x: rect.midX + radius * cos(angle),
y: rect.midY + radius * sin(angle))
}
}
struct CircularDialView: View {
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Circle()
.stroke(Color.black, lineWidth: 5)
.frame(width: 250, height: 250, alignment: .center)
ClockText(
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map {"\($0)"},
angle: 45
)
.font(.system(size: 20))
.frame(width: 290, height: 290, alignment: .center)
}
.rotationEffect(Angle(degrees: rotateState))
.gesture(
RotationGesture()
.simultaneously(with: DragGesture())
.onChanged { value in
self.rotateState = value.first?.degrees ?? rotateState
}
)
}
}
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())
}
}