UIKit框架(四十四) —— CALayer的简单实用示例(二)

版本记录

版本号 时间
V1.0 2020.07.10 星期五

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
38. UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)
39. UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)
40. UIKit框架(四十) —— iOS 13中UISearchController 和 UISearchBar的新更改(二)
41. UIKit框架(四十一) —— 使用协议构建自定义Collection(一)
42. UIKit框架(四十二) —— 使用协议构建自定义Collection(二)
43. UIKit框架(四十三) —— CALayer的简单实用示例(一)

源码

1. Swift

首先看下工程组织结构

接着看下sb中的内容:

接着就是源码了

1. TrackBall.swift
import UIKit

func pow2(_ lhs: CGFloat) -> CGFloat {
  return pow(lhs, 2)
}

class TrackBall {
  let tolerance = 0.001

  var baseTransform = CATransform3DIdentity
  let trackBallRadius: CGFloat
  let trackBallCenter: CGPoint
  var trackBallStartPoint = (x: CGFloat(0.0), y: CGFloat(0.0), z: CGFloat(0.0))

  init(location: CGPoint, inRect bounds: CGRect) {
    if bounds.width > bounds.height {
      trackBallRadius = bounds.height * 0.5
    } else {
      trackBallRadius = bounds.width * 0.5
    }

    trackBallCenter = CGPoint(x: bounds.midX, y: bounds.midY)
    setStartPointFromLocation(location)
  }

  func setStartPointFromLocation(_ location: CGPoint) {
    trackBallStartPoint.x = location.x - trackBallCenter.x
    trackBallStartPoint.y = location.y - trackBallCenter.y
    let distance = pow2(trackBallStartPoint.x) + pow2(trackBallStartPoint.y)
    trackBallStartPoint.z = distance > pow2(trackBallRadius) ? CGFloat(0.0) : sqrt(pow2(trackBallRadius) - distance)
  }

  func finalizeTrackBallForLocation(_ location: CGPoint) {
    baseTransform = rotationTransformForLocation(location)
  }

  func rotationTransformForLocation(_ location: CGPoint) -> CATransform3D {
    var trackBallCurrentPoint = (x: location.x - trackBallCenter.x, y: location.y - trackBallCenter.y, z: CGFloat(0.0))
    let withinTolerance = fabs(Double(trackBallCurrentPoint.x - trackBallStartPoint.x)) < tolerance &&
      fabs(Double(trackBallCurrentPoint.y - trackBallStartPoint.y)) < tolerance

    if withinTolerance {
      return CATransform3DIdentity
    }

    let distance = pow2(trackBallCurrentPoint.x) + pow2(trackBallCurrentPoint.y)

    if distance > pow2(trackBallRadius) {
      trackBallCurrentPoint.z = 0.0
    } else {
      trackBallCurrentPoint.z = sqrt(pow2(trackBallRadius) - distance)
    }

    let startPoint = trackBallStartPoint
    let currentPoint = trackBallCurrentPoint
    let x = startPoint.y * currentPoint.z - startPoint.z * currentPoint.y
    let y = -startPoint.x * currentPoint.z + trackBallStartPoint.z * currentPoint.x
    let z = startPoint.x * currentPoint.y - startPoint.y * currentPoint.x
    var rotationVector = (x: x, y: y, z: z)

    let startLength = sqrt(Double(pow2(startPoint.x) + pow2(startPoint.y) + pow2(startPoint.z)))
    let currentLength = sqrt(Double(pow2(currentPoint.x) + pow2(currentPoint.y) + pow2(currentPoint.z)))
    let startDotCurrent = Double(
      startPoint.x * currentPoint.x +
        startPoint.y + currentPoint.y +
        startPoint.z + currentPoint.z)
    let rotationLength = sqrt(Double(pow2(rotationVector.x) + pow2(rotationVector.y) + pow2(rotationVector.z)))
    let angle = CGFloat(atan2(
      rotationLength / (startLength * currentLength),
      startDotCurrent / (startLength * currentLength)))

    let normalizer = CGFloat(rotationLength)
    rotationVector.x /= normalizer
    rotationVector.y /= normalizer
    rotationVector.z /= normalizer

    let rotationTransform = CATransform3DMakeRotation(angle, rotationVector.x, rotationVector.y, rotationVector.z)
    return CATransform3DConcat(baseTransform, rotationTransform)
  }
}
2. ClassDescription.swift
import Foundation

struct ClassDescription {
  let title: String
  let description: String
}
3. ScrollingView.swift
import UIKit
import QuartzCore

class ScrollingView: UIView {
  override class var layerClass: AnyClass {
    return CAScrollLayer.self
  }
}
4. AppDelegate.swift
import UIKit

let swiftOrangeColor = UIColor(red: 248 / 255.0, green: 96 / 255.0, blue: 47 / 255.0, alpha: 1.0)

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UILabel.appearance().font = UIFont(name: "Avenir-Light", size: 17.0)
    UILabel.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).font =
      UIFont(name: "Avenir-light", size: 14.0)
    UINavigationBar.appearance().tintColor = UIColor.white
    UINavigationBar.appearance().barTintColor = swiftOrangeColor
    UINavigationBar.appearance().titleTextAttributes = [
      NSAttributedString.Key.foregroundColor: UIColor.white,
      // swiftlint:disable:next force_unwrapping
      NSAttributedString.Key.font: UIFont(name: "Avenir-light", size: 17.0)!
    ]
    UITableView.appearance().separatorColor = swiftOrangeColor
    UITableViewCell.appearance().separatorInset = UIEdgeInsets.zero
    UISwitch.appearance().tintColor = swiftOrangeColor
    UISlider.appearance().tintColor = swiftOrangeColor
    UISegmentedControl.appearance().tintColor = swiftOrangeColor

    return true
  }
}
5. ClassListViewController.swift
import UIKit

class ClassListViewController: UITableViewController {
  let classes: [ClassDescription] = [
    ClassDescription(title: "CALayer", description: "Manage and animate visual content"),
    ClassDescription(title: "CAScrollLayer", description: "Display portion of a scrollable layer"),
    ClassDescription(title: "CATextLayer", description: "Render plain text or attributed strings"),
    ClassDescription(title: "AVPlayerLayer", description: "Display an AV player"),
    ClassDescription(title: "CAGradientLayer", description: "Apply a color gradient over the background"),
    ClassDescription(title: "CAReplicatorLayer", description: "Duplicate a source layer"),
    ClassDescription(title: "CAShapeLayer", description: "Draw using scalable vector paths"),
    ClassDescription(title: "CATransformLayer", description: "Draw 3D structures"),
    ClassDescription(title: "CAEmitterLayer", description: "Render animated particles")
  ]

  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.tableFooterView = UIView(frame: .zero)
  }
}

// MARK: - UITableViewDataSource
extension ClassListViewController {
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return classes.count
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ClassCell", for: indexPath)
    let classDescription = classes[indexPath.row]
    cell.textLabel?.text = classDescription.title
    cell.detailTextLabel?.text = classDescription.description
    cell.imageView?.image = UIImage(named: classDescription.title)
    return cell
  }
}

// MARK: - UITableViewDelegate
extension ClassListViewController {
  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let identifier = classes[indexPath.row].title
    performSegue(withIdentifier: identifier, sender: nil)
  }
}
6. CALayerViewController.swift
import UIKit

class CALayerViewController: UIViewController {
  @IBOutlet weak var viewForLayer: UIView!

  let layer = CALayer()

  // MARK: - View life cycle
  override func viewDidLoad() {
    super.viewDidLoad()
    setUpLayer()
    viewForLayer.layer.addSublayer(layer)
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "DisplayLayerControls" {
      (segue.destination as? CALayerControlsViewController)?.layerViewController = self
    }
  }
}

