feed

2023年03月18日, 編集履歴

SwiftUIの .opacity() と .onHover() は順番が重要

いつの頃からかは解らないが、少なくともmacOS 13.2.1のSwiftUIでは.opacity().onHover()の順番が重要になっている。

環境:

以下のようなSwiftUIビューがあったとする。

struct ContentView: View {

  @State private var isOn = false

  var body: some View {
    Toggle("Toggle", isOn: $isOn)
      .toggleStyle(.switch)
      .onHover { isHovering in
        print(isHovering)
      }
      .padding()
  }
}

Toggleスウィッチの領域にマウスポインタを持っていく(ホバーする)と、.onHover()が反応する。ここで.opacity()を付けてToggleに透明度を設定したいとする。

struct ContentView: View {

  @State private var isOn = false

  var body: some View {
    Toggle("Toggle", isOn: $isOn)
      .toggleStyle(.switch)
      .onHover { isHovering in
        print(isHovering)
      }
      .opacity(0.0)
      .padding()
  }
}

.onHover()の後ろに.opacity()を付与した場合、macOS 11では完全に透明にしても.onHover()は反応していたが、macOS 13では反応しなくなっていた。完全に透明にしたのがいけなかったのかと思い、.opacity(0.5)とかしてみると反応してくれる。なるほど。では、.opacity(0.3)なら? なぜか「Toggle」と書かれたラベル部分にマウスを乗せても反応せず、スウィッチ部分に乗せた場合にだけ反応する。.onHover()が反応する領域が変化するという変な挙動を示す。

struct ContentView: View {

  @State private var isOn = false

  var body: some View {
    Toggle("Toggle", isOn: $isOn)
      .toggleStyle(.switch)
      .opacity(0.0)
      .onHover { isHovering in
        print(isHovering)
      }
      .padding()
  }
}

のように.opacity()を先に記述すると、完全に透明にした場合でも.onHover()が反応するし、ラベル部分であってもちゃんと反応領域に含まれる。

というわけで、(少なくともmacOS 13.2.1では).opacity()が先、.onHover()が後という順番でないといけない。

2023年03月14日, 編集履歴

貧者のキーボードブリッジ

現在使用しているMacはMacBook Pro 15インチの2018年モデルで、これは悪名高きバタフライキーボードが採用されているものだ。曰く、打ち心地が悪い、不具合が発生しやすいといった声が聞こえてきていた。私自身は打ち心地は問題がなく、これまで不具合も出ていなかった。しかし使い始めて約5年、とうとう意図しない反復入力されてしまう不具合が発生するようになった。

バタフライキーボードの不具合に関してはAppleの無償修理プログラムがあるのだが、対象となるのが販売日より4年間ということで、ちょうど保証期間が切れた直後という間の悪さ。まだまだこのMacBook Proを使い続けたい意志はあるものの、他にバッテリーの状態も悪く、それらが同時に修理対象となると、費用が数万円かかることが予想される。それだけ出して有償修理するくらいなら新しいAppleシリコンMacが欲しいけれど、それこそ数十万円が必要になってくる。

そこで外付けキーボードを用意することにした。メルカリでAppleのMagic Keyboardがリーズナブルな金額で手に入ったので、それをMacBook Proのキーボード面に乗せるスタイルで運用する。問題になるのは、上に載せたMagic KeyboardによってMacBook Pro本体のキーボードが押されてしまうことである。

この問題を解消するために「キーボードブリッジ」という製品がある。ノートパソコンのキーボード面を覆うためのプレート状の製品である。そのプレートの上に外付けキーボードを置くという寸法である。バード電子というメーカから5,000円程度で販売されている。

ただ、結局のところ、板状のものでキーボード面を覆えれば良いわけである。ということで、

貧者のキーボードブリッジ。薄い段ボール板を敷いただけ pic.twitter.com/daVj640JWv

— Genji (@genji_tw) March 10, 2023

Amazonの梱包の中敷に使われていた薄い段ボール板を半分に切って、ダイソーで買ってきた滑り止めのゴム足を貼り付けたものをMacBook Proに乗せてみた。こんなものでもちゃんとキーボードブリッジとして機能している。名付けて「貧者のキーボードブリッジ」である。

ゴム足をつけているとはいえ、ほぼほぼMacBook Proのキーボード面に密着してしまっているので、放熱面では問題がある。強度面との兼ね合いがあるが、段ボールに穴を開けてしまってもいいかもしれない。

