インポート
NSOpenPanel
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import SwiftUI import AppKit import UniformTypeIdentifiers struct ContentView: View { @State private var selectedImages: [CGImage] = [] @State private var selectedPaths: [String] = [] var body: some View { VStack(spacing: 20) { Button("画像ファイルを選択") { selectImageFiles() } if !selectedPaths.isEmpty { Text("選択されたファイル:") .font(.headline) ScrollView { VStack(alignment: .leading) { ForEach(selectedPaths, id: \.self) { path in Text(path) .font(.caption) } } } .frame(height: 200) } } .padding() } private func selectImageFiles() { let panel = NSOpenPanel() // パネルの設定 panel.title = "画像ファイルを選択" panel.message = "表示したい画像ファイルを選択してください" panel.prompt = "選択" panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = true // 複数選択を許可 // 画像ファイルのみ選択可能にする panel.allowedContentTypes = [ .png, .jpeg, .heic, .gif, .bmp, .tiff ] // ダイアログを表示 panel.begin { response in if response == .OK { selectedPaths = panel.urls.map { $0.path } loadImages(from: panel.urls) } } } private func loadImages(from urls: [URL]) { selectedImages.removeAll() for url in urls { if let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) { selectedImages.append(cgImage) print("画像を読み込みました: \(url.lastPathComponent)") } } } } |
より詳細な設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
struct ImagePickerView: View { @State private var selectedImages: [ScreenshotItem] = [] var body: some View { VStack(spacing: 20) { Button("画像を選択") { selectImages() } Button("単一の画像を選択") { selectSingleImage() } Button("フォルダを選択") { selectFolder() } if !selectedImages.isEmpty { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { ForEach(selectedImages) { item in Image(decorative: item.cg_image, scale: 1.0) .resizable() .scaledToFit() .frame(height: 100) } } } } } .padding() } // 複数の画像を選択 private func selectImages() { let panel = NSOpenPanel() panel.title = "画像ファイルを選択" panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = true // 複数選択 panel.allowedContentTypes = [.png, .jpeg, .heic, .gif] // 初期ディレクトリを設定(オプション) panel.directoryURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first panel.begin { response in if response == .OK { loadImagesFromURLs(panel.urls) } } } // 単一の画像を選択 private func selectSingleImage() { let panel = NSOpenPanel() panel.title = "画像ファイルを選択" panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false // 単一選択 panel.allowedContentTypes = [.png, .jpeg, .heic] panel.begin { response in if response == .OK, let url = panel.url { loadImagesFromURLs([url]) } } } // フォルダを選択してその中の画像を全て読み込む private func selectFolder() { let panel = NSOpenPanel() panel.title = "フォルダを選択" panel.canChooseFiles = false panel.canChooseDirectories = true // フォルダを選択 panel.allowsMultipleSelection = false panel.begin { response in if response == .OK, let url = panel.url { loadImagesFromFolder(url) } } } private func loadImagesFromURLs(_ urls: [URL]) { for url in urls { if let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) { let item = ScreenshotItem(id: UUID(), cg_image: cgImage) selectedImages.append(item) } } } private func loadImagesFromFolder(_ folderURL: URL) { let fileManager = FileManager.default guard let enumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: nil) else { return } for case let fileURL as URL in enumerator { let ext = fileURL.pathExtension.lowercased() if ["png", "jpg", "jpeg", "heic", "gif"].contains(ext) { if let nsImage = NSImage(contentsOf: fileURL), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) { let item = ScreenshotItem(id: UUID(), cg_image: cgImage) selectedImages.append(item) } } } } } struct ScreenshotItem: Identifiable { let id: UUID let cg_image: CGImage } |
ドラッグ&ドロップも対応
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
struct ImageDropView: View { @State private var selectedImages: [CGImage] = [] var body: some View { VStack { // ファイル選択ボタン Button("ファイルを選択") { selectImages() } Divider() // ドロップゾーン Rectangle() .fill(Color.gray.opacity(0.2)) .frame(height: 200) .overlay( Text("ここに画像をドラッグ&ドロップ") .foregroundColor(.secondary) ) .onDrop(of: [.image, .fileURL], isTargeted: nil) { providers in handleDrop(providers: providers) return true } } .padding() } private func selectImages() { let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseFiles = true panel.allowedContentTypes = [.png, .jpeg] panel.begin { response in if response == .OK { loadImages(from: panel.urls) } } } private func handleDrop(providers: [NSItemProvider]) { for provider in providers { provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { item, error in if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { DispatchQueue.main.async { loadImages(from: [url]) } } } } } private func loadImages(from urls: [URL]) { for url in urls { if let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) { selectedImages.append(cgImage) } } } } |
主要なプロパティ
|
1 2 3 4 5 6 7 8 9 |
// NSOpenPanelの設定 panel.canChooseFiles = true // ファイルを選択可能 panel.canChooseDirectories = false // フォルダを選択不可 panel.allowsMultipleSelection = true // 複数選択を許可 panel.allowedContentTypes = [.png, .jpeg] // 許可するファイルタイプ panel.directoryURL = URL(...) // 初期ディレクトリ panel.title = "タイトル" // ダイアログのタイトル panel.message = "メッセージ" // 説明文 panel.prompt = "選択" // ボタンのラベル |
エクスポート
macOSの新規テキストエディット(TextEdit.app)
方法1: AppleScriptを使う
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import AppKit func openInTextEdit(text: String) { // テキストをエスケープ(改行やクォートを処理) let escapedText = text .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") let script = """ tell application "TextEdit" activate make new document set text of front document to "\(escapedText)" end tell """ if let appleScript = NSAppleScript(source: script) { var error: NSDictionary? appleScript.executeAndReturnError(&error) if let error = error { print("AppleScriptエラー: \(error)") } } } |
方法2: 一時ファイルを作成してTextEditで開く
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import AppKit func openInTextEdit(text: String) { // 一時ファイルのパスを作成 let tempDir = FileManager.default.temporaryDirectory let fileName = "OCR_\(Date().timeIntervalSince1970).txt" let fileURL = tempDir.appendingPathComponent(fileName) do { // ファイルに書き込み try text.write(to: fileURL, atomically: true, encoding: .utf8) // TextEditで開く NSWorkspace.shared.open( [fileURL], withApplicationAt: URL(fileURLWithPath: "/System/Applications/TextEdit.app"), configuration: NSWorkspace.OpenConfiguration() ) { app, error in if let error = error { print("TextEdit起動エラー: \(error)") } } } catch { print("ファイル書き込みエラー: \(error)") } } |
場所
/var/folders/[ランダム文字列]/[ランダム文字列]/T/OCR_[タイムスタンプ].txt
確認方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let tempDir = FileManager.default.temporaryDirectory print("一時ディレクトリ: \(tempDir.path)") // 出力例: /var/folders/zz/zyxvpxvq6csfxvn_n0000000000000/T/ ``` ## 特徴 - **ユーザーごとに異なる**: macOSが自動的に割り当てるプライベートな領域 - **自動削除**: システムが定期的にクリーンアップします(通常は再起動時や数日後) - **隠しフォルダ**: `/var`フォルダは通常Finderでは表示されません ## Finderで確認する方法 1. **Finderで「フォルダへ移動」**: `⌘ + Shift + G` 2. 以下を入力して移動: ``` $TMPDIR |
または実際のパスをコピペ
より明示的な場所に保存したい場合
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func openInTextEdit(text: String) { // デスクトップに保存 let desktopURL = FileManager.default.urls( for: .desktopDirectory, in: .userDomainMask ).first! let fileName = "OCR_\(Date().timeIntervalSince1970).txt" let fileURL = desktopURL.appendingPathComponent(fileName) do { try text.write(to: fileURL, atomically: true, encoding: .utf8) NSWorkspace.shared.open( [fileURL], withApplicationAt: URL(fileURLWithPath: "/System/Applications/TextEdit.app"), configuration: NSWorkspace.OpenConfiguration() ) { _, error in if let error = error { print("エラー: \(error)") } } } catch { print("エラー: \(error)") } } |
その他の保存場所オプション
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// ドキュメントフォルダ let documentsURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first! // ダウンロードフォルダ let downloadsURL = FileManager.default.urls( for: .downloadsDirectory, in: .userDomainMask ).first! // デスクトップ let desktopURL = FileManager.default.urls( for: .desktopDirectory, in: .userDomainMask ).first! |
一時フォルダは自動的にクリーンアップされるので、一時的な使用には便利ですが、ユーザーが保存したい場合はデスクトップやドキュメントフォルダの方が良いかもしれませんね!
方法3: クリップボード経由(簡易版)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import AppKit func copyToClipboardAndOpenTextEdit(text: String) { // クリップボードにコピー let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(text, forType: .string) // TextEditを起動(ユーザーが手動で貼り付け) NSWorkspace.shared.openApplication( at: URL(fileURLWithPath: "/System/Applications/TextEdit.app"), configuration: NSWorkspace.OpenConfiguration() ) { app, error in if let error = error { print("TextEdit起動エラー: \(error)") } } print("テキストをクリップボードにコピーしました。TextEditで⌘+Vで貼り付けてください。") } |
画像のエクスポート
Sandboxの設定必須
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
extension CanvasNSView { func exportImage() { let savePanel = NSSavePanel() savePanel.title = String(localized: "画像をエクスポート") savePanel.canCreateDirectories = true // 初期設定 let currentFormat: ExportFormat = .png savePanel.nameFieldStringValue = "Untitled.\(currentFormat.fileExtension)" savePanel.allowedContentTypes = [currentFormat.utType] // AppKitのNSPopUpButtonを使う let formatPopup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 200, height: 25)) formatPopup.addItems(withTitles: ExportFormat.allCases.map { $0.rawValue }) // 透過チェックボックス let transparencyCheckbox = NSButton(checkboxWithTitle: String(localized: "背景を透過(PNGのみ)"), target: nil, action: nil) transparencyCheckbox.state = .on // アクセサリビューを作成 let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 80)) let formatLabel = NSTextField(labelWithString: String(localized: "形式:")) formatLabel.frame = NSRect(x: 20, y: 42, width: 50, height: 24) formatPopup.frame = NSRect(x: 80, y: 40, width: 200, height: 25) // チェックボックスの配置 transparencyCheckbox.frame = NSRect(x: 80, y: 10, width: 200, height: 24) accessoryView.addSubview(formatLabel) accessoryView.addSubview(formatPopup) accessoryView.addSubview(transparencyCheckbox) savePanel.accessoryView = accessoryView savePanel.begin { [weak self] response in guard response == .OK, let url = savePanel.url else { return } // ユーザーが選択した形式を取得 let selectedIndex = formatPopup.indexOfSelectedItem let format = ExportFormat.allCases[selectedIndex] // ★ 透過設定を取得 let shouldTransparent = transparencyCheckbox.state == .on // URLの拡張子が選択した形式と一致しない場合は修正 let finalURL: URL if url.pathExtension.lowercased() != format.fileExtension { finalURL = url.deletingPathExtension().appendingPathExtension(format.fileExtension) } else { finalURL = url } // ★ 透過設定を渡す self?.performExport(to: finalURL, format: format, transparent: shouldTransparent) } } private func performExport(to url: URL, format: ExportFormat, transparent: Bool) { // image_layer_rootからCGImageを取得 guard let cg_image = makeBoundaryImage(transparent: transparent) else { return } // NSImageに変換 let size = NSSize(width: cg_image.width, height: cg_image.height) let ns_image = NSImage(cgImage: cg_image, size: size) // 形式に応じて保存 guard let data = imageData(from: ns_image, format: format) else { showAlert(title: String(localized: "エラー"), message: String(localized: "画像の変換に失敗しました")) return } do { try data.write(to: url) showAlert(title: String(localized: "成功"), message: String(localized: "画像を保存しました")) } catch { showAlert(title: String(localized: "エラー"), message: String(localized: "保存に失敗しました")) } } /// NSImageを指定形式のDataに変換 private func imageData(from image: NSImage, format: ExportFormat) -> Data? { guard let tiff_data = image.tiffRepresentation, let bitmap_image = NSBitmapImageRep(data: tiff_data) else { return nil } switch format { case .png: return bitmap_image.representation(using: .png, properties: [:]) case .jpeg: // JPEG品質を0.9に設定 let properties: [NSBitmapImageRep.PropertyKey: Any] = [ .compressionFactor: 0.9 ] return bitmap_image.representation(using: .jpeg, properties: properties) case .bmp: return bitmap_image.representation(using: .bmp, properties: [:]) } } /// アラート表示 private func showAlert(title: String, message: String) { DispatchQueue.main.async { [weak self] in guard let window = self?.window else { return } let alert = NSAlert() alert.messageText = title alert.informativeText = message alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.beginSheetModal(for: window) } } } |
コメント