【お知らせ】
SwiftUIで作った macOS Todo アプリ
ToDone
を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。
【本文】
メニューバーはデスクトップの一番上部に表示されるメニューです。
メニューバーは App で設定しますが、メニューアイテムで選んだコマンドの結果は View で表示されます。なので、App と View でデータを共有しなければなりません。ここではそのデータを共有するためのコードも記述しています。
このコーナーでは、任意のテキストエディタでコードを記述し、 ターミナルを使ってビルドする方法で作業を進めています。Xcode をお使いの場合は、Xcodeで作業する場合 をご一読ください。
なお、ターミナルで作業する場合も、Swift コンパイラや SwiftUI フレームワークなどを Mac にインストールするために Xcode をインストールして、一度起動させなければなりません。インストールが終われば Xcode は終了しても大丈夫です。
また、作成したアプリケーションを App Storeに提出するためのファイルにするには、Xcode を使わなければならなかったかもしれません。どこかで Xcode を使わずに作る方法を見たような気もするのですが...
私の開発環境は次のとおりです。
更新履歴
2022/07/13 詳しいコード説明をつけました。
2022/07/13「メニューバーから既存のメニュー項目を削除する」を追加しました。
2022/07/14「CommandGroup で消せるメニューアイテム一覧」を追加しました。
今回はいろいろな技術が必要となります。メニューは App で作りますが、メニューを選んだ結果は View に反映される場合が多いです。そのためには、App と View でデータがバインディング(共有と言った方が分かりやすいかも知れません)されていなければなりません。なぜなら SwiftUI の場合、View の表示の変更は、その View とバインディングされたデータを変更することによって行われるからです。
ここでバインディングについて、整理します。
では、実際にコーディングしていきます。次のファイルは、以前は Main.swift というファイル名でした。これからは App.swift というファイル名で統一したいと思います。Xcode で作業している場合は、プロダクト名App(プロダクト名App.swift)に次のコードを記述してください。
/************************************
App.swift
copyright : vivacocoa.jp
last modified: Jul. 05, 2022
************************************/
import SwiftUI
class ColorData: ObservableObject {
@Published var name = "Black"
@Published var color = Color.black
}
@main
struct FooApp: App {
@StateObject private var data = ColorData()
// @ObservedObject var data = ColorData() でも良い
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView(data: data)
//.environmentObject(data)
}
.commands {
CommandMenu("RGB") {
Button(action: {
data.color = .red; data.name = "Red"
}, label: { Text("Red")})
Button(action: {
data.color = .green; data.name = "Green"
}, label: {Text("Green")})
Button(action: {
data.color = .blue; data.name = "Blue"
}, label: {Text("Blue")})
}
CommandMenu("CMYK") {
Button(action: {
data.color = Color(red:0, green:1, blue:1); data.name = "Cyan"
}, label: {Text("Cyan")})
Button(action: {
data.color = Color(red:1, green:0, blue:1); data.name = "Magenta"
}, label: {Text("Magenta")})
Button(action: {
data.color = .yellow; data.name = "Yellow"
},label: {Text("Yellow")})
Button(action: {
data.color = .black; data.name = "Black"
}, label: {Text("Black")})
}
}
}
}
// 次のコードの NSWindowDelegate は要りません。
class AppDelegate: NSObject, NSApplicationDelegate/*, NSWindowDelegate*/ {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
Xcode で作業している場合は、ContentView(ContentView.swift)に次のコードを記述してください。
/************************************
View.swift
copyright : vivacocoa.jp
last modified: Jul. 05, 2022
************************************/
import SwiftUI
struct ContentView: View {
@ObservedObject var data: ColorData
var body: some View {
VStack {
Text(data.name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(data.color)
}
.padding(10)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
.contextMenu(menuItems:{
Button(action:{
data.color = Color(red:0, green:1, blue:1)
data.name = "Cyan"}){Text("Cyan")}
Button(action:{
data.color = Color(red:1, green:0, blue:1)
data.name = "Magenta"}){Text("Magenta")}
Button(action:{
data.color = .yellow; data.name = "Yellow"
}, label:{Text("Yellow")})
Button(action:{
data.color = .black; data.name = "Black"}){Text("Black")}
})
}
}
/* Xcode で記述してビルドする場合は、ContentView_Previews も変更しなければなりません。
変更しない場合は、プレビューは使えなくなりますが、コメントアウトしてください。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(data: ColorData())
.environmentObject(ColorData())
}
}
*/
アプリケーションの名前を変えるために、Info.plist の CFBundleName を書き換えます。ここでは「Menubar」という名前にしました。 Xcode で作業している場合は、ここは無視してください。 機会があれば、 また説明します。
アイコンも フリーアイコンSVG、PNG、ICO、ICNS からダウンロードしました。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>foo</string>
<key>CFBundleIconFile</key>
<string>icon-macbook.icns</string>
<key>CFBundleName</key>
<string>Menubar</string>
</dict>
</plist>
swiftc App.swift View.swift -o foo
mv foo Foo.app/Contents/MacOS
メニューバーに RGB メニューと CMYK メニューが追加されています
RGB メニューと CMYK メニューの中身は次のとおりです。
SwiftUI では、macOS のメニューバーを完全に独自のメニューバーに入れ替えることはできません。ただし、New Window アイテムと、Show Tab Bar アイテムと、Show All Tabs アイテムを削除する方法は見つかりました。また、AppDelegate を使ってアプリケーションの起動時だけ、アップルメニューと、アプリケーションメニュー以外のメニューを削除する方法もあります。しかしこの方法は、ウィンドウを最小化すると元のメニューに戻ってしまいます。
私の場合は、Show Tab Bar と Show All Tabs は削除したいです。New Window はできたら残したいのですが、New Window で新しいウィンドウを作ると、指定した大きさではなく、かなり大きなウィンドウになってしまいます。このことが直れば New Window は残したいのですが、今回は 3 つとも削除することにします。まあ、New Window がなければ、ウィンドウを閉じた場合にアプリケーションを修了する機能の実装も簡単になりますからね。
/************************************
App.swift
copyright : vivacocoa.jp
last modified: Jul. 14, 2022
************************************/
import SwiftUI
class ColorData: ObservableObject {
@Published var name = "Black"
@Published var color = Color.black
}
@main
struct FooApp: App {
@ObservedObject var data = ColorData()
init() {
// 次のコードでタブ(Tab)に関するメニューが無くなります。
NSWindow.allowsAutomaticWindowTabbing = false
}
var body: some Scene {
WindowGroup {
ContentView(data: data)
}
.commands {
// .newItem(New Window アイテム)を 中身のないもの { } に
// replacing(置き換える)とNew Window アイテムが無くなります。
CommandGroup(replacing: .newItem) {}
CommandMenu("RGB") {
Button(action: {
data.color = .red; data.name = "Red"
}, label: { Text("Red")})
Button(action: {
data.color = .green; data.name = "Green"
}, label: {Text("Green")})
Button(action: {
data.color = .blue; data.name = "Blue"
}, label: {Text("Blue")})
}
CommandMenu("CMYK") {
Button(action: {
data.color = Color(red:0, green:1, blue:1); data.name = "Cyan"
}, label: {Text("Cyan")})
Button(action: {
data.color = Color(red:1, green:0, blue:1); data.name = "Magenta"
}, label: {Text("Magenta")})
Button(action: {
data.color = .yellow; data.name = "Yellow"
},label: {Text("Yellow")})
Button(action: {
data.color = .black; data.name = "Black"
}, label: {Text("Black")})
}
}
}
}
// 今回はアプリケーションの終了も SwiftUI で実装しますので、AppDelegate はいりません。
/************************************
App.swift
copyright : vivacocoa.jp
last modified: Jul. 14, 2022
************************************/
import SwiftUI
struct ContentView: View {
@ObservedObject var data: ColorData
var body: some View {
VStack {
Text(data.name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(data.color)
}
.padding(10)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
// 今回はウィンドウが消えれば、アプリケーションが修了するようにします。
.onDisappear{
NSApplication.shared.terminate(self)
}
.contextMenu(menuItems:{
Button(action:{
data.color = Color(red:0, green:1, blue:1)
data.name = "Cyan"}){Text("Cyan")}
Button(action:{
data.color = Color(red:1, green:0, blue:1)
data.name = "Magenta"}){Text("Magenta")}
Button(action:{
data.color = .yellow; data.name = "Yellow"
}, label:{Text("Yellow")})
Button(action:{
data.color = .black; data.name = "Black"}){Text("Black")}
})
}
}
// ContentView_Previews は省略します。
File メニューから New Window アイテムが消えています。
View メニューから Tab 関係のアイテムが消えています。
コード | 効果 |
---|---|
CommandGroup(replacing: .newItem) {} | File メニューの New Window 項目が消えます。 |
CommandGroup(replacing: .undoRedo) {} | File メニューの Undo と Redo 項目が消えます。 |
CommandGroup(replacing: .pasteboard) {} | Edit メニューの Cut と Copy と Paste 項目が消えます。 |
CommandGroup(replacing: .windowSize) {} | Window メニューの Minimize と Zoom 項目が消えます。 |
CommandGroup(replacing: .help) {} | Help メニューの Help 項目が消えます。 |
CommandGroup(replacing: .textEditing) {} | 不明 |
CommandGroup(replacing: .windowList) {} | 不明 |
CommandGroup(replacing: .windowArrangement) {} | 不明 |