LoginSignup
108
68

More than 3 years have passed since last update.

[iOS13] UIScene APIを使用する [Xcode11]

Last updated at Posted at 2019-10-14

Xcode11では、このUIScene APIのライフサイクルを使用したものが、デフォルトテンプレートに採用されています。現時点では、UIApplicationベースでも問題ないものの、UIApplicationのUIに関するメソッドが非推奨となっていることから、今後このシーンの使用が一般的になると思われます。現時点でのメモを共有したいと思います。

UISceneとは

iOS12 でのアプリケーションのプロセスは1つで、それに対するUIインスタンス(ウインドウ)も1つでした1。iOS13(UIScene)以後のアプリケーションのプロセスは1つで、それに対するUIインスタンス(ウインドウ)は複数です。マルチウインドウと言います。開発者視点では、そのウインドウをシーンと呼びます。UIScene API とは、ひとまずアプリをマルチウインドウ(マルチシーン)に対応するAPIと言えそうです。

ユーザーによるマルチウインドウ(マルチシーン)の起動方法

iOS13でマルチウインドウ対応をしたアプリは、iPadで単独のアプリから複数のウインドウを起動し表示することが可能になります。ウインドウを追加起動する操作方法を紹介します。

システムにより提供された方法

iOS9から導入されているiPadのマルチタスク機能「Split View」「Slide Over」と、特定のアプリのウインドウ一覧を表示する「App Expose」です。

Split View Slide Over App Expose

アプリにより提供する方法

アイコンのドラッグ以外のカスタムなウインドウ起動を、アプリで実装することも出来たりします。標準アプリでの例を紹介します。

  • Safari内の【タブ】や、メールappで一覧の【セル】を、画面の端にDrag&DropしSplit Viewを起動する。
  • Safariで URLリンク をロングタップすると出る「新しいウインドウで開く」のポップアップを選択し起動する。

APPスイッチャーの変化

iPadのAPPスイッチャーでは、切り替えられるUIの単位が変わっています。iOS12以前は「起動中のアプリ」を指していましたが、iOS13以降では「アプリのシーン」を切り替えることが出来るようになりました。

標準アプリでは、Safari、メッセージ、メール、カレンダー、マップなどがマルチウインドウに対応していることを確認しました。(すべてではなく未対応のものもありました。)
WWDC2019にて発表された、MacOS Catalinaでの Mac Catalyst によりiPadアプリをMacで実行可能になり「マルチウインドウ化にはこのAPIが必須だった」とのことです。

マルチウインドウに対応させる意義

  • ドキュメント系のアプリを並べて表示させられるようになる。
  • 一つ前の状態を消さずに、別の処理を行える。(例えば、地図アプリで別のルートや場所を同時に表示させたい時。)
  • Slide Over上にウインドウを並べてTo Do代わりに使用する。(例えば、メールappなどで、書きかけのメールなどを並べます。)
  • 他方を参照しなばら、もう一方を利用したい時。(例えばメッセージappなどで、他のスレッドを参照しながら会話を行うなど。)
  • アプリ内でのデータ移動に利用する。(例えば、カレンダーappでマルチウインドウ上で別の月に予定をDrag&Dropする)

シーンの採用によるView Hierarchyの変化

UIScene API.png
UIScene APIを採用すると、ビュー階層が変わりUIScreenUIWindowの間にUIWindowSceneが入ります。

iOSアプリのビュー階層を示したものかと思われます。
一番下のは端末の画面オブジェクト UIScreen.mainです。その次の、新たに加わったUIWindowSceneは、View階層のトップレベルオブジェクトとなりました。アプリのひとつのUIインスタンスを管理します。windows: [UIWindow]プロパティを持ち、関連付けられているUIWindowの参照を取得できます。この中のひとつにはSceneDelegatevar window: UIWindow?プロパティで保持するインスタンスが含まれるはずです。
複数のUIViewUIViewController上のインスタンスです。UIViewControllerのルートオブジェクトはwindow.rootViewControllerで保持されます。

Xcodeのビューデバッガの比較

(iOS12)ビューデバッガ 表示画面
スクリーンショット 2019-10-08 14.48.38.png Simulator Screen Shot - iPad Pro (11-inch) - 2019-10-08 at 14.42.35.png
(iOS13)ビューデバッガ 表示画面

