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.
Related
I am trying to animate a subclass of UIView that has a some layers with shadow. In a view controller, I set this view inside a container. I animate container's height constraint. The container (the purple one in the video) animates properly, but the view that is supposed to be animated, doesn't animate the way it should be.
How it looks now
This is how I animate the container view.
func updateWhiteCircle(with progressHeight: CGFloat?) {
guard let progressHeight = progressHeight else {
return
}
neumorphicRingProgressHeightConstraint.constant = progressHeight
UIView.animate(withDuration: 1.0) { [weak self] in
self?.view.layoutIfNeeded()
}
}
Where do I get it wrong? Why doesn't it animate the way it should be?
You didn't show us how you're generating the "white shadow," but likely you're setting it in the view's layoutSubviews() func.
The problem is, that does not animate with the size of the view.
You probably want to animate the path for the "shadow layer."
Here's a quick example:
class WhiteCircleView: UIView {
private let shapeLayer = CAShapeLayer()
// public vars so we can set various properties
public var fillColor: UIColor = .white {
didSet {
shapeLayer.fillColor = fillColor.cgColor
}
}
public var shadowColor: UIColor = .white {
didSet {
shapeLayer.shadowColor = shadowColor.cgColor
}
}
public var shadowOpacity: Float = 1.0 {
didSet {
shapeLayer.shadowOpacity = shadowOpacity
}
}
public var shadowRadius: CGFloat = 20 {
didSet {
shapeLayer.shadowRadius = shadowRadius
}
}
public var shadowOffset: CGSize = .zero {
didSet {
shapeLayer.shadowOffset = shadowOffset
}
}
public var progress: CGFloat = 0 {
didSet {
animCircle()
}
}
private var curProgress: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.shadowColor = shadowColor.cgColor
shapeLayer.shadowOffset = shadowOffset
shapeLayer.shadowRadius = shadowRadius
shapeLayer.shadowOpacity = shadowOpacity
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
shapeLayer.path = newPath
}
private func animCircle() {
print(progress)
let w: CGFloat = bounds.width * CGFloat(progress)
let wi: CGFloat = (bounds.width - w) * 0.5
let curPath = shapeLayer.path
let newPath = UIBezierPath(ovalIn: bounds.insetBy(dx: wi, dy: wi)).cgPath
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = curPath
animation.toValue = newPath
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
CATransaction.setCompletionBlock({
// update to new path on anim end
self.shapeLayer.path = newPath
})
shapeLayer.add(animation, forKey: "grow")
CATransaction.commit()
}
}
class AnimCircleVC: UIViewController {
let wcv = WhiteCircleView()
let bkgColor: UIColor = UIColor(white: 0.75, alpha: 1.0)
var progress: CGFloat = 0
// a "center label" to show progress value
let pLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .systemYellow
v.textAlignment = .center
v.text = "0%"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = bkgColor
wcv.fillColor = bkgColor
wcv.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wcv)
pLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wcv.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wcv.centerYAnchor.constraint(equalTo: g.topAnchor, constant: 200.0),
wcv.heightAnchor.constraint(equalToConstant: 200.0),
wcv.widthAnchor.constraint(equalTo: wcv.heightAnchor),
pLabel.centerXAnchor.constraint(equalTo: wcv.centerXAnchor),
pLabel.centerYAnchor.constraint(equalTo: wcv.centerYAnchor),
pLabel.widthAnchor.constraint(equalToConstant: 100.0),
pLabel.heightAnchor.constraint(equalTo: pLabel.widthAnchor),
])
pLabel.layer.cornerRadius = 50.0
pLabel.layer.masksToBounds = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if progress >= 1.0 {
// shrink it away
progress = 0.0
} else {
if progress == 0.0 {
// start at 60%
progress = 0.6
} else {
// increment by 10%
progress += 0.10
}
}
// floating point can result in 0.99999...
// so round to 1/100th
progress = (progress * 100).rounded() / 100.0
wcv.progress = progress
pLabel.text = "\(Int(progress * 100))%"
}
}
I'm trying to create a circular progressBar with timer function in my UItableviewCell class. I have created a custom class timer, and I am using Jonni's progressBar template (Credit to
Jonni Ã…kesson) from - https://github.com/innoj/CountdownTimer.
The only noticeable difference between my code and source template is that I am adding the Custom ProgressBar class UIView programmatically whereas the source template used IBOutlet to connect to the Custom ProgressBar Class.
My goal is to ensure that the CAShapeLayers (both actual and background layers are shown as per below image - Source: https://www.youtube.com/watch?v=-KwFvGVstyc
Below is my code and I am unable to see both front CAShapeLayer and background CAShapeLayer. I am suspecting a logical error when I initialize constant "progressBar".
Please advise if it is possible to utilize the Custom ProgressBar Class, instead of re-writing the entire code.
import Foundation
import UIKit
import IQKeyboardManagerSwift
class ActiveExerciseTableViewCell: UITableViewCell, UITextFieldDelegate {
let progressBar: ProgressBar = {
let progressBar = ProgressBar()
return progressBar
}()
lazy var activeExerciseTimerUIView: UIView = { [weak self] in
let activeExerciseTimerUIView = UIView()
activeExerciseTimerUIView.backgroundColor = .black
activeExerciseTimerUIView.isUserInteractionEnabled = true
activeExerciseTimerUIView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapTimerView)))
return activeExerciseTimerUIView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(activeExerciseTimerUIView)
}
override func layoutSubviews() {
super.layoutSubviews()
setUpActiveExerciseUIViewLayout()
}
func setUpActiveExerciseUIViewLayout(){
activeExerciseTimerUIView.translatesAutoresizingMaskIntoConstraints = false
activeExerciseTimerUIView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
activeExerciseTimerUIView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: (contentView.frame.height-tableviewContentViewTabBarHeight)*0.25).isActive = true
activeExerciseTimerUIView.widthAnchor.constraint(equalToConstant: 225).isActive = true
activeExerciseTimerUIView.heightAnchor.constraint(equalToConstant: 225).isActive = true
activeExerciseTimerUIView.layer.cornerRadius = activeExerciseTimerUIView.frame.width/2
progressBar.frame = CGRect(
x: 0,
y: 0,
width: activeExerciseTimerUIView.frame.width,
height: activeExerciseTimerUIView.frame.height)
self.activeExerciseTimerUIView.addSubview(progressBar)
}
Below is the Custom ProgressBar Class from source file.
import UIKit
class ProgressBar: UIView, CAAnimationDelegate {
fileprivate var animation = CABasicAnimation()
fileprivate var animationDidStart = false
fileprivate var timerDuration = 0
lazy var fgProgressLayer: CAShapeLayer = {
let fgProgressLayer = CAShapeLayer()
return fgProgressLayer
}()
lazy var bgProgressLayer: CAShapeLayer = {
let bgProgressLayer = CAShapeLayer()
return bgProgressLayer
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
loadBgProgressBar()
loadFgProgressBar()
}
override init(frame: CGRect) {
super.init(frame: frame)
loadBgProgressBar()
loadFgProgressBar()
}
fileprivate func loadFgProgressBar() {
let startAngle = CGFloat(-Double.pi / 2)
let endAngle = CGFloat(3 * Double.pi / 2)
let centerPoint = CGPoint(x: frame.width/2 , y: frame.height/2)
let gradientMaskLayer = gradientMask()
fgProgressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: frame.width/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).cgPath
fgProgressLayer.backgroundColor = UIColor.clear.cgColor
fgProgressLayer.fillColor = nil
fgProgressLayer.strokeColor = UIColor.black.cgColor
fgProgressLayer.lineWidth = 4.0
fgProgressLayer.strokeStart = 0.0
fgProgressLayer.strokeEnd = 0.0
gradientMaskLayer.mask = fgProgressLayer
layer.addSublayer(gradientMaskLayer)
}
fileprivate func gradientMask() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 1.0]
let colorTop: AnyObject = CustomColor.lime.cgColor
let colorBottom: AnyObject = CustomColor.lime.cgColor
let arrayOfColors: [AnyObject] = [colorTop, colorBottom]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
fileprivate func loadBgProgressBar() {
let startAngle = CGFloat(-Double.pi / 2)
let endAngle = CGFloat(3 * Double.pi / 2)
let centerPoint = CGPoint(x: frame.width/2 , y: frame.height/2)
let gradientMaskLayer = gradientMaskBg()
bgProgressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: frame.width/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).cgPath
bgProgressLayer.backgroundColor = UIColor.clear.cgColor
bgProgressLayer.fillColor = nil
bgProgressLayer.strokeColor = UIColor.black.cgColor
bgProgressLayer.lineWidth = 4.0
bgProgressLayer.strokeStart = 0.0
bgProgressLayer.strokeEnd = 1.0
gradientMaskLayer.mask = bgProgressLayer
layer.addSublayer(gradientMaskLayer)
}
fileprivate func gradientMaskBg() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 1.0]
let colorTop: AnyObject = CustomColor.strawberry.cgColor
let colorBottom: AnyObject = CustomColor.strawberry.cgColor
let arrayOfColors: [AnyObject] = [colorTop, colorBottom]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
Resolve the issue by loading the background and foreground progress bars in layoutSubViews function.
ProgressBar.swift
import UIKit
import Pulsator
class ProgressBar: UIView, CAAnimationDelegate {
fileprivate var animation = CABasicAnimation()
fileprivate var animationDidStart = false
fileprivate var updatedAnimationAdded = false
fileprivate var updateExecuted = false
fileprivate var totalTimerDuration = 0
fileprivate var isProgressBarAdded:Bool = false
fileprivate var barlineWidth: CGFloat = 8
// let pulsator = Pulsator()
// fileprivate var width: CGFloat
// fileprivate var height: CGFloat
//
lazy var fgProgressLayer: CAShapeLayer = {
let fgProgressLayer = CAShapeLayer()
return fgProgressLayer
}()
lazy var bgProgressLayer: CAShapeLayer = {
let bgProgressLayer = CAShapeLayer()
return bgProgressLayer
}()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
loadBgProgressBar()
loadFgProgressBar()
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func layoutSubviews() {
super.layoutSubviews()
if self.frame.width > 0 && self.frame.height > 0 && isProgressBarAdded == false {
loadBgProgressBar()
loadFgProgressBar()
isProgressBarAdded = true
}
}
I'm trying to use a PDF vector image as the contents of a CALayer, but when it is scaled above it's initial size of 15x13, it looks very blurry. I have 'Preserve Vector Data' turned on in my asset catalog for the image in question. Here is the code for my view, which draws an outer circle on one layer, and uses a second layer to display an image of a checkmark in the center of the view if the isComplete property is set to true.
#IBDesignable
public class GoalCheckView: UIView {
// MARK: - Public properties
#IBInspectable public var isComplete: Bool = false {
didSet {
setNeedsLayout()
}
}
// MARK: - Private properties
private lazy var checkImage: UIImage? = {
let bundle = Bundle(for: type(of: self))
return UIImage(named: "check_event_carblog_confirm", in: bundle, compatibleWith: nil)
}()
private var checkImageSize: CGSize {
let widthRatio: CGFloat = 15 / 24 // Size of image is 15x13 when circle is 24x24
let heightRatio: CGFloat = 13 / 24
return CGSize(width: bounds.width * widthRatio, height: bounds.height * heightRatio)
}
private let circleLayer = CAShapeLayer()
private let checkLayer = CALayer()
private let lineWidth: CGFloat = 1
// MARK: - View lifecycle
public override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2))
circleLayer.path = path.cgPath
// Layout check
checkLayer.frame = CGRect(
origin: CGPoint(x: bounds.midX - checkImageSize.width / 2, y: bounds.midY - checkImageSize.height / 2),
size: checkImageSize
)
checkLayer.opacity = isComplete ? 1 : 0
}
// MARK: - Private methods
private func setupView() {
// Setup circle layer
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor(named: "goal_empty", in: bundle, compatibleWith: nil)?.cgColor
layer.addSublayer(circleLayer)
// Setup check layer
checkLayer.contentsScale = UIScreen.main.scale
checkLayer.contentsGravity = .resizeAspect
checkLayer.contents = checkImage?.cgImage
layer.addSublayer(checkLayer)
}
}
This code results in the following display if I set the size of the view to 240x240:
I was able to create a workaround for this. I can check the expected size of my image in layoutSubviews, and if it does not match the size of the UIImage I can use a UIGraphicsImageRenderer to create a new image that is scaled to the correct size. I created an extension of UIImage to facilitate this:
extension UIImage {
internal func imageScaled(toSize scaledSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: scaledSize)
let newImage = renderer.image { [unowned self] _ in
self.draw(in: CGRect(origin: .zero, size: scaledSize))
}
return newImage
}
}
Now, my updated layoutSubviews method looks like this:
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth.mid, dy: lineWidth.mid))
circleLayer.path = path.cgPath
// Layout check
if let checkImage = checkImage, checkImage.size != checkImageSize {
checkLayer.contents = checkImage.imageScaled(toSize: checkImageSize).cgImage
}
let checkOrigin = CGPoint(x: bounds.midX - checkImageSize.midW, y: bounds.midY - checkImageSize.midH)
checkLayer.frame = CGRect(origin: checkOrigin, size: checkImageSize)
}
This results in a nice crisp image:
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'm trying to implement Range Slider and I used custom control called NMRangeSlider.
But when I use it, the slider doesn't appear at all. Could it be also because it's all written in Objective-C?
This is how I've currently implemented it:
var rangeSlider = NMRangeSlider(frame: CGRectMake(16, 6, 275, 34))
rangeSlider.lowerValue = 0.54
rangeSlider.upperValue = 0.94
self.view.addSubview(rangeSlider)
To create a custom Range Slider I found a good solution here: range finder tutorial iOS 8 but I needed this in swift 3 for my project. I updated this for Swift 3 iOS 10 here:
in your main view controller add this to viewDidLayOut to show a range slider.
override func viewDidLayoutSubviews() {
let margin: CGFloat = 20.0
let width = view.bounds.width - 2.0 * margin
rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length + 170, width: width, height: 31.0)
}
create the helper function to print slider output below viewDidLayoutSubviews()
func rangeSliderValueChanged() { //rangeSlider: RangeSlider
print("Range slider value changed: \(rangeSlider.lowerValue) \(rangeSlider.upperValue) ")//(\(rangeSlider.lowerValue) \(rangeSlider.upperValue))
}
Create the file RangeSlider.swift and add this to it:
import UIKit
import QuartzCore
class RangeSlider: UIControl {
var minimumValue = 0.0
var maximumValue = 1.0
var lowerValue = 0.2
var upperValue = 0.8
let trackLayer = RangeSliderTrackLayer()//= CALayer() defined in RangeSliderTrackLayer.swift
let lowerThumbLayer = RangeSliderThumbLayer()//CALayer()
let upperThumbLayer = RangeSliderThumbLayer()//CALayer()
var previousLocation = CGPoint()
var trackTintColor = UIColor(white: 0.9, alpha: 1.0)
var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
var thumbTintColor = UIColor.white
var curvaceousness : CGFloat = 1.0
var thumbWidth: CGFloat {
return CGFloat(bounds.height)
}
override init(frame: CGRect) {
super.init(frame: frame)
trackLayer.rangeSlider = self
trackLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(trackLayer)
lowerThumbLayer.rangeSlider = self
lowerThumbLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(lowerThumbLayer)
upperThumbLayer.rangeSlider = self
upperThumbLayer.contentsScale = UIScreen.main.scale
layer.addSublayer(upperThumbLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func updateLayerFrames() {
trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height / 3)
trackLayer.setNeedsDisplay()
let lowerThumbCenter = CGFloat(positionForValue(value: lowerValue))
lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
lowerThumbLayer.setNeedsDisplay()
let upperThumbCenter = CGFloat(positionForValue(value: upperValue))
upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
upperThumbLayer.setNeedsDisplay()
}
func positionForValue(value: Double) -> Double {
return Double(bounds.width - thumbWidth) * (value - minimumValue) /
(maximumValue - minimumValue) + Double(thumbWidth / 2.0)
}
override var frame: CGRect {
didSet {
updateLayerFrames()
}
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
previousLocation = touch.location(in: self)
// Hit test the thumb layers
if lowerThumbLayer.frame.contains(previousLocation) {
lowerThumbLayer.highlighted = true
} else if upperThumbLayer.frame.contains(previousLocation) {
upperThumbLayer.highlighted = true
}
return lowerThumbLayer.highlighted || upperThumbLayer.highlighted
}
func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
return min(max(value, lowerValue), upperValue)
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
// 1. Determine by how much the user has dragged
let deltaLocation = Double(location.x - previousLocation.x)
let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - thumbWidth)
previousLocation = location
// 2. Update the values
if lowerThumbLayer.highlighted {
lowerValue += deltaValue
lowerValue = boundValue(value: lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
} else if upperThumbLayer.highlighted {
upperValue += deltaValue
upperValue = boundValue(value: upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
}
// 3. Update the UI
CATransaction.begin()
CATransaction.setDisableActions(true)
updateLayerFrames()
CATransaction.commit()
sendActions(for: .valueChanged)
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
lowerThumbLayer.highlighted = false
upperThumbLayer.highlighted = false
}
}
Next add the thumb layer subclass file RangeSliderThumbLayer.swift and add this to it:
import UIKit
class RangeSliderThumbLayer: CALayer {
var highlighted = false
weak var rangeSlider: RangeSlider?
override func draw(in ctx: CGContext) {
if let slider = rangeSlider {
let thumbFrame = bounds.insetBy(dx: 2.0, dy: 2.0)
let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)
// Fill - with a subtle shadow
let shadowColor = UIColor.gray
ctx.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 1.0, color: shadowColor.cgColor)
ctx.setFillColor(slider.thumbTintColor.cgColor)
ctx.addPath(thumbPath.cgPath)
ctx.fillPath()
// Outline
ctx.setStrokeColor(shadowColor.cgColor)
ctx.setLineWidth(0.5)
ctx.addPath(thumbPath.cgPath)
ctx.strokePath()
if highlighted {
ctx.setFillColor(UIColor(white: 0.0, alpha: 0.1).cgColor)
ctx.addPath(thumbPath.cgPath)
ctx.fillPath()
}
}
}
}
Finally add the track layer subclass file RangeSliderTrackLayer.swift and add the following to it:
import Foundation
import UIKit
import QuartzCore
class RangeSliderTrackLayer: CALayer {
weak var rangeSlider: RangeSlider?
override func draw(in ctx: CGContext) {
if let slider = rangeSlider {
// Clip
let cornerRadius = bounds.height * slider.curvaceousness / 2.0
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
ctx.addPath(path.cgPath)
// Fill the track
ctx.setFillColor(slider.trackTintColor.cgColor)
ctx.addPath(path.cgPath)
ctx.fillPath()
// Fill the highlighted range
ctx.setFillColor(slider.trackHighlightTintColor.cgColor)
let lowerValuePosition = CGFloat(slider.positionForValue(value: slider.lowerValue))
let upperValuePosition = CGFloat(slider.positionForValue(value: slider.upperValue))
let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
ctx.fill(rect)
}
}
}
Build Run and Get:
UPDATE:
It did not show to me, because it was all white. So the solution, without using any other framework and sticking with this one - you need to set all the views for all the components and then it will display well:
I have tried to import it in Swift as I used it before in Objective-C code, but without any luck. If I set everything properly and add it either in viewDidLoad() or viewDidAppear(), nothing gets displayed. One thing is worth mentioning, though - when I enter View Debug Hierarchy, the slider actually is there on the canvas:
But it's simply not rendered with all the colors that I did set before adding in it to the view. For the record - this is the code I used:
override func viewDidAppear(animated: Bool) {
var rangeSlider = NMRangeSlider(frame: CGRectMake(50, 50, 275, 34))
rangeSlider.lowerValue = 0.54
rangeSlider.upperValue = 0.94
let range = 10.0
let oneStep = 1.0 / range
let minRange: Float = 0.05
rangeSlider.minimumRange = minRange
let bgImage = UIView(frame: rangeSlider.frame)
bgImage.backgroundColor = .greenColor()
rangeSlider.trackImage = bgImage.pb_takeSnapshot()
let trackView = UIView(frame: CGRectMake(0, 0, rangeSlider.frame.size.width, 29))
trackView.backgroundColor = .whiteColor()
trackView.opaque = false
trackView.alpha = 0.3
rangeSlider.trackImage = UIImage(named: "")
let lowerThumb = UIView(frame: CGRectMake(0, 0, 8, 29))
lowerThumb.backgroundColor = .whiteColor()
let lowerThumbHigh = UIView(frame: CGRectMake(0, 0, 8, 29))
lowerThumbHigh.backgroundColor = UIColor.blueColor()
rangeSlider.lowerHandleImageNormal = lowerThumb.pb_takeSnapshot()
rangeSlider.lowerHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
rangeSlider.upperHandleImageNormal = lowerThumb.pb_takeSnapshot()
rangeSlider.upperHandleImageHighlighted = lowerThumbHigh.pb_takeSnapshot()
self.view.addSubview(rangeSlider)
self.view.backgroundColor = .lightGrayColor()
}
Using the method for capturing the UIView as UIImage mentioned in this question:
extension UIView {
func pb_takeSnapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.mainScreen().scale)
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Other solution:
You can also try sgwilly/RangeSlider instead, it's written in Swift and therefore you won't even need a Bridging Header.
try this code :
override func viewDidLayoutSubviews() {
let margin: CGFloat = 20.0
let width = view.bounds.width - 2.0 * margin
rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length,
width: width, height: 31.0)
}
I implemented the range slider using :
https://github.com/Zengzhihui/RangeSlider
In the GZRangeSlider class, there is a method called :
private func setLabelText()
In that method, just put :
leftTextLayer.frame = CGRectMake(leftHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftHandleLayer.frame.minY - kTextHeight, kTextWidth, kTextHeight)
rightTextLayer.frame = CGRectMake(rightHandleLayer.frame.minX - 0.5 * (kTextWidth - leftHandleLayer.frame.width), leftTextLayer.frame.minY, kTextWidth, kTextHeight)
to animate the lower and upper labels..
This one is working well for me and its in swift.. just try it..