// MARK: - Layer
extension CALayerViewController {
  func setUpLayer() {
    //1
    layer.frame = viewForLayer.bounds
    layer.contents = UIImage(named: "star")?.cgImage

    // 2
    layer.contentsGravity = .center
    layer.magnificationFilter = .linear

    // 3
    layer.cornerRadius = 100.0
    layer.borderWidth = 12.0
    layer.borderColor = UIColor.white.cgColor
    layer.backgroundColor = swiftOrangeColor.cgColor

    //4
    layer.shadowOpacity = 0.75
    layer.shadowOffset = CGSize(width: 0, height: 3)
    layer.shadowRadius = 3.0
    layer.isGeometryFlipped = false
  }
}
7. CALayerControlsViewController.swift
import UIKit

class CALayerControlsViewController: UITableViewController {
  @IBOutlet weak var contentsGravityPickerValueLabel: UILabel!
  @IBOutlet weak var contentsGravityPicker: UIPickerView!
  @IBOutlet var switches: [UISwitch]!
  @IBOutlet var sliderValueLabels: [UILabel]!
  @IBOutlet var sliders: [UISlider]!
  @IBOutlet weak var borderColorSlidersValueLabel: UILabel!
  @IBOutlet var borderColorSliders: [UISlider]!
  @IBOutlet weak var backgroundColorSlidersValueLabel: UILabel!
  @IBOutlet var backgroundColorSliders: [UISlider]!
  @IBOutlet weak var shadowOffsetSlidersValueLabel: UILabel!
  @IBOutlet var shadowOffsetSliders: [UISlider]!
  @IBOutlet weak var shadowColorSlidersValueLabel: UILabel!
  @IBOutlet var shadowColorSliders: [UISlider]!
  @IBOutlet weak var magnificationFilterSegmentedControl: UISegmentedControl!

  enum Row: Int {
    case contentsGravity,
    contentsGravityPicker,
    displayContents,
    geometryFlipped,
    hidden,
    opacity,
    cornerRadius,
    borderWidth,
    borderColor,
    backgroundColor,
    shadowOpacity,
    shadowOffset,
    shadowRadius,
    shadowColor,
    magnificationFilter
  }
  enum Switch: Int {
    case displayContents, geometryFlipped, hidden
  }
  enum Slider: Int {
    case opacity, cornerRadius, borderWidth, shadowOpacity, shadowRadius
  }
  enum ColorSlider: Int {
    case red, green, blue
  }
  enum MagnificationFilter: Int {
    case linear, nearest, trilinear
  }

  // swiftlint:disable:next implicitly_unwrapped_optional
  weak var layerViewController: CALayerViewController!
  let contentsGravityValues: [CALayerContentsGravity] = [
    .center, .top, .bottom, .left, .right, .topLeft, .topRight,
    .bottomLeft, .bottomRight, .resize, .resizeAspect, .resizeAspectFill
  ]
  var contentsGravityPickerVisible = false

  override func viewDidLoad() {
    super.viewDidLoad()
    updateSliderValueLabels()
  }
}

// MARK: - IBActions
extension CALayerControlsViewController {
  @IBAction func switchChanged(_ sender: UISwitch) {
    let switchesArray = switches as NSArray
    // swiftlint:disable:next force_unwrapping
    let theSwitch = Switch(rawValue: switchesArray.index(of: sender))!

    switch theSwitch {
    case .displayContents:
      layerViewController.layer.contents = sender.isOn ? UIImage(named: "star")?.cgImage : nil
    case .geometryFlipped:
      layerViewController.layer.isGeometryFlipped = sender.isOn
    case .hidden:
      layerViewController.layer.isHidden = sender.isOn
    }
  }

  @IBAction func sliderChanged(_ sender: UISlider) {
    let slidersArray = sliders as NSArray
    // swiftlint:disable:next force_unwrapping
    let slider = Slider(rawValue: slidersArray.index(of: sender))!

    switch slider {
    case .opacity:
      layerViewController.layer.opacity = sender.value
    case .cornerRadius:
      layerViewController.layer.cornerRadius = CGFloat(sender.value)
    case .borderWidth:
      layerViewController.layer.borderWidth = CGFloat(sender.value)
    case .shadowOpacity:
      layerViewController.layer.shadowOpacity = sender.value
    case .shadowRadius:
      layerViewController.layer.shadowRadius = CGFloat(sender.value)
    }

    updateSliderValueLabel(slider)
  }

  @IBAction func borderColorSliderChanged(_ sender: UISlider) {
    let colorLabel = colorAndLabel(forSliders: borderColorSliders)
    layerViewController.layer.borderColor = colorLabel.color
    borderColorSlidersValueLabel.text = colorLabel.label
  }

  @IBAction func backgroundColorSliderChanged(_ sender: UISlider) {
    let colorLabel = colorAndLabel(forSliders: backgroundColorSliders)
    layerViewController.layer.backgroundColor = colorLabel.color
    backgroundColorSlidersValueLabel.text = colorLabel.label
  }

  @IBAction func shadowOffsetSliderChanged(_ sender: UISlider) {
    let width = CGFloat(shadowOffsetSliders[0].value)
    let height = CGFloat(shadowOffsetSliders[1].value)
    layerViewController.layer.shadowOffset = CGSize(width: width, height: height)
    shadowOffsetSlidersValueLabel.text = "Width: \(Int(width)), Height: \(Int(height))"
  }

  @IBAction func shadowColorSliderChanged(_ sender: UISlider) {
    let colorLabel = colorAndLabel(forSliders: shadowColorSliders)
    layerViewController.layer.shadowColor = colorLabel.color
    shadowColorSlidersValueLabel.text = colorLabel.label
  }

  @IBAction func magnificationFilterSegmentedControlChanged(_ sender: UISegmentedControl) {
    // swiftlint:disable:next force_unwrapping
    let filter = MagnificationFilter(rawValue: sender.selectedSegmentIndex)!
    let filterValue: CALayerContentsFilter

    switch filter {
    case .linear:
      filterValue = .linear
    case .nearest:
      filterValue = .nearest
    case .trilinear:
      filterValue = .trilinear
    }

    layerViewController.layer.magnificationFilter = filterValue
  }
}

// MARK: - Triggered actions
extension CALayerControlsViewController {
  func showContentsGravityPicker() {
    contentsGravityPickerVisible = true
    relayoutTableViewCells()
    let index = contentsGravityValues.firstIndex(of: layerViewController.layer.contentsGravity) ?? 0
    contentsGravityPicker.selectRow(index, inComponent: 0, animated: false)
    contentsGravityPicker.isHidden = false
    contentsGravityPicker.alpha = 0.0

    UIView.animate(withDuration: 0.25) {
      self.contentsGravityPicker.alpha = 1.0
    }
  }

  func hideContentsGravityPicker() {
    if contentsGravityPickerVisible {
      tableView.isUserInteractionEnabled = false
      contentsGravityPickerVisible = false
      relayoutTableViewCells()

      UIView.animate(
        withDuration: 0.25,
        animations: {
          self.contentsGravityPicker.alpha = 0.0
        }, completion: { _ in
          self.contentsGravityPicker.isHidden = true
          self.tableView.isUserInteractionEnabled = true
        })
    }
  }
}

// MARK: - Helpers
extension CALayerControlsViewController {
  func updateContentsGravityPickerValueLabel() {
    contentsGravityPickerValueLabel.text = layerViewController.layer.contentsGravity.rawValue
  }

  func updateSliderValueLabels() {
    for slider in Slider.opacity.rawValue...Slider.shadowRadius.rawValue {
      // swiftlint:disable:next force_unwrapping
      updateSliderValueLabel(Slider(rawValue: slider)!)
    }
  }

