feed

2024年10月24日, 編集履歴

Keyknockr ver. 2.0.1をリリース

macOS用ランチャアプリケーション「Keyknockr」の新しいヴァージョン2.0.1が出ました。

ver. 2.0.1の変更点は、

です。

また、このヴァージョンからは、macOS 15 Sequoia以降のみをサポートとします。どうぞよろしく。

2024年09月30日, 編集履歴

Murasaki ver. 2.5をリリース

macOS用EPUBリーダアプリ「Murasaki」のver. 2.5が出ました。

ver. 2.5での変更点は、

です。

新しいQuick Look拡張機能に関して、macOS 15 Sequoia以降は従来のQuick Lookプラグイン形式(.qlgenerator)がサポートされなくなったので、Sequoia以降で動作する機能拡張形式(.appex)で実装し直しました。

それに伴い、Quick Lookの動作設定を変更できるようにもなりました。

それぞれを、Murasaki本体の設定ウィンドウから変更できるようになっています。

読み込みデータサイズ制限は、Quick LookがEPUBデータを展開、プレヴューを構成していくなかで、全データを読み込むのではなく、設定したデータサイズで読み込みを打ち切ります。大きなデータサイズのEPUBの場合、プレヴュー構成に時間が掛かることがあるための制限となります(これまでは固定値でした)。

デフォルトのコンテントサイズは、Quick Lookプレヴューの初期ウィンドウサイズを設定できます。

また、新しい拡張機能形式はシステム設定から拡張機能の有効・無効を設定することができます。

どうぞよろしくお願いします。

2024年03月03日, 編集履歴

Keyknockr(旧CLCL)v2.0をリリース

macOS用ランチャアプリケーション「CLCL」を全面的に作り直し、名称を「Keyknockr」へと変更しました(バージョン番号は継続)。

Keyknockr(旧CLCL)はCommandキーやShiftキーなどの修飾キーを連打することで、設定したアプリケーションやURLを開くことができるランチャアプリケーションです。

今回のバージョンアップにともない、従来のLite版・有料版販売モデルから、フリーミアムモデル(基本無料+サブスクリプションによる機能解放)へと移行し、配信場所はMac App Storeへ統一されました。

v2.0ではイチからSwift/SwiftUIを用いて再構築され、新しい特殊アクションとして、

が追加されました。

「実行中のアプリケーション」アクションは現在実行中の(Dockに表示される)アプリケーションの一覧メニューがポップアップされ、そこから当該アプリケーションをアクティブ化できます。

「ポップアップメニュー」アクションは事前に設定したアプリケーションやローカルファイル、URLの一覧メニューがポップアップされ、そこから起動、アクティブ化、開く操作が実行できます。

また、「ログイン時に起動」設定を追加したので、Keyknockrの設定画面からかんたんに起動設定を変更できるようになりました。

どうぞよろしく。

以前にCLCLをGumroadで購入されていた方へはKeyknockr v2.0機能解放版を提供しています。Gumroadのシステムを通してメールを送信しているので、ご確認ください。

2024年02月20日, 編集履歴

SwiftUIのmacOSアプリでTableにコンテキストメニューをつける

Tableの基本的な使い方は「SwiftUI Tableの使い方」を参照。

macOSアプリにSwiftUIのTableを組み込み、副ボタンクリック(右クリック)によるコンテキストメニューをつけた時、テーブル行として表現されているオブジェクトに対して何らかの操作を行うような場合を考える。たとえばリスト表示にしたFinderで、ファイルやフォルダを右クリックしたときにでるコンテキストメニューをイメージしてもらうと良い。

このときTableColumn配下の要素やTableRowに.contextMenu(menuItems:)モディファイアをつけてしまいがちだが、macOSアプリのテーブルに対するコンテキストメニューの自然な挙動を実現するには、Table自体に.contextMenu(forSelectionType:menu:primaryAction:)モディファイアをつけなければならない。

テーブルの選択状態と操作対象

テーブル行をふつうにクリックした場合、行選択の状態となり、行全体にハイライトがつく。行を右クリックした場合、行の縁だけハイライトがつき、コンテキストメニューの起点、操作対象の状態になる。このふたつは別の状態である。選択範囲とコンテキストメニューの操作対象は重なる場合もあれば、異なる場合もある。

コンテキストメニューの操作対象は以下のようになる:

上図のコンテキストメニューの内容から操作対象が変わっていることが解る。

実装

以下のような実装が良いと思う。

import SwiftUI
import UniformTypeIdentifiers

struct Bookmark: Identifiable {

  var id = UUID()
  var title: String
  var url: URL
}

struct ContentView: View {

  @State private var sampleData: [Bookmark] = [
    .init(title: "Genji App", url: URL(string: "https://genjiapp.com")!),
    .init(title: "Apple", url: URL(string: "https://www.apple.com")!),
    .init(title: "Google", url: URL(string: "https://www.google.com")!)
  ]
  @State private var selectedIDs = Set<Bookmark.ID>()

