Merge pull request #171 from sgl0v/nsnotificationqueue
Initial implementation of the NSNotificationQueue
diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj
index 4bb9473..3c6388c 100755
--- a/Foundation.xcodeproj/project.pbxproj
+++ b/Foundation.xcodeproj/project.pbxproj
@@ -204,6 +204,7 @@
5BF7AEC01BCD51F9008F214A /* NSUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4B1BCC5DCB00ED97BB /* NSUUID.swift */; };
5BF7AEC11BCD51F9008F214A /* NSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4C1BCC5DCB00ED97BB /* NSValue.swift */; };
5E5835F41C20C9B500C81317 /* TestNSThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5835F31C20C9B500C81317 /* TestNSThread.swift */; };
+ 5EF673AC1C28B527006212A3 /* TestNSNotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */; };
612952F91C1B235900BE0FD9 /* TestNSNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612952F81C1B235900BE0FD9 /* TestNSNull.swift */; };
61A395FA1C2484490029B337 /* TestNSLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A395F91C2484490029B337 /* TestNSLocale.swift */; };
61D6C9EF1C1DFE9500DEF583 /* TestNSTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D6C9EE1C1DFE9500DEF583 /* TestNSTimer.swift */; };
@@ -559,6 +560,7 @@
5BF7AEC21BCD568D008F214A /* ForSwiftFoundationOnly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ForSwiftFoundationOnly.h; sourceTree = "<group>"; };
5E5835F31C20C9B500C81317 /* TestNSThread.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSThread.swift; sourceTree = "<group>"; };
5EB6A15C1C188FC40037DCB8 /* TestNSJSONSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSJSONSerialization.swift; sourceTree = "<group>"; };
+ 5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSNotificationQueue.swift; sourceTree = "<group>"; };
612952F81C1B235900BE0FD9 /* TestNSNull.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSNull.swift; sourceTree = "<group>"; };
61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = "<group>"; };
61D6C9EE1C1DFE9500DEF583 /* TestNSTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTimer.swift; sourceTree = "<group>"; };
@@ -1085,6 +1087,7 @@
5EB6A15C1C188FC40037DCB8 /* TestNSJSONSerialization.swift */,
61A395F91C2484490029B337 /* TestNSLocale.swift */,
61F8AE7C1C180FC600FB62F0 /* TestNSNotificationCenter.swift */,
+ 5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */,
EA66F63F1BF1619600136161 /* TestNSNumber.swift */,
4DC1D07F1C12EEEF00B5948A /* TestNSPipe.swift */,
400E22641C1A4E58007C5933 /* TestNSProcessInfo.swift */,
@@ -1828,6 +1831,7 @@
84BA558E1C16F90900F48C54 /* TestNSTimeZone.swift in Sources */,
52829AD71C160D64003BC4EF /* TestNSCalendar.swift in Sources */,
45BBD92B1C30E9C200540DDB /* TestNSTask.swift in Sources */,
+ 5EF673AC1C28B527006212A3 /* TestNSNotificationQueue.swift in Sources */,
C93559291C12C49F009FD6A9 /* TestNSAffineTransform.swift in Sources */,
DCDBB8331C1768AC00313299 /* TestNSData.swift in Sources */,
83712C8E1C1684900049AD49 /* TestNSURLRequest.swift in Sources */,
diff --git a/Foundation/NSNotificationQueue.swift b/Foundation/NSNotificationQueue.swift
index 1923c0e..b94f827 100644
--- a/Foundation/NSNotificationQueue.swift
+++ b/Foundation/NSNotificationQueue.swift
@@ -1,3 +1,11 @@
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
public enum NSPostingStyle : UInt {
@@ -15,26 +23,105 @@
}
public class NSNotificationQueue : NSObject {
-
+
+ internal typealias NotificationQueueList = NSMutableArray
+ internal typealias NSNotificationListEntry = (NSNotification, [String]) // Notification ans list of modes the notification may be posted in.
+ internal typealias NSNotificationList = [NSNotificationListEntry] // The list of notifications to post
+
+ internal let notificationCenter: NSNotificationCenter
+ internal var asapList = NSNotificationList()
+ internal var idleList = NSNotificationList()
+
+ // The NSNotificationQueue instance is associated with current thread.
+ // The _notificationQueueList represents a list of notification queues related to the current thread.
+ private static var _notificationQueueList = NSThreadSpecific<NSMutableArray>()
+ internal static var notificationQueueList: NotificationQueueList {
+ return _notificationQueueList.get() {
+ return NSMutableArray()
+ }
+ }
+
+ // The default notification queue for the current thread.
+ private static var _defaultQueue = NSThreadSpecific<NSNotificationQueue>()
public class func defaultQueue() -> NSNotificationQueue {
- NSUnimplemented()
+ return _defaultQueue.get() {
+ return NSNotificationQueue(notificationCenter: NSNotificationCenter.defaultCenter())
+ }
}
public init(notificationCenter: NSNotificationCenter) {
- NSUnimplemented()
+ self.notificationCenter = notificationCenter
+ super.init()
+ NSNotificationQueue.registerQueue(self)
}
-
+
+ deinit {
+ NSNotificationQueue.unregisterQueue(self)
+ }
+
public func enqueueNotification(notification: NSNotification, postingStyle: NSPostingStyle) {
- NSUnimplemented()
+ enqueueNotification(notification, postingStyle: postingStyle, coalesceMask: [.CoalescingOnName, .CoalescingOnSender], forModes: nil)
}
public func enqueueNotification(notification: NSNotification, postingStyle: NSPostingStyle, coalesceMask: NSNotificationCoalescing, forModes modes: [String]?) {
- NSUnimplemented()
+ var runloopModes = [NSDefaultRunLoopMode]
+ if let modes = modes {
+ runloopModes = modes
+ }
+
+ if !coalesceMask.isEmpty {
+ self.dequeueNotificationsMatching(notification, coalesceMask: coalesceMask)
+ }
+
+ switch postingStyle {
+ case .PostNow:
+ let currentMode = NSRunLoop.currentRunLoop().currentMode
+ if currentMode == nil || runloopModes.contains(currentMode!) {
+ self.notificationCenter.postNotification(notification)
+ }
+ case .PostASAP:
+ self.asapList.append((notification, runloopModes))
+ NSUnimplemented() // post at the end of the current notification callout or timer
+ case .PostWhenIdle:
+ self.idleList.append((notification, runloopModes))
+ NSUnimplemented() // wait until the runloop is idle, then post the notification
+ }
}
- public func dequeueNotificationsMatching(notification: NSNotification, coalesceMask: Int) {
- NSUnimplemented()
+ public func dequeueNotificationsMatching(notification: NSNotification, coalesceMask: NSNotificationCoalescing) {
+ var predicate: (NSNotificationListEntry) -> Bool
+ switch coalesceMask {
+ case [.CoalescingOnName, .CoalescingOnSender]:
+ predicate = { (entryNotification, _) in
+ return notification.object === entryNotification.object && notification.name == entryNotification.name
+ }
+ case [.CoalescingOnName]:
+ predicate = { (entryNotification, _) in
+ return notification.name == entryNotification.name
+ }
+ case [.CoalescingOnSender]:
+ predicate = { (entryNotification, _) in
+ return notification.object === entryNotification.object
+ }
+ default:
+ return
+ }
+
+ self.asapList = self.asapList.filter(predicate)
+ self.idleList = self.idleList.filter(predicate)
+ }
+
+ // MARK: Private
+
+ private static func registerQueue(notificationQueue: NSNotificationQueue) {
+ self.notificationQueueList.addObject(notificationQueue)
+ }
+
+ private static func unregisterQueue(notificationQueue: NSNotificationQueue) {
+ guard self.notificationQueueList.indexOfObject(notificationQueue) != NSNotFound else {
+ return
+ }
+ self.notificationQueueList.removeObject(notificationQueue)
}
}
-
diff --git a/Foundation/NSThread.swift b/Foundation/NSThread.swift
index 066c942..ad2661d 100644
--- a/Foundation/NSThread.swift
+++ b/Foundation/NSThread.swift
@@ -20,13 +20,13 @@
Unmanaged<AnyObject>.fromOpaque(COpaquePointer(ctx)).release()
}
-private var NSThreadSpecificKeySet = false
-private var NSThreadSpecificKeyLock = NSLock()
-private var NSThreadSpecificKey = pthread_key_t()
-
internal class NSThreadSpecific<T: AnyObject> {
-
- private static var key: pthread_key_t {
+
+ private var NSThreadSpecificKeySet = false
+ private var NSThreadSpecificKeyLock = NSLock()
+ private var NSThreadSpecificKey = pthread_key_t()
+
+ private var key: pthread_key_t {
get {
NSThreadSpecificKeyLock.lock()
if !NSThreadSpecificKeySet {
@@ -40,18 +40,18 @@
}
internal func get(generator: (Void) -> T) -> T {
- let specific = pthread_getspecific(NSThreadSpecific.key)
+ let specific = pthread_getspecific(self.key)
if specific != UnsafeMutablePointer<Void>() {
return Unmanaged<T>.fromOpaque(COpaquePointer(specific)).takeUnretainedValue()
} else {
let value = generator()
- pthread_setspecific(NSThreadSpecific.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
+ pthread_setspecific(self.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
return value
}
}
internal func set(value: T) {
- let specific = pthread_getspecific(NSThreadSpecific.key)
+ let specific = pthread_getspecific(self.key)
var previous: Unmanaged<T>?
if specific != UnsafeMutablePointer<Void>() {
previous = Unmanaged<T>.fromOpaque(COpaquePointer(specific))
@@ -61,7 +61,7 @@
return
}
}
- pthread_setspecific(NSThreadSpecific.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
+ pthread_setspecific(self.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
if let prev = previous {
prev.release()
}
diff --git a/TestFoundation/TestNSNotificationQueue.swift b/TestFoundation/TestNSNotificationQueue.swift
new file mode 100644
index 0000000..27a1356
--- /dev/null
+++ b/TestFoundation/TestNSNotificationQueue.swift
@@ -0,0 +1,150 @@
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+
+#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
+ import Foundation
+ import XCTest
+#else
+ import SwiftFoundation
+ import SwiftXCTest
+#endif
+
+
+class TestNSNotificationQueue : XCTestCase {
+ var allTests : [(String, () -> ())] {
+ return [
+ ("test_defaultQueue", test_defaultQueue),
+ ("test_postNowToDefaultQueueWithoutCoalescing", test_postNowToDefaultQueueWithoutCoalescing),
+ ("test_postNowToDefaultQueueWithCoalescing", test_postNowToDefaultQueueWithCoalescing),
+ ("test_postNowToCustomQueue", test_postNowToCustomQueue),
+ ("test_postNowForDefaultRunLoopMode", test_postNowForDefaultRunLoopMode),
+ ("test_notificationQueueLifecycle", test_notificationQueueLifecycle),
+ ]
+ }
+
+ func test_defaultQueue() {
+ let defaultQueue1 = NSNotificationQueue.defaultQueue()
+ XCTAssertNotNil(defaultQueue1)
+ let defaultQueue2 = NSNotificationQueue.defaultQueue()
+ XCTAssertEqual(defaultQueue1, defaultQueue2)
+
+ executeInBackgroundThread() {
+ let defaultQueueForBackgroundThread = NSNotificationQueue.defaultQueue()
+ XCTAssertNotNil(defaultQueueForBackgroundThread)
+ XCTAssertEqual(defaultQueueForBackgroundThread, NSNotificationQueue.defaultQueue())
+ XCTAssertNotEqual(defaultQueueForBackgroundThread, defaultQueue1)
+ }
+ }
+
+ func test_postNowToDefaultQueueWithoutCoalescing() {
+ let notificationName = "test_postNowWithoutCoalescing"
+ let dummyObject = NSObject()
+ let notification = NSNotification(name: notificationName, object: dummyObject)
+ var numberOfCalls = 0
+ NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+ numberOfCalls += 1
+ }
+ let queue = NSNotificationQueue.defaultQueue()
+ queue.enqueueNotification(notification, postingStyle: .PostNow)
+ XCTAssertEqual(numberOfCalls, 1)
+ }
+
+ func test_postNowToDefaultQueueWithCoalescing() {
+ let notificationName = "test_postNowToDefaultQueueWithCoalescingOnName"
+ let dummyObject = NSObject()
+ let notification = NSNotification(name: notificationName, object: dummyObject)
+ var numberOfCalls = 0
+ NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+ numberOfCalls += 1
+ }
+ let queue = NSNotificationQueue.defaultQueue()
+ queue.enqueueNotification(notification, postingStyle: .PostNow)
+ queue.enqueueNotification(notification, postingStyle: .PostNow)
+ queue.enqueueNotification(notification, postingStyle: .PostNow)
+ // Coalescing doesn't work for the NSPostingStyle.PostNow. That is why we expect 3 calls here
+ XCTAssertEqual(numberOfCalls, 3)
+ }
+
+ func test_postNowToCustomQueue() {
+ let notificationName = "test_postNowToCustomQueue"
+ let dummyObject = NSObject()
+ let notification = NSNotification(name: notificationName, object: dummyObject)
+ var numberOfCalls = 0
+ let notificationCenter = NSNotificationCenter()
+ notificationCenter.addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+ numberOfCalls += 1
+ }
+ let notificationQueue = NSNotificationQueue(notificationCenter: notificationCenter)
+ notificationQueue.enqueueNotification(notification, postingStyle: .PostNow)
+ XCTAssertEqual(numberOfCalls, 1)
+ }
+
+ func test_postNowForDefaultRunLoopMode() {
+ let notificationName = "test_postNowToDefaultQueueWithCoalescingOnName"
+ let dummyObject = NSObject()
+ let notification = NSNotification(name: notificationName, object: dummyObject)
+ var numberOfCalls = 0
+ NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+ numberOfCalls += 1
+ }
+ let queue = NSNotificationQueue.defaultQueue()
+
+ let runLoop = NSRunLoop.currentRunLoop()
+ let endDate = NSDate(timeInterval: NSTimeInterval(0.05), sinceDate: NSDate())
+
+ let dummyTimer = NSTimer.scheduledTimer(0.01, repeats: false) { _ in
+ guard let runLoopMode = runLoop.currentMode else {
+ return
+ }
+
+ // post 2 notifications for the NSDefaultRunLoopMode mode
+ queue.enqueueNotification(notification, postingStyle: .PostNow, coalesceMask: [], forModes: [runLoopMode])
+ queue.enqueueNotification(notification, postingStyle: .PostNow)
+ // here we post notification for the NSRunLoopCommonModes. It shouldn't have any affect, because the timer is scheduled in NSDefaultRunLoopMode.
+ // The notification queue will only post the notification to its notification center if the run loop is in one of the modes provided in the array.
+ queue.enqueueNotification(notification, postingStyle: .PostNow, coalesceMask: [], forModes: [NSRunLoopCommonModes])
+ }
+ runLoop.addTimer(dummyTimer, forMode: NSDefaultRunLoopMode)
+ runLoop.runMode(NSDefaultRunLoopMode, beforeDate: endDate)
+ XCTAssertEqual(numberOfCalls, 2)
+ }
+
+ func test_notificationQueueLifecycle() {
+ // check that notificationqueue is associated with current thread. when the thread is destroyed, the queue should be deallocated as well
+ weak var notificationQueue: NSNotificationQueue?
+
+ self.executeInBackgroundThread() {
+ notificationQueue = NSNotificationQueue(notificationCenter: NSNotificationCenter())
+ XCTAssertNotNil(notificationQueue)
+ }
+
+ XCTAssertNil(notificationQueue)
+ }
+
+ // MARK: Private
+
+ private func executeInBackgroundThread(operation: () -> ()) {
+ var isFinished = false
+ let bgThread = NSThread() {
+ operation()
+ isFinished = true
+ }
+ bgThread.start()
+
+ self.waitForExpectation({ isFinished }, withTimeout: 0.2)
+ }
+
+ private func waitForExpectation(expectation: () -> Bool, withTimeout timeout: NSTimeInterval) {
+ let timeoutDate = NSDate(timeIntervalSinceNow: timeout)
+ while !expectation() && timeoutDate.timeIntervalSinceNow > 0.0 {
+ NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.01))
+ }
+ }
+
+}
diff --git a/TestFoundation/main.swift b/TestFoundation/main.swift
index 82d42c9..206ace5 100644
--- a/TestFoundation/main.swift
+++ b/TestFoundation/main.swift
@@ -39,6 +39,7 @@
TestNSLocale(),
TestNSNotificationCenter(),
TestNSNull(),
+ TestNSNotificationQueue(),
TestNSNumber(),
TestNSNumberFormatter(),
TestNSPipe(),