美文网首页
UIKit框架(四十四) —— CALayer的简单实用示例(二

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

作者: 刀客传奇 | 来源:发表于2020-07-10 17:09 被阅读0次

    版本记录

    版本号 时间
    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<UITouch>, 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<UITouch>, 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<UITouch>, 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<UITouch>, with event: UIEvent?) {
        if let location = touches.first?.location(in: viewForEmitterLayer) {
          emitterLayer.emitterPosition = location
        }
      }
    
      override func touchesMoved(_ touches: Set<UITouch>, 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的简单实用示例(二

          本文链接:https://www.haomeiwen.com/subject/kzvmcktx.html