Touchdown 1.2を実装中に気がついたのですが、macOS 10.3 High Sierraから、ファイルのダブルクリックやDockアイコンへのドラッグ時にファイルの検証が実行されることがあるようです。
通常使用では問題ないのですが、巨大なファイルや大量のファイルをドラッグした時に問題が発生。
アプリアイコンへのファイルドラッグを受け付けるNSApplicationDelegateのfunc application(NSApplication, openFiles: [String])が呼び出されるのが非常に遅くなっています。
しかも、もともとこのopenFilesはファイル数が多いと複数回呼び出される謎仕様なので、Touchdownの「まとめて受け取って一括変換」という仕様と相性が悪く、ver.1.1までは短いタイマーを貼って全ファイルの受け取りを待つという残念な実装になっていました。
High Sierraからはかなり長い間隔をあけてopenFilesが複数回呼び出されるケースがあるため、適当な対応では対処しきれなくなったため、変換前に確認する場合の仕様を変更することにしました。
従来:最後にまとめてドラッグされたファイルを変換
Ver1.2:変換確認中にドラッグされたファイルをキューに追加していって変換
これに伴い変換するファイルの管理もArrayからSetに変更して、重複しないようにしました。
OSがバージョンアップするとやはり色々ありますね。
Swift 4に移行したらファイルのドラッグ時にパスが取り出せない?
TouchdownをSwift 4に変換した際に、フレームワーク側の変更に伴うコードの置換が発生していました。
ファイルのドラッグ&ドロップに関する部分ではドラッグ受付登録が、
register(forDraggedTypes: [NSFilenamesPboardType]);
から
registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL]);
になっています。
NSFilenamesPboardTypeの代わりにfileURLを使うようになったようです。
これに合わせてペーストボードから値を取得する部分が
sender.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType.fileURL) as? [String]
のようなコードに置換されたのですが、ここでエラーが発生。
sender.draggingPasteboard().typesにfileURLが含まれているにも関わらず、どうも[String]にうまくキャストできていないようです。
色々調べたところ以下のコードがうまく動きました。
sender.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? [String]
フレームワーク側の不具合でしょうか?
ひとまずワークアラウンドとしてこの対応で進めることにします。
ファイルのドラッグ&ドロップに関する部分ではドラッグ受付登録が、
register(forDraggedTypes: [NSFilenamesPboardType]);
から
registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL]);
になっています。
NSFilenamesPboardTypeの代わりにfileURLを使うようになったようです。
これに合わせてペーストボードから値を取得する部分が
sender.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType.fileURL) as? [String]
のようなコードに置換されたのですが、ここでエラーが発生。
sender.draggingPasteboard().typesにfileURLが含まれているにも関わらず、どうも[String]にうまくキャストできていないようです。
色々調べたところ以下のコードがうまく動きました。
sender.draggingPasteboard().propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? [String]
フレームワーク側の不具合でしょうか?
ひとまずワークアラウンドとしてこの対応で進めることにします。
Swift 4に変換でNSKeyedUnarchiverがエラー
macOS High SierraになりSwiftも4にバージョンアップ。
TouchdownもSwift 3からSwift 4に移行します。
基本的にはXcodeの移行ツールに従うだけで、途中で聞かれる
もせっかくなのでSwift 4推奨のMinimize Inferenceを選択しました。
ワーニングやエラーを手動で修正して実行したところ、以下のエラーが発生。
[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (ConvertSetting) for key (NS.objects); the class may be defined in source code or a library that is not linked
Swift 3版でConvertSettingというクラスをarchiveして保存していたのですが、Swift 4版では互換性がなくなり復元できなくなってしまいました。
原因を調べてみたところ、Swift 3では自動で付与されていた@objcが、Swift 4では自動で付与されなくなり、今後は明示的に記述する必要があるようです。
つまり、もともとCocoa BindingのためConvertSettingには暗黙で@objcが付いていたのが、Swift 4以降のタイミングで付与されなくなったと予想。
クラス定義の部分に
@objc(ConvertSetting)
を追記することで解決しました。
Cocoa Bindingのため@objcMembersも記載してあります。
というわけで、Swift 4(macOS High Sierra)版Touchdownは既知の不具合などを修正してからリリース予定です。
TouchdownもSwift 3からSwift 4に移行します。
基本的にはXcodeの移行ツールに従うだけで、途中で聞かれる
- Minimize Inference
- Match Swift 3 Behavior
もせっかくなのでSwift 4推奨のMinimize Inferenceを選択しました。
ワーニングやエラーを手動で修正して実行したところ、以下のエラーが発生。
[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (ConvertSetting) for key (NS.objects); the class may be defined in source code or a library that is not linked
原因を調べてみたところ、Swift 3では自動で付与されていた@objcが、Swift 4では自動で付与されなくなり、今後は明示的に記述する必要があるようです。
つまり、もともとCocoa BindingのためConvertSettingには暗黙で@objcが付いていたのが、Swift 4以降のタイミングで付与されなくなったと予想。
クラス定義の部分に
@objc(ConvertSetting)
を追記することで解決しました。
Cocoa Bindingのため@objcMembersも記載してあります。
というわけで、Swift 4(macOS High Sierra)版Touchdownは既知の不具合などを修正してからリリース予定です。
NSTableViewの行をドラッグ&ドロップ
やり方を理解するのに結構手間取ったのがNSArrayControllerのビューになっているNSTableViewの行をドラッグ&ドロップで入れ替える実装方法。
詳細な説明は省きますが、概念さえわかれば中身は意外とシンプルです。
NSTableViewDataSourceとして以下の3つを実装すればOK。
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool
ドラッグ&ドロップの実態は、コピー&ペーストに近いものがあります。
ここでやるべきことはNSPastboardにユーザーが移動しようとしている行のIndexSet(複数行ドラッグの可能性があるためIndexSetが渡ってきます)をセットするだけ。
(コピーに相当。)
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation
.moveを返せばドラッグ可能になります。
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool
NSPasteboardから取り出した(ペーストに相当)IndexSetと、移動先として渡ってきた行番号を手がかりにNSArrayControllerが管理している配列を並び替えるだけです。
わかってしまえばシンプルで泥臭い実装をするだけですね。
詳細な説明は省きますが、概念さえわかれば中身は意外とシンプルです。
NSTableViewDataSourceとして以下の3つを実装すればOK。
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool
ドラッグ&ドロップの実態は、コピー&ペーストに近いものがあります。
ここでやるべきことはNSPastboardにユーザーが移動しようとしている行のIndexSet(複数行ドラッグの可能性があるためIndexSetが渡ってきます)をセットするだけ。
(コピーに相当。)
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation
.moveを返せばドラッグ可能になります。
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool
NSPasteboardから取り出した(ペーストに相当)IndexSetと、移動先として渡ってきた行番号を手がかりにNSArrayControllerが管理している配列を並び替えるだけです。
わかってしまえばシンプルで泥臭い実装をするだけですね。
Touch Barでダブルタップ検知
TouchdownではTouch Barの設定(NSSegmentedControl)をダブルタップで実行する機能を実装しています。
やり方は意外と簡単。
NSClickGestureRecognizerで検知したいジェスチャーを指定し、任意のControlに設定するだけです。
let gesture = NSClickGestureRecognizer() // NSClickGestureRecognizerのインスタンスを作成
gesture.target = self // 通知を受けるオブジェクトを設定
gesture.numberOfTouchesRequired = 1 // 検知する指の本数
gesture.numberOfClicksRequired = 2 // 検知するタップ回数
gesture.allowedTouchTypes = .direct // 指によるダイレクトタッチを検知
gesture.action = #selector(deferConvertFiles(_:)) // 通知を受けたときに実行するセレクタ
segmentedControl.addGestureRecognizer(gesture) // Controlにジェスチャーを設定
シンプルで強力な仕組みですね。
やり方は意外と簡単。
NSClickGestureRecognizerで検知したいジェスチャーを指定し、任意のControlに設定するだけです。
let gesture = NSClickGestureRecognizer() // NSClickGestureRecognizerのインスタンスを作成
gesture.target = self // 通知を受けるオブジェクトを設定
gesture.numberOfTouchesRequired = 1 // 検知する指の本数
gesture.numberOfClicksRequired = 2 // 検知するタップ回数
gesture.allowedTouchTypes = .direct // 指によるダイレクトタッチを検知
gesture.action = #selector(deferConvertFiles(_:)) // 通知を受けたときに実行するセレクタ
segmentedControl.addGestureRecognizer(gesture) // Controlにジェスチャーを設定
シンプルで強力な仕組みですね。
JPEGに変換
画像をJPEGに変換するのはとっても簡単。
なんらかの方法でNSBitmapImageRepを作成。
let imgRep = NSBitmapImageRep(...)
あとは、NSBitmapImageFileType.NSJPEGFileType を指定して Data を取得するだけ。
properties には圧縮率などを辞書として渡せます。
let data = imgRep.representation(using: NSBitmapImageFileType.NSJPEGFileType, properties: [NSImageCompressionFactor:Float(0.8)])
最後に data を適当な場所に書き出せばOKです。
data.write(to: pathString, options:[])
なんらかの方法でNSBitmapImageRepを作成。
let imgRep = NSBitmapImageRep(...)
あとは、NSBitmapImageFileType.NSJPEGFileType を指定して Data を取得するだけ。
properties には圧縮率などを辞書として渡せます。
let data = imgRep.representation(using: NSBitmapImageFileType.NSJPEGFileType, properties: [NSImageCompressionFactor:Float(0.8)])
最後に data を適当な場所に書き出せばOKです。
data.write(to: pathString, options:[])
NSImageをリサイズ
意外と複雑な画像のリサイズ。
やり方は色々ありそうですが、今回使った方法を記録しておきます。
大まかな流れは
まずはあらかじめ準備したNSImageからNSBitmapImageRepを取り出し、さらにCGImageを取り出す。
guard let image = NSBitmapImageRep(data: sourceImage.tiffRepresentation!)?.cgImage else {
// エラー処理
}
次に各種パラメータを準備して、CGContextを作成。
let bitsPerComponent = 8
let bytesPerRow = 4 * newWidth
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
guard let bitmapContext = CGContext(data: nil, width: newWidth, height: newHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
// エラー処理
}
サイズを指定して書き込む。
let bitmapSize = NSMakeSize(CGFloat(newWidth), CGFloat(newHeight))
let bitmapRect = NSMakeRect(0.0, 0.0, bitmapSize.width, bitmapSize.height)
bitmapContext.draw(image, in: bitmapRect)
最後にNSImageに戻す。
guard let newImageRef = bitmapContext.makeImage() else{
// エラー処理
}
let newImage = NSImage(cgImage: newImageRef, size: bitmapSize)
やり方は色々ありそうですが、今回使った方法を記録しておきます。
大まかな流れは
- CGImageを準備
- CGContextを準備
- リサイズ後のサイズを指定
- contextを使って書き込む
- NSImageに戻す
まずはあらかじめ準備したNSImageからNSBitmapImageRepを取り出し、さらにCGImageを取り出す。
guard let image = NSBitmapImageRep(data: sourceImage.tiffRepresentation!)?.cgImage else {
// エラー処理
}
次に各種パラメータを準備して、CGContextを作成。
let bitsPerComponent = 8
let bytesPerRow = 4 * newWidth
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
guard let bitmapContext = CGContext(data: nil, width: newWidth, height: newHeight, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
// エラー処理
}
let bitmapSize = NSMakeSize(CGFloat(newWidth), CGFloat(newHeight))
let bitmapRect = NSMakeRect(0.0, 0.0, bitmapSize.width, bitmapSize.height)
bitmapContext.draw(image, in: bitmapRect)
最後にNSImageに戻す。
guard let newImageRef = bitmapContext.makeImage() else{
// エラー処理
}
let newImage = NSImage(cgImage: newImageRef, size: bitmapSize)
NSArrayControllerでNSTableViewをBind!
Touchdownでは複数の設定を保存して切り替えられるようにするため、NSTableViewをUIとして使います。
まずNSTableViewとArrayControllerを2箇所バインド。
ContentとはarrangedObjects、Selection IndexesはselectionIndexes。

さらにTableViewCellのValueにTableCellViewのobjectValueをバインド。

TableViewCellのBehaviorをEditableにしておきます。
基本的なとこだけど、何かと分かりにくいCocoa Binding。
iOSでは使えないらしく情報も少なめなので、簡単にまとめておきます。
ViewControllerにArrayを定義
NSTableViewは2箇所Bind
まずNSTableViewとArrayControllerを2箇所バインド。
ContentとはarrangedObjects、Selection IndexesはselectionIndexes。

さらにTableViewCellのValueにTableCellViewのobjectValueをバインド。

値を変更できるようにする
TableViewCellのBehaviorをEditableにしておきます。

登録:
投稿 (Atom)
Touchdown 1.3公開!
Touchdown 1.3を公開しました。 ダウンロードは こちら から。 Ver.1.3の変更点は以下の通りです。 新機能 macOS Mojave 10.13以降に対応 ダークモードに対応 ぜひお試しください。

-
TouchdownをSwift 4に変換した際に、フレームワーク側の変更に伴うコードの置換が発生していました。 ファイルのドラッグ&ドロップに関する部分ではドラッグ受付登録が、 register(forDraggedTypes: [NSFilenamesPboard...
-
インスタンスをダウンキャストするときに気になるのが型チェック。 Swiftではas?という便利なキャストが使えます。 ?が付いていることから予想できる通りoptionalを返すため、キャストできないときはnilとなります。 オプショナルバインディングと組み合わせるのが良さそうです...
-
Touchdown 1.3を公開しました。 ダウンロードは こちら から。 Ver.1.3の変更点は以下の通りです。 新機能 macOS Mojave 10.13以降に対応 ダークモードに対応 ぜひお試しください。