How to using ReactiveCocoa to transparently authenticate before making API calls? - cocoa

I am using ReactiveCocoa in an app which makes calls to remote web APIs. But before any thing can be retrieved from a given API host, the app must provide the user's credentials and retrieve an API token, which is then used to sign subsequent requests.
I want to abstract away this authentication process so that it happens automatically whenever I make an API call. Assume I have an API client class that contains the user's credentials.
// getThing returns RACSignal yielding the data returned by GET /thing.
// if the apiClient instance doesn't already have a token, it must
// retrieve one before calling GET /thing
RAC(self.thing) = [apiClient getThing];
How can I use ReactiveCocoa to transparently cause the first (and only the first) request to an API to retrieve and, as a side effect, safely store an API token before any subsequent requests are made?
It is also a requirement that I can use combineLatest: (or similar) to kick off multiple simultaneous requests and that they will all implicitly wait for the token to be retrieved.
RAC(self.tupleOfThisAndThat) = [RACSignal combineLatest:#[ [apiClient getThis], [apiClient getThat]]];
Further, if the retrieve-token request is already in flight when an API call is made, that API call must wait until the retrieve-token request has completed.
My partial solution follows:
The basic pattern is going to be to use flattenMap: to map a signal which yields the token to a signal that, given the token, performs the desired request and yields the result of the API call.
Assuming some convenient extensions to NSURLRequest:
- (RACSignal *)requestSignalWithURLRequest:(NSURLRequest *)urlRequest {
if ([urlRequest isSignedWithAToken])
return [self performURLRequest:urlRequest];
return [[self getToken] flattenMap:^ RACSignal * (id token) {
NSURLRequest *signedRequest = [urlRequest signedRequestWithToken:token];
assert([urlRequest isSignedWithAToken]);
return [self requestSignalWithURLRequest:signedRequest];
}
}
Now consider the subscription implementation of -getToken.
In the trivial case, when the token has already been retrieved, the subscription yields the token immediately.
If the token has not been retrieved, the subscription defers to an authentication API call which returns the token.
If the authentication API call is in flight, it should be safe to add another observer without causing the authentication API call to be repeated over the wire.
However I'm not sure how to do this. Also, how and where to safely store the token? Some kind of persistent/repeatable signal?

So, there are two major things going on here:
You want to share some side effects (in this case, fetching a token) without re-triggering them every time there's a new subscriber.
You want anyone subscribing to -getToken to get the same values no matter what.
In order to share side effects (#1 above), we'll use RACMulticastConnection. Like the documentation says:
A multicast connection encapsulates the idea of sharing one subscription to a signal to many subscribers. This is most often needed if the subscription to the underlying signal involves side-effects or shouldn't be called more than once.
Let's add one of those as a private property on the API client class:
#interface APIClient ()
#property (nonatomic, strong, readonly) RACMulticastConnection *tokenConnection;
#end
Now, this will solve the case of N current subscribers that all need the same future result (API calls waiting on the request token being in-flight), but we still need something else to ensure that future subscribers get the same result (the already-fetched token), no matter when they subscribe.
This is what RACReplaySubject is for:
A replay subject saves the values it is sent (up to its defined capacity) and resends those to new subscribers. It will also replay an error or completion.
To tie these two concepts together, we can use RACSignal's -multicast: method, which turns a normal signal into a connection by using a specific kind of subject.
We can hook up most of the behaviors at initialization time:
- (id)init {
self = [super init];
if (self == nil) return nil;
// Defer the invocation of -reallyGetToken until it's actually needed.
// The -defer: is only necessary if -reallyGetToken might kick off
// a request immediately.
RACSignal *deferredToken = [RACSignal defer:^{
return [self reallyGetToken];
}];
// Create a connection which only kicks off -reallyGetToken when
// -connect is invoked, shares the result with all subscribers, and
// pushes all results to a replay subject (so new subscribers get the
// retrieved value too).
_tokenConnection = [deferredToken multicast:[RACReplaySubject subject]];
return self;
}
Then, we implement -getToken to trigger the fetch lazily:
- (RACSignal *)getToken {
// Performs the actual fetch if it hasn't started yet.
[self.tokenConnection connect];
return self.tokenConnection.signal;
}
Afterwards, anything that subscribes to the result of -getToken (like -requestSignalWithURLRequest:) will get the token if it hasn't been fetched yet, start fetching it if necessary, or wait for an in-flight request if there is one.

How about
...
#property (nonatomic, strong) RACSignal *getToken;
...
- (id)init {
self = [super init];
if (self == nil) return nil;
self.getToken = [[RACSignal defer:^{
return [self reallyGetToken];
}] replayLazily];
return self;
}
To be sure, this solution is functional identical to Justin's answer above. Basically we take advantage of the fact that convenience method already exists in RACSignal's public API :)

Thinking about token will expire later and we have to refresh it.
I store token in a MutableProperty, and used a lock to prevent multiple expired request to refresh the token, once the token is gained or refreshed, just request again with new token.
For the first few requests, since there's no token, request signal will flatMap to error, and thus trigger refreshAT, meanwhile we do not have refreshToken, thus trigger refreshRT, and set both at and rt in the final step.
here's full code
static var headers = MutableProperty(["TICKET":""])
static let atLock = NSLock()
static let manager = Manager(
configuration: NSURLSessionConfiguration.defaultSessionConfiguration()
)
internal static func GET(path:String!, params:[String: String]) -> SignalProducer<[String: AnyObject], NSError> {
let reqSignal = SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
manager.request(Router.GET(path: path, params: params))
.validate()
.responseJSON({ (response) -> Void in
if let error = response.result.error {
sink.sendFailed(error)
} else {
sink.sendNext(response.result.value!)
sink.sendCompleted()
}
})
}
return reqSignal.flatMapError { (error) -> SignalProducer<[String: AnyObject], NSError> in
return HHHttp.refreshAT()
}.flatMapError({ (error) -> SignalProducer<[String : AnyObject], NSError> in
return HHHttp.refreshRT()
}).then(reqSignal)
}
private static func refreshAT() -> SignalProducer<[String: AnyObject], NSError> {
return SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
if atLock.tryLock() {
Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
.validate()
.responseJSON({ (response) -> Void in
if let error = response.result.error {
sink.sendFailed(error)
} else {
let v = response.result.value!["data"]
headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
sink.sendCompleted()
}
atLock.unlock()
})
} else {
headers.signal.observe(Observer(next: { value in
print("get headers from local: \(value)")
sink.sendCompleted()
}))
}
}
}
private static func refreshRT() -> SignalProducer<[String: AnyObject], NSError> {
return SignalProducer<[String: AnyObject], NSError> {
sink, dispose in
Alamofire.Manager.sharedInstance.request(.POST, "http://example.com/auth/refresh")
.responseJSON({ (response) -> Void in
let v = response.result.value!["data"]
headers.value.updateValue(v!["at"] as! String, forKey: "TICKET")
sink.sendCompleted()
})
}
}

