🐤

【Swift】closureをコードでざっくり理解

2022/06/15に公開

Swiftの備忘録シリーズ2。今回はclosureです。

closure

closureとは、文中に埋め込む命令の塊のことをいう(関数like)。
(https://developer.apple.com/documentation/swiftui/button)[SwiftUIにおけるButton]を例に出すと、Button構造体のイニシャライザはこんな感じになっている。

init(action: () -> Void, label: () -> Label)

例えば、ボタンを押したときにログで「pressed」と出力し、「ボタン」と書かれたラベルのボタンを作成しようとするとこんな感じのコードを書くことになる。

ContentView.swift
struct ContentView: View {
    var body: some View {
        Button(action: printLog, label: label)
    }
    func label() -> Text {
        Text("ボタン")
    }
    func printLog(){
        print("pressed")
    }
}

しかし、実際のコードの多くではこんな感じで書かれていることが多い。

ContentView.swift
Button(action: { print("pressed") }){
  Text("ボタン")
}

上記との違いは2つあり、

  • actionの中に関数が入っている
  • labelがButton()の中になく、その代わりに()の右側に関数が入っている
    この二つはどちらもclosureになる。
    actionの中に関数を入れるのは、再利用しないメソッドを外側に書いておくのはコードが煩雑になってよろしくないから中に書いちゃおうね、というお気持ちからくるもの。
    labelがなくなって()の右側に関数が入っているのはトレイリングクロージャーといい、「最後の引数がクロージャの場合はそのクロージャを()の外に出して、その引数名を省略することができる」 というルールに基づいて変わっている。

では、SwiftUIじゃない場合はどうか? 実際のコードではこんな感じで書かれていた。

ViewController
// ...
let header = collectionView.dequeueReusableSupplementaryView(
  ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderIdentifier.large.rawValue, for: indexPath
) as! LargeTitleHeaderView
header.setup(title: "Button Title")
header.buttonDidTapClosure = { [weak self] in
  self?.viewModel.toggleFilter()
  header.setCheckmark(self?.viewModel.isFiltered.value ?? false)
}
// ...
class LargeTitleHeaderView: UICollectionReusableView {
  public lazy var toggleFilterButton: UIButton = {
       let button = UIButton()
       button.setTitle("Toggle", for: .normal)
       button.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
       button.setImage(Asset.Images.buttonSelectOff.image, for: .normal)
  }
  func setCheckmark(_ isChecked: Bool) {
       let image = isChecked ? Asset.Images.buttonSelectOn.image : Asset.Images.buttonSelectOff.image
       toggleFilterButton.setImage(image, for: .normal)
  }
  var buttonDidTapClosure: (() -> Void)?
  @objc private func buttonDidTap() {
      buttonDidTapClosure?()
  }
}
// ...
ViewModel
// ...
let isFiltered: MutableProperty<Bool> = .init(false)
func toggleFilter() {
   isFiltered.value.toggle()
}
// ...

ViewControllerでは、LargeTitleHeaderViewクラスで buttonDidTapClosureという変数を宣言する(この時、型を() -> Voidと指定)。
実際にインスタンスが生成されたとき、buttonDidTapClosureに先ほどの宣言と合致した型の関数(クロージャ)を代入することで、ボタンが押されたときの動作(addTargetのactionに設定)を設定する。

ViewModelでは、isFilteredというプロパティを持っており(Reactiveのコードなので)、これは先ほどのクロージャの中で処理されるボタンが押されているか否かのBoolを表している。

「クロージャがなにか」という疑問というよりは「クロージャを使ったイベント処理の仕方」に対する疑問があったので、今回はその疑問を解消することができました。

Discussion