iOS13(UIScene採用したもの)のキャプチャでUIWindowSceneが追加されていることがわかります。また、あえてSplit View表示にして確認したのですが、この時片側の画面(スペース)が個別のシーン(UIWindowScene)となります。冒頭のAPPスイッチャー上のイメージ図では「スナップショットが1つのシーン」と簡略化して説明されているように思うのですが、「Split Viewのウインドウは、片側の画面ずつ2つのシーン」と捉えられそうです。

シーンとセッション

UIWindowSceneは、1つまたは複数のUIWindowを含む、アプリのUIの1つのインスタンスの管理、ライフサイクルを管理します。シーンがバックグラウンドに移行しインタラクティブでなくなると、システム側で自動的に破棄が行われます。

UISceneSessionは、ユーザーが新規シーンの追加を行うと、セッションが作成され、シーンを追跡します。セッションは、一意な識別子シーンコンフィグレーションを含みます。ユーザー操作の状態UserActivityを持っています。ユーザーがアプリスイッチャーでシーンを閉じることに応じて破棄されます。システムによりシーンが開放されてもセッションは残っていて、状態復元に使用されます。

ライフサイクルの変化

iOS12 でのアプリケーションのプロセスは1つで、それに対するUIインスタンスも1つでした。「プロセスのライフサイクル(起動や終了)」「UIの状態のライフサイクル」すべてをシステムはAppDelegateに通知していました。
実際のアプリのAppDelegateでは、ワンタイムの非UIの処理(データベースへの接続やデータ構造の初期化など)を行ったあと、UIのセットアップの処理を行う、全てが行われていました。

iOS13(UIScene)以後のアプリケーションのプロセスは1つで、それに対するUIインスタンスは複数です。プロセスは複数のシーンセッションに共有されます。
それに伴いAppDelegateの責任は変わります。「プロセスのライフサイクル(起動や終了)」のみになり、新しいSceneDelegateが「UIの状態のライフサイクル」の責任を担います。(そしてAppDelegateには新しく「シーンの作成・破棄」の責任が加わります。)
UIのセットアップや、不要になったUIの取り外しの処理はSceneDelegateで行うようにします。

iOS12 / iOS13 (UIScene)
76bdb54d-905a-bc9b-15ba-bc08fac93efc.png

iOS13での、シーンライフサイクルを採用すると、「UIの状態のライフサイクル」に関するAppDelegateのデリゲートメソッドは呼びだしはされません。バックグラウンドやフォアグラウンドといったUIの状態はSceneDelegateへと通知されるようになります。移行するメソッドの内容はたいてい1対1となるためシンプルです。これらのデリゲートメソッドは移行が必要です。

iOS12 → iOS13 (UIScene)

iOS13で、マルチウインドウを採用しても、iOS12以前のサポートは可能です。単に両方のメソッドを保持して、実行時にUIKitが適切な方を呼び出します。
(iOS12以下にターゲットを変更、コンパイラのエラーFixサジェストが参考になります。)

UIScene API の採用手順

Supporting Multiple Windows on iPadのサンプルコードを参考に、具体的な手順をみていきます。

  • App TARGETS の General > [Development Info]の[Supports multiple windows]チェックボックスを有効にする
  • チェックボックスを有効にすると、XcodeによりInfo.plistファイルにUIApplicationSceneManifestキーが追加される。

このキーが追加されると、UIScene APIを用いた新しいライフサイクルメソッドが呼ばれるようになります。

  • Enable Multiple Windows 項目をNOにすると「UISceneのライフサイクルは使用するが、マルチウインドウ化はしない」となります。先ほどONにしたSupports multiple windowsのチェックがOFFになります。
  • その他の項目については Specifying the Scenes Your App Supportsを参照

シーンの構成(UISceneConfiguration)は、次の2つの方法でシステムに提供します。

(A) Info.plistにシーン構成を定義して提供。
(B) UIApplicationのデリゲートメソッドを実装してシーン構成を提供。

(A) Info.plistにシーン構成を定義

Info.plist
<key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <true/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_NAME).SceneDelegate</string>
                    <key>UISceneStoryboardFile</key>
                    <string>Main</string>
                </dict>
            </array>
        </dict>
    </dict>

(B) UIApplicationのデリゲートメソッドを実装してシーン構成を提供。