Related

How do I use delegates within Xamarin (specifically UNUserNotificationCenterDelegate)

I need to use the iOS 10 features in UNUserNotificationCenterDelegate. How can I implement this delegate in c# / Xamarin?
When using the an UNUserNotificationCenterDelegate, make sure that you assign it in the WillFinishLaunching or the FinishedLaunching methods in your app's UIApplicationDelegate.
You must assign your delegate object to the UNUserNotificationCenter object no later before your app finishes launching.
Ref: UNUserNotificationCenterDelegate
AppDelegate.cs Example
public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert, (approved, err) =>
{
// Handle the user approval or refusal of your notifications...
});
UNUserNotificationCenter.Current.Delegate = new MyUNUserNotificationCenterDelegate();
return true;
}
In that example, I am creating/assigning a delegate class named MyUNUserNotificationCenterDelegate, so you need to implement that class.
MyUNUserNotificationCenterDelegate class example:
This UNUserNotificationCenterDelegate example will capture each local notification sent and toggle between showing it on the lock screen or outputting the details to the syslog.
public class MyUNUserNotificationCenterDelegate : UNUserNotificationCenterDelegate
{
bool toggle;
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
if (toggle)
completionHandler(UNNotificationPresentationOptions.Alert);
else
{
Console.WriteLine(notification);
completionHandler(UNNotificationPresentationOptions.None);
}
toggle = !toggle;
}
}
Now you will actually need to send some notifications, this sets up a simple repeating notification:
Create/Schedule Local Notification:
// Schedule a repeating Notification...
var content = new UNMutableNotificationContent();
content.Title = new NSString("From SushiHangover");
content.Body = new NSString("StackOverflow rocks");
content.Sound = UNNotificationSound.Default;
var trigger = UNTimeIntervalNotificationTrigger.CreateTrigger(timeInterval: 60, repeats: true);
var request = UNNotificationRequest.FromIdentifier(identifier: "FiveSecond", content: content, trigger: trigger);
UNUserNotificationCenter.Current.AddNotificationRequest(request, (NSError error) =>
{
if (error != null) Console.WriteLine(error);
});
Every 60 seconds a notification is dispatched and if you are on the lock screen you will receive an alert every 120 seconds...
Recommend reading to understand how you Xamarin.iOS/C# to interact with delegates, protocols, and events:
iOS uses Objective-C delegates to implement the delegation pattern, in which one object passes work off to another. The object doing the work is the delegate of the first object. An object tells its delegate to do work by sending it messages after certain things happen. Sending a message like this in Objective-C is functionally equivalent to calling a method in C#. A delegate implements methods in response to these calls, and so provides functionality to the application.
Ref: Xamarin.iOS and Delegates

