4 | 5 | [](https://travis-ci.org/luoxiu/Once) 6 |  7 |  8 |  9 |  10 | 11 |
12 | 13 | Once 可以让你用直观的 API 管理任务的执行次数。 14 | 15 | ## Highlight 16 | 17 | - [x] 安全 18 | - [x] 高效 19 | - [x] 持久化 20 | 21 | ## Usage 22 | 23 | ### Token 24 | 25 | `Token` 在内存中记录任务的执行次数,它可以让任务在整个 app 生命期内只执行一次。 26 | 27 | 你可以把它看作 OC 中 `dispatch_once` 的替代品: 28 | 29 | ```objectivec 30 | static dispatch_once_t token; 31 | dispatch_once(&token, ^{ 32 | // do something only once 33 | }); 34 | ``` 35 | 36 | 使用 `Token` 的 swift 代码如下: 37 | 38 | ```swift 39 | let token = Token.makeStatic() 40 | token.do { 41 | // do something only once 42 | } 43 | ``` 44 | 45 | 或者,更简单一点: 46 | 47 | ```swift 48 | Token.do { 49 | // do something only once 50 | } 51 | ``` 52 | 53 | 你也可以不用 `static`: 54 | 55 | ```swift 56 | class Manager { 57 | let loadToken = Token.make() 58 | 59 | func ensureLoad() { 60 | loadToken.do { 61 | // do something only once per manager. 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ### PersistentToken 68 | 69 | 不同于 `Token`,`PersistentToken` 会持久化任务的执行历史(使用 `UserDefault`)。 70 | 71 | `PersistentToken` 根据 `Scope` 和 `TimesPredicate` 判断是否应该执行本次任务。 72 | 73 | #### Scope 74 | 75 | `Scope` 表示时间范围。它是一个枚举: 76 | 77 | - `.install`: 从应用安装到现在 78 | - `.version`: 从应用升级到现在 79 | - `.session`: 从应用启动到现在 80 | - `.since(let since)`: 从 since 到现在 81 | - `.until(let until)`: 从开始到 until 82 | 83 | #### TimesPredicate 84 | 85 | `TimesPredicate` 表示次数范围。 86 | 87 | ```swift 88 | let p0 = TimesPredicate.equalTo(1) 89 | let p1 = TimesPredicate.lessThan(1) 90 | let p2 = TimesPredicate.moreThan(1) 91 | let p3 = TimesPredicate.lessThanOrEqualTo(1) 92 | let p4 = TimesPredicate.moreThanOrEqualTo(1) 93 | ``` 94 | 95 | #### do 96 | 97 | 你可以使用 `Scope` 和 `TimesPredicate` 组合成任意你想要的计划,而这,同样是线程安全的。 98 | 99 | ```swift 100 | let token = PersistentToken.make("showTutorial") 101 | token.do(in: .version, if: .equalTo(0)) { 102 | app.showTutorial() 103 | } 104 | 105 | // or 106 | let later = 2.days.later 107 | token.do(in: .until(later), if: .lessThan(5)) { 108 | app.showTutorial() 109 | } 110 | ``` 111 | 112 | #### done 113 | 114 | 有时,你的异步任务可能会失败,你并不想把失败的任务标记为 done,你可以: 115 | 116 | ```swift 117 | let token = PersistentToken.make("showAD") 118 | token.do(in: .install, if: .equalTo(0)) { task in 119 | networkService.fetchAD { result in 120 | if result.isSuccess { 121 | showAD(result) 122 | task.done() 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | 要提醒的是,这时的判断就不再是绝对安全的了——如果有多个线程同时检查该 token 的话,但这应该很少发生,😉。 129 | 130 | #### reset 131 | 132 | 你还可以清除一个任务的执行历史: 133 | 134 | ```swift 135 | token.reset() 136 | ``` 137 | 138 | 清除所有任务的执行历史也是允许的,但要后果自负: 139 | 140 | ```swift 141 | PersistentToken.resetAll() 142 | ``` 143 | 144 | ## 安装 145 | 146 | ### CocoaPods 147 | 148 | ```ruby 149 | pod 'Once', '~> 1.0.0' 150 | ``` 151 | 152 | ### Carthage 153 | 154 | ```ruby 155 | github "luoxiu/Once" ~> 1.0.0 156 | ``` 157 | 158 | ### Swift Package Manager 159 | 160 | ```swift 161 | dependencies: [ 162 | .package(url: "https://github.com/luoxiu/Once", .upToNextMinor(from: "1.0.0")) 163 | ] 164 | ``` 165 | 166 | ## 贡献 167 | 168 | 遇到一个 bug?想要更多的功能?尽管开一个 issue 或者直接提交一个 pr 吧! -------------------------------------------------------------------------------- /Once.xcodeproj/xcshareddata/xcschemes/Once-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 |
4 |
5 | [](https://travis-ci.org/luoxiu/Once)
6 | 
7 | 
8 | 
9 | 
10 |
11 |
12 |
13 | Once allows you to manage the number of executions of a task using an intuitive API.
14 |
15 |
16 | ## Highlight
17 |
18 | - [x] Safe
19 | - [x] Efficient
20 | - [x] Persistent
21 |
22 | ## Usage
23 |
24 | ### Token
25 |
26 | `Token` records the number of times the task is executed in memory, which allows the task to be executed only once during the entire lifetime of the app.
27 |
28 | You can think of it as an alternative to `dispatch_once` in OC:
29 |
30 | ```objectivec
31 | static dispatch_once_t token;
32 | dispatch_once(&token, ^{
33 | // do something only once
34 | });
35 | ```
36 |
37 | The swift code using `Token` is as follows:
38 |
39 | ```swift
40 | let token = Token.makeStatic()
41 | token.do {
42 | // do something only once
43 | }
44 | ```
45 |
46 | Or, more simple:
47 |
48 | ```swift
49 | Token.do {
50 | // do something only once
51 | }
52 | ```
53 |
54 | You can also don't use `static`:
55 |
56 | ```swift
57 | class Manager {
58 | let loadToken = Token.make()
59 |
60 | func ensureLoad() {
61 | loadToken.do {
62 | // do something only once per manager.
63 | }
64 | }
65 | }
66 | ```
67 |
68 | #### PersistentToken
69 |
70 | Unlike `run`, `do` will persist the execution history of the task (using `UserDefault`).
71 |
72 | `PersistentToken` determines whether this task should be executed based on `Scope` and `TimesPredicate`.
73 |
74 | #### Scope
75 |
76 | `Scope` represents a time range, it is an enum:
77 |
78 | - `.install`: from app installation
79 | - `.version`: from app update
80 | - `.session`: from app launch
81 | - `.since(let since)`: from `since(Date)`
82 | - `.until(let until)`: to `until(Date)`
83 |
84 | #### TimesPredicate
85 |
86 | `TimesPredicate` represents a range of times.
87 |
88 | ```swift
89 | let p0 = TimesPredicate.equalTo(1)
90 | let p1 = TimesPredicate.lessThan(1)
91 | let p2 = TimesPredicate.moreThan(1)
92 | let p3 = TimesPredicate.lessThanOrEqualTo(1)
93 | let p4 = TimesPredicate.moreThanOrEqualTo(1)
94 | ```
95 |
96 | #### do
97 |
98 | You can use `Scope` and `TimesPredicate` to make any plan you want, and, yes, it is thread-safe.
99 |
100 | ```swift
101 | let token = PersistentToken.make("showTutorial")
102 | token.do(in: .version, if: .equalTo(0)) {
103 | app.showTutorial()
104 | }
105 |
106 | // or
107 | let later = 2.days.later
108 | token.do(in: .until(later), if: .lessThan(5)) {
109 | app.showTutorial()
110 | }
111 | ```
112 |
113 | #### done
114 |
115 | Sometimes your asynchronous task may fail. You don't want to mark the failed task as done. You can:
116 |
117 | ```swift
118 | let token = PersistentToken.make("showAD")
119 | token.do(in: .install, if: .equalTo(0)) { task in
120 | networkService.fetchAD { result in
121 | if result.isSuccess {
122 | showAD(result)
123 | task.done()
124 | }
125 | }
126 | }
127 | ```
128 |
129 | But at this time, the judgment is no longer absolutely safe - if there are multiple threads checking the token at the same time, but it should rarely happen, 😉.
130 |
131 | #### reset
132 |
133 | You can also clear the execution history of a task:
134 |
135 | ```swift
136 | token.reset()
137 | ```
138 |
139 | It is also permissible to clear the execution history of all tasks, but at your own risk:
140 |
141 | ```swift
142 | PersistentToken.resetAll()
143 | ```
144 |
145 | ## Installation
146 |
147 | ### CocoaPods
148 |
149 | ```ruby
150 | pod 'Once', '~> 1.0.0'
151 | ```
152 |
153 | ### Carthage
154 |
155 | ```ruby
156 | github "luoxiu/Once" ~> 1.0.0
157 | ```
158 |
159 | ### Swift Package Manager
160 |
161 | ```swift
162 | dependencies: [
163 | .package(url: "https://github.com/luoxiu/Once", .upToNextMinor(from: "1.0.0"))
164 | ]
165 | ```
166 |
167 | ## Contributing
168 |
169 | Encounter a bug? want more features? Feel free to open an issue or submit a pr directly!
170 |
--------------------------------------------------------------------------------
/Sources/Once/PersistentToken.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public final class PersistentToken {
4 |
5 | private enum Key {
6 | static let version = "com.v2ambition.once.version"
7 | static let versionUpdateDate = "com.v2ambition.once.versionUpdateDate"
8 |
9 | static let context = "com.v2ambition.once.context"
10 | static let timestamps = "timestamps"
11 | }
12 |
13 | private static let registry = Atom<[String: PersistentToken]>(value: [:])
14 | private static let isInitialized = Atom(value: false, lock: DispatchSemaphoreLock())
15 |
16 | private static var sessionStartDate = Date()
17 | private static var versionUpdateDate: Date?
18 |
19 | public let name: String
20 |
21 | private let lock = NSRecursiveLock()
22 | private let contextKey: String
23 | private var context: [String: Any]
24 |
25 | private init(_ name: String) {
26 | self.name = name
27 | self.contextKey = Key.context + "." + name
28 | self.context = UserDefaults.standard.object(forKey: self.contextKey) as? [String: Any] ?? [:]
29 | }
30 |
31 | public static func make(_ name: String) -> PersistentToken {
32 | return registry.once_get(name, PersistentToken(name))
33 | }
34 |
35 | private func flushContext() {
36 | UserDefaults.standard.set(context, forKey: contextKey)
37 | }
38 |
39 | static func initialize() {
40 | sessionStartDate = Date()
41 |
42 | let currentVersion = appVersion()
43 | if UserDefaults.standard.string(forKey: Key.version) == currentVersion {
44 | versionUpdateDate = UserDefaults.standard.object(forKey: Key.versionUpdateDate) as? Date
45 | } else {
46 | UserDefaults.standard.set(currentVersion, forKey: Key.version)
47 | UserDefaults.standard.set(Date(), forKey: Key.versionUpdateDate)
48 | versionUpdateDate = Date()
49 | }
50 | }
51 |
52 | private static func ensureInitialized() {
53 | isInitialized.once_run(initialize)
54 | }
55 |
56 | private var timestamps: [Date] {
57 | get {
58 | return context[Key.timestamps] as? [Date] ?? []
59 | }
60 | set {
61 | context[Key.timestamps] = newValue
62 | }
63 | }
64 | }
65 |
66 | extension PersistentToken {
67 |
68 | private func filteredTimestamps(in scope: Scope) -> [Date] {
69 | let timestamps = self.timestamps
70 | return timestamps.filter { date in
71 | switch scope {
72 | case .install: return true
73 | case .version:
74 | guard let versionUpdateDate = PersistentToken.versionUpdateDate else { return true }
75 | return date > versionUpdateDate
76 | case .session: return date > PersistentToken.sessionStartDate
77 | case .since(let since): return date > since
78 | case .until(let until): return date < until
79 | }
80 | }
81 | }
82 |
83 | public func hasBeenDone(in scope: Scope, _ timesPredicate: TimesPredicate) -> Bool {
84 | return timesPredicate.evaluate(filteredTimestamps(in: scope).count)
85 | }
86 | }
87 |
88 | extension PersistentToken {
89 |
90 | public func `do`(in scope: Scope, if timesPredicate: TimesPredicate, _ task: (PersistentToken) -> Void) {
91 | PersistentToken.ensureInitialized()
92 | lock.withLock {
93 | if hasBeenDone(in: scope, timesPredicate) {
94 | task(self)
95 | }
96 | }
97 | }
98 |
99 | public func `do`(in scope: Scope, if timesPredicate: TimesPredicate, _ task: () -> Void) {
100 | PersistentToken.ensureInitialized()
101 | lock.withLock {
102 | if hasBeenDone(in: scope, timesPredicate) {
103 | task()
104 | timestamps.append(Date())
105 | }
106 | }
107 | flushContext()
108 | }
109 |
110 | public func done() {
111 | lock.withLock {
112 | timestamps.append(Date())
113 | }
114 | flushContext()
115 | }
116 |
117 | public var lastDone: Date? {
118 | return lock.withLock {
119 | timestamps.last
120 | }
121 | }
122 |
123 | public func reset() {
124 | lock.withLock {
125 | self.context = [:]
126 | }
127 | UserDefaults.standard.set(nil, forKey: contextKey)
128 | }
129 |
130 | public static func resetAll() {
131 | for (_, token) in self.registry.get() {
132 | token.reset()
133 | }
134 | }
135 | }
136 |
137 | extension PersistentToken {
138 |
139 | private static func appVersion() -> String {
140 | guard
141 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
142 | let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
143 | else {
144 | return "0.0.0"
145 | }
146 | return "\(version) (\(build))"
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Tests/OnceTests/PersistentTokenTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Once
3 |
4 | class PersistentTokenTests: XCTestCase {
5 |
6 | let count = 100
7 |
8 | func testMake() {
9 | let tokens = Atom<[PersistentToken]>(value: [])
10 | let name = UUID().uuidString
11 |
12 | asyncAndWait(concurrent: count) {
13 | tokens.append(PersistentToken.make(name))
14 | }
15 |
16 | let ts = tokens.get().reduce(into: [PersistentToken]()) { (r, t) in
17 | if !r.contains(where: { $0 === t }) { r.append(t) }
18 | }
19 | XCTAssertEqual(ts.count, 1)
20 | }
21 |
22 | func testIfEqualTo_Install() {
23 | let token = PersistentToken.make(UUID().uuidString)
24 | var i = 0
25 | asyncAndWait(concurrent: count) {
26 | token.do(in: .install, if: .equalTo(0)) {
27 | i += 1
28 | }
29 | }
30 | XCTAssertEqual(i, 1)
31 | }
32 |
33 | func testIfLessThan_Install() {
34 | let token = PersistentToken.make(UUID().uuidString)
35 | var i = 0
36 | asyncAndWait(concurrent: count) {
37 | token.do(in: .install, if: .lessThan(3)) {
38 | i += 1
39 | }
40 | }
41 | XCTAssertEqual(i, 3)
42 | }
43 |
44 | func testVersion() {
45 | func resetVersion() {
46 | UserDefaults.standard.set(nil, forKey: "com.v2ambition.once.version")
47 | }
48 |
49 | let token = PersistentToken.make(UUID().uuidString)
50 | var i = 0
51 |
52 | asyncAndWait(concurrent: count) {
53 | token.do(in: .version, if: .equalTo(0)) {
54 | i += 1
55 | }
56 | }
57 | XCTAssertEqual(i, 1)
58 |
59 | resetVersion()
60 | PersistentToken.initialize()
61 |
62 | asyncAndWait(concurrent: count) {
63 | token.do(in: .version, if: .equalTo(0)) {
64 | i += 1
65 | }
66 | }
67 | XCTAssertEqual(i, 2)
68 | }
69 |
70 | func testSession() {
71 | let token = PersistentToken.make(UUID().uuidString)
72 | var i = 0
73 | asyncAndWait(concurrent: count) {
74 | token.do(in: .session, if: .lessThan(3)) {
75 | i += 1
76 | }
77 | }
78 | XCTAssertEqual(i, 3)
79 | }
80 |
81 | func testReset() {
82 | let token = PersistentToken.make(UUID().uuidString)
83 | var i = 0
84 | asyncAndWait(concurrent: count) {
85 | token.do(in: .session, if: .equalTo(0)) {
86 | i += 1
87 | }
88 | }
89 | XCTAssertEqual(i, 1)
90 |
91 | token.reset()
92 |
93 | asyncAndWait(concurrent: count) {
94 | token.do(in: .session, if: .equalTo(0)) {
95 | i += 1
96 | }
97 | }
98 | XCTAssertEqual(i, 2)
99 | }
100 |
101 | func testResetAll() {
102 | let tokens = Atom<[PersistentToken]>(value: [])
103 | let num = Atom(value: 0)
104 | asyncAndWait(concurrent: count) {
105 | let token = PersistentToken.make(UUID().uuidString)
106 | tokens.append(token)
107 | token.do(in: .session, if: .equalTo(0)) {
108 | num.add(1)
109 | }
110 | }
111 | XCTAssertEqual(num.get(), count)
112 |
113 | PersistentToken.resetAll()
114 |
115 | tokens.get().forEach {
116 | $0.do(in: .session, if: .equalTo(0)) {
117 | num.add(1)
118 | }
119 | }
120 | XCTAssertEqual(num.get(), count + count)
121 | }
122 |
123 | func testSince() {
124 | let token = PersistentToken.make(UUID().uuidString)
125 | var i = 0
126 | asyncAndWait(concurrent: count) {
127 | token.do(in: .since(Date() - 0.5), if: .lessThan(3)) {
128 | i += 1
129 | }
130 | }
131 | XCTAssertEqual(i, 3)
132 |
133 | Thread.sleep(forTimeInterval: 0.5)
134 | asyncAndWait(concurrent: count) {
135 | token.do(in: .since(Date() - 0.5), if: .lessThan(3)) {
136 | i += 1
137 | }
138 | }
139 | XCTAssertEqual(i, 6)
140 | }
141 |
142 | func testUntil() {
143 | let token = PersistentToken.make(UUID().uuidString)
144 | var i = 0
145 | asyncAndWait(concurrent: count) {
146 | token.do(in: .until(Date() + 0.5), if: .lessThan(3)) {
147 | i += 1
148 | }
149 | }
150 | XCTAssertEqual(i, 3)
151 |
152 | token.reset()
153 |
154 | asyncAndWait(concurrent: count) {
155 | token.do(in: .until(Date() + 0.5), if: .lessThan(3)) {
156 | i += 1
157 | }
158 | }
159 | XCTAssertEqual(i, 6)
160 | }
161 |
162 | func testHasBeenDone() {
163 | let token = PersistentToken.make(UUID().uuidString)
164 | var i = 0
165 | asyncAndWait(concurrent: 10) {
166 | token.do(in: .session, if: .lessThan(3)) {
167 | i += 1
168 | }
169 | }
170 | XCTAssertEqual(i, 3)
171 | XCTAssertTrue(token.hasBeenDone(in: .session, .equalTo(3)))
172 | }
173 |
174 | static var allTests = [
175 | "testMake": testMake,
176 | "testIfEqualTo_Install": testIfEqualTo_Install,
177 | "testIfLessThan_Install": testIfLessThan_Install,
178 | "testVersion": testVersion,
179 | "testSession": testSession,
180 | "testReset": testReset,
181 | "testResetAll": testResetAll,
182 | "testSince": testSince,
183 | "testUntil": testUntil,
184 | "testHasBeenDone": testHasBeenDone
185 | ]
186 | }
187 |
--------------------------------------------------------------------------------
/Once.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 47;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 52D6D9871BEFF229002C0205 /* Once.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* Once.framework */; };
11 | 6211B25D2160BC76004358F2 /* PersistentToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6211B25C2160BC76004358F2 /* PersistentToken.swift */; };
12 | 6211B25E2160BC76004358F2 /* PersistentToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6211B25C2160BC76004358F2 /* PersistentToken.swift */; };
13 | 6211B25F2160BC76004358F2 /* PersistentToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6211B25C2160BC76004358F2 /* PersistentToken.swift */; };
14 | 6211B2602160BC76004358F2 /* PersistentToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6211B25C2160BC76004358F2 /* PersistentToken.swift */; };
15 | 66145EA7215E6A8300A8E10E /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66145EA6215E6A8300A8E10E /* Scope.swift */; };
16 | 66145EA8215E6A8300A8E10E /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66145EA6215E6A8300A8E10E /* Scope.swift */; };
17 | 66145EA9215E6A8300A8E10E /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66145EA6215E6A8300A8E10E /* Scope.swift */; };
18 | 66145EAA215E6A8300A8E10E /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66145EA6215E6A8300A8E10E /* Scope.swift */; };
19 | 6625AA3C2317F5EE00423CE9 /* Atom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA3B2317F5EE00423CE9 /* Atom.swift */; };
20 | 6625AA3D2317F5EE00423CE9 /* Atom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA3B2317F5EE00423CE9 /* Atom.swift */; };
21 | 6625AA3E2317F5EE00423CE9 /* Atom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA3B2317F5EE00423CE9 /* Atom.swift */; };
22 | 6625AA3F2317F5EE00423CE9 /* Atom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA3B2317F5EE00423CE9 /* Atom.swift */; };
23 | 6625AA412317F6E300423CE9 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA402317F6E300423CE9 /* Lock.swift */; };
24 | 6625AA422317F6E300423CE9 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA402317F6E300423CE9 /* Lock.swift */; };
25 | 6625AA432317F6E300423CE9 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA402317F6E300423CE9 /* Lock.swift */; };
26 | 6625AA442317F6E300423CE9 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6625AA402317F6E300423CE9 /* Lock.swift */; };
27 | 66D8B43521678CD10059A3EF /* TimesPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43421678CD10059A3EF /* TimesPredicate.swift */; };
28 | 66D8B43621678CD10059A3EF /* TimesPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43421678CD10059A3EF /* TimesPredicate.swift */; };
29 | 66D8B43721678CD10059A3EF /* TimesPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43421678CD10059A3EF /* TimesPredicate.swift */; };
30 | 66D8B43821678CD10059A3EF /* TimesPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43421678CD10059A3EF /* TimesPredicate.swift */; };
31 | 66D8B4402167B1C90059A3EF /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43F2167B1C90059A3EF /* Token.swift */; };
32 | 66D8B4412167B1C90059A3EF /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43F2167B1C90059A3EF /* Token.swift */; };
33 | 66D8B4422167B1C90059A3EF /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43F2167B1C90059A3EF /* Token.swift */; };
34 | 66D8B4432167B1C90059A3EF /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B43F2167B1C90059A3EF /* Token.swift */; };
35 | 66D8B44C2167B8360059A3EF /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4472167B82C0059A3EF /* TokenTests.swift */; };
36 | 66D8B44D2167B8370059A3EF /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4472167B82C0059A3EF /* TokenTests.swift */; };
37 | 66D8B44E2167B8370059A3EF /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4472167B82C0059A3EF /* TokenTests.swift */; };
38 | 66D8B4502167BAB30059A3EF /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B44F2167BAB30059A3EF /* XCTestManifests.swift */; };
39 | 66D8B4512167BAB30059A3EF /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B44F2167BAB30059A3EF /* XCTestManifests.swift */; };
40 | 66D8B4522167BAB30059A3EF /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B44F2167BAB30059A3EF /* XCTestManifests.swift */; };
41 | 66D8B4542167BB390059A3EF /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4532167BB390059A3EF /* Utils.swift */; };
42 | 66D8B4552167BB390059A3EF /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4532167BB390059A3EF /* Utils.swift */; };
43 | 66D8B4562167BB390059A3EF /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B4532167BB390059A3EF /* Utils.swift */; };
44 | 66D8B45B2167BCA00059A3EF /* TimesPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B45A2167BCA00059A3EF /* TimesPredicateTests.swift */; };
45 | 66D8B45C2167BCA00059A3EF /* TimesPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B45A2167BCA00059A3EF /* TimesPredicateTests.swift */; };
46 | 66D8B45D2167BCA00059A3EF /* TimesPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D8B45A2167BCA00059A3EF /* TimesPredicateTests.swift */; };
47 | 66EC2C1E2168A50500DCAE9F /* PersistentTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66EC2C192168A4FF00DCAE9F /* PersistentTokenTests.swift */; };
48 | 66EC2C1F2168A50600DCAE9F /* PersistentTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66EC2C192168A4FF00DCAE9F /* PersistentTokenTests.swift */; };
49 | DD7502881C68FEDE006590AF /* Once.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Once.framework */; };
50 | DD7502921C690C7A006590AF /* Once.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Once.framework */; };
51 | /* End PBXBuildFile section */
52 |
53 | /* Begin PBXContainerItemProxy section */
54 | 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */ = {
55 | isa = PBXContainerItemProxy;
56 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */;
57 | proxyType = 1;
58 | remoteGlobalIDString = 52D6D97B1BEFF229002C0205;
59 | remoteInfo = Once;
60 | };
61 | DD7502801C68FCFC006590AF /* PBXContainerItemProxy */ = {
62 | isa = PBXContainerItemProxy;
63 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */;
64 | proxyType = 1;
65 | remoteGlobalIDString = 52D6DA0E1BF000BD002C0205;
66 | remoteInfo = "Once-macOS";
67 | };
68 | DD7502931C690C7A006590AF /* PBXContainerItemProxy */ = {
69 | isa = PBXContainerItemProxy;
70 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */;
71 | proxyType = 1;
72 | remoteGlobalIDString = 52D6D9EF1BEFFFBE002C0205;
73 | remoteInfo = "Once-tvOS";
74 | };
75 | /* End PBXContainerItemProxy section */
76 |
77 | /* Begin PBXFileReference section */
78 | 52D6D97C1BEFF229002C0205 /* Once.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Once.framework; sourceTree = BUILT_PRODUCTS_DIR; };
79 | 52D6D9861BEFF229002C0205 /* Once-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Once-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
80 | 52D6D9E21BEFFF6E002C0205 /* Once.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Once.framework; sourceTree = BUILT_PRODUCTS_DIR; };
81 | 52D6D9F01BEFFFBE002C0205 /* Once.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Once.framework; sourceTree = BUILT_PRODUCTS_DIR; };
82 | 52D6DA0F1BF000BD002C0205 /* Once.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Once.framework; sourceTree = BUILT_PRODUCTS_DIR; };
83 | 6211B25C2160BC76004358F2 /* PersistentToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentToken.swift; sourceTree = "