動的に行う必要がある場合は application(_:configurationForConnecting:options:) で UISceneConfiguration オブジェクトを返します。(A)のplist相当のコンフィグレーションは以下のようになります。
引数はUIViewController等ではなくUIStoryboardとなっているところが、個人的には注目ポイントだと思いました。(画面表示に必要なUIWindowUIViewControllerの両方をインスタンス化する仕組みを利用し、同時に提供できるようにしていると推察します。)

コードでのUISceneConfiguration作成
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: .windowApplication)
        configuration.delegateClass = SceneDelegate.self
        configuration.storyboard = UIStoryboard(name: "Main", bundle: .main)
    }

SceneDelegate.swiftファイルの作成

Info.plist内でデリゲート先クラスとして指定したSceneDelegate.swiftファイルを以下のように作成します。

UIの状態が復元される最小限のSceneDelegate
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {   
    // (1) windowインスタンスはシーンごとに保持する
    var window: UIWindow? 
    // (2) シーン切断時に呼ばれます。保存するシーンuserActivityを返します
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        return scene.userActivity
    }
    // (3) シーン接続時に呼ばれます。保存されたuserActivityを元にUIを復元します
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // 得られるuserActivityから具体的なUI復元(画面遷移など)を書く
        if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
            if !configure(window: window, with: userActivity) {
                print("Failed to restore DetailViewController from \(userActivity)")
            }
        }
        // アクティビティが無い場合は、何もする必要なし。コンフィグレーションで指定したStoryboardの初期VCが起動します。
    }
}
PhotoDetailViewController
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // (4)
        view.window?.windowScene?.userActivity = photo?.openDetailUserActivity
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // (4)
        view.window?.windowScene?.userActivity = nil
    }

(1) UIWindow?プロパティを宣言。iOS12までは通常、AppDelegateで保持していたところ、windowインスタンスはシーンごとにSceneDelegate で保持します。指定したMain.storyboardのイニシャルVC呼び出し時に自動的に代入されます。

(2) アプリがバックグラウンドに入り非アクティブになった時に呼ばれます。UI状態の復元が必要なシーンは、このメソッドでscene.userActivityを返します。ここで渡したuserActivityはシステムにより永続化され、セッションが消されるまでUIKitにより維持されます。状態復元が不要なsceneではnilを返せばよいと思われます。
シーンの切断状態が続くとメモリ開放のためシステムによりシーンは破棄され、セッションだけが残ります。ユーザーがAPPスイッチャに見えているのはシーンのスナップショットなので、このメソッドで保存してない場合、そのシーンを復元するためのUserActivityがscene(willConnectTo:session:options)で得られなくなります。

(3) シーン接続時に呼ばれます。シーンがセッションと接続する時、 UIScene.ConnectionOptionsUISceneSessionの参照が得られます。いずれかのuserActivityからUI復元処理を行います。他のシステムからのUserActivityの受け渡しがない、または、アプリで(前回)保存したUserActivityがない場合は、どちらもnilになります。

Appleのサンプルコードでは、特別にマルチウインドウに関係のないものもシーンベースに移行しているようで、いくつか見たところでは以下のような実装が定番となっています。

  • connectionOptions.userActivities.firstから取得できるかチェックして、次にsession.stateRestorationActivityを取得するのがお決まりのようです。

私が調査したところによると、たいてい、同じユーザーアクティビティが入るのではと思います。詳しくは ー> 【iOS13】scene(_:willConnectTo:options:)のオプションとセッションの NSUserActivity の違いについて

  • configure(window:with:)は自前のメソッドです。遷移の有無を返す戻り値のBoolは、利用しないパターンも見受けられましたので、@discardableResultを追記しても良さそうです。
configure(window\
   @discardableResult
   private func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
        if let detailViewController =  UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "DetailViewController") as? DetailViewController {
            if let navigationController = window?.rootViewController as? UINavigationController {
                navigationController.pushViewController(detailViewController, animated: false)
                detailViewController.restoreUserActivityState(activity)
                return true
            }
        }
        return false
    }