Is it possible to automatically fetch parse objects?

Many im services automatically display messages once the user on the other end has sent a message.
Right now, the only way I can think of to do this is to use an nstimer which will run the appropriate block of code which fetches the messages and updates the table view. This is resources intensive and can waste one of the requests per second. Is there any way to automate this process and make it happen only when a new message has been sent/received?
Here's an example of using didReceiveRemoteNotification inside of your app delegate to respond to push notifications. In particular, you care about the case where you are receiving the notification while the app is active.
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if (PFUser.currentUser() == nil) {
return
}
if (application.applicationState == UIApplicationState.Inactive || application.applicationState == UIApplicationState.Background) {
// Received the push notification when the app was in the background
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
// Inspect userInfo for the push notification payload
if let notificationPayloadTypeKey: String = userInfo["someKey"] as? String {
// Do something
}
} else {
// Received the push notification while the app is active
if let notificationPayloadTypeKey: String = userInfo["someKey"] as? String {
// Use NSNotificationCenter to inform your view to reload
NSNotificationCenter.defaultCenter().postNotificationName("loadMessages", object: nil)
}
}
}
Then you just need to add a listener inside of your view controller. Inside of viewDidLoad add the following which will call the function loadMessages whenever a notification is received.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "loadMessages", name: "loadMessages", object: nil)
If you download the code for Parse's Anypic example project you can see how they handle remote notifications.

struggling with asynchronous patterns using NSURLSession