  func updateSliderValueLabel(_ sliderEnum: Slider) {
    let index = sliderEnum.rawValue
    let label = sliderValueLabels[index]
    let slider = sliders[index]

    switch sliderEnum {
    case .opacity, .shadowOpacity:
      label.text = String(format: "%.1f", slider.value)
    case .cornerRadius, .borderWidth, .shadowRadius:
      label.text = "\(Int(slider.value))"
    }
  }

  func colorAndLabel(forSliders sliders: [UISlider]) -> (color: CGColor, label: String) {
    let red = CGFloat(sliders[0].value)
    let green = CGFloat(sliders[1].value)
    let blue = CGFloat(sliders[2].value)
    let color = UIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: 1.0).cgColor
    let label = "RGB: \(Int(red)), \(Int(green)), \(Int(blue))"
    return (color: color, label: label)
  }

  func relayoutTableViewCells() {
    tableView.beginUpdates()
    tableView.endUpdates()
  }
}

// MARK: - UITableViewDelegate
extension CALayerControlsViewController {
  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // swiftlint:disable:next force_unwrapping
    let row = Row(rawValue: indexPath.row)!

    if row == .contentsGravityPicker {
      return contentsGravityPickerVisible ? 162.0 : 0.0
    } else {
      return 44.0
    }
  }

  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // swiftlint:disable:next force_unwrapping
    let row = Row(rawValue: indexPath.row)!

    switch row {
    case .contentsGravity where !contentsGravityPickerVisible:
      showContentsGravityPicker()
    default:
      hideContentsGravityPicker()
    }
  }
}

// MARK: - UIPickerViewDataSource
extension CALayerControlsViewController: UIPickerViewDataSource {
  func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
  }

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    return contentsGravityValues.count
  }
}

// MARK: - UIPickerViewDelegate
extension CALayerControlsViewController: UIPickerViewDelegate {
  func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    return contentsGravityValues[row].rawValue
  }

  func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    layerViewController.layer.contentsGravity = CALayerContentsGravity(rawValue: contentsGravityValues[row].rawValue)
    updateContentsGravityPickerValueLabel()
  }
}
8. CAScrollLayerViewController.swift
import UIKit

class CAScrollLayerViewController: UIViewController {
  @IBOutlet weak var scrollingView: ScrollingView!
  @IBOutlet weak var horizontalScrollingSwitch: UISwitch!
  @IBOutlet weak var verticalScrollingSwitch: UISwitch!

  var scrollingViewLayer: CAScrollLayer {
    // swiftlint:disable:next force_cast
    return scrollingView.layer as! CAScrollLayer
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    scrollingViewLayer.scrollMode = .both
  }
}

// MARK: - IBActions
extension CAScrollLayerViewController {
  @IBAction func panRecognized(_ sender: UIPanGestureRecognizer) {
    var newPoint = scrollingView.bounds.origin
    newPoint.x -= sender.translation(in: scrollingView).x
    newPoint.y -= sender.translation(in: scrollingView).y
    sender.setTranslation(.zero, in: scrollingView)
    scrollingViewLayer.scroll(to: newPoint)

    if sender.state == .ended {
      UIView.animate(withDuration: 0.3) {
        self.scrollingViewLayer.scroll(to: CGPoint.zero)
      }
    }
  }

  @IBAction func scrollingSwitchChanged(_ sender: UISwitch) {
    switch (horizontalScrollingSwitch.isOn, verticalScrollingSwitch.isOn) {
    case (true, true):
      scrollingViewLayer.scrollMode = .both
    case (true, false):
      scrollingViewLayer.scrollMode = .horizontally
    case (false, true):
      scrollingViewLayer.scrollMode = .vertically
    default:
      scrollingViewLayer.scrollMode = .none
    }
  }
}
9. CATextLayerViewController.swift
import UIKit

class CATextLayerViewController: UIViewController {
  @IBOutlet weak var viewForTextLayer: UIView!
  @IBOutlet weak var fontSizeSliderValueLabel: UILabel!
  @IBOutlet weak var fontSizeSlider: UISlider!
  @IBOutlet weak var wrapTextSwitch: UISwitch!
  @IBOutlet weak var alignmentModeSegmentedControl: UISegmentedControl!
  @IBOutlet weak var truncationModeSegmentedControl: UISegmentedControl!

  enum Font: Int {
    case helvetica, noteworthyLight
  }

  enum AlignmentMode: Int {
    case left, center, justified, right
  }
  enum TruncationMode: Int {
    case start, middle, end
  }

  private enum Constants {
    static let baseFontSize: CGFloat = 24.0
  }
  let noteworthyLightFont = CTFontCreateWithName("Noteworthy-Light" as CFString, Constants.baseFontSize, nil)
  let helveticaFont = CTFontCreateWithName("Helvetica" as CFString, Constants.baseFontSize, nil)
  let textLayer = CATextLayer()
  var previouslySelectedTruncationMode = TruncationMode.end

  override func viewDidLoad() {
    super.viewDidLoad()
    setUpTextLayer()
    viewForTextLayer.layer.addSublayer(textLayer)
  }

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    textLayer.frame = viewForTextLayer.bounds
  }
}

// MARK: - Layer setup
extension CATextLayerViewController {
  func setUpTextLayer() {
    textLayer.frame = viewForTextLayer.bounds

    let string = (1...20)
      .map { _ in
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum."
      }
      .joined(separator: " ")

    textLayer.string = string

    // 1
    textLayer.font = helveticaFont
    textLayer.fontSize = Constants.baseFontSize

    // 2
    textLayer.foregroundColor = UIColor.darkGray.cgColor
    textLayer.isWrapped = true
    textLayer.alignmentMode = .left
    textLayer.truncationMode = .end

    // 3
    textLayer.contentsScale = UIScreen.main.scale
  }
}

// MARK: - IBActions
extension CATextLayerViewController {
  @IBAction func fontSegmentedControlChanged(_ sender: UISegmentedControl) {
    switch sender.selectedSegmentIndex {
    case Font.helvetica.rawValue:
      textLayer.font = helveticaFont
    case Font.noteworthyLight.rawValue:
      textLayer.font = noteworthyLightFont
    default:
      break
    }
  }

  @IBAction func fontSizeSliderChanged(_ sender: UISlider) {
    fontSizeSliderValueLabel.text = "\(Int(sender.value * 100.0))%"
    textLayer.fontSize = Constants.baseFontSize * CGFloat(sender.value)
  }

  @IBAction func wrapTextSwitchChanged(_ sender: UISwitch) {
    alignmentModeSegmentedControl.selectedSegmentIndex = AlignmentMode.left.rawValue
    textLayer.alignmentMode = CATextLayerAlignmentMode.left

    if sender.isOn {
      if let truncationMode = TruncationMode(rawValue: truncationModeSegmentedControl.selectedSegmentIndex) {
        previouslySelectedTruncationMode = truncationMode
      }

      truncationModeSegmentedControl.selectedSegmentIndex = UISegmentedControl.noSegment
      textLayer.isWrapped = true
    } else {
      textLayer.isWrapped = false
      truncationModeSegmentedControl.selectedSegmentIndex = previouslySelectedTruncationMode.rawValue
    }
  }

  @IBAction func alignmentModeSegmentedControlChanged(_ sender: UISegmentedControl) {
    wrapTextSwitch.isOn = true
    textLayer.isWrapped = true
    truncationModeSegmentedControl.selectedSegmentIndex = UISegmentedControl.noSegment
    textLayer.truncationMode = CATextLayerTruncationMode.none

    switch sender.selectedSegmentIndex {
    case AlignmentMode.left.rawValue:
      textLayer.alignmentMode = .left
    case AlignmentMode.center.rawValue:
      textLayer.alignmentMode = .center
    case AlignmentMode.justified.rawValue:
      textLayer.alignmentMode = .justified
    case AlignmentMode.right.rawValue:
      textLayer.alignmentMode = .right
    default:
      textLayer.alignmentMode = .left
    }
  }

