feed
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内で宣言しておかなければならない。
2023年06月19日, 編集履歴
Mailto Interceptor ver. 1.4をリリースしました。
mailto:
リンクのクリック等によるメールアプリケーションの意図しない即時起動を抑制できるmacOSアプリケーションです。デフォルトのメールアプリケーションとしてMailto Interceptorを指定することで、mailto:
リンクのクリック等があったときに、
何もしない
メールアドレスをコピー
URLを開く(ウェブメールの作成画面等)
動作を選択できるポップアップメニューを表示させる
といった動作をさせることができます。
ver. 1.4では、
「動作を通知」機能をシステム標準の通知センターを使うように変更
コードの近代化改修
を行いました。
今となってはmailto:
リンクを見ることも少なくなってはきましたが、どうぞよろしくお願いします。
2023年03月18日, 編集履歴
いつの頃からかは解らないが、少なくともmacOS 13.2.1のSwiftUIでは.opacity()
と.onHover()
の順番が重要になっている。
環境:
macOS 13.2.1
Xcode 14.2 (14C18)
以下のような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円程度で販売されている。
ただ、結局のところ、板状のものでキーボード面を覆えれば良いわけである。ということで、
Amazonの梱包の中敷に使われていた薄い段ボール板を半分に切って、ダイソーで買ってきた滑り止めのゴム足を貼り付けたものをMacBook Proに乗せてみた。こんなものでもちゃんとキーボードブリッジとして機能している。名付けて「貧者のキーボードブリッジ」である。
ゴム足をつけているとはいえ、ほぼほぼMacBook Proのキーボード面に密着してしまっているので、放熱面では問題がある。強度面との兼ね合いがあるが、段ボールに穴を開けてしまってもいいかもしれない。
機能確認はできたので、改めて本家バード電子のキーボードブリッジの購入を考えるか、あるいは、アクリル板のサイズや加工方法を指定してお安く購入できるネットショップがあるようなので、そこで注文してみるのもいいかもしれない。
最後に、いちばんの問題はTouch IDが塞がれてしまったことである。購入したMagic KeyboardはTouch IDが付いていないタイプ。Touch ID付きのMagic Keyboardについては、Touch ID機能はIntel Macでは動作しないようなのでどうしようもない。Touch IDが使いたい時はキーボードブリッジをずらすしかない。