Swiftでdeinitまで処理をdeferする

この記事は投稿日から1年以上が経過しています。

deferしてますか?

Swift2でみんな大好きdeferさんが導入されましたね!

guardと違いそんなに使う機会は訪れていないのですが、昨日、こんな感じで使いたい!という場面に遭遇しました。

CocoaLumberjackを使ってデバッグ用にUITextViewにログを吐くCustom Loggerを設定していたのですが、とあるViewControllerだけでそれを使いたく、ViewControllerがdeinitされたらそのCustom Loggerも当然外したい。

そんなコードを書く場合、defer大好きっ子ならCustom Loggerを登録した後にこんな感じで解除したくなりますよね(実際は僕はこのとき初めて実験でないところでdeferを使ったので、本当のdefer大好きっ子はこんな間違いはしないだろう)。

let logger = TextViewLogger(textView: textView)
DDLog.addLogger(logger)

defer {
    DDLog.removeLogger(logger)
}

defer使って、必要なくなったら漏れなくCustom Loggerを解放する俺様は超カッコいいぜ!と言いたかったのだが、当然のごとくこのコードは間違っていて、これを実行し終わるときにはdeferした処理も実行されて登録したCustom Loggerが即解除されるというお馬鹿な状況になるわけです。

でもdeferしたいよね?

とお馬鹿な前置きは置いておいても、上のような雰囲気で終処理書けたら便利な気はする。 普通にdeinitでやれば済む話なんだけど、今回のケースだとpropertyにloggerをもたせて、deinitloggerがあればremoveLoggerする的なことを書かないといけない。まあ普通のことではあるんだけど、できたら、

let logger = TextViewLogger(textView: textView)
DDLog.addLogger(logger)

deferToDeinit {
    DDLog.removeLogger(logger)
}

と、deinitまで処理を遅延させる的な書き方できたら面白いよね、ということで…

deinitまでdeferさせてみよう!その1

まず、超smellな秘伝のBaseViewControllerを使ってベタにやってみると、

typealias DeferredClosure = () -> Void

class BaseViewController: UIViewController {
    var deferreds: [DeferredClosure] = []

    deinit {
        for deferred in deferreds {
            deferred()
        }
    }

    func deferToDeinit(closure: DeferredClosure) {
        deferreds.append(closure)
    }
}

てな感じでBaseViewController君を作っておけば、このsubclassではみんなdeferToDeinitが使えるようになって、ひとまず目的は果たせる(はず)。

ぼく的にはこれでも良かったのですが、これだとみんなから超smellだ!○ね!と怒られる場合があるかもしれないので要注意です。

deinitまでdeferさせてみよう!その2

なので、今をときめくProtocol Extensionsでなんとかできないかも考えてみます。

まず、deinitまで処理を遅延させるのが目的なのにSwiftのExtensionではdeinitを拡張はできないのでどうしたものか、と。 deinit代わりにStored Propertyを使う方法も考えられるが(Stored Propertyの親がdeinitされたらそのPropertyもdeinitされる)、ExtensionでStored Propertyを追加することはできない。

Stored Propertyがダメなら、Computed Property + Associated Objectsでなんとかなるかも?と試してみました。

typealias DeferredClosure = () -> Void

protocol Deferrable {
    func deferToDeinit(closure: DeferredClosure)
}

var PropertyDeferreds: UInt8 = 0

extension UIViewController: Deferrable {
    class Deferreds {
        var deferreds: [DeferredClosure] = []

        deinit {
            for deferred in deferreds {
                deferred()
            }
        }

        func append(closure: DeferredClosure) {
            deferreds.append(closure)
        }
    }

    var deferreds: Deferreds {
        get {
            guard let deferreds = objc_getAssociatedObject(self, &PropertyDeferreds) as? Deferreds else {
                let deferreds = Deferreds()
                objc_setAssociatedObject(self, &PropertyDeferreds, deferreds, .OBJC_ASSOCIATION_RETAIN)
                return deferreds
            }
            return deferreds
        }
    }

    func deferToDeinit(closure: DeferredClosure) {
        deferreds.append(closure)
    }
}

もともとdeinitで何をやりたかったかと言えば、遅延実行用に渡したclosureの実行なので、上では、

  • Deferredsという適当なClassにclosureを保持させる
  • DeferredsをAssociated ObjectsとしてUIViewControllerに保持させる
  • UIViewControllerがdeinitされるとそれが保持しているDeferredsもあわせてdeinitされる
  • Deferredsのdeinitで遅延実行用closureをまとめて実行する

という流れでそれを実現しています。

キーポイントはUIViewControllerがdeinitされるとそれが保持しているDeferredsもあわせてdeinitされるのところで、Associated Objectsでもdeinitが自動で呼ばれるのかが心配だったのですが試してみたところきちんと呼ばれてました。

BaseViewController君さようなら!

まとめ

  • これは昨日思いつきで試してみただけなので、deferToDeinitが本当に便利なのかはまだわかりません
  • deferToDeinitの実装方法はどっちのやりかたが偉いってこともないと思いますのでケースバイケースで
  • もっとこんなスマートなやりかたあるよ!というコメントを是非お願いします!

所の執筆・監修した書籍

iOS 11 Programming

iOS 11 Programming

  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川 克己,所 友太,永野 哲久,加藤 寛人,
  • 製本版,電子版
  • PEAKSで購入する