  @IBAction func truncationModeSegmentedControlChanged(_ sender: UISegmentedControl) {
    wrapTextSwitch.isOn = false
    textLayer.isWrapped = false
    alignmentModeSegmentedControl.selectedSegmentIndex = UISegmentedControl.noSegment
    textLayer.alignmentMode = .left

    switch sender.selectedSegmentIndex {
    case TruncationMode.start.rawValue:
      textLayer.truncationMode = .start
    case TruncationMode.middle.rawValue:
      textLayer.truncationMode = .middle
    case TruncationMode.end.rawValue:
      textLayer.truncationMode = .end
    default:
      textLayer.truncationMode = .none
    }
  }
}
10. AVPlayerLayerViewController.swift

import UIKit
import AVFoundation

class AVPlayerLayerViewController: UIViewController {
  @IBOutlet weak var viewForPlayerLayer: UIView!
  @IBOutlet weak var playButton: UIButton!
  @IBOutlet weak var rateSegmentedControl: UISegmentedControl!
  @IBOutlet weak var loopSwitch: UISwitch!
  @IBOutlet weak var volumeSlider: UISlider!

  enum Rate: Int {
    case slowForward, normal, fastForward
  }

  let playerLayer = AVPlayerLayer()
  var player: AVPlayer? {
    return playerLayer.player
  }
  var rate: Float {
    switch rateSegmentedControl.selectedSegmentIndex {
    case 0:
      return 0.5
    case 2:
      return 2.0
    default:
      return 1.0
    }
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    rateSegmentedControl.selectedSegmentIndex = 1
    setUpPlayerLayer()
    viewForPlayerLayer.layer.addSublayer(playerLayer)
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(AVPlayerLayerViewController.playerDidReachEndNotificationHandler(_:)),
      name: NSNotification.Name(rawValue: "AVPlayerItemDidPlayToEndTimeNotification"),
      object: player?.currentItem)
    playButton.setTitle("Pause", for: .normal)
  }
}

// MARK: - Layer setup
extension AVPlayerLayerViewController {
  func setUpPlayerLayer() {
    // 1
    playerLayer.frame = viewForPlayerLayer.bounds

    // 2
    // swiftlint:disable:next force_unwrapping
    let url = Bundle.main.url(forResource: "colorfulStreak", withExtension: "m4v")!
    let item = AVPlayerItem(asset: AVAsset(url: url))
    let player = AVPlayer(playerItem: item)

    // 3
    player.actionAtItemEnd = .none

    // 4
    player.volume = 1.0
    player.rate = 1.0

    playerLayer.player = player
  }
}

// MARK: - IBActions
extension AVPlayerLayerViewController {
  @IBAction func playButtonTapped(_ sender: UIButton) {
    if player?.rate == 0 {
      player?.rate = rate
      updatePlayButtonTitle(isPlaying: true)
    } else {
      player?.pause()
      updatePlayButtonTitle(isPlaying: false)
    }
  }

  @IBAction func rateSegmentedControlChanged(_ sender: UISegmentedControl) {
    player?.rate = rate
    updatePlayButtonTitle(isPlaying: true)
  }

  @IBAction func loopSwitchChanged(_ sender: UISwitch) {
    if sender.isOn {
      player?.actionAtItemEnd = .none
    } else {
      player?.actionAtItemEnd = .pause
    }
  }

  @IBAction func volumeSliderChanged(_ sender: UISlider) {
    player?.volume = sender.value
  }
}

// MARK: - Triggered actions
extension AVPlayerLayerViewController {
  @objc func playerDidReachEndNotificationHandler(_ notification: Notification) {
    // 1
    guard let playerItem = notification.object as? AVPlayerItem else { return }

    // 2
    playerItem.seek(to: .zero, completionHandler: nil)

    // 3
    if player?.actionAtItemEnd == .pause {
      player?.pause()
      updatePlayButtonTitle(isPlaying: false)
    }
  }
}

// MARK: - Helpers
extension AVPlayerLayerViewController {
  func updatePlayButtonTitle(isPlaying: Bool) {
    if isPlaying {
      playButton.setTitle("Pause", for: .normal)
    } else {
      playButton.setTitle("Play", for: .normal)
    }
  }
}
11. CAGradientLayerViewController.swift
import UIKit

class CAGradientLayerViewController: UIViewController {
  @IBOutlet weak var viewForGradientLayer: UIView!
  @IBOutlet weak var startPointSlider: UISlider!
  @IBOutlet weak var startPointSliderValueLabel: UILabel!
  @IBOutlet weak var endPointSlider: UISlider!
  @IBOutlet weak var endPointSliderValueLabel: UILabel!
  @IBOutlet var colorSwitches: [UISwitch]!
  @IBOutlet var locationSliders: [UISlider]!
  @IBOutlet var locationSliderValueLabels: [UILabel]!

  let gradientLayer = CAGradientLayer()
  let colors: [CGColor] = [
    UIColor(red: 209, green: 0, blue: 0),
    UIColor(red: 255, green: 102, blue: 34),
    UIColor(red: 255, green: 218, blue: 33),
    UIColor(red: 51, green: 221, blue: 0),
    UIColor(red: 17, green: 51, blue: 204),
    UIColor(red: 34, green: 0, blue: 102),
    UIColor(red: 51, green: 0, blue: 68)
  ].map { $0.cgColor }

  let locations: [Float] = [0, 1 / 6.0, 1 / 3.0, 0.5, 2 / 3.0, 5 / 6.0, 1.0]

  override func viewDidLoad() {
    super.viewDidLoad()
    sortOutletCollections()
    setUpGradientLayer()
    viewForGradientLayer.layer.addSublayer(gradientLayer)
    setUpLocationSliders()
    updateLocationSliderValueLabels()
  }
}

// MARK: - Setup layer
extension CAGradientLayerViewController {
  func sortOutletCollections() {
    colorSwitches.sortUIViewsInPlaceByTag()
    locationSliders.sortUIViewsInPlaceByTag()
    locationSliderValueLabels.sortUIViewsInPlaceByTag()
  }

  func setUpGradientLayer() {
    gradientLayer.frame = viewForGradientLayer.bounds
    gradientLayer.colors = colors
    gradientLayer.locations = locations.map { NSNumber(value: $0) }
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
  }

  func setUpLocationSliders() {
    guard let sliders = locationSliders else {
      return
    }

    for (index, slider) in sliders.enumerated() {
      slider.value = locations[index]
    }
  }
}

// MARK: - @IBActions
extension CAGradientLayerViewController {
  @IBAction func startPointSliderChanged(_ sender: UISlider) {
    gradientLayer.startPoint = CGPoint(x: CGFloat(sender.value), y: 0.0)
    updateStartAndEndPointValueLabels()
  }

  @IBAction func endPointSliderChanged(_ sender: UISlider) {
    gradientLayer.endPoint = CGPoint(x: CGFloat(sender.value), y: 1.0)
    updateStartAndEndPointValueLabels()
  }

  @IBAction func colorSwitchChanged(_ sender: UISwitch) {
    var gradientLayerColors: [AnyObject] = []
    var locations: [NSNumber] = []

    for (index, colorSwitch) in colorSwitches.enumerated() {
      let slider = locationSliders[index]

      if colorSwitch.isOn {
        gradientLayerColors.append(colors[index])
        locations.append(NSNumber(value: slider.value as Float))
        slider.isEnabled = true
      } else {
        slider.isEnabled = false
      }
    }

    if gradientLayerColors.count == 1 {
      gradientLayerColors.append(gradientLayerColors[0])
    }

    gradientLayer.colors = gradientLayerColors
    gradientLayer.locations = locations.count > 1 ? locations : nil
    updateLocationSliderValueLabels()
  }

