i'm currently working on continuous animation on path. The Path should be updated with new values periodically and update should trigger start of new animation with those values. I'm having a problem while trying to refresh view with new values. The animation with path is shown at start with animation and after trying to trigger view update from method it's not showing any new view or updated path. Just white screen. My code for this view and updating is:
The chart view wrapped into UIViewRepresentable:
struct AnimatedChartView: UIViewRepresentable {
let geometryProxy: GeometryProxy
let leadValues: [Float]//Binding<[Float]>
//Chart control values
//step coefficinet
let xStepCoefficient: CGFloat = 0.00035
//Single step on view
var xStep: CGFloat{
let xStepValue = xStepCoefficient * geometryProxy.frame(in: .local).width
return xStepValue
}
var rootView: UIView
var chartLayer: CAShapeLayer
//Make chart view with animation
func makeUIView(context: Context) -> some UIView {
//Prepare view for animation and chart
chartLayer.delegate = context.coordinator
rootView.layer.addSublayer(chartLayer)
return rootView
}
init(geometry: GeometryProxy, values: [Float]) {
rootView = UIView()
geometryProxy = geometry
leadValues = values
chartLayer = CAShapeLayer()
chartLayer = prepareAnimationLayer()
}
//Update viewu
func updateUIView(_ uiView: UIViewType, context: Context) {
print("Update uiView")
DispatchQueue.main.async {
let newChartLayer = prepareAnimationLayer()
rootView.layer.addSublayer(newChartLayer)
rootView.setNeedsDisplay()
}
}
//Assign
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
//Delegates from view
class Coordinator: NSObject, CALayerDelegate {
var parent: AnimatedChartView
init(_ parentView: AnimatedChartView) {
self.parent = parentView
}
func draw(_ layer: CALayer, in ctx: CGContext) {
print("Drawing layer in context")
}
func layerWillDraw(_ layer: CALayer) {
print("layer will draw")
}
}
//Assign animation layer for chart
private func prepareAnimationLayer() -> CAShapeLayer {
//Create animation proeprties
let chartLayer = CAShapeLayer()
let strokeAnimation = makeAnimation()
let chartPath = makeChartFromValues(newValues: leadValues)
//Assign path and marks start and end of stroke to animate
chartLayer.path = chartPath.cgPath
chartLayer.strokeStart = 0.0
chartLayer.strokeEnd = 0.0
chartLayer.strokeColor = UIColor.green.cgColor
chartLayer.lineWidth = 2
chartLayer.fillColor = UIColor.clear.cgColor
chartLayer.add(strokeAnimation, forKey: "strokeAnimation")
return chartLayer
}
//Setup chart animation
private func makeAnimation() -> CABasicAnimation{
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
//Prepare animation
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
strokeEndAnimation.duration = 1
strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
strokeEndAnimation.repeatCount = 0
strokeEndAnimation.duration = 1
return strokeEndAnimation
}
//makes chart path
private func makeChartFromValues(newValues: [Float]) -> UIBezierPath{
let chartPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: .init(width: 200, height: 120)), byRoundingCorners: .allCorners, cornerRadii: .zero)
chartPath.move(to: .zero)
var currentX = xStep
newValues.forEach{ value in
//Value point to draw
let nextPoint = CGPoint(x: currentX, y: CGFloat(value / 1000))
//Adds line to chart
chartPath.addLine(to:nextPoint)
//Move to next value
currentX += xStep
}
return chartPath
}
}
and view function with viewmodel with timer which updates the values
struct UIKitAnimatedPathView: View {
#State var animationRect: CGRect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
#StateObject var viewModel = AnimationViewModel()
var body: some View {
GeometryReader{ geo in
VStack(content: {
ForEach(0 ..< 18, id: \.self){ i in
AnimatedChartView(geometry: geo, values: viewModel.values)
}
})
}
}
}
Viewmodel:
class AnimationViewModel: ObservableObject{
#Published var values: [Float] = [Float](repeating: 1, count: 2000)
#Published var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {time in
//print("time in: \(time.timeInterval)")
var vals:[Float] = []
for _ in 0 ..< 2000{
let rand = Float.random(in: -20000 ..< 0)
vals.append(rand)
}
self.values.removeAll()
self.values = vals
})
}
}
You can see commented out some part of code as i was trying to make it work somehow but with no luck so far... The functionality i want to achive is continuously updating chart. This code works for first run. Thank you in advance for any guidence on how to make it working :))
Related
I am creating a sketch or canvas editor using the Apple Pencil Kit and I want to add moveable, rotatable, and zoomable items (text, image, etc.) to canvas. When I use the Text element in SwiftUI and add Drag Gesture, Magnification Gesture, and Rotate Gesture, the drag gesture doesn't work simultaneously with the other gestures. So, I have decided to use UIViewRepresentable and create elements on the UIKit side. However, now I am unable to pan any items on the UIKit side. Please could you help me?
//
// SwiftUIView.swift
// Drawing
//
// Created by Furkan Ergün on 24.01.2023.
//
import SwiftUI
struct PanLabel: UIViewRepresentable {
#State private var position = CGSize.zero
#State private var pointSize: CGFloat = 0
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.text = "Pan me!"
label.font = UIFont.systemFont(ofSize: 30.0)
label.backgroundColor = UIColor.green
let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.panLabel(recognizer:)))
label.isUserInteractionEnabled = true
panGesture.delegate = context.coordinator
label.addGestureRecognizer(panGesture)
let pinchGesture = UIPinchGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.pinchLabel(gesture:)))
label.addGestureRecognizer(pinchGesture)
pinchGesture.delegate = context.coordinator
let rotateGesture = UIRotationGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.rotateLabel(gesture:)))
label.addGestureRecognizer(rotateGesture)
rotateGesture.delegate = context.coordinator
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIGestureRecognizerDelegate {
var parent: PanLabel
init(_ parent: PanLabel) {
self.parent = parent
}
open var isGestureEnabled = true
// MARK: - gesture handle
// location will jump when finger number change
private var initPanFingerNumber:Int = 1
private var isPanFingerNumberChangedInThisSession = false
private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
#objc func panLabel(recognizer: UIPanGestureRecognizer) {
guard isGestureEnabled, let view = recognizer.view else { return }
// init
if recognizer.state == .began {
lastPanPoint = recognizer.location(in: view)
initPanFingerNumber = recognizer.numberOfTouches
isPanFingerNumberChangedInThisSession = false
}
// judge valid
if recognizer.numberOfTouches != initPanFingerNumber {
isPanFingerNumberChangedInThisSession = true
}
if isPanFingerNumberChangedInThisSession {
return
}
// perform change
let point = recognizer.location(in: view)
view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
lastPanPoint = recognizer.location(in: view)
}
#objc func pinchLabel(gesture: UIPinchGestureRecognizer) {
// let label = gesture.view as! UILabel
//
// if gesture.state == .began {
// let font = label.font
// parent.pointSize = font!.pointSize
// gesture.scale = label.font!.pointSize * 0.1
// }
//
// if 1 <= gesture.scale && gesture.scale <= 10 {
// label.font = UIFont.systemFont(ofSize: gesture.scale * 10)
//
// resizeLabelToText(textLabel: label)
// }
// gesture.scale = 1
}
#objc func rotateLabel(gesture: UIRotationGestureRecognizer) {
let label = gesture.view as! UILabel
switch gesture.state {
case .changed:
// Update label's transform
label.transform = label.transform.rotated(by: gesture.rotation)
gesture.rotation = 0
default:
break
}
}
func resizeLabelToText(textLabel : UILabel) {
let labelSize = textLabel.intrinsicContentSize
textLabel.bounds.size = labelSize
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
}
struct SwiftUIView: View {
#State private var rotation: Double = 0
#State private var offset: CGSize = .zero
#State private var scale: CGFloat = 1
var body: some View {
VStack{
PanLabel().frame(width: 50, height: 50)
Spacer()
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
And also I tried this solution solution but I can't fit this for SwiftUI side.
What I'm doing:
I am creating a music app. Within this music app, I have a music player that links with Apple Music and Spotify. The music player displays the current song's album art on a UIImageView. Whenever the song is playing, the UIImageView rotates (like a record on a record player).
What my problem is:
The album art for the UIImageView is square by default. I am rounding the UIImageView's layer's corners and setting UIImageView.clipsToBounds equal to true to make it appear as a circle.
Whenever the UIImageView rotates, some of the pixels outside of the UIImageView's layer (the part that is cut off after rounding the image) are bleeding through.
Here is what the bug looks like: https://www.youtube.com/watch?v=OJxX5PQc7Jo&feature=youtu.be
My code:
The UIImageView is rounded by setting its layer's cornerRadius equal to UIImageView.frame.height / 2 and setting UIImageView.clipsToBounds = true:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
//I've also tried the following code, and am getting the same behavior:
/*
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.masksToBounds = true
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
*/
}
}
Whenever a button is pressed, the UIImageView begins to rotate. I've used the following extension to make UIView's rotate:
extension UIView {
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
layer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
}
I also have the following extension to end the rotation:
extension UIView {
...
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
if layer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = layer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
layer.transform = CATransform3DMakeRotation((CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle, 0.0, 0.0, 1.0)
layer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
} else {
return nil
}
}
}
This is how these extension functions are used in the context of my view controller:
class MyViewController: UIViewController {
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
...
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
So, all together, MyViewController looks something like this:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
I copy your code into the project and I can reproduce this issue. But if you add the animation to another CALayer, that seems to resolve the issue.
extension UIView {
static var kRotationAnimationKey: String {
return "kRotationAnimationKey"
}
func makeAnimationLayer() -> CALayer {
let results: [CALayer]? = layer.sublayers?.filter({ $0.name ?? "" == "animationLayer" })
let animLayer: CALayer
if let sublayers = results, sublayers.count > 0 {
animLayer = sublayers[0]
}
else {
animLayer = CAShapeLayer()
animLayer.name = "animationLayer"
animLayer.frame = self.bounds
animLayer.contents = UIImage(named: "imageNam")?.cgImage
layer.addSublayer(animLayer)
}
return animLayer
}
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let animLayer = makeAnimationLayer()
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
animLayer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
let animLayer = makeAnimationLayer()
if animLayer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = animLayer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
animLayer.transform = CATransform3DMakeRotation(CGFloat(angle) * (2 * CGFloat.pi) + startingAngle, 0.0, 0.0, 1.0)
animLayer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
}
else {
return nil
}
}
Clip to bounds does not include rounded corners. Try using corner masking instead.
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
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)
}
}
I have implemented a custom view with adding CALayer as sublayer for UIView. When I animate the view with the following:UIView.animateWithDuration(2.0) { self.slider.bounds.size *= 2.0}, the scaling animation is kind of wrong. The CALayer start at the wrong position with scaled size and move to the final position instead of scaling with the view.
The CustomeView Code :
import UIKit
class GridMaskView: UIView {
private let cornerLayer: CAShapeLayer
private let borderLayer: CAShapeLayer
private let gridLayer: CAShapeLayer
private let gridSize: (horizontal: UInt, vertical: UInt) = (3, 3)
private let cornerThickness: CGFloat = 3.0
private let cornerLength: CGFloat = 20.0
private let borderThickness: CGFloat = 2.0
private let gridThickness: CGFloat = 1.0
private let lineColor: UIColor = UIColor(r: 120, g: 179, b: 193, a: 1)
var showGridLines: Bool = true {
didSet {
gridLayer.hidden = !showGridLines
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
cornerLayer = CAShapeLayer()
cornerLayer.fillColor = lineColor.CGColor
borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = lineColor.CGColor
borderLayer.lineWidth = borderThickness
gridLayer = CAShapeLayer()
gridLayer.strokeColor = lineColor.CGColor
gridLayer.lineWidth = gridThickness
super.init(frame: frame)
layer.addSublayer(cornerLayer)
layer.addSublayer(borderLayer)
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
layoutLayers()
}
private func layoutLayers() {
drawCorner()
drawBorder()
drawGrid()
}
private func drawCorner() {
cornerLayer.frame = bounds.insetBy(dx: -cornerThickness, dy: -cornerThickness)
cornerLayer.path = cornerPath(forBounds: cornerLayer.bounds)
}
private func cornerPath(forBounds bounds: CGRect) -> CGPathRef {
let horizontalSize = CGSize(width: cornerLength, height: cornerThickness)
let verticalSize = CGSize(width: cornerThickness, height: cornerLength)
let corners: [(CGRectEdge, CGRectEdge)] = [(.MinXEdge, .MinYEdge), (.MinXEdge, .MaxYEdge), (.MaxXEdge, .MinYEdge), (.MaxXEdge, .MaxYEdge)]
var cornerRects = [CGRect]()
for corner in corners {
cornerRects.append(bounds.align(horizontalSize, corner: corner.0, corner.1))
cornerRects.append(bounds.align(verticalSize, corner: corner.0, corner.1))
}
let cornerPath = CGPathCreateMutable()
CGPathAddRects(cornerPath, nil, cornerRects, cornerRects.count)
return cornerPath
}
private func drawBorder() {
borderLayer.frame = bounds
borderLayer.path = borderPath(forBounds: borderLayer.bounds)
}
private func borderPath(forBounds bounds: CGRect) -> CGPathRef {
let borderPath = CGPathCreateMutable()
let borderCornerPoints = [bounds.topLeft, bounds.topRight, bounds.bottomRight, bounds.bottomLeft, bounds.topLeft]
CGPathAddLines(borderPath, nil, borderCornerPoints, borderCornerPoints.count)
return borderPath
}
private func drawGrid() {
gridLayer.frame = bounds
gridLayer.path = gridPath(forBounds: gridLayer.bounds)
}
private func gridPath(forBounds bounds: CGRect) -> CGPathRef {
let stepSize = bounds.size / (CGFloat(gridSize.horizontal), CGFloat(gridSize.vertical))
let gridPath = CGPathCreateMutable()
for i in (1...gridSize.vertical) {
let x = CGFloat(i) * stepSize.width
CGPathMoveToPoint(gridPath, nil, x, 0)
CGPathAddLineToPoint(gridPath, nil, x, bounds.size.height)
}
for i in (1...gridSize.horizontal) {
let y = CGFloat(i) * stepSize.height
CGPathMoveToPoint(gridPath, nil, 0, y)
CGPathAddLineToPoint(gridPath, nil, bounds.size.width, y)
}
return gridPath
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: cornerLength * 2, height: cornerLength * 2)
}
}
Anyone know how to fit this?
The problem is that when you do view animation you don't get any automatic animation of sublayers. You'd be better off using a subview of your original UIView, because view animation will animate that together with the original view, according to its autolayout constraints.
I have implemented a custom view with adding CALayer as sublayer for UIView. When I animate the view with the following:UIView.animateWithDuration(2.0) { self.slider.bounds.size *= 2.0}, the scaling animation is kind of wrong. The CALayer start at the wrong position with scaled size and move to the final position instead of scaling with the view.
The CustomeView Code :
import UIKit
class GridMaskView: UIView {
private let cornerLayer: CAShapeLayer
private let borderLayer: CAShapeLayer
private let gridLayer: CAShapeLayer
private let gridSize: (horizontal: UInt, vertical: UInt) = (3, 3)
private let cornerThickness: CGFloat = 3.0
private let cornerLength: CGFloat = 20.0
private let borderThickness: CGFloat = 2.0
private let gridThickness: CGFloat = 1.0
private let lineColor: UIColor = UIColor(r: 120, g: 179, b: 193, a: 1)
var showGridLines: Bool = true {
didSet {
gridLayer.hidden = !showGridLines
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
cornerLayer = CAShapeLayer()
cornerLayer.fillColor = lineColor.CGColor
borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = lineColor.CGColor
borderLayer.lineWidth = borderThickness
gridLayer = CAShapeLayer()
gridLayer.strokeColor = lineColor.CGColor
gridLayer.lineWidth = gridThickness
super.init(frame: frame)
layer.addSublayer(cornerLayer)
layer.addSublayer(borderLayer)
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
layoutLayers()
}
private func layoutLayers() {
drawCorner()
drawBorder()
drawGrid()
}
private func drawCorner() {
cornerLayer.frame = bounds.insetBy(dx: -cornerThickness, dy: -cornerThickness)
cornerLayer.path = cornerPath(forBounds: cornerLayer.bounds)
}
private func cornerPath(forBounds bounds: CGRect) -> CGPathRef {
let horizontalSize = CGSize(width: cornerLength, height: cornerThickness)
let verticalSize = CGSize(width: cornerThickness, height: cornerLength)
let corners: [(CGRectEdge, CGRectEdge)] = [(.MinXEdge, .MinYEdge), (.MinXEdge, .MaxYEdge), (.MaxXEdge, .MinYEdge), (.MaxXEdge, .MaxYEdge)]
var cornerRects = [CGRect]()
for corner in corners {
cornerRects.append(bounds.align(horizontalSize, corner: corner.0, corner.1))
cornerRects.append(bounds.align(verticalSize, corner: corner.0, corner.1))
}
let cornerPath = CGPathCreateMutable()
CGPathAddRects(cornerPath, nil, cornerRects, cornerRects.count)
return cornerPath
}
private func drawBorder() {
borderLayer.frame = bounds
borderLayer.path = borderPath(forBounds: borderLayer.bounds)
}
private func borderPath(forBounds bounds: CGRect) -> CGPathRef {
let borderPath = CGPathCreateMutable()
let borderCornerPoints = [bounds.topLeft, bounds.topRight, bounds.bottomRight, bounds.bottomLeft, bounds.topLeft]
CGPathAddLines(borderPath, nil, borderCornerPoints, borderCornerPoints.count)
return borderPath
}
private func drawGrid() {
gridLayer.frame = bounds
gridLayer.path = gridPath(forBounds: gridLayer.bounds)
}
private func gridPath(forBounds bounds: CGRect) -> CGPathRef {
let stepSize = bounds.size / (CGFloat(gridSize.horizontal), CGFloat(gridSize.vertical))
let gridPath = CGPathCreateMutable()
for i in (1...gridSize.vertical) {
let x = CGFloat(i) * stepSize.width
CGPathMoveToPoint(gridPath, nil, x, 0)
CGPathAddLineToPoint(gridPath, nil, x, bounds.size.height)
}
for i in (1...gridSize.horizontal) {
let y = CGFloat(i) * stepSize.height
CGPathMoveToPoint(gridPath, nil, 0, y)
CGPathAddLineToPoint(gridPath, nil, bounds.size.width, y)
}
return gridPath
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: cornerLength * 2, height: cornerLength * 2)
}
}
Anyone know how to fit this?
The problem is that when you do view animation you don't get any automatic animation of sublayers. You'd be better off using a subview of your original UIView, because view animation will animate that together with the original view, according to its autolayout constraints.