機能確認はできたので、改めて本家バード電子のキーボードブリッジの購入を考えるか、あるいは、アクリル板のサイズや加工方法を指定してお安く購入できるネットショップがあるようなので、そこで注文してみるのもいいかもしれない。

最後に、いちばんの問題はTouch IDが塞がれてしまったことである。購入したMagic KeyboardはTouch IDが付いていないタイプ。Touch ID付きのMagic Keyboardについては、Touch ID機能はIntel Macでは動作しないようなのでどうしようもない。Touch IDが使いたい時はキーボードブリッジをずらすしかない。

2022年11月03日, 編集履歴

SwiftUI macOSアプリでメニューバーエクストラを出す

macOSのメニューバーエクストラは、画面最上部のメニューバー右側に配置されるアイコンであり、それをクリックする事でメニューあるいはポップオーバーを表示し、アプリ等が最前面になくとも機能を呼び出せるUIである。常駐アプリ等で設定変更や状態確認のために使われることが多い。

SwiftUI macOSアプリでメニューバーアイコンを出すにはMenuBarExtraを使う。

文字列表示

メニューバーエクストラに文字列として項目を表示したい場合は、

@main
struct MenuBarExtraSampleApp: App {
  var body: some Scene {
    MenuBarExtra("MenuBarExtra") {
      Button("About App") {
        NSApp.orderFrontStandardAboutPanel(nil)
        NSApp.activate(ignoringOtherApps: true)
      }
      Button("Settings...") {
        NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
        NSApp.activate(ignoringOtherApps: true)
      }
      Divider()
      Button("Quit App") {
        NSApp.terminate(nil)
      }
    }
  }
}

のようにする。この例ではメニュー形式のメニューバーエクストラが作られる。

アイコン表示

アイコンを表示する場合は、

MenuBarExtra("MenuBarExtra", image: "icon") {
  ...
}

や、

MenuBarExtra("MenuBarExtra", systemImage: "star.fill") {
  ...
}

とする。このとき、第一引数の文字列は表示されなくなる。

アイコンと文字列表示

アイコンも文字列も両方出したい場合は、

MenuBarExtra {
  ...
} label: {
  Label("MenuBarExtra", systemImage: "star.fill")
    .labelStyle(.titleAndIcon)
}

とする。ここで単にLabelだけだとやはり文字列が表示されない。.labelStyle(.titleAndIcon)を指定することでアイコンと文字列両方が表示される。

ポップオーバー表示

通常はメニュー形式で表示されるメニューバーエクストラだが、ポップオーバー形式にすることもできる。その場合は、

MenuBarExtra("MenuBarExtra") {
  HStack {
    Image(systemName: "heart.fill")
    Button("MenuBarExtra") {}
    Image(systemName: "heart.fill")
  }
  .frame(width: 300, height: 200)
}
.menuBarExtraStyle(.window)

のようにmenuBarExtraStyleを使う。

メニューバーエクストラの表示・非表示切り替え

メニューバーエクストラの表示・非表示の切り替えをしたい場合はisInsertedが入っているイニシャライザを使う。

@main
struct StatusBarSampleApp: App {

  @AppStorage("MenuBarExtraShown") private var menuBarExtraShown = true

  var body: some Scene {
    MenuBarExtra("MenuBarExtra", systemImage: "star.fill", isInserted: $menuBarExtraShown) {
      ...
    }
  }
}

この場合、どこかのビューで、

struct GeneralSettingsView: View {

  @AppStorage("MenuBarExtraShown") private var menuBarExtraShown: Bool = true

  var body: some View {
    Form {
      Toggle("Menu Bar Extra", isOn: $menuBarExtraShown)
    }
    .padding()
  }
}

のように切り替えをするUIを用意する。

常駐アプリ化

メインウィンドウを持たずにメニューバーエクストラを操作の起点とするような常駐アプリにしたい場合は、ターゲットの「Info」欄で「Application is agent (UIElement)」を「YES」にする。こうすることで、アプリがDockやAppスイッチャーに表示されなくなる。

2022年10月31日, 編集履歴

SwiftUI macOSアプリの設定ウィンドウ項目をLabeledContentで整列させる

SwiftUI macOSアプリで、以下のような設定ウィンドウを作りたいとする。

ラベルは右揃えで、ポップアップメニューやテキストフィールド等の操作部品は左揃えにして整列させたい。VStackや単純なFormではきれいな整列ができない(……ことはないがややこしい黒魔術が必要となる)。よくあるパターンのレイアウトなのに変な感じだった。