  @IBAction func locationSliderChanged(_ sender: UISlider) {
    var gradientLayerLocations: [NSNumber] = []

    for (index, slider) in locationSliders.enumerated() {
      let colorSwitch = colorSwitches[index]

      if colorSwitch.isOn {
        gradientLayerLocations.append(NSNumber(value: slider.value as Float))
      }
    }

    gradientLayer.locations = gradientLayerLocations
    updateLocationSliderValueLabels()
  }
}

// MARK: - Triggered actions
extension CAGradientLayerViewController {
  func updateStartAndEndPointValueLabels() {
    startPointSliderValueLabel.text = String(format: "(%.1f, 0.0)", startPointSlider.value)
    endPointSliderValueLabel.text = String(format: "(%.1f, 1.0)", endPointSlider.value)
  }

  func updateLocationSliderValueLabels() {
    for (index, label) in locationSliderValueLabels.enumerated() {
      let colorSwitch = colorSwitches[index]

      if colorSwitch.isOn {
        let slider = locationSliders[index]
        label.text = String(format: "%.2f", slider.value)
        label.isEnabled = true
      } else {
        label.isEnabled = false
      }
    }
  }
}

// MARK: - Helpers
private extension UIColor {
  convenience init(red: Int, green: Int, blue: Int) {
    self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
  }
}
12. CAReplicatorLayerViewController.swift
import UIKit

class CAReplicatorLayerViewController: UIViewController {
  @IBOutlet weak var viewForReplicatorLayer: UIView!
  @IBOutlet weak var layerSizeSlider: UISlider!
  @IBOutlet weak var layerSizeSliderValueLabel: UILabel!
  @IBOutlet weak var instanceCountSlider: UISlider!
  @IBOutlet weak var instanceCountSliderValueLabel: UILabel!
  @IBOutlet weak var instanceDelaySlider: UISlider!
  @IBOutlet weak var instanceDelaySliderValueLabel: UILabel!
  @IBOutlet weak var offsetRedSwitch: UISwitch!
  @IBOutlet weak var offsetGreenSwitch: UISwitch!
  @IBOutlet weak var offsetBlueSwitch: UISwitch!
  @IBOutlet weak var offsetAlphaSwitch: UISwitch!

  let lengthMultiplier: CGFloat = 3.0
  let replicatorLayer = CAReplicatorLayer()
  let instanceLayer = CALayer()
  let fadeAnimation = CABasicAnimation(keyPath: "opacity")

  override func viewDidLoad() {
    super.viewDidLoad()
    setUpReplicatorLayer()
    setUpInstanceLayer()
    setUpLayerFadeAnimation()
    instanceDelaySliderChanged(instanceDelaySlider)
    updateLayerSizeSliderValueLabel()
    updateInstanceCountSliderValueLabel()
    updateInstanceDelaySliderValueLabel()
  }

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    setUpReplicatorLayer()
    setUpInstanceLayer()
  }
}

// MARK: - Layer setup
extension CAReplicatorLayerViewController {
  func setUpReplicatorLayer() {
    // 1
    replicatorLayer.frame = viewForReplicatorLayer.bounds

    // 2
    let count = instanceCountSlider.value
    replicatorLayer.instanceCount = Int(count)
    replicatorLayer.instanceDelay = CFTimeInterval(instanceDelaySlider.value / count)

    // 3
    replicatorLayer.instanceColor = UIColor.white.cgColor
    replicatorLayer.instanceRedOffset = offsetValueForSwitch(offsetRedSwitch)
    replicatorLayer.instanceGreenOffset = offsetValueForSwitch(offsetGreenSwitch)
    replicatorLayer.instanceBlueOffset = offsetValueForSwitch(offsetBlueSwitch)
    replicatorLayer.instanceAlphaOffset = offsetValueForSwitch(offsetAlphaSwitch)

    //4
    let angle = Float.pi * 2.0 / count
    replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)

    //5
    viewForReplicatorLayer.layer.addSublayer(replicatorLayer)
  }

  func setUpInstanceLayer() {
    let layerWidth = CGFloat(layerSizeSlider.value)
    let midX = viewForReplicatorLayer.bounds.midX - layerWidth / 2.0
    instanceLayer.frame = CGRect(
      x: midX,
      y: 0.0,
      width: layerWidth,
      height: layerWidth * lengthMultiplier)
    instanceLayer.backgroundColor = UIColor.white.cgColor
    replicatorLayer.addSublayer(instanceLayer)
  }

  func setUpLayerFadeAnimation() {
    fadeAnimation.fromValue = 1.0
    fadeAnimation.toValue = 0.0
    fadeAnimation.repeatCount = Float(Int.max)
  }
}

// MARK: - IBActions
extension CAReplicatorLayerViewController {
  @IBAction func layerSizeSliderChanged(_ sender: UISlider) {
    let value = CGFloat(sender.value)
    instanceLayer.bounds = CGRect(origin: .zero, size: CGSize(width: value, height: value * lengthMultiplier))
    updateLayerSizeSliderValueLabel()
  }

  @IBAction func instanceCountSliderChanged(_ sender: UISlider) {
    replicatorLayer.instanceCount = Int(sender.value)
    replicatorLayer.instanceAlphaOffset = offsetValueForSwitch(offsetAlphaSwitch)
    updateInstanceCountSliderValueLabel()
  }

  @IBAction func instanceDelaySliderChanged(_ sender: UISlider) {
    if sender.value > 0.0 {
      replicatorLayer.instanceDelay = CFTimeInterval(sender.value / Float(replicatorLayer.instanceCount))
      setLayerFadeAnimation()
    } else {
      replicatorLayer.instanceDelay = 0.0
      instanceLayer.opacity = 1.0
      instanceLayer.removeAllAnimations()
    }

    updateInstanceDelaySliderValueLabel()
  }

  @IBAction func offsetSwitchChanged(_ sender: UISwitch) {
    switch sender {
    case offsetRedSwitch:
      replicatorLayer.instanceRedOffset = offsetValueForSwitch(sender)
    case offsetGreenSwitch:
      replicatorLayer.instanceGreenOffset = offsetValueForSwitch(sender)
    case offsetBlueSwitch:
      replicatorLayer.instanceBlueOffset = offsetValueForSwitch(sender)
    case offsetAlphaSwitch:
      replicatorLayer.instanceAlphaOffset = offsetValueForSwitch(sender)
    default:
      break
    }
  }
}

// MARK: - Triggered actions
extension CAReplicatorLayerViewController {
  func setLayerFadeAnimation() {
    instanceLayer.opacity = 0.0
    fadeAnimation.duration = CFTimeInterval(instanceDelaySlider.value)
    instanceLayer.add(fadeAnimation, forKey: "FadeAnimation")
  }
}

// MARK: - Helpers
extension CAReplicatorLayerViewController {
  func offsetValueForSwitch(_ offsetSwitch: UISwitch) -> Float {
    if offsetSwitch == offsetAlphaSwitch {
      let count = Float(replicatorLayer.instanceCount)
      return offsetSwitch.isOn ? -1.0 / count : 0.0
    } else {
      return offsetSwitch.isOn ? 0.0 : -0.05
    }
  }

  func updateLayerSizeSliderValueLabel() {
    let value = layerSizeSlider.value
    layerSizeSliderValueLabel.text = String(format: "%.0f x %.0f", value, value * Float(lengthMultiplier))
  }

  func updateInstanceCountSliderValueLabel() {
    instanceCountSliderValueLabel.text = String(format: "%.0f", instanceCountSlider.value)
  }

  func updateInstanceDelaySliderValueLabel() {
    instanceDelaySliderValueLabel.text = String(format: "%.0f", instanceDelaySlider.value)
  }
}
13. CAShapeLayerViewController.swift
import UIKit

