Skip to content

Commit 1ff5bf4

Browse files
committed
Initial commit :)
0 parents  commit 1ff5bf4

File tree

6 files changed

+293
-0
lines changed

6 files changed

+293
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 Suyeol Jeon (xoul.kr)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// swift-tools-version:5.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "WeakMapTable",
7+
products: [
8+
.library(name: "WeakMapTable", targets: ["WeakMapTable"]),
9+
],
10+
targets: [
11+
.target( name: "WeakMapTable", dependencies: []),
12+
.testTarget(name: "WeakMapTableTests", dependencies: ["WeakMapTable"]),
13+
]
14+
)

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# WeakMapTable
2+
3+
![Swift](https://img.shields.io/badge/Swift-5.1-orange.svg)
4+
[![CocoaPods](http://img.shields.io/cocoapods/v/WeakMapTable.svg)](https://cocoapods.org/pods/WeakMapTable)
5+
[![Build Status](https://github.com/ReactorKit/WeakMapTable/workflows/CI/badge.svg)](https://github.com/ReactorKit/WeakMapTable/actions)
6+
[![CodeCov](https://img.shields.io/codecov/c/github/ReactorKit/WeakMapTable.svg)](https://codecov.io/gh/ReactorKit/WeakMapTable)
7+
8+
A weak-to-strong map table. It is inspired by [`NSMapTable`](https://developer.apple.com/documentation/foundation/nsmaptable) but **guarantees thread safety** and **deals better with weak references**. [`NSMapTable.weakToStrongObjects()`](https://developer.apple.com/documentation/foundation/nsmaptable/1391346-weaktostrongobjects) doesn't free the value object when the key object is deallocated but WeakMapTable does.
9+
10+
## APIs
11+
12+
```swift
13+
public func value(forKey key: Key) -> Value?
14+
public func value(forKey key: Key, default: @autoclosure () -> Value) -> Value
15+
public func setValue(_ value: Value?, forKey key: Key)
16+
```
17+
18+
## Installation
19+
20+
**Podfile**
21+
22+
```ruby
23+
pod 'WeakMapTable'
24+
```
25+
26+
## License
27+
28+
WeakMapTable is under MIT license. See the [LICENSE](LICENSE) file for more info.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import Foundation
2+
3+
4+
// MARK: - WeakMapTable
5+
6+
final public class WeakMapTable<Key, Value> where Key: AnyObject {
7+
private var dictionary: [Weak<Key>: Value] = [:]
8+
private let lock = NSRecursiveLock()
9+
10+
11+
// MARK: Initializing
12+
13+
public init() {
14+
}
15+
16+
17+
// MARK: Getting and Setting Values
18+
19+
public func value(forKey key: Key) -> Value? {
20+
let weakKey = Weak(key)
21+
22+
self.lock.lock()
23+
defer {
24+
self.lock.unlock()
25+
self.installDeallocHook(to: key)
26+
}
27+
28+
return self.unsafeValue(forKey: weakKey)
29+
}
30+
31+
public func value(forKey key: Key, default: @autoclosure () -> Value) -> Value {
32+
let weakKey = Weak(key)
33+
34+
self.lock.lock()
35+
defer {
36+
self.lock.unlock()
37+
self.installDeallocHook(to: key)
38+
}
39+
40+
if let value = self.unsafeValue(forKey: weakKey) {
41+
return value
42+
}
43+
44+
let defaultValue = `default`()
45+
self.unsafeSetValue(defaultValue, forKey: weakKey)
46+
return defaultValue
47+
}
48+
49+
public func setValue(_ value: Value?, forKey key: Key) {
50+
let weakKey = Weak(key)
51+
52+
self.lock.lock()
53+
defer {
54+
self.lock.unlock()
55+
if value != nil {
56+
self.installDeallocHook(to: key)
57+
}
58+
}
59+
60+
if let value = value {
61+
self.dictionary[weakKey] = value
62+
} else {
63+
self.dictionary.removeValue(forKey: weakKey)
64+
}
65+
}
66+
67+
68+
// MARK: Getting and Setting Values without Locking
69+
70+
private func unsafeValue(forKey key: Weak<Key>) -> Value? {
71+
return self.dictionary[key]
72+
}
73+
74+
private func unsafeSetValue(_ value: Value?, forKey key: Weak<Key>) {
75+
if let value = value {
76+
self.dictionary[key] = value
77+
} else {
78+
self.dictionary.removeValue(forKey: key)
79+
}
80+
}
81+
82+
83+
// MARK: Dealloc Hook
84+
85+
private var deallocHookKey: Void?
86+
87+
private func installDeallocHook(to key: Key) {
88+
let isInstalled = (objc_getAssociatedObject(key, &deallocHookKey) != nil)
89+
guard !isInstalled else { return }
90+
91+
let weakKey = Weak(key)
92+
let hook = DeallocHook(handler: { [weak self] in
93+
self?.lock.lock()
94+
self?.dictionary.removeValue(forKey: weakKey)
95+
self?.lock.unlock()
96+
})
97+
objc_setAssociatedObject(key, &deallocHookKey, hook, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
98+
}
99+
}
100+
101+
102+
// MARK: - Weak
103+
104+
private final class Weak<T>: Hashable where T: AnyObject {
105+
private let objectHashValue: Int
106+
weak var object: T?
107+
108+
init(_ object: T) {
109+
self.objectHashValue = ObjectIdentifier(object).hashValue
110+
self.object = object
111+
}
112+
113+
func hash(into hasher: inout Hasher) {
114+
hasher.combine(self.objectHashValue)
115+
}
116+
117+
static func == (lhs: Weak<T>, rhs: Weak<T>) -> Bool {
118+
return lhs.objectHashValue == rhs.objectHashValue
119+
}
120+
}
121+
122+
123+
// MARK: - DeallocHook
124+
125+
private final class DeallocHook {
126+
private let handler: () -> Void
127+
128+
init(handler: @escaping () -> Void) {
129+
self.handler = handler
130+
}
131+
132+
deinit {
133+
self.handler()
134+
}
135+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import XCTest
2+
import WeakMapTable
3+
4+
final class WeakMapTableTests: XCTestCase {
5+
func testSetValueForKey() {
6+
let map = WeakMapTable<KeyObject, ValueObject>()
7+
8+
let (key1, value1) = (KeyObject(), ValueObject())
9+
let (key2, value2) = (KeyObject(), ValueObject())
10+
map.setValue(value1, forKey: key1)
11+
map.setValue(value2, forKey: key2)
12+
13+
XCTAssert(map.value(forKey: key1) === value1)
14+
XCTAssert(map.value(forKey: key2) === value2)
15+
}
16+
17+
func testSetAnotherValue() {
18+
let map = WeakMapTable<KeyObject, ValueObject>()
19+
20+
let key = KeyObject()
21+
weak var weakOldValue: ValueObject?
22+
weak var weakNewValue: ValueObject?
23+
24+
_ = {
25+
let oldValue = ValueObject()
26+
map.setValue(oldValue, forKey: key)
27+
weakOldValue = oldValue
28+
29+
let newValue = ValueObject()
30+
map.setValue(newValue, forKey: key)
31+
weakNewValue = newValue
32+
}()
33+
34+
XCTAssertNil(weakOldValue)
35+
XCTAssertNotNil(weakNewValue)
36+
XCTAssert(map.value(forKey: key) === weakNewValue)
37+
}
38+
39+
func testSetNil() {
40+
let map = WeakMapTable<KeyObject, ValueObject>()
41+
42+
let key = KeyObject()
43+
weak var weakValue: ValueObject?
44+
45+
_ = {
46+
let value = ValueObject()
47+
map.setValue(value, forKey: key)
48+
weakValue = value
49+
50+
map.setValue(nil, forKey: key)
51+
}()
52+
53+
XCTAssertNil(map.value(forKey: key))
54+
XCTAssertNil(weakValue)
55+
}
56+
57+
func testDefaultValue() {
58+
let map = WeakMapTable<KeyObject, ValueObject>()
59+
60+
let key = KeyObject()
61+
let expectedValue = ValueObject()
62+
let actualValue = map.value(forKey: key, default: expectedValue)
63+
64+
XCTAssert(actualValue === expectedValue)
65+
}
66+
67+
func testReleaseKeyAndValue() {
68+
let map = WeakMapTable<KeyObject, ValueObject>()
69+
70+
weak var weakKey: KeyObject?
71+
weak var weakValue: ValueObject?
72+
73+
_ = {
74+
let key = KeyObject()
75+
let value = ValueObject()
76+
map.setValue(value, forKey: key)
77+
weakKey = key
78+
weakValue = value
79+
}()
80+
81+
XCTAssertNil(weakKey)
82+
XCTAssertNil(weakValue)
83+
}
84+
}
85+
86+
private final class KeyObject {
87+
}
88+
89+
private final class ValueObject {
90+
}

0 commit comments

Comments
 (0)