UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)

181 阅读4分钟
原文链接: www.jianshu.com

版本记录

版本号 时间
V1.0 2019.05.16 星期四

前言

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边栏动画的实现(一)

源码

1. Swift

首先看下代码组织结构

接着看下sb中的内容

下面就是源码了

1. UIColor+ArrayInstantiation.swift
import UIKit

extension UIColor {
  convenience init(colorArray array: [CGFloat]) {
    var source = array
    if array.count != 3 {
      source = [255, 255, 255]
    }

    let red = source[0]/255
    let green = source[1]/255
    let blue = source[2]/255
    self.init(red: red, green: green, blue: blue, alpha: 1.0)
  }
}
2. CGFloat+Clamp.swift
import UIKit

extension CGFloat {
  func unitClamp() -> CGFloat {
    return Swift.min(Swift.max(0, self), 1.0)
  }
}
3. UINavigationBar+StatusBar.swift
import UIKit

extension UINavigationController {
  open override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  }
}
4. RootViewController.swift
import UIKit

extension UIView {
  func embedInsideSafeArea(_ subview: UIView) {
    addSubview(subview)
    subview.translatesAutoresizingMaskIntoConstraints = false
    subview.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor)
      .isActive = true
    subview.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)
      .isActive = true
    subview.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)
      .isActive = true
    subview.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
      .isActive = true
  }
}

class RootViewController: UIViewController {
  let menuWidth: CGFloat = 80.0
  lazy var threshold = menuWidth/2.0

  var menuContainer = UIView(frame: .zero)
  var detailContainer = UIView(frame: .zero)
  var menuViewController: MenuViewController?
  var detailViewController: DetailViewController?
  var hamburgerView: HamburgerView?

  // 1
  lazy var scroller: UIScrollView = {
    let scroller = UIScrollView(frame: .zero)
    scroller.isPagingEnabled = true
    scroller.delaysContentTouches = false
    scroller.bounces = false
    scroller.showsHorizontalScrollIndicator = false
    scroller.delegate = self
    return scroller
  }()

  // 2
  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor(named: "rw-dark")
    view.embedInsideSafeArea(scroller)
    installMenuContainer()
    installDetailContainer()
    menuViewController
      = installFromStoryboard("MenuViewController",
                              into: menuContainer) as? MenuViewController
    detailViewController
      =  installFromStoryboard("DetailViewController",
                               into: detailContainer) as? DetailViewController
    menuViewController?.delegate = self
    if let detailViewController = detailViewController {
      installBurger(in: detailViewController)
    }
    hamburgerView?.setFractionOpen(1.0)
  }

  // 3
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  }

  func installMenuContainer() {
    // 1
    scroller.addSubview(menuContainer)
    menuContainer.translatesAutoresizingMaskIntoConstraints = false
    menuContainer.backgroundColor = .orange

    // 2
    menuContainer.leadingAnchor.constraint(equalTo: scroller.leadingAnchor)
      .isActive = true
    menuContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
      .isActive = true
    menuContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
      .isActive = true

    // 3
    menuContainer.widthAnchor.constraint(equalToConstant: menuWidth)
      .isActive = true
    menuContainer.heightAnchor.constraint(equalTo: scroller.heightAnchor)
      .isActive = true
  }

  func installDetailContainer() {
    //1
    scroller.addSubview(detailContainer)
    detailContainer.translatesAutoresizingMaskIntoConstraints = false
    detailContainer.backgroundColor = .red

    //2
    detailContainer.trailingAnchor.constraint(equalTo: scroller.trailingAnchor)
      .isActive = true
    detailContainer.topAnchor.constraint(equalTo: scroller.topAnchor)
      .isActive = true
    detailContainer.bottomAnchor.constraint(equalTo: scroller.bottomAnchor)
      .isActive = true

    //3
    detailContainer.leadingAnchor
      .constraint(equalTo: menuContainer.trailingAnchor)
      .isActive = true
    detailContainer.widthAnchor.constraint(equalTo: scroller.widthAnchor)
      .isActive = true
  }

}

