記録。

めも。

Alertを自作してみる

はじめに

普段は業務系のアプリの開発に携わっているので、 リッチなUIやUIのカスタマイズという部分は、求められることや実際の経験というのは少ないのかなと思っていたりします。

そんなことを思い始めたので、よく見るものから「どうなってるんだろ?」と調べて実装してみたので、 それを書いていきたいと思います。

UIAlertControllerを自作する

今日はこの話で書きます。

デフォルトのUIAlertControllerで実際は楽々済むのですが、 意外とちょっとした場面で見たり、今後自身が必要だと思うことが当然のようにあるのではないかと思って作ってみました。

完成はこんな感じになります。

f:id:jksdaba:20181216144508g:plain
customAlert

今日の内容はアニメーションで表示させるところまでで、実際のAlertの中のボタンやタイトル、メッセージの実装は省略します。

自作するにあたって

これは調べて見たら、そこまで複雑な実装はないことがわかりました。 以下の2つのプロトコル(1つはデリゲート)を実装することで、表示の遷移のアニメーションを表現することができました。

  • UIViewControllerAnimatedTransitioning
  • UIViewControllerTransitioningDelegate

この2つは画面遷移に関するアニメーションを実現するAPIで、これを使用することで 2つのクラスを作成して、自作のAlertを実現します。

  • AlertAnimation (アラートの表示時のアニメーションを実装する) UIViewControllerAnimatedTransitioningに準拠
  • AlertController (アラートのViewを管理する) UIViewControllerを継承、UIViewControllerTransitioningDelegateに準拠

AlertControllerを作る

画面の構成はこんな感じです。

f:id:jksdaba:20181216161216p:plain

右2つのViewから成り立っています。

  1. 右から2番目の薄いグレーのView (UIView)

これは標準のUIAlertControllerでも見るのもので、一時的にアラート以外の入力はできないようにしたり、アラートだということを示すための土台となるViewです。

  1. 一番右のView (UIView)

これが実際のアラートの画面です。今回は省略していますが、ここにボタンやタイトルなどを配置していきます。

実装コードはこのようになります。

import UIKit

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    //1. の土台のView
    lazy var baseView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.gray
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    //2. のアラートのView
    lazy var AlertView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.white
        view.layer.cornerRadius = 10
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        //init時に宣言しないと、前の画面を透過しない
        self.providesPresentationContextTransitionStyle = true
        self.definesPresentationContext = true
        self.modalPresentationStyle = UIModalPresentationStyle.custom
        self.transitioningDelegate = self
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        layoutView()
    }
    
    func layoutView() {
        view.addSubview(baseView)
        view.addSubview(AlertView)
        
        baseView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        baseView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        baseView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
        baseView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        
        AlertView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        AlertView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        AlertView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7).isActive = true
        AlertView.heightAnchor.constraint(greaterThanOrEqualTo: self.view.heightAnchor, multiplier: 0.2).isActive = true
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(true)
    }
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(false)
    }

}

また、実際にアラートっぽく表示させるためにデリゲートに準拠します。

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    //...中略
    
    //表示時のアニメーション
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(true)
    }
    
 //閉じる時のアニメーション
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AlertAnimation(false)
    }

}

returnしているAlertAnimationはアニメーションが実装されているクラスで、この後説明します。 このDelegateによってアニメーション(画面遷移)をAlertAnimationに委譲します。

Animationを実装する

画面遷移のアニメーションを実装するには、UIViewControllerAnimatedTransitioningに準拠し、 以下2つのメソッドを実装する必要があります。

    
 //アニメーションの時間
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    
 //実際のアニメーション処理
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
    

実際のコードは以下のようになりました。

class AlertAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    
    //true: dismiss
    //false: present
    let isPresent: Bool
    
    init(_ isPresent: Bool) {
        self.isPresent = isPresent
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.25
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if isPresent {
            dismissAnimation(transitionContext)
        } else {
            presentAnimation(transitionContext)
        }
    }
    
    //表示時のアニメーション
    func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        let alert = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! AlertController
        
        let container = transitionContext.containerView
        
        alert.baseView.alpha = 0
        alert.AlertView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
        
        //すでにfromのviewControllerはaddSubviewされているので、addSubviewやinsertSubviewの必要はない
        container.addSubview(alert.view)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       animations: {
                        alert.baseView.alpha = 0.7
                        alert.AlertView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) },
                       completion: { bool in
                        UIView.animate(withDuration: 0.1, animations: {
                            alert.AlertView.transform = CGAffineTransform.identity
                        })
                        transitionContext.completeTransition(true) })
        
    }
    
 //閉じる時のアニメーション
    func dismissAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        
        let alert = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! AlertController
        
        UIView.animate(withDuration: 0.3, animations: {
            alert.baseView.alpha = 0
            alert.AlertView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
        }, completion: { finished in
            transitionContext.completeTransition(true)
        })
    }
    
}

表示の時も、閉じる時も同じanimateTransition(using transitionContext: UIViewControllerContextTransitioning)が呼び出されるため、 Bool値で判定させて、処理を分岐しています。

transitionContextのviewConroller(forkey: UITransitionContextViewControllerKey)で、遷移元と遷移先のViewControllerを取得できるので、 そのViewをCGAffainTransformやUIView.animateを使用してアニメーションを実装すれば、アニメーション処理は終わりです。

画面を透過させるために

ただし、これだけだとUIAlertControllerのように遷移元の画面が透けて見えるようになりません。 それを実現するためには先ほど作成したAlertControllerのinit時に以下のプロパティを設定する必要があります。

class AlertController: UIViewController, UIViewControllerTransitioningDelegate {
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        //init時に宣言しないと、前の画面を透過しない
        self.providesPresentationContextTransitionStyle = true
        self.definesPresentationContext = true
        self.modalPresentationStyle = UIModalPresentationStyle.custom
        self.transitioningDelegate = self
    }
}

プレゼンテーションのスタイルを設定するプロパティで、これを設定することで遷移元のviewControllerの画面が透過して見えます。