I'm using Xcode 7 and Swift 2 but my question isn't necessarily code specific, I'll gladly take help of any variety.
In my app I have a list of favorites. Due to API TOS I can't store any data, so I just keep a stub I can use to lookup when the user opens the app. I also have to look up each favorite one by one as there is no batch method. Right now I have something like this:
self.api.loadFavorite(id, completion: { (event, errorMessage) -> Void in
if errorMessage == "" {
if let rc = self.refreshControl {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
rc.endRefreshing()
}
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.viewData.append(event)
self.viewData.sortInPlace({ $0.eventDate.compare($1.eventDate) == NSComparisonResult.OrderedDescending })
self.tableView.reloadData()
}
} else {
// some more error handling here
}
})
in api.loadFavorite I'm making a typical urlSession.dataTaskWithURL which is itself asynchronous.
You can see what happens here is that the results are loaded in one by one and after each one the view refreshes. This does work but its not optimal, for long lists you get a noticeable "flickering" as the view sorts and refreshes.
I want to be able to get all the results then just refresh once. I tried putting a dispatch group around the api.loadFavorites but the async calls in dataTaskWith URL don't seem to be bound by that group. I also tried putting the dispatch group around just the dataTaskWithURL but didn't have any better luck. The dispatch_group_notify always fires before all the data tasks are done.
Am I going at this all wrong? (probably) I considered switching to synchronous calls in the background thread since the api only allows one connection per client anyway but that just feels like the wrong approach.
I'd love to know how to get async calls that make other async calls grouped up so that I can get a single notification to update my UI.
For the record I've read about every dispatch group thread I could find here and I haven't been able to make any of them work. Most examples on the web are very simple, a series of print's in a dispatch group with a sleep to prove the case.
Thanks in advance.
If you want to invoke your method loadFavorite asynchronously in a loop for all favorite ids - which executes them in parallel - you can achieve this with a new method as shown below:
func loadFavorites(ids:[Int], completion: ([Event], ErrorType?) -> ()) {
var count = ids.count
var events = [Event]()
if count == 0 {
dispatch_async(dispatch_get_global_queue(0, 0)) {
completion(events, nil)
}
return
}
let sync_queue = dispatch_queue_create("sync_queue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0))
for i in ids {
self.api.loadFavorite(i) { (event, message) in
dispatch_async(sync_queue) {
if message == "" {
events.append(event)
if --count == 0 {
dispatch_async(dispatch_get_global_queue(0, 0)) {
completion(events, nil)
}
}
}
else {
// handle error
}
}
}
}
}
Note:
- Use a sync queue in order to synchronise access to shared array
events and the counter!
- Use a global dispatch queue where you invoke the completion handler!
Then call it like below:
self.loadFavorites(favourites) { (events, error) in
if (error == nil) {
events.sortInPlace({ $0.eventDate.compare($1.eventDate) == NSComparisonResult.OrderedDescending })
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.viewData = events
self.tableView.reloadData()
}
}
if let rc = self.refreshControl {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
rc.endRefreshing()
}
}
Note also, that you need a different approach when you want to ensure that your calls to loadFavorite should be sequential.
If you need to support cancellation (well, who does not require this?), you might try to cancel the NSURLSession's tasks. However, in this case I would recommend to utilise a third party library which already supports cancellation of network tasks.
Alternatively, and in order to greatly simplify your asynchronous problems like those, build your network task and any other asynchronous task around a general utility class, frequently called Future or Promise. A future represents an eventual result, and is quite light wight. They are also "composable", that is you can define "continuations" which get invoked when the future completes, which in turn returns yet another future where you can add more continuations, and so force. See wiki Futures and Promises.
There are a couple of implementations in Swift and Objective-C. Ideally, these should also support cancellation. Unfortunately, I don't know any Swift library implementing Futures or Promises which support cancellation at this time - except my own library, which is not yet Open Source.
Another library which helps to solve common and also very complex asynchronous patterns is ReactiveCocoa, though it has a very steep learning curve and adds quite a lot of code to your project.
This is what finally worked for me. Easy once I figured it out. My problem was trying to take ObjC examples and rework them for swift.
func migrateFavorites(completion:(error: Bool) -> Void) {
let migrationGroup = dispatch_group_create()
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// A lot of other code in there, fetching some core data etc
dispatch_group_enter(migrationGroup)
self.api.loadFavorite(id, completion: { (event, errorMessage) -> Void in
if errorMessage == "" {
if let rc = self.refreshControl {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
rc.endRefreshing()
}
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.viewData.append(event)
self.viewData.sortInPlace({ $0.eventDate.compare($1.eventDate) == NSComparisonResult.OrderedDescending })
self.tableView.reloadData()
}
} else {
// some more error handling here
}
dispatch_group_leave(migrationGroup)
})
dispatch_group_notify(migrationGroup, queue) { () -> Void in
NSLog("Migration Queue Complete")
dispatch_async(dispatch_get_main_queue()) { () -> Void in
completion(error: migrationError)
}
}
}
The key was:
ENTER the group just before the async call
LEAVE the group as the last line in the completion handler
As I mentioned all this is wrapped up in a function so I put the function's completion handler inside the dispatch_group_notify. So I call this function and the completion handler only gets invoked when all the async tasks are complete. Back on my main thread I check for the error and refresh the ui.
Hopefully this helps someone with the same problem.

