Crop image according to rectangle in SwiftUI - swift

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())
}
}

Related

How to implement a slider whose minimum track color is begin from center(value=0) not left in SwiftUI

I want to custom Slider in SwiftUI. Just something like this.
I tried Slider with GeometryReader but it isn't working.
//MARK: Left - Right Balance
GeometryReader { geo in
VStack {
Text("\(String(format: "%.2f", balanceVolume))")
HStack {
Text("L")
Slider(value: $balanceVolume, in: minValue...maxValue, step: 0.1) {editing in
print("editing", editing)
isEditing = editing
if !editing {
player.pan = Float(balanceVolume)
}
}
.tint(.none)
.accentColor(.gray)
Text("R")
}
}
.padding(20)
}
Thank you you all.
I have created a simple custom slider, I hope it helps
Output:
Use:
struct slider: View {
#State var sliderPosition: Float = 50
var body: some View {
SliderView(value: $sliderPosition, bounds: 1...100).padding(.all)
}
}
Code:
struct SliderView: View {
let currentValue: Binding<Float>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<Float>, bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}
var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)
}
}
#ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
let sliderViewXCenter = sliderSize.width / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.gray)
.frame(height: 3)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
let thumbLocation = CGFloat(currentValue.wrappedValue) * stepWidthInPixel
// Path between starting point to thumb
lineBetweenThumbs(from: .init(x: sliderViewXCenter, y: sliderViewYCenter), to: .init(x: thumbLocation, y: sliderViewYCenter))
// Thumb Handle
let thumbPoint = CGPoint(x: thumbLocation, y: sliderViewYCenter)
thumbView(position: thumbPoint, value: Float(currentValue.wrappedValue))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(dragLocation.x, sliderSize.width)
let newValue = Float(sliderBounds.lowerBound / sliderBounds.upperBound) + Float(xThumbOffset / stepWidthInPixel)
if newValue > Float(sliderBounds.lowerBound) && newValue < Float(sliderBounds.upperBound + 1) {
currentValue.wrappedValue = newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.blue, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(round(value)))
.font(.headline)
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}

SwiftUI - Drag the object along the path

I would like to make my rectangle only move along a path. I have no idea how I could do that
struct _FollowingPathView: View {
#State private var position = CGSize()
#State private var finalPosition = CGSize()
#State private var currentPosition: CGFloat = 0.5
var body: some View {
VStack {
Rectangle()
.frame(width: 300, height: 300)
.foregroundColor(.clear)
.border(.black, width: 2)
.overlay {
CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300))
.stroke(Color.red, style: StrokeStyle(lineWidth: 2))
Rectangle()
.frame(width: 50, height: 50)
.foregroundColor(.clear)
.border(.black, width: 2)
.offset(
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged {
position.width = min(max(-125, $0.translation.width + finalPosition.width), 125)
position.height = min(max(-125, $0.translation.height + finalPosition.height), 125)
}
.onEnded {
position.width = min(max(-125, $0.translation.width + finalPosition.width), 125)
position.height = min(max(-125, $0.translation.height + finalPosition.height), 125)
finalPosition = position
}
)
}
}
}
}
struct _FollowingPathView_Previews: PreviewProvider {
static var previews: some View {
_FollowingPathView()
}
}
enter image description here
I tried with trimmedPath, and modifying the current position by using dragGesture. But it wasn't working properly
.position(
x: CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300)).trimmedPath(from: 0, to: currentPosition).currentPoint?.x ?? CGPoint(),
y: CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300)).trimmedPath(from: 0, to: currentPosition).currentPoint?.y ?? CGPoint(),
)

SwiftUI Custom Shape doesn't work correctly

I have a custom separator as a Shape. I also have a view on which I put my separator (I add it 3 times and separated by spacers).
Below I added a capsule with spacing 4. According to the idea my custom separator and capsule should have the same spacings.
Why doesn’t it work like that?
// Separator struct
struct ProgressBarSeparator: View{
struct RightShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.maxX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
struct LeftShape: Shape {
func path(in rect: CGRect) -> Path {
let offsetX = rect.minX - rect.width / 4
let crect = CGRect(origin: .zero, size: CGSize(width: 4, height: 4)).offsetBy(dx: offsetX, dy: .zero)
var path = Rectangle().path(in: rect)
path.addPath(Circle().path(in: crect))
return path
}
}
var body: some View {
Rectangle()
.frame(width: 8, height: 4)
.foregroundColor(.gray)
.mask(RightShape().fill(style: .init(eoFill: true)))
.mask(LeftShape().fill(style: .init(eoFill: true)))
}
}
// Progress bar view
private var progressBar: some View {
GeometryReader { proxy in
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(ColorName.black400.color)
.frame(height: 4)
Capsule()
.fill(gradient)
.frame(width: (proxy.size.width - 12) * 0 / 4, height: 4)
.animation(.easeInOut)
HStack.zeroSpacing {
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
ProgressBarSeparator()
Spacer()
}
}
}
}
// Example capsule
private var procentCashback: some View {
HStack(spacing: 4) {
ForEach(0 ... 3) { index in
ZStack(alignment: .leading) {
Capsule()
.fill(.orange)
.frame(height: 4)
}
}
}
}
My broken view

SwiftUI Circular slider issue not displaying the correct stroke when using two control point

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))
}

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)
}
}