はじめに
前回、Mac Catalyst
でAppkit
固有のAPIを使用する方法について、SwiftUIで書いた「逆ポーランド記法で入力する電卓アプリ」を例に紹介した。
前回の記事はこちら
今回は、同じアプリにて、Mac Catalyst
の場合に『常に手前に表示(Always on Top)』を実装する方法について説明する。Mac Catalyst
アプリでAppKit
を使用する、より実用的な内容である。
1. AppKitによるAlways on Top
の実装
これの実装は、ごくごく簡単で、mainWindowのlevelプロパティを設定するだけ。
guard let window = NSApplication.shared.mainWindow else { return }
if isOnTop {
window.level = .floating //最前面に固定
} else {
window.level = .normal //通常
}
次に、『「常に手前に表示」する/しない』 の選択を、アプリのウィンドウタイトルバー(ツールバー)に置いたアイコンのクリックにて、トグル式に切り替えられるようにする。
アイコン (通常時) |
![]() |
---|---|
アイコン (常に手前に表示) |
![]() |
アイコンのイメージデータは、上のような2種類を用意した。サイズは30×30程度。
2. アプリのウィンドウタイトルバーにアイコンを置く
アプリのウィンドウタイトルバーにアイコンを置くためには、いくつかの手順を踏む必要があり、順に説明していく。
-
NSToolbarDelegate
を実装する -
UIWindowSceneDelegate
を実装する
2.1 NSToolbarDelegate
を実装する
-
NSToolbarDelegate
はAppKitのAPIであるため、ToolbarDelegate
独自クラスでラップしてiOSでもコンパイルが通るようにする -
ToolbarDelegate
クラスのextensionでNSToolbarDelegate
を実装する。このextensionはコンパイルオプションでMac Catalyst
のみ有効とする
新規にSwiftファイルを追加し、ToolbarDelegate.swiftと名付ける。内容は以下の通り。
class ToolbarDelegate: NSObject {
private var pinned = false
}
#if targetEnvironment(macCatalyst)
extension ToolbarDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.label = "Always on Top"
toolbarItem.paletteLabel = "Always on Top"
toolbarItem.toolTip = "Always on Top"
toolbarItem.tag = toolbar.items.count
toolbarItem.target = self
toolbarItem.isEnabled = true
toolbarItem.image = UIImage(named: "pin")
toolbarItem.action = #selector(onClicked)
return toolbarItem
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
let itemId = NSToolbarItem.Identifier("ALWAYS_ON_TOP")
let identifiers: [NSToolbarItem.Identifier] = [ itemId ]
return identifiers
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return toolbarDefaultItemIdentifiers(toolbar)
}
@objc func onClicked(_ toolbarItem: NSToolbarItem) {
pinned.toggle()
toolbarItem.image = pinned ? UIImage(named: "pinned")?.withTintColor(.tintColor) : UIImage(named: "pin")
loadPlugin(pinned)
}
}
#endif
NSToolbarDelegate
プロトコルの3つの関数で、以下を実装する。
- ツールバーの識別子を定義(ここでは
ALWAYS_ON_TOP
) - 上記識別子に対応したツールバーアイテムを作成
- ツールバーアイテムにアイコン(イメージデータ)を設定
- ツールバーアイテムのアクション(
onClicked
)を設定 -
onClicked
(「常に手前に表示」する/しない切り替え)関数は、状態の切り替えと後述するプラグインのロードを行う
ここで、ツールバーアイテムに設定するイメージデータは、本来はNSImage
のはずだが、なぜかUIImage
を要求してくる。理由は不明だが、UIImage
で実際に動作するため、深追いはやめた。
2.2 UIApplicationDelegate
を実装する
上で定義したツールバーの作成を、UIWindowSceneDelegate
プロトコルのscene
メソッドで行うのだが、UIWindowSceneDelegate
を実装するためには、まずAppDelegate(UIApplicationDelegate
)を実装する必要がある。
SwiftUIアプリの場合、AppDelegateやSceneDelegateは、プロジェクト作成時には普通は作られないため、次の内容で作成する。各Delegateは本来はSwiftファイルを分けるべきだが、今回はAppMain
にまとめて書くことにする。
@main
struct RPNCalcApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
勝手にAppMain
と呼んでしまったが、元々は上のような内容。ここに@UIApplicationDelegateAdaptor
ディレクティブを追加し、AppDelegateとSceneDelegateの実装クラスを定義する。
@main
struct RPNCalcApp: App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
+ //以降を追加
class AppDelegate: NSObject, UIApplicationDelegate {
func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
config.delegateClass = SceneDelegate.self
return config
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
//中身はこの次で実装する
}
2.3 UIWindowSceneDelegate
を実装する
コンパイルオプションでMac Catalyst
のみ有効として、ツールバーの作成とToolbarDelegate
のインスタンスを設定する。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var toolbarDelegate = ToolbarDelegate()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(macCatalyst)
let toolbar = NSToolbar(identifier: "main")
toolbar.delegate = toolbarDelegate
toolbar.displayMode = .iconOnly
toolbar.isVisible = true
if let titlebar = windowScene.titlebar {
titlebar.toolbar = toolbar
}
#endif
}
func sceneDidDisconnect(_ scene: UIScene) {
}
}
3. Plugin.swift
を作り直す
前回の記事で作成したPlugin.swift
の内容を以下のように変更する。(前回のloadPlugin
メソッドは削除する)
import Foundation
@objc(Plugin)
protocol Plugin: NSObjectProtocol {
init()
func alwaysOnTop(isOnTop: Bool)
}
4. MacPlugin.swift
を作り直す
前回の記事で作成したMacPlugin.swift
の内容を以下のように変更する。
import AppKit
class MacPlugin: NSObject, Plugin {
required override init() {
}
func alwaysOnTop(isOnTop: Bool) {
guard let window = NSApplication.shared.mainWindow else { return }
if isOnTop {
window.isOpaque = true
window.alphaValue = 0.9
window.level = .floating
} else {
window.isOpaque = false
window.alphaValue = 1.0
window.level = .normal
}
}
}
mainWindowのlevelプロパティの設定に加え、「常に手前に表示」にした場合は、裏に隠れるウィンドウがうっすら透けて見えるように、window.alphaValue
値を0.9
とした。
5. loadPlugin
を作り直す
ToolbarDelegate
クラスのextension内に、新たなloadPlugin
を作成する。
#if targetEnvironment(macCatalyst)
extension ToolbarDelegate: NSToolbarDelegate {
: : (中略)
private func loadPlugin(_ pinned: Bool) {
let bundleFileName = "MacPlugin.bundle"
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(bundleFileName), let bundle = Bundle(url: bundleURL), let pluginClass = bundle.principalClass as? Plugin.Type else { return }
let plugin = pluginClass.init()
plugin.stayOnTop(isOnTop: pinned)
}
}
#endif
6. ContentView
内の前回の例を削除
前回の例でContentView
に加えた処理を削除(または、コメントアウト)して元に戻しておく。
: : (略)
Button(action: {
- #if targetEnvironment(macCatalyst)
+ //#if targetEnvironment(macCatalyst)
let stacks = stack.isEmpty ? "(empty)" : stack.reversed().map { $0.string }.joined(separator: "\n")
- loadPlugin(stacks)
+ //loadPlugin(stacks)
- #else
+ //#else
showingStack.toggle()
- #endif
+ //#endif
}, label: {
Text("Show Stack")
})
: : (略)
以上で必要なすべての実装が完了した。
Mac Catalyst
でビルドして実行すると、ウィンドウタイトルバーにアイコンが表示されるはずだ。
アイコンをクリックして、『「常に手前に表示」する/しない』 を試してもらいたい。
もちろん、iPhoneやiPadでビルドして実行しても、何も起きない(これまでと変化がない)ようになっている。
おわりに
今回説明した実装を使った、とあるMac Catalyst
のMacアプは、Appleの審査を通過した実績を持っている。
ぜひ参考にしてもらいたい。
以上