diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index fb7318fe2..3dd52408a 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84B99C9B1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; }; 84BB45431D6909C700B48537 /* NSMutableDictionary-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */; }; + 84C326872038C9F6006A025C /* CoalescingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C326862038C9F6006A025C /* CoalescingQueue.swift */; }; 84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */; }; 84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -225,6 +226,7 @@ 84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayNameProvider.swift; path = RSCore/DisplayNameProvider.swift; sourceTree = ""; }; 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OPMLRepresentable.swift; path = RSCore/OPMLRepresentable.swift; sourceTree = ""; }; 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableDictionary-Extensions.swift"; sourceTree = ""; }; + 84C326862038C9F6006A025C /* CoalescingQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoalescingQueue.swift; path = RSCore/CoalescingQueue.swift; sourceTree = ""; }; 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSAppleEventDescriptor+RSCore.h"; path = "AppKit/NSAppleEventDescriptor+RSCore.h"; sourceTree = ""; }; 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSAppleEventDescriptor+RSCore.m"; path = "AppKit/NSAppleEventDescriptor+RSCore.m"; sourceTree = ""; }; 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SendToBlogEditorApp.h; path = AppKit/SendToBlogEditorApp.h; sourceTree = ""; }; @@ -369,6 +371,7 @@ 848F6AE81FC2BC50002D422E /* ThreadSafeCache.swift */, 844B5B561FE9D36000C7C76A /* Keyboard.swift */, 84AD1EA420315A8700BC20B7 /* PasteboardWriterOwner.swift */, + 84C326862038C9F6006A025C /* CoalescingQueue.swift */, 84CFF5241AC3C8A200CEA6C8 /* Foundation */, 84CFF5551AC3CF4A00CEA6C8 /* AppKit */, 84CFF5661AC3D13F00CEA6C8 /* Images */, @@ -824,6 +827,7 @@ 844F91D61D90D86100820C48 /* RSTransparentContainerView.m in Sources */, 84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */, 8453F7DF1BDF337800B1C8ED /* RSMacroProcessor.m in Sources */, + 84C326872038C9F6006A025C /* CoalescingQueue.swift in Sources */, 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */, 84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */, 849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */, diff --git a/Frameworks/RSCore/RSCore/CoalescingQueue.swift b/Frameworks/RSCore/RSCore/CoalescingQueue.swift new file mode 100644 index 000000000..ce22ad5bf --- /dev/null +++ b/Frameworks/RSCore/RSCore/CoalescingQueue.swift @@ -0,0 +1,95 @@ +// +// CoalescingQueue.swift +// RSCore +// +// Created by Brent Simmons on 2/17/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// Use when you want to coalesce calls for something like updating visible table cells. +// Calls are uniqued. If you add a call with the same target and selector as a previous call, you’ll just get one call. +// Targets are weakly-held. If a target goes to nil, the call is not performed. +// The perform date is pushed off every time a call is added. +// Calls are FIFO. + +struct QueueCall: Equatable { + + weak var target: AnyObject? + let selector: Selector + + init(target: AnyObject, selector: Selector) { + + self.target = target + self.selector = selector + } + + func perform() { + + let _ = target?.perform(selector) + } + + static func ==(lhs: QueueCall, rhs: QueueCall) -> Bool { + + return lhs.target === rhs.target && lhs.selector == rhs.selector + } +} + +@objc public final class CoalescingQueue: NSObject { + + public let name: String + private let interval: TimeInterval + private var timer: Timer? = nil + private var calls = [QueueCall]() + + public init(name: String, interval: TimeInterval = 0.05) { + + self.name = name + self.interval = interval + } + + public func add(_ target: AnyObject, _ selector: Selector) { + + let queueCall = QueueCall(target: target, selector: selector) + add(queueCall) + } + + @objc func timerDidFire(_ sender: Any?) { + + let callsToMake = calls // Make a copy in case calls are added to the queue while performing calls. + resetCalls() + callsToMake.forEach { $0.perform() } + } +} + +private extension CoalescingQueue { + + func add(_ call: QueueCall) { + + restartTimer() + + if !calls.contains(call) { + calls.append(call) + } + } + + func resetCalls() { + + calls = [QueueCall]() + } + + func restartTimer() { + + invalidateTimer() + timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(timerDidFire(_:)), userInfo: nil, repeats: false) + } + + func invalidateTimer() { + + if let timer = timer, timer.isValid { + timer.invalidate() + } + timer = nil + } +}