Skip to content

Commit 7dc59df

Browse files
committed
Scheduler factories
1 parent 2eb96a3 commit 7dc59df

File tree

8 files changed

+113
-93
lines changed

8 files changed

+113
-93
lines changed

Package.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.5
1+
// swift-tools-version:5.7
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -34,7 +34,7 @@ let package = Package(
3434
),
3535
],
3636
dependencies: [
37-
.package(name: "NeedleFoundation", url: "https://github.com/uber/needle.git", .branch("master")),
37+
.package(url: "https://github.com/uber/needle.git", branch: "master"),
3838
],
3939
targets: [
4040
.target(name: "CombineExtensions",
@@ -49,16 +49,18 @@ let package = Package(
4949
dependencies: ["Lifecycle"]),
5050
.testTarget(name: "SPIRTests",
5151
dependencies: ["SPIR",
52-
"NeedleFoundation"]),
52+
.product(name: "NeedleFoundation", package: "needle")]),
5353
.target(name: "MVVM",
5454
dependencies: ["Lifecycle"]),
5555
.testTarget(name: "MVVMTests",
5656
dependencies: ["MVVM",
57-
"NeedleFoundation"]),
57+
.product(name: "NeedleFoundation", package: "needle")]),
5858
.target(name: "RIBs",
5959
dependencies: ["Lifecycle"]),
6060
.testTarget(name: "RIBsTests",
61-
dependencies: ["RIBs",
62-
"NeedleFoundation"]),
61+
dependencies: [
62+
"RIBs",
63+
.product(name: "NeedleFoundation", package: "needle")
64+
]),
6365
]
6466
)