class CAShapeLayerViewController: UIViewController {
  @IBOutlet weak var viewForShapeLayer: UIView!
  @IBOutlet weak var hueSlider: UISlider!
  @IBOutlet weak var lineWidthSlider: UISlider!
  @IBOutlet weak var lineDashSwitch: UISwitch!
  @IBOutlet weak var lineCapSegmentedControl: UISegmentedControl!
  @IBOutlet weak var lineJoinSegmentedControl: UISegmentedControl!

  enum LineCap: Int {
    case butt, round, square, cap
  }
  enum LineJoin: Int {
    case miter, round, bevel
  }

  let shapeLayer = CAShapeLayer()
  var color = swiftOrangeColor
  let openPath = UIBezierPath()
  let closedPath = UIBezierPath()

  override func viewDidLoad() {
    super.viewDidLoad()
    setUpPath()
    setUpShapeLayer()
  }
}

// MARK: - Layer setup
extension CAShapeLayerViewController {
  func setUpPath() {
    openPath.move(to: CGPoint(x: 30, y: 196))

    openPath.addCurve(
      to: CGPoint(x: 112.0, y: 12.5),
      controlPoint1: CGPoint(x: 110.56, y: 13.79),
      controlPoint2: CGPoint(x: 112.07, y: 13.01))

    openPath.addCurve(
      to: CGPoint(x: 194, y: 196),
      controlPoint1: CGPoint(x: 111.9, y: 11.81),
      controlPoint2: CGPoint(x: 194, y: 196))

    openPath.addLine(to: CGPoint(x: 30.0, y: 85.68))
    openPath.addLine(to: CGPoint(x: 194.0, y: 48.91))
    openPath.addLine(to: CGPoint(x: 30, y: 196))
  }

  func setUpShapeLayer() {
    // 1
    shapeLayer.path = openPath.cgPath

    // 2
    shapeLayer.lineCap = .butt
    shapeLayer.lineJoin = .miter
    shapeLayer.miterLimit = 4.0

    // 3
    shapeLayer.lineWidth = CGFloat(lineWidthSlider.value)
    shapeLayer.strokeColor = swiftOrangeColor.cgColor
    shapeLayer.fillColor = UIColor.white.cgColor

    // 4
    shapeLayer.lineDashPattern = nil
    shapeLayer.lineDashPhase = 0.0

    viewForShapeLayer.layer.addSublayer(shapeLayer)
  }
}

// MARK: - IBActions
extension CAShapeLayerViewController {
  @IBAction func hueSliderChanged(_ sender: UISlider) {
    let hue = CGFloat(sender.value / 359.0)
    let color = UIColor(hue: hue, saturation: 0.81, brightness: 0.97, alpha: 1.0)
    shapeLayer.strokeColor = color.cgColor
    self.color = color
  }

  @IBAction func lineWidthSliderChanged(_ sender: UISlider) {
    shapeLayer.lineWidth = CGFloat(sender.value)
  }

  @IBAction func lineDashSwitchChanged(_ sender: UISwitch) {
    if sender.isOn {
      shapeLayer.lineDashPattern = [50, 50]
      shapeLayer.lineDashPhase = 25.0
    } else {
      shapeLayer.lineDashPattern = nil
      shapeLayer.lineDashPhase = 0
    }
  }

  @IBAction func lineCapSegmentedControlChanged(_ sender: UISegmentedControl) {
    shapeLayer.path = openPath.cgPath

    let lineCap: CAShapeLayerLineCap
    switch sender.selectedSegmentIndex {
    case LineCap.round.rawValue:
      lineCap = .round
    case LineCap.square.rawValue:
      lineCap = .square
    default:
      lineCap = .butt
    }

    shapeLayer.lineCap = lineCap
  }

  @IBAction func lineJoinSegmentedControlChanged(_ sender: UISegmentedControl) {
    let lineJoin: CAShapeLayerLineJoin

    switch sender.selectedSegmentIndex {
    case LineJoin.round.rawValue:
      lineJoin = .round
    case LineJoin.bevel.rawValue:
      lineJoin = .bevel
    default:
      lineJoin = .miter
    }

    shapeLayer.lineJoin = lineJoin
  }
}
14. CATransformLayerViewController.swift
import UIKit

func degreesToRadians(_ degrees: Double) -> CGFloat {
  return CGFloat(degrees * Double.pi / 180.0)
}

func radiansToDegrees(_ radians: Double) -> CGFloat {
  return CGFloat(radians / Double.pi * 180.0)
}

class CATransformLayerViewController: UIViewController {
  @IBOutlet weak var boxTappedLabel: UILabel!
  @IBOutlet weak var viewForTransformLayer: UIView!
  @IBOutlet var colorAlphaSwitches: [UISwitch]!

  enum Color: Int {
    case red, orange, yellow, green, blue, purple
  }
  let sideLength = CGFloat(160.0)
  let reducedAlpha = CGFloat(0.8)

  // swiftlint:disable:next implicitly_unwrapped_optional
  var transformLayer: CATransformLayer!
  let swipeMeTextLayer = CATextLayer()
  var redColor = UIColor.red
  var orangeColor = UIColor.orange
  var yellowColor = UIColor.yellow
  var greenColor = UIColor.green
  var blueColor = UIColor.blue
  var purpleColor = UIColor.purple
  var trackBall: TrackBall?

  override func viewDidLoad() {
    super.viewDidLoad()
    setUpSwipeMeTextLayer()
    buildCube()
    sortOutletCollections()
  }
}

// MARK: - Layer setup
extension CATransformLayerViewController {
  // swiftlint:disable:next function_body_length
  func buildCube() {
    // 1
    transformLayer = CATransformLayer()

    // 2
    let redLayer = sideLayer(color: redColor)
    redLayer.addSublayer(swipeMeTextLayer)
    transformLayer.addSublayer(redLayer)

    // 3
    let orangeLayer = sideLayer(color: orangeColor)
    var orangeTransform = CATransform3DMakeTranslation(
      sideLength / 2.0,
      0.0,
      sideLength / -2.0)
    orangeTransform = CATransform3DRotate(
      orangeTransform,
      degreesToRadians(90.0),
      0.0,
      1.0,
      0.0)
    orangeLayer.transform = orangeTransform
    transformLayer.addSublayer(orangeLayer)

    let yellowLayer = sideLayer(color: yellowColor)
    yellowLayer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)
    transformLayer.addSublayer(yellowLayer)

    let greenLayer = sideLayer(color: greenColor)
    var greenTransform = CATransform3DMakeTranslation(
      sideLength / -2.0,
      0.0,
      sideLength / -2.0)
    greenTransform = CATransform3DRotate(
      greenTransform,
      degreesToRadians(90.0),
      0.0,
      1.0,
      0.0)
    greenLayer.transform = greenTransform
    transformLayer.addSublayer(greenLayer)

    let blueLayer = sideLayer(color: blueColor)
    var blueTransform = CATransform3DMakeTranslation(
      0.0,
      sideLength / -2.0,
      sideLength / -2.0)
    blueTransform = CATransform3DRotate(
      blueTransform,
      degreesToRadians(90.0),
      1.0,
      0.0,
      0.0)
    blueLayer.transform = blueTransform
    transformLayer.addSublayer(blueLayer)

    let purpleLayer = sideLayer(color: purpleColor)
    var purpleTransform = CATransform3DMakeTranslation(
      0.0,
      sideLength / 2.0,
      sideLength / -2.0)
    purpleTransform = CATransform3DRotate(
      purpleTransform,
      degreesToRadians(90.0),
      1.0,
      0.0,
      0.0)
    purpleLayer.transform = purpleTransform
    transformLayer.addSublayer(purpleLayer)

