Is there a way to animate the stroke drawing of a path whenever its destination point changes?
Here is a snippet of my code:
struct JourneyMapLineView: View {
#Binding var rects: [CGRect]
var body: some View {
rects.count != 0 ?
JourneyMapLineShape(startRect: rects[0], endRect: rects[rects.count-1])
.stroke(Color.red), lineWidth: 8)
.animation(.easeInOut(duration: 0.3)) //<-- Is not working
: nil
}
}
struct JourneyMapLineShape: Shape {
var startRect: CGRect
var endRect: CGRect
func path(in _: CGRect) -> Path {
var path = Path()
path.move(to: startRect.origin)
path.addLine(to: endRect.origin)
return path
}
}
Currently, as you can see, there is no animation by changing the value of endRect:
I have already looked at some similar questions, it seems that this is a new case,.
Thank you so much!
Update: Xcode 13.4 / iOS 15.5
The .animation has deprecated, so corrected part is
Route(points: self.vm.points, head: self.vm.lastPoint)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [], dashPhase: 0))
.foregroundColor(.red)
.animation(.linear(duration: 0.5), value: self.vm.lastPoint) // << here !!
Test module is here
Original
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
// Route shape animating head point
struct Route: Shape {
var points: [CGPoint]
var head: CGPoint
// make route animatable head position only
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(head.x, head.y) }
set {
head.x = newValue.first
head.y = newValue.second
}
}
func path(in rect: CGRect) -> Path {
Path { path in
guard points.count > 1 else { return }
path.move(to: points.first!)
_ = points.dropFirst().dropLast().map { path.addLine(to: $0) }
path.addLine(to: head)
}
}
}
// Route view model holding all points and notifying when last one changed
class RouteVM: ObservableObject {
var points = [CGPoint.zero] {
didSet {
self.lastPoint = points.last ?? CGPoint.zero
}
}
#Published var lastPoint = CGPoint.zero
}
struct DemoRouteView: View {
#ObservedObject var vm = RouteVM()
var body: some View {
GeometryReader { gp in
ZStack { // area
Rectangle().fill(Color.white)
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
self.vm.points.append(value.location) // read coordinates in area
})
Circle().fill(Color.blue).frame(width: 20)
.position(self.vm.points.first!) // show initial point
// draw route when head changed, animating
Route(points: self.vm.points, head: self.vm.lastPoint)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [], dashPhase: 0))
.foregroundColor(.red)
.animation(.linear(duration: 0.5))
}
.onAppear {
let area = gp.frame(in: .global)
// just initail point at the bottom of screen
self.vm.points = [CGPoint(x: area.midX, y: area.maxY)]
}
}.edgesIgnoringSafeArea(.all)
}
}
Related
When the user touches the screen, it will record the gesture as CGPoint and then display them with Path. But now the line is not smooth at the turning point. What should I do?
This is my code:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
Path { path in
path.addLines(singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
}
.gesture(
DragGesture()
.onChanged { state in
if removeLine {
singleLineData.removeAll()
removeLine = false
}
singleLineData.append(state.location)
}
.onEnded { _ in
removeLine = true
}
)
.frame(width: 370, height: 500)
}
}
struct LineView_Previews: PreviewProvider {
static var previews: some View {
LineView()
}
}
I'm back and I know a solution to make line smoother.
First I created a class to return Path 「PS: I learned this from Mrs. Karin Prater's youtube course("How to make a drawing app with SwiftUI 3"), Shoutout to her」
class DrawingEngine {
func createPath(for points: [CGPoint]) -> Path {
var path = Path()
if let firstPoint = points.first {
path.move(to: firstPoint)
}
for index in 1..<points.count {
let mid = calculateMidPoint(points[index-1], points[index])
path.addQuadCurve(to: mid, control: points[index - 1])
}
if let last = points.last {
path.addLine(to: last)
}
return path
}
func calculateMidPoint(_ p1: CGPoint, _ p2: CGPoint) -> CGPoint {
let newMidPoint = CGPoint(x: (p1.x+p2.x)/2, y: (p1.y+p2.y)/2)
return newMidPoint
}
}
Then I only record new state points whose linear distance from the previous point exceeds a certain value(10 or 20...)
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > 20
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
This is full code of LineView:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
let engine = DrawingEngine()
let minimumDistance: CGFloat = 20
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
if singleLineData.count != 0 {
Path { path in
path = engine.createPath(for: singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
}
}
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > minimumDistance
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
}
}
My English is not good, hope this solution could help you ;)
I'm trying to create circles that can be dragged and dropped.
I am able to get it to work with just the first circle, however, anything after the first doesn't work.
Expected behaviour: Circle follows my cursor while dragging and lands on final position when drag ends
Actual behaviour: Circle follows my cursor's horizontal position, but not vertical position (vertical position is always significantly below my cursor)
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .center) {
ForEach(0..<5) { _ in
DraggableCircles()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DraggableCircles: View {
#State var dragAmount: CGPoint = CGPoint.zero
var body: some View {
Circle().fill(Color.red)
.frame(width: 50, height: 50)
.gesture(
DragGesture(coordinateSpace: .global).onChanged {action in
let location = action.location
let newWidth = location.x
let newHeight = location.y
let size = CGPoint(x: newWidth, y: newHeight)
self.dragAmount = size
}.onEnded{action in
let location = action.location
let newWidth = location.x
let newHeight = location.y
let size = CGPoint(x: newWidth, y: newHeight)
self.dragAmount = size
}
)
.position(x: dragAmount.x, y: dragAmount.y)
}
}
You have to add drag value to the last location. The correct calculation is here.
struct DraggableCircles: View {
#State private var location: CGPoint = CGPoint(x: 50, y: 50)
#GestureState private var startLocation: CGPoint? = nil
var body: some View {
// Here is create DragGesture and handel jump when you again start the dragging/
let dragGesture = DragGesture()
.onChanged { value in
var newLocation = startLocation ?? location
newLocation.x += value.translation.width
newLocation.y += value.translation.height
self.location = newLocation
}.updating($startLocation) { (value, startLocation, transaction) in
startLocation = startLocation ?? location
}
return Circle().fill(Color.red)
.frame(width: 50, height: 50)
.position(location)
.gesture(dragGesture)
}
}
Is there a way, using SwiftUI, to create a Slider with 2 handles?
I'm working on a project that that requires settings a low and high point for a random value to be created between, and sliders seem to fit that need perfectly. I currently have it implemented as 2 separate sliders, but it would much rather have it 1 slider with 2 handles.
I've been searching and I cannot find any examples of it in SwiftUI, but I did find a webpage example of what I'm looking to do here: https://jqueryui.com/slider/#range
Is this possible in iOS via SwiftUI?
I've created a custom slider for you. I hope that's enough for your needs. Let me know if there is anything else I can do.
Slider:
import SwiftUI
import Combine
//SliderValue to restrict double range: 0.0 to 1.0
#propertyWrapper
struct SliderValue {
var value: Double
init(wrappedValue: Double) {
self.value = wrappedValue
}
var wrappedValue: Double {
get { value }
set { value = min(max(0.0, newValue), 1.0) }
}
}
class SliderHandle: ObservableObject {
//Slider Size
let sliderWidth: CGFloat
let sliderHeight: CGFloat
//Slider Range
let sliderValueStart: Double
let sliderValueRange: Double
//Slider Handle
var diameter: CGFloat = 40
var startLocation: CGPoint
//Current Value
#Published var currentPercentage: SliderValue
//Slider Button Location
#Published var onDrag: Bool
#Published var currentLocation: CGPoint
init(sliderWidth: CGFloat, sliderHeight: CGFloat, sliderValueStart: Double, sliderValueEnd: Double, startPercentage: SliderValue) {
self.sliderWidth = sliderWidth
self.sliderHeight = sliderHeight
self.sliderValueStart = sliderValueStart
self.sliderValueRange = sliderValueEnd - sliderValueStart
let startLocation = CGPoint(x: (CGFloat(startPercentage.wrappedValue)/1.0)*sliderWidth, y: sliderHeight/2)
self.startLocation = startLocation
self.currentLocation = startLocation
self.currentPercentage = startPercentage
self.onDrag = false
}
lazy var sliderDragGesture: _EndedGesture<_ChangedGesture<DragGesture>> = DragGesture()
.onChanged { value in
self.onDrag = true
let dragLocation = value.location
//Restrict possible drag area
self.restrictSliderBtnLocation(dragLocation)
//Get current value
self.currentPercentage.wrappedValue = Double(self.currentLocation.x / self.sliderWidth)
}.onEnded { _ in
self.onDrag = false
}
private func restrictSliderBtnLocation(_ dragLocation: CGPoint) {
//On Slider Width
if dragLocation.x > CGPoint.zero.x && dragLocation.x < sliderWidth {
calcSliderBtnLocation(dragLocation)
}
}
private func calcSliderBtnLocation(_ dragLocation: CGPoint) {
if dragLocation.y != sliderHeight/2 {
currentLocation = CGPoint(x: dragLocation.x, y: sliderHeight/2)
} else {
currentLocation = dragLocation
}
}
//Current Value
var currentValue: Double {
return sliderValueStart + currentPercentage.wrappedValue * sliderValueRange
}
}
class CustomSlider: ObservableObject {
//Slider Size
let width: CGFloat = 300
let lineWidth: CGFloat = 8
//Slider value range from valueStart to valueEnd
let valueStart: Double
let valueEnd: Double
//Slider Handle
#Published var highHandle: SliderHandle
#Published var lowHandle: SliderHandle
//Handle start percentage (also for starting point)
#SliderValue var highHandleStartPercentage = 1.0
#SliderValue var lowHandleStartPercentage = 0.0
var anyCancellableHigh: AnyCancellable?
var anyCancellableLow: AnyCancellable?
init(start: Double, end: Double) {
valueStart = start
valueEnd = end
highHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _highHandleStartPercentage
)
lowHandle = SliderHandle(sliderWidth: width,
sliderHeight: lineWidth,
sliderValueStart: valueStart,
sliderValueEnd: valueEnd,
startPercentage: _lowHandleStartPercentage
)
anyCancellableHigh = highHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
anyCancellableLow = lowHandle.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}
//Percentages between high and low handle
var percentagesBetween: String {
return String(format: "%.2f", highHandle.currentPercentage.wrappedValue - lowHandle.currentPercentage.wrappedValue)
}
//Value between high and low handle
var valueBetween: String {
return String(format: "%.2f", highHandle.currentValue - lowHandle.currentValue)
}
}
Slider implementation:
import SwiftUI
struct ContentView: View {
#ObservedObject var slider = CustomSlider(start: 10, end: 100)
var body: some View {
VStack {
Text("Value: " + slider.valueBetween)
Text("Percentages: " + slider.percentagesBetween)
Text("High Value: \(slider.highHandle.currentValue)")
Text("Low Value: \(slider.lowHandle.currentValue)")
//Slider
SliderView(slider: slider)
}
}
}
struct SliderView: View {
#ObservedObject var slider: CustomSlider
var body: some View {
RoundedRectangle(cornerRadius: slider.lineWidth)
.fill(Color.gray.opacity(0.2))
.frame(width: slider.width, height: slider.lineWidth)
.overlay(
ZStack {
//Path between both handles
SliderPathBetweenView(slider: slider)
//Low Handle
SliderHandleView(handle: slider.lowHandle)
.highPriorityGesture(slider.lowHandle.sliderDragGesture)
//High Handle
SliderHandleView(handle: slider.highHandle)
.highPriorityGesture(slider.highHandle.sliderDragGesture)
}
)
}
}
struct SliderHandleView: View {
#ObservedObject var handle: SliderHandle
var body: some View {
Circle()
.frame(width: handle.diameter, height: handle.diameter)
.foregroundColor(.white)
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 0)
.scaleEffect(handle.onDrag ? 1.3 : 1)
.contentShape(Rectangle())
.position(x: handle.currentLocation.x, y: handle.currentLocation.y)
}
}
struct SliderPathBetweenView: View {
#ObservedObject var slider: CustomSlider
var body: some View {
Path { path in
path.move(to: slider.lowHandle.currentLocation)
path.addLine(to: slider.highHandle.currentLocation)
}
.stroke(Color.green, lineWidth: slider.lineWidth)
}
}
This range slider will work with any width provided.
It uses GeometryReader to get the slider width
The slider is bounded by the value range and the thumbs handle cannot cross each other
RangeSliderView Usage
#State var sliderPosition: ClosedRange<Float> = 3...8
RangedSliderView(value: $sliderPosition, bounds: 1...10)
RangeSlideView Implementation
struct RangedSliderView: View {
let currentValue: Binding<ClosedRange<Float>>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<ClosedRange<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
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.nojaPrimary30)
.frame(height: 4)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
// Calculate Left Thumb initial position
let leftThumbLocation: CGFloat = currentValue.wrappedValue.lowerBound == Float(sliderBounds.lowerBound)
? 0
: CGFloat(currentValue.wrappedValue.lowerBound - Float(sliderBounds.lowerBound)) * stepWidthInPixel
// Calculate right thumb initial position
let rightThumbLocation = CGFloat(currentValue.wrappedValue.upperBound) * stepWidthInPixel
// Path between both handles
lineBetweenThumbs(from: .init(x: leftThumbLocation, y: sliderViewYCenter), to: .init(x: rightThumbLocation, y: sliderViewYCenter))
// Left Thumb Handle
let leftThumbPoint = CGPoint(x: leftThumbLocation, y: sliderViewYCenter)
thumbView(position: leftThumbPoint, value: Float(currentValue.wrappedValue.lowerBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(max(0, dragLocation.x), sliderSize.width)
let newValue = Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel)
// Stop the range thumbs from colliding each other
if newValue < currentValue.wrappedValue.upperBound {
currentValue.wrappedValue = newValue...currentValue.wrappedValue.upperBound
}
})
// Right Thumb Handle
thumbView(position: CGPoint(x: rightThumbLocation, y: sliderViewYCenter), value: currentValue.wrappedValue.upperBound)
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(max(CGFloat(leftThumbLocation), dragLocation.x), sliderSize.width)
var newValue = Float(xThumbOffset / stepWidthInPixel) // convert back the value bound
newValue = min(newValue, Float(sliderBounds.upperBound))
// Stop the range thumbs from colliding each other
if newValue > currentValue.wrappedValue.lowerBound {
currentValue.wrappedValue = currentValue.wrappedValue.lowerBound...newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.nojaPrimary, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(value))
.font(.secondaryFont(weight: .semibold, size: 10))
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.nojaPrimary)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}
There are some improvements that can be added
e.g adding a step property or
also implementing the slider with a generic init to support Int, Float and other number types
use ViewBuilder to build a custom label for the slider
I modified the code from #culjo. This code supports preview and the logical parts are moved to viewModel.
import SwiftUI
struct RangeSlider: View {
#ObservedObject var viewModel: ViewModel
#State private var isActive: Bool = false
let sliderPositionChanged: (ClosedRange<Float>) -> Void
var body: some View {
GeometryReader { geometry in
sliderView(sliderSize: geometry.size,
sliderViewYCenter: geometry.size.height / 2)
}
.frame(height: ** insert your height of range slider **)
}
#ViewBuilder private func sliderView(sliderSize: CGSize, sliderViewYCenter: CGFloat) -> some View {
lineBetweenThumbs(from: viewModel.leftThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
to: viewModel.rightThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter))
thumbView(position: viewModel.leftThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
value: Float(viewModel.sliderPosition.lowerBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let newValue = viewModel.newThumbLocation(dragLocation: dragValue.location,
width: sliderSize.width)
if newValue < viewModel.sliderPosition.upperBound {
viewModel.sliderPosition = newValue...viewModel.sliderPosition.upperBound
sliderPositionChanged(viewModel.sliderPosition)
isActive = true
}
})
thumbView(position: viewModel.rightThumbLocation(width: sliderSize.width,
sliderViewYCenter: sliderViewYCenter),
value: Float(viewModel.sliderPosition.upperBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let newValue = viewModel.newThumbLocation(dragLocation: dragValue.location,
width: sliderSize.width)
if newValue > viewModel.sliderPosition.lowerBound {
viewModel.sliderPosition = viewModel.sliderPosition.lowerBound...newValue
sliderPositionChanged(viewModel.sliderPosition)
isActive = true
}
})
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(** insert your color **)
.frame(height: 4)
Path { path in
path.move(to: from)
path.addLine(to: to)
}
.stroke(isActive ? ** insert your color ** : ** insert your color **,
lineWidth: 4)
}
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
Circle()
.frame(size: .rangeSliderThumb)
.foregroundColor(isActive ? ** insert your color ** : ** insert your color **)
.contentShape(Rectangle())
.position(x: position.x, y: position.y)
.animation(.spring(), value: isActive)
}
}
extension RangeSlider {
final class ViewModel: ObservableObject {
#Published var sliderPosition: ClosedRange<Float>
let sliderBounds: ClosedRange<Int>
let sliderBoundDifference: Int
init(sliderPosition: ClosedRange<Float>,
sliderBounds: ClosedRange<Int>) {
self.sliderPosition = sliderPosition
self.sliderBounds = sliderBounds
self.sliderBoundDifference = sliderBounds.count - 1
}
func leftThumbLocation(width: CGFloat, sliderViewYCenter: CGFloat = 0) -> CGPoint {
let sliderLeftPosition = CGFloat(sliderPosition.lowerBound - Float(sliderBounds.lowerBound))
return .init(x: sliderLeftPosition * stepWidthInPixel(width: width),
y: sliderViewYCenter)
}
func rightThumbLocation(width: CGFloat, sliderViewYCenter: CGFloat = 0) -> CGPoint {
let sliderRightPosition = CGFloat(sliderPosition.upperBound - Float(sliderBounds.lowerBound))
return .init(x: sliderRightPosition * stepWidthInPixel(width: width),
y: sliderViewYCenter)
}
func newThumbLocation(dragLocation: CGPoint, width: CGFloat) -> Float {
let xThumbOffset = min(max(0, dragLocation.x), width)
return Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel(width: width))
}
private func stepWidthInPixel(width: CGFloat) -> CGFloat {
width / CGFloat(sliderBoundDifference)
}
}
}
struct RangeSlider_Previews: PreviewProvider {
static var previews: some View {
RangeSlider(viewModel: .init(sliderPosition: 2...8,
sliderBounds: 1...10),
sliderPositionChanged: { _ in })
}
}
I'm trying to add a target-like overlay to the photo preview in an app that uses
portrait on some devices and landscape on others. I can manually set coordinates but
have not been able to get the correct coordinates of the frame of the camera preview.
This is a SwiftUI application, so I am doing the camera work in a
UIViewControllerRepresentable. Here's what I have and obviously, the target circle is
always wrong when the device and/or orientation change. I don't seem to be able to capture
the frame of the modal view where the camera preview exists. I would settle to be
able to specify the frame and location of the camera preview on the modal view.
struct CaptureImageView: View {
#Binding var isShown: Bool
#Binding var image: Image?
#Binding var newUIImage: UIImage?
#Binding var showSaveButton: Bool
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, image: $newUIImage, showSaveButton: $showSaveButton)
}
}
extension CaptureImageView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
let vc = UIImagePickerController()
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
vc.sourceType = .camera
vc.allowsEditing = true
vc.delegate = context.coordinator
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
vc.cameraOverlayView = CircleView(frame: CGRect(x: (screenWidth / 2) - 50, y: (screenWidth / 2) + 25, width: 100, height: 100))
return vc
}
return UIImagePickerController()
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<CaptureImageView>) {
}
}
Here is the idea - but the target always needs to be centered:
And the target file:
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(3.0)
UIColor.red.set()
let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let radius = (frame.size.width - 10) / 2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.strokePath()
}
}
}
Any guidance would be appreciated. Xcode Version 11.3.1 (11C504)
While I would still like to find an answer for the way to get the frame of the camera - my goal could be mostly achieved by creating a circle view in a ZStack placing the camera view behind my circle view in SwiftUI like this:
if showCaptureImageView {
ZStack {
CaptureImageView(isShown: $showCaptureImageView, image: $myImage, newUIImage: $newUIImage, showSaveButton: $showSaveButton)
Circle()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10 ))
.frame(width: 100, height: 100)
.offset(CGSize(width: 0, height: -50.0))
}
}//if show
As noted by GrandSteph - a better approach would be to wrap the camera view in a Geometry Reader. Here's one approach:
if showCaptureImageView {
ZStack {
VStack {
Rectangle()//just to show that the circle below can be centered
.frame(width: 300, height: 150)
.background(Color.blue)
GeometryReader { geo in
CaptureImageView(isShown: self.$showCaptureImageView, image: self.$image)
.onAppear {
self.cameraWindowWidth = geo.size.width
self.cameraWindowHeight = geo.size.height
}
}//geo
}//v
Circle().frame(width: 50, height: 50)
.position(x: self.cameraWindowWidth / 2, y: (self.cameraWindowHeight / 2) + 150)
.foregroundColor(.red)
}//z
}//if show
How would it be possible to animate Text or TextField views from Swift UI?
By animation I mean, that when the text changes it will "count up".
For example given some label, how can an animation be created that when I set the labels text to "100" it goes up from 0 to 100. I know this was possible in UIKit using layers and CAAnimations, but using the .animation() function in Swift UI and changing the text of a Text or TextField does not seem to do anything in terms of animation.
I've taken a look at Animatable protocol and its related animatableData property but it doesn't seem like Text nor TextField conform to this. I'm trying to create a label that counts up, so given some value, say a Double the changes to that value would be tracked, either using #State or #Binding and then the Text or TextField would animate its content (the actual string text) from what the value was at to what it was set to.
Edit:
To make it clearer, I'd like to recreate a label that looks like this when animated:
There is a pure way of animating text in SwiftUI. Here's an implementation of your progress indicator using the AnimatableModifier protocol in SwiftUI:
I've written an extensive article documenting the use of AnimatableModifier (and its bugs). It includes the progress indicator too. You can read it here: https://swiftui-lab.com/swiftui-animations-part3/
struct ContentView: View {
#State private var percent: CGFloat = 0
var body: some View {
VStack {
Spacer()
Color.clear.overlay(Indicator(pct: self.percent))
Spacer()
HStack(spacing: 10) {
MyButton(label: "0%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0 } }
MyButton(label: "27%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0.27 } }
MyButton(label: "100%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 1.0 } }
}
}.navigationBarTitle("Example 10")
}
}
struct Indicator: View {
var pct: CGFloat
var body: some View {
return Circle()
.fill(LinearGradient(gradient: Gradient(colors: [.blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(width: 150, height: 150)
.modifier(PercentageIndicator(pct: self.pct))
}
}
struct PercentageIndicator: AnimatableModifier {
var pct: CGFloat = 0
var animatableData: CGFloat {
get { pct }
set { pct = newValue }
}
func body(content: Content) -> some View {
content
.overlay(ArcShape(pct: pct).foregroundColor(.red))
.overlay(LabelView(pct: pct))
}
struct ArcShape: Shape {
let pct: CGFloat
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
radius: rect.height / 2.0 + 5.0,
startAngle: .degrees(0),
endAngle: .degrees(360.0 * Double(pct)), clockwise: false)
return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
}
}
struct LabelView: View {
let pct: CGFloat
var body: some View {
Text("\(Int(pct * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
}
You can use a CADisplayLink in a BindableObject to create a timer that updates your text during the animation. Gist
class CADisplayLinkBinding: NSObject, BindableObject {
let didChange = PassthroughSubject<CADisplayLinkBinding, Never>()
private(set) var progress: Double = 0.0
private(set) var startTime: CFTimeInterval = 0.0
private(set) var duration: CFTimeInterval = 0.0
private(set) lazy var displayLink: CADisplayLink = {
let link = CADisplayLink(target: self, selector: #selector(tick))
link.add(to: .main, forMode: .common)
link.isPaused = true
return link
}()
func run(for duration: CFTimeInterval) {
let now = CACurrentMediaTime()
self.progress = 0.0
self.startTime = now
self.duration = duration
self.displayLink.isPaused = false
}
#objc private func tick() {
let elapsed = CACurrentMediaTime() - self.startTime
self.progress = min(1.0, elapsed / self.duration)
self.displayLink.isPaused = self.progress >= 1.0
self.didChange.send(self)
}
deinit {
self.displayLink.invalidate()
}
}
And then to use it:
#ObjectBinding var displayLink = CADisplayLinkBinding()
var body: some View {
Text("\(Int(self.displayLink.progress*100))")
.onAppear {
self.displayLink.run(for: 10.0)
}
}