├── .gitignore ├── Extensions └── UIKit │ ├── UIScrollView.m │ └── UIView.m ├── LICENSE ├── Meta ├── SLUG Lightning 2015 notes.txt └── SLUG Lightning 2015.md ├── README.md ├── SPAsync (Mac) ├── SPAsync__Mac_.h └── SPAsync__Mac_.m ├── SPAsync (Mac)Tests └── Info.plist ├── SPAsync.podspec ├── SPAsync.xcodeproj └── project.pbxproj ├── SPAsyncTests ├── SPAgentTest.h ├── SPAgentTest.m ├── SPAsyncTests-Info.plist ├── SPAwaitTest.h ├── SPAwaitTest.m ├── SPKVOTaskTest.m ├── SPTaskTest.h ├── SPTaskTest.m └── en.lproj │ └── InfoPlist.strings ├── Sources ├── NSObject+SPInvocationGrabbing.m ├── SPAgent.m ├── SPAwait.m ├── SPKVOTask.m └── SPTask.m ├── Support ├── SPAsync-Framework-Info.plist └── SPAsync-Prefix.pch ├── Swift ├── Swift-Bridging-Header.h ├── Task.swift └── main.swift └── include └── SPAsync ├── NSObject+SPInvocationGrabbing.h ├── SPAgent.h ├── SPAsync.h ├── SPAsyncNamespacing.h ├── SPAwait.h ├── SPKVOTask.h ├── SPTask.h └── UIKit ├── UIKit.h ├── UIScrollView.h └── UIView.h /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | xcuserdata 3 | Pods 4 | Podfile.lock 5 | Packages 6 | *.dylib 7 | *.bundle 8 | *.bundle.tgz 9 | *.codekit 10 | *.xcworkspacedata 11 | *.xccheckout 12 | *.xcscmblueprint 13 | 14 | .DS_Store -------------------------------------------------------------------------------- /Extensions/UIKit/UIScrollView.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface SPAsyncScrollViewClosure : NSObject 6 | @property(nonatomic) SPTaskCompletionSource *source; 7 | @property(nonatomic,weak) id oldDelegate; 8 | @end 9 | 10 | @implementation UIScrollView (SPAsyncAnimation) 11 | static const void *scrollClosureKey = &scrollClosureKey; 12 | - (SPTask*)task_setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated 13 | { 14 | if(!animated) { 15 | [self setContentOffset:contentOffset animated:NO]; 16 | return [SPTask delay:0]; 17 | } else { 18 | SPAsyncScrollViewClosure *closure = [SPAsyncScrollViewClosure new]; 19 | closure.source = [SPTaskCompletionSource new]; 20 | closure.oldDelegate = self.delegate; 21 | objc_setAssociatedObject(self, scrollClosureKey, closure, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 22 | 23 | self.delegate = closure; 24 | [self setContentOffset:contentOffset animated:YES]; 25 | 26 | return closure.source.task; 27 | } 28 | } 29 | @end 30 | 31 | @implementation SPAsyncScrollViewClosure 32 | - (BOOL)respondsToSelector:(SEL)aSelector 33 | { 34 | return [super respondsToSelector:aSelector] || [self.oldDelegate respondsToSelector:aSelector]; 35 | } 36 | - (id)forwardingTargetForSelector:(SEL)aSelector 37 | { 38 | return self.oldDelegate; 39 | } 40 | 41 | - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 42 | { 43 | scrollView.delegate = self.oldDelegate; 44 | if([self.oldDelegate respondsToSelector:_cmd]) 45 | [self.oldDelegate scrollViewDidEndScrollingAnimation:scrollView]; 46 | [self.source completeWithValue:nil]; 47 | objc_setAssociatedObject(scrollView, scrollClosureKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 48 | } 49 | 50 | @end -------------------------------------------------------------------------------- /Extensions/UIKit/UIView.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @implementation UIView (SPAsyncAnimation) 5 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations 6 | { 7 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 8 | [self animateWithDuration:duration delay:delay options:options animations:animations completion:^(BOOL finished) { 9 | [source completeWithValue:@(finished)]; 10 | }]; 11 | return source.task; 12 | } 13 | 14 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations 15 | { 16 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 17 | [self animateWithDuration:duration animations:animations completion:^(BOOL finished) { 18 | [source completeWithValue:@(finished)]; 19 | }]; 20 | return source.task; 21 | 22 | } 23 | 24 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations 25 | { 26 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 27 | [self animateWithDuration:duration delay:delay usingSpringWithDamping:dampingRatio initialSpringVelocity:velocity options:options animations:animations completion:^(BOOL finished) { 28 | [source completeWithValue:@(finished)]; 29 | }]; 30 | return source.task; 31 | } 32 | @end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 by Joachim Bengtsson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Meta/SLUG Lightning 2015 notes.txt: -------------------------------------------------------------------------------- 1 | SLUG Lightning 2015 notes 2 | 3 | Hello SLUG! I'm nevyn, and I'm very honored to get to show you my hacks. I usually do my stupid shit using the Objective-C runtime, as I've done for years at Spotify, and now for a few years at Lookback. One of my favorite pieces of ObjC is my work on SPAsync, which is a light-weight promises and async primitives library. A while ago, I ported it to Swift and found some interesting uses, which I'd love to show you. 4 | 5 | First some concepts. One of my least favorite code smells is deeply nested scopes, and in particular the callback-in-a-callback. There are three problem areas. 6 | 7 | * Error handling. Like the if-success-else-error pattern, it moves the error handling code as far away from the source of the error as possible, which is very difficult to read and follow. The more we nest the callbacks, the worse this error becomes. 8 | * Cancellation. Each API has a different way of handling cancellation, and if you're between callbacks, you'll need to add your own cancellation code as well. 9 | * It's very difficult to write code with multiple dependencies, e g if you're waiting for task A and B to finish before you can do C. This gets much worse with error handling. 10 | 11 | In short, this is a situation that requires abstraction. A single concept that abstracts the four concepts "a value I will get in the future", error handling, cancellation, and dependencies between asynchronous tasks. 12 | 13 | Both GCD and NSOperationQueue can give you a rather awkward API for working with these four concepts, but they're really models for concurrency rather than for asynchrony. In particular, NSOperation does not abstract “value in the future”. Apple tried to convince us otherwise with the “Advanced Operations” talk this year at WWDC, but: Like, if NSOperation was a great way of modeling asynchronous tasks, why doesn't EVERY iOS API return an NSOperation for any asynchronous task, like animation, network operations, file operations, user interactions? NSOperationQueue is more useful to indicate where you want to concurrently run your work. 14 | 15 | This is where third party libraries like ReactiveCocoa step in. RAC has great abstractions for these four concepts, but also does a lot more that you might not want for your app. RAC is inspired from the Reactive Extensions library in C#, and it turns out that C# has a lot of other nice libraries for async, such as the Task class. A Task can also be called a "promise" or a "future", and is a single class that encompasses all four concepts, and is very composable and nice to design apps with. 16 | 17 | To me, this is the missing piece in iOS development. KVO gives us generic value change callbacks to ANY iOS code, which is amazingly powerful and allows us to layer even more powerful tools on top of it, like RAC. If Apple incorporated promises into the standard libraries, we could have a way to abstract callbacks altogether throughout the platform, and we could build great tools on top of it. 18 | 19 | The next best thing is for myself to build it, I ported the C# class to Objective-C, and then to swift. Let's take a look. 20 | 21 | * First there's a factory API to create and complete tasks, and this is separate from the Task itself so that the producer and consumer of a task don't interfere with each other. It's not very interesting. 22 | 23 | * Here's the Task class itself, templated on the type of the future value. There's no way to synchronously get the value, because that could only be implemented using locking. Instead, there are callbacks for getting a value, an error, and one that runs in both cases, even on cancellation. If I stopped here, imagine how nice that would be! A single generic way of adding callbacks to any asynchronous task anywhere! Even multiple callbacks if you need it! 24 | 25 | * I digress. Next up is a way of expressing dependencies, by chaining tasks together. This is where the type signatures start getting hairy. The first variant lets you work on a value once it comes in, and yielding a derived value; such as parsing network data once it comes in into a model object. If you already have a parser method that returns a task, you can use the second variant. 26 | 27 | I'll show you. Let's fetch a network resource, parse it, and then update the UI. In scenario A, we have a synchronous 'parse' function. It's slow, so we're calling then with a background queue. 'then' returns a brand new Task, which we add a callback to, that is called once we're done parsing the model. We can then update the UI with it. 28 | 29 | In scenario B, parse already returns a Task because it itself has an internal worker queue. We can just give it to then, and then will give us a new Task that represents first fetching the network resource, then parsing it with the parser. We add a callback to it that updates the UI. 30 | 31 | Here I'm also showing a really nice thing that Swift does that ObjC doesn't. Since function pointers, method pointers and blocks are the same thing, we can just give it an existing method instead of creating an anonymous block. I like it. 32 | 33 | Let's finish up with some niceties. cancel lets us cancel a whole chain of tasks with a single call. awaitAll lets us wait for the value of several tasks in one go. Here's a not very nice example because it FIRST downloads all the images, then parses them, but it's really nice that a single line can cancel all of those operations. 34 | 35 | Now that we got all the basic functionality down, let's look at the really fun stuff that happens once you try to use the Swift type system to generically wrap existing callback-based APIs. SPTask contains Task.wrap, which takes a callback-based function and returns a task-based function. 36 | 37 | For example, we can wrap sendAsynchronousRequest, so that we get an alternative version that instead of taking a completionHandler, it returns a Task. We can then call it like any other function, and it is automatically and type-safely wrapped. 38 | 39 | I'm used to doing that kind of stuff in Objective C, but there we have no type safety. In Swift, we can to a higher order function transformation and retain all the type information. How'd you even do that? Let's look at the glorious prototype. Unfortunately Swift doesn't have variadic generics, so this one is hard-coded for two parameters. What we have here is a function that takes a asyncFunction that takes two parameters and a callback that takes a return value and an error. The callback and the asyncFunction both return nothing. The wrap function itself however, returns a new function. This new function takes the same two parameters as asyncFunction, but instead of taking a callback, it returns a Task, parameterized with the same type as the parameter type of the callback. Kind of scary-looking, but actually rather straightforward. 40 | 41 | In the implementation, we're creating a task factory, and returning the new task-oriented function. It in turn just calls the original function, implements the callback, and signals the task to complete or fail, based on the values we get. 42 | 43 | 44 | 45 | I did a variant of this presentation the first time a few years ago, and afterwards someone came up to me and told me that I had rediscovered the monad. I hadn't done enough functional back then to understand what he meant, and I still haven't quite, but I'm starting to realize that you could probably express all of these concepts much more functionally and much nicer. I guess I'll have to come back here when that happens :) 46 | 47 | If you want to read more about my thoughts on asynchrony, I have an extremely ranty article called "Methods of Concurrency" (at http://overooped.com/post/41803252527/methods-of-concurrency), which I invite you to read. I'm also working on a follow-up, detailing the very very juicy runtime hackery that a version of the "await" feature from C# ported to Objective-C entails.. 48 | 49 | There's also PromiseKit, which is very similar to what I just presented. 50 | 51 | You can reach me on Twitter, and I believe we have a few minutes for QA. Thank you for listening. -------------------------------------------------------------------------------- /Meta/SLUG Lightning 2015.md: -------------------------------------------------------------------------------- 1 | # Promise, it's async! 2 | ## SLUG Lightning 2015-09-24 3 | ### @nevyn, @lookback 4 | 5 | --- 6 | 7 | # [Fit] SPAsync 8 | 9 | --- 10 | 11 | # Nested scopes 12 | ```swift 13 | 💩.fetchFromNetwork(input) { intermediate in 14 | 💩.parseResponse(intermediate) { again in 15 | dispatch_async(dispatch_get_main_queue()) { 16 | updateUI(output) 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | --- 23 | 24 | ## Error handling 25 | ```swift 26 | 💩.fetchFromNetwork(input, callback: { intermediate in 27 | 💩.parseResponse(intermediate, callback: { again in 28 | dispatch_async(dispatch_get_main_queue()) { 29 | updateUI(output) 30 | } 31 | }, errback: { error in 32 | displayError("when parsing response, ", error) 33 | }) 34 | }, errback: { error in 35 | // This error is VERY far away from fetchFromNetwork! 36 | displayError("when fetching from network, ", error) 37 | }) 38 | ``` 39 | 40 | --- 41 | 42 | ## Cancellation 43 | ```swift 44 | var cancelled = false 45 | block var cancellable : Cancellable? 46 | let operation = 💩.fetchFromNetwork(input, callback: { intermediate in 47 | if(cancelled) return 48 | cancellable = 💩.parseResponse(intermediate, callback: { again in 49 | if(cancelled) return 50 | ... 51 | }) 52 | }) 53 | 54 | func cancel() { 55 | cancelled = true 56 | operation.cancel() 57 | cancellable?.stopOperation() 58 | } 59 | ``` 60 | 61 | --- 62 | 63 | ## Dependencies 64 | 65 | ``` swift 66 | func 💩()... 67 | ``` 68 | 69 | --- 70 | 71 | # Four async concepts 72 | 73 | * A-value-in-the-future 74 | * Error handler 75 | * Cancellation 76 | * Dependencies 77 | 78 | --- 79 | 80 | # [Fit] GCD? 81 | # [Fit] NSOperation? 82 | 83 | --- 84 | 85 | # [Fit] ReactiveCocoa? 86 | 87 | --- 88 | 89 | # [Fit] C♯ 90 | 91 | --- 92 | 93 | # [Fit] Task 94 | ## [Fit] / "promise" / "future" 95 | 96 | 97 | --- 98 | 99 | # Help me Apple, you're my only hope! 100 | 101 | * A-value-in-the-future 102 | * Error handler 103 | * Cancellation 104 | * Dependencies 105 | 106 | ... IN FOUNDATION 107 | 108 | --- 109 | 110 | # SPTask.swift 1/4 111 | 112 | ```swift 113 | 114 | class TaskCompletionSource { 115 | public let task: Task 116 | func completeWithValue(value: T) 117 | func failWithError(error: NSError!) 118 | } 119 | 120 | ``` 121 | 122 | --- 123 | 124 | # SPTask.swift 2/4 125 | 126 | ```swift 127 | class Task { 128 | public func addCallback( 129 | on queue: dispatch_queue_t, 130 | callback: (T -> Void) 131 | ) -> Self 132 | 133 | public func addErrorCallback( 134 | on queue: dispatch_queue_t, 135 | callback: (NSError! -> Void) 136 | ) -> Self 137 | 138 | public func addFinallyCallback( 139 | on queue: dispatch_queue_t, 140 | callback: (Bool -> Void) 141 | ) -> Self 142 | } 143 | ``` 144 | 145 | --- 146 | 147 | ## Callback example 148 | 149 | ```swift 150 | 151 | // Two of these three are executed immediately after each other 152 | network.fetch(resource).addCallback { json in 153 | let modelObject = parse(json) 154 | updateUI(modelObject) 155 | }.addErrback { error in 156 | displayDialog(error) 157 | }.addFinally { cancelled in 158 | if !cancelled { 159 | viewController.dismiss() 160 | } 161 | } 162 | 163 | ``` 164 | 165 | 166 | --- 167 | 168 | # SPTask.swift 3/4 169 | 170 | ```swift 171 | class Task { 172 | public func then(on queue:dispatch_queue_t, worker: (T -> T2)) -> Task 173 | public func then(chainer: (T -> Task)) -> Task 174 | } 175 | ``` 176 | 177 | --- 178 | 179 | ## Chaining example 180 | 181 | ```swift 182 | 183 | // A: inline background parsing on _worker_queue 184 | func parse(json) -> T 185 | 186 | network.fetch(resource) 187 | .then(on: _worker_queue) { json in 188 | // First this function runs, running parse on _worker_queue... 189 | return parse(json) 190 | }.addCallback { modelObject in 191 | // ... and when it's done, this function runs on main 192 | updateUI(modelObject) 193 | }.addErrorCallback { ... } 194 | 195 | // B: background parsing on Parser's own thread with async method 196 | class Parser { 197 | func parse(json) -> Task 198 | } 199 | 200 | network.fetch(resource) 201 | .then(_parser.parse) // parser is responsible for doing async work on its own 202 | .addCallback(updateUI) // and then updateUI is called with the model object 203 | .addErrorCallback(displayError) 204 | ``` 205 | 206 | --- 207 | 208 | # SPTask.swift 4/4 209 | 210 | ```swift 211 | class Task { 212 | public func cancel() 213 | 214 | static func awaitAll(tasks: [Task]) -> Task<[Any]> 215 | } 216 | ``` 217 | 218 | --- 219 | 220 | ## cancel and awaitAll example 221 | 222 | ```swift 223 | 224 | let imagesTask = Task.awaitAll(network.fetchImages(resource)).then { imageDatas in 225 | return Task.awaitAll(imageDatas.map { data in 226 | return parseImage(data) 227 | }) 228 | }.addCallback { images in 229 | showImages(image) 230 | } 231 | 232 | func viewDidDisappear() 233 | { 234 | 235 | // All downloading and parsing is cancelled 236 | imagesTask.cancel() 237 | } 238 | 239 | ``` 240 | 241 | --- 242 | 243 | # [Fit] Grand finale 244 | 245 | --- 246 | 247 | # [Fit] Task.wrap() 248 | 249 | --- 250 | 251 | ```swift 252 | 253 | class NSURLConnection { 254 | func sendAsynchronousRequest( 255 | request: NSURLRequest, 256 | queue: NSOperationQueue, 257 | completionHandler: (NSURLResponse?, NSError?) -> Void 258 | ) 259 | } 260 | 261 | extension NSURLConnection { 262 | // : (NSURLRequest, queue) -> Task 263 | let asyncTaskRequest = Task.wrap(NSURLConnection.sendAsynchronousRequest) 264 | } 265 | 266 | NSURLConnection.asyncTaskRequest(myRequest, mainQueue) 267 | .then(_parser.parse) 268 | .then(_db.store) 269 | .then(_ui.update) 270 | ``` 271 | 272 | 273 | --- 274 | 275 | ```swift 276 | 277 | extension Task { 278 | func wrap ( 279 | asyncFunction: ( 280 | p1: P1, 281 | p2: P2, 282 | callback: ( 283 | r1: R1, 284 | err: NSError? 285 | ) -> Void 286 | ) -> Void 287 | ) -> (P1, P2) -> Task 288 | } 289 | ``` 290 | 291 | --- 292 | 293 | ```swift 294 | 295 | extension Task { 296 | func wrap ( 297 | asyncFunction: ( 298 | p1: P1, 299 | p2: P2, 300 | callback: ( 301 | r1: R1, 302 | err: NSError? 303 | ) -> Void 304 | ) -> Void 305 | ) -> (P1, P2) -> Task 306 | { 307 | let source = TaskCompletionSource() 308 | 309 | return { (p1: P1, p2: P2) -> Task in 310 | asyncFunc(p1: p1, p2: p2, callback: { (r1: R1, error: NSError?) -> Void in 311 | if let error = error { 312 | source.failWithError(error) 313 | } else { 314 | source.completeWithValue(r1) 315 | } 316 | }) 317 | return source.task 318 | } 319 | } 320 | } 321 | ``` 322 | 323 | --- 324 | 325 | # Functional Task? 326 | 327 | Monads? Applicatives? Huh?! 328 | 329 | --- 330 | 331 | # Blog: Methods of Concurrency 332 | #   333 | # [Fit] http://bit.do/concurrency 334 | 335 | http://overooped.com/post/41803252527/methods-of-concurrency 336 | 337 | --- 338 | 339 | 340 | ![150% original](http://promisekit.org/public/img/tight-header.png) 341 | 342 | --- 343 | 344 | 345 | # [fit] Thank you 346 | 347 | # @nevyn 348 | ## nevyn@lookback.io 349 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPAsync 2 | 3 | by Nevyn Joachim Bengtsson 4 | 5 | Tools for abstracting asynchrony in Objective-C. Read [the introductory blog post](http://overooped.com/post/41803252527/methods-of-concurrency) for much more detail. SPTask is the most interesting tool in here. 6 | 7 | ## SPTask 8 | 9 | Wraps any asynchronous operation or value that someone might want to know the result of, or get the value of, in the future. 10 | 11 | You can use SPTask in any place where you'd traditionally use a callback. 12 | 13 | Instead of doing a Pyramid Of Doom like this: 14 | 15 | [thing fetchNetworkThingie:url callback:^(NSData *data) { 16 | [AsyncJsonParser parse:data callback:^(NSDictionary *parsed) { 17 | [_database updateWithData:parsed callback:^(NSError *err) { 18 | if(err) 19 | ... and it just goes on... 20 | }]; 21 | // don't forget error handling here 22 | }]; 23 | // don't forget error handling here too 24 | }]; 25 | 26 | you can get a nice chain of things like this: 27 | 28 | [[[[[thing fetchNetworkThingie:url] chain:^(NSData *data) { 29 | return [AsyncJsonParser parse:data]; 30 | }] chain:^(NSDictionary *parsed) { 31 | return [_database updateWithData:data]; 32 | }] addCallback:^{ 33 | NSLog(@"Yay!"); 34 | }] addErrorCallback:^(NSError *error) { 35 | NSLog(@"An error caught anywhere along the line can be handled here in this one place: %@", error); 36 | }]; 37 | 38 | That's nicer, yeah? 39 | 40 | By using task trees like this, you can make your interfaces prettier, make cancellation easier, centralize your 41 | error handling, make it easier to work with dispatch_queues, and so on. 42 | 43 | 44 | SPTask is basically a copy of System.Threading.Tasks.Task in .Net. It's a standard library class representing any asynchronous operation that yields a single value. This is deceivingly simple, and gives you much more power of abstraction than you would initially believe. You can think of it as an extremely lightweight ReactiveCocoa, in a single file. 45 | 46 | 47 | ## SPAgent 48 | 49 | An experimental multithreading primitive. The goal is to make every "manager" style class multithreaded with its own work queue, and then make communication between these agents trivial. 50 | 51 | ## SPAwait 52 | 53 | Experimental copy of C# 5's "await" keyword, using the preprocessor. 54 | 55 | - (SPTask *)uploadThing:(NSData*)thing 56 | { 57 | // Variables you want to use need to be declared as __block at the top of the method. 58 | __block NSData *encrypted, *confirmation; 59 | 60 | SPAsyncMethodBegin // Start of an async method, similar to 'async' keyword in C# 61 | 62 | // Do work like normal 63 | [self prepareFor:thing]; 64 | 65 | // When you make a call to something returning an SPTask, you can wait for its value. The method 66 | // will actually return at this point, and resume on the next line when the encrypted value is available. 67 | encrypted = SPAsyncAwait([_encryptor encrypt:thing]); 68 | 69 | [_network send:encrypted]; 70 | 71 | confirmation = SPAsyncAwait([_network read:1]); 72 | 73 | // Returning will complete the SPTask, sending this value to all the callbacks registered with it 74 | return @([confirmation bytes][0] == 0); 75 | 76 | SPAsyncMethodEnd 77 | } 78 | 79 | 80 | Extensions 81 | ---------- 82 | 83 | In the Extensions folder you'll find extensions to other libraries, making them compatible with SPTask in various ways. You'll have to compile these in on your own when you need them; otherwise they would become dependencies for this library. -------------------------------------------------------------------------------- /SPAsync (Mac)/SPAsync__Mac_.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPAsync__Mac_.h 3 | // SPAsync (Mac) 4 | // 5 | // Created by Joachim Bengtsson on 2014-08-31. 6 | // Copyright (c) 2014 ThirdCog. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SPAsync__Mac_ : NSObject 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /SPAsync (Mac)/SPAsync__Mac_.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPAsync__Mac_.m 3 | // SPAsync (Mac) 4 | // 5 | // Created by Joachim Bengtsson on 2014-08-31. 6 | // Copyright (c) 2014 ThirdCog. All rights reserved. 7 | // 8 | 9 | #import "SPAsync__Mac_.h" 10 | 11 | @implementation SPAsync__Mac_ 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /SPAsync (Mac)Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | eu.thirdcog.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SPAsync.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "SPAsync" 4 | s.version = "0.1.0" 5 | s.summary = "Tools for abstracting asynchrony in Objective-C." 6 | 7 | s.description = <<-DESC 8 | SPAsync 9 | ======= 10 | by Joachim Bengtsson 11 | 12 | Tools for abstracting asynchrony in Objective-C. Read [the introductory blog entry](http://overooped.com/post/41803252527/methods-of-concurrency) for much more detail. 13 | DESC 14 | 15 | s.homepage = "https://github.com/nevyn/SPAsync" 16 | 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | 19 | s.author = { "Joachim Bengtsson" => "joachimb@gmail.com" } 20 | 21 | s.source = { :git => "https://github.com/nevyn/SPAsync.git", :commit => "bd5730018a606f4ff91c964679eb1b7baba06f7d" } 22 | 23 | s.source_files = 'Sources', 'Sources/**/*.{h,m}', 'include/**/*.h' 24 | s.public_header_files = 'include/SPAsync/**/*.h' 25 | 26 | s.ios.deployment_target = '5.0' 27 | 28 | s.requires_arc = true 29 | 30 | end 31 | -------------------------------------------------------------------------------- /SPAsync.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0519E16419C8D0AB00DF8974 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0519E16319C8D0AB00DF8974 /* XCTest.framework */; }; 11 | 0519EFE01816CCC100CFDCA4 /* SPAsyncNamespacing.h in Headers */ = {isa = PBXBuildFile; fileRef = 0519EFDF1816CCC100CFDCA4 /* SPAsyncNamespacing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 054B255719B3300B00F61F64 /* libSPAsync.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05B647DA16B85AF90050002D /* libSPAsync.a */; }; 13 | 054B258819B3350A00F61F64 /* SPAgentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6480A16B85BB60050002D /* SPAgentTest.m */; }; 14 | 054B258919B3350A00F61F64 /* SPTaskTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6480C16B85BB60050002D /* SPTaskTest.m */; }; 15 | 054B258A19B3350A00F61F64 /* SPAwaitTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6484316B94C010050002D /* SPAwaitTest.m */; }; 16 | 054B258B19B3356300F61F64 /* NSObject+SPInvocationGrabbing.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481516B85BC20050002D /* NSObject+SPInvocationGrabbing.m */; }; 17 | 054B258C19B3356300F61F64 /* SPAgent.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481616B85BC20050002D /* SPAgent.m */; }; 18 | 054B258D19B3356300F61F64 /* SPTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481816B85BC20050002D /* SPTask.m */; }; 19 | 054B258E19B3356300F61F64 /* SPAwait.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6484016B93F2A0050002D /* SPAwait.m */; }; 20 | 058DBD1C199D43E8008F7741 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058DBD1B199D43E8008F7741 /* main.swift */; }; 21 | 058DBD21199D4424008F7741 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058DBD20199D4424008F7741 /* Task.swift */; }; 22 | 05B647DE16B85AF90050002D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05B647DD16B85AF90050002D /* Foundation.framework */; }; 23 | 05B6481916B85BC20050002D /* NSObject+SPInvocationGrabbing.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481516B85BC20050002D /* NSObject+SPInvocationGrabbing.m */; }; 24 | 05B6481A16B85BC20050002D /* SPAgent.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481616B85BC20050002D /* SPAgent.m */; }; 25 | 05B6481C16B85BC20050002D /* SPTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481816B85BC20050002D /* SPTask.m */; }; 26 | 05B6484116B93F2A0050002D /* SPAwait.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6484016B93F2A0050002D /* SPAwait.m */; }; 27 | 05BBF3B216EF2EA30011E948 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05BBF3B116EF2EA30011E948 /* Cocoa.framework */; }; 28 | 05BBF3DD16EF30100011E948 /* NSObject+SPInvocationGrabbing.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481516B85BC20050002D /* NSObject+SPInvocationGrabbing.m */; }; 29 | 05BBF3DE16EF30100011E948 /* SPAgent.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481616B85BC20050002D /* SPAgent.m */; }; 30 | 05BBF3DF16EF30100011E948 /* SPTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6481816B85BC20050002D /* SPTask.m */; }; 31 | 05BBF3E016EF30100011E948 /* SPAwait.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B6484016B93F2A0050002D /* SPAwait.m */; }; 32 | 05BBF3E116EF301C0011E948 /* SPAsync.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B6481216B85BC20050002D /* SPAsync.h */; settings = {ATTRIBUTES = (Public, ); }; }; 33 | 05BBF3E216EF301C0011E948 /* NSObject+SPInvocationGrabbing.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B6481016B85BC20050002D /* NSObject+SPInvocationGrabbing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34 | 05BBF3E316EF301C0011E948 /* SPAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B6481116B85BC20050002D /* SPAgent.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35 | 05BBF3E416EF301C0011E948 /* SPTask.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B6481316B85BC20050002D /* SPTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; 36 | 05BBF3E516EF301C0011E948 /* SPAwait.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B6483F16B93F200050002D /* SPAwait.h */; settings = {ATTRIBUTES = (Public, ); }; }; 37 | 05C3925219C8CFFB003B862A /* SPAsync.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05BBF3B016EF2EA30011E948 /* SPAsync.framework */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXCopyFilesBuildPhase section */ 41 | 058DBD17199D43E8008F7741 /* CopyFiles */ = { 42 | isa = PBXCopyFilesBuildPhase; 43 | buildActionMask = 2147483647; 44 | dstPath = /usr/share/man/man1/; 45 | dstSubfolderSpec = 0; 46 | files = ( 47 | ); 48 | runOnlyForDeploymentPostprocessing = 1; 49 | }; 50 | 05B647D816B85AF90050002D /* CopyFiles */ = { 51 | isa = PBXCopyFilesBuildPhase; 52 | buildActionMask = 2147483647; 53 | dstPath = "include/${PRODUCT_NAME}"; 54 | dstSubfolderSpec = 16; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXCopyFilesBuildPhase section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 0519E16319C8D0AB00DF8974 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 63 | 0519EFDF1816CCC100CFDCA4 /* SPAsyncNamespacing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAsyncNamespacing.h; sourceTree = ""; }; 64 | 054B257419B3346800F61F64 /* libSPAsync.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPAsync.a; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 054B257E19B3346800F61F64 /* SPAsyncTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPAsyncTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 057E7ECB182E8302009D0318 /* UIScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIScrollView.h; sourceTree = ""; }; 67 | 057E7ECC182E830C009D0318 /* UIScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIScrollView.m; sourceTree = ""; }; 68 | 057E7ED3182E8DE8009D0318 /* UIKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIKit.h; sourceTree = ""; }; 69 | 058DBD19199D43E8008F7741 /* SwiftDemo */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SwiftDemo; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 058DBD1B199D43E8008F7741 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 71 | 058DBD20199D4424008F7741 /* Task.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; 72 | 058DBD22199D443F008F7741 /* Swift-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Swift-Bridging-Header.h"; sourceTree = ""; }; 73 | 05B16D20182D9CAC00025797 /* UIView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIView.h; sourceTree = ""; }; 74 | 05B16D23182D9D9500025797 /* UIView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIView.m; sourceTree = ""; }; 75 | 05B647DA16B85AF90050002D /* libSPAsync.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPAsync.a; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 05B647DD16B85AF90050002D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 77 | 05B647E116B85AF90050002D /* SPAsync-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SPAsync-Prefix.pch"; path = "Support/SPAsync-Prefix.pch"; sourceTree = ""; }; 78 | 05B647EC16B85AF90050002D /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; 79 | 05B647EE16B85AF90050002D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 80 | 05B647F616B85AF90050002D /* SPAsyncTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SPAsyncTests-Info.plist"; sourceTree = ""; }; 81 | 05B647F816B85AF90050002D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 82 | 05B6480916B85BB60050002D /* SPAgentTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAgentTest.h; sourceTree = ""; }; 83 | 05B6480A16B85BB60050002D /* SPAgentTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPAgentTest.m; sourceTree = ""; }; 84 | 05B6480B16B85BB60050002D /* SPTaskTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTaskTest.h; sourceTree = ""; }; 85 | 05B6480C16B85BB60050002D /* SPTaskTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTaskTest.m; sourceTree = ""; }; 86 | 05B6481016B85BC20050002D /* NSObject+SPInvocationGrabbing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+SPInvocationGrabbing.h"; sourceTree = ""; }; 87 | 05B6481116B85BC20050002D /* SPAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAgent.h; sourceTree = ""; }; 88 | 05B6481216B85BC20050002D /* SPAsync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAsync.h; sourceTree = ""; }; 89 | 05B6481316B85BC20050002D /* SPTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTask.h; sourceTree = ""; }; 90 | 05B6481516B85BC20050002D /* NSObject+SPInvocationGrabbing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+SPInvocationGrabbing.m"; sourceTree = ""; }; 91 | 05B6481616B85BC20050002D /* SPAgent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPAgent.m; sourceTree = ""; }; 92 | 05B6481816B85BC20050002D /* SPTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTask.m; sourceTree = ""; }; 93 | 05B6483F16B93F200050002D /* SPAwait.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAwait.h; sourceTree = ""; }; 94 | 05B6484016B93F2A0050002D /* SPAwait.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPAwait.m; sourceTree = ""; }; 95 | 05B6484216B94C010050002D /* SPAwaitTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPAwaitTest.h; sourceTree = ""; }; 96 | 05B6484316B94C010050002D /* SPAwaitTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPAwaitTest.m; sourceTree = ""; }; 97 | 05BBF3B016EF2EA30011E948 /* SPAsync.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SPAsync.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 98 | 05BBF3B116EF2EA30011E948 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; 99 | 05BBF3B416EF2EA30011E948 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 100 | 05BBF3B516EF2EA30011E948 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 101 | 05BBF3B616EF2EA30011E948 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 102 | 05BBF3DB16EF2F780011E948 /* SPAsync-Framework-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "SPAsync-Framework-Info.plist"; path = "Support/SPAsync-Framework-Info.plist"; sourceTree = SOURCE_ROOT; }; 103 | /* End PBXFileReference section */ 104 | 105 | /* Begin PBXFrameworksBuildPhase section */ 106 | 054B257119B3346800F61F64 /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | 054B257B19B3346800F61F64 /* Frameworks */ = { 114 | isa = PBXFrameworksBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | 0519E16419C8D0AB00DF8974 /* XCTest.framework in Frameworks */, 118 | 05C3925219C8CFFB003B862A /* SPAsync.framework in Frameworks */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | 058DBD16199D43E8008F7741 /* Frameworks */ = { 123 | isa = PBXFrameworksBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | 054B255719B3300B00F61F64 /* libSPAsync.a in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | 05B647D716B85AF90050002D /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | 05B647DE16B85AF90050002D /* Foundation.framework in Frameworks */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | 05BBF3AC16EF2EA30011E948 /* Frameworks */ = { 139 | isa = PBXFrameworksBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 05BBF3B216EF2EA30011E948 /* Cocoa.framework in Frameworks */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXFrameworksBuildPhase section */ 147 | 148 | /* Begin PBXGroup section */ 149 | 058DBD1A199D43E8008F7741 /* Swift */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 058DBD1B199D43E8008F7741 /* main.swift */, 153 | 058DBD20199D4424008F7741 /* Task.swift */, 154 | 058DBD22199D443F008F7741 /* Swift-Bridging-Header.h */, 155 | ); 156 | path = Swift; 157 | sourceTree = SOURCE_ROOT; 158 | }; 159 | 05B16D1F182D9CAC00025797 /* UIKit */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 057E7ED3182E8DE8009D0318 /* UIKit.h */, 163 | 05B16D20182D9CAC00025797 /* UIView.h */, 164 | 057E7ECB182E8302009D0318 /* UIScrollView.h */, 165 | ); 166 | path = UIKit; 167 | sourceTree = ""; 168 | }; 169 | 05B16D21182D9D8A00025797 /* Extensions */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 05B16D22182D9D8A00025797 /* UIKit */, 173 | ); 174 | path = Extensions; 175 | sourceTree = SOURCE_ROOT; 176 | }; 177 | 05B16D22182D9D8A00025797 /* UIKit */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 05B16D23182D9D9500025797 /* UIView.m */, 181 | 057E7ECC182E830C009D0318 /* UIScrollView.m */, 182 | ); 183 | path = UIKit; 184 | sourceTree = ""; 185 | }; 186 | 05B647CF16B85AF90050002D = { 187 | isa = PBXGroup; 188 | children = ( 189 | 05B647DF16B85AF90050002D /* SPAsync */, 190 | 05B647F416B85AF90050002D /* SPAsyncTests */, 191 | 05B647DC16B85AF90050002D /* Frameworks */, 192 | 05B647DB16B85AF90050002D /* Products */, 193 | ); 194 | sourceTree = ""; 195 | }; 196 | 05B647DB16B85AF90050002D /* Products */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 05B647DA16B85AF90050002D /* libSPAsync.a */, 200 | 05BBF3B016EF2EA30011E948 /* SPAsync.framework */, 201 | 058DBD19199D43E8008F7741 /* SwiftDemo */, 202 | 054B257419B3346800F61F64 /* libSPAsync.a */, 203 | 054B257E19B3346800F61F64 /* SPAsyncTests.xctest */, 204 | ); 205 | name = Products; 206 | sourceTree = ""; 207 | }; 208 | 05B647DC16B85AF90050002D /* Frameworks */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | 0519E16319C8D0AB00DF8974 /* XCTest.framework */, 212 | 05B647DD16B85AF90050002D /* Foundation.framework */, 213 | 05B647EC16B85AF90050002D /* SenTestingKit.framework */, 214 | 05B647EE16B85AF90050002D /* UIKit.framework */, 215 | 05BBF3B116EF2EA30011E948 /* Cocoa.framework */, 216 | 05BBF3B316EF2EA30011E948 /* Other Frameworks */, 217 | ); 218 | name = Frameworks; 219 | sourceTree = ""; 220 | }; 221 | 05B647DF16B85AF90050002D /* SPAsync */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 05B6480F16B85BC20050002D /* Interfaces */, 225 | 05B6481416B85BC20050002D /* Sources */, 226 | 05B16D21182D9D8A00025797 /* Extensions */, 227 | 058DBD1A199D43E8008F7741 /* Swift */, 228 | 05B647E016B85AF90050002D /* Supporting Files */, 229 | ); 230 | path = SPAsync; 231 | sourceTree = ""; 232 | }; 233 | 05B647E016B85AF90050002D /* Supporting Files */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 05BBF3DB16EF2F780011E948 /* SPAsync-Framework-Info.plist */, 237 | 05B647E116B85AF90050002D /* SPAsync-Prefix.pch */, 238 | ); 239 | name = "Supporting Files"; 240 | sourceTree = ""; 241 | }; 242 | 05B647F416B85AF90050002D /* SPAsyncTests */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 05B6480916B85BB60050002D /* SPAgentTest.h */, 246 | 05B6480A16B85BB60050002D /* SPAgentTest.m */, 247 | 05B6480B16B85BB60050002D /* SPTaskTest.h */, 248 | 05B6480C16B85BB60050002D /* SPTaskTest.m */, 249 | 05B6484216B94C010050002D /* SPAwaitTest.h */, 250 | 05B6484316B94C010050002D /* SPAwaitTest.m */, 251 | 05B647F516B85AF90050002D /* Supporting Files */, 252 | ); 253 | path = SPAsyncTests; 254 | sourceTree = ""; 255 | }; 256 | 05B647F516B85AF90050002D /* Supporting Files */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | 05B647F616B85AF90050002D /* SPAsyncTests-Info.plist */, 260 | 05B647F716B85AF90050002D /* InfoPlist.strings */, 261 | ); 262 | name = "Supporting Files"; 263 | sourceTree = ""; 264 | }; 265 | 05B6480F16B85BC20050002D /* Interfaces */ = { 266 | isa = PBXGroup; 267 | children = ( 268 | 05B6481216B85BC20050002D /* SPAsync.h */, 269 | 05B6481016B85BC20050002D /* NSObject+SPInvocationGrabbing.h */, 270 | 05B6481116B85BC20050002D /* SPAgent.h */, 271 | 05B6481316B85BC20050002D /* SPTask.h */, 272 | 05B6483F16B93F200050002D /* SPAwait.h */, 273 | 0519EFDF1816CCC100CFDCA4 /* SPAsyncNamespacing.h */, 274 | 05B16D1F182D9CAC00025797 /* UIKit */, 275 | ); 276 | name = Interfaces; 277 | path = include/SPAsync; 278 | sourceTree = SOURCE_ROOT; 279 | }; 280 | 05B6481416B85BC20050002D /* Sources */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | 05B6481516B85BC20050002D /* NSObject+SPInvocationGrabbing.m */, 284 | 05B6481616B85BC20050002D /* SPAgent.m */, 285 | 05B6481816B85BC20050002D /* SPTask.m */, 286 | 05B6484016B93F2A0050002D /* SPAwait.m */, 287 | ); 288 | path = Sources; 289 | sourceTree = SOURCE_ROOT; 290 | }; 291 | 05BBF3B316EF2EA30011E948 /* Other Frameworks */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 05BBF3B416EF2EA30011E948 /* AppKit.framework */, 295 | 05BBF3B516EF2EA30011E948 /* CoreData.framework */, 296 | 05BBF3B616EF2EA30011E948 /* Foundation.framework */, 297 | ); 298 | name = "Other Frameworks"; 299 | sourceTree = ""; 300 | }; 301 | /* End PBXGroup section */ 302 | 303 | /* Begin PBXHeadersBuildPhase section */ 304 | 054B257219B3346800F61F64 /* Headers */ = { 305 | isa = PBXHeadersBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | 05BBF3AD16EF2EA30011E948 /* Headers */ = { 312 | isa = PBXHeadersBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | 05BBF3E116EF301C0011E948 /* SPAsync.h in Headers */, 316 | 05BBF3E216EF301C0011E948 /* NSObject+SPInvocationGrabbing.h in Headers */, 317 | 05BBF3E316EF301C0011E948 /* SPAgent.h in Headers */, 318 | 0519EFE01816CCC100CFDCA4 /* SPAsyncNamespacing.h in Headers */, 319 | 05BBF3E416EF301C0011E948 /* SPTask.h in Headers */, 320 | 05BBF3E516EF301C0011E948 /* SPAwait.h in Headers */, 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXHeadersBuildPhase section */ 325 | 326 | /* Begin PBXNativeTarget section */ 327 | 054B257319B3346800F61F64 /* SPAsync (Mac) */ = { 328 | isa = PBXNativeTarget; 329 | buildConfigurationList = 054B258219B3346900F61F64 /* Build configuration list for PBXNativeTarget "SPAsync (Mac)" */; 330 | buildPhases = ( 331 | 054B257019B3346800F61F64 /* Sources */, 332 | 054B257119B3346800F61F64 /* Frameworks */, 333 | 054B257219B3346800F61F64 /* Headers */, 334 | ); 335 | buildRules = ( 336 | ); 337 | dependencies = ( 338 | ); 339 | name = "SPAsync (Mac)"; 340 | productName = "SPAsync (Mac)"; 341 | productReference = 054B257419B3346800F61F64 /* libSPAsync.a */; 342 | productType = "com.apple.product-type.library.static"; 343 | }; 344 | 054B257D19B3346800F61F64 /* SPAsyncTests */ = { 345 | isa = PBXNativeTarget; 346 | buildConfigurationList = 054B258519B3346900F61F64 /* Build configuration list for PBXNativeTarget "SPAsyncTests" */; 347 | buildPhases = ( 348 | 054B257A19B3346800F61F64 /* Sources */, 349 | 054B257B19B3346800F61F64 /* Frameworks */, 350 | 054B257C19B3346800F61F64 /* Resources */, 351 | ); 352 | buildRules = ( 353 | ); 354 | dependencies = ( 355 | ); 356 | name = SPAsyncTests; 357 | productName = "SPAsync (Mac)Tests"; 358 | productReference = 054B257E19B3346800F61F64 /* SPAsyncTests.xctest */; 359 | productType = "com.apple.product-type.bundle.unit-test"; 360 | }; 361 | 058DBD18199D43E8008F7741 /* SwiftDemo */ = { 362 | isa = PBXNativeTarget; 363 | buildConfigurationList = 058DBD1D199D43E8008F7741 /* Build configuration list for PBXNativeTarget "SwiftDemo" */; 364 | buildPhases = ( 365 | 058DBD15199D43E8008F7741 /* Sources */, 366 | 058DBD16199D43E8008F7741 /* Frameworks */, 367 | 058DBD17199D43E8008F7741 /* CopyFiles */, 368 | ); 369 | buildRules = ( 370 | ); 371 | dependencies = ( 372 | ); 373 | name = SwiftDemo; 374 | productName = Swift; 375 | productReference = 058DBD19199D43E8008F7741 /* SwiftDemo */; 376 | productType = "com.apple.product-type.tool"; 377 | }; 378 | 05B647D916B85AF90050002D /* SPAsync (iOS) */ = { 379 | isa = PBXNativeTarget; 380 | buildConfigurationList = 05B647FF16B85AF90050002D /* Build configuration list for PBXNativeTarget "SPAsync (iOS)" */; 381 | buildPhases = ( 382 | 05B647D616B85AF90050002D /* Sources */, 383 | 05B647D716B85AF90050002D /* Frameworks */, 384 | 05B647D816B85AF90050002D /* CopyFiles */, 385 | ); 386 | buildRules = ( 387 | ); 388 | dependencies = ( 389 | ); 390 | name = "SPAsync (iOS)"; 391 | productName = SPAsync; 392 | productReference = 05B647DA16B85AF90050002D /* libSPAsync.a */; 393 | productType = "com.apple.product-type.library.static"; 394 | }; 395 | 05BBF3AF16EF2EA30011E948 /* SPAsync Framework */ = { 396 | isa = PBXNativeTarget; 397 | buildConfigurationList = 05BBF3D916EF2EA30011E948 /* Build configuration list for PBXNativeTarget "SPAsync Framework" */; 398 | buildPhases = ( 399 | 05BBF3AB16EF2EA30011E948 /* Sources */, 400 | 05BBF3AC16EF2EA30011E948 /* Frameworks */, 401 | 05BBF3AD16EF2EA30011E948 /* Headers */, 402 | 05BBF3AE16EF2EA30011E948 /* Resources */, 403 | ); 404 | buildRules = ( 405 | ); 406 | dependencies = ( 407 | ); 408 | name = "SPAsync Framework"; 409 | productName = SPAsync_; 410 | productReference = 05BBF3B016EF2EA30011E948 /* SPAsync.framework */; 411 | productType = "com.apple.product-type.framework"; 412 | }; 413 | /* End PBXNativeTarget section */ 414 | 415 | /* Begin PBXProject section */ 416 | 05B647D116B85AF90050002D /* Project object */ = { 417 | isa = PBXProject; 418 | attributes = { 419 | LastSwiftUpdateCheck = 0730; 420 | LastUpgradeCheck = 1000; 421 | ORGANIZATIONNAME = ThirdCog; 422 | TargetAttributes = { 423 | 054B257319B3346800F61F64 = { 424 | CreatedOnToolsVersion = 6.0; 425 | }; 426 | 054B257D19B3346800F61F64 = { 427 | CreatedOnToolsVersion = 6.0; 428 | }; 429 | 058DBD18199D43E8008F7741 = { 430 | CreatedOnToolsVersion = 6.0; 431 | }; 432 | }; 433 | }; 434 | buildConfigurationList = 05B647D416B85AF90050002D /* Build configuration list for PBXProject "SPAsync" */; 435 | compatibilityVersion = "Xcode 3.2"; 436 | developmentRegion = English; 437 | hasScannedForEncodings = 0; 438 | knownRegions = ( 439 | en, 440 | ); 441 | mainGroup = 05B647CF16B85AF90050002D; 442 | productRefGroup = 05B647DB16B85AF90050002D /* Products */; 443 | projectDirPath = ""; 444 | projectRoot = ""; 445 | targets = ( 446 | 05B647D916B85AF90050002D /* SPAsync (iOS) */, 447 | 05BBF3AF16EF2EA30011E948 /* SPAsync Framework */, 448 | 058DBD18199D43E8008F7741 /* SwiftDemo */, 449 | 054B257319B3346800F61F64 /* SPAsync (Mac) */, 450 | 054B257D19B3346800F61F64 /* SPAsyncTests */, 451 | ); 452 | }; 453 | /* End PBXProject section */ 454 | 455 | /* Begin PBXResourcesBuildPhase section */ 456 | 054B257C19B3346800F61F64 /* Resources */ = { 457 | isa = PBXResourcesBuildPhase; 458 | buildActionMask = 2147483647; 459 | files = ( 460 | ); 461 | runOnlyForDeploymentPostprocessing = 0; 462 | }; 463 | 05BBF3AE16EF2EA30011E948 /* Resources */ = { 464 | isa = PBXResourcesBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | ); 468 | runOnlyForDeploymentPostprocessing = 0; 469 | }; 470 | /* End PBXResourcesBuildPhase section */ 471 | 472 | /* Begin PBXSourcesBuildPhase section */ 473 | 054B257019B3346800F61F64 /* Sources */ = { 474 | isa = PBXSourcesBuildPhase; 475 | buildActionMask = 2147483647; 476 | files = ( 477 | 054B258B19B3356300F61F64 /* NSObject+SPInvocationGrabbing.m in Sources */, 478 | 054B258C19B3356300F61F64 /* SPAgent.m in Sources */, 479 | 054B258D19B3356300F61F64 /* SPTask.m in Sources */, 480 | 054B258E19B3356300F61F64 /* SPAwait.m in Sources */, 481 | ); 482 | runOnlyForDeploymentPostprocessing = 0; 483 | }; 484 | 054B257A19B3346800F61F64 /* Sources */ = { 485 | isa = PBXSourcesBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | 054B258819B3350A00F61F64 /* SPAgentTest.m in Sources */, 489 | 054B258919B3350A00F61F64 /* SPTaskTest.m in Sources */, 490 | 054B258A19B3350A00F61F64 /* SPAwaitTest.m in Sources */, 491 | ); 492 | runOnlyForDeploymentPostprocessing = 0; 493 | }; 494 | 058DBD15199D43E8008F7741 /* Sources */ = { 495 | isa = PBXSourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | 058DBD21199D4424008F7741 /* Task.swift in Sources */, 499 | 058DBD1C199D43E8008F7741 /* main.swift in Sources */, 500 | ); 501 | runOnlyForDeploymentPostprocessing = 0; 502 | }; 503 | 05B647D616B85AF90050002D /* Sources */ = { 504 | isa = PBXSourcesBuildPhase; 505 | buildActionMask = 2147483647; 506 | files = ( 507 | 05B6481916B85BC20050002D /* NSObject+SPInvocationGrabbing.m in Sources */, 508 | 05B6481A16B85BC20050002D /* SPAgent.m in Sources */, 509 | 05B6481C16B85BC20050002D /* SPTask.m in Sources */, 510 | 05B6484116B93F2A0050002D /* SPAwait.m in Sources */, 511 | ); 512 | runOnlyForDeploymentPostprocessing = 0; 513 | }; 514 | 05BBF3AB16EF2EA30011E948 /* Sources */ = { 515 | isa = PBXSourcesBuildPhase; 516 | buildActionMask = 2147483647; 517 | files = ( 518 | 05BBF3DD16EF30100011E948 /* NSObject+SPInvocationGrabbing.m in Sources */, 519 | 05BBF3DE16EF30100011E948 /* SPAgent.m in Sources */, 520 | 05BBF3DF16EF30100011E948 /* SPTask.m in Sources */, 521 | 05BBF3E016EF30100011E948 /* SPAwait.m in Sources */, 522 | ); 523 | runOnlyForDeploymentPostprocessing = 0; 524 | }; 525 | /* End PBXSourcesBuildPhase section */ 526 | 527 | /* Begin PBXVariantGroup section */ 528 | 05B647F716B85AF90050002D /* InfoPlist.strings */ = { 529 | isa = PBXVariantGroup; 530 | children = ( 531 | 05B647F816B85AF90050002D /* en */, 532 | ); 533 | name = InfoPlist.strings; 534 | sourceTree = ""; 535 | }; 536 | /* End PBXVariantGroup section */ 537 | 538 | /* Begin XCBuildConfiguration section */ 539 | 054B258319B3346900F61F64 /* Debug */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | CLANG_ENABLE_MODULES = YES; 543 | CLANG_WARN_BOOL_CONVERSION = YES; 544 | CLANG_WARN_CONSTANT_CONVERSION = YES; 545 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 546 | CLANG_WARN_ENUM_CONVERSION = YES; 547 | CLANG_WARN_INT_CONVERSION = YES; 548 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 549 | CLANG_WARN_UNREACHABLE_CODE = YES; 550 | ENABLE_STRICT_OBJC_MSGSEND = YES; 551 | EXECUTABLE_PREFIX = lib; 552 | GCC_PREPROCESSOR_DEFINITIONS = ( 553 | "DEBUG=1", 554 | "$(inherited)", 555 | ); 556 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 557 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 558 | GCC_WARN_UNDECLARED_SELECTOR = YES; 559 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 560 | GCC_WARN_UNUSED_FUNCTION = YES; 561 | MTL_ENABLE_DEBUG_INFO = YES; 562 | ONLY_ACTIVE_ARCH = YES; 563 | PRODUCT_MODULE_NAME = SPAsync; 564 | PRODUCT_NAME = SPAsync; 565 | SDKROOT = macosx; 566 | }; 567 | name = Debug; 568 | }; 569 | 054B258419B3346900F61F64 /* Release */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | CLANG_ENABLE_MODULES = YES; 573 | CLANG_WARN_BOOL_CONVERSION = YES; 574 | CLANG_WARN_CONSTANT_CONVERSION = YES; 575 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 576 | CLANG_WARN_ENUM_CONVERSION = YES; 577 | CLANG_WARN_INT_CONVERSION = YES; 578 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 579 | CLANG_WARN_UNREACHABLE_CODE = YES; 580 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 581 | ENABLE_NS_ASSERTIONS = NO; 582 | ENABLE_STRICT_OBJC_MSGSEND = YES; 583 | EXECUTABLE_PREFIX = lib; 584 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 585 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 586 | GCC_WARN_UNDECLARED_SELECTOR = YES; 587 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 588 | GCC_WARN_UNUSED_FUNCTION = YES; 589 | MTL_ENABLE_DEBUG_INFO = NO; 590 | PRODUCT_MODULE_NAME = SPAsync; 591 | PRODUCT_NAME = SPAsync; 592 | SDKROOT = macosx; 593 | }; 594 | name = Release; 595 | }; 596 | 054B258619B3346900F61F64 /* Debug */ = { 597 | isa = XCBuildConfiguration; 598 | buildSettings = { 599 | CLANG_ENABLE_MODULES = YES; 600 | CLANG_WARN_BOOL_CONVERSION = YES; 601 | CLANG_WARN_CONSTANT_CONVERSION = YES; 602 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 603 | CLANG_WARN_ENUM_CONVERSION = YES; 604 | CLANG_WARN_INT_CONVERSION = YES; 605 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 606 | CLANG_WARN_UNREACHABLE_CODE = YES; 607 | COMBINE_HIDPI_IMAGES = YES; 608 | ENABLE_STRICT_OBJC_MSGSEND = YES; 609 | FRAMEWORK_SEARCH_PATHS = ( 610 | "$(DEVELOPER_FRAMEWORKS_DIR)", 611 | "$(inherited)", 612 | ); 613 | GCC_PREPROCESSOR_DEFINITIONS = ( 614 | "DEBUG=1", 615 | "$(inherited)", 616 | ); 617 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 618 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 619 | GCC_WARN_UNDECLARED_SELECTOR = YES; 620 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 621 | GCC_WARN_UNUSED_FUNCTION = YES; 622 | INFOPLIST_FILE = "SPAsync (Mac)Tests/Info.plist"; 623 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 624 | MTL_ENABLE_DEBUG_INFO = YES; 625 | ONLY_ACTIVE_ARCH = YES; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SDKROOT = macosx; 628 | }; 629 | name = Debug; 630 | }; 631 | 054B258719B3346900F61F64 /* Release */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | CLANG_ENABLE_MODULES = YES; 635 | CLANG_WARN_BOOL_CONVERSION = YES; 636 | CLANG_WARN_CONSTANT_CONVERSION = YES; 637 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 638 | CLANG_WARN_ENUM_CONVERSION = YES; 639 | CLANG_WARN_INT_CONVERSION = YES; 640 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 641 | CLANG_WARN_UNREACHABLE_CODE = YES; 642 | COMBINE_HIDPI_IMAGES = YES; 643 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 644 | ENABLE_NS_ASSERTIONS = NO; 645 | ENABLE_STRICT_OBJC_MSGSEND = YES; 646 | FRAMEWORK_SEARCH_PATHS = ( 647 | "$(DEVELOPER_FRAMEWORKS_DIR)", 648 | "$(inherited)", 649 | ); 650 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 651 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 652 | GCC_WARN_UNDECLARED_SELECTOR = YES; 653 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 654 | GCC_WARN_UNUSED_FUNCTION = YES; 655 | INFOPLIST_FILE = "SPAsync (Mac)Tests/Info.plist"; 656 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 657 | MTL_ENABLE_DEBUG_INFO = NO; 658 | PRODUCT_NAME = "$(TARGET_NAME)"; 659 | SDKROOT = macosx; 660 | }; 661 | name = Release; 662 | }; 663 | 058DBD1E199D43E8008F7741 /* Debug */ = { 664 | isa = XCBuildConfiguration; 665 | buildSettings = { 666 | CLANG_ENABLE_MODULES = YES; 667 | CLANG_WARN_BOOL_CONVERSION = YES; 668 | CLANG_WARN_CONSTANT_CONVERSION = YES; 669 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 670 | CLANG_WARN_ENUM_CONVERSION = YES; 671 | CLANG_WARN_INT_CONVERSION = YES; 672 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 673 | CLANG_WARN_UNREACHABLE_CODE = YES; 674 | ENABLE_STRICT_OBJC_MSGSEND = YES; 675 | GCC_PREPROCESSOR_DEFINITIONS = ( 676 | "DEBUG=1", 677 | "$(inherited)", 678 | ); 679 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 680 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 681 | GCC_WARN_UNDECLARED_SELECTOR = YES; 682 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 683 | GCC_WARN_UNUSED_FUNCTION = YES; 684 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 685 | MACOSX_DEPLOYMENT_TARGET = 10.10; 686 | MTL_ENABLE_DEBUG_INFO = YES; 687 | ONLY_ACTIVE_ARCH = YES; 688 | PRODUCT_NAME = "$(TARGET_NAME)"; 689 | SDKROOT = macosx; 690 | SWIFT_OBJC_BRIDGING_HEADER = "Swift/Swift-Bridging-Header.h"; 691 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 692 | }; 693 | name = Debug; 694 | }; 695 | 058DBD1F199D43E8008F7741 /* Release */ = { 696 | isa = XCBuildConfiguration; 697 | buildSettings = { 698 | CLANG_ENABLE_MODULES = YES; 699 | CLANG_WARN_BOOL_CONVERSION = YES; 700 | CLANG_WARN_CONSTANT_CONVERSION = YES; 701 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 702 | CLANG_WARN_ENUM_CONVERSION = YES; 703 | CLANG_WARN_INT_CONVERSION = YES; 704 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 705 | CLANG_WARN_UNREACHABLE_CODE = YES; 706 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 707 | ENABLE_NS_ASSERTIONS = NO; 708 | ENABLE_STRICT_OBJC_MSGSEND = YES; 709 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 710 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 711 | GCC_WARN_UNDECLARED_SELECTOR = YES; 712 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 713 | GCC_WARN_UNUSED_FUNCTION = YES; 714 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 715 | MACOSX_DEPLOYMENT_TARGET = 10.10; 716 | MTL_ENABLE_DEBUG_INFO = NO; 717 | PRODUCT_NAME = "$(TARGET_NAME)"; 718 | SDKROOT = macosx; 719 | SWIFT_OBJC_BRIDGING_HEADER = "Swift/Swift-Bridging-Header.h"; 720 | }; 721 | name = Release; 722 | }; 723 | 05B647FD16B85AF90050002D /* Debug */ = { 724 | isa = XCBuildConfiguration; 725 | buildSettings = { 726 | ALWAYS_SEARCH_USER_PATHS = NO; 727 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 728 | CLANG_CXX_LIBRARY = "libc++"; 729 | CLANG_ENABLE_OBJC_ARC = YES; 730 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 731 | CLANG_WARN_BOOL_CONVERSION = YES; 732 | CLANG_WARN_COMMA = YES; 733 | CLANG_WARN_CONSTANT_CONVERSION = YES; 734 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 735 | CLANG_WARN_EMPTY_BODY = YES; 736 | CLANG_WARN_ENUM_CONVERSION = YES; 737 | CLANG_WARN_INFINITE_RECURSION = YES; 738 | CLANG_WARN_INT_CONVERSION = YES; 739 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 740 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 741 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 742 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 743 | CLANG_WARN_STRICT_PROTOTYPES = YES; 744 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 745 | CLANG_WARN_UNREACHABLE_CODE = YES; 746 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 747 | COPY_PHASE_STRIP = NO; 748 | ENABLE_STRICT_OBJC_MSGSEND = YES; 749 | GCC_C_LANGUAGE_STANDARD = gnu99; 750 | GCC_DYNAMIC_NO_PIC = NO; 751 | GCC_NO_COMMON_BLOCKS = YES; 752 | GCC_OPTIMIZATION_LEVEL = 0; 753 | GCC_PREPROCESSOR_DEFINITIONS = ( 754 | "$(inherited)", 755 | "OS_OBJECT_USE_OBJC=0", 756 | "DEBUG=1", 757 | ); 758 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 759 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 760 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 761 | GCC_WARN_UNDECLARED_SELECTOR = YES; 762 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 763 | GCC_WARN_UNUSED_FUNCTION = YES; 764 | GCC_WARN_UNUSED_VARIABLE = YES; 765 | HEADER_SEARCH_PATHS = "\"$(SRCROOT)/include\""; 766 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 767 | MACOSX_DEPLOYMENT_TARGET = 10.8; 768 | SDKROOT = iphoneos; 769 | }; 770 | name = Debug; 771 | }; 772 | 05B647FE16B85AF90050002D /* Release */ = { 773 | isa = XCBuildConfiguration; 774 | buildSettings = { 775 | ALWAYS_SEARCH_USER_PATHS = NO; 776 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 777 | CLANG_CXX_LIBRARY = "libc++"; 778 | CLANG_ENABLE_OBJC_ARC = YES; 779 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 780 | CLANG_WARN_BOOL_CONVERSION = YES; 781 | CLANG_WARN_COMMA = YES; 782 | CLANG_WARN_CONSTANT_CONVERSION = YES; 783 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 784 | CLANG_WARN_EMPTY_BODY = YES; 785 | CLANG_WARN_ENUM_CONVERSION = YES; 786 | CLANG_WARN_INFINITE_RECURSION = YES; 787 | CLANG_WARN_INT_CONVERSION = YES; 788 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 789 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 790 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 791 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 792 | CLANG_WARN_STRICT_PROTOTYPES = YES; 793 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 794 | CLANG_WARN_UNREACHABLE_CODE = YES; 795 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 796 | COPY_PHASE_STRIP = YES; 797 | ENABLE_STRICT_OBJC_MSGSEND = YES; 798 | GCC_C_LANGUAGE_STANDARD = gnu99; 799 | GCC_NO_COMMON_BLOCKS = YES; 800 | GCC_PREPROCESSOR_DEFINITIONS = ( 801 | "$(inherited)", 802 | "OS_OBJECT_USE_OBJC=0", 803 | ); 804 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 805 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 806 | GCC_WARN_UNDECLARED_SELECTOR = YES; 807 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 808 | GCC_WARN_UNUSED_FUNCTION = YES; 809 | GCC_WARN_UNUSED_VARIABLE = YES; 810 | HEADER_SEARCH_PATHS = "\"$(SRCROOT)/include\""; 811 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 812 | MACOSX_DEPLOYMENT_TARGET = 10.8; 813 | SDKROOT = iphoneos; 814 | VALIDATE_PRODUCT = YES; 815 | }; 816 | name = Release; 817 | }; 818 | 05B6480016B85AF90050002D /* Debug */ = { 819 | isa = XCBuildConfiguration; 820 | buildSettings = { 821 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 822 | DSTROOT = /tmp/SPAsync.dst; 823 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 824 | GCC_PREFIX_HEADER = "Support/SPAsync-Prefix.pch"; 825 | OTHER_LDFLAGS = "-ObjC"; 826 | PRODUCT_MODULE_NAME = SPAsync; 827 | PRODUCT_NAME = SPAsync; 828 | SKIP_INSTALL = YES; 829 | }; 830 | name = Debug; 831 | }; 832 | 05B6480116B85AF90050002D /* Release */ = { 833 | isa = XCBuildConfiguration; 834 | buildSettings = { 835 | ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; 836 | DSTROOT = /tmp/SPAsync.dst; 837 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 838 | GCC_PREFIX_HEADER = "Support/SPAsync-Prefix.pch"; 839 | OTHER_LDFLAGS = "-ObjC"; 840 | PRODUCT_MODULE_NAME = SPAsync; 841 | PRODUCT_NAME = SPAsync; 842 | SKIP_INSTALL = YES; 843 | }; 844 | name = Release; 845 | }; 846 | 05BBF3D516EF2EA30011E948 /* Debug */ = { 847 | isa = XCBuildConfiguration; 848 | buildSettings = { 849 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 850 | CLANG_WARN_CONSTANT_CONVERSION = YES; 851 | CLANG_WARN_ENUM_CONVERSION = YES; 852 | CLANG_WARN_INT_CONVERSION = YES; 853 | COMBINE_HIDPI_IMAGES = YES; 854 | DYLIB_COMPATIBILITY_VERSION = 1; 855 | DYLIB_CURRENT_VERSION = 1; 856 | FRAMEWORK_VERSION = A; 857 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 858 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 859 | GCC_PREFIX_HEADER = "Support/SPAsync-Prefix.pch"; 860 | GCC_PREPROCESSOR_DEFINITIONS = ( 861 | "DEBUG=1", 862 | "$(inherited)", 863 | ); 864 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 865 | INFOPLIST_FILE = "Support/SPAsync-Framework-Info.plist"; 866 | ONLY_ACTIVE_ARCH = YES; 867 | PRODUCT_NAME = SPAsync; 868 | SDKROOT = macosx; 869 | WRAPPER_EXTENSION = framework; 870 | }; 871 | name = Debug; 872 | }; 873 | 05BBF3D616EF2EA30011E948 /* Release */ = { 874 | isa = XCBuildConfiguration; 875 | buildSettings = { 876 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 877 | CLANG_WARN_CONSTANT_CONVERSION = YES; 878 | CLANG_WARN_ENUM_CONVERSION = YES; 879 | CLANG_WARN_INT_CONVERSION = YES; 880 | COMBINE_HIDPI_IMAGES = YES; 881 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 882 | DYLIB_COMPATIBILITY_VERSION = 1; 883 | DYLIB_CURRENT_VERSION = 1; 884 | FRAMEWORK_VERSION = A; 885 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 886 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 887 | GCC_PREFIX_HEADER = "Support/SPAsync-Prefix.pch"; 888 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 889 | INFOPLIST_FILE = "Support/SPAsync-Framework-Info.plist"; 890 | PRODUCT_NAME = SPAsync; 891 | SDKROOT = macosx; 892 | WRAPPER_EXTENSION = framework; 893 | }; 894 | name = Release; 895 | }; 896 | /* End XCBuildConfiguration section */ 897 | 898 | /* Begin XCConfigurationList section */ 899 | 054B258219B3346900F61F64 /* Build configuration list for PBXNativeTarget "SPAsync (Mac)" */ = { 900 | isa = XCConfigurationList; 901 | buildConfigurations = ( 902 | 054B258319B3346900F61F64 /* Debug */, 903 | 054B258419B3346900F61F64 /* Release */, 904 | ); 905 | defaultConfigurationIsVisible = 0; 906 | defaultConfigurationName = Release; 907 | }; 908 | 054B258519B3346900F61F64 /* Build configuration list for PBXNativeTarget "SPAsyncTests" */ = { 909 | isa = XCConfigurationList; 910 | buildConfigurations = ( 911 | 054B258619B3346900F61F64 /* Debug */, 912 | 054B258719B3346900F61F64 /* Release */, 913 | ); 914 | defaultConfigurationIsVisible = 0; 915 | defaultConfigurationName = Release; 916 | }; 917 | 058DBD1D199D43E8008F7741 /* Build configuration list for PBXNativeTarget "SwiftDemo" */ = { 918 | isa = XCConfigurationList; 919 | buildConfigurations = ( 920 | 058DBD1E199D43E8008F7741 /* Debug */, 921 | 058DBD1F199D43E8008F7741 /* Release */, 922 | ); 923 | defaultConfigurationIsVisible = 0; 924 | defaultConfigurationName = Release; 925 | }; 926 | 05B647D416B85AF90050002D /* Build configuration list for PBXProject "SPAsync" */ = { 927 | isa = XCConfigurationList; 928 | buildConfigurations = ( 929 | 05B647FD16B85AF90050002D /* Debug */, 930 | 05B647FE16B85AF90050002D /* Release */, 931 | ); 932 | defaultConfigurationIsVisible = 0; 933 | defaultConfigurationName = Release; 934 | }; 935 | 05B647FF16B85AF90050002D /* Build configuration list for PBXNativeTarget "SPAsync (iOS)" */ = { 936 | isa = XCConfigurationList; 937 | buildConfigurations = ( 938 | 05B6480016B85AF90050002D /* Debug */, 939 | 05B6480116B85AF90050002D /* Release */, 940 | ); 941 | defaultConfigurationIsVisible = 0; 942 | defaultConfigurationName = Release; 943 | }; 944 | 05BBF3D916EF2EA30011E948 /* Build configuration list for PBXNativeTarget "SPAsync Framework" */ = { 945 | isa = XCConfigurationList; 946 | buildConfigurations = ( 947 | 05BBF3D516EF2EA30011E948 /* Debug */, 948 | 05BBF3D616EF2EA30011E948 /* Release */, 949 | ); 950 | defaultConfigurationIsVisible = 0; 951 | defaultConfigurationName = Release; 952 | }; 953 | /* End XCConfigurationList section */ 954 | }; 955 | rootObject = 05B647D116B85AF90050002D /* Project object */; 956 | } 957 | -------------------------------------------------------------------------------- /SPAsyncTests/SPAgentTest.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPAgentTest.h 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface SPAgentTest : XCTestCase 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /SPAsyncTests/SPAgentTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPAgentTest.m 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | // 7 | // 8 | 9 | #import "SPAgentTest.h" 10 | #import "SPTaskTest.h" 11 | #import 12 | #import 13 | 14 | @interface TestAgent : NSObject 15 | - (id)leet; 16 | @end 17 | 18 | @implementation SPAgentTest 19 | 20 | - (void)testAgentAsyncTask 21 | { 22 | TestAgent *agent = [TestAgent new]; 23 | 24 | SPTask *leetTask = [[agent sp_agentAsync] leet]; 25 | __block BOOL gotLeet = NO; 26 | [leetTask addCallback:^(id value) { 27 | XCTAssertEqualObjects(value, @(1337), @"Got an unexpected value"); 28 | gotLeet = YES; 29 | } on:dispatch_get_main_queue()]; 30 | 31 | // Spin the runloop 32 | SPTestSpinRunloopWithCondition(gotLeet, 1.0); 33 | XCTAssertEqual(gotLeet, YES, @"Expected to have gotten leet by now"); 34 | } 35 | 36 | @end 37 | 38 | @implementation TestAgent 39 | { 40 | dispatch_queue_t _workQueue; 41 | } 42 | - (id)init 43 | { 44 | if(!(self = [super init])) 45 | return nil; 46 | 47 | _workQueue = dispatch_queue_create("SPAsync.testworkqueue", DISPATCH_QUEUE_SERIAL); 48 | 49 | return self; 50 | } 51 | 52 | - (void)dealloc 53 | { 54 | dispatch_release(_workQueue); 55 | } 56 | 57 | - (dispatch_queue_t)workQueue 58 | { 59 | return _workQueue; 60 | } 61 | 62 | - (id)leet 63 | { 64 | #pragma clang diagnostic push 65 | #pragma clang diagnostic ignored "-Wdeprecated" 66 | NSAssert(_workQueue == dispatch_get_current_queue(), @"Expected getter to be called on work queue"); 67 | #pragma clang diagnostic pop 68 | 69 | return @(1337); 70 | } 71 | @end -------------------------------------------------------------------------------- /SPAsyncTests/SPAsyncTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.spotify.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SPAsyncTests/SPAwaitTest.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPAwaitTest.h 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2013-01-30. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface SPAwaitTest : XCTestCase 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /SPAsyncTests/SPAwaitTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPAwaitTest.m 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2013-01-30. 6 | // 7 | // 8 | 9 | #import "SPAwaitTest.h" 10 | #import "SPTaskTest.h" 11 | #import 12 | #import 13 | 14 | @implementation SPAwaitTest 15 | 16 | - (SPTask *)awaitableNumber:(NSNumber*)num 17 | { 18 | return [SPTask delay:0.01 completeValue:num]; 19 | } 20 | 21 | - (SPTask *)simple 22 | { 23 | __block NSNumber *number; 24 | SPAsyncMethodBegin 25 | 26 | number = SPAsyncAwait([self awaitableNumber:@42]); 27 | 28 | return @([number intValue]*2); 29 | 30 | SPAsyncMethodEnd 31 | } 32 | - (void)testSimple 33 | { 34 | SPAssertTaskCompletesWithValueAndTimeout([self simple], @(84), 0.1); 35 | } 36 | 37 | 38 | 39 | - (SPTask *)multipleReturns 40 | { 41 | SPAsyncMethodBegin 42 | 43 | if(NO) 44 | return @2; 45 | else 46 | return @3; 47 | 48 | XCTFail(@"Shouldn't reach past return"); 49 | 50 | SPAsyncMethodEnd 51 | } 52 | - (void)testMultipleReturns 53 | { 54 | SPAssertTaskCompletesWithValueAndTimeout([self multipleReturns], @(3), 0.1); 55 | } 56 | 57 | - (SPTask *)awaitInConditional 58 | { 59 | __block NSNumber *number; 60 | SPAsyncMethodBegin 61 | 62 | if(NO) { 63 | number = SPAsyncAwait([self awaitableNumber:@1]); 64 | } else { 65 | number = SPAsyncAwait([self awaitableNumber:@2]); 66 | } 67 | 68 | return number; 69 | 70 | SPAsyncMethodEnd 71 | } 72 | - (void)testAwaitInConditional 73 | { 74 | SPAssertTaskCompletesWithValueAndTimeout([self awaitInConditional], @(2), 0.1); 75 | } 76 | 77 | - (SPTask*)voidMethod 78 | { 79 | SPAsyncMethodBegin 80 | SPAsyncMethodEnd 81 | } 82 | - (void)testVoidMethod 83 | { 84 | SPAssertTaskCompletesWithValueAndTimeout([self voidMethod], nil, 0.1); 85 | } 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /SPAsyncTests/SPKVOTaskTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPKVOTaskTest.m 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2014-08-25. 6 | // Copyright (c) 2014 Spotify. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "SPTaskTest.h" 12 | 13 | @interface SPKVOTaskTest : XCTestCase 14 | @end 15 | 16 | @interface Dummy : NSObject 17 | @property(nonatomic) id value; 18 | @property(nonatomic) float primitive; 19 | @end 20 | @implementation Dummy 21 | @end 22 | 23 | @implementation SPKVOTaskTest 24 | 25 | - (void)testAwaitObject 26 | { 27 | Dummy *dummy = [Dummy new]; 28 | __block BOOL found = NO; 29 | 30 | [[SPKVOTask awaitValue:@"hello" onObject:dummy forKeyPath:@"value"] addCallback:^(id value) { 31 | found = YES; 32 | } on:dispatch_get_main_queue()]; 33 | 34 | dummy.value = @"world"; 35 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 36 | XCTAssertFalse(found, @"Didn't expect to get callback when changing to this value"); 37 | 38 | dummy.value = @"hello"; 39 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 40 | XCTAssertTrue(found, @"Expected callback to have run"); 41 | } 42 | 43 | - (void)testPrimitive 44 | { 45 | Dummy *dummy = [Dummy new]; 46 | __block BOOL found = NO; 47 | 48 | [[SPKVOTask awaitValue:@(3.0) onObject:dummy forKeyPath:@"primitive"] addCallback:^(id value) { 49 | found = YES; 50 | } on:dispatch_get_main_queue()]; 51 | 52 | dummy.primitive = 2.0; 53 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 54 | XCTAssertFalse(found, @"Didn't expect to get callback when changing to this value"); 55 | 56 | dummy.primitive = 3.0; 57 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 58 | XCTAssertTrue(found, @"Expected callback to have run"); 59 | } 60 | 61 | - (void)testInitial 62 | { 63 | Dummy *dummy = [Dummy new]; 64 | __block BOOL found = NO; 65 | 66 | dummy.value = @"hello"; 67 | 68 | [[SPKVOTask awaitValue:@"hello" onObject:dummy forKeyPath:@"value"] addCallback:^(id value) { 69 | found = YES; 70 | } on:dispatch_get_main_queue()]; 71 | 72 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 73 | XCTAssertTrue(found, @"Expected callback to have run"); 74 | } 75 | 76 | - (void)testCancellation 77 | { 78 | Dummy *dummy = [Dummy new]; 79 | __block BOOL found = NO; 80 | SPTask *task = [[SPKVOTask awaitValue:@"hello" onObject:dummy forKeyPath:@"value"] addCallback:^(id value) { 81 | found = YES; 82 | } on:dispatch_get_main_queue()]; 83 | [task cancel]; 84 | 85 | // Ok, don't crash now! 86 | dummy.value = @"hello"; 87 | 88 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]]; 89 | XCTAssertFalse(found, @"Task was cancelled, callback should not have been run"); 90 | 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /SPAsyncTests/SPTaskTest.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPTaskTest.h 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface SPTaskTest : XCTestCase 12 | 13 | @end 14 | 15 | #define SPTestSpinRunloopWithCondition(condition, timeout) ({ \ 16 | NSTimeInterval __elapsed = 0; \ 17 | static const NSTimeInterval pollInterval = 0.01; \ 18 | while(!(condition) && __elapsed < timeout) { \ 19 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pollInterval]]; \ 20 | __elapsed += pollInterval; \ 21 | } \ 22 | XCTAssertTrue(__elapsed < timeout, @"Timeout reached without completion"); \ 23 | }) 24 | 25 | 26 | #define SPAssertTaskCompletesWithValueAndTimeout(task, expected, timeout) ({ \ 27 | __block BOOL __triggered = NO; \ 28 | [task addCallback:^(id value) {\ 29 | XCTAssertEqualObjects(expected, value, @"Wrong value completed"); \ 30 | __triggered = YES; \ 31 | } on:dispatch_get_main_queue()]; \ 32 | [task addErrorCallback:^(NSError *error) {\ 33 | XCTFail(@"Didn't expect task to fail"); \ 34 | __triggered = YES; \ 35 | } on:dispatch_get_main_queue()]; \ 36 | SPTestSpinRunloopWithCondition(__triggered, timeout); \ 37 | XCTAssertTrue(__triggered, @"Timeout reached without completion"); \ 38 | }) 39 | 40 | #define SPAssertTaskFailsWithErrorAndTimeout(task, expected, timeout) ({ \ 41 | __block BOOL __triggered = NO; \ 42 | [task addCallback:^(id value) {\ 43 | XCTFail(@"Task should have failed"); \ 44 | __triggered = YES; \ 45 | } on:dispatch_get_main_queue()]; \ 46 | [task addErrorCallback:^(NSError *error) {\ 47 | XCTAssertEqualObjects(error, expected, @"Not the expected error"); \ 48 | __triggered = YES; \ 49 | } on:dispatch_get_main_queue()]; \ 50 | SPTestSpinRunloopWithCondition(__triggered, timeout); \ 51 | XCTAssertTrue(__triggered, @"Timeout reached without completion"); \ 52 | }) 53 | 54 | #define SPAssertTaskCancelledWithTimeout(task, timeout) ({ \ 55 | __block BOOL __triggered = NO; \ 56 | [task addCallback:^(id value) {\ 57 | XCTFail(@"Didn't expect task to complete"); \ 58 | __triggered = YES; \ 59 | } on:dispatch_get_main_queue()]; \ 60 | [task addErrorCallback:^(NSError *error) {\ 61 | XCTFail(@"Didn't expect task to fail"); \ 62 | __triggered = YES; \ 63 | } on:dispatch_get_main_queue()]; \ 64 | SPTestSpinRunloopWithCondition(!__triggered, timeout); \ 65 | XCTAssertFalse(__triggered, @"Timeout reached with completion"); \ 66 | }) 67 | -------------------------------------------------------------------------------- /SPAsyncTests/SPTaskTest.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPTaskTest.m 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | // 7 | // 8 | 9 | #import "SPTaskTest.h" 10 | #import 11 | 12 | @implementation SPTaskTest 13 | 14 | - (void)testCallback 15 | { 16 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 17 | SPTask *task = source.task; 18 | dispatch_queue_t callbackQueue = dispatch_get_main_queue(); 19 | __block BOOL firstCallbackTriggered = NO; 20 | __block BOOL secondCallbackTriggered = NO; 21 | 22 | __weak __typeof(task) weakTask = task; 23 | [task addCallback:^(id value) { 24 | XCTAssertTrue(weakTask.isCompleted, @"Should be completed by the time the first callback fires."); 25 | XCTAssertEqualObjects(value, @(1337), @"Unexpected value"); 26 | XCTAssertEqual(firstCallbackTriggered, NO, @"Callback should only trigger once"); 27 | firstCallbackTriggered = YES; 28 | } on:callbackQueue]; 29 | [task addErrorCallback:^(id value) { 30 | XCTAssertTrue(NO, @"Error should not have triggered"); 31 | } on:callbackQueue]; 32 | [task addCallback:^(id value) { 33 | XCTAssertEqualObjects(value, @(1337), @"Unexpected value"); 34 | XCTAssertEqual(firstCallbackTriggered, YES, @"First callback should have triggered before the second"); 35 | secondCallbackTriggered = YES; 36 | } on:callbackQueue]; 37 | 38 | [source completeWithValue:@(1337)]; 39 | 40 | // Spin the runloop 41 | while(!secondCallbackTriggered) 42 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; 43 | 44 | XCTAssertEqual(firstCallbackTriggered, YES, @"First callback should have triggered"); 45 | XCTAssertEqual(secondCallbackTriggered, YES, @"Second callback should have triggered"); 46 | XCTAssertTrue(task.isCompleted, @"Completion state not altered by callbacks."); 47 | } 48 | 49 | - (void)testErrback 50 | { 51 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 52 | SPTask *task = source.task; 53 | dispatch_queue_t callbackQueue = dispatch_get_main_queue(); 54 | __block BOOL firstErrbackTriggered = NO; 55 | __block BOOL secondErrbackTriggered = NO; 56 | 57 | __weak __typeof(task) weakTask = task; 58 | [task addErrorCallback:^(NSError *error) { 59 | XCTAssertTrue(weakTask.isCompleted, @"Should be completed by the time the first callback fires."); 60 | XCTAssertEqual(error.code, (NSInteger)1337, @"Unexpected error code"); 61 | XCTAssertEqual(firstErrbackTriggered, NO, @"Errback should only trigger once"); 62 | firstErrbackTriggered = YES; 63 | } on:callbackQueue]; 64 | [task addCallback:^(id value) { 65 | XCTAssertTrue(NO, @"Callback should not have triggered"); 66 | } on:callbackQueue]; 67 | [task addErrorCallback:^(NSError *error) { 68 | XCTAssertEqual(error.code, (NSInteger)1337, @"Unexpected error code"); 69 | XCTAssertEqual(firstErrbackTriggered, YES, @"First errback should have triggered before the second"); 70 | secondErrbackTriggered = YES; 71 | } on:callbackQueue]; 72 | 73 | [source failWithError:[NSError errorWithDomain:@"test" code:1337 userInfo:nil]]; 74 | 75 | // Spin the runloop 76 | while(!secondErrbackTriggered) 77 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; 78 | 79 | XCTAssertEqual(firstErrbackTriggered, YES, @"First errback should have triggered"); 80 | XCTAssertEqual(secondErrbackTriggered, YES, @"Second errback should have triggered"); 81 | XCTAssertTrue(task.isCompleted, @"Completion state not altered by callbacks."); 82 | } 83 | 84 | - (void)testLateCallback 85 | { 86 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 87 | SPTask *task = source.task; 88 | dispatch_queue_t callbackQueue = dispatch_get_main_queue(); 89 | __block BOOL firstCallbackTriggered = NO; 90 | __block BOOL secondCallbackTriggered = NO; 91 | 92 | [task addCallback:^(id value) { 93 | XCTAssertEqualObjects(value, @(1337), @"Unexpected value"); 94 | XCTAssertEqual(firstCallbackTriggered, NO, @"Callback should only trigger once"); 95 | firstCallbackTriggered = YES; 96 | } on:callbackQueue]; 97 | 98 | [source completeWithValue:@(1337)]; 99 | 100 | [task addCallback:^(id value) { 101 | XCTAssertEqualObjects(value, @(1337), @"Unexpected value"); 102 | secondCallbackTriggered = YES; 103 | } on:callbackQueue]; 104 | 105 | 106 | // Spin the runloop 107 | while(!secondCallbackTriggered) 108 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; 109 | 110 | XCTAssertEqual(firstCallbackTriggered, YES, @"First callback should have triggered"); 111 | XCTAssertEqual(secondCallbackTriggered, YES, @"Second callback should have triggered"); 112 | } 113 | 114 | - (void)testLateErrback 115 | { 116 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 117 | SPTask *task = source.task; 118 | dispatch_queue_t callbackQueue = dispatch_get_main_queue(); 119 | __block BOOL firstErrbackTriggered = NO; 120 | __block BOOL secondErrbackTriggered = NO; 121 | 122 | [task addErrorCallback:^(NSError *error) { 123 | XCTAssertEqual(error.code, (NSInteger)1337, @"Unexpected value"); 124 | XCTAssertEqual(firstErrbackTriggered, NO, @"Callback should only trigger once"); 125 | firstErrbackTriggered = YES; 126 | } on:callbackQueue]; 127 | 128 | [source failWithError:[NSError errorWithDomain:@"test" code:1337 userInfo:nil]]; 129 | 130 | [task addErrorCallback:^(NSError *error) { 131 | XCTAssertEqual(error.code, (NSInteger)1337, @"Unexpected value"); 132 | secondErrbackTriggered = YES; 133 | } on:callbackQueue]; 134 | 135 | // Spin the runloop 136 | while(!secondErrbackTriggered) 137 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; 138 | 139 | XCTAssertEqual(firstErrbackTriggered, YES, @"First callback should have triggered"); 140 | XCTAssertEqual(secondErrbackTriggered, YES, @"Second callback should have triggered"); 141 | } 142 | 143 | - (void)testThen 144 | { 145 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 146 | [source completeWithValue:@(10)]; 147 | 148 | __block BOOL done = NO; 149 | 150 | [[[source.task then:^id(id value) { 151 | return @([value intValue]*20); 152 | } on:dispatch_get_main_queue()] then:^id(id value) { 153 | return @([value intValue]*30); 154 | } on:dispatch_get_main_queue()] addCallback:^(id value) { 155 | XCTAssertEqualObjects(value, @(6000), @"Chain didn't chain as expected"); 156 | done = YES; 157 | } on:dispatch_get_main_queue()]; 158 | 159 | while(!done) 160 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; 161 | } 162 | 163 | - (void)testRecoverSuccess 164 | { 165 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 166 | [source failWithError:[NSError errorWithDomain:@"lol" code:4 userInfo:nil]]; 167 | 168 | SPAssertTaskCompletesWithValueAndTimeout([source.task recover:^SPTask*(NSError *err) { 169 | return [SPTask completedTask:@6]; 170 | } on:dispatch_get_main_queue()], @6, 0.1); 171 | } 172 | 173 | - (void)testRecoverFailure 174 | { 175 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 176 | NSError *err = [NSError errorWithDomain:@"lol" code:4 userInfo:nil]; 177 | [source failWithError:err]; 178 | 179 | SPAssertTaskFailsWithErrorAndTimeout([source.task recover:^SPTask*(NSError *err) { 180 | return nil; 181 | } on:dispatch_get_main_queue()], err, 0.1); 182 | } 183 | 184 | - (void)testTest 185 | { 186 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 187 | [source completeWithValue:@(10)]; 188 | SPAssertTaskCompletesWithValueAndTimeout(source.task, @(10), 0.1); 189 | } 190 | 191 | - (void)testAwaitAllSuccess 192 | { 193 | SPTaskCompletionSource *source1 = [SPTaskCompletionSource new]; 194 | SPTaskCompletionSource *source2 = [SPTaskCompletionSource new]; 195 | SPTaskCompletionSource *nullSource = [SPTaskCompletionSource new]; 196 | 197 | SPTask *all = [SPTask awaitAll:@[source1.task, source2.task, nullSource.task]]; 198 | id expected = @[@1, @2, [NSNull null]]; 199 | 200 | [source1 completeWithValue:@1]; 201 | [source2 completeWithValue:@2]; 202 | [nullSource completeWithValue:nil]; 203 | 204 | SPAssertTaskCompletesWithValueAndTimeout(all, expected, 0.1); 205 | } 206 | 207 | - (void)testAwaitAllFailure 208 | { 209 | SPTaskCompletionSource *source1 = [SPTaskCompletionSource new]; 210 | SPTaskCompletionSource *source2 = [SPTaskCompletionSource new]; 211 | 212 | SPTask *all = [SPTask awaitAll:@[source1.task, source2.task]]; 213 | 214 | NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; 215 | [source1 completeWithValue:@1]; 216 | [source2 failWithError:error]; 217 | 218 | SPAssertTaskFailsWithErrorAndTimeout(all, error, 0.1); 219 | } 220 | 221 | - (void)testBasicCancellation 222 | { 223 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 224 | 225 | __block BOOL callbackWasRun = NO; 226 | [source.task addCallback:^(id value) { 227 | callbackWasRun = YES; 228 | } on:dispatch_get_main_queue()]; 229 | 230 | [source.task cancel]; 231 | 232 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 233 | if(!source.task.cancelled) 234 | [source completeWithValue:@1]; 235 | }); 236 | 237 | NSTimeInterval __elapsed = 0; 238 | static const NSTimeInterval pollInterval = 0.01; 239 | while(__elapsed < 0.1) { 240 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pollInterval]]; 241 | __elapsed += pollInterval; 242 | } 243 | 244 | XCTAssertEqual(callbackWasRun, NO, @"Callback should not have been run"); 245 | XCTAssertEqual(source.task.cancelled, YES, @"Task should be cancelled"); 246 | } 247 | 248 | - (void)testCallbackCancellation 249 | { 250 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 251 | 252 | __block BOOL cancelled = NO; 253 | [source addCancellationCallback:^{ 254 | cancelled = YES; 255 | }]; 256 | 257 | __block BOOL callbackWasRun = NO; 258 | [source.task addCallback:^(id value) { 259 | callbackWasRun = YES; 260 | } on:dispatch_get_main_queue()]; 261 | 262 | [source.task cancel]; 263 | 264 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 265 | if(!cancelled) 266 | [source completeWithValue:@1]; 267 | }); 268 | 269 | NSTimeInterval __elapsed = 0; 270 | static const NSTimeInterval pollInterval = 0.01; 271 | while(__elapsed < 0.1) { 272 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pollInterval]]; 273 | __elapsed += pollInterval; 274 | } 275 | 276 | XCTAssertEqual(callbackWasRun, NO, @"Callback should not have been run"); 277 | XCTAssertEqual(cancelled, YES, @"Cancellation callback wasn't run"); 278 | XCTAssertEqual(source.task.cancelled, YES, @"Task should be cancelled"); 279 | } 280 | 281 | - (void)testCancellationChain 282 | { 283 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 284 | 285 | __block BOOL callbackWasRun = NO; 286 | SPTask *chained = [source.task then:^id(id value) { 287 | callbackWasRun = YES; 288 | return nil; 289 | } on:dispatch_get_main_queue()]; 290 | 291 | [source.task cancel]; 292 | [source completeWithValue:@1]; 293 | 294 | NSTimeInterval __elapsed = 0; 295 | static const NSTimeInterval pollInterval = 0.01; 296 | while(__elapsed < 0.1) { 297 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pollInterval]]; 298 | __elapsed += pollInterval; 299 | } 300 | 301 | XCTAssertEqual(source.task.cancelled, YES, @"Source task should be cancelled"); 302 | XCTAssertEqual(chained.cancelled, YES, @"Chained task should be cancelled"); 303 | XCTAssertEqual(callbackWasRun, NO, @"Chained callback shouldn't have been called"); 304 | } 305 | 306 | - (void)testCancellationChainCompleted 307 | { 308 | // test that a chain that is completed and then cancelled will cancel 309 | // the created task if it has not been completed yet. 310 | 311 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 312 | __block SPTask *delayed = nil; 313 | 314 | SPTask *chained = [source.task chain:^SPTask *(id value) { 315 | delayed = [SPTask delay:0.1]; 316 | return delayed; 317 | } on:dispatch_get_main_queue()]; 318 | 319 | [source completeWithValue:@1]; 320 | 321 | SPAssertTaskCompletesWithValueAndTimeout(source.task, @1, 0.1); 322 | 323 | [source.task cancel]; 324 | 325 | SPAssertTaskCancelledWithTimeout(delayed, 0.2); 326 | 327 | XCTAssertTrue(source.task.cancelled, @"Source task should be cancelled"); 328 | XCTAssertTrue(chained.cancelled, @"Chain task should be cancelled"); 329 | XCTAssertTrue(delayed.cancelled, @"Chained task should be cancelled"); 330 | } 331 | 332 | 333 | - (void)testFinally 334 | { 335 | SPTaskCompletionSource *successSource = [SPTaskCompletionSource new]; 336 | SPTaskCompletionSource *failureSource = [SPTaskCompletionSource new]; 337 | SPTaskCompletionSource *cancellationSource = [SPTaskCompletionSource new]; 338 | __block int i = 0; 339 | 340 | [[[successSource.task addCallback:^(id value) { 341 | XCTAssertEqualObjects(value, @1, @"Task didn't complete with the correct value"); 342 | } on:dispatch_get_main_queue()] addErrorCallback:^(NSError *error) { 343 | XCTAssertNil(error, @"Task shouldn't have an error"); 344 | } on:dispatch_get_main_queue()] addFinally:^(BOOL cancelled) { 345 | XCTAssertEqual(cancelled, NO, @"Task should not be cancelled"); 346 | i++; 347 | } on:dispatch_get_main_queue()]; 348 | 349 | NSError *expected = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; 350 | [[[failureSource.task addCallback:^(id value) { 351 | XCTAssertEqualObjects(value, nil, @"Task failed and shouldn't have a value"); 352 | } on:dispatch_get_main_queue()] addErrorCallback:^(NSError *error) { 353 | XCTAssertEqual(error, expected, @"Task should have an error"); 354 | } on:dispatch_get_main_queue()] addFinally:^(BOOL cancelled) { 355 | XCTAssertEqual(cancelled, NO, @"Task should not be cancelled"); 356 | i++; 357 | } on:dispatch_get_main_queue()]; 358 | 359 | [[[cancellationSource.task addCallback:^(id value) { 360 | XCTAssertEqualObjects(value, nil, @"Task was cancelled and shouldn't have a value"); 361 | } on:dispatch_get_main_queue()] addErrorCallback:^(NSError *error) { 362 | XCTAssertNil(error, @"Task was cancelled shouldn't have an error"); 363 | } on:dispatch_get_main_queue()] addFinally:^(BOOL cancelled) { 364 | XCTAssertEqual(cancelled, YES, @"Task should be cancelled"); 365 | i++; 366 | } on:dispatch_get_main_queue()]; 367 | 368 | [successSource completeWithValue:@1]; 369 | [failureSource failWithError:expected]; 370 | [cancellationSource.task cancel]; 371 | 372 | SPTestSpinRunloopWithCondition(i == 3, 0.1); 373 | XCTAssertEqual(i, 3, @"A finalizer wasn't called"); 374 | } 375 | 376 | - (void)testAwaitAll_FailThenComplete 377 | { 378 | SPTaskCompletionSource *successSource = [SPTaskCompletionSource new]; 379 | SPTaskCompletionSource *failureSource = [SPTaskCompletionSource new]; 380 | 381 | SPTask* awaited = [SPTask awaitAll:@[successSource.task, failureSource.task]]; 382 | 383 | // when one task in an +awaitAll group fails... 384 | NSError* error = [NSError errorWithDomain:@"test" code:-1 userInfo:nil]; 385 | [failureSource failWithError:error]; 386 | 387 | // ...then another one resolves, an exception is thrown on the main thread in the +awaitAll callback 388 | [successSource completeWithValue:@1]; 389 | 390 | SPAssertTaskFailsWithErrorAndTimeout(awaited, error, 0.1); 391 | // NOTE: STAssertThrows/NoThrow doesn't help us because the exception is thrown inside a block in +awaitAll 392 | } 393 | 394 | - (void)testAwaitAll_FailThenFail 395 | { 396 | SPTaskCompletionSource *failureSource = [SPTaskCompletionSource new]; 397 | SPTaskCompletionSource *secondFailureSource = [SPTaskCompletionSource new]; 398 | 399 | SPTask* awaited = [SPTask awaitAll:@[secondFailureSource.task, failureSource.task]]; 400 | 401 | // when one task in an +awaitAll group fails... 402 | NSError* error = [NSError errorWithDomain:@"test" code:-1 userInfo:nil]; 403 | [failureSource failWithError:error]; 404 | 405 | // ...then another one fails, a redundant completion assertion is triggered 406 | [secondFailureSource failWithError:[NSError errorWithDomain:@"test" code:-2 userInfo:nil]]; 407 | 408 | SPAssertTaskFailsWithErrorAndTimeout(awaited, error, 0.1); 409 | // NOTE: STAssertThrows/NoThrow doesn't help us because the exception is thrown inside a block in +awaitAll 410 | } 411 | 412 | - (void)testAwaitAll_FailThenCancel 413 | { 414 | SPTaskCompletionSource *failureSource = [SPTaskCompletionSource new]; 415 | SPTaskCompletionSource *canceledSource = [SPTaskCompletionSource new]; 416 | 417 | SPTask* awaited = [SPTask awaitAll:@[canceledSource.task, failureSource.task]]; 418 | 419 | // when one task in an +awaitAll group fails... 420 | NSError* error = [NSError errorWithDomain:@"test" code:-1 userInfo:nil]; 421 | [failureSource failWithError:error]; 422 | 423 | // ...then another one is cancelled 424 | [canceledSource.task cancel]; 425 | 426 | SPAssertTaskFailsWithErrorAndTimeout(awaited, error, 0.1); 427 | } 428 | 429 | - (void)testAwaitAll_CancelThenComplete 430 | { 431 | SPTaskCompletionSource *source1 = [SPTaskCompletionSource new]; 432 | SPTaskCompletionSource *source2 = [SPTaskCompletionSource new]; 433 | 434 | SPTask* awaited = [SPTask awaitAll:@[source2.task, source1.task]]; 435 | 436 | [source1.task cancel]; 437 | 438 | [source2 completeWithValue:@1]; 439 | 440 | SPAssertTaskCancelledWithTimeout(awaited, 0.1); 441 | } 442 | 443 | - (void)testAwaitAll_CancelThenFail 444 | { 445 | SPTaskCompletionSource *source1 = [SPTaskCompletionSource new]; 446 | SPTaskCompletionSource *source2 = [SPTaskCompletionSource new]; 447 | 448 | SPTask* awaited = [SPTask awaitAll:@[source2.task, source1.task]]; 449 | 450 | [source1.task cancel]; 451 | 452 | [source2 failWithError:[NSError errorWithDomain:@"test" code:-1 userInfo:nil]]; 453 | 454 | SPAssertTaskCancelledWithTimeout(awaited, 0.1); 455 | } 456 | 457 | @end 458 | -------------------------------------------------------------------------------- /SPAsyncTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Sources/NSObject+SPInvocationGrabbing.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #pragma mark Invocation grabbing 5 | @interface SPInvocationGrabber () { 6 | int frameCount; 7 | char **frameStrings; 8 | BOOL backgroundAfterForward; 9 | BOOL onMainAfterForward; 10 | BOOL waitUntilDone; 11 | } 12 | @property (readwrite, retain, nonatomic) id object; 13 | @property (readwrite, retain, nonatomic) NSInvocation *invocation; 14 | 15 | @end 16 | 17 | @implementation SPInvocationGrabber 18 | - (id)initWithObject:(id)obj; 19 | { 20 | return [self initWithObject:obj stacktraceSaving:YES]; 21 | } 22 | 23 | -(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; 24 | { 25 | self.object = obj; 26 | 27 | if(saveStack) 28 | [self saveBacktrace]; 29 | 30 | return self; 31 | } 32 | -(void)dealloc; 33 | { 34 | free(frameStrings); 35 | } 36 | 37 | @synthesize backgroundAfterForward, onMainAfterForward, waitUntilDone; 38 | - (void)runInBackground; 39 | { 40 | @autoreleasepool { 41 | [self invoke]; 42 | } 43 | } 44 | 45 | 46 | - (void)forwardInvocation:(NSInvocation *)anInvocation { 47 | [anInvocation retainArguments]; 48 | anInvocation.target = _object; 49 | self.invocation = anInvocation; 50 | 51 | if(backgroundAfterForward) 52 | [NSThread detachNewThreadSelector:@selector(runInBackground) toTarget:self withObject:nil]; 53 | else if(onMainAfterForward) 54 | [self performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:waitUntilDone]; 55 | else if(_afterForwardInvocation) 56 | _afterForwardInvocation(); 57 | } 58 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)inSelector { 59 | NSMethodSignature *signature = [super methodSignatureForSelector:inSelector]; 60 | if (signature == NULL) 61 | signature = [_object methodSignatureForSelector:inSelector]; 62 | 63 | return signature; 64 | } 65 | 66 | - (void)invoke; 67 | { 68 | 69 | @try { 70 | [_invocation invoke]; 71 | } 72 | @catch (NSException * e) { 73 | NSLog(@"SPInvocationGrabber's target raised %@:\n\t%@\nInvocation was originally scheduled at:", e.name, e); 74 | [self printBacktrace]; 75 | printf("\n"); 76 | [e raise]; 77 | } 78 | 79 | self.invocation = nil; 80 | self.object = nil; 81 | self.afterForwardInvocation = nil; 82 | } 83 | 84 | -(void)saveBacktrace; 85 | { 86 | void *backtraceFrames[128]; 87 | frameCount = backtrace(&backtraceFrames[0], 128); 88 | frameStrings = backtrace_symbols(&backtraceFrames[0], frameCount); 89 | } 90 | -(void)printBacktrace; 91 | { 92 | for(int x = 3; x < frameCount; x++) { 93 | if(frameStrings[x] == NULL) { break; } 94 | printf("%s\n", frameStrings[x]); 95 | } 96 | } 97 | @end 98 | 99 | @implementation NSObject (SPInvocationGrabbing) 100 | -(id)grab; 101 | { 102 | return [[SPInvocationGrabber alloc] initWithObject:self]; 103 | } 104 | -(id)grabWithoutStacktrace 105 | { 106 | return [[SPInvocationGrabber alloc] initWithObject:self stacktraceSaving:NO]; 107 | } 108 | -(id)invokeAfter:(NSTimeInterval)delta; 109 | { 110 | id grabber = [self grab]; 111 | [NSTimer scheduledTimerWithTimeInterval:delta target:grabber selector:@selector(invoke) userInfo:nil repeats:NO]; 112 | return grabber; 113 | } 114 | - (id)nextRunloop; 115 | { 116 | return [self invokeAfter:0]; 117 | } 118 | -(id)inBackground; 119 | { 120 | SPInvocationGrabber *grabber = [self grab]; 121 | grabber.backgroundAfterForward = YES; 122 | return grabber; 123 | } 124 | -(id)onMainAsync:(BOOL)async; 125 | { 126 | SPInvocationGrabber *grabber = [self grab]; 127 | grabber.onMainAfterForward = YES; 128 | grabber.waitUntilDone = !async; 129 | return grabber; 130 | } 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /Sources/SPAgent.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @implementation NSObject (SPAgentDo) 6 | - (instancetype)sp_agentAsync 7 | { 8 | SPInvocationGrabber *grabber = [self grabWithoutStacktrace]; 9 | __weak SPInvocationGrabber *weakGrabber = grabber; 10 | 11 | SPTaskCompletionSource *completionSource = [SPTaskCompletionSource new]; 12 | SPTask *task = completionSource.task; 13 | __block void *unsafeTask = (__bridge void *)(task); 14 | 15 | grabber.afterForwardInvocation = ^{ 16 | NSInvocation *invocation = [weakGrabber invocation]; 17 | 18 | // Let the caller get the result of the invocation as a task. 19 | // Block guarantees lifetime of 'task', so just bridge it here. 20 | BOOL hasObjectReturn = strcmp([invocation.methodSignature methodReturnType], @encode(id)) == 0; 21 | if(hasObjectReturn) 22 | [invocation setReturnValue:&unsafeTask]; 23 | 24 | dispatch_async([(id)self workQueue], ^{ 25 | #pragma clang diagnostic push 26 | #pragma clang diagnostic ignored "-Warc-retain-cycles" 27 | // "invoke" will break the cycle, and the block must hold on to grabber 28 | // until this moment so that it survives (nothing else is holding the grabber) 29 | [grabber invoke]; 30 | #pragma clang diagnostic pop 31 | if(hasObjectReturn) { 32 | __unsafe_unretained id result = nil; 33 | [invocation getReturnValue:&result]; 34 | [completionSource completeWithValue:result]; 35 | } 36 | }); 37 | }; 38 | return grabber; 39 | } 40 | 41 | @end -------------------------------------------------------------------------------- /Sources/SPAwait.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @implementation SPAwaitCoroutine { 6 | SPAwaitCoroutineBody _body; 7 | SPTaskCompletionSource *_source; 8 | id _yieldedValue; 9 | BOOL _completed; 10 | } 11 | - (id)init 12 | { 13 | if(!(self = [super init])) 14 | return nil; 15 | _source = [SPTaskCompletionSource new]; 16 | 17 | // We need to live until the coroutine is complete 18 | CFRetain((__bridge CFTypeRef)(self)); 19 | 20 | return self; 21 | } 22 | - (void)setBody:(SPAwaitCoroutineBody)body; 23 | { 24 | _body = [body copy]; 25 | } 26 | 27 | - (void)resumeAt:(int)line 28 | { 29 | id ret = _body(line); 30 | if(ret == [SPAwaitCoroutine awaitSentinel]) 31 | return; 32 | 33 | _yieldedValue = ret; 34 | [self finish]; 35 | } 36 | 37 | + (id)awaitSentinel 38 | { 39 | static id sentinel; 40 | static dispatch_once_t onceToken; 41 | dispatch_once(&onceToken, ^{ 42 | sentinel = [NSObject new]; 43 | }); 44 | return sentinel; 45 | } 46 | 47 | - (void)finish 48 | { 49 | NSAssert(!_completed, @"Didn't expect to complete twice"); 50 | _completed = YES; 51 | [_source completeWithValue:_yieldedValue]; 52 | 53 | // The coroutine is complete. We can remove it now. 54 | CFRelease((__bridge CFTypeRef)(self)); 55 | } 56 | 57 | - (void)yieldValue:(id)value 58 | { 59 | _yieldedValue = value; 60 | } 61 | 62 | - (SPTask*)task 63 | { 64 | return _source.task; 65 | } 66 | 67 | - (dispatch_queue_t)queueFor:(id)object 68 | { 69 | if([NSRunLoop currentRunLoop] == [NSRunLoop mainRunLoop]) 70 | return dispatch_get_main_queue(); 71 | if([object respondsToSelector:@selector(workQueue)]) 72 | return [object workQueue]; 73 | return dispatch_get_global_queue(0, 0); 74 | } 75 | 76 | @end -------------------------------------------------------------------------------- /Sources/SPKVOTask.m: -------------------------------------------------------------------------------- 1 | #import "SPKVOTask.h" 2 | 3 | static void *kContext = &kContext; 4 | 5 | @interface SPA_NS(KVOTaskContainer) : NSObject 6 | @property(nonatomic) SPA_NS(TaskCompletionSource) *source; 7 | @property(nonatomic) id value; 8 | @end 9 | 10 | @implementation SPA_NS(KVOTask) 11 | + (id)awaitValue:(id)value onObject:(id)object forKeyPath:(NSString*)keyPath 12 | { 13 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 14 | SPA_NS(KVOTaskContainer) *container = [SPA_NS(KVOTaskContainer) new]; // owned by the task 15 | container.source = source; 16 | 17 | container.value = value; 18 | [source.task addFinallyCallback:^(BOOL cancelled) { 19 | [object removeObserver:container forKeyPath:keyPath context:kContext]; 20 | }]; 21 | [object addObserver:container forKeyPath:keyPath options:NSKeyValueObservingOptionInitial context:kContext]; 22 | return (id)source.task; 23 | } 24 | @end 25 | @implementation SPA_NS(KVOTaskContainer) 26 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 27 | { 28 | if(context != kContext) 29 | return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 30 | 31 | id newValue = [object valueForKeyPath:keyPath]; 32 | if([newValue isEqual:self.value]) 33 | [self.source completeWithValue:newValue]; 34 | } 35 | @end 36 | -------------------------------------------------------------------------------- /Sources/SPTask.m: -------------------------------------------------------------------------------- 1 | // 2 | // SPTask.m 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface SPA_NS(Task) () 12 | { 13 | NSMutableArray *_callbacks; 14 | NSMutableArray *_errbacks; 15 | NSMutableArray *_finallys; 16 | NSMutableArray *_childTasks; 17 | id _completedValue; 18 | NSError *_completedError; 19 | __weak SPA_NS(TaskCompletionSource) *_source; 20 | } 21 | @property(getter=isCancelled,readwrite) BOOL cancelled; 22 | @property(getter=isCompleted,readwrite) BOOL completed; 23 | @end 24 | 25 | @interface SPA_NS(TaskCompletionSource) () 26 | - (void)cancel; 27 | @end 28 | 29 | @implementation SPA_NS(Task) 30 | @synthesize cancelled = _isCancelled; 31 | @synthesize completed = _isCompleted; 32 | 33 | - (instancetype)initFromSource:(SPA_NS(TaskCompletionSource)*)source; 34 | { 35 | if(!(self = [super init])) 36 | return nil; 37 | _callbacks = [NSMutableArray new]; 38 | _errbacks = [NSMutableArray new]; 39 | _finallys = [NSMutableArray new]; 40 | _childTasks = [NSMutableArray new]; 41 | _source = source; 42 | return self; 43 | } 44 | 45 | - (instancetype)addCallback:(SPTaskCallback)callback on:(dispatch_queue_t)queue 46 | { 47 | @synchronized(_callbacks) { 48 | if(_isCompleted) { 49 | if(!_completedError) { 50 | dispatch_async(queue, ^{ 51 | callback(self->_completedValue); 52 | }); 53 | } 54 | } else { 55 | [_callbacks addObject:[[SPA_NS(CallbackHolder) alloc] initWithCallback:callback onQueue:queue]]; 56 | } 57 | } 58 | return self; 59 | } 60 | 61 | - (instancetype)addCallback:(SPTaskCallback)callback 62 | { 63 | return [self addCallback:callback on:dispatch_get_main_queue()]; 64 | } 65 | 66 | - (instancetype)addErrorCallback:(SPTaskErrback)errback on:(dispatch_queue_t)queue 67 | { 68 | @synchronized(_callbacks) { 69 | if(_isCompleted) { 70 | if(_completedError) { 71 | dispatch_async(queue, ^{ 72 | errback(self->_completedError); 73 | }); 74 | } 75 | } else { 76 | [_errbacks addObject:[[SPA_NS(CallbackHolder) alloc] initWithCallback:errback onQueue:queue]]; 77 | } 78 | } 79 | return self; 80 | } 81 | 82 | - (instancetype)addErrorCallback:(SPTaskErrback)errback 83 | { 84 | return [self addErrorCallback:errback on:dispatch_get_main_queue()]; 85 | } 86 | 87 | - (instancetype)addFinallyCallback:(SPTaskFinally)finally on:(dispatch_queue_t)queue 88 | { 89 | @synchronized(_callbacks) { 90 | if(_isCompleted) { 91 | dispatch_async(queue, ^{ 92 | finally(self->_isCancelled); 93 | }); 94 | } else { 95 | [_finallys addObject:[[SPA_NS(CallbackHolder) alloc] initWithCallback:(id)finally onQueue:queue]]; 96 | } 97 | } 98 | return self; 99 | } 100 | 101 | 102 | - (instancetype)addFinallyCallback:(SPTaskFinally)finally 103 | { 104 | return [self addFinallyCallback:finally on:dispatch_get_main_queue()]; 105 | } 106 | 107 | + (instancetype)awaitAll:(NSArray*)tasks 108 | { 109 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 110 | 111 | if([tasks count] == 0) { 112 | [source completeWithValue:@[]]; 113 | return source.task; 114 | } 115 | 116 | NSMutableArray *values = [NSMutableArray new]; 117 | NSMutableSet *remainingTasks = [NSMutableSet setWithArray:tasks]; 118 | 119 | int i = 0; 120 | for(SPA_NS(Task) *task in tasks) { 121 | [source.task->_childTasks addObject:task]; 122 | 123 | __weak SPA_NS(Task) *weakTask = task; 124 | 125 | [values addObject:[NSNull null]]; 126 | [[[task addCallback:^(id value) { 127 | if(value) 128 | [values replaceObjectAtIndex:i withObject:value]; 129 | 130 | [remainingTasks removeObject:weakTask]; 131 | if([remainingTasks count] == 0) 132 | [source completeWithValue:values]; 133 | } on:dispatch_get_main_queue()] addErrorCallback:^(NSError *error) { 134 | if ([remainingTasks count] == 0) { 135 | return; 136 | } 137 | 138 | [remainingTasks removeObject:weakTask]; 139 | [source failWithError:error]; 140 | 141 | [remainingTasks makeObjectsPerformSelector:@selector(cancel)]; 142 | [remainingTasks removeAllObjects]; 143 | [values removeAllObjects]; 144 | } on:dispatch_get_main_queue()] addFinallyCallback:^(BOOL canceled) { 145 | if(canceled) { 146 | [source.task cancel]; 147 | } 148 | } on:dispatch_get_main_queue()]; 149 | 150 | i++; 151 | } 152 | return source.task; 153 | } 154 | 155 | - (void)completeWithValue:(id)value 156 | { 157 | if([value isKindOfClass:[NSError class]]) { 158 | return [self failWithError:value ignoreIfAlreadyCompleted:NO]; 159 | } 160 | 161 | NSAssert(!_isCompleted, @"Can't complete a task twice"); 162 | if(_isCompleted) 163 | return; 164 | 165 | if(self.cancelled) 166 | return; 167 | 168 | NSArray *callbacks = nil; 169 | NSArray *finallys = nil; 170 | @synchronized(_callbacks) { 171 | _isCompleted = YES; 172 | _completedValue = value; 173 | callbacks = [_callbacks copy]; 174 | finallys = [_finallys copy]; 175 | 176 | for(SPA_NS(CallbackHolder) *holder in callbacks) { 177 | dispatch_async(holder.callbackQueue, ^{ 178 | if(self.cancelled) 179 | return; 180 | 181 | holder.callback(value); 182 | }); 183 | } 184 | 185 | for(SPA_NS(CallbackHolder) *holder in finallys) { 186 | dispatch_async(holder.callbackQueue, ^{ 187 | ((SPTaskFinally)holder.callback)(self.cancelled); 188 | }); 189 | } 190 | 191 | [_callbacks removeAllObjects]; 192 | [_errbacks removeAllObjects]; 193 | [_finallys removeAllObjects]; 194 | } 195 | } 196 | 197 | - (void)failWithError:(NSError*)error ignoreIfAlreadyCompleted:(BOOL)ignoreSubsequentValues 198 | { 199 | if(!ignoreSubsequentValues) { 200 | NSAssert(!_isCompleted, @"Can't complete a task twice"); 201 | } 202 | if(_isCompleted) 203 | return; 204 | 205 | if(self.cancelled) 206 | return; 207 | 208 | NSArray *errbacks = nil; 209 | NSArray *finallys = nil; 210 | @synchronized(_callbacks) { 211 | _isCompleted = YES; 212 | _completedError = error; 213 | errbacks = [_errbacks copy]; 214 | finallys = [_finallys copy]; 215 | 216 | for(SPA_NS(CallbackHolder) *holder in errbacks) { 217 | dispatch_async(holder.callbackQueue, ^{ 218 | holder.callback(error); 219 | }); 220 | } 221 | 222 | for(SPA_NS(CallbackHolder) *holder in finallys) { 223 | dispatch_async(holder.callbackQueue, ^{ 224 | ((SPTaskFinally)holder.callback)(self.cancelled); 225 | }); 226 | } 227 | 228 | 229 | [_callbacks removeAllObjects]; 230 | [_errbacks removeAllObjects]; 231 | [_finallys removeAllObjects]; 232 | } 233 | } 234 | @end 235 | 236 | @implementation SPA_NS(Task) (SPTaskCancellation) 237 | @dynamic cancelled; // provided in main implementation block as a synthesize 238 | 239 | - (void)cancel 240 | { 241 | BOOL shouldCancel = NO; 242 | @synchronized(self) { 243 | shouldCancel = !self.cancelled; 244 | self.cancelled = YES; 245 | } 246 | 247 | if(shouldCancel) { 248 | [_source cancel]; 249 | // break any circular references between source<> task by removing 250 | // callbacks and errbacks which might reference the source 251 | @synchronized(_callbacks) { 252 | [_callbacks removeAllObjects]; 253 | [_errbacks removeAllObjects]; 254 | 255 | for(SPA_NS(CallbackHolder) *holder in _finallys) { 256 | dispatch_async(holder.callbackQueue, ^{ 257 | ((SPTaskFinally)holder.callback)(YES); 258 | }); 259 | } 260 | 261 | [_finallys removeAllObjects]; 262 | } 263 | } 264 | 265 | for(SPA_NS(Task) *child in _childTasks) 266 | [child cancel]; 267 | } 268 | @end 269 | 270 | @implementation SPA_NS(Task) (SPTaskExtended) 271 | - (instancetype)then:(SPTaskThenCallback)worker on:(dispatch_queue_t)queue 272 | { 273 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 274 | SPA_NS(Task) *then = source.task; 275 | [_childTasks addObject:then]; 276 | 277 | [self addCallback:^(id value) { 278 | id result = worker(value); 279 | [source completeWithValue:result]; 280 | } on:queue]; 281 | [self addErrorCallback:^(NSError *error) { 282 | [source failWithError:error]; 283 | } on:queue]; 284 | 285 | return then; 286 | } 287 | 288 | - (instancetype)chain:(SPTaskChainCallback)chainer on:(dispatch_queue_t)queue 289 | { 290 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 291 | SPA_NS(Task) *chain = source.task; 292 | [_childTasks addObject:chain]; 293 | 294 | [self addCallback:^(id value) { 295 | SPA_NS(Task) *workToBeProvided = chainer(value); 296 | 297 | [chain->_childTasks addObject:workToBeProvided]; 298 | 299 | [source completeWithTask:workToBeProvided]; 300 | } on:queue]; 301 | [self addErrorCallback:^(NSError *error) { 302 | [source failWithError:error]; 303 | } on:queue]; 304 | 305 | return chain; 306 | } 307 | 308 | - (instancetype)chain 309 | { 310 | return [self chain:^SPA_NS(Task) *(id value) { 311 | return value; 312 | } on:dispatch_get_global_queue(0, 0)]; 313 | } 314 | 315 | - (instancetype)recover:(SPTaskRecoverCallback)recoverer on:(dispatch_queue_t)queue 316 | { 317 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 318 | SPA_NS(Task) *chain = source.task; 319 | [_childTasks addObject:chain]; 320 | 321 | [self addErrorCallback:^(NSError *error) { 322 | SPA_NS(Task) *workToBeProvided = recoverer(error); 323 | if(!workToBeProvided) { 324 | [source failWithError:error]; 325 | } else { 326 | [chain->_childTasks addObject:workToBeProvided]; 327 | [source completeWithTask:workToBeProvided]; 328 | } 329 | } on:queue]; 330 | [self addCallback:^(id value) { 331 | [source completeWithValue:value]; 332 | } on:queue]; 333 | 334 | return chain; 335 | 336 | } 337 | @end 338 | 339 | @implementation SPA_NS(Task) (SPTaskConvenience) 340 | + (instancetype)delay:(NSTimeInterval)delay completeValue:(id)completeValue 341 | { 342 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 343 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC); 344 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 345 | if (source.task.cancelled) 346 | return; 347 | 348 | [source completeWithValue:completeValue]; 349 | }); 350 | 351 | return source.task; 352 | } 353 | 354 | + (instancetype)delay:(NSTimeInterval)delay 355 | { 356 | return [SPA_NS(Task) delay:delay completeValue:nil]; 357 | } 358 | 359 | + (instancetype)performWork:(SPTaskWorkGeneratingCallback)work onQueue:(dispatch_queue_t)queue; 360 | { 361 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 362 | dispatch_async(queue, ^{ 363 | id value = work(); 364 | [source completeWithValue:value]; 365 | }); 366 | return source.task; 367 | } 368 | 369 | + (instancetype)fetchWork:(SPTaskTaskGeneratingCallback)work onQueue:(dispatch_queue_t)queue 370 | { 371 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 372 | dispatch_async(queue, ^{ 373 | SPA_NS(Task) *task = work(); 374 | [source completeWithTask:task]; 375 | }); 376 | return source.task; 377 | } 378 | 379 | + (instancetype)completedTask:(id)completeValue; 380 | { 381 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 382 | [source completeWithValue:completeValue]; 383 | return source.task; 384 | } 385 | 386 | + (instancetype)failedTask:(NSError*)failure 387 | { 388 | SPA_NS(TaskCompletionSource) *source = [SPA_NS(TaskCompletionSource) new]; 389 | [source failWithError:failure]; 390 | return source.task; 391 | } 392 | 393 | @end 394 | 395 | @implementation SPA_NS(TaskCompletionSource) 396 | { 397 | SPA_NS(Task) *_task; 398 | NSMutableArray *_cancellationHandlers; 399 | } 400 | 401 | - (instancetype)init 402 | { 403 | if(!(self = [super init])) 404 | return nil; 405 | _cancellationHandlers = [NSMutableArray new]; 406 | _task = [[SPA_NS(Task) alloc] initFromSource:self]; 407 | return self; 408 | } 409 | 410 | - (SPA_NS(Task)*)task 411 | { 412 | return _task; 413 | } 414 | 415 | - (void)completeWithValue:(id)value 416 | { 417 | [self.task completeWithValue:value]; 418 | } 419 | 420 | - (void)failWithError:(NSError*)error ignoreIfAlreadyCompleted:(BOOL)ignoreSubsequentValues 421 | { 422 | [self.task failWithError:error ignoreIfAlreadyCompleted:ignoreSubsequentValues]; 423 | } 424 | 425 | - (void)failWithError:(NSError*)error 426 | { 427 | [self failWithError:error ignoreIfAlreadyCompleted:NO]; 428 | } 429 | 430 | - (void)completeWithTask:(SPA_NS(Task)*)task 431 | { 432 | [[task addCallback:^(id value) { 433 | [self.task completeWithValue:value]; 434 | } on:dispatch_get_global_queue(0, 0)] addErrorCallback:^(NSError *error) { 435 | [self.task failWithError:error ignoreIfAlreadyCompleted:NO]; 436 | } on:dispatch_get_global_queue(0, 0)]; 437 | } 438 | 439 | - (dispatch_block_t)voidResolver 440 | { 441 | return [^{ 442 | [self completeWithValue:nil]; 443 | } copy]; 444 | } 445 | 446 | - (void(^)(id))resolver 447 | { 448 | return [^(id param){ 449 | [self completeWithValue:param]; 450 | } copy]; 451 | } 452 | 453 | - (void)addCancellationCallback:(void(^)(void))cancellationCallback 454 | { 455 | [_cancellationHandlers addObject:cancellationCallback]; 456 | } 457 | 458 | - (void)cancel 459 | { 460 | for(void(^cancellationHandler)(void) in _cancellationHandlers) 461 | cancellationHandler(); 462 | } 463 | 464 | @end 465 | 466 | @implementation SPA_NS(CallbackHolder) 467 | - (id)initWithCallback:(SPTaskCallback)callback onQueue:(dispatch_queue_t)callbackQueue 468 | { 469 | if(!(self = [super init])) 470 | return nil; 471 | 472 | self.callback = callback; 473 | self.callbackQueue = callbackQueue; 474 | 475 | return self; 476 | } 477 | 478 | - (void)dealloc 479 | { 480 | self.callbackQueue = nil; 481 | } 482 | 483 | - (void)setCallbackQueue:(dispatch_queue_t)callbackQueue 484 | { 485 | #if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE 486 | if(callbackQueue) 487 | dispatch_retain(callbackQueue); 488 | if(_callbackQueue) 489 | dispatch_release(_callbackQueue); 490 | #endif 491 | _callbackQueue = callbackQueue; 492 | } 493 | @end 494 | 495 | @implementation SPA_NS(Task) (Deprecated) 496 | - (instancetype)addErrback:(SPTaskErrback)errback on:(dispatch_queue_t)queue; 497 | { 498 | return [self addErrorCallback:errback on:queue]; 499 | } 500 | 501 | - (instancetype)addFinally:(SPTaskFinally)finally on:(dispatch_queue_t)queue; 502 | { 503 | return [self addFinallyCallback:finally on:queue]; 504 | } 505 | @end 506 | -------------------------------------------------------------------------------- /Support/SPAsync-Framework-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.spotify.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSHumanReadableCopyright 26 | Copyright © 2013 Spotify. All rights reserved. 27 | NSPrincipalClass 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Support/SPAsync-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'SPAsync' target in the 'SPAsync' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /Swift/Swift-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | -------------------------------------------------------------------------------- /Swift/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2014-08-14. 6 | // Copyright (c) 2014 ThirdCog. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Task : Cancellable, Equatable 12 | { 13 | // MARK: Public interface: Callbacks 14 | 15 | public func addCallback(callback: (T -> Void)) -> Self 16 | { 17 | return addCallback(on:dispatch_get_main_queue(), callback: callback) 18 | } 19 | 20 | public func addCallback(on queue: dispatch_queue_t, callback: (T -> Void)) -> Self 21 | { 22 | synchronized(self.callbackLock) { 23 | if self.isCompleted { 24 | if self.completedError == nil { 25 | dispatch_async(queue) { 26 | callback(self.completedValue!) 27 | } 28 | } 29 | } else { 30 | self.callbacks.append(TaskCallbackHolder(on: queue, callback: callback)) 31 | } 32 | } 33 | return self 34 | } 35 | 36 | public func addErrorCallback(callback: (NSError! -> Void)) -> Self 37 | { 38 | return addErrorCallback(on:dispatch_get_main_queue(), callback:callback) 39 | } 40 | public func addErrorCallback(on queue: dispatch_queue_t, callback: (NSError! -> Void)) -> Self 41 | { 42 | synchronized(self.callbackLock) { 43 | if self.isCompleted { 44 | if self.completedError != nil { 45 | dispatch_async(queue) { 46 | callback(self.completedError!) 47 | } 48 | } 49 | } else { 50 | self.errbacks.append(TaskCallbackHolder(on: queue, callback: callback)) 51 | } 52 | } 53 | return self 54 | } 55 | 56 | public func addFinallyCallback(callback: (Bool -> Void)) -> Self 57 | { 58 | return addFinallyCallback(on:dispatch_get_main_queue(), callback:callback) 59 | } 60 | public func addFinallyCallback(on queue: dispatch_queue_t, callback: (Bool -> Void)) -> Self 61 | { 62 | synchronized(self.callbackLock) { 63 | if(self.isCompleted) { 64 | dispatch_async(queue, { () -> Void in 65 | callback(self.isCancelled) 66 | }) 67 | } else { 68 | self.finallys.append(TaskCallbackHolder(on: queue, callback: callback)) 69 | } 70 | } 71 | return self 72 | } 73 | 74 | 75 | // MARK: Public interface: Advanced callbacks 76 | 77 | public func then(worker: (T -> T2)) -> Task 78 | { 79 | return then(on:dispatch_get_main_queue(), worker: worker) 80 | } 81 | public func then(on queue:dispatch_queue_t, worker: (T -> T2)) -> Task 82 | { 83 | let source = TaskCompletionSource(); 84 | let then = source.task; 85 | self.childTasks.append(then) 86 | 87 | self.addCallback(on: queue, callback: { (value: T) -> Void in 88 | let result = worker(value) 89 | source.completeWithValue(result) 90 | }) 91 | self.addErrorCallback(on: queue, callback: { (error: NSError!) -> Void in 92 | source.failWithError(error) 93 | }) 94 | return then 95 | } 96 | 97 | public func then(chainer: (T -> Task)) -> Task 98 | { 99 | return then(on:dispatch_get_main_queue(), chainer: chainer) 100 | } 101 | public func then(on queue:dispatch_queue_t, chainer: (T -> Task)) -> Task 102 | { 103 | let source = TaskCompletionSource(); 104 | let chain = source.task; 105 | self.childTasks.append(chain) 106 | 107 | self.addCallback(on: queue, callback: { (value: T) -> Void in 108 | let workToBeProvided : Task = chainer(value) 109 | 110 | chain.childTasks.append(workToBeProvided) 111 | source.completeWithTask(workToBeProvided) 112 | }) 113 | self.addErrorCallback(on: queue, callback: { (error: NSError!) -> Void in 114 | source.failWithError(error) 115 | }) 116 | 117 | return chain; 118 | } 119 | 120 | /// Transforms Task> into a Task asynchronously 121 | // dunno how to do this with static typing... 122 | /*public func chain() -> Task 123 | { 124 | return self.then({(value: Task) -> T2 in 125 | return value 126 | }) 127 | }*/ 128 | 129 | 130 | // MARK: Public interface: Cancellation 131 | 132 | public func cancel() 133 | { 134 | var shouldCancel = false 135 | synchronized(callbackLock) { () -> Void in 136 | shouldCancel = !self.isCancelled 137 | self.isCancelled = true 138 | } 139 | 140 | if shouldCancel { 141 | self.source!.cancel() 142 | // break any circular references between source<> task by removing 143 | // callbacks and errbacks which might reference the source 144 | synchronized(callbackLock) { 145 | self.callbacks.removeAll() 146 | self.errbacks.removeAll() 147 | 148 | for holder in self.finallys { 149 | dispatch_async(holder.callbackQueue, { () -> Void in 150 | holder.callback(true) 151 | }) 152 | } 153 | 154 | self.finallys.removeAll() 155 | } 156 | } 157 | 158 | for child in childTasks { 159 | child.cancel() 160 | } 161 | 162 | } 163 | 164 | public private(set) var isCancelled = false 165 | 166 | 167 | // MARK: Public interface: construction 168 | 169 | class func performWork(on queue:dispatch_queue_t, work: Void -> T) -> Task 170 | { 171 | let source = TaskCompletionSource() 172 | dispatch_async(queue) { 173 | let value = work() 174 | source.completeWithValue(value) 175 | } 176 | return source.task 177 | } 178 | 179 | class func fetchWork(on queue:dispatch_queue_t, work: Void -> Task) -> Task 180 | { 181 | let source = TaskCompletionSource() 182 | dispatch_async(queue) { 183 | let value = work() 184 | source.completeWithTask(value) 185 | } 186 | return source.task 187 | 188 | } 189 | 190 | class func delay(interval: NSTimeInterval, value : T) -> Task 191 | { 192 | let source = TaskCompletionSource() 193 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in 194 | source.completeWithValue(value) 195 | } 196 | return source.task 197 | } 198 | 199 | class func completedTask(value: T) -> Task 200 | { 201 | let source = TaskCompletionSource() 202 | source.completeWithValue(value) 203 | return source.task 204 | } 205 | 206 | class func failedTask(error: NSError!) -> Task 207 | { 208 | let source = TaskCompletionSource() 209 | source.failWithError(error) 210 | return source.task 211 | } 212 | 213 | 214 | // MARK: Public interface: other convenience 215 | 216 | class func awaitAll(tasks: [Task]) -> Task<[Any]> 217 | { 218 | let source = TaskCompletionSource<[Any]>() 219 | 220 | if tasks.count == 0 { 221 | source.completeWithValue([]) 222 | return source.task; 223 | } 224 | 225 | var values : [Any] = [] 226 | var remainingTasks : [Task] = tasks 227 | 228 | var i : Int = 0 229 | for task in tasks { 230 | source.task.childTasks.append(task) 231 | weak var weakTask = task 232 | 233 | values.append(NSNull()) 234 | task.addCallback(on: dispatch_get_main_queue(), callback: { (value: Any) -> Void in 235 | values[i] = value 236 | remainingTasks.removeAtIndex(find(remainingTasks, weakTask!)!) 237 | if remainingTasks.count == 0 { 238 | source.completeWithValue(values) 239 | } 240 | }).addErrorCallback(on: dispatch_get_main_queue(), callback: { (error: NSError!) -> Void in 241 | if remainingTasks.count == 0 { 242 | // ?? how could this happen? 243 | return 244 | } 245 | 246 | remainingTasks.removeAtIndex(find(remainingTasks, weakTask!)!) 247 | source.failWithError(error) 248 | for task in remainingTasks { 249 | task.cancel() 250 | } 251 | remainingTasks.removeAll() 252 | values.removeAll() 253 | 254 | }).addFinallyCallback(on: dispatch_get_main_queue(), callback: { (canceled: Bool) -> Void in 255 | if canceled { 256 | source.task.cancel() 257 | } 258 | }) 259 | 260 | i++; 261 | } 262 | return source.task; 263 | } 264 | 265 | 266 | // MARK: Private implementation 267 | 268 | var callbacks : [TaskCallbackHolder Void>] = [] 269 | var errbacks : [TaskCallbackHolder Void>] = [] 270 | var finallys : [TaskCallbackHolder Void>] = [] 271 | var callbackLock : NSLock = NSLock() 272 | 273 | var isCompleted = false 274 | var completedValue : T? = nil 275 | var completedError : NSError? = nil 276 | weak var source : TaskCompletionSource? 277 | var childTasks : [Cancellable] = [] 278 | 279 | internal init() 280 | { 281 | // temp 282 | } 283 | 284 | internal init(source: TaskCompletionSource) 285 | { 286 | self.source = source 287 | } 288 | 289 | func completeWithValue(value: T) 290 | { 291 | assert(self.isCompleted == false, "Can't complete a task twice") 292 | if self.isCompleted { 293 | return 294 | } 295 | 296 | if self.isCancelled { 297 | return 298 | } 299 | 300 | synchronized(callbackLock) { 301 | self.isCompleted = true 302 | self.completedValue = value 303 | let copiedCallbacks = self.callbacks 304 | let copiedFinallys = self.finallys 305 | 306 | for holder in copiedCallbacks { 307 | dispatch_async(holder.callbackQueue) { 308 | if !self.isCancelled { 309 | holder.callback(value) 310 | } 311 | } 312 | } 313 | for holder in copiedFinallys { 314 | dispatch_async(holder.callbackQueue) { 315 | holder.callback(self.isCancelled) 316 | } 317 | } 318 | 319 | self.callbacks.removeAll() 320 | self.errbacks.removeAll() 321 | self.finallys.removeAll() 322 | } 323 | 324 | } 325 | func failWithError(error: NSError!) 326 | { 327 | assert(self.isCompleted == false, "Can't complete a task twice") 328 | if self.isCompleted { 329 | return 330 | } 331 | 332 | if self.isCancelled { 333 | return 334 | } 335 | 336 | synchronized(callbackLock) { 337 | self.isCompleted = true 338 | self.completedError = error 339 | let copiedErrbacks = self.errbacks 340 | let copiedFinallys = self.finallys 341 | 342 | for holder in copiedErrbacks { 343 | dispatch_async(holder.callbackQueue) { 344 | if !self.isCancelled { 345 | holder.callback(error) 346 | } 347 | } 348 | } 349 | for holder in copiedFinallys { 350 | dispatch_async(holder.callbackQueue) { 351 | holder.callback(self.isCancelled) 352 | } 353 | } 354 | 355 | self.callbacks.removeAll() 356 | self.errbacks.removeAll() 357 | self.finallys.removeAll() 358 | } 359 | 360 | } 361 | } 362 | 363 | public func ==(lhs: Task, rhs: Task) -> Bool 364 | { 365 | return lhs === rhs 366 | } 367 | 368 | // MARK: 369 | public class TaskCompletionSource : NSObject { 370 | public override init() 371 | { 372 | 373 | } 374 | 375 | public let task = Task() 376 | private var cancellationHandlers : [(() -> Void)] = [] 377 | 378 | /** Signal successful completion of the task to all callbacks */ 379 | public func completeWithValue(value: T) 380 | { 381 | self.task.completeWithValue(value) 382 | } 383 | 384 | /** Signal failed completion of the task to all errbacks */ 385 | public func failWithError(error: NSError!) 386 | { 387 | self.task.failWithError(error) 388 | } 389 | 390 | /** Signal completion for this source's task based on another task. */ 391 | public func completeWithTask(task: Task) 392 | { 393 | task.addCallback(on:dispatch_get_global_queue(0, 0), callback: { 394 | (v: T) -> Void in 395 | self.task.completeWithValue(v) 396 | }).addErrorCallback(on:dispatch_get_global_queue(0, 0), callback: { 397 | (e: NSError!) -> Void in 398 | self.task.failWithError(e) 399 | }) 400 | } 401 | 402 | /** If the task is cancelled, your registered handlers will be called. If you'd rather 403 | poll, you can ask task.cancelled. */ 404 | public func onCancellation(callback: () -> Void) 405 | { 406 | synchronized(self) { 407 | self.cancellationHandlers.append(callback) 408 | } 409 | } 410 | 411 | func cancel() { 412 | var handlers: [()->()] = [] 413 | synchronized(self) { () -> Void in 414 | handlers = self.cancellationHandlers 415 | } 416 | for callback in handlers { 417 | callback() 418 | } 419 | } 420 | } 421 | 422 | protocol Cancellable { 423 | func cancel() -> Void 424 | } 425 | 426 | class TaskCallbackHolder 427 | { 428 | init(on queue:dispatch_queue_t, callback: T) { 429 | callbackQueue = queue 430 | self.callback = callback 431 | } 432 | 433 | var callbackQueue : dispatch_queue_t 434 | var callback : T 435 | } 436 | 437 | func synchronized(on: AnyObject, closure: () -> Void) { 438 | objc_sync_enter(on) 439 | closure() 440 | objc_sync_exit(on) 441 | } 442 | 443 | func synchronized(on: NSLock, closure: () -> Void) { 444 | on.lock() 445 | closure() 446 | on.unlock() 447 | } 448 | 449 | func synchronized(on: NSLock, closure: () -> T) -> T { 450 | on.lock() 451 | let r = closure() 452 | on.unlock() 453 | return r 454 | } -------------------------------------------------------------------------------- /Swift/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func slowAddition(a: Int, b: Int, callback: (sum: Int, error: NSError?) -> Void) { 4 | let sum = a + b 5 | callback(sum: sum, error: nil) 6 | } 7 | 8 | func taskize ( 9 | asyncFunc: ( 10 | p1: P1, 11 | p2: P2, 12 | callback: ( 13 | r1: R1, 14 | err: NSError? 15 | ) -> Void 16 | ) -> Void 17 | ) -> (P1, P2) -> Task 18 | { 19 | let source = TaskCompletionSource() 20 | 21 | return { (p1: P1, p2: P2) -> Task in 22 | asyncFunc(p1: p1, p2: p2, { (r1: R1, error: NSError?) -> Void in 23 | source.completeWithValue(r1) 24 | }) 25 | return source.task 26 | } 27 | } 28 | 29 | class Hello { 30 | func slowAddition(a: Int, b: Int, callback: (sum: Int, error: NSError?) -> Void) { 31 | let sum = a + b 32 | callback(sum: sum, error: nil) 33 | } 34 | } 35 | 36 | // Calling a function with three parameters: two ints and a callback. 37 | slowAddition(4, 5) { (sum: Int, error: NSError?) -> Void in 38 | println("Look ma, callback summarized! \(sum)") 39 | } 40 | 41 | // Creating and calling a function with two parameters (two ints) that returns a task! 42 | taskize(slowAddition)(4, 5).addCallback({ (sum: Int) -> () in 43 | println("Look ma, future summarized! \(sum)") 44 | }) 45 | 46 | let taskedAddition = taskize(slowAddition) 47 | let task1 = taskedAddition(4, 5) 48 | let task2 = taskedAddition(7, 8) 49 | Task.awaitAll([task1, task2]).addCallback({ (sums: [Any]) -> Void in 50 | println("The sums! \(sums)") 51 | }) 52 | 53 | 54 | let hello = Hello() 55 | taskize(hello.slowAddition)(4, 6).addCallback({ (sum: Int) -> () in 56 | println("From an object even! \(sum)") 57 | }) 58 | 59 | NSRunLoop.mainRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.1)) -------------------------------------------------------------------------------- /include/SPAsync/NSObject+SPInvocationGrabbing.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface SPInvocationGrabber : NSObject 4 | -(id)initWithObject:(id)obj; 5 | -(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; 6 | @property (readonly, retain, nonatomic) id object; 7 | @property (readonly, retain, nonatomic) NSInvocation *invocation; 8 | @property (nonatomic, copy) void(^afterForwardInvocation)(void); 9 | @property BOOL backgroundAfterForward; 10 | @property BOOL onMainAfterForward; 11 | @property BOOL waitUntilDone; 12 | -(void)invoke; // will release object and invocation 13 | -(void)printBacktrace; 14 | -(void)saveBacktrace; 15 | @end 16 | 17 | @interface NSObject (SPInvocationGrabbing) 18 | -(id)grab; 19 | -(id)grabWithoutStacktrace; 20 | -(id)invokeAfter:(NSTimeInterval)delta; 21 | -(id)nextRunloop; 22 | -(id)inBackground; 23 | -(id)onMainAsync:(BOOL)async; 24 | @end 25 | -------------------------------------------------------------------------------- /include/SPAsync/SPAgent.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /** 4 | Experimental multithreading primitive: An object conforming to SPAgent is not thread safe, 5 | but it runs in its own thread. To perform any of the methods on it, you must first dispatch to its 6 | workQueue. 7 | */ 8 | @protocol SPAgent 9 | @property(nonatomic,readonly) dispatch_queue_t workQueue; 10 | @end 11 | 12 | /// Returns invocation grabber; resulting invocation will be performed on workQueue. Proxied invocation returns an SPTask. 13 | @interface NSObject (SPAgentDo) 14 | - (instancetype)sp_agentAsync; 15 | @end -------------------------------------------------------------------------------- /include/SPAsync/SPAsync.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | -------------------------------------------------------------------------------- /include/SPAsync/SPAsyncNamespacing.h: -------------------------------------------------------------------------------- 1 | /** In order for SPAsync to be embeddable in libraries, all visible symbols must 2 | be namespaced so that they don't clash with other copies of the same library. 3 | For example, if Lookback.framework embeds SPAsync, and Lookback gets linked into 4 | Spotify.app, and Spotify.app also uses SPAsync, Spotify.app must either 5 | 1) use the SPAsync in Lookback; 6 | 2) use the same package manager as Lookback (if everyone in the world used CocoaPods, I would be so happy); 7 | 3) or Lookback could use a namespaced version of SPAsync, which could co-exist 8 | with a version of SPAsync in Spotify.app. 9 | 10 | This header file implements support for #3, which users of SPAsync can optionally use. To use it, 11 | embed the .h and .m files you need in your project, and #define SPASYNC_NAMESPACE to a valid c identifier, 12 | either in your prefix header or with a "Preprocessor Macros" build setting. 13 | 14 | Thanks, wolf! http://rentzsch.tumblr.com/post/40806448108/ns-poor-mans-namespacing-for-objective-c 15 | */ 16 | 17 | #ifndef SPASYNC_NAMESPACE 18 | // Default to using the 'SP' prefix 19 | #define SPASYNC_NAMESPACE SP 20 | #endif 21 | 22 | #define JRNS_CONCAT_TOKENS(a,b) a##b 23 | #define JRNS_EVALUATE(a,b) JRNS_CONCAT_TOKENS(a,b) 24 | #define SPA_NS(original_name) JRNS_EVALUATE(SPASYNC_NAMESPACE, original_name) 25 | -------------------------------------------------------------------------------- /include/SPAsync/SPAwait.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPAwait.h 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2013-01-30. 6 | // 7 | // 8 | 9 | /** @file SPAwait 10 | @abstract Emulates the behavior of the 'await' keyword in C#, letting you pause execution of a method, waiting for a value. 11 | @example 12 | - (SPTask *)uploadThing:(NSData*)thing 13 | { 14 | // Variables you want to use need to be declared as __block at the top of the method. 15 | __block NSData *encrypted, *confirmation; 16 | // Immediately after, you need to state that you are starting an async method body 17 | SPAsyncMethodBegin 18 | 19 | // Do work like normal 20 | [self prepareFor:thing]; 21 | 22 | // When you make a call to something returning an SPTask, you can wait for its value. The method 23 | // will actually return at this point, and resume on the next line when the encrypted value is available. 24 | encrypted = SPAsyncAwait([_encryptor encrypt:thing]); 25 | 26 | // Keep doing work as normal. This line might run much later, as we have suspended and waited for the encrypted 27 | // value. 28 | hash ≈ [encrypted hash]; 29 | [_network send:encrypted]; 30 | [_network send:hash]; 31 | 32 | confirmation = SPAsyncAwait([_network read:1]); 33 | 34 | // Returning will complete the SPTask, sending this value to all the callbacks registered with it 35 | return @([confirmation bytes][0] == 0); 36 | 37 | // You must also clean up the async method body manually. 38 | SPAsyncMethodEnd 39 | } 40 | */ 41 | 42 | /** @macro SPAsyncMethodBegin 43 | @abstract Place at the beginning of an async method, after 44 | variable declarations 45 | */ 46 | #define SPAsyncMethodBegin \ 47 | __block SPAwaitCoroutine *__awaitCoroutine = [SPAwaitCoroutine new]; \ 48 | __block __weak SPAwaitCoroutine *__weakAwaitCoroutine = __awaitCoroutine; \ 49 | [__awaitCoroutine setBody:^ id (int resumeAt) { \ 50 | switch (resumeAt) { \ 51 | case 0:; 52 | 53 | /** @macro SPAsyncAwait 54 | @abstract Pauses the execution of the calling method, waiting for the value in 55 | 'awaitable' to be available. 56 | */ 57 | 58 | #define SPAsyncAwait(awaitable) \ 59 | ({ \ 60 | [awaitable addCallback:^(id value) { \ 61 | __weakAwaitCoroutine.lastAwaitedValue = value; \ 62 | [__weakAwaitCoroutine resumeAt:__LINE__]; \ 63 | } on:[__weakAwaitCoroutine queueFor:self]]; \ 64 | return [SPAwaitCoroutine awaitSentinel]; \ 65 | case __LINE__:; \ 66 | __weakAwaitCoroutine.lastAwaitedValue; \ 67 | }) 68 | 69 | /** @macro SPAsyncMethodEnd 70 | @abstract Place at the very end of an async method 71 | */ 72 | #define SPAsyncMethodEnd \ 73 | } /* switch ends here */ \ 74 | return nil; \ 75 | }]; /* setBody ends here */ \ 76 | [__weakAwaitCoroutine resumeAt:0]; \ 77 | return [__weakAwaitCoroutine task]; 78 | 79 | #import 80 | 81 | @class SPTask; 82 | typedef id(^SPAwaitCoroutineBody)(int resumeAt); 83 | 84 | /** @class SPAwaitCoroutine 85 | @abstract Private implementation detail of SPAwait 86 | */ 87 | @interface SPAwaitCoroutine : NSObject 88 | // if returned from body, the method has not completed 89 | + (id)awaitSentinel; 90 | @property(nonatomic,retain) id lastAwaitedValue; 91 | 92 | - (void)setBody:(SPAwaitCoroutineBody)body; 93 | - (void)resumeAt:(int)line; 94 | - (void)finish; 95 | 96 | - (SPTask*)task; 97 | 98 | /// works out a suitable queue to continue running on 99 | - (dispatch_queue_t)queueFor:(id)object; 100 | @end 101 | -------------------------------------------------------------------------------- /include/SPAsync/SPKVOTask.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface SPA_NS(KVOTask) : SPA_NS(Task) 4 | /** 5 | * Create a task that will complete once 'keypath' on 'object' becomes 'value'. 6 | * 7 | * @param value Value to await. Task will complete when [object valueForKeyPath:keypath isEqual:value] 8 | * @param object Object to observe. Will be retained. 9 | * @param keyPath keyPath to check for changes. 10 | */ 11 | + (id)awaitValue:(id)value onObject:(id)object forKeyPath:(NSString*)keyPath; 12 | @end 13 | -------------------------------------------------------------------------------- /include/SPAsync/SPTask.h: -------------------------------------------------------------------------------- 1 | // 2 | // SPTask.h 3 | // SPAsync 4 | // 5 | // Created by Joachim Bengtsson on 2012-12-26. 6 | 7 | #import 8 | #import 9 | 10 | #pragma mark Boring build time details (scroll down for actual interface) 11 | /* 12 | For wrapping SPTask in binary libraries (read: Lookback) without conflicting with SPTask's existance in the app that 13 | uses the library, the binary library can change the name of this class to something else (such as LBTask) by defining 14 | the macro `-DSPASYNC_NAMESPACE=LB` at build time in that project. 15 | */ 16 | @class SPA_NS(Task); 17 | 18 | /* 19 | For backwards compatibility with ObjC before lightweight generics, these macros allow us to define 20 | SPTask with generics support only if it's available. In this case, whenever you see SPA_GENERIC_TYPE(PromisedType) 21 | as a parameter or return value, pretend that it just says `id`. 22 | */ 23 | #if __has_feature(objc_generics) 24 | # define SPA_GENERIC(class, ...) class<__VA_ARGS__> 25 | # define SPA_GENERIC_TYPE(type) type 26 | #else 27 | # define SPA_GENERIC(class, ...) class 28 | # define SPA_GENERIC_TYPE(type) id 29 | #endif 30 | 31 | #pragma mark - SPTask and friends! 32 | 33 | /** @class SPTask 34 | @abstract Wraps any asynchronous operation that someone might want to know the result of in the future. 35 | 36 | You can use SPTask in any place where you'd traditionally use a callback. 37 | 38 | Instead of doing a Pyramid Of Doom like this: 39 | 40 | [thing fetchNetworkThingie:url callback:^(NSData *data) { 41 | [AsyncJsonParser parse:data callback:^(NSDictionary *parsed) { 42 | [_database updateWithData:parsed callback:^(NSError *err) { 43 | if(err) 44 | ... and it just goes on... 45 | }]; 46 | // don't forget error handling here 47 | }]; 48 | // don't forget error handling here too 49 | }]; 50 | 51 | you can get a nice chain of things like this: 52 | 53 | [[[[[thing fetchNetworkThingie:url] chain:^(NSData *data) { 54 | return [AsyncJsonParser parse:data]; 55 | }] chain:^(NSDictionary *parsed) { 56 | return [_database updateWithData:data]; 57 | }] addCallback:^{ 58 | NSLog(@"Yay!"); 59 | }] addErrorCallback:^(NSError *error) { 60 | NSLog(@"An error caught anywhere along the line can be handled here in this one place: %@", error); 61 | }]; 62 | 63 | That's nicer, yeah? 64 | 65 | By using task trees like this, you can make your interfaces prettier, make cancellation easier, centralize your 66 | error handling, make it easier to work with dispatch_queues, and so on. 67 | */ 68 | @interface SPA_GENERIC(SPA_NS(Task), PromisedType) : NSObject 69 | 70 | typedef void(^SPTaskCallback)(SPA_GENERIC_TYPE(PromisedType) value); 71 | typedef void(^SPTaskErrback)(NSError *error); 72 | typedef void(^SPTaskFinally)(BOOL cancelled); 73 | typedef id(^SPTaskThenCallback)(SPA_GENERIC_TYPE(PromisedType) value); 74 | typedef id(^SPTaskWorkGeneratingCallback)(void); 75 | typedef SPA_NS(Task*)(^SPTaskTaskGeneratingCallback)(void); 76 | typedef SPA_NS(Task)*(^SPTaskChainCallback)(SPA_GENERIC_TYPE(PromisedType) value); 77 | typedef SPA_NS(Task)*(^SPTaskRecoverCallback)(NSError *error); 78 | 79 | 80 | /** @method addCallback:on: 81 | Add a callback to be called async when this task finishes, including the queue to 82 | call it on. If the task has already finished, the callback will be called immediately 83 | (but still asynchronously) 84 | @return self, in case you want to add more call/errbacks on the same task */ 85 | - (instancetype)addCallback:(SPTaskCallback)callback on:(dispatch_queue_t)queue; 86 | 87 | /** @method addCallback: 88 | @discussion Like addCallback:on:, but defaulting to the main queue. */ 89 | - (instancetype)addCallback:(SPTaskCallback)callback; 90 | 91 | /** @method addErrorCallback:on: 92 | Like callback, but for when the task fails 93 | @return self, in case you want to add more call/errbacks on the same task */ 94 | - (instancetype)addErrorCallback:(SPTaskErrback)errback on:(dispatch_queue_t)queue; 95 | 96 | /** @method addErrorCallback: 97 | @discussion Like addErrorCallback:on:, but defaulting to the main queue. */ 98 | - (instancetype)addErrorCallback:(SPTaskErrback)errback; 99 | 100 | /** @method addFinally:on: 101 | Called on both success, failure and cancellation. 102 | @return self, in case you want to add more call/errbacks on the same task */ 103 | - (instancetype)addFinallyCallback:(SPTaskFinally)finally on:(dispatch_queue_t)queue; 104 | 105 | /** @method addFinallyCallback:on: 106 | @discussion Like addFinallyCallback:on:, but defaulting to the main queue. */ 107 | - (instancetype)addFinallyCallback:(SPTaskFinally)finally; 108 | 109 | /** @method awaitAll: 110 | @return A task that will complete when all the given tasks have completed. 111 | */ 112 | + (instancetype)awaitAll:(NSArray*)tasks; 113 | 114 | @end 115 | 116 | 117 | @interface SPA_NS(Task) (SPTaskCancellation) 118 | /** @property cancelled 119 | Whether someone has explicitly cancelled this task. 120 | */ 121 | @property(getter=isCancelled,readonly) BOOL cancelled; 122 | 123 | /** @method cancel 124 | Tells the owner of this task to cancel the operation if possible. This method also 125 | tries to cancel callback calling, but unless you're on the same queue as the callback 126 | being cancelled, it might trigger before the invocation of 'cancel' completes. 127 | */ 128 | - (void)cancel; 129 | @end 130 | 131 | 132 | @interface SPA_NS(Task) (SPTaskExtended) 133 | 134 | /** @method then:on: 135 | Add a callback, and return a task that represents the return value of that 136 | callback. Useful for doing background work with the result of some other task. 137 | This task will fail if the parent task fails, chaining them together. 138 | @return A new task to be executed when 'self' completes, representing 139 | the work in 'worker' 140 | */ 141 | - (instancetype)then:(SPTaskThenCallback)worker on:(dispatch_queue_t)queue; 142 | 143 | /** @method chain:on: 144 | Add a callback that will be used to provide further work to be done. The 145 | returned SPTask represents this work-to-be-provided. 146 | @return A new task to be executed when 'self' completes, representing 147 | the work provided by 'worker' 148 | */ 149 | - (instancetype)chain:(SPTaskChainCallback)chainer on:(dispatch_queue_t)queue; 150 | 151 | /** @method chain 152 | @abstract Convenience for asynchronously waiting on a task that returns a task. 153 | @discussion Equivalent to [task chain:^SPTask*(SPTask *task) { return task; } ...] 154 | @example sp_agentAsync returns a task. When run on a method that returns a task, 155 | you want to wait on the latter, rather than the former. Thus, you chain: 156 | [[[[foo sp_agentAsync] fetchSomething] chain] addCallback:^(id something) {}]; 157 | ... to first convert `Task>` into `Task` through chain, 158 | then into `Thing` through addCallback. 159 | */ 160 | - (instancetype)chain; 161 | 162 | /** @method recover:on: 163 | If the receiver fails, this callback will be called as if it was an error callback. 164 | If it returns a new SPTask, its completion will determine the completion of the returned 165 | task. If it returns nil, the original error will be propagated. */ 166 | - (instancetype)recover:(SPTaskRecoverCallback)recoverer on:(dispatch_queue_t)queue; 167 | @end 168 | 169 | 170 | @interface SPA_GENERIC(SPA_NS(Task), PromisedType) (SPTaskConvenience) 171 | 172 | /** @method performWork:onQueue: 173 | Convenience method to do work on a specified queue, completing the task with the value 174 | returned from the block. */ 175 | + (instancetype)performWork:(SPTaskWorkGeneratingCallback)work onQueue:(dispatch_queue_t)queue; 176 | /** @method fetchWork:onQueue: 177 | Like performWork:onQueue, but returning a task from the block that we'll wait on before 178 | completing the task. */ 179 | + (instancetype)fetchWork:(SPTaskTaskGeneratingCallback)work onQueue:(dispatch_queue_t)queue; 180 | 181 | /** @method delay:completeValue: 182 | Create a task that will complete after the specified time interval and 183 | with specified complete value. 184 | @return A new task delayed task. 185 | */ 186 | + (instancetype)delay:(NSTimeInterval)delay completeValue:(SPA_GENERIC_TYPE(PromisedType))completeValue; 187 | 188 | /** @method delay: 189 | Create a task that will complete after the specified time interval with 190 | complete value nil. 191 | @return A new task delayed task. 192 | */ 193 | + (instancetype)delay:(NSTimeInterval)delay; 194 | 195 | /** @method completedTask: 196 | Convenience method for when an asynchronous caller happens to immediately have an 197 | available value. 198 | @return A new task with a completed value. */ 199 | + (instancetype)completedTask:(SPA_GENERIC_TYPE(PromisedType))completeValue; 200 | 201 | /** @method failedTask: 202 | Convenience method for when an asynchronous caller happens to immediately knows it 203 | will fail with a specific failure. 204 | @return A new task with an associated error. */ 205 | + (instancetype)failedTask:(NSError*)failure; 206 | 207 | @end 208 | 209 | /** @class SPTaskCompletionSource 210 | Task factory for a single task that the caller knows how to complete/fail. 211 | */ 212 | @interface SPA_GENERIC(SPA_NS(TaskCompletionSource), PromisedType) : NSObject 213 | /** The task that this source can mark as completed. */ 214 | - (SPA_GENERIC(SPA_NS(Task), PromisedType)*)task; 215 | 216 | /** 217 | Signal successful completion of the task to all callbacks. Asserts 218 | if you try to complete or fail more than once. 219 | NOTE: If you pass an NSError, this call will forward to failWithError: 220 | */ 221 | - (void)completeWithValue:(SPA_GENERIC_TYPE(PromisedType))value; 222 | /** 223 | Signal failed completion of the task to all errbacks. Asserts if you 224 | try to complete or fail more than once. 225 | */ 226 | - (void)failWithError:(NSError*)error; 227 | /** 228 | Same as failWithError:, except that if you send YES to ignoreSubsequentValues, 229 | subsequent errors will be ignored instead of asserting. Use if it's not an error 230 | in your code to fail for multiple different and it's okay if upstream code doesn't 231 | know which one of the errors is the "real" or "important" one. 232 | */ 233 | - (void)failWithError:(NSError*)error ignoreIfAlreadyCompleted:(BOOL)ignoreSubsequentValues; 234 | 235 | /** Signal completion for this source's task based on another task. */ 236 | - (void)completeWithTask:(SPA_GENERIC(SPA_NS(Task), PromisedType)*)task; 237 | 238 | /** Returns a block that when called calls completeWithValue:nil. 239 | @example 240 | SPTaskCompletionSource *source = [SPTaskCompletionSource new]; 241 | [_assetWriter finishWritingWithCompletionHandler:[source voidResolver]]; 242 | return source.task; // task triggers callbacks when asset writer finishes writing 243 | */ 244 | - (dispatch_block_t)voidResolver; 245 | /** Returns a block that when called calls completeWithValue: with its first parameter*/ 246 | - (void(^)(SPA_GENERIC_TYPE(PromisedType)))resolver; 247 | 248 | /** If the task is cancelled, your registered handlers will be called. If you'd rather 249 | poll, you can ask task.cancelled. */ 250 | - (void)addCancellationCallback:(void(^)(void))cancellationCallback; 251 | @end 252 | 253 | 254 | @interface SPA_NS(Task) (Testing) 255 | 256 | /** Check a task's completed state. 257 | @warning This is intended for testing purposes only, and it is not recommended to use 258 | this property as a means to do micro optimizations for synchronous code. 259 | 260 | @return Whether or not the receiver has been completed with a value or error. */ 261 | @property(getter=isCompleted,readonly) BOOL completed; 262 | 263 | @end 264 | 265 | /** Convenience holder of a callback and the queue that the callback should be called on */ 266 | @interface SPA_NS(CallbackHolder) : NSObject 267 | - (id)initWithCallback:(SPTaskCallback)callback onQueue:(dispatch_queue_t)callbackQueue; 268 | @property(nonatomic,assign) dispatch_queue_t callbackQueue; 269 | @property(nonatomic,copy) SPTaskCallback callback; 270 | @end 271 | 272 | 273 | @interface SPA_NS(Task) (Deprecated) 274 | /** @discussion use addErrorCallback:: instead */ 275 | - (instancetype)addErrback:(SPTaskErrback)errback on:(dispatch_queue_t)queue; 276 | 277 | /** @discussion Use addFinallyCallback:: instead */ 278 | - (instancetype)addFinally:(SPTaskFinally)finally on:(dispatch_queue_t)queue; 279 | @end 280 | -------------------------------------------------------------------------------- /include/SPAsync/UIKit/UIKit.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import -------------------------------------------------------------------------------- /include/SPAsync/UIKit/UIScrollView.h: -------------------------------------------------------------------------------- 1 | #import 2 | @class SPTask; 3 | 4 | @interface UIScrollView (SPAsyncAnimation) 5 | - (SPTask*)task_setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; 6 | @end -------------------------------------------------------------------------------- /include/SPAsync/UIKit/UIView.h: -------------------------------------------------------------------------------- 1 | #import 2 | @class SPTask; 3 | 4 | @interface UIView (SPAsyncAnimation) 5 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations; 6 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations; 7 | + (SPTask*)task_animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations; 8 | @end --------------------------------------------------------------------------------