デザイナーさんが好きなアプリのマイクロインタラクションを実装してみた

友人のデザイナー、村上氏(@ryuki_kyoto)が好きなアプリのマイクロインタラクションの紹介と、それのサンプルを実装してみました。

## 紹介編

## Pinterest 
ピンを保存するボードリストをホールドした時、画像が右にずれ、文字間が詰まる事でユーザにフィードバックを返しています。
リストの一行が一枚の幕になっており、その膜に指を置いたことで文字が収縮したような感覚になります。
ユーザに対してより現実に近い表現のフィードバックを与えることで、UIと現実の境目が少しだけ小さくなっています。（by 村上氏）

![2019-1-4-pinterest.gif](https://qiita-image-store.s3.amazonaws.com/0/63855/e224fead-fc0c-c341-015e-cf23bb19bba8.gif)


[Pinterest
レシピやインテリア、ファッションコーデなど試したくなるアイデアを発見しましょう。
](https://www.pinterest.jp/)



## Lifesum
ボタンを押すと、そのボタンが画面全体に広がり次の画面に遷移します。
自分の行動によって画面全体に影響が及ぶ爽快感や、そのボタンを押したことで自分がアプリの中に入っていくような没入感で、アプリのオンボーディングにおけるユーザのモチベーションを向上させます。（by 村上氏）

![2019-1-1-lifesum.gif](https://qiita-image-store.s3.amazonaws.com/0/63855/a9cbb174-f461-4659-41dc-3bff0b1c4c9c.gif)

[Lifesum Health App – Get Healthy & Lose Weight – Lifesum](https://lifesum.com/)


## 実装編
PinterestとLifesumのインタラクションを参考に 「フィードバックを返すボタン」 と　「画面全体に広がり画面遷移をするボタン」のサンプルを実装してみました。
よりスマートな書き方があると思うのでぜひコメントで教えてください！ :bow: 

## フィードバックを返すボタン

UIButtonクラスを継承してCustomButtonクラスを作成。
touchDown　時に、サムネイル画像が左にズレる & 文字間隔が詰まるアニメーションを発火させています。

![2019-1-1-kunyu.gif](https://qiita-image-store.s3.amazonaws.com/0/63855/11e6fa38-1b50-e1df-162f-de10cff05281.gif)


``` swift:CustomButton.swift
import UIKit

class CustomButton: UIButton {
    private var width: CGFloat = 0
    private var height: CGFloat = 0
    private var margin: CGFloat = 8
    private var offset: CGFloat = 2

    private var thumbnail: UIImageView!
    private var label: UILabel!

    private var tapping: Bool = false

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

    override init(frame: CGRect) {
        super.init(frame: frame)

        layer.masksToBounds = false
        layer.cornerRadius = 4
        layer.shadowOffset = CGSize(width: 0, height: 2)
        layer.shadowRadius = 4;
        layer.shadowOpacity = 0.4;
        backgroundColor = UIColor.init(named: "unTap")

        thumbnail = UIImageView()
        thumbnail.image = UIImage(named: "icon")
        thumbnail.layer.masksToBounds = false
        thumbnail.layer.cornerRadius = 4
        addSubview(thumbnail)

        label = UILabel()
        label.text = "Snorlax"
        label.textAlignment = .right
        label.textColor = .gray
        label.font = UIFont.systemFont(ofSize: 40)
        addSubview(label)
    }

    override func layoutSubviews() {
        width = frame.width
        height = frame.height
        if tapping {
            onTapPosition()
        } else {
            unTapPosition()
        }
    }

    func unTapPosition() {
        let thmbnailSize = height - margin * 2
        thumbnail.frame = CGRect(x: margin, y: (height - thmbnailSize) / 2, width: thmbnailSize, height: thmbnailSize)

        let KernAttr = [NSAttributedString.Key.kern: 4]
        label.attributedText = NSMutableAttributedString(string: label.text!, attributes: KernAttr)
        let labelWidth = label.sizeThatFits(CGSize()).width
        label.frame = CGRect(x: height, y: 0, width: labelWidth, height: height)
    }

    func onTapPosition() {
        let thmbnailSize = height - (margin * 2)
        thumbnail.frame = CGRect(x: margin + offset, y: (height - thmbnailSize) / 2, width: thmbnailSize, height: thmbnailSize)

        let KernAttr = [NSAttributedString.Key.kern: 3.8]
        label.attributedText = NSMutableAttributedString(string: label.text!, attributes: KernAttr)
        let labelWidth = label.sizeThatFits(CGSize()).width
        label.frame = CGRect(x: height + offset, y: 0, width: labelWidth, height: height)
    }

    func unTap() {
        backgroundColor = UIColor.init(named: "unTap")
        tapping = false
        UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
                self.unTapPosition()
            }, completion: nil)
    }

    func onTap() {
        backgroundColor = UIColor.init(named: "onTap")
        tapping = true
        UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
                self.onTapPosition()
            }, completion: nil)
    }
}
```

``` swift:UIViewController.swift
import UIKit

class ViewController: UIViewController {
    var button: CustomButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        let width = view.frame.width
        let height = view.frame.height
        let buttonWidth = width * 0.8
        let buttonHeight: CGFloat = 80

        button = CustomButton()
        button.addTarget(self, action: #selector(touchUpInside(_:)), for: UIControl.Event.touchUpInside)
        button.addTarget(self, action: #selector(touchDown(_:)), for: UIControl.Event.touchDown)
        button.addTarget(self, action: #selector(touchDragExit(_:)), for: UIControl.Event.touchDragExit)
        button.setTitle("ボタンのテキスト", for: UIControl.State.normal)
        button.setTitleColor(.red, for: UIControl.State.normal)
        button.frame = CGRect(x: (width - buttonWidth) / 2, y: (height - buttonHeight) / 2, width: buttonWidth, height: buttonHeight)
        view.addSubview(button)
    }

    @objc func touchDown(_ sender: UIButton) {
        print("touchDown")
        (sender as! CustomButton).onTap()
    }

    @objc func touchUpInside(_ sender: UIButton) {
        print("touchUpInside")
        (sender as! CustomButton).unTap()
    }

    @objc func touchDragExit(_ sender: UIButton) {
        print("touchDragExit")
        (sender as! CustomButton).unTap()
    }
}
```


## ボタンが広がって遷移するアニメーション

FirstViewController から SecondViewController への遷移時に下からボタンが現れるアニメーションが発火します。
ThirdViewController へ遷移時もフェードのアニメーションを付与しています。

![2019-1-1-seni.gif](https://qiita-image-store.s3.amazonaws.com/0/63855/292edf73-b1d9-83f3-37c6-5a720d16bf69.gif)

``` swift:CustomButton.swift
import UIKit

class CustomButton: UIButton {
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor(named: "emerald")
        setTitleColor(UIColor.white, for: UIControl.State.normal)
        titleLabel?.font = UIFont.systemFont(ofSize: 24)
        layer.masksToBounds = false
        layer.cornerRadius = 20.0
        layer.shadowColor = UIColor.lightGray.cgColor
        layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
        layer.shadowOpacity = 0.8
    }
}
```


``` swift:AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var navigationController: UINavigationController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window!.makeKeyAndVisible()
        let firstViewController: FirstViewController? = FirstViewController()
        navigationController = UINavigationController(rootViewController: firstViewController!)
        navigationController?.setNavigationBarHidden(true, animated: false)
        window!.rootViewController = navigationController
        return true
    }
    // 略
}
``` 

``` swift:FirstViewController.swift
import UIKit

class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        let buttonHeight: CGFloat = 60

        let button = CustomButton()
        button.frame.size = CGSize(width: view.frame.width * 0.6, height: buttonHeight)
        button.addTarget(self, action: #selector(buttonTaped(sender:)), for: .touchUpInside)
        button.setTitle("Push Me", for: UIControl.State.normal)
        button.center = view.center
        view.addSubview(button)
    }

    @objc func buttonTaped(sender: UIButton) {
        navigationController?.pushViewController(SecondViewController(), animated: true)
    }
}
```

``` swift:SecondViewController.swift
import UIKit

class SecondViewController: UIViewController {

    var width: CGFloat!
    var height: CGFloat!
    let buttonHeight: CGFloat = 60
    let radius: CGFloat = 100

    var button: CustomButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        width = view.frame.width
        height = view.frame.height

        //ボタンの生成
        button = CustomButton()
        button.frame.size = CGSize(width: width * 0.6, height: buttonHeight)
        button.center.x = view.frame.width / 2
        button.center.y = view.frame.height / 2 + 40
        button.addTarget(self, action: #selector(cornerCircleButtonClicked(sender:)), for: .touchUpInside)
        button.setTitle("Show Snorlax", for: UIControl.State.normal)
        button.alpha = 0.3
        button.isEnabled = false
        view.addSubview(button)

        // 遷移直後のアニメーション
        UIView.animate(withDuration: 0.3, delay: 0.1, options: [.curveLinear], animations: {
                self.button.isEnabled = true
                self.button.alpha = 1.0
                self.button.center = self.view.center
                self.button.frame.size = CGSize(width: self.width * 0.6, height: self.buttonHeight)
            }, completion: nil)
    }

    //角丸ボタンが押されたら呼ばれます
    @objc func cornerCircleButtonClicked(sender: UIButton) {
        button.setTitle("", for: .normal)
        // 丸くするアニメーション
        UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveLinear], animations: {
                self.button.layer.cornerRadius = self.radius / 2
                self.button.frame.size = CGSize(width: self.radius, height: self.radius)
                self.button.center = self.view.center
            }, completion: { _ in
                //  広がっていくアニメーション
                UIView.animate(withDuration: 0.2, delay: 0.1, options: [.curveLinear], animations: {
                        self.button.layer.cornerRadius = self.height * 1.5 / 2
                        self.button.frame.size = CGSize(width: self.height * 1.5, height: self.height * 1.5)
                        self.button.center = self.view.center
                    }, completion: { _ in
                        self.pushViewController()
                    })
            })
    }

    func pushViewController() {
        // 画面遷移＆アニメーション
        let transition = CATransition()
        transition.duration = 0.5
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
        transition.type = CATransitionType.fade
        self.navigationController?.view.layer.add(transition, forKey: nil)
        let viewController = ThirdViewController()
        self.navigationController?.pushViewController(viewController, animated: false)
    }
}
```


``` swift:ThirdViewController.swift
import UIKit

class ThirdViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.frame = view.frame
        imageView.image = UIImage(named: "icon")
        view.addSubview(imageView)
    }
}
```