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`を採用すると、ビュー階層が変わり`UIScreen`と`UIWindow`の間に[`UIWindowScene`](https://developer.apple.com/documentation/uikit/uiwindowscene)が入ります。iOSアプリのビュー階層を示したものかと思われます。
一番下のは端末の画面オブジェクト UIScreen.main
です。その次の、新たに加わったUIWindowScene
は、View階層のトップレベルオブジェクトとなりました。アプリのひとつのUIインスタンスを管理します。windows: [UIWindow]
プロパティを持ち、関連付けられているUIWindow
の参照を取得できます。この中のひとつにはSceneDelegate
のvar window: UIWindow?
プロパティで保持するインスタンスが含まれるはずです。
複数のUIView
はUIViewController
上のインスタンスです。UIViewController
のルートオブジェクトはwindow.rootViewController
で保持されます。
####Xcodeのビューデバッガの比較
(iOS12)ビューデバッガ | 表示画面 |
---|---|
(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) |
---|
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にシーン構成を定義
<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
となっているところが、個人的には注目ポイントだと思いました。(画面表示に必要なUIWindow
とUIViewController
の両方をインスタンス化する仕組みを利用し、同時に提供できるようにしていると推察します。)
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
ファイルを以下のように作成します。
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が起動します。
}
}
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.ConnectionOptions とUISceneSession
の参照が得られます。いずれかのuserActivityからUI復元処理を行います。他のシステムからのUserActivityの受け渡しがない、または、アプリで(前回)保存したUserActivityがない場合は、どちらもnilになります。
Appleのサンプルコードでは、特別にマルチウインドウに関係のないものもシーンベースに移行しているようで、いくつか見たところでは以下のような実装が定番となっています。
-
connectionOptions.userActivities.first
から取得できるかチェックして、次にsession.stateRestorationActivity
を取得するのがお決まりのようです。
私が調査したところによると、たいてい、同じユーザーアクティビティが入るのではと思います。詳しくは ー> 【iOS13】scene(_:willConnectTo:options:)のオプションとセッションの NSUserActivity の違いについて
-
configure(window:with:)
は自前のメソッドです。遷移の有無を返す戻り値のBoolは、利用しないパターンも見受けられましたので、@discardableResultを追記しても良さそうです。
@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といった情報を持たせると良さそうです。シーンの再接続時の状態復元に必要な情報をカプセル化します。
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の状態とプロセスのライフサイクルの責任が分離されました。
これに伴いUIStatusBar
、UIWindow
はUIApplication
の管理するところではなくなり、これらのメソッドやプロパティはUIApplication
から非推奨となっています。非推奨となったのUIの状態に関するメソッドは、UIWindowScene
を使って置き換えられます。マルチウインドウを使う予定がなくても、今後マルチウインドウにしたい時に役に立つので、新しいプロパティを採用することがお勧め、とのことです。
例えば、シーンごとに、ステータスバーの色をライトモードまたはダークモードに表示できます。
Xcode11では、このUIScene API
のライフサイクルを使用したものが、デフォルトテンプレートとなっています。
プログラムからのシーンの作成・更新・破棄
引数となるセッションは、UIViewを介しても取得することが出来ます。
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)
- シーン作成:UIApplication#requestSceneSessionActivation(_:userActivity:options:errorHandler:)
- シーン更新:UIApplication#requestSceneSessionRefresh(_:)
- シーン破棄:UIApplication#requestSceneSessionDestruction(_:options:errorHandler:)
マルチシーンの実践・デバッグ
ステートが共有されたことによる不具合の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変更を実行するコードも一箇所で済みます。
// 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) }
}
}
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つなら問題ありませんでした。
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 |
---|---|
新しい型を作成し、それを更新イベントと呼びます。
// (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()
を備え、自身の値を監視するオブジェクトへ通知できるようにしています。
// (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)の処理はここへ移動しました。
これですべてのシーンが更新されるようになりました。(めでたし)
参考リンク
- Scenes
- Introducing Multiple Windows on iPad
- Architecting Your App for Multiple Windows
- Targeting Content with Multiple Windows
-
- 例外的にSafariのみ、iOS12のiPadでマルチウインドウ出来たようです。