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:
Related
I'm making a custom UISlider with thumb like this:
But here is what I get:
I want my thumb show with full height of the grey container, my image has size 160x160 for #2x. But when set into UISlider it doesn't act like that. Here is the view debugging:
I'm use a custom class for this UISlider, where's wrong in the code?:
class CustomSlider: UISlider {
#IBInspectable var trackHeight: CGFloat = 0.0001
// #IBInspectable var thumbRadius: CGFloat = 20
// Custom thumb view which will be converted to UIImage
// and set as thumb. You can customize it's colors, border, etc.
private lazy var thumbView: UIView = {
let thumb = UIImageView()
// thumb.backgroundColor = .white
// thumb.layer.borderWidth = 0.4
// thumb.layer.borderColor = UIColor.white.cgColor
thumb.image = UIImage(named: "icon_slider")
return thumb
}()
override func awakeFromNib() {
super.awakeFromNib()
let thumb = thumbImage()
setThumbImage(thumb, for: .normal)
}
private func thumbImage() -> UIImage {
thumbView.frame = CGRect(x: 0, y: 50/2, width: 50, height: 50)
// thumbView.dropShadow(color: .red, opacity: 1, offSet: CGSize(width: -1, height: 1), radius: 3, scale: true)
thumbView.layer.cornerRadius = 4
thumbView.layer.masksToBounds = true
// Convert thumbView to UIImage
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: thumbView.bounds)
return renderer.image { rendererContext in
thumbView.layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(thumbView.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
// Set custom track height
// As seen here: https://stackoverflow.com/a/49428606/7235585
var newRect = super.trackRect(forBounds: bounds)
newRect.size.height = trackHeight
return newRect
}
}
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 am trying to set a collectionView with round images I get using KingFisher (image caching library).
I am not sure what I should set with round corner, the imageView or the cell itself. Even if the cell is round the image seems to fill the square.
So far I am using this code (If I dont set it after I change the image its square):
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
if let path = user.profilePictureURL {
if let url = URL(string: path) {
cell.profilePictureImageView.kf.setImage(with: url)
cell.profilePictureImageView.layer.cornerRadius = cell.profilePictureImageView.frame.width/2.0
}
}
And the ImageView :
class ProfilePicture : UIImageView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
func commonInit(){
self.layer.masksToBounds = false
self.layer.cornerRadius = self.layer.frame.width/2.0
self.clipsToBounds = true
}
}
But the first time it downloads the images , they are like this (cell background is blue)
You can use like this:
extension UIImageView {
func setRounded() {
let radius = CGRectGetWidth(self.frame) / 2
self.layer.cornerRadius = radius
self.layer.masksToBounds = true
}
}
then call the method as below:
imageView.setRounded()
You can get a circle by adjusting height (in portrait) instead of width:
self.layer.cornerRadius = self.layer.frame.height/2.0
you'll have to adjust your contentMode.scaleAspect if you are unhappy with the results. By adjusting on the long edge you can get a circle but you won't see the entire image.
You can get a circle in the image with a extension of UIView
extension UIImage {
var circleMask: UIImage {
let square = size.width < size.height ? CGSize(width: size.width, height: size.width) : CGSize(width: size.height, height: size.height)
let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
imageView.contentMode = UIView.ContentMode.scaleAspectFill
imageView.image = self
imageView.layer.cornerRadius = square.width/2
//imageView.layer.borderColor = UIColor.white.cgColor
//imageView.layer.borderWidth = 5
imageView.layer.masksToBounds = true
UIGraphicsBeginImageContext(imageView.bounds.size)
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result!
}}
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.