クロージャを“変数”として持つことで、オブジェクトが「後から実行される処理」を自由に差し込める
|
1 2 3 |
struct AAA { var a: (String) -> Void } |
これは、
AAAが「String を受け取って Void を返す関数」を持っている- その関数は 後から自由にセットできる
- AAA が “処理を外部に任せる” ための仕組み
という意味です。
🔶 これは何のために使うの?
✨ ① コールバック(イベント)を受け取る時
たとえば「ボタンが押されたらこれを実行してね」のような用途。
|
1 2 3 |
struct Button { var onTap: ()->Void } |
使用側で処理を inject(注入)できる:
|
1 2 3 |
let button = Button(onTap: { print("押された!") }) |
= SwiftUI の .onTapGesture { } と同じ発想。
✨ ② ロジックを構造体に渡したい時
処理内容を構造体の外で決めたいときに便利。
|
1 2 3 4 5 6 7 |
struct Validator { var rule: (String) -> Bool func validate(_ text: String) -> Bool { rule(text) // ← 渡された“ロジック”を実行 } } |
使う側:
|
1 2 3 |
let emailValidator = Validator { text in return text.contains("@") } |
Validator の中身は変えずに、判定ロジックだけ差し替えられる。
=「Strategy パターン」の代わりに使える。
✨ ③ 非同期処理や状態変化を外へ通知するとき
例えばダウンロード完了通知:
|
1 2 3 4 5 6 7 8 |
class Downloader { var onFinish: (Data?) -> Void = { _ in } func start() { // ... ダウンロード ... onFinish(data) } } |
✨ ④ SwiftUI の ViewModel と View の連携
|
1 2 3 |
class ViewModel: ObservableObject { var onError: (String) -> Void = { _ in } } |
🔶 仕組み:関数も“値として扱える” から可能
Swift では 関数は first-class citizen(第一級オブジェクト)。
だから変数として保持できる。
(Int) -> String のような “型” を持っている
その型をプロパティとして宣言するだけ。
🔶 注意点①:強参照循環に注意
クロージャは捕捉(capture)するので retain cycle の元に。
例:
|
1 2 3 4 5 6 7 8 9 |
class A { var callback: (() -> Void)? func setup() { callback = { print(self) // ← self を強参照 } } } |
対策:
|
1 2 3 |
callback = { [weak self] in print(self) } |
🔶 注意点②:デフォルト値がない場合は初期化必須
|
1 2 3 4 5 |
struct AAA { var a: (String) -> Void // 初期値なし } let x = AAA() // ❌ エラー:初期値がない |
対策:
|
1 2 3 |
struct AAA { var a: (String) -> Void = { _ in } } |
🔶 まとめ
| できること | 内容 |
|---|---|
| コールバック | イベント発生時に実行される処理を注入 |
| ロジック差し替え | Validator や Strategy パターン的に利用 |
| 非同期通知 | 完了時に何をするか外から指定 |
| UI 連携 | ViewModel → View の通知 |
→ 「処理の内容を後から自由に変えたい」時に使う
→ SwiftUI でも大量に使われている基本テクニック
デフォルト値を入れたくない場合
初期値がない時はエラーになる
|
1 2 3 4 5 |
class GGG { var onFinish: (String?) -> Void // ← 初期値が無い! } var onFinish: (String?) -> Void = { _ in } // ←正解の書き方 |
対策:init を書く
|
1 2 3 4 5 6 7 |
class GGG { var onFinish: (String?) -> Void init(onFinish: @escaping (String?) -> Void) { self.onFinish = onFinish } } |
使う側:
|
1 |
@State private var ggg = GGG(onFinish: { _ in }) |
例1
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct BBB { var d: (String) -> Bool func c(e: String) -> Bool { d(e) } } let f = BBB { k in return k.contains("a") } print(f.c(e: "ab.com")) |
🔵 まず全体の流れ(超ざっくり)
BBBのインスタンス(f)を作るときにdにクロージャ(関数)を渡しているf.c(e:)が呼ばれると
内部で d(e) が実行される- つまり
dに渡したクロージャが実行される
🔥 図解:全部の流れをつなげるとこうなる!
① BBB のインスタンス生成
|
1 2 3 |
let f = BBB { k in return k.contains("a") } |
これを理解するとすべてが繋がる。
|
1 2 3 |
BBB(...) の部分は ↓ BBB.init(d: { k in return k.contains("a") }) |
なので、
|
1 2 3 4 5 6 7 8 |
f.d には (String) -> Bool の関数が入っている その関数の中身は 入力: k = "文字列" 処理: k.contains("a") 出力: Bool(true/false) というロジック。 |
② 関数呼び出し:f.c(e: “ab.com”)
|
1 2 |
f.c(e: "ab.com") └── e = "ab.com" |
③ c(e:) の中身
|
1 2 3 |
func c(e: String) -> Bool { d(e) } |
ここが今回の疑問ポイントだね。
流れはこう:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
c(e:) が呼ばれる ↓ d(e) を実行する ↓ d に入っている “クロージャ” が実行される ↓ クロージャは k = e ("ab.com") を受け取り ↓ k.contains("a") を実行 ↓ true が返る ↓ c(e:) が true を返す ↓ print(true) と出る |
省略せず書くと
|
1 2 3 4 5 6 7 8 9 10 11 12 |
struct BBB { // ① 関数を値として保持するプロパティ var d: (_: String) -> Bool // ② その関数を呼び出すメソッド func c(e: String) -> Bool { // ③ プロパティ d に入っている関数を実行 return d(e) } } |
さらに「普通の関数」に置き換えて考えると
もし d が「普通のメソッド」だったらこう:
|
1 2 3 4 5 6 7 8 9 |
struct BBB { func d(_ value: String) -> Bool { return value.contains("a") } func c(e: String) -> Bool { return d(e) } } |
違いはたった一つ:
d の中身を、外から差し替えられるかどうか
クロージャを代入している瞬間を完全に書くと
|
1 2 3 4 5 |
let f = BBB( d: { (k: String) -> Bool in return k.contains("a") } ) |
🔥 超重要な一文
var d: (String) -> Boolは
「関数を持てるようにしただけ」
まだ何も実行していない
実行されるのは d(e) の瞬間だけ。
例2
in の前について
Swiftのクロージャで
inの前には、クロージャの引数(パラメータ)を定義します。これは|引数名: 型|の形式で書き、引数がない場合は||、型推論される場合は型を省略できますが、inキーワードは引数リストとクロージャの本体(処理内容)を区切るための重要な記号です。
基本的な構文:
|
1 2 3 4 5 |
{ (引数1: 型, 引数2: 型) in // 処理内容 (クロージャの本体) // 引数1や引数2を使って処理を行う return 値 // (戻り値がある場合) } |
具体例:
1. 引数と型指定がある場合:
|
1 2 3 4 |
let greet = { (name: String) in print("Hello, \(name)!") } greet("World") // 出力: Hello, World! |
name: String の部分がinの前に来ます。
2. 引数名のみで型推論される場合:
|
1 2 3 4 5 |
let numbers = [1, 2, 3, 4] let doubled = numbers.map { number in // 'number'は型推論される return number * 2 } print(doubled) // [2, 4, 6, 8] |
number がinの前に来ています。
3. $0, $1などの短縮記法(引数名なし):
|
1 2 3 |
let numbers = [1, 2, 3, 4] let doubled = numbers.map { $0 * 2 } // $0が最初の引数 print(doubled) // [2, 4, 6, 8] |
この場合、引数リスト自体が省略され、{の直後から処理内容が始まりますが、これは「引数がない」という意味ではなく、引数リストの省略形です。
まとめ:inの前の部分は、そのクロージャが受け取るデータの「名前(ラベル)」と「型」を指定する場所であり、これがクロージャの使いやすさや可読性を高める重要な要素となります。
例3
|
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 |
func startRectangleSelection() { // 既存のウィンドウを隠す if let window = NSApplication.shared.windows.first { window.orderOut(nil) } selectionWindow = RectangleSelectionWindow { text in self.captured_text = text DispatchQueue.main.async { // 元のウィンドウを再表示 if let window = NSApplication.shared.windows.first { window.makeKeyAndOrderFront(nil) selectionWindow = nil } } } selectionWindow?.orderFront(nil) } } // 矩形選択用のオーバーレイウィンドウ class RectangleSelectionWindow: NSWindow { private var startPoint: CGPoint? private var currentRect: CGRect = .zero private var overlayView: SelectionOverlayView! private var completion: ((String) -> Void)? init(completion: @escaping (String) -> Void) { // 全スクリーンをカバーするフレームを取得 let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) 〜省略〜 |
実行の流れ
startRectangleSelection()が呼ばれる- クロージャの性質について
|
1 2 3 4 5 6 |
selectionWindow = RectangleSelectionWindow { text in self.captured_text = text DispatchQueue.main.async { // ... } } |
- このクロージャ
{ text in ... }はすぐには実行されません RectangleSelectionWindow内部で保持され、後で呼び出されるのを待ちます- 非同期とは少し違い、「コールバック関数」として渡されているイメージです
selectionWindow?.orderFront(nil)が実行- ユーザーが矩形選択を完了すると
重要なポイント
- クロージャは非同期処理ではなく「コールバック」: 渡した時点では実行されず、
RectangleSelectionWindow側でcompletion?(text)が呼ばれた時に初めて実行されます DispatchQueue.main.asyncは非同期: クロージャ内のこの部分は確かに非同期で、UI更新をメインスレッドで安全に行うためのものです- 「{}から出ずに終了」について: クロージャは一度実行されたら最後まで実行されます。
DispatchQueue.main.asyncの中身だけが非同期キューに入り、クロージャ自体は同期的に最後まで実行されます
大まかな認識は合っていますが、「非同期のように動く」というよりは「後で呼び出されるコールバック関数」という理解の方が正確ですね。
引数を追加する方法
RectangleSelectionWindowのイニシャライザに引数を追加する場合は、以下のように書きます:
|
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 |
// 例:screenIndexという引数を追加する場合 class RectangleSelectionWindow: NSWindow { private var startPoint: CGPoint? private var currentRect: CGRect = .zero private var overlayView: SelectionOverlayView! private var completion: ((String) -> Void)? // 引数を追加 init(screenIndex: Int, completion: @escaping (String) -> Void) { let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) // 引数を使った処理 // ... self.completion = completion } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } |
呼び出し側
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func startRectangleSelection() { if let window = NSApplication.shared.windows.first { window.orderOut(nil) } // 引数を渡してインスタンス化 selectionWindow = RectangleSelectionWindow(screenIndex: 0) { text in self.captured_text = text DispatchQueue.main.async { if let window = NSApplication.shared.windows.first { window.makeKeyAndOrderFront(nil) self.selectionWindow = nil } } } selectionWindow?.orderFront(nil) } |
ポイント:
completion引数は最後に配置すると、クロージャを{ }で書けて読みやすくなります(trailing closure構文)- 複数引数を追加する場合も同様に、
completionの前に並べます
例:
|
1 |
init(screenIndex: Int, quality: Double, completion: @escaping (String) -> Void) |
クロージャの結果を分岐させる例
方法1: 列挙型(enum)を使う
|
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 |
// 結果の型を定義 enum SelectionResult { case text(String) case image(CGImage) } // 矩形選択用のオーバーレイウィンドウ class RectangleSelectionWindow: NSWindow { private var startPoint: CGPoint? private var currentRect: CGRect = .zero private var overlayView: SelectionOverlayView! private var completion: ((SelectionResult) -> Void)? private var shouldCaptureImage: Bool // ← 追加 init(captureImage: Bool = false, completion: @escaping (SelectionResult) -> Void) { let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) self.shouldCaptureImage = captureImage self.completion = completion // ... 残りの初期化コード } // 選択完了時の処理 private func finishSelection() { if shouldCaptureImage { // CGImageをキャプチャ if let image = captureScreenImage(rect: currentRect) { completion?(.image(image)) } } else { // OCRでテキストを取得 if let text = performOCR(rect: currentRect) { completion?(.text(text)) } } close() } } // 使用例 private func startRectangleSelection() { if let window = NSApplication.shared.windows.first { window.orderOut(nil) } // Boolで動作を切り替え selectionWindow = RectangleSelectionWindow(captureImage: false) { result in switch result { case .text(let text): self.captured_text = text self.MBM.captured_text = text print("テキスト取得: \(text)") case .image(let image): // CGImageの処理 self.capturedImage = image print("画像取得: \(image)") } DispatchQueue.main.async { if let window = NSApplication.shared.windows.first { window.makeKeyAndOrderFront(nil) self.selectionWindow = nil } } } selectionWindow?.orderFront(nil) } |
方法2: 2つの別々のクロージャを使う
|
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 |
class RectangleSelectionWindow: NSWindow { private var startPoint: CGPoint? private var currentRect: CGRect = .zero private var overlayView: SelectionOverlayView! private var textCompletion: ((String) -> Void)? private var imageCompletion: ((CGImage) -> Void)? private var captureMode: CaptureMode enum CaptureMode { case text case image } // テキスト用の初期化 init(textCompletion: @escaping (String) -> Void) { let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) self.captureMode = .text self.textCompletion = textCompletion // ... 初期化コード } // 画像用の初期化 init(imageCompletion: @escaping (CGImage) -> Void) { let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) self.captureMode = .image self.imageCompletion = imageCompletion // ... 初期化コード } private func finishSelection() { switch captureMode { case .text: if let text = performOCR(rect: currentRect) { textCompletion?(text) } case .image: if let image = captureScreenImage(rect: currentRect) { imageCompletion?(image) } } close() } } // 使用例 private func startTextSelection() { selectionWindow = RectangleSelectionWindow { text in self.captured_text = text } selectionWindow?.orderFront(nil) } private func startImageSelection() { selectionWindow = RectangleSelectionWindow { image in self.capturedImage = image } selectionWindow?.orderFront(nil) } |
方法3: オプショナルで両方持つ(シンプル)
|
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 |
class RectangleSelectionWindow: NSWindow { private var startPoint: CGPoint? private var currentRect: CGRect = .zero private var overlayView: SelectionOverlayView! private var textCompletion: ((String) -> Void)? private var imageCompletion: ((CGImage) -> Void)? init( textCompletion: ((String) -> Void)? = nil, imageCompletion: ((CGImage) -> Void)? = nil ) { let screenFrame = NSScreen.main?.frame ?? .zero super.init( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) self.textCompletion = textCompletion self.imageCompletion = imageCompletion // ... 初期化コード } private func finishSelection() { // テキストのコールバックがあれば実行 if let textCompletion = textCompletion { if let text = performOCR(rect: currentRect) { textCompletion(text) } } // 画像のコールバックがあれば実行 if let imageCompletion = imageCompletion { if let image = captureScreenImage(rect: currentRect) { imageCompletion(image) } } close() } } // 使用例 private func startSelection(captureImage: Bool) { if captureImage { selectionWindow = RectangleSelectionWindow( imageCompletion: { image in self.capturedImage = image } ) } else { selectionWindow = RectangleSelectionWindow( textCompletion: { text in self.captured_text = text } ) } selectionWindow?.orderFront(nil) } |
方法1: 列挙型(enum)を使う で質問があります。 Enumの性質についてです。 — if let text = performOCR(rect: currentRect) { completion?(.text(text)) } — ここで文字列が入っているのに — selectionWindow = RectangleSelectionWindow(captureImage: false) { result in switch result { case .text(let text): — case .text(let text) ここで変数の再初期化みたいになっているのはなぜですか?
いい質問です!これは列挙型の**関連値(Associated Values)**という仕組みです。説明しますね。
何が起きているか
1. 値を「包む」(Wrap)
|
1 2 3 4 5 6 7 |
let text = "こんにちは" // ← 最初の変数 completion?(.text(text)) // ← 文字列を列挙型の中に「包んでいる」 ``` ここで`SelectionResult`型の値が作られます: ``` SelectionResult.text("こんにちは") |
2. 値を「取り出す」(Unwrap)
|
1 2 3 |
switch result { // result は SelectionResult型 case .text(let text): // ← 列挙型の中から文字列を「取り出している」 print(text) // "こんにちは" |
図解
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 送信側 performOCR() ↓ "こんにちは" (String型) ↓ .text("こんにちは") (SelectionResult型に包む) ↓ completion呼び出し // 受信側 result (SelectionResult型) ↓ switch result ↓ case .text(let text): ← 中の文字列を取り出す ↓ text (String型) = "こんにちは" |
なぜこの仕組みが必要?
列挙型は複数の異なる型を扱えます:
|
1 2 3 4 |
enum SelectionResult { case text(String) // String型を持つケース case image(CGImage) // CGImage型を持つケース } |
resultはSelectionResult型なので、中身がStringなのかCGImageなのか開けてみないと分からないんです。
具体例で理解
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 例:プレゼントの箱 enum Present { case toy(String) case candy(Int) } // 箱に詰める let gift1 = Present.toy("ぬいぐるみ") let gift2 = Present.candy(10) // 箱を開ける switch gift1 { case .toy(let toyName): // 箱を開けて、中のおもちゃの名前を取り出す print("おもちゃは: \(toyName)") case .candy(let count): print("飴は: \(count)個") } // 出力: おもちゃは: ぬいぐるみ |
変数名が同じだけど別物
|
1 2 3 4 5 6 7 8 9 10 |
// 例:名前を変えてみる if let originalText = performOCR(rect: currentRect) { completion?(.text(originalText)) // ← "originalText"を包む } // 受信側 selectionWindow = RectangleSelectionWindow(captureImage: false) { result in switch result { case .text(let extractedText): // ← 取り出して"extractedText"に入れる print(extractedText) // 使える! |
たまたま同じtextという名前を使っているだけで、実際は別のスコープの別の変数なんです。
もっと分かりやすい例
|
1 2 3 4 5 6 7 8 9 10 |
// これと同じ構造 func sendGift(name: String) { let box = Box(content: name) // 箱に入れる deliver(box) } func receive(box: Box) { let content = box.content // 箱から取り出す print(content) } |
列挙型は「型安全な箱」のようなものです。中に何が入っているか保証されるので、間違った型で取り出すことができません!
コメント