Swift Dictionary の key, value を weak 参照したい

たとえば

UIView を key にして、Datevalue にした Dictionary を使いたいと考えたとき。

ここで問題があるのは Dictionary の key, valuestrong で参照するため、 UIView を key にするとメモリリークが発生する。(本来の UView のライフサイクルが終了しても、解放されない)

この対応策が以下。

2つ方法を書いたが、他にもあると思う。

1. NSMapTable を使用する

NSMapTable は Dictionary と似ているが、key と value の参照を weak, strong からそれぞれ選ぶことができる。

https://developer.apple.com/documentation/foundation/nsmaptable

key を weak, value を strong にすると以下のコードになる。

        let table: NSMapTable = NSMapTable<UIView, NSDate>(keyOptions: .weakMemory, valueOptions: .strongMemory)

        autoreleasepool {
            let view1 = UIView()
            table.setObject(Date() as NSDate, forKey: view1)
        }
        
        let view2 = UIView()
        table.setObject(Date() as NSDate, forKey: view2)

        let view3 = UIView()
        table.setObject(Date() as NSDate, forKey: view3)
        
        // 追加した要素は3個だが、うち1つは autoreleasepool ブロック内でライフサイクルが終わっている(=weak 参照の NSMapTable の要素も消える)ので、count は 2 となる。
        print(table.count)

2. NSCache を使用する

要素の数が限られていて良い。という場合は、NSCache を使う方法も取れる。

その場合、UIView をそのままキーにするのではなく ObjectIdentifier を key にする。

https://developer.apple.com/documentation/swift/objectidentifier

ObjectIdentifier を key にすることで、UIView の参照カウントを増やさずに、UIView を一意なキーとして Dictionary で扱える。

コードはこんな感じ。

        let cache = NSCache<AnyObject, NSDate>()
        cache.countLimit = 2
        
        let view1 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view1) as AnyObject)
        
        let view2 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view2) as AnyObject)
        
        let view3 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view3) as AnyObject)
        
        // キャッシュ最大個数(2)を超えているので、一番最初に追加した要素はなくなっている
        print("view1 -> \(cache.object(forKey: ObjectIdentifier(view1) as AnyObject))")    // view1 -> nil
        print("view2 -> \(cache.object(forKey: ObjectIdentifier(view2) as AnyObject))")   // view2 -> Optional(2022-01-09 06:12:40 +0000)
        print("view3 -> \(cache.object(forKey: ObjectIdentifier(view3) as AnyObject))")   // view3 -> Optional(2022-01-09 06:12:40 +0000)