Storing a Boolean for access by Parse login

I want to store a Boolean which grants access to certain options when a user logs in with Parse, but when I log in and declare it to be true, the variable seems to remain false.
I tried using NSUserDefaults and storing a global variable.
This is where I log in on one view controller:
PFUser.logInWithUsernameInBackground(userName.text!, password: password.text!) {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "accessGranted")
}
This functions fine and prints "Success" if I try.
On another view controller I have things like this:
addButton.enabled = NSUserDefaults.standardUserDefaults().boolForKey("accessGranted")
You can use NSUserDefaults to store your Bool globally this way:
Set your Bool this way:
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "YourKey")
Then you can access it anywhere in your project this way:
let yourBool = NSUserDefaults.standardUserDefaults().boolForKey("YourKey")
Hope this will help.
NSUserDefaults:
Your are missing key component for it to work - you have to save the settings:
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "accessGranted")
NSUserDefaults.standardUserDefaults().synchronize()
After that, your data that you stored will be stored to disk and you can access them anytime.
One-time assign to property:
After reading through comment, in order to access property only once, you can use something like this:
var accessGranted : Bool?
if accessGranted == nil {
accessGranted = yourValue
}
or as of Swift 2.0 (more on it here):
var accessGranted : Bool?
guard let granted = accessGranted else {
accessGranted = yourValue
}
Edit 2:
To notify second VC about changes, you can use multiple mechanisms (protocols / delegates etc.), but usually for this kind of message you would use Notifications (so your whole application can listen to that).
Notifications
You can use notifications like this:
In your "login" call, notify application that your access status changed, like this:
let NOTIFICATION_ACCESS_CHANGED_KEY = "NotificationAccessChanged"
NSNotificationCenter.defaultCenter().postNotificationName(NOTIFICATION_ACCESS_CHANGED)
then, everywhere where you are interested to listen to that change, put notification listener:
NSNotificationCenter.defaultCenter().addObserverForName(NOTIFICATION_ACCESS_CHANGED, object: nil, queue: nil) { (notification) -> Void in
// This block will be called when you post notification
self.doSomething()
}
Hope it helps!

Can I receive a callback whenever an NSPasteboard is written to?

