├── IKAsyncable.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── IKAsyncableApp ├── AppDelegate.swift ├── MyDelegate.swift ├── MyDataSource.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.swift ├── MyCell.swift ├── Info.plist └── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── IKAsyncable ├── IKAsyncOperationState.swift ├── IKAsyncable.swift ├── IKAsyncOperationManager.swift ├── IKAsyncOperation.swift ├── IKAsyncTableViewDelegate.swift └── IKAsyncCollectionViewDelegate.swift ├── IKAsyncableTests ├── Info.plist ├── IKAsyncOperationManagerTests.swift └── IKAsyncOperationTests.swift ├── .gitignore └── README.md /IKAsyncable.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IKAsyncableApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // IKAsyncTableViewDelegate_temp 4 | // 5 | // Created by Ian Keen on 27/08/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 15 | // Override point for customization after application launch. 16 | return true 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncOperationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncOperationState.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 1/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum IKAsyncOperationState : CustomStringConvertible { 12 | case InProgress 13 | case Complete(Any) 14 | case Failed(NSError) 15 | 16 | public var description: String { 17 | switch self { 18 | case .InProgress: return "In Progress" 19 | case .Complete(let value): return "Complete: \(value)" 20 | case .Failed(let error): return "Failure: \(error)" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /IKAsyncableApp/MyDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyDelegate.swift 3 | // 4 | // Created by Ian Keen on 1/09/2015. 5 | // Copyright (c) 2015 Mustard. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public class MyDelegate: IKAsyncTableViewDelegate { 11 | var urls: [String]? 12 | 13 | override public func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { 14 | if let cell = cell as? MyCell, let url = self.urls?[indexPath.row] { 15 | cell.setup(url) 16 | } 17 | 18 | super.tableView(tableView, willDisplayCell: cell, forRowAtIndexPath: indexPath) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /IKAsyncableApp/MyDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyDataSource.swift 3 | // 4 | // Created by Ian Keen on 1/09/2015. 5 | // Copyright (c) 2015 Mustard. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class MyDataSource: NSObject, UITableViewDataSource { 11 | var urls: [String]? 12 | 13 | func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } 14 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.urls?.count ?? 0 } 15 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 16 | return tableView.dequeueReusableCellWithIdentifier("MyCell", forIndexPath: indexPath) as! MyCell 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncable.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 27/08/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias IKAsyncSuccess = (Any) -> Void 12 | public typealias IKAsyncFailure = (NSError) -> Void 13 | public typealias IKAsyncOperationClosure = (success: IKAsyncSuccess, failure: IKAsyncFailure) -> Void 14 | public typealias IKAsyncStateChange = (IKAsyncOperation) -> Void 15 | 16 | public protocol IKAsyncableManager { 17 | func resetOperation(asyncable: IKAsyncable) 18 | } 19 | 20 | public protocol IKAsyncable { 21 | func ikAsyncOperation() -> IKAsyncOperationClosure 22 | func ikAsyncOperationState(manager: IKAsyncableManager, state: IKAsyncOperationState) 23 | } 24 | -------------------------------------------------------------------------------- /IKAsyncableTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | ### Objective-C ### 4 | # Xcode 5 | # 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.xcuserstate 22 | 23 | # CocoaPods 24 | # 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Pods/ 30 | 31 | 32 | ### OSX ### 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Icon must end with two \r 38 | Icon 39 | 40 | 41 | # Thumbnails 42 | ._* 43 | 44 | # Files that might appear on external disk 45 | .Spotlight-V100 46 | .Trashes 47 | 48 | # Directories potentially created on remote AFP share 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk -------------------------------------------------------------------------------- /IKAsyncableApp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /IKAsyncableApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 4 | // Created by Ian Keen on 27/08/2015. 5 | // Copyright (c) 2015 Mustard. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | @IBOutlet private var tableView: UITableView! 12 | @IBOutlet private var dataSource: MyDataSource! 13 | @IBOutlet private var delegate: MyDelegate! 14 | 15 | override func viewWillAppear(animated: Bool) { 16 | super.viewWillAppear(animated) 17 | self.getPhotos { urls in 18 | self.dataSource.urls = urls 19 | self.delegate.urls = urls 20 | self.tableView.reloadData() 21 | } 22 | } 23 | 24 | let apiUrl = "http://jsonplaceholder.typicode.com/photos" 25 | private func getPhotos(result: [String] -> Void) { 26 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 27 | if 28 | let url = NSURL(string: self.apiUrl), 29 | let data = NSData(contentsOfURL: url), 30 | let object = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSArray, 31 | let urls = object.valueForKeyPath("url") as? [String] { 32 | dispatch_async(dispatch_get_main_queue()) { 33 | result(urls) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /IKAsyncableApp/MyCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyCell.swift 3 | // 4 | // Created by Ian Keen on 1/09/2015. 5 | // Copyright (c) 2015 Mustard. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class MyCell : UITableViewCell, IKAsyncable { 11 | @IBOutlet private var imgView: UIImageView! 12 | 13 | private var url: String! 14 | func setup(url: String) { 15 | self.url = url 16 | } 17 | 18 | func ikAsyncOperation() -> IKAsyncOperationClosure { 19 | return { success, failure in 20 | if 21 | let url = NSURL(string: self.url), 22 | let data = NSData(contentsOfURL: url), 23 | let image = UIImage(data: data) { 24 | success(image) 25 | 26 | } else { 27 | failure(NSError(domain: "domain", code: 0, userInfo: [NSLocalizedDescriptionKey: "IMAGE"])) 28 | } 29 | } 30 | } 31 | func ikAsyncOperationState(manager: IKAsyncableManager, state: IKAsyncOperationState) { 32 | self.imgView.image = nil 33 | 34 | let color: UIColor 35 | switch state { 36 | case .InProgress: 37 | color = .yellowColor() 38 | case .Complete(let result): 39 | self.imgView.image = result as? UIImage 40 | color = .whiteColor() 41 | case .Failed(_): 42 | color = .redColor() 43 | //manager.resetOperation(self) 44 | } 45 | 46 | self.backgroundColor = color 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncOperationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncOperationManager.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 1/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class IKAsyncOperationManager : NSObject { 12 | //MARK : - Constants 13 | public static let UnlimitedRetries = Int.max 14 | 15 | //MARK : - Internal Properties 16 | internal var operations = [NSIndexPath: IKAsyncOperation]() 17 | internal var stateChange: IKAsyncStateChange? 18 | 19 | //MARK : - Public Properties 20 | @IBInspectable public var maxNumberOfFailures: Int = 3 21 | 22 | //MARK : - Public Functions 23 | public func addOperationIfNeeded(indexPath: NSIndexPath, operation: IKAsyncOperationClosure) -> IKAsyncOperation { 24 | if let asyncOperation = self.operations[indexPath] { 25 | return asyncOperation 26 | 27 | } else { 28 | let newOperation = IKAsyncOperation(indexPath: indexPath, operation: operation) { op in 29 | self.stateChange?(op) 30 | } 31 | self.operations[indexPath] = newOperation 32 | return newOperation 33 | } 34 | } 35 | public func resetOperations() { 36 | self.operations.removeAll() 37 | } 38 | 39 | //MARK : - Internal - Operations 40 | internal func handleAsyncableState(indexPath: NSIndexPath) { 41 | if let operation = self.operations[indexPath] { 42 | operation.performOperation(self.maxNumberOfFailures) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /IKAsyncableApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | NSAppTransportSecurity 45 | 46 | NSAllowsArbitraryLoads 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncOperation.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 1/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class IKAsyncOperation { 12 | //MARK : - Public Properties 13 | let operation: IKAsyncOperationClosure 14 | let indexPath: NSIndexPath 15 | 16 | private(set) var state: IKAsyncOperationState? { 17 | didSet(newValue) { 18 | self.dispatchStateOnMainThread() 19 | } 20 | } 21 | 22 | //MARK : - Private Properties 23 | private var failureCount: Int = 0 24 | private let stateChange: IKAsyncStateChange 25 | 26 | //MARK : - Lifecycle 27 | init(indexPath: NSIndexPath, operation: IKAsyncOperationClosure, stateChange: IKAsyncStateChange) { 28 | self.indexPath = indexPath 29 | self.stateChange = stateChange 30 | self.operation = operation 31 | self.dispatchStateOnMainThread() 32 | } 33 | 34 | //MARK : - Public Functions 35 | public func performOperation(maxFailures: Int) { 36 | if let _ = self.state { 37 | self.dispatchStateOnMainThread() 38 | } else { 39 | self.executeOperation(maxFailures) 40 | } 41 | } 42 | 43 | //MARK : - Private Functions 44 | private func executeOperation(maxFailures: Int) { 45 | self.state = .InProgress 46 | 47 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { 48 | self.operation( 49 | success: { result in 50 | self.state = .Complete(result) 51 | 52 | }, failure: { error in 53 | self.failureCount++ 54 | if (maxFailures != IKAsyncOperationManager.UnlimitedRetries && 55 | self.failureCount >= maxFailures) { 56 | self.state = .Failed(error) 57 | } else { 58 | self.executeOperation(maxFailures) 59 | } 60 | } 61 | ) 62 | } 63 | } 64 | private func dispatchStateOnMainThread() { 65 | if (NSThread.currentThread().isMainThread) { 66 | self.stateChange(self) 67 | } else { 68 | dispatch_async(dispatch_get_main_queue()) { 69 | self.stateChange(self) 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /IKAsyncableTests/IKAsyncOperationManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncOperationManagerTests.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ryan Arana on 9/1/15. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class IKAsyncOperationManagerTests: XCTestCase { 13 | 14 | var mgr = IKAsyncOperationManager() 15 | override func setUp() { 16 | super.setUp() 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | mgr = IKAsyncOperationManager() 19 | } 20 | 21 | override func tearDown() { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | super.tearDown() 24 | } 25 | 26 | func testAddOperationIfNeeded_whenGivenANewIndexPath_shouldAddTheOperationToTheCache() { 27 | // given 28 | let closure: IKAsyncOperationClosure = { success, failure in } 29 | let path = NSIndexPath(forItem: 0, inSection: 0) 30 | 31 | // sanity check 32 | XCTAssertEqual(mgr.operations.count, 0, "There should be nothing in the cache to start with") 33 | 34 | // when 35 | _ = mgr.addOperationIfNeeded(path, operation: closure) 36 | 37 | // then 38 | XCTAssertEqual(mgr.operations.count, 1, "There should be 1 operation in the cache after add the operation") 39 | } 40 | 41 | func testAddOperationIfNeeded_whenGivenAnExistingIndexPath_shouldNotAddTheOperationToTheCache() { 42 | // given 43 | let closure1: IKAsyncOperationClosure = { success, failure in } 44 | let path1 = NSIndexPath(forItem: 0, inSection: 0) 45 | let closure2: IKAsyncOperationClosure = { success, failure in } 46 | let path2 = NSIndexPath(forItem: 0, inSection: 0) 47 | 48 | // sanity check 49 | XCTAssertEqual(mgr.operations.count, 0, "There should be nothing in the cache to start with") 50 | 51 | // when 52 | _ = mgr.addOperationIfNeeded(path1, operation: closure1) 53 | _ = mgr.addOperationIfNeeded(path2, operation: closure2) 54 | 55 | // then 56 | XCTAssertEqual(mgr.operations.count, 1, "There should be 1 operation in the cache after adding two operations with the same path") 57 | } 58 | 59 | func testResetOperations_shouldRemoveAllOperations() { 60 | // given 61 | let closure: IKAsyncOperationClosure = { success, failure in } 62 | mgr.addOperationIfNeeded(NSIndexPath(forItem: 0, inSection: 0), operation: closure) 63 | 64 | // sanity check 65 | XCTAssertEqual(mgr.operations.count, 1, "There should be 1 operation in the cache after adding 1") 66 | 67 | // when 68 | mgr.resetOperations() 69 | 70 | // then 71 | XCTAssertEqual(mgr.operations.count, 0, "There should be no operations in the cache after resetting them") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncTableViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncTableViewDelegate.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 1/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class IKAsyncTableViewDelegate : IKAsyncOperationManager, IKAsyncableManager, UITableViewDelegate { 12 | //MARK : - Private Properties 13 | private weak var tableView: UITableView? 14 | 15 | //MARK : - Public Functions 16 | public func resetOperation(asyncable: IKAsyncable) { 17 | if 18 | let asyncable = asyncable as? UITableViewCell, 19 | let tableView = self.tableView, 20 | let indexPath = tableView.indexPathForCell(asyncable) { 21 | self.operations.removeValueForKey(indexPath) 22 | 23 | if let cell = tableView.cellForRowAtIndexPath(indexPath) { 24 | self.handleCell(cell, indexPath: indexPath) 25 | } 26 | } 27 | } 28 | override public func resetOperations() { 29 | super.resetOperations() 30 | 31 | if let tableView = self.tableView, 32 | let indexPaths = tableView.indexPathsForVisibleRows { 33 | for indexPath in indexPaths { 34 | if let cell = tableView.cellForRowAtIndexPath(indexPath) { 35 | self.handleCell(cell, indexPath: indexPath) 36 | } 37 | } 38 | } 39 | } 40 | public func resetOperation(indexPath: NSIndexPath) { 41 | self.operations.removeValueForKey(indexPath) 42 | 43 | if let tableView = self.tableView { 44 | if let cell = tableView.cellForRowAtIndexPath(indexPath) { 45 | self.handleCell(cell, indexPath: indexPath) 46 | } 47 | } 48 | } 49 | 50 | //MARK : - UITableViewDelegate 51 | public func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { 52 | self.tableView = tableView 53 | self.stateChange = self.operationStateChanged 54 | 55 | self.handleCell(cell, indexPath: indexPath) 56 | } 57 | 58 | //MARK: - Private Functions 59 | private func handleCell(cell: UITableViewCell, indexPath: NSIndexPath) { 60 | if let cell = cell as? IKAsyncable { 61 | let operation = self.addOperationIfNeeded(indexPath, operation: cell.ikAsyncOperation()) 62 | self.handleAsyncableState(indexPath) 63 | self.dispatchState(operation, asyncable: cell) 64 | } 65 | } 66 | private func operationStateChanged(operation: IKAsyncOperation) { 67 | let indexPath = NSIndexPath(forRow: operation.indexPath.row, inSection: operation.indexPath.section) 68 | 69 | if 70 | let tableView = self.tableView, 71 | let cell = tableView.cellForRowAtIndexPath(indexPath), 72 | let asyncable = cell as? IKAsyncable { 73 | self.dispatchState(operation, asyncable: asyncable) 74 | } 75 | } 76 | private func dispatchState(operation: IKAsyncOperation, asyncable: IKAsyncable) { 77 | if let state = operation.state { 78 | asyncable.ikAsyncOperationState(self, state: state) 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /IKAsyncable/IKAsyncCollectionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncCollectionViewDelegate.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 1/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS, introduced=8.0) 12 | public class IKAsyncCollectionViewDelegate : IKAsyncOperationManager, IKAsyncableManager, UICollectionViewDelegate { 13 | //MARK : - Private Properties 14 | private weak var collectionView: UICollectionView? 15 | 16 | //MARK : - Public Functions 17 | public func resetOperation(asyncable: IKAsyncable) { 18 | if 19 | let asyncable = asyncable as? UICollectionViewCell, 20 | let collectionView = self.collectionView, 21 | let indexPath = collectionView.indexPathForCell(asyncable) { 22 | self.operations.removeValueForKey(indexPath) 23 | 24 | if let cell = collectionView.cellForItemAtIndexPath(indexPath) { 25 | self.handleCell(cell, indexPath: indexPath) 26 | } 27 | } 28 | } 29 | override public func resetOperations() { 30 | super.resetOperations() 31 | 32 | if let collectionView = self.collectionView { 33 | for indexPath in collectionView.indexPathsForVisibleItems() { 34 | if let cell = collectionView.cellForItemAtIndexPath(indexPath) { 35 | self.handleCell(cell, indexPath: indexPath) 36 | } 37 | } 38 | } 39 | } 40 | public func resetOperation(indexPath: NSIndexPath) { 41 | self.operations.removeValueForKey(indexPath) 42 | 43 | if let collectionView = self.collectionView { 44 | if let cell = collectionView.cellForItemAtIndexPath(indexPath) { 45 | self.handleCell(cell, indexPath: indexPath) 46 | } 47 | } 48 | } 49 | 50 | //MARK : - UICollectionViewDelegate 51 | public func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) { 52 | self.collectionView = collectionView 53 | self.stateChange = self.operationStateChanged 54 | 55 | self.handleCell(cell, indexPath: indexPath) 56 | } 57 | 58 | //MARK: - Private Functions 59 | private func handleCell(cell: UICollectionViewCell, indexPath: NSIndexPath) { 60 | if let cell = cell as? IKAsyncable { 61 | let operation = self.addOperationIfNeeded(indexPath, operation: cell.ikAsyncOperation()) 62 | self.handleAsyncableState(indexPath) 63 | self.dispatchState(operation, asyncable: cell) 64 | } 65 | } 66 | private func operationStateChanged(operation: IKAsyncOperation) { 67 | let indexPath = NSIndexPath(forRow: operation.indexPath.row, inSection: operation.indexPath.section) 68 | 69 | if 70 | let collectionView = self.collectionView, 71 | let cell = collectionView.cellForItemAtIndexPath(indexPath), 72 | let asyncable = cell as? IKAsyncable { 73 | self.dispatchState(operation, asyncable: asyncable) 74 | } 75 | } 76 | private func dispatchState(operation: IKAsyncOperation, asyncable: IKAsyncable) { 77 | if let state = operation.state { 78 | asyncable.ikAsyncOperationState(self, state: state) 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IKAsyncable 2 | An (almost) drop in UITableViewDelegate & UICollectionViewDelegate for better async cell operations 3 | 4 | ## Why? 5 | This is the basically the result of my blog post on how to properly 6 | handle async tasks in table views: 7 | [Make your UIViewController Awesynchronous!](http://blog.ios-developers.io/make-your-uiviewcontroller-awesynchronous/). 8 | 9 | ## How to use 10 | Getting up and running is a peice of cake... No matter if you are working with a new or existing codebase! 11 | If you currently don't use a delegate in your `UITableView` or `UICollectionView` 12 | you can simply hook up an instance of `IKAsyncTableViewDelegate` or `IKAsyncCollectionViewDelegate` 13 | 14 | otherwise... 15 | 16 | ### UITableViewDelegate 17 | 18 | * Make your existing `UITableViewDelegate` inherit from `IKAsyncTableViewDelegate` 19 | * If you are using the `willDisplayCell` method you just need to mark it `overrides` and call the super method 20 | 21 | ### UITableViewCell 22 | 23 | * Have your cell conform to the `IKAsyncable` protocol 24 | * Implement `func ikAsyncOperation() -> IKAsyncOperationClosure` and place all your async operations here. 25 | This will allow the code to stay with the cell, but it will be executed outside the lifecycle of the cell. 26 | The code is automatically executed on a background thread so you don't need to do any GCD wrapping. 27 | The sucessful result or error from failure will be cached for the next time the cell needs it. 28 | 29 | ```swift 30 | func ikAsyncOperation() -> IKAsyncOperationClosure { 31 | return { success, failure in 32 | /* 33 | perform async code here 34 | and call success(result) or failure(error) 35 | depending on the outcome. 36 | */ 37 | } 38 | } 39 | ``` 40 | 41 | * Implement `func ikAsyncOperationState(state: IKAsyncOperationState)` to be notified when the state of the operation changes. 42 | This will be called each time the state changes; if the cell is visible it will receive the change right away, 43 | otherwise it will get the state the next time the cell is visible. This is always delivered safely on the main thread. 44 | 45 | ```swift 46 | func ikAsyncOperationState(state: IKAsyncOperationState) { 47 | let color: UIColor 48 | switch state { 49 | case .InProgress: color = .yellowColor() 50 | case .Complete(let result): color = .greenColor() 51 | case .Failed(let error): color = .redColor() 52 | } 53 | 54 | self.backgroundColor = color 55 | self.textLabel?.text = "\(state)" 56 | } 57 | ``` 58 | 59 | ### UICollectionViewDelegate 60 | 61 | The setup for a UICollectionView is exactly the same :) 62 | 63 | ## Invalidating the Cache 64 | 65 | If you implement a pull to refresh or you have some other 66 | reason for needing to reset the async operations you can call: 67 | 68 | `delegate.resetOperations()` - This will reset all async operations causing them to start again 69 | `delegate.resetOperation(cell)` - This will reset the operation for the passed item and cause it to start again 70 | 71 | ## Handling failure 72 | 73 | You can configure `IKAsyncable` operations to retry a given number of times before reporting failure. 74 | This can be adjusted by setting the `delegate.maxNumberOfFailures` property (the default is 3). 75 | If you want operations to retry indefinitely you can set `maxNumberOfFailures` to `IKAsyncOperationManager.UnlimitedRetries` 76 | 77 | ## The rest... 78 | 79 | This is written for Swift 2 80 | 81 | Check out the included app to see it in action; 82 | I've tried to make this simple to use and as easy to integrate into existing systems as I can. 83 | 84 | Feel free to submit any feedback or pull requests! 85 | 86 | -------------------------------------------------------------------------------- /IKAsyncableApp/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /IKAsyncableApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /IKAsyncableTests/IKAsyncOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IKAsyncOperationTests.swift 3 | // IKAsyncable 4 | // 5 | // Created by Ian Keen on 3/09/2015. 6 | // Copyright (c) 2015 Mustard. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class IKAsyncOperationTests: XCTestCase { 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | super.tearDown() 20 | } 21 | 22 | func testPerformOperation_whenClosureOperationIsSuccessful_shouldUpdateStateWithCompletion() { 23 | // given 24 | let expectation = self.expectationWithDescription("") 25 | 26 | let indexPath = NSIndexPath(forRow: 0, inSection: 0) 27 | let operation = IKAsyncOperation( 28 | indexPath: indexPath, 29 | operation: exampleSuccessfulOperation(0)) { op in 30 | if let state = op.state { 31 | switch state { 32 | case .Complete(let result): 33 | if (result as! Bool) { 34 | expectation.fulfill() 35 | } 36 | default: break; 37 | } 38 | } 39 | } 40 | 41 | // when 42 | operation.performOperation(0) 43 | 44 | // then 45 | self.waitForExpectationsWithTimeout(2.0, handler: nil) 46 | } 47 | 48 | func testPerformOperation_whenClosureOperationIsUnsuccessful_shouldUpdateStateWithFailure() { 49 | // given 50 | let expectation = self.expectationWithDescription("") 51 | 52 | let indexPath = NSIndexPath(forRow: 0, inSection: 0) 53 | let operation = IKAsyncOperation( 54 | indexPath: indexPath, 55 | operation: exampleUnsuccessfulOperation()) { op in 56 | if let state = op.state { 57 | switch state { 58 | case .Failed: 59 | expectation.fulfill() 60 | default: break; 61 | } 62 | } 63 | } 64 | 65 | // when 66 | operation.performOperation(0) 67 | 68 | // then 69 | self.waitForExpectationsWithTimeout(2.0, handler: nil) 70 | } 71 | 72 | func testPerformOperation_whenClosureOperationIsSuccessfulWithinMaxFailures_shouldUpdateStateWithCompletion() { 73 | // given 74 | let expectation = self.expectationWithDescription("") 75 | 76 | let indexPath = NSIndexPath(forRow: 0, inSection: 0) 77 | let operation = IKAsyncOperation( 78 | indexPath: indexPath, 79 | operation: exampleSuccessfulOperation(3)) { op in //fail 3 times, then succeed 80 | if let state = op.state { 81 | switch state { 82 | case .Complete(let result): 83 | if (result as! Bool) { 84 | expectation.fulfill() 85 | } 86 | default: break; 87 | } 88 | } 89 | } 90 | 91 | // when 92 | operation.performOperation(4) 93 | 94 | // then 95 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 96 | } 97 | 98 | func testPerformOperation_whenClosureOperationIsUnsuccessfulWithinMaxFailures_shouldUpdateStateWithFailure() { 99 | // given 100 | let expectation = self.expectationWithDescription("") 101 | 102 | let indexPath = NSIndexPath(forRow: 0, inSection: 0) 103 | let operation = IKAsyncOperation( 104 | indexPath: indexPath, 105 | operation: exampleSuccessfulOperation(3)) { op in //fail 3 times, then succeed 106 | if let state = op.state { 107 | switch state { 108 | case .Failed: 109 | expectation.fulfill() 110 | default: break; 111 | } 112 | } 113 | } 114 | 115 | // when 116 | operation.performOperation(2) 117 | 118 | // then 119 | self.waitForExpectationsWithTimeout(5.0, handler: nil) 120 | } 121 | 122 | func testPerformOperation_whenClosureOperationIsUnsuccessfulAndMaxFailuresIsUnlimited_shouldNotUpdateStateWithCompletionOrFailure() { 123 | // given 124 | let expectation = self.expectationWithDescription("") 125 | 126 | let indexPath = NSIndexPath(forRow: 0, inSection: 0) 127 | let operation = IKAsyncOperation( 128 | indexPath: indexPath, 129 | operation: exampleUnsuccessfulOperation()) { op in 130 | if let state = op.state { 131 | switch state { 132 | case .InProgress: break; 133 | default: 134 | XCTFail("a state other than InProgress should never be sent") 135 | } 136 | } 137 | } 138 | 139 | // when 140 | operation.performOperation(IKAsyncOperationManager.UnlimitedRetries) 141 | 142 | // then 143 | let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(9.5 * Double(NSEC_PER_SEC))) 144 | dispatch_after(delay, dispatch_get_main_queue()) { 145 | expectation.fulfill() 146 | } 147 | self.waitForExpectationsWithTimeout(10.0, handler: nil) 148 | } 149 | 150 | //MARK : - Helpers 151 | func exampleSuccessfulOperation(failCount: Int) -> IKAsyncOperationClosure { 152 | var failures = 0 153 | 154 | return { success, failure in 155 | let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(1.0 * Double(NSEC_PER_SEC))) 156 | dispatch_after(delay, dispatch_get_main_queue()) { 157 | failures++ 158 | if (failures > failCount) { 159 | success(true) 160 | } else { 161 | failure(self.exampleError()) 162 | } 163 | } 164 | } 165 | } 166 | func exampleUnsuccessfulOperation() -> IKAsyncOperationClosure { 167 | return { success, failure in 168 | let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(1.0 * Double(NSEC_PER_SEC))) 169 | dispatch_after(delay, dispatch_get_main_queue()) { 170 | failure(self.exampleError()) 171 | } 172 | } 173 | } 174 | func exampleError() -> NSError { 175 | let error = NSError(domain: "Domain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error"]) 176 | return error 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /IKAsyncable.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 32F65A511B968AA000614E7E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A491B968AA000614E7E /* AppDelegate.swift */; }; 11 | 32F65A521B968AA000614E7E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32F65A4A1B968AA000614E7E /* LaunchScreen.xib */; }; 12 | 32F65A531B968AA000614E7E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32F65A4C1B968AA000614E7E /* Main.storyboard */; }; 13 | 32F65A541B968AA000614E7E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32F65A4E1B968AA000614E7E /* Images.xcassets */; }; 14 | 32F65A561B968AA000614E7E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A501B968AA000614E7E /* ViewController.swift */; }; 15 | 32F65A591B968C3600614E7E /* MyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A571B968C3600614E7E /* MyCell.swift */; }; 16 | 32F65A5A1B968C3600614E7E /* MyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A581B968C3600614E7E /* MyDataSource.swift */; }; 17 | 32F65A5C1B968C4800614E7E /* IKAsyncable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5B1B968C4800614E7E /* IKAsyncable.swift */; }; 18 | 32F65A5E1B968C8300614E7E /* IKAsyncTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5D1B968C8300614E7E /* IKAsyncTableViewDelegate.swift */; }; 19 | 32F65A601B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5F1B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift */; }; 20 | 32F65A621B96919000614E7E /* IKAsyncOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A611B96919000614E7E /* IKAsyncOperationState.swift */; }; 21 | 32F65A641B96920900614E7E /* IKAsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A631B96920900614E7E /* IKAsyncOperation.swift */; }; 22 | 32F65A661B96922A00614E7E /* IKAsyncOperationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A651B96922A00614E7E /* IKAsyncOperationManager.swift */; }; 23 | 32F65A681B96AB6A00614E7E /* MyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A671B96AB6A00614E7E /* MyDelegate.swift */; }; 24 | 32F65A781B991D7600614E7E /* IKAsyncOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A771B991D7600614E7E /* IKAsyncOperationTests.swift */; }; 25 | 94E493561B96D16C00FA67D8 /* IKAsyncOperationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E493551B96D16C00FA67D8 /* IKAsyncOperationManagerTests.swift */; }; 26 | 94E493571B96D48800FA67D8 /* IKAsyncable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5B1B968C4800614E7E /* IKAsyncable.swift */; }; 27 | 94E493581B96D48800FA67D8 /* IKAsyncTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5D1B968C8300614E7E /* IKAsyncTableViewDelegate.swift */; }; 28 | 94E493591B96D48800FA67D8 /* IKAsyncCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A5F1B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift */; }; 29 | 94E4935A1B96D48800FA67D8 /* IKAsyncOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A611B96919000614E7E /* IKAsyncOperationState.swift */; }; 30 | 94E4935B1B96D48800FA67D8 /* IKAsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A631B96920900614E7E /* IKAsyncOperation.swift */; }; 31 | 94E4935C1B96D48800FA67D8 /* IKAsyncOperationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F65A651B96922A00614E7E /* IKAsyncOperationManager.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 32F65A381B968A7400614E7E /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 32F65A1A1B968A7400614E7E /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 32F65A211B968A7400614E7E; 40 | remoteInfo = IKAsyncable; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 32F65A221B968A7400614E7E /* IKAsyncable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IKAsyncable.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 32F65A371B968A7400614E7E /* IKAsyncableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IKAsyncableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 32F65A3C1B968A7400614E7E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 32F65A491B968AA000614E7E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | 32F65A4B1B968AA000614E7E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 50 | 32F65A4D1B968AA000614E7E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 51 | 32F65A4E1B968AA000614E7E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 52 | 32F65A4F1B968AA000614E7E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 32F65A501B968AA000614E7E /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 54 | 32F65A571B968C3600614E7E /* MyCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyCell.swift; sourceTree = ""; }; 55 | 32F65A581B968C3600614E7E /* MyDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyDataSource.swift; sourceTree = ""; }; 56 | 32F65A5B1B968C4800614E7E /* IKAsyncable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncable.swift; sourceTree = ""; }; 57 | 32F65A5D1B968C8300614E7E /* IKAsyncTableViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncTableViewDelegate.swift; sourceTree = ""; }; 58 | 32F65A5F1B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncCollectionViewDelegate.swift; sourceTree = ""; }; 59 | 32F65A611B96919000614E7E /* IKAsyncOperationState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncOperationState.swift; sourceTree = ""; }; 60 | 32F65A631B96920900614E7E /* IKAsyncOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncOperation.swift; sourceTree = ""; }; 61 | 32F65A651B96922A00614E7E /* IKAsyncOperationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncOperationManager.swift; sourceTree = ""; }; 62 | 32F65A671B96AB6A00614E7E /* MyDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyDelegate.swift; sourceTree = ""; }; 63 | 32F65A771B991D7600614E7E /* IKAsyncOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncOperationTests.swift; sourceTree = ""; }; 64 | 94E493551B96D16C00FA67D8 /* IKAsyncOperationManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IKAsyncOperationManagerTests.swift; sourceTree = ""; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXFrameworksBuildPhase section */ 68 | 32F65A1F1B968A7400614E7E /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | 32F65A341B968A7400614E7E /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | 32F65A191B968A7400614E7E = { 86 | isa = PBXGroup; 87 | children = ( 88 | 32F65A471B968AA000614E7E /* IKAsyncable */, 89 | 32F65A481B968AA000614E7E /* IKAsyncableApp */, 90 | 32F65A3A1B968A7400614E7E /* IKAsyncableTests */, 91 | 32F65A231B968A7400614E7E /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | 32F65A231B968A7400614E7E /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 32F65A221B968A7400614E7E /* IKAsyncable.app */, 99 | 32F65A371B968A7400614E7E /* IKAsyncableTests.xctest */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 32F65A3A1B968A7400614E7E /* IKAsyncableTests */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 94E493551B96D16C00FA67D8 /* IKAsyncOperationManagerTests.swift */, 108 | 32F65A771B991D7600614E7E /* IKAsyncOperationTests.swift */, 109 | 32F65A3B1B968A7400614E7E /* Supporting Files */, 110 | ); 111 | path = IKAsyncableTests; 112 | sourceTree = ""; 113 | }; 114 | 32F65A3B1B968A7400614E7E /* Supporting Files */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 32F65A3C1B968A7400614E7E /* Info.plist */, 118 | ); 119 | name = "Supporting Files"; 120 | sourceTree = ""; 121 | }; 122 | 32F65A471B968AA000614E7E /* IKAsyncable */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 32F65A5B1B968C4800614E7E /* IKAsyncable.swift */, 126 | 32F65A5D1B968C8300614E7E /* IKAsyncTableViewDelegate.swift */, 127 | 32F65A5F1B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift */, 128 | 32F65A611B96919000614E7E /* IKAsyncOperationState.swift */, 129 | 32F65A631B96920900614E7E /* IKAsyncOperation.swift */, 130 | 32F65A651B96922A00614E7E /* IKAsyncOperationManager.swift */, 131 | ); 132 | path = IKAsyncable; 133 | sourceTree = ""; 134 | }; 135 | 32F65A481B968AA000614E7E /* IKAsyncableApp */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 32F65A571B968C3600614E7E /* MyCell.swift */, 139 | 32F65A581B968C3600614E7E /* MyDataSource.swift */, 140 | 32F65A671B96AB6A00614E7E /* MyDelegate.swift */, 141 | 32F65A501B968AA000614E7E /* ViewController.swift */, 142 | 32F65A491B968AA000614E7E /* AppDelegate.swift */, 143 | 32F65A4A1B968AA000614E7E /* LaunchScreen.xib */, 144 | 32F65A4C1B968AA000614E7E /* Main.storyboard */, 145 | 32F65A4E1B968AA000614E7E /* Images.xcassets */, 146 | 32F65A4F1B968AA000614E7E /* Info.plist */, 147 | ); 148 | path = IKAsyncableApp; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 32F65A211B968A7400614E7E /* IKAsyncable */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 32F65A411B968A7400614E7E /* Build configuration list for PBXNativeTarget "IKAsyncable" */; 157 | buildPhases = ( 158 | 32F65A1E1B968A7400614E7E /* Sources */, 159 | 32F65A1F1B968A7400614E7E /* Frameworks */, 160 | 32F65A201B968A7400614E7E /* Resources */, 161 | ); 162 | buildRules = ( 163 | ); 164 | dependencies = ( 165 | ); 166 | name = IKAsyncable; 167 | productName = IKAsyncable; 168 | productReference = 32F65A221B968A7400614E7E /* IKAsyncable.app */; 169 | productType = "com.apple.product-type.application"; 170 | }; 171 | 32F65A361B968A7400614E7E /* IKAsyncableTests */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = 32F65A441B968A7400614E7E /* Build configuration list for PBXNativeTarget "IKAsyncableTests" */; 174 | buildPhases = ( 175 | 32F65A331B968A7400614E7E /* Sources */, 176 | 32F65A341B968A7400614E7E /* Frameworks */, 177 | 32F65A351B968A7400614E7E /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | 32F65A391B968A7400614E7E /* PBXTargetDependency */, 183 | ); 184 | name = IKAsyncableTests; 185 | productName = IKAsyncableTests; 186 | productReference = 32F65A371B968A7400614E7E /* IKAsyncableTests.xctest */; 187 | productType = "com.apple.product-type.bundle.unit-test"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 32F65A1A1B968A7400614E7E /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastSwiftMigration = 0700; 196 | LastSwiftUpdateCheck = 0700; 197 | LastUpgradeCheck = 0700; 198 | ORGANIZATIONNAME = Mustard; 199 | TargetAttributes = { 200 | 32F65A211B968A7400614E7E = { 201 | CreatedOnToolsVersion = 6.4; 202 | }; 203 | 32F65A361B968A7400614E7E = { 204 | CreatedOnToolsVersion = 6.4; 205 | TestTargetID = 32F65A211B968A7400614E7E; 206 | }; 207 | }; 208 | }; 209 | buildConfigurationList = 32F65A1D1B968A7400614E7E /* Build configuration list for PBXProject "IKAsyncable" */; 210 | compatibilityVersion = "Xcode 3.2"; 211 | developmentRegion = English; 212 | hasScannedForEncodings = 0; 213 | knownRegions = ( 214 | en, 215 | Base, 216 | ); 217 | mainGroup = 32F65A191B968A7400614E7E; 218 | productRefGroup = 32F65A231B968A7400614E7E /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | 32F65A211B968A7400614E7E /* IKAsyncable */, 223 | 32F65A361B968A7400614E7E /* IKAsyncableTests */, 224 | ); 225 | }; 226 | /* End PBXProject section */ 227 | 228 | /* Begin PBXResourcesBuildPhase section */ 229 | 32F65A201B968A7400614E7E /* Resources */ = { 230 | isa = PBXResourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 32F65A541B968AA000614E7E /* Images.xcassets in Resources */, 234 | 32F65A521B968AA000614E7E /* LaunchScreen.xib in Resources */, 235 | 32F65A531B968AA000614E7E /* Main.storyboard in Resources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | 32F65A351B968A7400614E7E /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | }; 246 | /* End PBXResourcesBuildPhase section */ 247 | 248 | /* Begin PBXSourcesBuildPhase section */ 249 | 32F65A1E1B968A7400614E7E /* Sources */ = { 250 | isa = PBXSourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 32F65A5E1B968C8300614E7E /* IKAsyncTableViewDelegate.swift in Sources */, 254 | 32F65A5C1B968C4800614E7E /* IKAsyncable.swift in Sources */, 255 | 32F65A621B96919000614E7E /* IKAsyncOperationState.swift in Sources */, 256 | 32F65A591B968C3600614E7E /* MyCell.swift in Sources */, 257 | 32F65A601B968D5400614E7E /* IKAsyncCollectionViewDelegate.swift in Sources */, 258 | 32F65A681B96AB6A00614E7E /* MyDelegate.swift in Sources */, 259 | 32F65A561B968AA000614E7E /* ViewController.swift in Sources */, 260 | 32F65A661B96922A00614E7E /* IKAsyncOperationManager.swift in Sources */, 261 | 32F65A641B96920900614E7E /* IKAsyncOperation.swift in Sources */, 262 | 32F65A5A1B968C3600614E7E /* MyDataSource.swift in Sources */, 263 | 32F65A511B968AA000614E7E /* AppDelegate.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 32F65A331B968A7400614E7E /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 94E493571B96D48800FA67D8 /* IKAsyncable.swift in Sources */, 272 | 94E493581B96D48800FA67D8 /* IKAsyncTableViewDelegate.swift in Sources */, 273 | 94E493591B96D48800FA67D8 /* IKAsyncCollectionViewDelegate.swift in Sources */, 274 | 94E4935A1B96D48800FA67D8 /* IKAsyncOperationState.swift in Sources */, 275 | 32F65A781B991D7600614E7E /* IKAsyncOperationTests.swift in Sources */, 276 | 94E4935B1B96D48800FA67D8 /* IKAsyncOperation.swift in Sources */, 277 | 94E4935C1B96D48800FA67D8 /* IKAsyncOperationManager.swift in Sources */, 278 | 94E493561B96D16C00FA67D8 /* IKAsyncOperationManagerTests.swift in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | /* End PBXSourcesBuildPhase section */ 283 | 284 | /* Begin PBXTargetDependency section */ 285 | 32F65A391B968A7400614E7E /* PBXTargetDependency */ = { 286 | isa = PBXTargetDependency; 287 | target = 32F65A211B968A7400614E7E /* IKAsyncable */; 288 | targetProxy = 32F65A381B968A7400614E7E /* PBXContainerItemProxy */; 289 | }; 290 | /* End PBXTargetDependency section */ 291 | 292 | /* Begin PBXVariantGroup section */ 293 | 32F65A4A1B968AA000614E7E /* LaunchScreen.xib */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 32F65A4B1B968AA000614E7E /* Base */, 297 | ); 298 | name = LaunchScreen.xib; 299 | sourceTree = ""; 300 | }; 301 | 32F65A4C1B968AA000614E7E /* Main.storyboard */ = { 302 | isa = PBXVariantGroup; 303 | children = ( 304 | 32F65A4D1B968AA000614E7E /* Base */, 305 | ); 306 | name = Main.storyboard; 307 | sourceTree = ""; 308 | }; 309 | /* End PBXVariantGroup section */ 310 | 311 | /* Begin XCBuildConfiguration section */ 312 | 32F65A3F1B968A7400614E7E /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ALWAYS_SEARCH_USER_PATHS = NO; 316 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 317 | CLANG_CXX_LIBRARY = "libc++"; 318 | CLANG_ENABLE_MODULES = YES; 319 | CLANG_ENABLE_OBJC_ARC = YES; 320 | CLANG_WARN_BOOL_CONVERSION = YES; 321 | CLANG_WARN_CONSTANT_CONVERSION = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 330 | COPY_PHASE_STRIP = NO; 331 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 332 | ENABLE_STRICT_OBJC_MSGSEND = YES; 333 | ENABLE_TESTABILITY = YES; 334 | GCC_C_LANGUAGE_STANDARD = gnu99; 335 | GCC_DYNAMIC_NO_PIC = NO; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_OPTIMIZATION_LEVEL = 0; 338 | GCC_PREPROCESSOR_DEFINITIONS = ( 339 | "DEBUG=1", 340 | "$(inherited)", 341 | ); 342 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 350 | MTL_ENABLE_DEBUG_INFO = YES; 351 | ONLY_ACTIVE_ARCH = YES; 352 | SDKROOT = iphoneos; 353 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | }; 356 | name = Debug; 357 | }; 358 | 32F65A401B968A7400614E7E /* Release */ = { 359 | isa = XCBuildConfiguration; 360 | buildSettings = { 361 | ALWAYS_SEARCH_USER_PATHS = NO; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BOOL_CONVERSION = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 369 | CLANG_WARN_EMPTY_BODY = YES; 370 | CLANG_WARN_ENUM_CONVERSION = YES; 371 | CLANG_WARN_INT_CONVERSION = YES; 372 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 376 | COPY_PHASE_STRIP = NO; 377 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 378 | ENABLE_NS_ASSERTIONS = NO; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu99; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 389 | MTL_ENABLE_DEBUG_INFO = NO; 390 | SDKROOT = iphoneos; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | VALIDATE_PRODUCT = YES; 393 | }; 394 | name = Release; 395 | }; 396 | 32F65A421B968A7400614E7E /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | INFOPLIST_FILE = "$(SRCROOT)/IKAsyncableApp/Info.plist"; 401 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 403 | PRODUCT_BUNDLE_IDENTIFIER = "com.mustard.$(PRODUCT_NAME:rfc1034identifier)"; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | }; 406 | name = Debug; 407 | }; 408 | 32F65A431B968A7400614E7E /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | INFOPLIST_FILE = "$(SRCROOT)/IKAsyncableApp/Info.plist"; 413 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 414 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 415 | PRODUCT_BUNDLE_IDENTIFIER = "com.mustard.$(PRODUCT_NAME:rfc1034identifier)"; 416 | PRODUCT_NAME = "$(TARGET_NAME)"; 417 | }; 418 | name = Release; 419 | }; 420 | 32F65A451B968A7400614E7E /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | BUNDLE_LOADER = "$(TEST_HOST)"; 424 | FRAMEWORK_SEARCH_PATHS = ( 425 | "$(SDKROOT)/Developer/Library/Frameworks", 426 | "$(inherited)", 427 | ); 428 | GCC_PREPROCESSOR_DEFINITIONS = ( 429 | "DEBUG=1", 430 | "$(inherited)", 431 | ); 432 | INFOPLIST_FILE = IKAsyncableTests/Info.plist; 433 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 434 | PRODUCT_BUNDLE_IDENTIFIER = "com.mustard.$(PRODUCT_NAME:rfc1034identifier)"; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IKAsyncable.app/IKAsyncable"; 437 | }; 438 | name = Debug; 439 | }; 440 | 32F65A461B968A7400614E7E /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | BUNDLE_LOADER = "$(TEST_HOST)"; 444 | FRAMEWORK_SEARCH_PATHS = ( 445 | "$(SDKROOT)/Developer/Library/Frameworks", 446 | "$(inherited)", 447 | ); 448 | INFOPLIST_FILE = IKAsyncableTests/Info.plist; 449 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 450 | PRODUCT_BUNDLE_IDENTIFIER = "com.mustard.$(PRODUCT_NAME:rfc1034identifier)"; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IKAsyncable.app/IKAsyncable"; 453 | }; 454 | name = Release; 455 | }; 456 | /* End XCBuildConfiguration section */ 457 | 458 | /* Begin XCConfigurationList section */ 459 | 32F65A1D1B968A7400614E7E /* Build configuration list for PBXProject "IKAsyncable" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | 32F65A3F1B968A7400614E7E /* Debug */, 463 | 32F65A401B968A7400614E7E /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 32F65A411B968A7400614E7E /* Build configuration list for PBXNativeTarget "IKAsyncable" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 32F65A421B968A7400614E7E /* Debug */, 472 | 32F65A431B968A7400614E7E /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | 32F65A441B968A7400614E7E /* Build configuration list for PBXNativeTarget "IKAsyncableTests" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 32F65A451B968A7400614E7E /* Debug */, 481 | 32F65A461B968A7400614E7E /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | /* End XCConfigurationList section */ 487 | }; 488 | rootObject = 32F65A1A1B968A7400614E7E /* Project object */; 489 | } 490 | --------------------------------------------------------------------------------