iPhone Duoに関する6本のTech Talksには、Objective-Cのコードが一行もありません。予想どおりです——Appleは新しいものをSwiftとSwiftUIで見せます。しかし業界の現実は別物です。銀行アプリ、メッセンジャー、2010年代から書かれ5回のリデザインを生き延びてきたものすべてが、ObjCの中核を抱えています。iOSでの8年間で私はそうしたコードベースを十分に見てきたので、断言できます。「Swiftに書き直してから対応する」は、永遠に実行されない計画です。適応させるべきは、いま手元にあるコードです。
朗報:Duo対応に必要なもののほぼすべてはUIKitに住んでいて、UIKitは今でもObjective-Cを話します。この記事では、直接使えるもの、UIKitの対応物経由で使えるもの、そしてSwift層なしでは済まない2箇所を整理します。シグネチャは2026年9月のTech Talks由来です。安定版Xcode 27.1が出るまでは、綴りをSDKヘッダと照合してください。
ステップ0:再ビルドと監査#
最初のアクションは言語を問いません。プロジェクトをXcode 27.1でiOS 27.1 SDK向けにビルドすることです。これなしにはedge-to-edgeも縦型バーも新APIもなく、アプリは画面の端に余白をあてがわれるだけです。
次はgrepです。以下のパターンにヒットしたものは、どれもDuo上の潜在的な不具合です。
[UIScreen mainScreen] → ambiguous with two screens
interfaceOrientation → the inner display ignores orientations
UI_USER_INTERFACE_IDIOM() → Duo is not a new idiom; idiom branches break
userInterfaceIdiom
safeAreaInsets.left * 2 → insets are asymmetric
hardcoded widths (390, 428, ...) → width changes with a movement of the hand古いコードベースでは[[UIScreen mainScreen] bounds]が何十箇所も見つかります——フレーム計算、コレクションの構成、サイズのキャッシュ。置き換えはこうです。
// Scale comes from the trait collection, not from the screen
CGFloat scale = self.traitCollection.displayScale;
// If you really need the screen (rare) — via the window scene
UIScreen *screen = self.view.window.windowScene.screen;
// Layout size — own view bounds inset by the safe area
CGRect content = UIEdgeInsetsInsetRect(self.view.bounds,
self.view.safeAreaInsets);向きの代わりにsize classes——昔ながらの、しかし本気の#
Trait collectionsのAPIはiOS 8から変わっていません。変わったのは、それを無視することの代償です。ポートレート固定のアプリでも、Duoの内側ディスプレイではsupportedInterfaceOrientationsに関係なく回転します。つまり「ランドスケープでは表示を変える」というロジックはすべてsize classesに引っ越さなければなりません。
- (void)traitCollectionDidChange:(UITraitCollection *)previous {
[super traitCollectionDidChange:previous];
if (previous.horizontalSizeClass == self.traitCollection.horizontalSizeClass) {
return;
}
BOOL isWide = self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular;
// compact — Duo's outer screen (and every regular iPhone),
// regular — the inner screen (and iPad)
[self rebuildLayoutForWideMode:isWide];
}iOS 17以降なら、traitCollectionDidChange:のオーバーライドの代わりに登録型の監視(registerForTraitChanges:)が使えます。こちらもObjective-Cに公開されています。
監査の独立した項目として、Auto Layoutと手動フレーム計算の対比があります。safeAreaLayoutGuideへの制約はDuoを無修正で生き延びます。self.view.frame.size.widthにオフセットを足す手計算のフレームは生き延びません。移行の優先度は後者に与えるべきです。
バー:自作を一掃し、システムをマークアップする#
Tech Talk 111462のルールは、legacyコードに最も強く突き刺さります。縦型レイアウトに参加するのはシステムコンテナ——UINavigationControllerとUITabBarController——だけです。サブビューとして追加したカスタムUIToolbarも、ボタンを並べたUIView製の自作「ヘッダー」も、横向きのまま残り、システムの縦型バーと衝突します。
バーがシステム製なら、新しい挙動のマークアップは丸ごとObjective-Cで書けます。
// Pin the primary action in the vertical bar
self.navigationItem.pinnedTrailingGroup =
[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil
items:@[sendButton]];
// Custom back/close is the leading item; opt out of the system back button
self.navigationItem.leftItemsSupplementBackButton = NO;
// Secondary actions go to the overflow menu
self.navigationItem.additionalOverflowItems =
[UIDeferredMenuElement elementWithProvider:^(void (^completion)(NSArray *)) {
completion(@[self.shareAction, self.exportAction]);
}];
// Priorities: what moves to overflow first when space runs out
filterButton.visibilityPriority = UIBarButtonItemVisibilityPriorityLow;
// A badge instead of a text counter
inboxButton.badge = [UIBarButtonItemBadge countBadgeWithInteger:7];縦型バーは幅が狭く、中に入る要素はアイコンのみです。「すべて送信」のようなテキストボタンやsegmented controlは押し込めないので、そうした要素はnavigation barに残します。特定のコントローラーでのオプトアウトはpreferredVerticalBarBehaviorのオーバーライドです。
シーン:返済のときが来た負債#
多くのObjCアプリは今もAppDelegate中心のライフサイクルのまま、シーンへの移行を「いつかそのうち」と先送りしてきました。Duoはその「いつか」に日付を入れました。同一アプリの複数ウィンドウと全アプリ対象のSplit Viewを備えた、初のiPhoneだからです。移行自体は標準的です(Info.plistにUIApplicationSupportsMultipleScenesを含むUIApplicationSceneManifest、AppDelegateとの結合の代わりにUIWindowSceneDelegate)——Duo専用のキーはありません。
新しい挙動はひとつだけですが、狡猾です。ウィンドウは内側ディスプレイでしか作成されません。閉じた状態のデバイスではシーンのリクエストが失敗します。
UISceneSessionActivationRequest *request =
[UISceneSessionActivationRequest requestWithRole:UIWindowSceneSessionRoleApplication];
[UIApplication.sharedApplication activateSceneSessionForRequest:request
errorHandler:^(NSError *error) {
// Device is closed: no new windows — show in the current window
[self openDocumentInCurrentWindow];
}];UI内の「新しいウィンドウで開く」ボタンは、UIWindowSceneのシステムactivationアフォーダンス(ウィンドウが使えないときは自動で隠れます)を使うか、エラーを誠実にハンドリングするか、どちらかにすべきです。
Reserved regionsとヒンジをObjective-Cから#
折り目とカメラのジオメトリはUIViewレベルで取得できます。
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
if (folds.count > 0) {
CGRect foldFrame = folds.firstObject.frame;
// For example: keep the floating button out of this rectangle
}UIHingeInteractionはUIInteractionファミリーなのでObjC互換です:[view addInteraction:]。ただしヒンジの角度に手を伸ばす前に、それが本当に必要かを自問すべきです。角度からのlayoutはAppleが明確に禁じていますし、システムコンポーネント(sheet、alert、メニュー、システムバー)はあなたの関与なしに折り目を避けてくれます。
それでもSwiftが必要になる場所#
正直な境界線はこうです。SwiftUI限定のAPI——onHingeChange、ArrangementView、CameraCaptureAccessory付きの.sceneAccessory——はObjective-Cから使えませんが、前者2つには本格的なUIKit対応物(UIHingeInteraction、UIArrangementViewController)があり、大半のlegacyアプリにはそれで十分です。
Swift層が必要になるのは2つのケースです。第1に、モジュールを明示的に管理するカメラアプリ。AVCaptureDeviceDirectionCoordinatorはmain actorに隔離され、デバイスをAVCaptureDeviceDescriptor経由で渡します。ObjCから扱うことは形式上可能ですが、Swift Concurrencyのactorモデル自体がObjCには存在しないため、カメラ調整は独立したSwift型として実装し、ObjC互換のファサードを被せる方が安全です。第2に、外側画面のテレプロンプターのようなdual-display機能。CameraCaptureAccessoryはSwiftUIのscene accessory APIとして宣言されています。
パターンはいつも同じです。新APIをカプセル化し、legacyコードにdelegateかblockを公開する、小さな@objcクラスをSwiftで書く。HealthKitもWidgetKitもApp Intentsもこうやって乗り越えてきました——この点でDuoは何も新しいことを発明していません。
今四半期の計画#
リストにまとめると、ObjCアプリの適応はこうなります。
- iOS 27.1 SDKでの再ビルド、Duoシミュレータ(Device Hub)での動作確認。
- Grep監査:
mainScreen、向き、イディオム、対称前提のインセット、幅のハードコード。 - 自作バーのシステムコンテナへの移行+pinned/overflowのマークアップ。
- シーン:マニフェスト、
UIWindowSceneDelegate、ウィンドウ作成失敗のハンドリング。 - プロダクトに必要なら、カメラとdual-display向けのピンポイントなSwift層。
項目1〜2は数日、3〜4は数週間で溜め込んだ負債の量に依存し、項目5は全員に必要なわけではありません。デバイスがユーザーの手に渡る10月23日まで、最初の2項目をこなす時間はどのチームにもあります。



