デバイスの回転に追随させて表示する(&フェードインアニメーション)

エンジニアの德光です。 あまり労力をかけずに、(そこそこ)ちゃんとしたアプリを開発するための小技の3回目です。…

エンジニアの德光です。
あまり労力をかけずに、(そこそこ)ちゃんとしたアプリを開発するための小技の3回目です。

◆進捗くるくる表示の改善

前回、なるべくコードを書かずに進捗くるくる表示を実装しましたが、ちょっと肉付けをしていきます。
(そもそも進捗表示してないっすよね。。。)

デバイスの回転に対応させる

▼実現方法:

デバイスが回転したら、あらためて全面表示させれば解決です。
全面表示は、初回表示と同じに、「self.frame = UIScreen.main.bounds」で良いかなと。
あとは、デバイスの回転さえ知ることができれば、なんとかなります!
ViewControllerだったら、何か override func でデバイス回転がわかるかもしれませんけど、今回はViewなので通知に頼りましょう。

さっそく、良さげな通知がないかを探してみます。
通知は「NotificationCenter」で「addObserver」で登録しますが、その通知定義名は「NSNotification.Name」として定義されています。
[Command]+Tap するなり、「Jump to Definition」するなりで定義箇所に跳んでみるも、そこに定義は列挙されておらず。。。
仕方がないので「NSNotification.Name.」と打って入力補完させて一覧を眺めてみます。
最近のXcodeは賢くなってるので、「NSNotification.Name.orientation」と打っても、途中に存在するものでも絞り込んでくれて、「UIDeviceOrientationDidChange」や「UIApplicationDidChangeStatusBarOrientation」「UIApplicationWillChangeStatusBarOrientation」が表示されます。
とりあえず、これら3種類が使えそうなので試してみましょうか。
と、それぞれの定義を確認。
なんか、userInfoで渡されるオマケ情報には向きしか入っておらず、変更後の座標とかわからないので、Will系は使い物になりませんね。

    public static let UIDeviceOrientationDidChange: NSNotification.Name

    public static let UIApplicationWillChangeStatusBarOrientation: NSNotification.Name // userInfo contains NSNumber with new orientation
    public static let UIApplicationDidChangeStatusBarOrientation: NSNotification.Name // userInfo contains NSNumber with old orientation

▼実装:

「通知の登録(と解除)」「デバイス回転を検知での全画面表示」があれば良し。
今回は通知登録をシングルトン生成時にやってしまうので、実質的に解除は不要。
くるくる表示前に通知登録して、くるくる表示完了時に通知解除するのが行儀が良いかもしれませんけどね!
(くるくる表示をしていなくても、通知をうけとって回転時の処理を実施してしまうため。。。今回は、まぁ、軽いし。。。)

    private func commonInit() {
	//  :
	// (省略)
	//  :
        self.initNotify()//通知登録(デバイス回転の通知)
    }
    //=== デバイス回転に対応させる
    func initNotify() {
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(self.handleDidChangeOrientation(_:)), name: .UIApplicationDidChangeStatusBarOrientation, object: nil)
    }
    func handleDidChangeOrientation(_ notification: Foundation.Notification) {
        self.frame = UIScreen.main.bounds    //画面全体を覆う
    }

▼実行確認:

通知を「.UIDeviceOrientationDidChange」と「.UIApplicationDidChangeStatusBarOrientation」の両方で試すと、前者だと表示の追随がワンテンポ遅れたりもするので後者を採用します!
でなわけで対応完了ってことで。
念のために「handleDidChangeOrientation()」で通知にのってくる情報を表示させてみます。

    func handleDidChangeOrientation(_ notification: Foundation.Notification) {
        print(notification.userInfo?.description)
        self.frame = UIScreen.main.bounds    //画面全体を覆う
    }

ちゃんと回転に応じて「UIApplicationStatusBarOrientationUserInfoKey」の値が表示されてますね。
「UIApplicationDidChangeStatusBarOrientation」を使うことにしたので、コメント「 userInfo contains NSNumber with old orientation」にあるように、回転前の向きが返ってきていることに注意。
そして、くるくる表示が消えた状態でデバイス回転をさせても、このログが出力されますね。
想定どおりだけど、、、やっぱダサいっすね。

てなわけで、ちゃんと通知の解除も用意してやり、「initNotify()」の呼び出しを「commonInit()」ではなく「show()」で実施し、「dismiss()」では「deinitNotify()」を呼んでやりましょう。
このくらいの手間は惜しまないように。(確認したら print(〜) は削除しておきましょうね!)

    //=== デバイス回転に対応させる
    func initNotify() {//通知登録
        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self, selector: #selector(self.handleDidChangeOrientation(_:)), name: .UIApplicationDidChangeStatusBarOrientation, object: nil)
    }
    func deinitNotify() {//通知解除
        let center = NotificationCenter.default
        center.removeObserver(self, name: .UIApplicationDidChangeStatusBarOrientation, object: nil)
    }
    func handleDidChangeOrientation(_ notification: Foundation.Notification) {
        print(notification.userInfo?.description)
        self.frame = UIScreen.main.bounds    //画面全体を覆う
    }

▼その他:

ちなみに、ViewControllerだったとしても、下記のメソッドたちはdeprecatedでしたね。

    // Notifies when rotation begins, reaches halfway point and ends.
    @available(iOS, introduced: 2.0, deprecated: 8.0, message: "Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
    open func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval)

    @available(iOS, introduced: 2.0, deprecated: 8.0)
    open func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation)

    @available(iOS, introduced: 3.0, deprecated: 8.0, message: "Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
    open func willAnimateRotation(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval)

フェードイン・フェードアウトさせる

思ったより長くなってしまったので、この項目はあっさりと。
画面が急に暗くなったり明るくなったりするのもダサいので、フェードイン・フェードアウトさせることにします。

▼実装:

    //=== 表示・非表示の制御
    func show() {
        self.initNotify()//通知登録(デバイス回転の通知)
        self.frame = UIScreen.main.bounds //画面全体を覆わせるため
        let vc = UIApplication.shared.keyWindow?.rootViewController
        vc!.view.addSubview(self)    //ビューを追加して表示させる
        self.alpha = 0.0
        UIView.animate(withDuration: 0.3, animations: {
            self.alpha = 1.0
        }) { (isFinish: Bool) in
        }
    }
    func dismiss() {
        UIView.animate(withDuration: 0.3, animations: {
            self.alpha = 0.0
        }) { (isFinish: Bool) in
            self.removeFromSuperview()  //ビューを除去して非表示にする
            self.deinitNotify()//通知解除(デバイス回転の通知)
        }
    }

さきほどのデバイス回転通知の登録はフェードイン開始前にやるけど、解除はフェードイン完了後に実施させるってことくらいが注意点でしょうか。
(このあたりは「withDuration: 0.3」の数値を増やしてアニメーション表示にかかる時間を延ばしてみれば確認できることでしょう)