extension RootViewController: UIScrollViewDelegate {
  //1
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offset = scrollView.contentOffset
    scrollView.isPagingEnabled = offset.x < threshold
    let fraction = calculateMenuDisplayFraction(scrollView)
    updateViewVisibility(menuContainer, fraction: fraction)
    hamburgerView?.setFractionOpen(1.0 - fraction)
  }

  //2
  func scrollViewDidEndDragging(_ scrollView: UIScrollView,
                                willDecelerate decelerate: Bool) {
    let offset = scrollView.contentOffset
    if offset.x > threshold {
      hideMenu()
    }
  }

  //3
  func moveMenu(nextPosition: CGFloat) {
    let nextOffset = CGPoint(x: nextPosition, y: 0)
    scroller.setContentOffset(nextOffset, animated: true)
  }

  //4
  func hideMenu() {
    moveMenu(nextPosition: menuWidth)
  }

  func showMenu() {
    moveMenu(nextPosition: 0)
  }

  func toggleMenu() {
    let menuIsHidden = scroller.contentOffset.x > threshold
    if menuIsHidden {
      showMenu()
    } else {
      hideMenu()
    }
  }
}

extension RootViewController {
  func installInNavigationController(_ rootController: UIViewController)
    -> UINavigationController {
      let nav = UINavigationController(rootViewController: rootController)

      //1
      nav.navigationBar.barTintColor = UIColor(named: "rw-dark")
      nav.navigationBar.tintColor = UIColor(named: "rw-light")
      nav.navigationBar.isTranslucent = false
      nav.navigationBar.clipsToBounds = true

      //2
      addChild(nav)

      return nav
  }

func installFromStoryboard(_ identifier: String,
                           into container: UIView)
  -> UIViewController {
    guard let viewController = storyboard?
      .instantiateViewController(withIdentifier: identifier) else {
        fatalError(" broken storyboard expected \(identifier) to be available")
    }

    let nav = installInNavigationController(viewController)
    container
      .embedInsideSafeArea(nav.view)
    return viewController
}

}

extension RootViewController: MenuDelegate {
  func didSelectMenuItem(_ item: MenuItem) {
    detailViewController?.menuItem = item
  }
}

extension RootViewController {
  func installBurger(in viewController: UIViewController) {
    let action = #selector(burgerTapped(_:))
    let tapGestureRecognizer = UITapGestureRecognizer(target: self,
                                                      action: action)
    let burger = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    burger.addGestureRecognizer(tapGestureRecognizer)
    viewController.navigationItem.leftBarButtonItem
      = UIBarButtonItem(customView: burger)
    hamburgerView = burger
  }

  @objc func burgerTapped(_ sender: Any) {
    toggleMenu()
  }
}

extension RootViewController {
  func transformForFraction(_ fraction: CGFloat, ofWidth width: CGFloat)
    -> CATransform3D {
      //1
      var identity = CATransform3DIdentity
      identity.m34 = -1.0 / 1000.0

      //2
      let angle = -fraction * .pi/2.0
      let xOffset = width/2.0 + width * fraction/4.0

      //3
      let rotateTransform = CATransform3DRotate(identity, angle, 0.0, 1.0, 0.0)
      let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
      return CATransform3DConcat(rotateTransform, translateTransform)
  }
}

extension RootViewController {
  //1
  func calculateMenuDisplayFraction(_ scrollview: UIScrollView) -> CGFloat {
    let fraction = scrollview.contentOffset.x/menuWidth
    let clamped = Swift.min(Swift.max(0, fraction), 1.0)
    return clamped
  }

  //2
  func updateViewVisibility(_ container: UIView, fraction: CGFloat) {
    container.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
    container.layer.transform = transformForFraction(fraction,
                                                     ofWidth: menuWidth)
    container.alpha = 1.0 - fraction
  }
}
5. HamburgerView.swift
import UIKit

