🖥️

Mac Catalystアプリを良い感じにする固有テクニック2(+6)選

2023/12/16に公開

Mac CatalystはiPadOS(+iOS)対応アプリを超低コストでMacへ移植するためのツールです。

Mac Catalyst紹介

私はMac Catalystを活用して2つのiPhone/iPadアプリをMacアプリとしてリリースしました。
既存のコードをMac Catalyst向けにそのままビルドし、動作確認後に少しだけ手直しするだけで簡単に移植作業がほぼ完了しました。
良い感じのアプリにするためにMac Catalyst固有のテクニックを2つだけ用いる事となりました。
また、Mac Catalyst固有ではないiPadOS/macOS向けのテクニックをいくつか用いました。
今回はそれらを紹介します。

アプリの例

不要なビルドインのメニューバー項目を取り除く

MacにはiPhone/iPadと異なり「メニューバー」が存在します。Mac Catalystではメニューバーに多くのコマンドがビルドインされています。
対象アプリで利用しないビルドインコマンドを非表示にして見た目をスッキリさせましょう。

ビルドインコマンド

class アプリモデル: UIResponder, UIApplicationDelegate {
    override func buildMenu(with builder: UIMenuBuilder) {
        builder.remove(menu: .services)
	builder.remove(menu: .file)
	builder.remove(menu: .edit)
	builder.remove(menu: .format)
	builder.remove(menu: .toolbar)
	builder.remove(menu: .sidebar)
	builder.remove(menu: .help)
    }
}

コマンド非表示

タイトルバーを調整する

Mac Catalystアプリのウインドウには「タイトルバー」が追加されています。
アプリによってはこのタイトルバーの見た目やタイトルテキストが邪魔に感じるかもしれません。私のアプリではタイトルテキストを隠しました。

タイトル非表示

(UIApplication.shared.connectedScenes.first as? UIWindowScene)?
    .titlebar?
    .titleVisibility = .hidden

Mac Catalyst固有ではないiPadOS向けテクニック

Mac Catalyst固有のテクニックというわけではないですが、iPadOS向けテクニックをいくつか用いました。Mac CatalystきっかけでiPadアプリも一緒に改善されるというわけです。

Commands

アプリの主要な機能だけでもCommands対応するとユーザーは嬉しいでしょう。

Commands

WindowGroup { /* ... */ }
.commands {
    CommandMenu("アクション") {
        Button("リロード") { /* ... */ }
            .keyboardShortcut("r")
    }
}

ボタンにkeyboardShortcut

あるボタンに対して予想しやすい標準的なキーボード操作にはkeyboardShortcutを設定する。
例えば「シートを閉じるボタン」であればescapeキーで呼び出せるようにしましょう。

シートを閉じるボタン

Button {
    self.dismiss()
} label: {
    Image(systemName: "xmark.circle.fill")
}
.keyboardShortcut(.cancelAction)

Multiple WindowsをNO

iPhoneと異なりiPadやMac Catalystでは複数のウインドウを持つことが出来ます。Xcodeのテンプレート構成やデフォルト設定では複数ウインドウが可能になっています。
ユーザーからの想定外の入力による不具合を回避するために複数ウインドウを無効にしましょう。Info.plistの「Enable Multiple Windows」をNOにするだけです。

Enable Multiple Windows

コンテンツを横に間延びさせない

iPhone/iPadアプリが横長ウインドウとして表示される際に起きる典型的な課題の1つに「コンテンツが横に間延びしすぎて不自然になる事」です。
理想としてはアプリの構成を見直してUIを再設計する事ですが、それでは非常に多くの開発リソースを消費します。

コンテンツが横に間延び

今回は多くのiPadアプリで採用されている「コンテンツの左右に余白を追加するアプローチ」を紹介します。

余白追加

struct 余白追加: ViewModifier {
    func body(content: Content) -> some View {
        GeometryReader {
            content
                .safeAreaPadding(.horizontal,
                                 $0.size.width > 1100 ? 200 : 0)
        }
    }
}
//===================================
struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List { 
                    /* 省略 */
                }
                .modifier(余白追加())
                /* 省略 */
            }
            /* 省略 */
        }
    }
}

Mac Catalyst固有ではないmacOS向けテクニック

Mac Catalyst固有のテクニックというわけではないですが、macOS向けテクニックをいくつか用いました。これらのテクニックはフルネイティブMacアプリを開発する際にそのまま活かせます。

アプリ名をちゃんとローカライズ

iOS/iPadOSでアプリ名のローカライズをしている場合、メニューバーやアプリウインドウ内でのアプリ名は同様にローカライズが適用されます。しかしLaunchpadやDock等での表記がローカライズされません。
Info.plistのApplication has localized display name(LSHasLocalizedDisplayName)をYESにすれば解決します。

Application has localized display name

ウインドウサイズの指定

iPhone/iPadには「ウインドウサイズの指定」という概念は無いですが、Macアプリとしては必要になることもあるかもしれません。
Mac Catalystでウインドウのサイズを制限したい場合、数年前まではワークアラウンドな知識(UIKitやAppKit)が必要だったっぽいです。
Mac Catalyst17.0以降ではMacアプリ同様にwindowResizabilityやdefaultSizeでウインドウサイズの指定が簡単に可能になりました。

WindowGroup {
    ContentView()
}
.windowResizability(.contentMinSize)
.defaultSize(width: 360, height: 600)

ウインドウの最大サイズも指定したい場合

windowResizabilityをcontentSizeにして、ViewのframeでmaxWidth/maxHeightを設定すればウインドウの最大サイズを指定出来ます。

WindowGroup {
    ContentView()
        .frame(minWidth: 360, maxWidth: 600, minHeight: 600, maxHeight: 800)
}
.windowResizability(.contentSize)

しかし例外があります。ウインドウをフルスクリーンにした場合に破綻してしまうのです。

フルスクリーンにした場合に破綻

ワークアラウンドっぽい実装になりますが、UIWindowScene.sizeRestrictions.allowsFullScreenをfalseにすればフルスクリーンを無効に出来ます。

(UIApplication.shared.connectedScenes.first as? UIWindowScene)?
    .sizeRestrictions?.allowsFullScreen = false

補足: Mac Catalystの最初の1歩

Supported DestinationsMac(Mac Catalyst)を追加します。

Supported Destinations Mac

補足: Mac Catalystだけで実行するコード

#if targetEnvironment(macCatalyst)
print("ここはMacでのみ実行される")
#else
print("ここはiPhone/iPadでのみ実行される")
#endif
#if !targetEnvironment(macCatalyst)
print("ここはMacで実行されません")
#endif

関連リンク

https://developer.apple.com/jp/mac-catalyst/

https://zenn.dev/huiygfutfgvjknj/articles/eea68b91b7306c

https://apps.apple.com/app/id1620268476

Discussion