(4) UIの状態を復元したいポイントでwindowScene?.userActivityに状態復元に必要な情報を作成したNSUserActivityを入れます。システム側に(2)のメソッドで提供するアクティビティオブジェクトになります。
UIScene APIではNSUserActivityを利用し状態の保存と復元が行う手法を採用しました。Handoff APIのクラスを借りてきたとのことです。
view.window?.windowScene?.userActivity = photo?.openDetailUserActivityで保持する内容は以下のようなものです。画面のパス、記事ID、URLといった情報を持たせると良さそうです。シーンの再接続時の状態復元に必要な情報をカプセル化します。

NSUserActivityのサンプル
        let GalleryOpenDetailActivityType = "com.example.gallery.openDetail"
        let GalleryOpenDetailPath = "openDetail"
        let GalleryOpenDetailPhotoIdKey = "photoId"

        let userActivity = NSUserActivity(activityType: GalleryOpenDetailActivityType)
        userActivity.title = GalleryOpenDetailPath
        userActivity.userInfo = [GalleryOpenDetailPhotoIdKey: name]

以上が、UISceneの採用手順です。

シーンベースでのコールスタック

イベントの概要や行うとよい処理を併記しています。

◾️ユーザーがアイコンをタップし、アプリの初回起動が行われました。デリゲートメソッドが次の順序で呼ばれます。

(1) UIApplicationDelegate#application(_:didFinishLaunchingWithOptions:)
・ワンタイムの非UIなセットアップ処理(データベース接続やデータ構造の初期化)を行う。

(2) UIApplicationDelegate#application(_:configurationForConnecting:options:)
・コンフィグレーションを指定し、シーンセッションの作成を行う。
・コンフィグレーションはコードで動的に設定をするか、Info.plistで静的に行う。
・Info.plistで静的に行った場合には、name引数で参照し、connectingSceneSession.roleを渡す。(コード参照)
・コンフィグレーションには、メインシーン、アクセサリといった種類があり、アプリに併せて正しいコンテキスト選択に使用できる。
・SceneDelegateクラスの指定、初期Storyboardの指定、作成したいシーン(のサブクラス)の指定を行う。

シーンのコンフィグレーション実装例
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default", sessionRole: connectingSceneSession.role)
}

<注意>シーンの新規作成直前にしか呼ばれません。Xcodeで実行した際に毎回呼ばれるだろうと思っていたのですが、最初のシーン新規作成時だけ呼ばれて、次回以降そのセッションが生きている間、シーンがセッションへ再接続時する時はコールされないようです(多分、セッションに既にコンフィグレーションを持っているため)。
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)を実行して、意図的にセッション破棄を実行すると、他に起動していないシーンがなければ次回起動時にシーンが新規作成されコールされました(他のシーンがあればそちらが起動する模様)。また、マルチタスク機能でのシーン追加操作を行うことでも呼ばれました。

(3) UISceneDelegate#scene(_:willConnectTo:options:)

・この時点では、UIは作成されていないが、シーンセッションは作成されSceneDelegateと接続されている。

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: .ConnectionOptions) {
        window = UIWindow(windowScene: scene as! UIWindowScene)
        if let activity = options.userActivities.first ?? session.stateRestorationActivity {
            configure(window: window, with: activity)
        }
    }
}

◾️ここでユーザーがホームバーをスワイプして、ホームに戻ったとします。

(4) UISceneDelegate#sceneWillResignActive(_:)
(5) UISceneDelegate#sceneDidEnterBackground(_:)

・使用途中のテキストの下書きのようなユーザーデータはシーンの再接続時のために削除せずに保存、または保存したままにしておきます。

この後、ある時点でシステムの判断によりシーン切断が起こりえます。メモリにはシーンに関連する多量のリソースが保持されており、割り当てを解除してリソースを回収するためです。

(5.5?) UISceneDelegate#sceneDidDisconnect(_:)

◾️ユーザーがAPPスイッチャーから、シーンを上にスワイプしキルしました。

(6) UIApplicationDelegate#application(_:didDiscardSceneSessions:)
・シーンが切断され、セッションが破棄されます。
・ユーザーにより明示的に削除が実行されたので、テキストの下書きのようなユーザーデータはこのタイミングで削除できます。

新しいアプリではベストプラクティスとして推奨

Deprecated Added

UIApplication

UIWindowScene