そんな中、macOS 13.0から使える新しいLabeledContentFormと組み合わせることで、かんたんにきれいな整列が実現できるようになった。

環境は以下の通り。

VStackや単純なFormの場合

まずはこれまでの状態を確認してみる。UIを縦に並べていく場合、VStackFormを使う。VStackでは単純な左揃えや中央揃え等になってしまうので却下。こういった設定項目を縦に並べる場合はFormを使う。たとえば以下のような感じ。

Form {
  Picker("Pref 1:", selection: .constant(1)) {
    Text("Option 1").tag(1)
    Text("Option 2").tag(2)
  }
  .fixedSize()

  TextField("Preference 2:", text: .constant(""))
    .frame(width: 200)

  HStack {
    Text("Long Preference 3:")
    Toggle("Enabled Hoge", isOn: .constant(true))
  }

  HStack {
    Text("Pref 4:")
    Button("Button") { }
  }
}
.padding()

このとき、PickerTextFieldなど、標準で左側にラベル的要素を持つUIは意図通りきれいに整列される。しかし、左側にラベル的要素を持たないToggleButtonなどは、ラベル的要素を表現するためにHStackと組み合わせる必要があり、その場合、意図通りには整列されない。

macOS 12までは、Example of aligning labels in SwiftUI.Form on macOSにあるような.alignmentGuideを使うややこしい黒魔術的な手法で整列させなければならなかった。

LabeledContentの場合

macOS 13.0からLabeledContentが使えるようになった。これは前述のToggleButton等の、標準では左側にラベル的要素を持たないUI部品にラベルを付与できるようになる。そして、それをFormと組み合わせることできれいな整列をかんたんに実現できる。こんな感じ。

Form {
  Picker("Pref 1:", selection: .constant(1)) {
    Text("Option 1").tag(1)
    Text("Option 2").tag(2)
  }
  .fixedSize()

  TextField("Preference 2:", text: .constant(""))
    .frame(width: 300)

  LabeledContent("Long Preference 3:") {
    Toggle("Enabled Hoge", isOn: .constant(true))
  }

  LabeledContent("Pref 4:") {
    Button("Button") {}
  }
}
.padding()

前項のコード例と違うのは、ToggleButtonの部分でHStackの代わりにLabeledContentを使ってラベルを持たせている点。

特に黒魔術は必要なく、素直に意図通りのレイアウトが実現できる。SwiftUIもだんだん良くなってきた!

追記 .formStyle(.grouped)

macOS 13.0 VenturaからはFormに対して.formStyle()で表示形式を変更できるようになった。

Form {
  Picker("Pref 1", selection: .constant(1)) {
    Text("Option 1").tag(1)
    Text("Option 2").tag(2)
  }

  TextField("Preference 2", text: .constant(""))

  Toggle("Long Long Preference 3", isOn: .constant(true))

  HStack {
    Text("Pref 4")
    Spacer()
    Button("Button") { }
  }

  LabeledContent("Pref 5") {
    Button("Button") { }
  }
}
.formStyle(.grouped)

このように.formStyle(.grouped)を指定することで、Venturaの新しいシステム設定のような表示形式にできる。

この場合、Toggleは前述のLabeledContentを使わずとも自然に記述できる。Buttonに関してはHStackでラベル的要素を付けて整列できるが、LabeledContentを使った方が自然な記述ができる。

2022年07月06日, 編集履歴

ImageDiff ver. 2.0をリリースしました

ImageDiff ver. 2.0をリリースした。ふたつの画像間の差異をいくつかの方法で視覚化するmacOSアプリである。

ImageDiffは2011年に最初のヴァージョンを公開して以降、大きな変更を加えることなく放置気味だったのだが、この度、SwiftUIを用いてイチから書き直した。SwiftUIでのmacOSアプリを構築する感触を確かめるには、ちょうど良い規模感だったように思う。

主な変更点は、

である。

ver. 2.0は、前ヴァージョンまでとは別のアプリとしてMac App Storeに登録したので、前ヴァージョンをご利用の方は改めてダウンロードしなおす必要がある。また、前ヴァージョンは有料アプリだったが、ver. 2.0からは基本無料アプリとした。基本無料アプリとは言っても全機能無料で使用できる。アプリ内課金機能で開発者支援ができるので、気が向いた方は是非。

どうぞよろしく。