class HamburgerView: UIView {
  //1
  let imageView: UIImageView = {
    let view = UIImageView(image: UIImage(imageLiteralResourceName: "Hamburger"))
    view.contentMode = .center
    return view
  }()

  //2
  required override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    configure()
  }

  private func configure() {
    addSubview(imageView)
  }

  func setFractionOpen(_ fraction: CGFloat) {
    let angle = fraction * .pi/2.0
    imageView.transform = CGAffineTransform(rotationAngle: angle)
  }
}
6. DetailViewController.swift
import UIKit

class DetailViewController: UIViewController {
  @IBOutlet weak var backgroundImageView: UIImageView!

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = UIColor(named: "rw-dark")
  }

  var menuItem: MenuItem? {
    didSet {
      prepare(menuItem)
    }
  }

  func prepare(_ menuItem: MenuItem?) {
    if let newMenuItem = menuItem {
      view.backgroundColor = newMenuItem.color
      backgroundImageView?.image = newMenuItem.bigImage
    }
  }
}
7. MenuViewController.swift
import UIKit

protocol MenuDelegate: class {
  func didSelectMenuItem(_ item: MenuItem)
}

class MenuViewController: UITableViewController {
  let maxCellHeight: CGFloat = 100
  private var datasource: MenuDataSource = MenuDataSource()

  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.dataSource = datasource
  }

  override func tableView(_ tableView: UITableView,
                          heightForRowAt indexPath: IndexPath) -> CGFloat {
    let proposedHeight = tableView.safeAreaLayoutGuide.layoutFrame.height/CGFloat(datasource.menuItems.count)
    return min(maxCellHeight, proposedHeight)
  }

  //1
  weak var delegate: MenuDelegate?

  override func tableView(_ tableView: UITableView,
                          didSelectRowAt indexPath: IndexPath) {
    //2
    let item = datasource.menuItems[indexPath.row]
    delegate?.didSelectMenuItem(item)

    //3
    DispatchQueue.main.async {
      tableView.deselectRow(at: indexPath, animated: true)
    }
  }
}
8. MenuItemCell.swift
import UIKit

class MenuItemCell: UITableViewCell {
  @IBOutlet weak var menuItemImageView: UIImageView!

  func configureForMenuItem(_ menuItem: MenuItem) {
    menuItemImageView.image = menuItem.image
    backgroundColor = menuItem.color
  }
}
9. MenuDataSource.swift
import UIKit

class MenuDataSource: NSObject, UITableViewDataSource {
  private(set) var menuItems: [MenuItem] = []

  override init() {
    super.init()
    prepare()
  }

  func prepare() {
    guard let url = Bundle.main.url(forResource: "MenuItems", withExtension: "json") else {
      assertionFailure("project config error - unable to find MenuItems.json in bundle")
      return
    }
    do {
      let data = try Data(contentsOf: url)
      menuItems = try JSONDecoder().decode([MenuItem].self, from: data)
    } catch {
      assertionFailure("config error - unable to decode json file - \(error)")
    }
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
    -> Int {
    return menuItems.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
    -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "MenuItemCell",
                                                   for: indexPath)
      as? MenuItemCell else {
      fatalError("expected to dequeue MenuItemCell - check storyboard")
    }
    let menuItem = menuItems[indexPath.row]
    cell.configureForMenuItem(menuItem)
    return cell
  }
}
10. MenuItem.swift
import UIKit

struct MenuItem: Decodable {
    var colorArray: [CGFloat]
    var bigImageName: String
    var imageName: String
}

extension MenuItem {
  var image: UIImage {
    return UIImage(imageLiteralResourceName: imageName)
  }

  var bigImage: UIImage {
    return UIImage(imageLiteralResourceName: bigImageName)
  }

  var color: UIColor {
    return UIColor(colorArray: colorArray)
  }
}

后记

本篇主要讲述了基于CALayer属性的一种3D边栏动画的实现,感兴趣的给个赞或者关注~~~