ViewIsAppearingの備忘録 ~画面描画前にレイアウトを計算・変更する~

## はじめに
WWDC23(2023 Apple Worldwide Developers Confere)でUIKitの新機能が色々発表され、その中に一度ドロ沼で苦労した機能の手軽な解決策になりそうな```viewIsAppearing(_:)```を見つけました。今後の利用頻度も高そうなので備忘録としてまとめました。

公式ドキュメント

https://developer.apple.com/documentation/uikit/uiviewcontroller/4195485-viewisappearing

## viewIsAppearingの概要
### 機能概要
UIViewControllerのInstance Methodで画面が表示される時に呼び出されます。

### 対象バージョン
開発環境としてXcode15が必要で、iOS13以降で利用可能

### 呼び出されるタイミング
ざっくりまとめてしまうと```viewWillAppear(_:)```の後、```viewDidAppear(_:)```の前に一度の画面遷移で一回のみ呼び出されます。
ただし、```viewWillAppear(_:)```と```viewIsAppearing(_:)```のコールバックは同一のトランザクション内で実行されるため、両者で行われた変更は同時にユーザに表示されます。
```viewWillLayoutSubviews(_:)```と```viewDidLayoutSubviews(_:)```も同様のタイミングですが、これらは複数回呼び出されます。（※layoutSubviews() でも呼び出されます。）
また、```viewIsAppearing(_:)```は```viewDidAppear(_:)```と異なり画面遷移のアニメーション前に呼び出されます。

![viewIsAppearing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/c1b04a29-0f62-d4c5-a388-0a50aa0b4322.png)
[公式サイト](https://developer.apple.com/documentation/uikit/uiviewcontroller/4195485-viewisappearing)より引用

### 特性と使い方
```viewIsAppearing(_:)```は```viewWillAppear(_:)```と同じく画面遷移のアニメーション前に呼び出されますが、```viewWillAppear(_:)```と異なりビューのサイズや Trait Collection は決定してるため、それらをインプットにして画面描画前にレイアウトに対し変更を加えたい場合に使用します。
一例として、各種画面サイズとオブジェクトサイズに応じてレイアウトを変更するような場合にはそれぞれで分岐を入れていくのは現実的でなく、画面サイズオフセットを使うにも画面描画完了まで画面サイズが正しく取れないといったケースが考えられます。
そういったケースを```viewIsAppearing(_:)```がスマートに解決してくれます。

## 実装例
### 遷移元画面（ボタンを配置し、クリックイベントをtoSecondView(_ sender: Any) に紐付け）
```swift
import UIKit

class ViewController: UIViewController {
        
    @IBAction func toSecondView(_ sender: Any) {
        let storyboard = UIStoryboard(name: "SecondView", bundle: nil)
        guard let secondViewController = storyboard.instantiateViewController(withIdentifier: "secondView") as?　SecondViewController else {
            return
        }
        
        self.navigationController?.pushViewController(secondViewController, animated: true)
    }
}
```

### 遷移先画面（Horizontal Stack ViewにLabelを横並びで配置）
```swift
import UIKit

class SecondViewController: UIViewController {
    
    @IBOutlet weak var stackView: UIStackView!
    @IBOutlet weak var heightLabel: UILabel!
    @IBOutlet weak var randomStringLabel: UILabel!
    
    public var randomString = "1234567890123456789"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.randomStringLabel.text = self.randomString
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // レイアウト修正
        setStackViewAxis()
    }
    
    override func viewIsAppearing(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // レイアウト修正
//        setStackViewAxis()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // レイアウト修正
//        setStackViewAxis()
    }
    
    func setStackViewAxis() {
        // 画面内に収まらない場合にはStackViewを縦並びに変更
        if self.view.readableContentGuide.layoutFrame.width < self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width {
            self.stackView.axis = .vertical
            self.randomStringLabel.numberOfLines = 0
            self.heightLabel.text = "Vertical"
            
        }
    }
}
```

## 実際の動き
### viewWillAppearでレイアウト修正
self.view.readableContentGuide.layoutFrame.widthが正しく取得できずif判定がfalseになり、ラベルが横並びのまま画面に収まりきらず右端が三点リーダーで丸められる。
![viewWillAppear.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/22cd0b35-de3d-5ded-b219-fc905f485836.gif)

### viewIsAppearingでレイアウト修正
ラベルが縦並びに変更済みで画面が表示される。
![viewIsAppearing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/914da70b-31e9-ee5c-8153-a96e9aa8882c.gif)

### viewDidAppearでレイアウト修正
画面表示完了後、ラベルが横並びから縦並びに変更される。
![viewDidAppear.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/5378ebbb-7410-ce57-ed3a-0f0aa0a82970.gif)

## まとめ
以前、端末の画面サイズとオブジェクトサイズの関係で実装が複雑化し、可読性・保守性もかなぐり捨てて画面サイズオフセットとハードコーディング地獄の実装をしてしまったことがありました。
他にも```viewIsAppearing(_:)```発表前は```viewDidAppear(_:)```でレイアウト変更を行なっていましたが、画面のカクツキを見せないためにローディングビューを画面上に重ねたりしていました。
手軽に自然な形で実装できるようになり大変嬉しい限りです。