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
Attached video here
I want create this before and after view for side by side image comparison image here
You can do this with a layer mask.
Essentially:
private let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.black.cgColor
leftImage.layer.mask = maskLayer
var r = bounds
r.size.width = bounds.width * pct
maskLayer.frame = r
That will "clip" the image view and show only the portion covered by the mask layer.
Here's a complete example...
Using these "before" (left-side) and "after" (right-side) images:
With this sample custom view:
class RevealImageView: UIImageView {
public var leftImage: UIImage? {
didSet {
if let img = leftImage {
leftImageLayer.contents = img.cgImage
}
}
}
public var rightImage: UIImage? {
didSet {
if let img = rightImage {
self.image = img
}
}
}
// private properties
private let leftImageLayer = CALayer()
private let maskLayer = CALayer()
private let lineView = UIView()
private var pct: CGFloat = 0.5 {
didSet {
updateView()
}
}
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// any opaque color
maskLayer.backgroundColor = UIColor.black.cgColor
leftImageLayer.mask = maskLayer
// the "reveal" image layer
layer.addSublayer(leftImageLayer)
// the vertical line
lineView.backgroundColor = .lightGray
addSubview(lineView)
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first
else { return }
let loc = t.location(in: self)
pct = loc.x / bounds.width
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first
else { return }
let loc = t.location(in: self)
pct = loc.x / bounds.width
}
private func updateView() {
// move the vertical line to the touch point
lineView.frame = CGRect(x: bounds.width * pct, y: bounds.minY, width: 1, height: bounds.height)
// update the "left image" mask to the touch point
var r = bounds
r.size.width = bounds.width * pct
// disable layer animation
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.frame = r
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
leftImageLayer.frame = bounds
updateView()
}
}
and this simple example controller:
class RevealImageTestVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let leftIMG = UIImage(named: "before"),
let rightIMG = UIImage(named: "after")
else {
return
}
let revealView = RevealImageView()
revealView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(revealView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
revealView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
revealView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
revealView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// give reveal view the same aspect ratio as the image
revealView.heightAnchor.constraint(equalTo: revealView.widthAnchor, multiplier: leftIMG.size.height / leftIMG.size.width),
])
revealView.leftImage = leftIMG
revealView.rightImage = rightIMG
}
}
We get this - the vertical line and the "viewable" portion of the images will move as you touch-and-drag:
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 have been following a tutorial explaining a custom ios control. There is no problem with the tutorial but I got really confused about the frame/bounds clipping (not part of the tutorial).
I have a UIView instance in the scene in the storyboard. This UIView is sized at 120 x 120. I am adding a custom control (extending UIControl) to this container view with addSubview. I began to experiment with setting different widths and height to the frame and bounds of the custom control, this is the initializer of the control:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
}
...and produces this result (red is the parent, blue circle is the child):
Now I change the init to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.frame.size.width = 40
self.frame.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
And that produces this result:
and prints:
frame: (0.0,0.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
But when I change the initliazer to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
I get this:
and prints: frame: (40.0,40.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
I cannot seem to comprehend why this view is centered in its parent view when I change its bounds. What exactly is causing it? Is the 'frame' clipped always towards its center? Or is this view centered in its parent after the bounds have been modified and that causes the update of the 'frame'? Is it some property that can be changed? How could I manage to put it to top-left corner, for example (exactly in the same way as when I modify the 'frame')? Thanks a lot!
EDIT:
class ViewController: UIViewController {
#IBOutlet var knobPlaceholder: UIView!
#IBOutlet var valueLabel: UILabel!
#IBOutlet var valueSlider: UISlider!
#IBOutlet var animateSwitch: UISwitch!
var knob: Knob!
override func viewDidLoad() {
super.viewDidLoad()
knob = Knob(frame: knobPlaceholder.bounds)
knobPlaceholder.addSubview(knob)
knobPlaceholder.backgroundColor = UIColor.redColor();
}
#IBAction func sliderValueChanged(slider: UISlider) {
}
#IBAction func randomButtonTouched(button: UIButton) {
}
}
And the knob:
public class Knob: UIControl {
private let knobRenderer = KnobRenderer()
private var backingValue: Float = 0.0
/** Contains the receiver’s current value. */
public var value: Float {
get {
return backingValue
}
set {
setValue(newValue, animated: false)
}
}
/** Sets the receiver’s current value, allowing you to animate the change visually. */
public func setValue(value: Float, animated: Bool) {
if value != backingValue {
backingValue = min(maximumValue, max(minimumValue, value))
}
}
/** Contains the minimum value of the receiver. */
public var minimumValue: Float = 0.0
/** Contains the maximum value of the receiver. */
public var maximumValue: Float = 1.0
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createSublayers() {
knobRenderer.update(bounds)
knobRenderer.strokeColor = tintColor
knobRenderer.startAngle = -CGFloat(M_PI * 11.0 / 8.0);
knobRenderer.endAngle = CGFloat(M_PI * 3.0 / 8.0);
knobRenderer.pointerAngle = knobRenderer.startAngle;
knobRenderer.lineWidth = 2.0
knobRenderer.pointerLength = 6.0
layer.addSublayer(knobRenderer.trackLayer)
layer.addSublayer(knobRenderer.pointerLayer)
}
}
private class KnobRenderer {
let trackLayer = CAShapeLayer()
let pointerLayer = CAShapeLayer()
var strokeColor: UIColor {
get {
return UIColor(CGColor: trackLayer.strokeColor)
}
set(strokeColor) {
trackLayer.strokeColor = strokeColor.CGColor
pointerLayer.strokeColor = strokeColor.CGColor
}
}
var lineWidth: CGFloat = 1.0 {
didSet {
update();
}
}
var startAngle: CGFloat = 0.0 {
didSet {
update();
}
}
var endAngle: CGFloat = 0.0 {
didSet {
update()
}
}
var backingPointerAngle: CGFloat = 0.0
var pointerAngle: CGFloat {
get { return backingPointerAngle }
set { setPointerAngle(newValue, animated: false) }
}
func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
backingPointerAngle = pointerAngle
}
var pointerLength: CGFloat = 0.0 {
didSet {
update()
}
}
init() {
trackLayer.fillColor = UIColor.clearColor().CGColor
pointerLayer.fillColor = UIColor.clearColor().CGColor
}
func updateTrackLayerPath() {
let arcCenter = CGPoint(x: trackLayer.bounds.width / 2.0, y: trackLayer.bounds.height / 2.0)
let offset = max(pointerLength, trackLayer.lineWidth / 2.0)
let radius = min(trackLayer.bounds.height, trackLayer.bounds.width) / 2.0 - offset;
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
}
func updatePointerLayerPath() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: pointerLayer.bounds.width - pointerLength - pointerLayer.lineWidth / 2.0, y: pointerLayer.bounds.height / 2.0))
path.addLineToPoint(CGPoint(x: pointerLayer.bounds.width, y: pointerLayer.bounds.height / 2.0))
pointerLayer.path = path.CGPath
}
func update(bounds: CGRect) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
func update() {
trackLayer.lineWidth = lineWidth
pointerLayer.lineWidth = lineWidth
updateTrackLayerPath()
updatePointerLayerPath()
}
}
In your update function you are centering the view.
func update(bounds: CGRect) {
**let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)**
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
If you take that out, then you're view won't be centered anymore. The reason setting the frame would leave it in the upper left hand corner while setting the bounds resulted in it being in the center is because setting the bounds x/y does not override the frame x/y. When you set the frame then later on your code only sets the bounds, so the frame x/y is never overwritten so the view stays in the upper left hand corner. However, in the second there is no x/y set for the frame, so I guess it's taken what you set for the bounds, so it get's centered.
I would recommend not setting the bounds x/y for the view as that should always be 0, 0. If you want to reposition it then use the frame. Remember the frame is relative the parent while the bounds is relative to it's self.