mitsu's techlog

Customer Reliability Engineerになりました。備忘録も兼ねて技術ネタを適当に。技術以外はこっち→http://mitsu9.hatenablog.com/

Swiftで作るかっこいいwalkthrough(Airbnb風)

Airbnbのwalkthroughがかっこいいなーとおもったので同じようなものをつくってみました。

Airbnbの良いところは、

  • 横にスクロールすると今の画像がフェードアウトし、次の画像がフェードインする
  • 画像が常にアニメーションしている
  • 横スクロールに合わせて下のボタンの色がなめらかに変わる

です。

今回はこれらの項目を実装しました。

f:id:rayc5:20150622004404g:plain

1つめ | 横にスクロールすると今の画像がフェードアウトし、次の画像がフェードインする

今回はImageViewを乗せるためのimageViewContainerを用意し、そちらにImageViewを乗せるという方法で実装しました。

walkthroughの長さによってImageViewの数が変わってくるので、常に全てを表示するのではなく、今表示しているものと左右にスクロールしたときに出てくるものの計3枚のImageViewが一度にこのimageViewContainerにのっています。 左右にスクロールした時に出てくるビューはスクロールの開始時に追加され、終了時に取り除かれるので、スクロールしていないときはそのとき表示されているImageViewが表示されるようにしました。

ViewDidLoad

まずViewDidLoad内で下準備をします。

今回scrollViewはstoryBoardで実装し、imageViewContainerはコードで実装するという変なことをしたので、余計な部分がありますが気にしないでください。

基本的には、ImageViewを用意し配列に保存し、一枚目のImageViewだけimageViewContainerに追加し表示する処理をおこなっています。

class ViewController: UIViewController, UIScrollViewDelegate {

var imageViewContainer: UIView!
var imageViewArray = [UIImageView]()

override func viewDidLoad() {
    super.viewDidLoad()

    /** 関係ない部分は省略 */
        
    // imageViewContainer. scrollViewの後ろ側に追加
    imageViewContainer = UIView(frame: CGRectMake(0, 0, view.frame.width, view.frame.width))
    view.insertSubview(imageViewContainer, belowSubview: scrollView)
        
    // ImageViewの設定
    let frame = CGRectMake(0.0, 0.0, view.frame.size.width, view.frame.size.height)
     
    var imageView1 = UIImageView(image: UIImage(named: "image1.jpg"))
    imageView1.frame = frame
    imageViewArray.append(imageView1)
        
    var imageView2 = UIImageView(image: UIImage(named: "image2.jpg"))
    imageView2.frame = frame
    imageViewArray.append(imageView2)
        
    var imageView3 = UIImageView(image: UIImage(named: "image3.jpg"))
    imageView3.frame = frame
    imageViewArray.append(imageView3)
        
    // 最初は1枚目だけを追加. 後のビューはスクロールに合わせて追加・削除する.
    imageViewContainer.addSubview(imageViewArray[0])
}

UIScrollViewDelegate

アニメーションはscrollViewのdelegateメソッドを利用します。

まず初めにscrollViewWillBeginDraggingにて、左右のImageViewをimageViewContainerに追加します。

次にscrollViewDidScrollにて、ImageViewのalpha値を変更しフェードイン・アウトを実装します。

alpha値は現在のcontentOffsetと今のページのx座標の差分からスクロール量を計算し、それを元に計算します。progressがスクロールの進み具合を示す変数で、0~1の値をとるようになっているので、この変数を使ってalpha値を決めていきます。

func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    addBothSidesImageView()
}

func scrollViewDidScroll(scrollView: UIScrollView) {
    let pageWidth = scrollView.frame.size.width
    if Int(fmod(scrollView.contentOffset.x , pageWidth)) == 0 {
        pageControl.currentPage = Int(scrollView.contentOffset.x / pageWidth)
        imageViewArray[pageControl.currentPage].alpha = 1.0
        removeBothSidesImageView()
    } else {
        // ここで画像と色の変化を実装
        let currentView = imageViewArray[pageControl.currentPage]
        if let index = nextIndex {
            let def = pageWidth * CGFloat(pageControl.currentPage) - scrollView.contentOffset.x
            let progress = CGFloat(abs(def)) / pageWidth
            
            let nextView = imageViewArray[index]
            currentView.alpha = 1 - progress
            nextView.alpha = progress
            
            /** 関係ない部分は省略 */
        }
    }
}