iOS13では、UIApplication、UIApplicationDelegate からUIの状態とプロセスのライフサイクルの責任が分離されました。
これに伴いUIStatusBarUIWindowUIApplicationの管理するところではなくなり、これらのメソッドやプロパティはUIApplicationから非推奨となっています。非推奨となったのUIの状態に関するメソッドは、UIWindowSceneを使って置き換えられます。マルチウインドウを使う予定がなくても、今後マルチウインドウにしたい時に役に立つので、新しいプロパティを採用することがお勧め、とのことです。

例えば、シーンごとに、ステータスバーの色をライトモードまたはダークモードに表示できます。
Xcode11では、このUIScene APIのライフサイクルを使用したものが、デフォルトテンプレートとなっています。

プログラムからのシーンの作成・更新・破棄

引数となるセッションは、UIViewを介しても取得することが出来ます。

UIViewからUISceneSessionを取得する
   let session = view.window?.windowScene?.session

シーンの作成です。

シーンの作成
   // (A) 既存セッションから作成
   UIApplication.shared.requestSceneSessionActivation(session, userActivity: nil, options: nil)

   // (B) 新規に作成(必要に応じてアクティビティをセット)
   let activity = NSUserActivity(activityType: "com.example.MyApp.EditDocument")
   activity.userInfo["url"] = url
   UIApplication.shared.requestSceneSessionActivation(session, userActivity: activity, options: nil) 

シーンの更新。これを行うとUIが更新されたスナップショットがAPPスイッチャーに保存されます。

シーンの更新
   UIApplication.shared.requestSceneSessionRefresh(session)

シーンの破棄では、シーンを閉じる時のアニメーションを選択することが出来ます。

シーンの破棄
   let options = UIWindowScene.DestructionRequestOptions()
   options.windowDismissalAnimation = .standard // シーンを閉じる時のアニメーションを選べる
   UIApplication.shared.requestSceneSessionDestruction(session, options: options)

マルチシーンの実践・デバッグ

ステートが共有されたことによる不具合の2つの事例

その1:シーンの非同期(区別)

これらのようなオブジェクトはシングルトンとして、またはそれに近い形で利用される可能性があり、アプリの中でよく使用されます。

問題:マルチシーンでこのようなオブジェクトを扱う際、別々の処理内容として扱うべきであるのに、同時に一箇所にデータを書き込むようなことが発生しがちです。
例えば、テキストエディタアプリで、これまで編集中の内容は1つのファイルとして保存していたところ、マルチシーンでは、他方の内容がもう一方の内容を上書きしてしまい、不整合がおきました。

解決:この場合、シーン(セッション)ごとの編集中のデータが、別々のファイルに保存されるようにします。UISceneSession#persistentIdentifierを編集中のデータに加え、シーンと関連付けるIdとして利用できます。シーンごとにステートを区別出来るようにしましょう。

Before After

シーン再接続の際にscene(_:willConnectTo:options:)で、マニュアルでの状態復元データとして使用できます。シーンの状態復元が終わったら、この復元用データは忘れずにクリーンアップしましょう。
また、シーンのライフサイクルと関連づいた復元用データはapplication(_:didDiscardSceneSessions:)で削除するのが便利です。ユーザーがシーンを破棄すると呼ばれます。

その2:シーンの同期

問題:すべてのシーン間で共有されるべきUIの設定変更が、変更を実行したシーンにしか反映されていません。
(下のバーが左側のシーンにしか表示されていない↓)

解決:シーン間で共有されるべき設定値は、KVO(Key-Value Observing)で共有するのが洗練された方法です。UserDefaultsを拡張し、コンピューテッドプロパティにラップして、\UserDefaults.isInfoBarHiddenといったKVO用のKeyPathを得られるようにしておきます。vc側では変更を監視するようにします。この時、observe時のoptionには.initialを指定しておけば、変更の有無にかかわらずviewDidLoad()で一度実行されるため、UI変更を実行するコードも一箇所で済みます。

UserDefaultsの拡張
// Add a Key-Value Observable Property to UserDefaults
extension UserDefaults {
    private static let isInfoBarHiddenKey = "IsInfoBarHidden"
    @objc dynamic var isInfoBarHidden: Bool {
         get { return bool(forKey: UserDefaults.isInfoBarHiddenKey) }
         set { set(newValue, forKey: UserDefaults.isInfoBarHiddenKey) }
    }
}
vc側での変更監視
    class DocumentViewController: UIViewController {
    private var observer: NSKeyValueObservation?
    override func viewDidLoad() {
        observer = UserDefaults.standard.observe(\UserDefaults.isInfoBarHidden,
options: .initial, changeHandler: { [weak self] (_, _) in
              // 変更内容
              let controller = self?.navigationController?
              controller.isToolbarHidden = UserDefaults.standard.isInfoBarHidden
        })
     }
}

