コードでAutoLayout-モーダルビュー

コードでAutolayoutしながらモーダルビューです
今回はアニメーションのやり方を説明してモーダルビューを表示するとこまでやります
[コードでAutolayouts入門はこちら](https://qiita.com/sakiyamaK/items/bf15a78de3f951a0e6e9)です


#アニメーション

![demo](https://media.giphy.com/media/1kHzK5SAQyr2NjzDpN/giphy.gif)

考え方としては**表示状態の制約**と**非表示状態の制約**のふたつの制約を用意して一方のみactiveにするというものです
サンプルコードは赤色の矩形を、表示状態ではsafeArea内のトップに現れるように、非表示状態では下に隠れるようにしてます

##ソースコード
```swift
import UIKit
import TinyConstraints

class TinyConstraintsViewController:UIViewController{
  
  private weak var moveView:UIView?
  private var showConstraint:Constraint?
  private var dismissConstraint:Constraint?

  
  override func viewDidLoad() {
    super.viewDidLoad()
    //制約をつける
    self.setup()
  }

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    //状態遷移する
    self.transitState()
  }
}

extension TinyConstraintsViewController{
  
  func setup(){
    let moveView = UIView()
    self.view.addSubview(moveView)
    self.moveView = moveView
    moveView.width(100)
    moveView.height(to:moveView, moveView.widthAnchor)
    moveView.backgroundColor = .red
    
    moveView.centerXToSuperview()
    
    //非表示の制約 最初はisActiveをfalseに
    self.dismissConstraint = moveView.topToBottom(of: self.view, isActive:false)
    //表示の制約 safeArea内
    self.showConstraint = moveView.topToSuperview(usingSafeArea:true)
  }
  
  func transitState(){
    /*
     表示・非表示の状態遷移
     /////////注意点/////////
     制約は先にtrue状態のものをfalseにする
     そうしないと一瞬両方がtrueになり制約がバッティングする
     */
    if self.showConstraint!.isActive {
      self.showConstraint!.isActive = false
      self.dismissConstraint?.isActive = true
    }
    else{
      self.dismissConstraint?.isActive = false
      self.showConstraint!.isActive = true
    }
    
    //1秒かけて制約の状態をviewに反映させる
    UIView.animate(withDuration: 1) {
      self.view.layoutIfNeeded()
    }
  }
}
```

##解説
実はTinyConstraintsでは制約をつけたメソッドを呼ぶと戻り値として`Constraint`もしくは`[Constraint]`の型を返してます
それをクラス変数に代入しておき状態遷移時に`true`と`false`を切り替えます
またふたつの制約が同時に実行されると矛盾するので一方の`isActive`パラメータを`false`にして制約が効かないようにします
アニメーション自体の切り替えは
・どういう制約からどういう制約に変えるのか指定する
・アニメーションメソッドでlayoutIfNeededを呼ぶ
という流れになります

##注意点
ソースのコメントにも書いてますが、**表示状態の制約**と**非表示状態の制約**が両方`true`になるタイミングがあるとxcodeが警告を吐きまくります
両方の制約の`isActive`を`false`にしてから、必要な制約の`isActive`を`true`にしましょう


#モーダルビュー
アニメーションを応用して下から出てくるモーダルビューを自作します
ソースコードはモーダルビューとそれを表示させるコントローラーのふたつです
![demo](https://media.giphy.com/media/3krBkZD5xJyliVaVKq/giphy.gif)

## ソースコード

```ModalView.swift
import UIKit
import TinyConstraints

class ModalView: UIView {
  
  var isShowing:Bool = false
  
  private weak var backgroundView:UIView?
  private weak var slideView:UIView?
  private weak var safeAreaView:UIView?
  
  private var showConstraint:Constraint?
  private var dismissConstraint:Constraint?

  convenience init() {self.init(frame: UIScreen.main.bounds)}
  override init(frame:CGRect) {
    super.init(frame: frame)
    self.setup()
  }
  
  override func didMoveToWindow() {
    super.didMoveToWindow()
    //windowに貼り付けたあとに制約をつける
    self.centerInSuperview()
  }
  required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
  
  private func setup() {
    //自身のサイズ
    self.backgroundColor = .clear
    self.alpha = 0
    self.size(UIScreen.main.bounds.size)

    //背景をブラックアウトさせるview
    let backgroundView = UIView()
    self.addSubview(backgroundView)
    self.backgroundView = backgroundView
    backgroundView.edgesToSuperview()
    backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.8)

    //スライドさせるview
    let slideView = UIView()
    self.addSubview(slideView)
    self.slideView = slideView
    slideView.backgroundColor = .clear
    slideView.size(to: self)
    slideView.centerXToSuperview()
    //表示の制約
    self.dismissConstraint = slideView.topToBottom(of: self)
    //非表示の制約
    self.showConstraint = slideView.topToSuperview(isActive: false)
    do{
      //セーフエリア内のview
      let safeAreaView = UIView()
      slideView.addSubview(safeAreaView)
      self.safeAreaView = safeAreaView
      safeAreaView.layer.masksToBounds = true
      if #available(iOS 11.0, *) {
        safeAreaView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
      } else {
        // Fallback on earlier versions
      }
      safeAreaView.layer.cornerRadius = 40.0
      safeAreaView.backgroundColor = .red
      safeAreaView.widthToSuperview(offset:-14)
      safeAreaView.centerXToSuperview()
      safeAreaView.bottomToSuperview(usingSafeArea:true)
      safeAreaView.topToSuperview(usingSafeArea:true)
      
      //セーフエリア内にコンテンツを入れる
      self.setMainContents(mainView: safeAreaView)

      //セーフエリアの外のview
      let bottomAreaView = UIView()
      slideView.addSubview(bottomAreaView)
      bottomAreaView.backgroundColor = safeAreaView.backgroundColor
      bottomAreaView.width(to: safeAreaView)
      bottomAreaView.centerXToSuperview()
      bottomAreaView.topToBottom(of: safeAreaView)
      bottomAreaView.bottomToSuperview()
    }
  }
  
  func show(_ animated:Bool = true, complition:(()->())? = nil) {
    self.alpha = 1
    //表示と非表示の制約を入れ替える
    self.dismissConstraint?.isActive = false
    self.showConstraint?.isActive = true
    let duration = animated ? 0.4 : 0.0
    UIView.animate(withDuration: duration, animations: {
      self.backgroundView?.alpha = 1
      self.layoutIfNeeded()
    }) { (isComp) in
      self.isShowing = true
      complition?()
    }
  }
  
  func hide(_ animated:Bool=true, complition:(()->())? = nil) {
    //表示と非表示の制約を入れ替える
    self.showConstraint?.isActive = false
    self.dismissConstraint?.isActive = true
    let duration = animated ? 0.4 : 0.0
    UIView.animate(withDuration: duration, animations: {
      self.backgroundView?.alpha = 0
      self.layoutIfNeeded()
    }) { (isComp) in
      self.alpha = 0
      self.isShowing = false
      complition?()
    }
  }
  
  private func setMainContents(mainView:UIView?){
    guard let mainView = mainView else { return }

    let topLabel = UILabel()
    mainView.addSubview(topLabel)
    topLabel.backgroundColor = .white
    topLabel.text = "モーダルの一番上だよ"
    topLabel.sizeToFit()
    topLabel.centerXToSuperview()
    topLabel.topToSuperview()
    
    let bottomLabel = UILabel()
    mainView.addSubview(bottomLabel)
    bottomLabel.backgroundColor = .white
    bottomLabel.text = "モーダルの一番下だよ"
    bottomLabel.sizeToFit()
    bottomLabel.centerXToSuperview()
    bottomLabel.bottomToSuperview()
  }
}
```

```ViewController.swift
import UIKit
import TinyConstraints

class ViewController:UIViewController{
  
  var modalView:ModalView?=nil
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.view.backgroundColor = .white
    
    //モーダルビューをセット
    let modalView = ModalView()
    self.view.addSubview(modalView)
    self.modalView = modalView
  }
  
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let modalView = self.modalView else { return }
    //非表示、非表示を切り替える
    if modalView.isShowing {
      modalView.dismiss()
    }
    else{
      modalView.show()
    }
  }
}
```

##解説
これまでの組み合わせなので解説することもないのですが一応。。。
モーダル自体はセーフエリアの外まで伸びているため、セーフエリア内の`safeAreaView`とセーフエリア外の`bottomAreaView`を用意してひとつのviewのように見せかけてます
実際にコンテンツを貼っていくのは`safeAreaView`内だけにしておくとセーフエリアを気にすることなくどの端末でも同じ操作性を保てます