I've read Apple's Pasteboard Programming Guide, but it doesn't answer a particular question I have.
I'm trying to write a Cocoa application (for OS X, not iOS) that will keep track of everything that is written to the general pasteboard (so, whenever any application copies and pastes, but not, say, drags-and-drops, which also makes use of NSPasteboard). I could (almost) accomplish this by basically polling the general pasteboard on a background thread constantly, and checking changeCount. Of course, doing this would make me feel very dirty on the inside.
My question is, is there a way to ask the Pasteboard server to notify me through some sort of callback any time a change is made to the general pasteboard? I couldn't find anything in the NSPasteboard class reference, but I'm hoping it lurks somewhere else.
Another way I could imagine accomplishing this is if there was a way to swap out the general pasteboard implementation with a subclass of NSPasteboard that I could define myself to issue a callback. Maybe something like this is possible?
I would greatly prefer if this were possible with public, App Store-legal APIs, but if using a private API is necessary, I'll take that too.
Thanks!
Unfortunately the only available method is by polling (booo!). There are no notifications and there's nothing to observe for changed pasteboard contents. Check out Apple's ClipboardViewer sample code to see how they deal with inspecting the clipboard. Add a (hopefully not overzealous) timer to keep checking for differences and you've got a basic (if clunky) solution that should be App-Store-Friendly.
File an enhancement request at bugreporter.apple.com to request notifications or some other callback. Unfortunately it wouldn't help you until the next major OS release at the earliest but for now it's polling until we all ask them to give us something better.
There was once a post on a mailing list where the decision against a notification api was described. I can't find it right now though. The bottom line was that probably too many applications would register for that api even though they really wouldn't need to. If you then copy something the whole system goes through the new clipboard content like crazy, creating lots of work for the computer. So i don't think they'll change that behavior anytime soon. The whole NSPasteboard API is internally built around using the changeCount, too. So even your custom subclass of NSPasteboard would still have to keep polling.
If you really want to check if the pasteboard changed, just keep observing the changeCount very half second. Comparing integers is really fast so there's really no performance issue here.
Based on answer provided by Joshua I came up with similar implementation but in swift, here is the link to its gist: PasteboardWatcher.swift
Code snippet from same:
class PasteboardWatcher : NSObject {
// assigning a pasteboard object
private let pasteboard = NSPasteboard.generalPasteboard()
// to keep track of count of objects currently copied
// also helps in determining if a new object is copied
private var changeCount : Int
// used to perform polling to identify if url with desired kind is copied
private var timer: NSTimer?
// the delegate which will be notified when desired link is copied
weak var delegate: PasteboardWatcherDelegate?
// the kinds of files for which if url is copied the delegate is notified
private let fileKinds : [String]
/// initializer which should be used to initialize object of this class
/// - Parameter fileKinds: an array containing the desired file kinds
init(fileKinds: [String]) {
// assigning current pasteboard changeCount so that it can be compared later to identify changes
changeCount = pasteboard.changeCount
// assigning passed desired file kinds to respective instance variable
self.fileKinds = fileKinds
super.init()
}
/// starts polling to identify if url with desired kind is copied
/// - Note: uses an NSTimer for polling
func startPolling () {
// setup and start of timer
timer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: Selector("checkForChangesInPasteboard"), userInfo: nil, repeats: true)
}
/// method invoked continuously by timer
/// - Note: To keep this method as private I referred this answer at stackoverflow - [Swift - NSTimer does not invoke a private func as selector](http://stackoverflow.com/a/30947182/217586)
#objc private func checkForChangesInPasteboard() {
// check if there is any new item copied
// also check if kind of copied item is string
if let copiedString = pasteboard.stringForType(NSPasteboardTypeString) where pasteboard.changeCount != changeCount {
// obtain url from copied link if its path extension is one of the desired extensions
if let fileUrl = NSURL(string: copiedString) where self.fileKinds.contains(fileUrl.pathExtension!){
// invoke appropriate method on delegate
self.delegate?.newlyCopiedUrlObtained(copiedUrl: fileUrl)
}
// assign new change count to instance variable for later comparison
changeCount = pasteboard.changeCount
}
}
}
Note: in the shared code I am trying to identify if user has copied a
file url or not, the provided code can easily be modified for other general
purposes.
For those who need simplified version of code snippet that gets the job done in Swift 5.7,
it just works (base on #Devarshi code):
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
how to use is as below:
watch {
print("detected : \($0)")
}
then if you attempt copy any text in your pasteboard, it will watch and print out to the console like below..
detected : your copied message in pasteboard
detected : your copied message in pasteboard
in case, full code sample for how to use it for example in SwiftUI:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
watch {
print("detect : \($0)")
}
}
}
}
func watch(using closure: #escaping (_ copiedString: String) -> Void) {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
guard let copiedString = pasteboard.string(forType: .string),
pasteboard.changeCount != changeCount else { return }
defer {
changeCount = pasteboard.changeCount
}
closure(copiedString)
}
}
}
It's not necessary to poll. Pasteboard would generally only be changed by the current view is inactive or does not have focus. Pasteboard has a counter that is incremented when contents change. When window regains focus (windowDidBecomeKey), check if changeCount has changed then process accordingly.
This does not capture every change, but lets your application respond if the Pasteboard is different when it becomes active.
In Swift...
var pasteboardChangeCount = NSPasteboard.general().changeCount
func windowDidBecomeKey(_ notification: Notification)
{ Swift.print("windowDidBecomeKey")
if pasteboardChangeCount != NSPasteboard.general().changeCount
{ viewController.checkPasteboard()
pasteboardChangeCount = NSPasteboard.general().changeCount
}
}
I have a solution for more strict case: detecting when your content in NSPasteboard was replaced by something else.
If you create a class that conforms to NSPasteboardWriting and pass it to -writeObjects: along with the actual content, NSPasteboard will retain this object until its content is replaced. If there are no other strong references to this object, it get deallocated.
Deallocation of this object is the moment when new NSPasteboard got new content.

Resources