Sources/CombineExtensions/Schedulers/Dispatch.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,3 @@ public func syncMain<T>(closure: () -> T) -> T {
3131
public func asyncMain(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
3232
DispatchQueue.main.async(delay: delay, execute: work)
3333
}
34-
35-
/// Submits a work item to the userInitiated dispatch queue for asynchronous execution after
36-
/// a specified time.
37-
public func asyncUserInitiated(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
38-
DispatchQueue.userInitiated.async(delay: delay, execute: work)
39-
}
40-
41-
/// Submits a work item to the userInteractive dispatch queue for asynchronous execution after
42-
/// a specified time.
43-
public func asyncUserInteractive(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
44-
DispatchQueue.userInteractive.async(delay: delay, execute: work)
45-
}
46-
47-
/// Submits a work item to the default dispatch queue for asynchronous execution after
48-
/// a specified time.
49-
public func asyncDefault(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
50-
DispatchQueue.default.async(delay: delay, execute: work)
51-
}
52-
53-
/// Submits a work item to the utility dispatch queue for asynchronous execution after
54-
/// a specified time.
55-
public func asyncUtility(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
56-
DispatchQueue.utility.async(delay: delay, execute: work)
57-
}
58-
59-
/// Submits a work item to the utility dispatch queue for asynchronous execution after
60-
/// a specified time.
61-
public func asyncBackground(delay: TimeInterval = 0, execute work: @escaping () -> Void) {
62-
DispatchQueue.background.async(delay: delay, execute: work)
63-
}

Sources/CombineExtensions/Schedulers/DispatchQueue.swift

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,35 @@ import Combine
1818
import Foundation
1919

2020
public struct DispatchQueue: DispatchQueueContext, Scheduler {
21-
21+
static let reverseDNSPrefix = "com.lifecyclekit."
22+
2223
/// `DispatchQueue.main` underlying queue.
2324
public static let main = DispatchQueue(backingQueue: Foundation.DispatchQueue.main)
2425

25-
/// `DispatchQueue.init(qos: .userInteractive)` underlying queue.
26-
public static let userInteractive = DispatchQueue(backingQueue: Foundation.DispatchQueue(label: "com.lifecyclekit.userinteractive",
27-
qos: .userInteractive))
26+
/// `DispatchQueue(qos: .userInteractive)` underlying queue.
27+
public static func userInteractive(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue {
28+
DispatchQueue(label: reverseDNSPrefix + "\(file):\(line)", qualityOfService: .userInteractive)
29+
}
2830

29-
/// `DispatchQueue.init(qos: .userInitiated)` underlying queue.
30-
public static let userInitiated = DispatchQueue(backingQueue: Foundation.DispatchQueue(label: "com.lifecyclekit.userInitiated",
31-
qos: .userInitiated))
31+
/// `DispatchQueue(qos: .userInitiated)` underlying queue.
32+
public static func userInitiated(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue {
33+
DispatchQueue(label: reverseDNSPrefix + "\(file):\(line)", qualityOfService: .userInitiated)
34+
}
3235

33-
/// `DispatchQueue.init(qos: .default)` underlying queue.
34-
public static let `default` = DispatchQueue(backingQueue: Foundation.DispatchQueue(label: "com.lifecyclekit.default",
35-
qos: .default))
36+
/// `DispatchQueue(qos: .default)` underlying queue.
37+
public static func `default`(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue {
38+
DispatchQueue(label: reverseDNSPrefix + "\(file):\(line)", qualityOfService: .default)
39+
}
3640

37-
/// `DispatchQueue.init(qos: .utility)` underlying queue.
38-
public static let utility = DispatchQueue(backingQueue: Foundation.DispatchQueue(label: "com.lifecyclekit.utility",
39-
qos: .utility))
41+
/// `DispatchQueue(qos: .utility)` underlying queue.
42+
public static func utility(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue {
43+
DispatchQueue(label: reverseDNSPrefix + "\(file):\(line)", qualityOfService: .utility)
44+
}
4045

41-
/// `DispatchQueue.init(qos: .background)` underlying queue.
42-
public static let background = DispatchQueue(backingQueue: Foundation.DispatchQueue(label: "com.lifecyclekit.background",
43-
qos: .background))
46+
/// `DispatchQueue(qos: .background)` underlying queue.
47+
public static func background(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue {
48+
DispatchQueue(label: reverseDNSPrefix + "\(file):\(line)", qualityOfService: .background)
49+
}
4450

4551
/// The quality of service, or the execution priority, to apply to tasks. From highest (`userInteractive`) to lowest (`background`).
4652
public enum QualityOfService {

Sources/CombineExtensions/Schedulers/Schedulers.swift

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,24 @@ public enum Schedulers {
2525
public static var asyncMain: DispatchQueue.Scheduler { .asyncMain }
2626

2727
/// In case `schedule` methods are called from `DispatchQueue.userInteractive`, it will perform action immediately without scheduling.
28-
public static var userInteractive: DispatchQueue.Scheduler { .userInteractive }
28+
public static func userInteractive(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue.Scheduler { DispatchQueue.Scheduler.userInteractive(file: file, line: line)
29+
}
2930

3031
/// In case `schedule` methods are called from `DispatchQueue.userInitiated`, it will perform action immediately without scheduling.
31-
public static var userInitiated: DispatchQueue.Scheduler { .userInitiated }
32+
public static func userInitiated(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue.Scheduler { DispatchQueue.Scheduler.userInitiated(file: file, line: line)
33+
}
3234

3335
/// In case `schedule` methods are called from `DispatchQueue.default`, it will perform action immediately without scheduling.
34-
public static var `default`: DispatchQueue.Scheduler { .default }
36+
public static func `default`(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue.Scheduler { DispatchQueue.Scheduler.default(file: file, line: line)
37+
}
3538

3639
/// In case `schedule` methods are called from `DispatchQueue.utility`, it will perform action immediately without scheduling.
37-
public static var utility: DispatchQueue.Scheduler { .utility }
40+
public static func utility(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue.Scheduler { DispatchQueue.Scheduler.utility(file: file, line: line)
41+
}
3842

3943
/// In case `schedule` methods are called from `DispatchQueue.background`, it will perform action immediately without scheduling.
40-
public static var background: DispatchQueue.Scheduler { .background }
44+
public static func background(file: StaticString = #fileID, line: UInt = #line) -> DispatchQueue.Scheduler { DispatchQueue.Scheduler.background(file: file, line: line)
45+
}
4146
}
4247

4348
public extension DispatchQueue {
@@ -51,19 +56,29 @@ public extension DispatchQueue {
5156
public static let asyncMain: Scheduler = .init(.main, alwaysAsync: true)
5257

5358
/// In case `schedule` methods are called from `DispatchQueue.userInteractive`, it will perform action immediately without scheduling.
54-
public static let userInteractive: Scheduler = .init(.userInteractive)
59+
public static func userInteractive(file: StaticString = #fileID, line: UInt = #line) -> Scheduler {
60+
Scheduler(DispatchQueue.userInteractive(file: file, line: line))
61+
}
5562

5663
/// In case `schedule` methods are called from `DispatchQueue.userInitiated`, it will perform action immediately without scheduling.
57-
public static let userInitiated: Scheduler = .init(.userInitiated)
64+
public static func userInitiated(file: StaticString = #fileID, line: UInt = #line) -> Scheduler {
65+
Scheduler(DispatchQueue.userInitiated(file: file, line: line))
66+
}
5867

5968
/// In case `schedule` methods are called from `DispatchQueue.default`, it will perform action immediately without scheduling.
60-
public static let `default`: Scheduler = .init(.default)
69+
public static func `default`(file: StaticString = #fileID, line: UInt = #line) -> Scheduler {
70+
Scheduler(DispatchQueue.default(file: file, line: line))
71+
}
6172

6273
/// In case `schedule` methods are called from `DispatchQueue.utility`, it will perform action immediately without scheduling.
63-
public static let utility: Scheduler = .init(.utility)
74+
public static func utility(file: StaticString = #fileID, line: UInt = #line) -> Scheduler {
75+
Scheduler(DispatchQueue.utility(file: file, line: line))
76+
}
6477

6578
/// In case `schedule` methods are called from `DispatchQueue.background`, it will perform action immediately without scheduling.
66-
public static let background: Scheduler = .init(.background)
79+
public static func background(file: StaticString = #fileID, line: UInt = #line) -> Scheduler {
80+
Scheduler(DispatchQueue.background(file: file, line: line))
81+
}
6782

6883
public typealias SchedulerOptions = Never
6984
public typealias SchedulerTimeType = Foundation.DispatchQueue.SchedulerTimeType

Sources/Lifecycle/Foundation/WeakSet.swift

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Foundation
2020
/// - warning: Element must conform to `AnyObject`.
2121
public struct WeakSet<Element>: ExpressibleByArrayLiteral, CustomDebugStringConvertible {
2222

23-
private typealias Key = AnyObject
23+
private typealias Key = NSNumber
2424
private typealias Value = AnyObject
2525

2626
/// Returns an array with a strong reference to elements.
@@ -37,7 +37,7 @@ public struct WeakSet<Element>: ExpressibleByArrayLiteral, CustomDebugStringConv
3737
asArray.count
3838
}
3939

40-
private var storage: NSMapTable<Key, Value> = .strongToWeakObjects()
40+
private var storage = NSMapTable<Key, Value>.strongToWeakObjects()
4141

4242
public init(arrayLiteral elements: Element...) {
4343
for element in elements {
@@ -59,11 +59,6 @@ public struct WeakSet<Element>: ExpressibleByArrayLiteral, CustomDebugStringConv
5959
}
6060

6161
public mutating func formUnion<T>(_ other: WeakSet<T>) {
62-
objc_sync_enter(storage); defer { objc_sync_exit(storage) }
63-
if !isKnownUniquelyReferenced(&storage) {
64-
storage = storageCopy()
65-
}
66-
6762
let enumerator = other.storage.objectEnumerator()
6863
while let object = enumerator?.nextObject() {
6964
if let object = object as? Element {
@@ -73,36 +68,40 @@ public struct WeakSet<Element>: ExpressibleByArrayLiteral, CustomDebugStringConv
7368
}
7469

7570
public mutating func insert(_ element: Element) {
76-
objc_sync_enter(storage); defer { objc_sync_exit(storage) }
71+
copyStorageIfNeeded()
72+
insertToStorage(object: element)
73+
}
74+
75+
private mutating func copyStorageIfNeeded() {
7776
if !isKnownUniquelyReferenced(&storage) {
78-
storage = storageCopy()
77+
let copy = NSMapTable<Key, Value>.strongToWeakObjects()
78+
for value in storage.dictionaryRepresentation().values {
79+
let key = keyForElement(value)
80+
copy.setObject(storage.object(forKey: key), forKey: key)
81+
}
82+
storage = copy
7983
}
80-
insertToStorage(object: element)
8184
}
8285

8386
private func insertToStorage(object: Element) {
8487
storage.setObject(object as Value, forKey: keyForElement(object))
8588
}
8689

8790
public mutating func removeAll() {
88-
objc_sync_enter(storage); defer { objc_sync_exit(storage) }
8991
storage = .strongToWeakObjects()
9092
}
9193

9294
public mutating func remove(_ element: Element) {
93-
objc_sync_enter(storage); defer { objc_sync_exit(storage) }
94-
if !isKnownUniquelyReferenced(&storage) {
95-
storage = storageCopy()
96-
}
95+
copyStorageIfNeeded()
9796
storage.removeObject(forKey: keyForElement(element))
9897
}
9998

10099
private func keyForElement(_ element: Element) -> Key {
101100
return NSNumber(value: ObjectIdentifier(element as AnyObject).hashValue)
102101
}
103-
104-
private func storageCopy() -> NSMapTable<Key, Value> {
105-
storage.copy() as? NSMapTable<Key, Value> ?? .strongToWeakObjects()
102+
103+
private func keyForElement<T: AnyObject>(_ element: T) -> Key {
104+
return NSNumber(value: ObjectIdentifier(element as AnyObject).hashValue)
106105
}
107106

108107
public var debugDescription: String {

Sources/Lifecycle/LifecycleSubscriber.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public protocol LifecycleSubscriber: AnyObject {
4141
public extension ScopeLifecycle {
4242
/// Binds to lifecycle states receiving on main thread.
4343
func subscribe(_ subscriber: LifecycleSubscriber) {
44+
#if DEBUG
45+
assert(Thread.isMainThread, "Subscribe from main thread only.")
46+
#endif
47+
4448
if subscribers.contains(subscriber) {
4549
assertionFailure("Binding to \(subscriber) that has already been subscribes to. \(subscribers)")
4650
}

Sources/Lifecycle/View/ViewLifecycleSubscriber.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public protocol ViewLifecycleSubscriber: AnyObject {
3232
public extension ViewLifecycle {
3333
/// Binds to lifecycle states receiving on main thread.
3434
func subscribe(_ subscriber: ViewLifecycleSubscriber) {
35+
#if DEBUG
36+
assert(Thread.isMainThread, "Subscribe from main thread only.")
37+
#endif
38+
3539
if subscribers.contains(subscriber) {
3640
assertionFailure("Binding to \(self) that has already been subscribes to. \(subscribers)")
3741
}

Tests/CombineExtensionsTests/SchedulersTests.swift

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,46 @@ final class SchedulersTests: XCTestCase {
4444
}
4545

4646
var schedulers: [CombineExtensions.DispatchQueue.Scheduler] = [
47-
.userInteractive,
48-
.userInitiated,
49-
.default,
50-
.utility,
51-
.background,
47+
.userInteractive(),
48+
.userInitiated(),
49+
.default(),
50+
.utility(),
51+
.background(),
5252
]
5353
schedulers.forEach(expectSync)
5454

5555
func expectAsync(scheduler: CombineExtensions.DispatchQueue.Scheduler) {
56-
var called = false
57-
let cancellable = Just(())
58-
.receive(on: scheduler)
59-
.sink(receiveValue: {
60-
called = true
61-
})
62-
XCTAssertFalse(called)
63-
cancellable.cancel()
56+
var called = false
57+
let cancellable = Just(())
58+
.receive(on: scheduler)
59+
.sink(receiveValue: {
60+
Thread.sleep(forTimeInterval: 1)
61+
called = true
62+
})
63+
XCTAssertFalse(called)
64+
cancellable.cancel()
6465
}
6566

6667
schedulers.append(.asyncMain)
6768
schedulers.forEach(expectAsync)
6869
}
70+
71+
func testSchedulerConcurrency() {
72+
let e = self.expectation(description: "Expect ordered calls to concurrent queue backed scheduler")
73+
var values: [Int] = []
74+
let startingValues: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
75+
let cancellable = startingValues
76+
.publisher
77+
.receive(on: Schedulers.default())
78+
.sink(receiveValue: { value in
79+
values.append(value)
80+
if values.count == 10 {
81+
XCTAssertEqual(startingValues, values)
82+
e.fulfill()
83+
}
84+
})
85+
86+
self.waitForExpectations(timeout: 10.0)
87+
cancellable.cancel()
88+
}
6989
}

0 commit comments

Comments
 (0)