ariok/BWWalkthrough を読む

# BWWalkthrough

|名前|説明|
|---|---|
|リポジトリ|https://github.com/ariok/BWWalkthrough|
|読んだバージョン|[eb8fbd2483b7871fb51068a8418d3a10ebaf1e61](https://github.com/ariok/BWWalkthrough/tree/eb8fbd2483b7871fb51068a8418d3a10ebaf1e61)|

# 概要

アプリの起動時にチュートリアルを表示するWalkThroughライブラリ。
表示したいチュートリアルViewをStoryboard上で描くので表示するviewは簡単に作れるし自由度もあると思います。
ビューの遷移は4つのアニメーションが用意されていてIBInspectableが定義されているのでStoryboardから設定が可能。遷移部分は自分でカスタムアニメーションを定義することもできます。

# 使い方

Storyboardを使うことが前提のライブラリである。
Containerとなる`BWWalkthroughViewController`とチュートリアルViewとなる`BWWalkthroughPageViewController`をCustom Classとして持つ`UIViewController`をStoryboardに用意すれば使えます。

# 実装

## BWWalkthroughViewController.swift (312行)

WalkThroughのContainerとなるUIViewControllerを定義しています。

最初にdelegateメソッドをprotocolで定義。各ボタンのクリックやページ変更時にアクションを起こしたいときはこのdelegateを実装すればよいです。

```swift

@objc public protocol BWWalkthroughViewControllerDelegate{
    
    @objc optional func walkthroughCloseButtonPressed()              // If the skipRequest(sender:) action is connected to a button, this function is called when that button is pressed.
    @objc optional func walkthroughNextButtonPressed()               //
    @objc optional func walkthroughPrevButtonPressed()               //
    @objc optional func walkthroughPageDidChange(pageNumber:Int)     // Called when current page changes

}
```

あらかじめ`@IBOutlet`が定義されているのでStoryboardで定義したViewをこちらにつないでいく。Storyboardを使う前提の作りと書いたのはココから。

```swift

    @IBOutlet public var pageControl:UIPageControl?
    @IBOutlet public var nextButton:UIButton?
    @IBOutlet public var prevButton:UIButton?
    @IBOutlet public var closeButton:UIButton?

```

チュートリアルのページ数、という概念があるのだけれど全てScrollViewのOffsetベースで計算するようになっています。

```swift
    public var currentPage:Int{    // The index of the current page (readonly)
        get{
            let page = Int((scrollview.contentOffset.x / view.bounds.size.width))
            return page
        }
    }
```

Containerの仕組みは`UIScrollView`の上に後で説明するBWWalkthroughPageViewControllerで定義されたviewを左から順に並べていくというもの。
`addViewController`でそのあたりの処理が書いてあります。Layout制約をコードで書いてるのだけれどVisualフォーマットで書いてるのは新鮮（僕は好きです）

```swift
    public func addViewController(vc:UIViewController)->Void{
        
        controllers.append(vc)
        
        // Setup the viewController view
        
        vc.view.translatesAutoresizingMaskIntoConstraints = false
        scrollview.addSubview(vc.view)
        
        // Constraints
        
        let metricDict = ["w":vc.view.bounds.size.width,"h":vc.view.bounds.size.height]
        
        // - Generic cnst
        
        vc.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[view(h)]", options:[], metrics: metricDict, views: ["view":vc.view]))
        vc.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[view(w)]", options:[], metrics: metricDict, views: ["view":vc.view]))
        scrollview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[view]|", options:[], metrics: nil, views: ["view":vc.view,]))
        
        // cnst for position: 1st element
        
        if controllers.count == 1{
            scrollview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[view]", options:[], metrics: nil, views: ["view":vc.view,]))
            
            // cnst for position: other elements
            
        }else{
            
            let previousVC = controllers[controllers.count-2]
            let previousView = previousVC.view;
            
            scrollview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[previousView]-0-[view]", options:[], metrics: nil, views: ["previousView":previousView,"view":vc.view]))
            
            if let cst = lastViewConstraint{
                scrollview.removeConstraints(cst as! [NSLayoutConstraint])
            }
            lastViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat("H:[view]-0-|", options:[], metrics: nil, views: ["view":vc.view])
            scrollview.addConstraints(lastViewConstraint! as! [NSLayoutConstraint])
        }
    }
```

アニメーションの開始もScrollViewのOffsetベースです。`walkthroughDidScroll`でアニメーションが実行されます。

```swift

    public func scrollViewDidScroll(sv: UIScrollView) {
        
        for var i=0; i < controllers.count; i++ {
            
            if let vc = controllers[i] as? BWWalkthroughPage{
            
                let mx = ((scrollview.contentOffset.x + view.bounds.size.width) - (view.bounds.size.width * CGFloat(i))) / view.bounds.size.width
                
                // While sliding to the "next" slide (from right to left), the "current" slide changes its offset from 1.0 to 2.0 while the "next" slide changes it from 0.0 to 1.0
                // While sliding to the "previous" slide (left to right), the current slide changes its offset from 1.0 to 0.0 while the "previous" slide changes it from 2.0 to 1.0
                // The other pages update their offsets whith values like 2.0, 3.0, -2.0... depending on their positions and on the status of the walkthrough
                // This value can be used on the previous, current and next page to perform custom animations on page's subviews.
                
                // print the mx value to get more info.
                // println("\(i):\(mx)")
                
                // We animate only the previous, current and next page
                if(mx < 2 && mx > -2.0){
                    vc.walkthroughDidScroll(scrollview.contentOffset.x, offset: mx)
                }
            }
        }
    }
```

`WIP` というコメントアウトしている箇所に`willTransitionToTraitCollection`と`viewWillTransitionToSize`の空実装があるので端末が回転したときの対応も考えているのかもしれません。

```swift
    /* WIP */
    override public func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        print("CHANGE")
    }
    
    override public func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        print("SIZE")
    }
```

## BWWalkthroughPageViewController.swift (173行)

チュートリアルViewのアニメーションを定義しているクラスです。

アニメーションはデフォルトでは4パターン用意されています。未設定だったり存在しないアニメーションを指定すると`.Linear`になります。

```swift
public enum WalkthroughAnimationType:String{
    case Linear = "Linear"
    case Curve = "Curve"
    case Zoom = "Zoom"
    case InOut = "InOut"
    
    init(_ name:String){
        
        if let tempSelf = WalkthroughAnimationType(rawValue: name){
            self = tempSelf
        }else{
            self = .Linear
        }
    }
}
```

アニメーションのスピードや種類、alphaやアニメーション対象外にしたいviewはStoryboardから設定できるように`@IBInspectable`が定義されています。

```swift
    @IBInspectable var speed:CGPoint = CGPoint(x: 0.0, y: 0.0);            // Note if you set this value via Attribute inspector it can only be an Integer (change it manually via User defined runtime attribute if you need a Float)
    @IBInspectable var speedVariance:CGPoint = CGPoint(x: 0.0, y: 0.0)     // Note if you set this value via Attribute inspector it can only be an Integer (change it manually via User defined runtime attribute if you need a Float)
    @IBInspectable var animationType:String {
        set(value){
            self.animation = WalkthroughAnimationType(rawValue: value)!
        }
        get{
            return self.animation.rawValue
        }
    }
    @IBInspectable var animateAlpha:Bool = false
    @IBInspectable var staticTags:String {                                 // A comma separated list of tags that you don't want to animate during the transition/scroll 
        set(value){
            self.notAnimatableViews = value.componentsSeparatedByString(",").map{Int($0)!}
        }
        get{
            return notAnimatableViews.map{String($0)}.joinWithSeparator(",")
        }
    }

```

アニメーションは `walkthroughDidScroll` で実行されます。カスタムアニメーションを作りたい場合はこのメソッドをoverrideすればOKです。ただ左から右に流れていくという前提のライブラリなので、そこに逆らう感じのアニメーションだとなじまない気がします。

```swift
    public func walkthroughDidScroll(position: CGFloat, offset: CGFloat) {
        
        for(var i = 0; i < subsWeights.count ;i++){
            
            // Perform Transition/Scale/Rotate animations
            switch animation{
                
            case .Linear:
                animationLinear(i, offset)
                
            case .Zoom:
                animationZoom(i, offset)
                
            case .Curve:
                animationCurve(i, offset)
                
            case .InOut:
                animationInOut(i, offset)
            }
            
            // Animate alpha
            if(animateAlpha){
                animationAlpha(i, offset)
            }
        }
    }
```

# 感想

依存ライブラリも無いしコードサイズも小さくUIKit絡みのライブラリだったので読みやすかったです。
チュートリアルデザインがStoryboardで作れるのは簡単で良い、と思う一方でいろいろと制限もあるので要件にマッチする場合に使っていきたいところです。