    transformLayer.anchorPointZ = sideLength / -2.0
    viewForTransformLayer.layer.addSublayer(transformLayer)
  }

  func setUpSwipeMeTextLayer() {
    swipeMeTextLayer.frame = CGRect(x: 0.0, y: sideLength / 4.0, width: sideLength, height: sideLength / 2.0)
    swipeMeTextLayer.string = "Swipe Me"
    swipeMeTextLayer.alignmentMode = CATextLayerAlignmentMode.center
    swipeMeTextLayer.foregroundColor = UIColor.white.cgColor
    let fontName = "Noteworthy-Light" as CFString
    let fontRef = CTFontCreateWithName(fontName, 24.0, nil)
    swipeMeTextLayer.font = fontRef
    swipeMeTextLayer.contentsScale = UIScreen.main.scale
  }
}

// MARK: - IBActions
extension CATransformLayerViewController {
  @IBAction func colorAlphaSwitchChanged(_ sender: UISwitch) {
    let alpha = sender.isOn ? reducedAlpha : 1.0

    switch (colorAlphaSwitches as NSArray).index(of: sender) {
    case Color.red.rawValue:
      redColor = colorForColor(redColor, withAlpha: alpha)
    case Color.orange.rawValue:
      orangeColor = colorForColor(orangeColor, withAlpha: alpha)
    case Color.yellow.rawValue:
      yellowColor = colorForColor(yellowColor, withAlpha: alpha)
    case Color.green.rawValue:
      greenColor = colorForColor(greenColor, withAlpha: alpha)
    case Color.blue.rawValue:
      blueColor = colorForColor(blueColor, withAlpha: alpha)
    case Color.purple.rawValue:
      purpleColor = colorForColor(purpleColor, withAlpha: alpha)
    default:
      break
    }

    transformLayer.removeFromSuperlayer()
    buildCube()
  }
}

// MARK: - Touch Handling
extension CATransformLayerViewController {
  override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    if let location = touches.first?.location(in: viewForTransformLayer) {
      if trackBall != nil {
        trackBall?.setStartPointFromLocation(location)
      } else {
        trackBall = TrackBall(location: location, inRect: viewForTransformLayer.bounds)
      }

      guard let layers = transformLayer.sublayers else {
        return
      }

      for layer in layers {
        if layer.hitTest(location) != nil {
          showBoxTappedLabel()
          break
        }
      }
    }
  }

  override func touchesMoved(_ touches: Set, with event: UIEvent?) {
    if let location = touches.first?.location(in: viewForTransformLayer) {
      if let transform = trackBall?.rotationTransformForLocation(location) {
        viewForTransformLayer.layer.sublayerTransform = transform
      }
    }
  }

  override func touchesEnded(_ touches: Set, with event: UIEvent?) {
    if let location = touches.first?.location(in: viewForTransformLayer) {
      trackBall?.finalizeTrackBallForLocation(location)
    }
  }

  func showBoxTappedLabel() {
    boxTappedLabel.alpha = 1.0
    boxTappedLabel.isHidden = false

    UIView.animate(
      withDuration: 0.5,
      animations: {
        self.boxTappedLabel.alpha = 0.0
      }, completion: { _ in
        self.boxTappedLabel.isHidden = true
      })
  }
}

// MARK: - Helpers
extension CATransformLayerViewController {
  func sideLayer(color: UIColor) -> CALayer {
    let layer = CALayer()
    layer.frame = CGRect(origin: .zero, size: CGSize(width: sideLength, height: sideLength))
    layer.position = CGPoint(x: viewForTransformLayer.bounds.midX, y: viewForTransformLayer.bounds.midY)
    layer.backgroundColor = color.cgColor
    return layer
  }

  func colorForColor(_ color: UIColor, withAlpha newAlpha: CGFloat) -> UIColor {
    var color = color
    var red = CGFloat()
    var green = red, blue = red, alpha = red

    if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
      color = UIColor(red: red, green: green, blue: blue, alpha: newAlpha)
    }

    return color
  }

  func sortOutletCollections() {
    colorAlphaSwitches.sortUIViewsInPlaceByTag()
  }
}
15. CAEmitterLayerViewController.swift
import UIKit

class CAEmitterLayerViewController: UIViewController {
  @IBOutlet weak var viewForEmitterLayer: UIView!

  @objc var emitterLayer = CAEmitterLayer()
  @objc var emitterCell = CAEmitterCell()

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    setUpEmitterCell()
    setUpEmitterLayer()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "DisplayEmitterControls" {
      (segue.destination as? CAEmitterLayerControlsViewController)?.emitterLayerViewController = self
    }
  }
}

// MARK: - Layer setup
extension CAEmitterLayerViewController {
  func setUpEmitterLayer() {
    // 1
    resetEmitterCells()
    emitterLayer.frame = viewForEmitterLayer.bounds
    viewForEmitterLayer.layer.addSublayer(emitterLayer)

    // 2
    emitterLayer.seed = UInt32(Date().timeIntervalSince1970)

    // 3
    emitterLayer.emitterPosition = CGPoint(x: viewForEmitterLayer.bounds.midX * 1.5, y: viewForEmitterLayer.bounds.midY)

    // 4
    emitterLayer.renderMode = .additive
  }

  func setUpEmitterCell() {
    // 1
    emitterCell.contents = UIImage(named: "smallStar")?.cgImage

    // 2
    emitterCell.velocity = 50.0
    emitterCell.velocityRange = 500.0

    // 3
    emitterCell.color = UIColor.black.cgColor

    // 4
    emitterCell.redRange = 1.0
    emitterCell.greenRange = 1.0
    emitterCell.blueRange = 1.0
    emitterCell.alphaRange = 0.0
    emitterCell.redSpeed = 0.0
    emitterCell.greenSpeed = 0.0
    emitterCell.blueSpeed = 0.0
    emitterCell.alphaSpeed = -0.5
    emitterCell.scaleSpeed = 0.1

    // 5
    let zeroDegreesInRadians = degreesToRadians(0.0)
    emitterCell.spin = degreesToRadians(130.0)
    emitterCell.spinRange = zeroDegreesInRadians
    emitterCell.emissionLatitude = zeroDegreesInRadians
    emitterCell.emissionLongitude = zeroDegreesInRadians
    emitterCell.emissionRange = degreesToRadians(360.0)

    // 6
    emitterCell.lifetime = 1.0
    emitterCell.birthRate = 250.0

    // 7
    emitterCell.xAcceleration = -800
    emitterCell.yAcceleration = 1000
  }

  func resetEmitterCells() {
    emitterLayer.emitterCells = nil
    emitterLayer.emitterCells = [emitterCell]
  }
}

// MARK: - Triggered actions
extension CAEmitterLayerViewController {
  override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    if let location = touches.first?.location(in: viewForEmitterLayer) {
      emitterLayer.emitterPosition = location
    }
  }

  override func touchesMoved(_ touches: Set, with event: UIEvent?) {
    if let location = touches.first?.location(in: viewForEmitterLayer) {
      emitterLayer.emitterPosition = location
    }
  }
}
16. CAEmitterLayerControlsViewController.swift
import UIKit

class CAEmitterLayerControlsViewController: UITableViewController {
  @IBOutlet weak var renderModePickerValueLabel: UILabel!
  @IBOutlet weak var renderModePicker: UIPickerView!
  @IBOutlet var sliderValueLabels: [UILabel]!
  @IBOutlet weak var enabledSwitch: UISwitch!
  @IBOutlet var sliders: [UISlider]!

  enum Section: Int {
    case emitterLayer, emitterCell
  }

  enum Slider: Int {
    case color,
    redRange,
    greenRange,
    blueRange,
    alphaRange,
    redSpeed,
    greenSpeed,
    blueSpeed,
    alphaSpeed,
    scale,
    scaleRange,
    spin,
    spinRange,
    emissionLatitude,
    emissionLongitude,
    emissionRange,
    lifetime,
    lifetimeRange,
    birthRate,
    velocity,
    velocityRange,
    xAcceleration,
    yAcceleration
  }

