Hello to the readers of Habr!
I am an iOS developer, and it so happened that I had to do a lot in ui: custom views, shadows, layouts, buttons, and that's all. In this and a couple of the following articles, I want to share some techniques that helped me achieve very beautiful and interesting effects in terms of drawing ui components. Hope it will be helpful to someone. Or just wondering.
Small introduction
I donβt presume to speak for everyone, but based on personal experience, I got the impression that for a sufficiently large number of developers, drawing some kind of "dice" with non-standard shape and behavior is an extremely undesirable task. Someone is more in architecture, someone is more about "doing business well" with minimal effort (accordingly, they ask designers to moderate their ardor), etc. And if you really have to do something out of the ordinary, then google, stackoverflow, experiments, etc. begin, which takes a lot of time, and you get the feeling that it is not worth it at all. Actually, I conceived this small series of articles as some kind of help, the reading of which will remove a number of questions and allow you to quickly evaluate / implement atypical ui components. Using specific examples, I will try to demonstrate how, what and why you can do it.
Example 1: view with custom border and shadow
In this case, the idea is simple: add another layer to the layer hierarchy of our view, cut the borders of this layer, and make the shape of the shadow (already at the view itself) exactly the same as the shape of the layer border.
Now a little more detail. CALayer has a mask property . You can read in the documentation that this is the same optional CALayer, and if it is not nil, then its alpha channel is used as a mask for the content of the original layer. That is, if you take a png image with a cat and transparency and somehow put it into a CALayer (let's call it catLayer), then when you assign layer.mask = catLayer layer , . , , layer . layer- . CAShapeLayer - CALayer, , , path. shapeLayer , , , shapeLayer.path, alpha = 0.
, UIBezierPath:
addLine(to:), move(to:), addArc(withCenter:radius:startAngle:endAngle:clockwise) ..
. path , ", ": , , . . addArc, startAngle endAngle, clockwise. clockwise , , , , . -Ο/2 0 clockwise true, view:
? ?
, , , , . mask view, .
, , CALayer shadowPath.
1
import UIKit
final class SimpleCustomBorderAndShadowView: UIView {
private let frontLayer = CALayer()
private let inset: CGFloat = 40
// MARK: Override
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
frontLayer.frame = bounds
let maskAndShadowPath = UIBezierPath()
maskAndShadowPath.move(to: CGPoint(x: 0, y: inset))
maskAndShadowPath.addLine(to: CGPoint(x: inset, y: 0))
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: 0))
maskAndShadowPath.addArc(withCenter: CGPoint(x: bounds.width - inset, y: inset),
radius: inset,
startAngle: -CGFloat.pi / 2,
endAngle: 0,
clockwise: true)
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - inset))
maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: bounds.height))
maskAndShadowPath.addLine(to: CGPoint(x: inset, y: bounds.height))
maskAndShadowPath.addArc(withCenter: CGPoint(x: inset, y: bounds.height - inset),
radius: inset,
startAngle: CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true)
maskAndShadowPath.close()
(frontLayer.mask as? CAShapeLayer)?.frame = bounds
(frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath
layer.shadowPath = maskAndShadowPath.cgPath
}
// MARK: Setup
private func setup() {
backgroundColor = .clear
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
frontLayer.mask = CAShapeLayer()
frontLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(frontLayer)
}
}
2: view
, : - , .
, - , , , UIBezierPath. , . , , path , view, UIBezierPath(roundedRect:cornerRadius:), , .
addQuadCurve(to:controlPoint:). UIBezierPath, addQuadCurve, lineWidth, path ... . - , : CoreGraphics - , - counter- , . , CGPath copy(strokingWithWidth:lineCap:lineJoin:miterLimit:). CGPath, , UIBezierPath, cgPath.
, , , , .
2
import UIKit
final class ErasedPathView: UIView {
private let frontLayer = CAShapeLayer()
// MARK: Override
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
frontLayer.frame = bounds
let maskAndShadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 20)
let curvePath = UIBezierPath()
curvePath.move(to: CGPoint(x: bounds.width / 4, y: bounds.height / 4))
curvePath.addQuadCurve(to: CGPoint(x: bounds.width * 3 / 4, y: bounds.height * 3 / 4),
controlPoint: CGPoint(x: bounds.width, y: 0))
let innerPath = UIBezierPath(cgPath: curvePath.cgPath.copy(strokingWithWidth: 70, lineCap: .round, lineJoin: .round, miterLimit: 0))
maskAndShadowPath.append(innerPath)
(frontLayer.mask as? CAShapeLayer)?.frame = bounds
(frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath
layer.shadowPath = maskAndShadowPath.cgPath
}
// MARK: Setup
private func setup() {
backgroundColor = .clear
frontLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(frontLayer)
let mask = CAShapeLayer()
mask.fillRule = .evenOdd
frontLayer.mask = mask
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
3: view
, view , , , CAShapeLayer. override layerClass view, ShapeLayer.self, 1 path.
, . . β path. CoreGraphics , . fillRule CAShapeLayer . , ( ) : , .
import UIKit
final class SpadeCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
let alpha = atan(2 * radius / size)
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))
path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2),
radius: radius, startAngle: 0,
endAngle: CGFloat.pi + 2 * alpha,
clockwise: true)
path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2))
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2),
radius: radius,
startAngle: -2 * alpha,
endAngle: CGFloat.pi,
clockwise: true)
path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.black.cgColor
selfLayer.strokeColor = UIColor.black.cgColor
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 10
layer.shadowOpacity = 1
}
}
import UIKit
final class DiamondCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
private let adjustment: CGFloat = 10
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
path.move(to: CGPoint(x: inset, y: bounds.height / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2),
controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 - adjustment))
path.addQuadCurve(to: CGPoint(x: bounds.width - inset, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 - adjustment))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 + adjustment))
path.addQuadCurve(to: CGPoint(x: inset, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 + adjustment))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.red.cgColor
selfLayer.strokeColor = UIColor.red.cgColor
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
import UIKit
final class ClubCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
private let adjustment: CGFloat = 10
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))
path.addArc(withCenter: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + adjustment),
radius: radius,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - radius),
radius: radius,
startAngle: CGFloat.pi / 2,
endAngle: 5 * CGFloat.pi / 2,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + adjustment),
radius: radius,
startAngle: CGFloat.pi,
endAngle: 3 * CGFloat.pi,
clockwise: true)
path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))
path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),
controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.black.cgColor
selfLayer.strokeColor = UIColor.black.cgColor
selfLayer.fillRule = .nonZero
selfLayer.lineWidth = 2
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
import UIKit
final class HeartCardView: UIView {
var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
private let inset: CGFloat = 20
// MARK: Override
static override var layerClass: AnyClass { CAShapeLayer.self }
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
let size = bounds.width - 2 * inset
let radius = size / 4
let alpha = atan(4 * radius / (3 * size))
path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2 - radius),
radius: radius,
startAngle: CGFloat.pi - 2 * alpha,
endAngle: 0,
clockwise: true)
path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 - radius),
radius: radius,
startAngle: -CGFloat.pi,
endAngle: 2 * alpha,
clockwise: true)
path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))
selfLayer.path = path.cgPath
}
// MARK: Setup
private func setup() {
selfLayer.fillColor = UIColor.red.cgColor
selfLayer.strokeColor = UIColor.red.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 20
layer.shadowOpacity = 1
}
}
, , things to remember:
+1 CALayer, mask, CAShapeLayer, shadowPath β
copy(strokingWithWidth:lineCap:lineJoin:miterLimit:) β path
CAShapeLayer, path + fillRule β
layout- . , !