  var body: some View {
    Table(of: Bookmark.self, selection: $selectedIDs) {
      TableColumn("Title") { bookmark in
        Text(bookmark.title)
//          .contextMenu {
//            Button("ここじゃない") {
//              print(bookmark)
//              print(selectedIDs)
//            }
//          }
      }
      TableColumn("URL") { bookmark in
        Text(bookmark.url.absoluteString)
      }
    } rows: {
      ForEach(sampleData) { bookmark in
        TableRow(bookmark)
//          .contextMenu {
//            Button("ここでもない") {
//              print(bookmark)
//              print(selectedIDs)
//            }
//          }
      }
    }
    .tableStyle(.bordered)
    // ここ!
    .contextMenu(forSelectionType: Bookmark.ID.self) { clickedRowIDs in
      addButton
      Button("Delete Bookmark") {
        sampleData.removeAll { clickedRowIDs.contains($0.id) }
      }
    }
    // ここはテーブル内余白を右クリックしたとき
    .contextMenu {
      addButton
    }
    .padding()
  }

  var addButton: some View {
    Button("Add Bookmark") {
      sampleData.append(.init(title: "Microsoft", url: URL(string: "https://www.microsoft.com")!))
    }
  }

}

TableColumn配下の要素に.contextMenu(menuItems:)モディファイアをつける場合、複数の列があるときはすべてにモディファイアをつけなければならないし、右クリックに反応するのは行の中の文字がある部分だけになる。

TableRowに.contextMenu(menuItems:)モディファイアをつける場合、選択行とコンテキストメニューの操作対象両方を取得できるが、両者が別の型になるし、両者の重なり具合を調べて操作対象を自分で計算する必要がある。

Table自体に.contextMenu(forSelectionType:menu:primaryAction:)モディファイアをつける場合、第2引数menuクロージャの引数として、標準的な操作対象となるオブジェクトのidがSetで取得できるので、その後の処理がストレートに記述できる。

第1引数forSelectionTypeには、テーブル行を表現するオブジェクトを特定するための型を指定する。オブジェクトはIdentifiableプロトコルに適合させているはずなので、idプロパティの型となる。第3引数primaryActionは、行をダブルクリックした時に実行される。

また、ドキュメントには.contextMenu(forSelectionType:menu:primaryAction:)の第2引数menuクロージャの引数が空のSetの場合はテーブルの余白部分を右クリックしたことになるとあるが、macOS 14.3.1とXcode Version 15.2 (15C500b)の環境ではコンテキストメニューが出ない。上のコード例ではTable自体にもうひとつ別の.contextMenu(menuItems:)をつけることで、余白部分を右クリックした場合をカバーしている。

2024年02月18日, 編集履歴

macOS App Sandbox下におけるSecurity-scoped Bookmarkについて

App Sandbox環境下のmacOSアプリにおいて、ローカルファイルへのアクセスには制限が課せられている。

コード内で適当に生成したURLではローカルファイルへアクセスできない。ファイルへのアクセス権を得るには、

等の限られた方法を取る必要がある。

ただしNSOpenPanel等で取得したURLは、その起動中はURL先のファイルにアクセス可能だが、そのURLを保存しておいたとしても、次の起動時にはアクセスできなくなってしまう。URLへのアクセスを永続化するには、Security-scopedなURL Bookmarkを作らなければならない。

URLからSecurity-scoped Bookmarkの生成

URLのbookmarkData(options:includingResourceValuesForKeys:relativeTo:)関数を使ってSecurity-scoped Bookmark(Data型)を作成する。options引数にはwithSecurityScope を指定。URLへのアクセスが読み込みのみで良い場合は、securityScopeAllowOnlyReadAccess も併用する。

let bookmarkData = try? url.bookmarkData(options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], includingResourceValuesForKeys: nil, relativeTo: nil)

Security-scoped BookmarkからURLを生成し、アクセスする

URLのinit(resolvingBookmarkData:options:relativeTo:bookmarkDataIsStale:)関数を使うことで、Security-scoped BookmarkなDataからURLを生成できる。resolvingBookmarkData引数にはSecurity-scoped BookmarkなDataを、options引数にはwithSecurityScopeを指定する。

生成されたURLに実際にアクセスするには、アクセス前にstartAccessingSecurityScopedResource()を実行し、アクセス後にはstopAccessingSecurityScopedResource()を実行する。

var isStale = false
let urlData = // 前項の手法で保存したURLのData
if let url = try? URL(resolvingBookmarkData: urlData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) {
  if url.startAccessingSecurityScopedResource() {

    // URL先への読み書き処理

    url.stopAccessingSecurityScopedResource()
  }
}

URLのinit(resolvingBookmarkData:options:relativeTo:bookmarkDataIsStale:)実行後にisStaleがtrueになっている場合、Bookmark Data作成以降にURL先ファイルの名前が変更されたり、場所が変更されたことを意味する。その場合でも返却されたURLは変更後のものになっており、アクセスは可能だが、新しくSecurity-scoped Bookmarkを生成し直す必要がある。

Security-scoped Bookmarkを再生成するときも、URL先へのアクセスに相当するので、startAccessingSecurityScopedResource()とstopAccessingSecurityScopedResource()が必要になる。