  // swiftlint:disable:next implicitly_unwrapped_optional
  weak var emitterLayerViewController: CAEmitterLayerViewController!
  var emitterLayer: CAEmitterLayer {
    return emitterLayerViewController.emitterLayer
  }
  var emitterCell: CAEmitterCell {
    return emitterLayerViewController.emitterCell
  }
  let emitterLayerRenderModes: [CAEmitterLayerRenderMode] = [
    .unordered,
    .oldestFirst,
    .oldestLast,
    .backToFront,
    .additive
  ]
  var renderModePickerVisible = false

  override func viewDidLoad() {
    super.viewDidLoad()
    updateSliderValueLabels()
  }
}

// MARK: - IBActions
extension CAEmitterLayerControlsViewController {
  @IBAction func enabledSwitchChanged(_ sender: UISwitch) {
    emitterLayerViewController.emitterCell.isEnabled = sender.isOn
    emitterLayerViewController.resetEmitterCells()
  }

  // swiftlint:disable:next cyclomatic_complexity
  @IBAction func sliderChanged(_ sender: UISlider) {
    let slidersArray = sliders as NSArray
    // swiftlint:disable:next force_unwrapping
    let slider = Slider(rawValue: slidersArray.index(of: sender))!
    var keyPath = "emitterCell."

    switch slider {
    case .color: keyPath += "color"
    case .redRange: keyPath += "redRange"
    case .greenRange: keyPath += "greenRange"
    case .blueRange: keyPath += "blueRange"
    case .alphaRange: keyPath += "alphaRange"
    case .redSpeed: keyPath += "redSpeed"
    case .greenSpeed: keyPath += "greenSpeed"
    case .blueSpeed: keyPath += "blueSpeed"
    case .alphaSpeed: keyPath += "alphaSpeed"
    case .scale: keyPath  += "scale"
    case .scaleRange: keyPath += "scaleRange"
    case .spin: keyPath += "spin"
    case .spinRange: keyPath += "spinRange"
    case .emissionLatitude: keyPath += "emissionLatitude"
    case .emissionLongitude: keyPath += "emissionLongitude"
    case .emissionRange: keyPath += "emissionRange"
    case .lifetime: keyPath += "lifetime"
    case .lifetimeRange: keyPath += "lifetimeRange"
    case .birthRate: keyPath += "birthRate"
    case .velocity: keyPath += "velocity"
    case .velocityRange: keyPath += "velocityRange"
    case .xAcceleration: keyPath += "xAcceleration"
    case .yAcceleration: keyPath += "yAcceleration"
    }

    if keyPath == "emitterCell.color" {
      let color = UIColor(hue: 0.0, saturation: 0.0, brightness: CGFloat(sender.value), alpha: 1.0)
      emitterLayerViewController.emitterCell.color = color.cgColor
      emitterLayerViewController.resetEmitterCells()
    } else {
      emitterLayerViewController.setValue(NSNumber(value: sender.value as Float), forKeyPath: keyPath)
      emitterLayerViewController.resetEmitterCells()
    }

    updateSliderValueLabel(slider)
  }
}

// MARK: - Triggered actions
extension CAEmitterLayerControlsViewController {
  func showEmitterLayerRenderModePicker() {
    renderModePickerVisible = true
    relayoutTableViewCells()
    let index = emitterLayerRenderModes.firstIndex(of: emitterLayer.renderMode) ?? 0
    renderModePicker.selectRow(index, inComponent: 0, animated: false)
    renderModePicker.isHidden = false
    renderModePicker.alpha = 0.0

    UIView.animate(withDuration: 0.25) {
      self.renderModePicker.alpha = 1.0
    }
  }

  func hideEmitterLayerRenderModePicker() {
    if renderModePickerVisible {
      view.isUserInteractionEnabled = false
      renderModePickerVisible = false
      relayoutTableViewCells()

      UIView.animate(
        withDuration: 0.25,
        animations: {
          self.renderModePicker.alpha = 0.0
        },
        completion: { _ in
          self.renderModePicker.isHidden = true
          self.view.isUserInteractionEnabled = true
        })
    }
  }
}

// MARK: - Helpers
extension CAEmitterLayerControlsViewController {
  func updateSliderValueLabels() {
    for slider in Slider.color.rawValue...Slider.yAcceleration.rawValue {
      // swiftlint:disable:next force_unwrapping
      updateSliderValueLabel(Slider(rawValue: slider)!)
    }
  }

  func updateSliderValueLabel(_ sliderEnum: Slider) {
    let index = sliderEnum.rawValue
    let label = sliderValueLabels[index]
    let slider = sliders[index]

    switch sliderEnum {
    case
    .redRange,
    .greenRange,
    .blueRange,
    .alphaRange,
    .redSpeed,
    .greenSpeed,
    .blueSpeed,
    .alphaSpeed,
    .scale,
    .scaleRange,
    .lifetime,
    .lifetimeRange:
      label.text = String(format: "%.1f", slider.value)
    case .color:
      label.text = String(format: "%.0f", slider.value * 100.0)
    case .spin, .spinRange, .emissionLatitude, .emissionLongitude, .emissionRange:
      let formatter = NumberFormatter()
      formatter.minimumFractionDigits = 0
      label.text = "\(Int(radiansToDegrees(Double(slider.value))))"
    case .birthRate, .velocity, .velocityRange, .xAcceleration, .yAcceleration:
      label.text = String(format: "%.0f", slider.value)
    }
  }

  func relayoutTableViewCells() {
    tableView.beginUpdates()
    tableView.endUpdates()
  }
}

// MARK: - UITableViewDelegate
extension CAEmitterLayerControlsViewController {
  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // swiftlint:disable:next force_unwrapping
    let section = Section(rawValue: indexPath.section)!

    if section == .emitterLayer && indexPath.row == 1 {
      return renderModePickerVisible ? 162.0 : 0.0
    } else {
      return 44.0
    }
  }

  override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // swiftlint:disable:next force_unwrapping
    let section = Section(rawValue: indexPath.section)!

    switch section {
    case .emitterLayer where !renderModePickerVisible:
      showEmitterLayerRenderModePicker()
    default:
      hideEmitterLayerRenderModePicker()
    }
  }
}

// MARK: - UIPickerViewDataSource
extension CAEmitterLayerControlsViewController: UIPickerViewDataSource {
  func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
  }

  func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    return emitterLayerRenderModes.count
  }
}

// MARK: - UIPickerViewDelegate
extension CAEmitterLayerControlsViewController: UIPickerViewDelegate {
  func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    return emitterLayerRenderModes[row].rawValue
  }

  func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
    emitterLayerViewController.emitterLayer.renderMode = emitterLayerRenderModes[row]
    renderModePickerValueLabel.text = emitterLayer.renderMode.rawValue
  }
}
17. Array+SortUIViewsInPlaceByTag.swift
import UIKit

extension Array where Element: UIView {
///    Sorts an array of UIViews or subclasses by tag. For example, this is useful when working with `IBOutletCollection`s, whose order of elements can be changed when manipulating the view objects in Interface Builder. Just tag your views in Interface Builder and then call this method on your `IBOutletCollection`s in `viewDidLoad()`.
///  - author: Scott Gardner
///  - seealso:
///  * [Source on GitHub](http://bit.ly/SortUIViewsInPlaceByTag)
  mutating func sortUIViewsInPlaceByTag() {
    sort { (left: Element, right: Element) in
      left.tag < right.tag
    }
  }
}
18. UITableViewCell+ZeroLayoutMargins.swift
import UIKit

extension UITableViewCell {
  override open var layoutMargins: UIEdgeInsets {
    get { return UIEdgeInsets.zero }
    // swiftlint:disable:next unused_setter_value
    set { }
  }
}

后记

本篇主要讲述了CALayer的简单实用示例,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(UIKit框架(四十四) —— CALayer的简单实用示例(二))