(iOS) 向Hero致敬與分析 (二) Double研究所

jamesdouble發表於2018-04-27

本篇繼上篇:http://www.jianshu.com/p/fdab69f7440a 本篇種點:

  • Plugin
  • heroModifierString

1.3.1)Present流程 - start() - Plugin, HeroPreprocessor , HeroAnimator


public func animateTransition(using context: UIViewControllerContextTransitioning) {
 /*
 略
.
*/
 start()
}
複製程式碼

我們來到最主要的程式段:start()

func start() {
    plugins == nil {
      print("plugins == nil")
     //把enabledPlugins裡的每個類別全部init後放進
      plugins = Hero.enabledPlugins.map({ return $0.init() })
    }
 /*
 略
.
*/
}
複製程式碼

一開頭就是我們沒見過的plugins這個變數,所以我們先來淺談plugins是什麼。

Plugin是Hero提供可以讓使用者、開發者自行在做自定義的處理動畫或是其他變數處理。只要讓類別實作HeroPlugin(繼承HeroPreprocessor, HeroAnimator兩個協定)這個類別,即可在過場的進行時,收到以下通知:

Hero's Plugin


現在我們來看看HeroPreprocessor, HeroAnimator Hero有以下兩個變數,可以把它們想成兩條執行緒,一條專門做一系列的HeroPreprocessor, 而另一條則做一系列的HeroAnimator。上面提到的Plugin同時屬於這兩類。

 var processors: [HeroPreprocessor]!
 var animators: [HeroAnimator]!

func start() {
/* 略 */
processors = Hero.builtInProcessors  //Hero內建程式
animators = Hero.builtInAnimator    //Hero內建動畫

 // 如果有外掛的話就加程式序
    for plugin in plugins {
      processors.append(plugin)
      animators.append(plugin)
    }
/* 略 */
// ask each preprocessor to process
    for processor in processors {
      processor.process(context:context, fromViews: context.fromViews, toViews: context.toViews)
    }
/* 略 */
 for animator in animators.reversed() {
      let currentFromViews = fromViews.filterInPlace{ [context] (view:UIView) -> Bool in
        return !animator.canAnimate(context: context!, view: view, appearing: false)
      }
      let currentToViews = toViews.filterInPlace{ [context] (view:UIView) -> Bool in
        return !animator.canAnimate(context: context!, view: view, appearing: true)
      }
      animatorViews.insert((currentFromViews, currentToViews), at: 0)
    }
}

複製程式碼

1.3.2)Present流程 - start() - 製作過場"舞臺"


複製一個起始view(from view)蓋上去(animatingViewContainer),之後所有動畫都在上面進行,算是最好懂的一塊。

func start() {
/* 略 */
transitionContainer.isUserInteractionEnabled = false
    // a view to hold all the animation views
    //transitionContainer是在context拿到的
    animatingViewContainer = UIView(frame: transitionContainer.bounds)
    transitionContainer.addSubview(animatingViewContainer) //加上一個空的等大容器
    // create a snapshot view to hide all the flashing that might happen
    let completeSnapshot = fromView.snapshotView(afterScreenUpdates: true)!
    transitionContainer.addSubview(completeSnapshot)
    
    animatingViewContainer.addSubview(fromView)
    animatingViewContainer.insertSubview(toView, belowSubview: fromView)
    animatingViewContainer.backgroundColor = toView.backgroundColor
/* 略 */

context = HeroContext(container:animatingViewContainer, fromView: fromView, toView:toView)
}
複製程式碼

1.4)HeroContext


這個類別算是Hero的特色也是最有技巧性的一部分。主要處理這兩個 "HeroID", "heroModifierString"

1.4.1) heroModifierString

一句話概括它,就是“指令”,指示這個View要執行怎樣的過場動畫。使用者在StoryBoard或是swift裡給定ModifierString後,即在變數的set{...}將字串轉為可執行的function指令。

public extension UIView{
  /* 略 */
@IBInspectable public var heroModifierString: String? {
  /* 略 */
      let modifierString = newValue as NSString
      var modifiers = [HeroModifier]() //function結果
      //modifiersRegex = "(\\w+)(?:\\(([^\\)]*)\\))?"
      for r in matches(for: modifiersRegex, text:modifierString) //A Loop
      {
        var parameters = [String]()
        if r.numberOfRanges > 2, r.rangeAt(2).location < modifierString.length
        {
          let parameterString = modifierString.substring(with: r.rangeAt(2)) as NSString
          //parameterRegex = "(?:\\-?\\d+(\\.?\\d+)?)|\\w+"
          for r in matches(for: parameterRegex, text: parameterString){ //B Loop
            parameters.append(parameterString.substring(with: r.range))
          }
        }
        let name = modifierString.substring(with: r.rangeAt(1))
        //取得Function
        if let modifier = HeroModifier.from(name: name, parameters: parameters){
          modifiers.append(modifier)
       /*略*/
複製程式碼

numberOfRanges:regx的組數,最外層()的數量。

rangeAt(0) = 有符合的結果, rangeAt(1) = 有一組以上,rangeAt(2) = 有兩組以上......

A Loop: 找出 一個以上非空白 可能出現一對括號 中間夾著多個非右括號 = 一個Fuction的格式 , 若 numberOfRanges > 2 則代表有(引數)。 B Loop: 引數 可是一串非空白 或是 數子(可負數或小數)

EX: heroModifierString = zPosition(2) arc

parameterString = 2 , name = zPosition name = arc

讀出個別指令後就是利用一個配對Function,可以看到最終每個字串有配對結果的話,可以得到一個個的**"閉包(Closure)"**

public class HeroModifier {
  internal let apply:(inout HeroTargetState) -> Void
  public init(applyFunction:@escaping (inout HeroTargetState) -> Void){
    apply = applyFunction
  }
/*略*/
static func from(name:String, parameters:[String]) -> HeroModifier?
{

switch name {
    case "zPosition": 
      if let zPosition = parameters.getCGFloat(0){ //拿引數
        modifier = .zPosition(zPosition)
      }
}
/*略*/
public static func zPosition(_ zPosition:CGFloat) -> HeroModifier {
    return HeroModifier { targetState in
      targetState.zPosition = zPosition
    }
  }
}
/*略*/
 public static func arc(intensity:CGFloat = 1) -> HeroModifier {
    return HeroModifier { targetState in
      targetState.arc = intensity
    }
  }
複製程式碼

走到這麼深之後,我們現在在回extension UIView頂層看看另外一個擴充變數,

public extension UIView{
/*略*/
public var heroModifiers: [HeroModifier]? 
複製程式碼

現在我們可以把它理解成一個可執行的閉包集合,因為每個HeroModifier都自帶一個閉包

apply:(inout HeroTargetState) -> Void
複製程式碼

它記著一個View在過場時,所有要執行的過場動畫,可利用 modifier.apply(&HeroTargetState)呼叫。

相關文章