private var nextIndex: Int? {
    get {
        var nextIndex: Int
        let currentPageX = CGFloat(pageControl.currentPage) * view.frame.width
        if scrollView.contentOffset.x < currentPageX { // 右スワイプ
            nextIndex = pageControl.currentPage - 1
        } else {
            nextIndex = pageControl.currentPage + 1
        }
            
        if nextIndex < 0 || pageCount <= nextIndex {
            return nil
        }
        return nextIndex
    }
}

2つめ | 画像が常にアニメーションしている

今回はimageViewContainerを作っているので、こちらを常にアニメーションすれば、ImageViewが常に変化するようになります。

アニメーションを永遠に繰り返すためにrepeatCountをHUGEにすることと、拡大縮小を繰り返すためにautoreversesをtrueにすることでこのようなアニメーションを実現できます。

override func viewDidLoad() {
    var animation = CABasicAnimation(keyPath: "transform.scale")
    animation.duration = 10.0
    animation.repeatCount = HUGE
    animation.autoreverses = true
    animation.fromValue = 1.0
    animation.toValue = 1.1
    imageViewContainer.layer.addAnimation(animation, forKey: "scale-layer")
}

3つめ | 横スクロールに合わせて下のボタンの色がなめらかに変わる

基本的には1つめの画像のフェードインと同じです。色の情報をarrayに格納しておき、その情報を元になめらかに色を変化させていきます。

画像はalpha値を変更したことに対し、今回は色のRGB値を変更することでなめらかに色を変化させていきます。

UIColorからRGB値を取得するためにはgetRed(red:UnsafeMutablePointer, green:UnsafeMutablePointer, blue:UnsafeMutablePointer, alpha:UnsafeMutablePointer)メソッドを使います。

これで今の色と次の色のRGB値を算出し、その差分をスクロールの量に合わせて追加することでなめらかに色を変化させることができます。

func scrollViewDidScroll(scrollView: UIScrollView) {
    let pageWidth = scrollView.frame.size.width
    if Int(fmod(scrollView.contentOffset.x , pageWidth)) == 0 {
        /** 関係ないところは省略 */
    } else {
        // ここで画像と色の変化を実装
        let currentView = imageViewArray[pageControl.currentPage]
        if let index = nextIndex {
            let def = pageWidth * CGFloat(pageControl.currentPage) - scrollView.contentOffset.x
            let progress = CGFloat(abs(def)) / pageWidth
            
            /** ImageViewの変更 - 略 - */
            
            let nextColor = colorArray[index]
            var currentColor = colorArray[pageControl.currentPage]
            var currentRGBColor = getRGB(currentColor)
            var defColors = defTwoColors(currentColor, second: nextColor)
            footerView.backgroundColor = UIColor(
                red: currentRGBColor.red + defColors.red * progress,
                green: currentRGBColor.green + defColors.green * progress,
                blue: currentRGBColor.blue + defColors.blue * progress,
                alpha: currentRGBColor.alpha + defColors.alpha * progress)
        }
    }
}

private func getRGB(color: UIColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
    var rgbValue = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    color.getRed(&rgbValue.red, green: &rgbValue.green, blue: &rgbValue.blue, alpha: &rgbValue.alpha)
    return (rgbValue.red, rgbValue.green, rgbValue.blue, rgbValue.alpha)
}

private func defTwoColors(first: UIColor, second: UIColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
    var currentColor = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    var nextColor = (red: CGFloat(0.0), green: CGFloat(0.0), blue: CGFloat(0.0), alpha: CGFloat(0.0))
    
    first.getRed(&currentColor.red, green: &currentColor.green, blue: &currentColor.blue, alpha: &currentColor.alpha)
    second.getRed(&nextColor.red, green: &nextColor.green, blue: &nextColor.blue, alpha: &nextColor.alpha)
    
    return (nextColor.red - currentColor.red, nextColor.green - currentColor.green, nextColor.blue - currentColor.blue, nextColor.alpha - currentColor.alpha)
}

最後に

全体像は以下のようになってます。

AirbnbWalkThrough