Help us understand the problem. What is going on with this article?

Mac CatalystでiOSアプリとMacアプリを同時に作る

More than 1 year has passed since last update.

はじめに

iOSアプリがmacOSで動くようになる *1

今年、Appleから発表されたMac Catalyst *2。Mac Catalystとは、iOSアプリのソースコードを流用してMacアプリを開発できるというツールセットです。TwitterがMac Catalystを使ってMac版デスクトップアプリを復活させる*3 など、既に商用アプリでも使われはじめています。今回はこのMac Catalystを使ってiOSアプリとMacアプリを開発し、その使い方を確認しました。

アプリ概要

今回作ったものは、とても簡単な手書きノートアプリです。App Storeにリリースしているので、以下リンクからインストールできます。なお、Mac Catalyst対応アプリはmacOS 10.15以上のPCでインストールできます。

Blue Sketch - iOSアプリ
Blue Sketch - Macアプリ

1stバージョンをリリースするまでの開発期間は3日+App Store申請に1日です。

iPad版とMac版のスクリーンショットを以下に載せます。

キャンバス画面: この青いキャンバス上に白線を書くことができます。
Qiita1.png

UIAlertController: Mac版は上からニョキッと出てくる感じです。
Qiita2.png

UIActivityViewController: 画像をエクスポートするために使っていますが、Mac版でも普通に動きます。
Qiita3.png

ご覧の通り、UIKitがそのままmacOSで使える感じです。

使用ライブラリ

Name Version 内容
Default 3.0.0 UserDefaultsのラッパー
MKAToggleButton 1.2.3 自作のマルチステートButtonライブラリ
R.swift 5.1.0 リソースファイルをいい感じに扱うライブラリ
XCGLogger 7.0.0 ロガー

すべてCocoaPodsでインストールしています。

最初はモバイルDBにRealmを使おうかと思いましたが、Mac Catalystでうまくビルドができずやめました。今回は、UserDefaultsとファイル保存でユーザデータを管理しています。

その他、Firebaseや広告SDKも試してみたいですが、今回は未検証です。

開発

ここからはアプリ開発において得た知見を書いていきます。

開発環境

Version
macOS 10.15.1
Xcode 11.3
Swift 5

Mac Catalyst対応

iOSアプリをMac Catalystに対応する手順は以下になります。

  1. Mac Catalyst対応するTarget > General > Deployment Info > Macにチェック

    スクリーンショット 2019-12-18 20.58.25.png

  2. My Macを選択して▶Runをクリック

    スクリーンショット 2019-12-13 23.46.58.png

    ビルドを開始します。無事ビルドが通れば成功です。

Macアプリ向けにカスタマイズ

ビルドが通ればMacアプリとして使うことはできますが、このままではデスクトップアプリケーションらしさが足りないため、いくつかのカスタマイズをします。

AppIcon

Macアプリ用にアイコンを指定しなければ、iOSアプリのアイコンが使われます。しかし、Macアプリのアイコンは、iOSアプリのように角丸正方形である必要はありません。折角なので、Macアプリ用にデザインを少し変えてみたいと思います。

手順

  1. Assets.xcassets > AppIconを開く
  2. 右パネルのMac Allにチェック
  3. Macアプリ用のアイコンホルダーができるので各画像ファイルをDrag & Drop

スクリーンショット 2019-12-18 21.13.12.png

LaunchScreen

Mac CatalystはLaunchScreenに対応していないようです。何か設定方法があるのかもしれませんが、今回はiOSアプリのみ対応しています。

ダークモード対応

特に手を加えることはなかったです。UIKitのダークモード対応がそのまま使えました。

Mac Catalystかどうかの判定

Mac Catalystの時だけ処理したい場合は以下のように書けます。

#if targetEnvironment(macCatalyst)
print("Mac Catalystだよ")
#else
print("Mac Catalystじゃないよ")
#endif

ちなみに、#available(iOS 13.0, *)を使った判定はMac Catalystでも通ります。APIとしてはiOSだからということでしょうか。

if #available(iOS 13.0, *) {
    print("Mac Catalystも通るよ")
}

タイトルバーとステータスバー

Macアプリにはタイトルバーがあります↓
スクリーンショット 2019-12-18 21.31.33.png

タイトルバーを消してモダンなデザインにしてみます↓
スクリーンショット 2019-12-18 21.27.08.png

ViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()

    #if targetEnvironment(macCatalyst)
    if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
        appDelegate.window?.windowScene?.titlebar?.titleVisibility = .hidden
    }
    #endif
}

titlebarはMac CatalystにしかないAPIのため#if targetEnvironment(macCatalyst) ~ #endifで囲みます。

なお、Macアプリにはステータスバーが無いため、statusBarFrameは(0, 0, 0, 0)を返します。

ViewController.swift
var statusBarFrame: CGRect {
    if #available(iOS 13.0, *) {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
              let statusBarManager = appDelegate.window?.windowScene?.statusBarManager else {
            return .zero
        }
        return statusBarManager.statusBarFrame  // => (0, 0, 0, 0) on Mac Catalyst
    }
    else {
        return UIApplication.shared.statusBarFrame
    }
}

メニューバー

Macアプリにはメニューバーがあります。🍎マークが並んでいるバーですね。デフォルトでは以下のようなメニューが並んでいます。

スクリーンショット 2019-12-18 23.25.42.png

アプリで使わないメニューがあるので以下のようにカスタマイズします。

スクリーンショット 2019-12-18 23.27.43.png

UIResponderクラスにメニューを構築するbuildMenu(with:)メソッドがあるので、これをオーバーライドします。

AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...

    #if targetEnvironment(macCatalyst)
    override func buildMenu(with builder: UIMenuBuilder) {
        guard builder.system == UIMenuSystem.main else { return }

        // 初期メニューで不要なものを削除
        builder.remove(menu: .file)
        builder.remove(menu: .format)
        builder.remove(menu: .help)
        builder.remove(menu: .services)
    }
    #endif
}

また、ショートカットキーをメニューに割り当てることもできます。今回作ったアプリもUndo/RedoをそれぞれCommand+z/Command+Shift+zに割り当てています。

AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    ...

    #if targetEnvironment(macCatalyst)
    override func buildMenu(with builder: UIMenuBuilder) {
        ...

        // Undo/Redoメニューの追加
        let undoAction = UIKeyCommand(title: "Undo",
                                      action: #selector(handleUndoMenu(_:)),
                                      input: "z",
                                      modifierFlags: [.command])
        let redoAction = UIKeyCommand(title: "Redo",
                                      action: #selector(handleRedoMenu(_:)),
                                      input: "z",
                                      modifierFlags: [.command, .shift])
        let editMenu   = UIMenu(title: "Edit",
                                image: nil,
                                identifier: "com.example.MyApp.editMenu",
                                options: .destructive,
                                children: [undoAction, redoAction])
        builder.insertSibling(editMenu, afterMenu: .application)
    }

    @objc func handleUndoMenu(_ command: UIKeyCommand) {
        // TODO: Undo処理
    }

    @objc func handleRedoMenu(_ command: UIKeyCommand) {
        // TODO: Redo処理
    }
    #endif
}

詳細は長くなるため、また別の機会に書きたいと思います。

ボタンのマウスオーバー(Hover)対応

Macアプリでは、マウスカーソルをボタンに重ねた時のスタイルを設定できます。

hover_button.gif

下記のコードは、マウスオーバー時にボタンのタイトルと枠線をハイライトする例です。

ViewController.swift
private let normalColor = UIColor.blue
private let hoverColor  = UIColor.cyan

private lazy var closeButton: UIButton = {
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 160.0, height: 44.0))
    button.setTitle("Close", for: .normal)
    button.setTitleColor(normalColor, for: .normal)
    button.layer.cornerRadius = 5.0
    button.layer.borderWidth = 1.0
    button.layer.borderColor = normalColor.cgColor
    button.addTarget(self, action: #selector(closeButtonClicked(_:)), for: .touchUpInside)

    if #available(iOS 13.0, *) {
        let hover = UIHoverGestureRecognizer(target: self, action: #selector(hovering(_:)))
        button.addGestureRecognizer(hover)
    }

    return button
}()

@available(iOS 13.0, *)
@objc func hovering(_ recognizer: UIHoverGestureRecognizer) {
    guard let button = recognizer.view as? UIButton else { return }

    switch recognizer.state {
        case .began, .changed:
            button.setTitleColor(hoverColor, for: .normal)
            button.layer.borderColor = hoverColor.cgColor
        case .ended:
            button.setTitleColor(normalColor, for: .normal)
            button.layer.borderColor = normalColor.cgColor
        default:
            break
    }
}

@objc func closeButtonClicked(_ sender: UIButton) {
    dismiss(animated: true)
}

なお、UIHoverGestureRecognizerは今のところiOSアプリには対応していないようです(指を重ねても何も起きません)。actionに指定したhovering(_:)メソッドが呼ばれていませんでした。

ウィンドウサイズの変更に追従する

Macアプリは、ウィンドウ枠をマウスドラッグしてウィンドウサイズが可変なため、ドラッグ中のレンダリング処理を見直す必要がありました。ドラッグ中はUIViewControllerのviewWillLayoutSubviews()およびviewDidLayoutSubviews()が繰り返し呼ばれるため、キャンバスの更新処理を追記しました。

CanvasViewController.swift
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // TODO: キャンバスの再計算
}

UIScreen.main.bounds/nativeBoundsの値が謎

Macアプリの場合、UIScreen.main.boundsおよびUIScreen.main.nativeBoundsの値が、アプリのウィンドウサイズでもなく、Macのディスプレイサイズとも異なる値を返してくるため、このプロパティに依存したコードを書いていると影響が出るかもしれません。UIScreen.main.boundsを使わないように修正する必要がありました。

App Store申請

今回、iOSアプリをApp Storeへ、MacアプリをMac App Storeへそれぞれ申請しました。申請手順については以下の記事で紹介しています。
iOSアプリとMac CatalystアプリのApp Store申請手順

まとめ

今回はView(見た目)の調整を中心にアプリを作りました。CameraやBluetooth、Audioなどハードウェアの利用、AWSやFirebaseといった外部サービス連携などは試せていませんが、少ない労力でiOS向けとmacOS向けのアプリを作ることができました。
特に、iOS開発に慣れた人がmacOS向けのアプリを開発する場合、Mac Catalystは短期間で高品質のアプリを作る有効な手段になるかもしれません。

参考

  1. 「この秋からiOSアプリがmacOSで動くようになる」, TechCrunch
    https://jp.techcrunch.com/2019/06/04/2019-06-03-ios-apps-will-run-on-macos-with-project-catalyst/

  2. 「iPad用AppをMacへ」, Apple
    https://developer.apple.com/jp/mac-catalyst/

  3. 「Twitter公式アプリのMac版がMac Catalystアプリとして復活」, TechCrunch
    https://jp.techcrunch.com/2019/10/12/2019-10-11-twitter-comes-back-to-the-mac/

hituziando
Android/iOS/macOS app developer, Front-end engineer, Rails engineer
https://hituzi-ando.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away