NotificationCenterを利用したシーンの同期

前段でUserDefaultsをKVOしてシーンの同期を行う事例でしたが、NotificationCenterを用いてモデルコントローラから変更通知できるようにした事例が、別の動画で紹介されていたため要約を記載します。

問題:
チャットアプリで、iOS13でマルチウインドウのサポートしました。
いま、同じ会話をSplit Viewで左右に並べて2つ、同時に表示しています。
相手にメッセージを送信したところ、更新されたのは操作を行ったシーンのみでした。(両方とも更新されるべき。)

このアプリでは、チャットでの1発言をMessageモデルとしChatModelController(シングルトン)で管理を行っています。
ChatViewControllerが送信ボタンのタップイベントを受け取り、チャットにメッセージを追加(ビューの更新)した後、モデルコントローラーにMessageの保存を依頼しています。UIインスタンスが1つなら問題ありませんでした。

Before このシーンのvc
class ChatViewController: UIViewController {
    @objc func didEnterMessage(sender: UITextField) {
        let message = Message(text: sender.text)
        // (1)自分のUIしか更新されなかった
        // Update views 
        self.animateNewRow(for: message)
        self.updateBadgeCount()
        // Update the model
        ChatModelController.shared.add(message: message)
    }
}

解決:
ビューの更新を他のシーン(UIインスタンス)へも通知する必要があります。
モデルコントローラーにNotificationCenterでの実装を施し、データ更新を監視する全てのシーンへ通知する仕組みに変更しました。

Before After

新しい型を作成し、それを更新イベントと呼びます。

After
// (2)
enum UpdateEvent {
    case NewMessage(message: Message)
    static let NewMessageNotificationName = Notification.Name(rawValue: "NewMessage")
    func post() {
        // Notify subscribers
        switch self {
        case .NewMessage(message: _):
            NotificationCenter.default.post(name: UpdateEvent.NewMessageNotificationName, object: self)
        }
    }
}

(2)UpdateEvent型を作成し、新着メッセージを付属型のenumのcaseとして定義しました。
また、NotificationCenterを実装に利用したpost()を備え、自身の値を監視するオブジェクトへ通知できるようにしています。

After
// (3)
class ChatModelController {
    static let shared = ChatModelController()
    func add(message: Message) {
        saveToDisk(message)
        let event = UpdateEvent.NewMessage(message: message)
        event.post()
    }
}

(3)モデルコントローラのadd(message:)には、これまでのMessageの永続化処理に加えて、更新イベントenumのインスタンス化とpost()の実行を行うようにしました。

class ChatViewController: UIViewController {
    @objc func didEnterMessage(sender: UITextField) {
        let message = Message(text: sender.text)
        // ((1)の処理はこの場所から削除)
        // Update the model
        ChatModelController.shared.add(message: message)
    }
    override func viewDidLoad() { 
        // (4)
        NotificationCenter.default.addObserver(self, selector: #selector(handle(notification:)), name: UpdateEvent.NewMessageNotificationName, object: nil)
    }
    // (5)
    @objc func handle(notification: Notification) {
        let event = notification.object as! UpdateEvent
        switch event {
        case .NewMessage(let newMessage):
            // (1)のコードはここへ移動された
            // Update the UI
            self.animateNewRow(for: newMessage)
            self.updateBadgeCount()
        }
    }
}

(4)viewDidLoad()でイベント監視の登録を行うようにします。モデルコントローラからの通知を受け取ることができるようになりました。
(5)handle(notification: Notification)は通知を受けとるハンドラメソッドです。
UpdateEventを列挙型(enum)にし通知オブジェクトとして利用したことにより、イベントの種類(case)によるSwitch処理も簡単で、Messageもオブジェクトから引き出すことができます。
(1)の処理はここへ移動しました。
これですべてのシーンが更新されるようになりました。(めでたし)

参考リンク


  1. 例外的にSafariのみ、iOS12のiPadでマルチウインドウ出来たようです。

108
68
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
108
68