feed
2024年09月30日, 編集履歴
macOS用EPUBリーダアプリ「Murasaki」のver. 2.5が出ました。
ver. 2.5での変更点は、
macOS 15 Sequoia以降のみをサポート
UIの調整
macOS 15 Sequoiaに対応した新しいQuick Look拡張機能を実装
です。
新しいQuick Look拡張機能に関して、macOS 15 Sequoia以降は従来のQuick Lookプラグイン形式(.qlgenerator
)がサポートされなくなったので、Sequoia以降で動作する機能拡張形式(.appex
)で実装し直しました。
それに伴い、Quick Lookの動作設定を変更できるようにもなりました。
読み込みデータサイズ制限
デフォルトのコンテントサイズ
それぞれを、Murasaki本体の設定ウィンドウから変更できるようになっています。
読み込みデータサイズ制限は、Quick LookがEPUBデータを展開、プレヴューを構成していくなかで、全データを読み込むのではなく、設定したデータサイズで読み込みを打ち切ります。大きなデータサイズのEPUBの場合、プレヴュー構成に時間が掛かることがあるための制限となります(これまでは固定値でした)。
デフォルトのコンテントサイズは、Quick Lookプレヴューの初期ウィンドウサイズを設定できます。
また、新しい拡張機能形式はシステム設定から拡張機能の有効・無効を設定することができます。
どうぞよろしくお願いします。
2024年03月03日, 編集履歴
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日, 編集履歴
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日, 編集履歴
App Sandbox環境下のmacOSアプリにおいて、ローカルファイルへのアクセスには制限が課せられている。
コード内で適当に生成したURL
ではローカルファイルへアクセスできない。ファイルへのアクセス権を得るには、
NSOpenPanel
fileImporter()
モディファイア
ドラッグ&ドロップ
等の限られた方法を取る必要がある。
ただし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()
が必要になる。
2024年02月17日, 編集履歴
以下のような構造体の配列をSwiftUIのTable
ビューに表示する場合を考える。
struct Bookmark {
var title : String
var url : URL
}
単純に表形式で表示する
データを単純にTable
に表示したいだけの場合、Table
のinit<Data>(Data, columns: () -> Columns)
と、TableColumn
のinit(Text, content: (RowValue) -> Content)
あたりを使う。このとき、表示するデータ構造はIdentifiable
プロトコルに適合させる必要があるので、プロパティにid
を追加した。
import SwiftUI
struct Bookmark : Identifiable {
var id = UUID ()
var title : String
var url : URL
}
struct ContentView : View {
@State var sampleData : [ Bookmark ] = [
Bookmark ( title : "Genji App" , url : URL ( string : "https://genjiapp.com/" ) ! ),
Bookmark ( title : "Apple" , url : URL ( string : "https://www.apple.com/" ) ! ),
Bookmark ( title : "Google" , url : URL ( string : "https://www.google.com/" ) ! )
]
var body : some View {
Table ( sampleData ) {
TableColumn ( "Title" ) { bookmark in
Text ( bookmark . title )
}
TableColumn ( "URL" ) { bookmark in
Text ( bookmark . url . absoluteString )
}
}
. tableStyle ( . bordered )
. padding ()
}
}
行を選択可能にする
テーブル行を選択可能にする場合、
init<Data>(Data, selection: Binding<Value.ID?>, columns: () -> Columns)
(単一選択)
init<Data>(Data, selection: Binding<Set<Value.ID>>, columns: () -> Columns)
(複数選択)
あたりを使う。selection
引数の型がID
のオプショナルか、ID
のSet
型かの違い。
struct ContentView : View {
@State var sampleData : [ Bookmark ] = [
Bookmark ( title : "Genji App" , url : URL ( string : "https://genjiapp.com/" ) ! ),
Bookmark ( title : "Apple" , url : URL ( string : "https://www.apple.com/" ) ! ),
Bookmark ( title : "Google" , url : URL ( string : "https://www.google.com/" ) ! )
]
@State private var selectedID : Bookmark . ID ?
var body : some View {
Table ( sampleData , selection : $ selectedID ) {
TableColumn ( "Title" ) { bookmark in
Text ( bookmark . title )
}
TableColumn ( "URL" ) { bookmark in
Text ( bookmark . url . absoluteString )
}
}
. tableStyle ( . bordered )
. padding ()
}
}
行クリック時に選択可能になった。
ドラッグ&ドロップで並べ替え
テーブル行をドラッグ&ドロップで並べ替え可能にするには、init(of: Value.Type, selection: Binding<Set<Value.ID>>, columns: () -> Columns, rows: () -> Rows)
あたりを使う。先ほどまでのイニシャライザとは使い方がちょっと違う。第一引数に表示するデータの型、最後のrows
引数にはTableRow
で表示データを供給する。以下のような感じ。
struct ContentView : View {
@State var sampleData : [ Bookmark ] = [
Bookmark ( title : "Genji App" , url : URL ( string : "https://genjiapp.com/" ) ! ),
Bookmark ( title : "Apple" , url : URL ( string : "https://www.apple.com/" ) ! ),
Bookmark ( title : "Google" , url : URL ( string : "https://www.google.com/" ) ! )
]
@State private var selectedID : Bookmark . ID ?
var body : some View {
Table ( of : Bookmark . self , selection : $ selectedID ) {
TableColumn ( "Title" ) { bookmark in
Text ( bookmark . title )
}
TableColumn ( "URL" ) { bookmark in
Text ( bookmark . url . absoluteString )
}
} rows : {
ForEach ( sampleData ) { bookmark in
TableRow ( bookmark )
}
}
. tableStyle ( . bordered )
. padding ()
}
}
ここまでは、まだデータの表示と行選択ができるだけで、前項までとは別のイニシャライザを使ったに過ぎない。行をドラッグ&ドロップできるようにするには、draggable
とdropDestination
モディファイアを使う。この時、表示データはTransferable
プロトコルに適合させる必要があり、そのためにはCodable
プロトコルにも適合させておくとかんたんになる。また、ドラッグ&ドロップさせるデータのためのカスタムUTType
も用意する。以下のような感じ。
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static let bookmark = UTType ( exportedAs : "com.genjiapp.TableSample.bookmark" )
}
struct Bookmark : Identifiable , Codable , Transferable {
static var transferRepresentation : some TransferRepresentation {
CodableRepresentation ( contentType : . bookmark )
}
var id = UUID ()
var title : String
var url : URL
}
struct ContentView : View {
@State var sampleData : [ Bookmark ] = [
Bookmark ( title : "Genji App" , url : URL ( string : "https://genjiapp.com/" ) ! ),
Bookmark ( title : "Apple" , url : URL ( string : "https://www.apple.com/" ) ! ),
Bookmark ( title : "Google" , url : URL ( string : "https://www.google.com/" ) ! )
]
@State private var selectedID : Bookmark . ID ?
var body : some View {
Table ( of : Bookmark . self , selection : $ selectedID ) {
TableColumn ( "Title" ) { bookmark in
Text ( bookmark . title )
}
TableColumn ( "URL" ) { bookmark in
Text ( bookmark . url . absoluteString )
}
} rows : {
ForEach ( sampleData ) { bookmark in
TableRow ( bookmark )
. draggable ( bookmark )
}
. dropDestination ( for : Bookmark . self ) { insertionIndex , insertionBookmarks in
if let bookmark = insertionBookmarks . first ,
let originalIndex = sampleData . firstIndex ( where : { $0 . id == bookmark . id }) {
sampleData . insert ( bookmark , at : insertionIndex )
let removeIndex = ( insertionIndex > originalIndex ) ? originalIndex : originalIndex + 1
sampleData . remove ( at : removeIndex )
}
}
}
. tableStyle ( . bordered )
. padding ()
}
}
draggable
はTableRow
に、dropDestination
はTableRow
を囲むForEach
に対して指定する。
また、用意したカスタムUTType
は、その識別子をInfo.plist内で宣言しておかなければならない。