iOS. UI. Techniques. Part 1

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- . , !








All Articles