├── selene-banner.png ├── Podfile ├── Selene.xcworkspace └── contents.xcworkspacedata ├── .travis.yml ├── Selene.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── krisk.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcuserdata │ └── krisk.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── Selene.xcscheme └── project.pbxproj ├── Selene ├── Selene-Prefix.pch ├── Selene.h ├── SLNTaskProtocol.h ├── SLNScheduler.h └── SLNScheduler.m ├── .gitignore ├── NOTICE ├── SeleneTests ├── Info.plist └── SeleneTests.m ├── Selene.podspec ├── README.md └── LICENSE /selene-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/Selene/HEAD/selene-banner.png -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, "7.0" 3 | 4 | target "Selene" do 5 | 6 | end 7 | 8 | target "SeleneTests" do 9 | 10 | end 11 | -------------------------------------------------------------------------------- /Selene.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_install: 3 | - brew uninstall xctool 4 | - brew update 5 | - brew install xctool 6 | script: xctool -workspace Selene.xcworkspace -scheme Selene -sdk iphonesimulator build test 7 | -------------------------------------------------------------------------------- /Selene.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Selene.xcodeproj/project.xcworkspace/xcuserdata/krisk.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInAttic/Selene/HEAD/Selene.xcodeproj/project.xcworkspace/xcuserdata/krisk.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Selene/Selene-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // PrefixHeader.pch 3 | // Selene 4 | // 5 | // Created by Kirollos Risk on 9/6/14. 6 | // Copyright (c) 2014 LinkedIn. All rights reserved. 7 | // 8 | 9 | #ifdef __OBJC__ 10 | #import 11 | #import 12 | #endif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | 19 | Podfile.lock 20 | Pods 21 | Podfile 22 | Resources 23 | build/* 24 | DerivedData 25 | 26 | config/ 27 | Blog/ 28 | *.xcworkspace 29 | docs/ 30 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | © 2014 LinkedIn Corp. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -------------------------------------------------------------------------------- /Selene/Selene.h: -------------------------------------------------------------------------------- 1 | // 2 | // Selene 3 | // 4 | // Copyright (c) 2014 LinkedIn Corp. All rights reserved. 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // 13 | 14 | #import 15 | #import "SLNTaskProtocol.h" 16 | #import "SLNScheduler.h" -------------------------------------------------------------------------------- /Selene.xcodeproj/xcuserdata/krisk.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Selene.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | A10ADD0519BB7BC60081601C 16 | 17 | primary 18 | 19 | 20 | A10ADD1019BB7BC60081601C 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SeleneTests/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 | -------------------------------------------------------------------------------- /Selene.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Selene" 3 | s.version = "2.0.0" 4 | s.summary = "Selene is a library for scheduling background tasks." 5 | s.description = <<-DESC 6 | Selene calculates a task's goodness to determine whether the task should be executed. 7 | DESC 8 | 9 | s.homepage = "https://github.com/linkedin/Selene" 10 | s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" } 11 | s.authors = { "Kirollos Risk" => "kirollos@gmail.com" } 12 | s.social_media_url = "http://twitter.com/kirorisk" 13 | 14 | s.platform = :ios, "7.0" 15 | s.source = { :git => "https://github.com/linkedin/Selene.git", :tag => "2.0.0" } 16 | s.source_files = "Selene/*.{h,m}" 17 | s.frameworks = "Foundation", "UIKit" 18 | s.requires_arc = true 19 | end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Selene: Background Task Scheduler](https://raw.githubusercontent.com/linkedin/Selene/master/selene-banner.png) 2 | 3 | Selene is an iOS library which schedules the execution of tasks on a [background fetch](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html#//apple_ref/doc/uid/TP40007072-CH4-SW56). 4 | 5 | [![Build Status](https://travis-ci.org/linkedin/Selene.svg?branch=master)](http://travis-ci.org/linkedin/Selene) 6 | 7 | # Installation 8 | 9 | ## CocoaPods 10 | 11 | Add to your Podfile: 12 | `pod Selene` 13 | 14 | ## Submodule 15 | 16 | You can also add this repo as a submodule and copy everything in the Selene folder into your project. 17 | 18 | # Use 19 | 20 | **1) Add the `fetch` background mode in your app’s `Info.plist` file.** 21 | 22 | **2) Create a task** 23 | 24 | A task must conform to `SLNTaskProtocol`. For example: 25 | 26 | ```objective-c 27 | @interface SampleTask: NSObject 28 | @end 29 | 30 | @implementation SampleTask 31 | 32 | + (NSString *)identifier { 33 | return NSStringFromClass(self); 34 | } 35 | 36 | + (NSOperation *)operationWithCompletion:(SLNTaskCompletion_t)completion { 37 | NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ 38 | // Do some work .... 39 | completion(UIBackgroundFetchResultNoData); 40 | }]; 41 | return operation; 42 | } 43 | 44 | + (CGFloat)averageResponseTime { 45 | return 5.0; 46 | } 47 | 48 | + (SLNTaskPriority)priority { 49 | return SLNTaskPriorityLow; 50 | } 51 | 52 | @end 53 | ``` 54 | 55 | **3) Add the task class to the scheduler** 56 | 57 | ```objective-c 58 | NSArray *tasks = @[[SomeTask class]]; 59 | // Run the scheduler every 5 minutes 60 | [SLNScheduler setMinimumBackgroundFetchInterval:60 * 5]; 61 | // Add the tasks 62 | [SLNScheduler scheduleTasks:tasks]; 63 | ``` 64 | 65 | In the application delegate: 66 | 67 | ```objective-c 68 | - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { 69 | [SLNScheduler startWithCompletion:completionHandler]; 70 | } 71 | ``` 72 | 73 | --- 74 | 75 | Interested? Here's the [blog post](http://engineering.linkedin.com/ios/introducing-selene-open-source-library-scheduling-tasks-ios) -------------------------------------------------------------------------------- /Selene.xcodeproj/xcshareddata/xcschemes/Selene.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Selene/SLNTaskProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // Selene 3 | // 4 | // Copyright (c) 2014 LinkedIn Corp. All rights reserved. 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // 13 | 14 | #import 15 | 16 | // Defines the completion handler with a UIBackgroundFetchResult, and decides whether the UI should be be refreshed or not 17 | typedef void (^SLNTaskCompletion_t)(UIBackgroundFetchResult result); 18 | 19 | // Defines the priority of a scheduled background task. Note that these enum is in positive integers 20 | // to facilate the score calculation 21 | typedef NS_ENUM(NSInteger, SLNTaskPriority) { 22 | SLNTaskPriorityVeryLow = 1, 23 | SLNTaskPriorityLow, 24 | SLNTaskPriorityNormal, 25 | SLNTaskPriorityHigh, 26 | SLNTaskPriorityVeryHigh 27 | }; 28 | 29 | @protocol SLNTaskProtocol 30 | 31 | @required 32 | 33 | + (NSString *)identifier; 34 | 35 | /*! 36 | @abstract 37 | Defines an instance of an NSOperation 38 | 39 | @discussion 40 | The completion block should be executed so that scheduler knows whether there's new data, no data, or an error, 41 | thus forwarding the result to the UIApplication. If the block isn't executed, the scheduler will assume 42 | that there's no new data. The SLNTaskCompletion_t block exists distinct from the [NSOperation completionBlock], 43 | since one may choose to have a custom NSOperation (or a subclass thereof) with a custom block which passes in 44 | the result of the operation. 45 | 46 | A simple implementation could be: 47 | 48 | @code 49 | + (NSOperation *)operationWithCompletion:(SLNTaskCompletion_t)completion { 50 | MyCustomNSOperation *op = [MyCustomNSOperation new]; 51 | [op setCustomCompletionBlock:^(id data, NSError *error){ 52 | if (data) { 53 | completion(UIBackgroundFetchResultNewData); 54 | } else if (error) { 55 | completion(UIBackgroundFetchResultFailed); 56 | } 57 | }]; 58 | return op 59 | } 60 | @endcode 61 | 62 | @param completion Completion block for the operation. 63 | 64 | @return 65 | The NSOperation which should execute as part of the scheduled background task. 66 | */ 67 | + (NSOperation *)operationWithCompletion:(SLNTaskCompletion_t)completion; 68 | 69 | /*! 70 | @abstract 71 | The average response time, in seconds, of the operation, should be in the range of 0..30 72 | 73 | @discussion 74 | The response time should be relative to how expensive the NSOperation is. 75 | For example, if the operation makes an HTTP request which is known to take a considerable time, 76 | then the response time is high. Therefore, response time is a function of time, memory consumption, etc... 77 | typically approximated as a constant. 78 | */ 79 | + (CGFloat)averageResponseTime; 80 | 81 | /*! 82 | @abstract 83 | Defines the priority of the scheduled background operation 84 | 85 | @discussion 86 | This priority (SLNTaskPriority) is distinct from the priority of the NSOperation 87 | (NSOperationQueuePriority). The priority, along with the cost, facilitates the calculation of the cost, necessary 88 | to determine whether the scheduled background operation is inserted in the queue, for execution. 89 | 90 | The NSOperation's priority merely dictates whether the operation, after it is inserted in the queue, 91 | is executed. Therefore, due to several factors (battery life, connectivity, etc..) the NSOperation 92 | might not necessarily execute, even if in the queue. 93 | */ 94 | + (SLNTaskPriority)priority; 95 | 96 | @optional 97 | 98 | /*! 99 | @abstract 100 | Number of previous data point to include when calculating the moving average for the response time 101 | 102 | @discussion 103 | These data points are used to calculate the moving average of the response time. Note that the higher the number, the more accurate the average will be. 104 | See http://en.wikipedia.org/wiki/Moving_average#Simple_moving_average 105 | 106 | Default: 3 107 | Min: 0 108 | Max: 30 109 | */ 110 | + (NSUInteger)numberOfPeriodsForResponseTime; 111 | 112 | @end 113 | 114 | -------------------------------------------------------------------------------- /Selene/SLNScheduler.h: -------------------------------------------------------------------------------- 1 | // 2 | // Selene 3 | // 4 | // Copyright (c) 2014 LinkedIn Corp. All rights reserved. 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // 13 | 14 | #import 15 | 16 | @protocol SLNTaskProtocol; 17 | 18 | /*! 19 | @abstract 20 | LCNBackgroundOperationManager holds the entire scheduling logic for all background tasks. 21 | 22 | @discussion 23 | The app delegate should setup the scheduler by doing the following: 24 | 25 | @code 26 | // Set the fetch interval 27 | [SLNScheduler setMinimumBackgroundFetchInterval:...]; 28 | 29 | // Add the task classes 30 | [SLNScheduler scheduleTasks:@[...]]; 31 | 32 | // Then, to start the execution, in the app delegate's application:performFetchWithCompletionHandler: 33 | [SLNScheduler startWithCompletion:completionHandler]; 34 | 35 | // When scheduling needs to stop, perhaps due to authentication issues, do the following: 36 | [SLNScheduler stop] 37 | @endcode 38 | */ 39 | @interface SLNScheduler : NSObject 40 | 41 | /*! 42 | @abstract 43 | Sets the user defaults 44 | 45 | @discussion 46 | This is used for storing all scheduling data. The scheduling mechanism uses the stored values for calculating 47 | and determining the rank and score. If no userDefaults is provided, the standardUserDefaults is used. 48 | 49 | @param userDefaults User defaults object to store scheduling data. 50 | */ 51 | + (void)setUserDefaults:(NSUserDefaults *)userDefaults; 52 | 53 | /*! 54 | @abstract 55 | Sets the maximum number of concurrent operations the the interval queue can execute. 56 | 57 | @discussion 58 | If you specify the value NSOperationQueueDefaultMaxConcurrentOperationCount (which is recommended), 59 | the maximum number of operations can change dynamically based on system conditions. 60 | 61 | @param maxConcurrentOperationCount Maximum number of concurrent operations 62 | The maximum number of concurrent operations. Specify the value NSOperationQueueDefaultMaxConcurrentOperationCount 63 | if you want the receiver to choose an appropriate value based on the number of available processors and other relevant factors. 64 | */ 65 | + (void)setMaxConcurrentOperationCount:(NSInteger)maxConcurrentOperationCount; 66 | 67 | /*! 68 | @abstract 69 | Sets the background fetch interval of [UIApplication sharedApplication] 70 | 71 | @discussion 72 | From Apple: "The minimum number of seconds that must elapse before another background fetch can be initiated. 73 | This value is advisory only and does not indicate the exact amount of time expected between fetch operations." 74 | */ 75 | + (void)setMinimumBackgroundFetchInterval:(NSTimeInterval)minimumBackgroundFetchInterval; 76 | 77 | /*! 78 | @abstract 79 | Sets the desired tasks to be scheduled. 80 | 81 | @param tasks 82 | An array of tasks, where each task is class which must conform to SLNTaskProtocol 83 | */ 84 | + (void)scheduleTasks:(NSArray *)tasks; 85 | 86 | /*! 87 | @abstract 88 | Stops the scheduler. 89 | 90 | @discussion 91 | This utilizes the [UIApplication sharedApplication], which will set the fetch interval to UIApplicationBackgroundFetchIntervalNever 92 | */ 93 | + (void)stop; 94 | 95 | 96 | /*! 97 | @abstract 98 | Executes the set of tasks. 99 | 100 | @discussion 101 | Should be called from the App Delegate's application:performFetchWithCompletionHandler: method. 102 | Must call this method from a single thread. 103 | 104 | @param completion 105 | Block which triggers when all operations are completed. It is executed on the main queue. 106 | */ 107 | + (void)startWithCompletion:(void (^)(UIBackgroundFetchResult))completion; 108 | 109 | /*! 110 | @abstract 111 | Schedules an background task to execute immediately, regardless of its priority and/or cost 112 | 113 | @param task Background task to be scheduled immediately. 114 | A class comforming to LCNScheduledBackgroundTaskProtocol 115 | */ 116 | + (void)scheduleNow:(id)task; 117 | 118 | /*! 119 | @abstract 120 | Resets all data stored in the scheduler. 121 | 122 | @discussion 123 | This will wait for all tasks that are currently executing to complete. 124 | */ 125 | + (void)reset; 126 | 127 | @end 128 | 129 | -------------------------------------------------------------------------------- /SeleneTests/SeleneTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Selene 3 | // 4 | // Copyright (c) 2014 LinkedIn Corp. All rights reserved. 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // 13 | 14 | #import 15 | #import "Selene.h" 16 | #import 17 | 18 | // Set the flag for a block completion handler 19 | #define StartBlock() __block BOOL waitingForBlock = YES 20 | // Set the flag to stop the loop 21 | #define EndBlock() waitingForBlock = NO 22 | // Wait and loop until flag is set 23 | #define WaitUntilBlockCompletes() WaitWhile(waitingForBlock) 24 | // Macro - Wait for condition to be NO/false in blocks and asynchronous calls 25 | #define WaitWhile(condition) \ 26 | do { \ 27 | while(condition) { \ 28 | [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; \ 29 | } \ 30 | } while(0) 31 | 32 | // Dummy task class which conforms to SLNTaskProtocol, simple here so 33 | // we can easily get its methods' parameters and return types when dynamically 34 | // creating task classes 35 | @interface DummyTask : NSObject 36 | @end 37 | @implementation DummyTask 38 | + (NSString *)identifier { 39 | return @""; 40 | } 41 | + (NSOperation *)operationWithCompletion:(SLNTaskCompletion_t)__unused completion { 42 | return nil; 43 | } 44 | + (CGFloat)averageResponseTime { 45 | return 0; 46 | } 47 | + (SLNTaskPriority)priority { 48 | return 0; 49 | } 50 | + (NSUInteger)numberOfPeriodsForResponseTime { 51 | return 5; 52 | } 53 | @end 54 | 55 | 56 | @interface SeleneTests : XCTestCase 57 | 58 | @end 59 | 60 | @implementation SeleneTests 61 | 62 | static const char * GetEncoding(SEL name) { 63 | return method_getTypeEncoding(class_getClassMethod([DummyTask class], name)); 64 | }; 65 | 66 | - (Class)createTaskClassWithPriority:(SLNTaskPriority)priority 67 | averageResponseTime:(CGFloat)averageResponseTime 68 | executionTime:(CGFloat)executionTime 69 | numberOfPeriodsForResponseTime:(NSUInteger)numberOfPeriodsForResponseTime 70 | fetchResult:(UIBackgroundFetchResult)fetchResult { 71 | Class taskClass = [self createTaskClassWithPriority:priority averageResponseTime:averageResponseTime executionTime:executionTime fetchResult:fetchResult]; 72 | 73 | class_addMethod(object_getClass(taskClass), @selector(numberOfPeriodsForResponseTime), imp_implementationWithBlock(^NSUInteger(id __unused innerSelf){ 74 | return numberOfPeriodsForResponseTime; 75 | }), GetEncoding(@selector(numberOfPeriodsForResponseTime))); 76 | 77 | return taskClass; 78 | } 79 | 80 | - (Class)createTaskClassWithPriority:(SLNTaskPriority)priority 81 | averageResponseTime:(CGFloat)averageResponseTime 82 | executionTime:(CGFloat)executionTime 83 | fetchResult:(UIBackgroundFetchResult)fetchResult { 84 | static int count = 0; 85 | 86 | NSString* taskClassName = [NSString stringWithFormat: @"Task%i", ++count]; 87 | const char *cString = [taskClassName cStringUsingEncoding:NSASCIIStringEncoding]; 88 | 89 | Class taskClass = objc_allocateClassPair([NSObject class], cString, 0); 90 | class_conformsToProtocol(taskClass, @protocol(SLNTaskProtocol)); 91 | 92 | //Add class methods 93 | class_addMethod(object_getClass(taskClass), @selector(identifier), imp_implementationWithBlock(^NSString*(id __unused innerSelf) { 94 | return NSStringFromClass([self class]); 95 | }), GetEncoding(@selector(identifier))); 96 | 97 | class_addMethod(object_getClass(taskClass), @selector(operationWithCompletion:), imp_implementationWithBlock(^NSOperation*(id __unused innerSelf, SLNTaskCompletion_t completion) { 98 | NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ 99 | dispatch_semaphore_t sema = dispatch_semaphore_create(0); 100 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 101 | (int64_t)(executionTime * NSEC_PER_SEC)), 102 | dispatch_queue_create([[NSString stringWithFormat:@"selene.test.scheduler.queue.%@", taskClassName] UTF8String], 103 | DISPATCH_QUEUE_SERIAL), ^{ 104 | completion(fetchResult); 105 | dispatch_semaphore_signal(sema); 106 | }); 107 | dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 108 | }]; 109 | return operation; 110 | }), GetEncoding(@selector(operationWithCompletion:))); 111 | 112 | class_addMethod(object_getClass(taskClass), @selector(averageResponseTime), imp_implementationWithBlock(^CGFloat(id __unused innerSelf){ 113 | return averageResponseTime; 114 | }), GetEncoding(@selector(averageResponseTime))); 115 | 116 | class_addMethod(object_getClass(taskClass), @selector(priority), imp_implementationWithBlock(^SLNTaskPriority(id __unused innerSelf){ 117 | return priority; 118 | }), GetEncoding(@selector(priority))); 119 | 120 | return taskClass; 121 | } 122 | 123 | - (void)reset { 124 | StartBlock(); 125 | [SLNScheduler reset]; 126 | 127 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 128 | EndBlock(); 129 | }); 130 | 131 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 132 | [userDefaults removeObjectForKey:@"kSLNExecutionSchedule"]; 133 | [userDefaults synchronize]; 134 | 135 | WaitUntilBlockCompletes(); 136 | } 137 | 138 | - (void)setUp { 139 | [super setUp]; 140 | [SLNScheduler setMinimumBackgroundFetchInterval:60 * 10]; 141 | } 142 | 143 | - (void)tearDown { 144 | [super tearDown]; 145 | [self reset]; 146 | } 147 | 148 | - (void)testExample { 149 | Class taskA = [self createTaskClassWithPriority:SLNTaskPriorityVeryLow averageResponseTime:4.0 executionTime:0 fetchResult:UIBackgroundFetchResultNewData]; 150 | Class taskB = [self createTaskClassWithPriority:SLNTaskPriorityVeryHigh averageResponseTime:5.0 executionTime:0 fetchResult:UIBackgroundFetchResultNewData]; 151 | Class taskC = [self createTaskClassWithPriority:SLNTaskPriorityLow averageResponseTime:5.0 executionTime:0 fetchResult:UIBackgroundFetchResultNewData]; 152 | 153 | NSArray *tasks = @[taskA, taskB, taskC]; 154 | [SLNScheduler scheduleTasks:tasks]; 155 | 156 | StartBlock(); 157 | // XCTestExpectation *expectation = [self expectationWithDescription:@"Scheduled tasks"]; 158 | 159 | void (^completion)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult __unused result) { 160 | EndBlock(); 161 | // [expectation fulfill]; 162 | XCTAssertTrue(YES, @"Tasks all successfully executed"); 163 | }; 164 | [SLNScheduler startWithCompletion:completion]; 165 | 166 | // [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { 167 | // XCTAssertFalse(NO, @"Tasks did not finish in time"); 168 | // }]; 169 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 170 | EndBlock(); 171 | }); 172 | WaitUntilBlockCompletes(); 173 | } 174 | 175 | - (void)testThreadSafeyAndMultipleExecutions { 176 | Class taskA = [self createTaskClassWithPriority:SLNTaskPriorityVeryLow averageResponseTime:2.0 executionTime:1 fetchResult:UIBackgroundFetchResultNewData]; 177 | Class taskB = [self createTaskClassWithPriority:SLNTaskPriorityVeryHigh averageResponseTime:3.0 executionTime:2 fetchResult:UIBackgroundFetchResultNewData]; 178 | Class taskC = [self createTaskClassWithPriority:SLNTaskPriorityLow averageResponseTime:4.0 executionTime:2 fetchResult:UIBackgroundFetchResultNewData]; 179 | Class taskD = [self createTaskClassWithPriority:SLNTaskPriorityLow averageResponseTime:5.0 executionTime:3 fetchResult:UIBackgroundFetchResultNewData]; 180 | 181 | NSArray *tasks = @[taskA, taskB, taskC, taskD]; 182 | 183 | const NSInteger cycles = 5; 184 | 185 | NSOperationQueue *queue = [NSOperationQueue new]; 186 | NSMutableArray *operations = [NSMutableArray new]; 187 | for (NSInteger i = 0; i < cycles; i++) { 188 | NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ 189 | StartBlock(); 190 | [SLNScheduler scheduleTasks:tasks]; 191 | [SLNScheduler startWithCompletion:^(UIBackgroundFetchResult __unused result) { 192 | EndBlock(); 193 | NSLog(@"Tasks completed"); 194 | }]; 195 | [SLNScheduler startWithCompletion:^(UIBackgroundFetchResult __unused result) { NSLog(@"Tasks completed"); }]; 196 | WaitUntilBlockCompletes(); 197 | }]; 198 | [operations addObject:op]; 199 | } 200 | 201 | [SLNScheduler startWithCompletion:^(UIBackgroundFetchResult __unused result) { 202 | NSLog(@"Inner completed"); 203 | }]; 204 | 205 | StartBlock(); 206 | // XCTestExpectation *expectation = [self expectationWithDescription:@"All tasks have been scheduled"]; 207 | NSBlockOperation *finalOp = [NSBlockOperation blockOperationWithBlock:^{ 208 | EndBlock(); 209 | // [expectation fulfill]; 210 | XCTAssertTrue(YES, @"Tasks should complete execution"); 211 | }]; 212 | [operations addObject:finalOp]; 213 | 214 | NSInteger i = (NSInteger)[operations count] - 1; 215 | while (i > 0) { 216 | NSOperation *op = [operations objectAtIndex:(NSUInteger)i]; 217 | NSOperation *previousOp = [operations objectAtIndex:(NSUInteger)(i-1)]; 218 | if (previousOp) { 219 | [op addDependency:previousOp]; 220 | } 221 | i--; 222 | } 223 | 224 | [queue addOperations:operations waitUntilFinished:NO]; 225 | 226 | // [self waitForExpectationsWithTimeout:60 handler:^(NSError *error) { 227 | // XCTAssertFalse(NO, @"Tasks did not finish in time"); 228 | // }]; 229 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 230 | EndBlock(); 231 | XCTAssertFalse(NO, @"Tasks did not finish in time"); 232 | }); 233 | WaitUntilBlockCompletes(); 234 | } 235 | 236 | @end 237 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Selene/SLNScheduler.m: -------------------------------------------------------------------------------- 1 | // 2 | // Selene 3 | // 4 | // Copyright (c) 2014 LinkedIn Corp. All rights reserved. 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // 13 | 14 | #import "SLNScheduler.h" 15 | #import "SLNTaskProtocol.h" 16 | 17 | #if !__has_feature(objc_arc) 18 | #error This file must be compiled with ARC. Convert your project to ARC or specify the -fobjc-arc flag. 19 | #endif 20 | 21 | #ifndef SLN_ENABLE_LOGGING 22 | #ifdef DEBUG 23 | #define SLN_ENABLE_LOGGING 1 24 | #else 25 | #define SLN_ENABLE_LOGGING 0 26 | #endif 27 | #endif 28 | 29 | #if SLN_ENABLE_LOGGING != 0 30 | // First, check if we can use Cocoalumberjack for logging 31 | #ifdef LOG_VERBOSE 32 | #define SLNLog(...) DDLogVerbose(__VA_ARGS__) 33 | #else 34 | #define SLNLog(...) NSLog(@"%s(%p) %@", __PRETTY_FUNCTION__, self, [NSString stringWithFormat:__VA_ARGS__]) 35 | #endif 36 | #else 37 | #define SLNLog(...) ((void)0) 38 | #endif 39 | 40 | // Used as key for storing the last execution time of tsaks, in NSUserDefaults 41 | static NSString * const kSLNExecutionSchedule = @"kSLNExecutionSchedule"; 42 | static NSString * const kSLNRecentResponseTimes = @"kSLNRecentResponseTimes"; 43 | static NSString * const kSLNLastExecutionTime = @"kSLNLastExecutionTime"; 44 | 45 | // Define the total available time for executing tasks on the background. 46 | // Since every task has an associcated to it, this is critical to determine which tasks to execute. 47 | // From Apple docs: 48 | // "..your app has up to 30 seconds of wall-clock time to perform the download operation and call the specified completion handler block." 49 | // https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html 50 | static CGFloat const kSLNAvailableTime = 30.0; 51 | 52 | // Total number of response times to store for a given task. 53 | // See http://en.wikipedia.org/wiki/Moving_average#Simple_moving_average 54 | static NSUInteger const kSLNDefaultNumberOfResponseTimesToInclude = 3; 55 | static NSUInteger const kSLNMinNumberOfResponseTimesToInclude = 0; 56 | static NSUInteger const kSLNMaxNumberOfResponseTimesToInclude = 30; 57 | 58 | /******/ 59 | 60 | #pragma mark - SLNTaskContainer 61 | 62 | // This is a convenience structure which wraps an id. 63 | // It is only an internal class, used for keeping score, execution time, and facilitate sorting. 64 | // 65 | @interface SLNTaskContainer : NSObject 66 | 67 | @property (nonatomic) CGFloat score; 68 | @property (nonatomic, strong) id task; 69 | 70 | // Store the last time the task was executed 71 | @property (nonatomic) NSTimeInterval lastExecutionTime; 72 | 73 | // Store the last N response times 74 | @property (nonatomic, strong) NSMutableArray *recentReponseTimes; 75 | 76 | @end 77 | 78 | /******/ 79 | 80 | @implementation SLNTaskContainer 81 | 82 | // Returns the elapsed time between now and its last execution 83 | - (NSTimeInterval)elapsedTimeSinceLastExecution { 84 | return [[NSDate date] timeIntervalSince1970] - self.lastExecutionTime; 85 | } 86 | 87 | // Returns its id identifier 88 | - (NSString *)key { 89 | return [self.task identifier]; 90 | } 91 | 92 | - (void)addResponseTime:(NSTimeInterval)responseTime { 93 | [self.recentReponseTimes addObject:@(responseTime)]; 94 | 95 | NSUInteger numberOfReponseTimesToInclude = kSLNDefaultNumberOfResponseTimesToInclude; 96 | 97 | if ([[self task] respondsToSelector:@selector(numberOfPeriodsForResponseTime)]) { 98 | numberOfReponseTimesToInclude = [self.task numberOfPeriodsForResponseTime]; 99 | if (numberOfReponseTimesToInclude < kSLNMinNumberOfResponseTimesToInclude) { 100 | numberOfReponseTimesToInclude = kSLNMinNumberOfResponseTimesToInclude; 101 | } else if (numberOfReponseTimesToInclude > kSLNMaxNumberOfResponseTimesToInclude) { 102 | numberOfReponseTimesToInclude = kSLNMaxNumberOfResponseTimesToInclude; 103 | } 104 | } 105 | 106 | if ([self.recentReponseTimes count] > numberOfReponseTimesToInclude) { 107 | [self.recentReponseTimes removeObjectAtIndex:0]; 108 | } 109 | } 110 | 111 | // Calculate a simple moving average of the last kSLNNumberOfReponseTimesToInclude. 112 | // This gives us a more useful measurement when deciding to schedule the task. 113 | // http://en.wikipedia.org/wiki/Moving_average#Simple_moving_average 114 | // 115 | - (CGFloat)movingAverageResponseTime { 116 | NSArray *responseTimes = [self.recentReponseTimes copy]; 117 | if ([responseTimes count] == 0) { 118 | return [self.task averageResponseTime]; 119 | } 120 | CGFloat totalResponseTime = 0; 121 | for (NSNumber *responseTime in responseTimes) { 122 | totalResponseTime += [responseTime floatValue]; 123 | } 124 | return totalResponseTime / [responseTimes count]; 125 | } 126 | 127 | // Deserialization into the current instance, with value of the NSDictionary from NSUserDefaults 128 | - (void)updateWithDictionary:(NSDictionary *)dict { 129 | if (dict) { 130 | NSArray *recentReponseTimes = [dict objectForKey:kSLNRecentResponseTimes]; 131 | NSInteger lastExecutionTime = [[dict objectForKey:kSLNLastExecutionTime] intValue]; 132 | self.recentReponseTimes = [recentReponseTimes mutableCopy]; 133 | self.lastExecutionTime = lastExecutionTime; 134 | } else { 135 | self.lastExecutionTime = [[NSDate date] timeIntervalSince1970]; 136 | self.recentReponseTimes = [NSMutableArray new]; 137 | } 138 | } 139 | 140 | // Serialization into dictionary for quick persistence into NSUserDefaults 141 | - (NSDictionary *)toDictionary { 142 | return @{ 143 | kSLNRecentResponseTimes: [self.recentReponseTimes copy], 144 | kSLNLastExecutionTime: @(self.lastExecutionTime) 145 | }; 146 | } 147 | 148 | - (NSString *)description { 149 | return [NSString stringWithFormat:@"task: %@, last execution time: %f, moving average response time: %f, last score: %f", 150 | [self key], 151 | self.lastExecutionTime, 152 | [self movingAverageResponseTime], 153 | self.score]; 154 | } 155 | 156 | @end 157 | 158 | /******/ 159 | 160 | #pragma mark - SLNScheduler 161 | 162 | @interface SLNScheduler () 163 | 164 | @property (nonatomic, strong) NSUserDefaults *userDefaults; 165 | 166 | // This is NSOpertionQueue which contains the list of NSOperations for every taks. The NSOperations 167 | // in this queue are marked for execution 168 | @property (nonatomic, strong) NSOperationQueue *operationQueue; 169 | 170 | // Stores the list of all SLNTaskContainers that should be checked for execution. 171 | @property (nonatomic, strong) NSArray *taskContainers; 172 | 173 | @property (nonatomic) NSTimeInterval minimumBackgroundFetchInterval; 174 | 175 | @property (nonatomic, getter=isExecuting) BOOL executing; 176 | 177 | @property (nonatomic, strong) NSMutableArray *completionBlocks; 178 | 179 | @property (nonatomic, strong) dispatch_queue_t dispatchQueue; 180 | 181 | @end 182 | 183 | @implementation SLNScheduler 184 | 185 | #pragma mark - Normalization & Score 186 | 187 | // Returns a normalized value 188 | // http://en.wikipedia.org/wiki/Normalization_(statistics) 189 | // 190 | static inline CGFloat Normalize(NSTimeInterval value, NSTimeInterval min, NSTimeInterval max) { 191 | return (CGFloat)((value - min)/(max == min ? 1 : (max - min))); 192 | }; 193 | 194 | // Calculates the score based on priority and last execution time. 195 | // Currently, this is a pretty rudimentary operation: (normalized priority) * (normalized last execution time) 196 | // 197 | static inline CGFloat Score(SLNTaskPriority priority, NSTimeInterval time, NSTimeInterval minTime, NSTimeInterval maxTime) { 198 | return Normalize(priority, 0, SLNTaskPriorityVeryHigh) * Normalize(time, minTime, maxTime); 199 | }; 200 | 201 | #pragma mark - Instance 202 | 203 | + (instancetype)sharedInstance { 204 | static dispatch_once_t once; 205 | static SLNScheduler *instance; 206 | dispatch_once(&once, ^{ 207 | instance = [self new]; 208 | instance.completionBlocks = [NSMutableArray new]; 209 | instance.operationQueue = [NSOperationQueue new]; 210 | instance.dispatchQueue = dispatch_queue_create("com.linkedin.selene.queue", DISPATCH_QUEUE_SERIAL); 211 | [instance.operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount]; 212 | }); 213 | return instance; 214 | } 215 | 216 | #pragma mark - Accessors 217 | 218 | + (void)setUserDefaults:(NSUserDefaults *)userDaults { 219 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 220 | dispatch_sync(instance.dispatchQueue, ^{ 221 | instance.userDefaults = userDaults; 222 | }); 223 | } 224 | 225 | + (void)setMinimumBackgroundFetchInterval:(NSTimeInterval)minimumBackgroundFetchInterval { 226 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 227 | dispatch_sync(instance.dispatchQueue, ^{ 228 | instance.minimumBackgroundFetchInterval = minimumBackgroundFetchInterval; 229 | [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:minimumBackgroundFetchInterval]; 230 | }); 231 | } 232 | 233 | + (void)setMaxConcurrentOperationCount:(NSInteger)maxConcurrentOperationCount { 234 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 235 | dispatch_sync(instance.dispatchQueue, ^{ 236 | [instance.operationQueue setMaxConcurrentOperationCount:maxConcurrentOperationCount]; 237 | }); 238 | } 239 | 240 | #pragma mark - Scheduling/Execution 241 | 242 | + (void)scheduleTasks:(NSArray *)tasks { 243 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 244 | dispatch_sync(instance.dispatchQueue, ^{ 245 | // Retrieve the execution schedule from the user defaults. Then: 246 | // 1) Create the task 247 | // 2) Iterate through the list of passed in scheduled tasks, and deserialize its content into the appropriate 248 | // SLNTaskContainer 249 | NSDictionary *executionSchedule = [instance.userDefaults dictionaryForKey:kSLNExecutionSchedule]; 250 | 251 | // This flag dictates where the execution schedule should be saved. This may occur when: 252 | // 1) The first time this code runs, thus there would be no execution schedule present 253 | // 2) When new tasks are inserted, thus they do not exist in the execution schedule. 254 | __block BOOL hasChanges = NO; 255 | 256 | NSMutableArray *taskContainers = [NSMutableArray new]; 257 | 258 | [tasks enumerateObjectsUsingBlock:^(id task, NSUInteger __unused idx, BOOL * __unused stop) { 259 | NSAssert(([task priority] >= SLNTaskPriorityVeryLow) && [task priority] <= SLNTaskPriorityVeryHigh, @"Priority must be between [SLNTaskPriorityVeryLow,SLNTaskPriorityVeryHigh]"); 260 | NSAssert(([task averageResponseTime] >= 0) && [task averageResponseTime] <= kSLNAvailableTime, @"averageResponseTime must be in the range of [0,30]"); 261 | 262 | SLNTaskContainer *t = [SLNTaskContainer new]; 263 | t.task = task; 264 | 265 | NSDictionary *schedule = [executionSchedule objectForKey:[t key]]; 266 | 267 | [t updateWithDictionary:schedule]; 268 | 269 | if (!schedule) { 270 | hasChanges = YES; 271 | } 272 | 273 | [taskContainers addObject:t]; 274 | }]; 275 | 276 | instance.taskContainers = taskContainers; 277 | 278 | if (hasChanges) { 279 | [instance save]; 280 | } 281 | }); 282 | } 283 | 284 | + (void)scheduleNow:(id)__unused task { 285 | // Purposely left blank. We'll implement this at a later date. For now this isn't needed, 286 | // but is left here as a reminder of good things to come. 287 | } 288 | 289 | + (void)startWithCompletion:(void (^)(UIBackgroundFetchResult))completion { 290 | NSAssert(completion != nil, @"Completion block must exist."); 291 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 292 | 293 | dispatch_async(instance.dispatchQueue, ^{ 294 | [instance.completionBlocks addObject:[completion copy]]; 295 | 296 | if (instance.isExecuting) { 297 | SLNLog(@"Scheduler already running."); 298 | return; 299 | } 300 | 301 | instance.executing = YES; 302 | 303 | // Retrieve the taks that need to execute 304 | NSArray *tasks = [instance nextTasks]; 305 | 306 | // If there are no operations, then there's nothing to do. Simply short-cicuit. 307 | if ([tasks count] == 0) { 308 | [instance completeWithResult:UIBackgroundFetchResultNoData]; 309 | return; 310 | } 311 | 312 | [instance execute:tasks]; 313 | }); 314 | } 315 | 316 | + (void)stop { 317 | [self setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever]; 318 | } 319 | 320 | + (void)reset { 321 | SLNScheduler *instance = [SLNScheduler sharedInstance]; 322 | dispatch_sync(instance.dispatchQueue, ^{ 323 | if ([instance.operationQueue operationCount] > 0) { 324 | [instance.operationQueue waitUntilAllOperationsAreFinished]; 325 | } 326 | instance.taskContainers = nil; 327 | [instance.userDefaults removeObjectForKey:kSLNExecutionSchedule]; 328 | [instance.userDefaults synchronize]; 329 | [instance.completionBlocks removeAllObjects]; 330 | }); 331 | } 332 | 333 | #pragma mark - Instance: Execution 334 | 335 | - (void)completeWithResult:(UIBackgroundFetchResult)result { 336 | SLNLog(@"Scheduled tasks completed with result: %lu", (unsigned long)result); 337 | __weak __typeof(self) weakSelf = self; 338 | self.executing = NO; 339 | dispatch_async(dispatch_get_main_queue(), ^{ 340 | __strong typeof(self) strongSelf = weakSelf; 341 | for (void (^block)(UIBackgroundFetchResult) in strongSelf.completionBlocks) { 342 | block(UIBackgroundFetchResultNoData); 343 | } 344 | [strongSelf.completionBlocks removeAllObjects]; 345 | }); 346 | } 347 | 348 | - (NSUserDefaults *)userDefaults { 349 | if (!_userDefaults) { 350 | _userDefaults = [NSUserDefaults standardUserDefaults]; 351 | } 352 | return _userDefaults; 353 | } 354 | 355 | // Executes the list of items. When the execution of *all* tasks is complete, the completion 356 | // block is invoked. 357 | - (void)execute:(NSArray *)taskItems { 358 | __weak __typeof(self) weakSelf = self; 359 | 360 | __block UIBackgroundFetchResult finalResult = UIBackgroundFetchResultNoData; 361 | 362 | NSDate *start = [NSDate date]; 363 | 364 | NSMutableArray *operations = [[NSMutableArray alloc] initWithCapacity:[taskItems count]]; 365 | 366 | // Iterate through the list of scheduled operations, and for each one, add its NSOperation 367 | // to the queue. Additionally, set a completion block responsible for updating the last sync time 368 | // of this scheduled operation 369 | dispatch_group_t group = dispatch_group_create(); 370 | 371 | for (SLNTaskContainer *t in taskItems) { 372 | dispatch_group_enter(group); 373 | NSOperation *operation = [t.task operationWithCompletion:^(UIBackgroundFetchResult result) { 374 | dispatch_async(self.dispatchQueue, ^{ 375 | if (result == UIBackgroundFetchResultNewData) { 376 | finalResult = UIBackgroundFetchResultNewData; 377 | } 378 | NSDate *finish = [NSDate date]; 379 | t.lastExecutionTime = [finish timeIntervalSince1970]; 380 | NSTimeInterval interval = [finish timeIntervalSinceDate:start]; 381 | [t addResponseTime:interval]; 382 | dispatch_group_leave(group); 383 | }); 384 | }]; 385 | [operations addObject:operation]; 386 | } 387 | 388 | [weakSelf.operationQueue addOperations:operations waitUntilFinished:NO]; 389 | 390 | dispatch_group_notify(group, self.dispatchQueue, ^{ 391 | __strong typeof(self) strongSelf = weakSelf; 392 | // Update the execution schedule 393 | [strongSelf save]; 394 | // And we're done! 395 | [strongSelf completeWithResult:finalResult]; 396 | }); 397 | } 398 | 399 | // Returns a list of SLNTaskContainer(s) which need to execute. 400 | // Note that this may be a subset of all task items. 401 | // 402 | // The logic is as follows: 403 | // 404 | // 1) For every task: 405 | // a) Retrieve the last execution time, and its priority. 406 | // b) Calculate the score 407 | // 2) Sort the tasks by their score 408 | // 3) Since every task has an average response time, we can determine which 409 | // tasks to run by using the total available response time. 410 | // 411 | // Example: 412 | // 413 | // Suppose... 414 | // - we've calculated the scores of tasks x,y,z to be S(x) = 3, S(y) = 2, S(z) = 1 415 | // - the average response times are T(x) = 20, T(y) = 5, T(z) = 10 416 | // - the available time is 30s 417 | // 418 | // Therfore, the execution list, sorted by their score would be [x,y,z]. 419 | // Since the available response time is 30, only x,y can be executed, since T(x) + T(y) <= 30. 420 | // 421 | // Note that since the score is a function of a background task's priority and last execution time, it is 422 | // guaranteed that unexecuted tasks will still execute at subsequent points in time, when their score 423 | // is higher in the list. 424 | // 425 | - (NSArray *)nextTasks { 426 | NSTimeInterval minElapsedTimeSinceLastExecution = 0; 427 | __block NSTimeInterval maxElapsedTimeSinceLastExecution = 0; 428 | 429 | // Calculate the max elapsed time 430 | [self.taskContainers enumerateObjectsUsingBlock:^(SLNTaskContainer *t, NSUInteger __unused idx, BOOL * __unused stop) { 431 | maxElapsedTimeSinceLastExecution = MAX([t elapsedTimeSinceLastExecution], maxElapsedTimeSinceLastExecution); 432 | }]; 433 | 434 | // Calculate the score for every task 435 | [self.taskContainers enumerateObjectsUsingBlock:^(SLNTaskContainer *t, NSUInteger __unused idx, BOOL * __unused stop) { 436 | t.score = Score([t.task priority], [t elapsedTimeSinceLastExecution], minElapsedTimeSinceLastExecution, maxElapsedTimeSinceLastExecution); 437 | }]; 438 | 439 | // Sort the tasks by score 440 | NSArray *sortedTasksByScore = [self.taskContainers sortedArrayUsingComparator:^NSComparisonResult(SLNTaskContainer *obj1, SLNTaskContainer *obj2) { 441 | if (obj1.score > obj2.score) { 442 | return NSOrderedAscending; 443 | } else if (obj1.score < obj2.score) { 444 | return NSOrderedDescending; 445 | } else { 446 | return NSOrderedDescending; 447 | } 448 | }]; 449 | 450 | // Determine which tasks to run, by looking at their cost 451 | NSMutableArray *scheduledTasks = [[NSMutableArray alloc] init]; 452 | __block CGFloat totalResponseTime = 0.0; 453 | 454 | [sortedTasksByScore enumerateObjectsUsingBlock:^(SLNTaskContainer *t, NSUInteger __unused idx, BOOL * __unused stop) { 455 | CGFloat average = [t movingAverageResponseTime]; 456 | if (totalResponseTime + average <= kSLNAvailableTime) { 457 | totalResponseTime += average; 458 | [scheduledTasks addObject:t]; 459 | } 460 | }]; 461 | 462 | return scheduledTasks; 463 | } 464 | 465 | - (void)save { 466 | NSMutableDictionary *nextExecutionSchedule = [NSMutableDictionary new]; 467 | for (SLNTaskContainer *t in self.taskContainers) { 468 | nextExecutionSchedule[[t key]] = [t toDictionary]; 469 | } 470 | [self.userDefaults setObject:[NSDictionary dictionaryWithDictionary:nextExecutionSchedule] forKey:kSLNExecutionSchedule]; 471 | [self.userDefaults synchronize]; 472 | } 473 | 474 | @end 475 | -------------------------------------------------------------------------------- /Selene.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1E2003E7AFA4BB27A503170B /* libPods-Selene.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 71E322340C664F634C0A2337 /* libPods-Selene.a */; }; 11 | A10ADD0A19BB7BC60081601C /* Selene.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = A10ADD0919BB7BC60081601C /* Selene.h */; }; 12 | A146DF7419BB7EBB00D1674E /* SeleneTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A146DF7319BB7EBB00D1674E /* SeleneTests.m */; }; 13 | A187F87019BB800C00445D76 /* libSelene.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A10ADD0619BB7BC60081601C /* libSelene.a */; }; 14 | A1AC4A5819BB7CDC003AEDEF /* SLNScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1AC4A5619BB7CDC003AEDEF /* SLNScheduler.m */; }; 15 | A1AC4A5A19BB7D29003AEDEF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1AC4A5919BB7D29003AEDEF /* UIKit.framework */; }; 16 | A1AC4A5C19BB7D30003AEDEF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1AC4A5B19BB7D30003AEDEF /* Foundation.framework */; }; 17 | DB6F1E73FBDA27DD4BBDE730 /* libPods-SeleneTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01304CDE7D643BC63A196EE9 /* libPods-SeleneTests.a */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | A18F809219BB8776008670CB /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = A10ADCFE19BB7BC60081601C /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = A10ADD0519BB7BC60081601C; 26 | remoteInfo = Selene; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXCopyFilesBuildPhase section */ 31 | A10ADD0419BB7BC60081601C /* CopyFiles */ = { 32 | isa = PBXCopyFilesBuildPhase; 33 | buildActionMask = 2147483647; 34 | dstPath = "include/$(PRODUCT_NAME)"; 35 | dstSubfolderSpec = 16; 36 | files = ( 37 | A10ADD0A19BB7BC60081601C /* Selene.h in CopyFiles */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXCopyFilesBuildPhase section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 01304CDE7D643BC63A196EE9 /* libPods-SeleneTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SeleneTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 28C411DEDEC34C38461EF59F /* Pods-SeleneTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SeleneTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SeleneTests/Pods-SeleneTests.debug.xcconfig"; sourceTree = ""; }; 46 | 71E322340C664F634C0A2337 /* libPods-Selene.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Selene.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | A10ADD0619BB7BC60081601C /* libSelene.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSelene.a; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | A10ADD0919BB7BC60081601C /* Selene.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Selene.h; sourceTree = ""; }; 49 | A10ADD1119BB7BC60081601C /* SeleneTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SeleneTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | A10ADD1419BB7BC60081601C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | A146DF7319BB7EBB00D1674E /* SeleneTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SeleneTests.m; sourceTree = ""; }; 52 | A1AC4A5519BB7CDC003AEDEF /* SLNScheduler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLNScheduler.h; sourceTree = ""; }; 53 | A1AC4A5619BB7CDC003AEDEF /* SLNScheduler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLNScheduler.m; sourceTree = ""; }; 54 | A1AC4A5719BB7CDC003AEDEF /* SLNTaskProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLNTaskProtocol.h; sourceTree = ""; }; 55 | A1AC4A5919BB7D29003AEDEF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 56 | A1AC4A5B19BB7D30003AEDEF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 57 | A1AC4A5E19BB7D59003AEDEF /* Selene-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Selene-Prefix.pch"; sourceTree = ""; }; 58 | B06CE0C4C7A008F6C63DCC35 /* Pods-Selene.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Selene.release.xcconfig"; path = "Pods/Target Support Files/Pods-Selene/Pods-Selene.release.xcconfig"; sourceTree = ""; }; 59 | C7E40E522E4DCDA253ED4DAB /* Pods-Selene.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Selene.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Selene/Pods-Selene.debug.xcconfig"; sourceTree = ""; }; 60 | CC084F3BCF5971FB30A918E3 /* Pods-SeleneTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SeleneTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SeleneTests/Pods-SeleneTests.release.xcconfig"; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | A10ADD0319BB7BC60081601C /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | A1AC4A5C19BB7D30003AEDEF /* Foundation.framework in Frameworks */, 69 | A1AC4A5A19BB7D29003AEDEF /* UIKit.framework in Frameworks */, 70 | 1E2003E7AFA4BB27A503170B /* libPods-Selene.a in Frameworks */, 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | A10ADD0E19BB7BC60081601C /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | A187F87019BB800C00445D76 /* libSelene.a in Frameworks */, 79 | DB6F1E73FBDA27DD4BBDE730 /* libPods-SeleneTests.a in Frameworks */, 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | /* End PBXFrameworksBuildPhase section */ 84 | 85 | /* Begin PBXGroup section */ 86 | 15A61601E71D669010443B84 /* Pods */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | C7E40E522E4DCDA253ED4DAB /* Pods-Selene.debug.xcconfig */, 90 | B06CE0C4C7A008F6C63DCC35 /* Pods-Selene.release.xcconfig */, 91 | 28C411DEDEC34C38461EF59F /* Pods-SeleneTests.debug.xcconfig */, 92 | CC084F3BCF5971FB30A918E3 /* Pods-SeleneTests.release.xcconfig */, 93 | ); 94 | name = Pods; 95 | sourceTree = ""; 96 | }; 97 | A10ADCFD19BB7BC60081601C = { 98 | isa = PBXGroup; 99 | children = ( 100 | A10ADD0819BB7BC60081601C /* Selene */, 101 | A10ADD1219BB7BC60081601C /* SeleneTests */, 102 | A1AC4A5D19BB7D37003AEDEF /* Frameworks */, 103 | A10ADD0719BB7BC60081601C /* Products */, 104 | 15A61601E71D669010443B84 /* Pods */, 105 | ); 106 | sourceTree = ""; 107 | }; 108 | A10ADD0719BB7BC60081601C /* Products */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | A10ADD0619BB7BC60081601C /* libSelene.a */, 112 | A10ADD1119BB7BC60081601C /* SeleneTests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | A10ADD0819BB7BC60081601C /* Selene */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | A10ADD0919BB7BC60081601C /* Selene.h */, 121 | A1AC4A5519BB7CDC003AEDEF /* SLNScheduler.h */, 122 | A1AC4A5619BB7CDC003AEDEF /* SLNScheduler.m */, 123 | A1AC4A5719BB7CDC003AEDEF /* SLNTaskProtocol.h */, 124 | A146DF7219BB7E9600D1674E /* Supporting Files */, 125 | ); 126 | path = Selene; 127 | sourceTree = ""; 128 | }; 129 | A10ADD1219BB7BC60081601C /* SeleneTests */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | A146DF7319BB7EBB00D1674E /* SeleneTests.m */, 133 | A10ADD1319BB7BC60081601C /* Supporting Files */, 134 | ); 135 | path = SeleneTests; 136 | sourceTree = ""; 137 | }; 138 | A10ADD1319BB7BC60081601C /* Supporting Files */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | A10ADD1419BB7BC60081601C /* Info.plist */, 142 | ); 143 | name = "Supporting Files"; 144 | sourceTree = ""; 145 | }; 146 | A146DF7219BB7E9600D1674E /* Supporting Files */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | A1AC4A5E19BB7D59003AEDEF /* Selene-Prefix.pch */, 150 | ); 151 | name = "Supporting Files"; 152 | sourceTree = ""; 153 | }; 154 | A1AC4A5D19BB7D37003AEDEF /* Frameworks */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | A1AC4A5B19BB7D30003AEDEF /* Foundation.framework */, 158 | A1AC4A5919BB7D29003AEDEF /* UIKit.framework */, 159 | 71E322340C664F634C0A2337 /* libPods-Selene.a */, 160 | 01304CDE7D643BC63A196EE9 /* libPods-SeleneTests.a */, 161 | ); 162 | name = Frameworks; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXGroup section */ 166 | 167 | /* Begin PBXNativeTarget section */ 168 | A10ADD0519BB7BC60081601C /* Selene */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = A10ADD1719BB7BC60081601C /* Build configuration list for PBXNativeTarget "Selene" */; 171 | buildPhases = ( 172 | 81AA89D1EBE683A63B94A79A /* [CP] Check Pods Manifest.lock */, 173 | A10ADD0219BB7BC60081601C /* Sources */, 174 | A10ADD0319BB7BC60081601C /* Frameworks */, 175 | A10ADD0419BB7BC60081601C /* CopyFiles */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | ); 181 | name = Selene; 182 | productName = Selene; 183 | productReference = A10ADD0619BB7BC60081601C /* libSelene.a */; 184 | productType = "com.apple.product-type.library.static"; 185 | }; 186 | A10ADD1019BB7BC60081601C /* SeleneTests */ = { 187 | isa = PBXNativeTarget; 188 | buildConfigurationList = A10ADD1A19BB7BC60081601C /* Build configuration list for PBXNativeTarget "SeleneTests" */; 189 | buildPhases = ( 190 | 619AA4B3D0E89C57CEF0131E /* [CP] Check Pods Manifest.lock */, 191 | A10ADD0D19BB7BC60081601C /* Sources */, 192 | A10ADD0E19BB7BC60081601C /* Frameworks */, 193 | A10ADD0F19BB7BC60081601C /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | A18F809319BB8776008670CB /* PBXTargetDependency */, 199 | ); 200 | name = SeleneTests; 201 | productName = SeleneTests; 202 | productReference = A10ADD1119BB7BC60081601C /* SeleneTests.xctest */; 203 | productType = "com.apple.product-type.bundle.unit-test"; 204 | }; 205 | /* End PBXNativeTarget section */ 206 | 207 | /* Begin PBXProject section */ 208 | A10ADCFE19BB7BC60081601C /* Project object */ = { 209 | isa = PBXProject; 210 | attributes = { 211 | CLASSPREFIX = SLN; 212 | LastUpgradeCheck = 0940; 213 | ORGANIZATIONNAME = LinkedIn; 214 | TargetAttributes = { 215 | A10ADD0519BB7BC60081601C = { 216 | CreatedOnToolsVersion = 6.0; 217 | }; 218 | A10ADD1019BB7BC60081601C = { 219 | CreatedOnToolsVersion = 6.0; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = A10ADD0119BB7BC60081601C /* Build configuration list for PBXProject "Selene" */; 224 | compatibilityVersion = "Xcode 3.2"; 225 | developmentRegion = English; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | ); 230 | mainGroup = A10ADCFD19BB7BC60081601C; 231 | productRefGroup = A10ADD0719BB7BC60081601C /* Products */; 232 | projectDirPath = ""; 233 | projectRoot = ""; 234 | targets = ( 235 | A10ADD0519BB7BC60081601C /* Selene */, 236 | A10ADD1019BB7BC60081601C /* SeleneTests */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | A10ADD0F19BB7BC60081601C /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXResourcesBuildPhase section */ 250 | 251 | /* Begin PBXShellScriptBuildPhase section */ 252 | 619AA4B3D0E89C57CEF0131E /* [CP] Check Pods Manifest.lock */ = { 253 | isa = PBXShellScriptBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | ); 257 | inputPaths = ( 258 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 259 | "${PODS_ROOT}/Manifest.lock", 260 | ); 261 | name = "[CP] Check Pods Manifest.lock"; 262 | outputPaths = ( 263 | "$(DERIVED_FILE_DIR)/Pods-SeleneTests-checkManifestLockResult.txt", 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | shellPath = /bin/sh; 267 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 268 | showEnvVarsInLog = 0; 269 | }; 270 | 81AA89D1EBE683A63B94A79A /* [CP] Check Pods Manifest.lock */ = { 271 | isa = PBXShellScriptBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | ); 275 | inputPaths = ( 276 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 277 | "${PODS_ROOT}/Manifest.lock", 278 | ); 279 | name = "[CP] Check Pods Manifest.lock"; 280 | outputPaths = ( 281 | "$(DERIVED_FILE_DIR)/Pods-Selene-checkManifestLockResult.txt", 282 | ); 283 | runOnlyForDeploymentPostprocessing = 0; 284 | shellPath = /bin/sh; 285 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 286 | showEnvVarsInLog = 0; 287 | }; 288 | /* End PBXShellScriptBuildPhase section */ 289 | 290 | /* Begin PBXSourcesBuildPhase section */ 291 | A10ADD0219BB7BC60081601C /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | A1AC4A5819BB7CDC003AEDEF /* SLNScheduler.m in Sources */, 296 | ); 297 | runOnlyForDeploymentPostprocessing = 0; 298 | }; 299 | A10ADD0D19BB7BC60081601C /* Sources */ = { 300 | isa = PBXSourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | A146DF7419BB7EBB00D1674E /* SeleneTests.m in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXSourcesBuildPhase section */ 308 | 309 | /* Begin PBXTargetDependency section */ 310 | A18F809319BB8776008670CB /* PBXTargetDependency */ = { 311 | isa = PBXTargetDependency; 312 | target = A10ADD0519BB7BC60081601C /* Selene */; 313 | targetProxy = A18F809219BB8776008670CB /* PBXContainerItemProxy */; 314 | }; 315 | /* End PBXTargetDependency section */ 316 | 317 | /* Begin XCBuildConfiguration section */ 318 | A10ADD1519BB7BC60081601C /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 323 | CLANG_CXX_LIBRARY = "libc++"; 324 | CLANG_ENABLE_MODULES = YES; 325 | CLANG_ENABLE_OBJC_ARC = YES; 326 | CLANG_WARN_ASSIGN_ENUM = YES; 327 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES_ERROR; 328 | CLANG_WARN_BOOL_CONVERSION = YES; 329 | CLANG_WARN_COMMA = YES_ERROR; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_CXX0X_EXTENSIONS = YES; 332 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 333 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 334 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 342 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; 344 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 345 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 346 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 347 | CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR; 348 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; 349 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 350 | CLANG_WARN_UNREACHABLE_CODE = YES; 351 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 352 | CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; 353 | COPY_PHASE_STRIP = NO; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | ENABLE_TESTABILITY = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu99; 357 | GCC_DYNAMIC_NO_PIC = NO; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | GCC_OPTIMIZATION_LEVEL = 0; 360 | GCC_PREPROCESSOR_DEFINITIONS = ( 361 | "DEBUG=1", 362 | "$(inherited)", 363 | ); 364 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 365 | GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; 366 | GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; 367 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 369 | "GCC_WARN_64_TO_32_BIT_CONVERSION[arch=*64]" = YES_ERROR; 370 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 371 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; 374 | GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; 375 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 376 | GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; 377 | GCC_WARN_SHADOW = YES; 378 | GCC_WARN_SIGN_COMPARE = YES; 379 | GCC_WARN_STRICT_SELECTOR_MATCH = YES; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 383 | GCC_WARN_UNUSED_FUNCTION = YES; 384 | GCC_WARN_UNUSED_LABEL = YES; 385 | GCC_WARN_UNUSED_PARAMETER = YES; 386 | GCC_WARN_UNUSED_VARIABLE = YES; 387 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 388 | MTL_ENABLE_DEBUG_INFO = YES; 389 | ONLY_ACTIVE_ARCH = YES; 390 | SDKROOT = iphoneos; 391 | }; 392 | name = Debug; 393 | }; 394 | A10ADD1619BB7BC60081601C /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ALWAYS_SEARCH_USER_PATHS = NO; 398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 399 | CLANG_CXX_LIBRARY = "libc++"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_WARN_ASSIGN_ENUM = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES_ERROR; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES_ERROR; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_CXX0X_EXTENSIONS = YES; 408 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 411 | CLANG_WARN_EMPTY_BODY = YES; 412 | CLANG_WARN_ENUM_CONVERSION = YES; 413 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 418 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 419 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; 420 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 421 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 422 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 423 | CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR; 424 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; 425 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 426 | CLANG_WARN_UNREACHABLE_CODE = YES; 427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 428 | CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; 429 | COPY_PHASE_STRIP = YES; 430 | ENABLE_NS_ASSERTIONS = NO; 431 | ENABLE_STRICT_OBJC_MSGSEND = YES; 432 | GCC_C_LANGUAGE_STANDARD = gnu99; 433 | GCC_NO_COMMON_BLOCKS = YES; 434 | GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; 435 | GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; 436 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | "GCC_WARN_64_TO_32_BIT_CONVERSION[arch=*64]" = YES_ERROR; 439 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 440 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; 443 | GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; 444 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 445 | GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; 446 | GCC_WARN_SHADOW = YES; 447 | GCC_WARN_SIGN_COMPARE = YES; 448 | GCC_WARN_STRICT_SELECTOR_MATCH = YES; 449 | GCC_WARN_UNDECLARED_SELECTOR = YES; 450 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 451 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 452 | GCC_WARN_UNUSED_FUNCTION = YES; 453 | GCC_WARN_UNUSED_LABEL = YES; 454 | GCC_WARN_UNUSED_PARAMETER = YES; 455 | GCC_WARN_UNUSED_VARIABLE = YES; 456 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 457 | MTL_ENABLE_DEBUG_INFO = NO; 458 | SDKROOT = iphoneos; 459 | VALIDATE_PRODUCT = YES; 460 | }; 461 | name = Release; 462 | }; 463 | A10ADD1819BB7BC60081601C /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | baseConfigurationReference = C7E40E522E4DCDA253ED4DAB /* Pods-Selene.debug.xcconfig */; 466 | buildSettings = { 467 | GCC_PREFIX_HEADER = "Selene/Selene-Prefix.pch"; 468 | OTHER_LDFLAGS = "-ObjC"; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | SKIP_INSTALL = YES; 471 | }; 472 | name = Debug; 473 | }; 474 | A10ADD1919BB7BC60081601C /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | baseConfigurationReference = B06CE0C4C7A008F6C63DCC35 /* Pods-Selene.release.xcconfig */; 477 | buildSettings = { 478 | GCC_PREFIX_HEADER = "Selene/Selene-Prefix.pch"; 479 | OTHER_LDFLAGS = "-ObjC"; 480 | PRODUCT_NAME = "$(TARGET_NAME)"; 481 | SKIP_INSTALL = YES; 482 | }; 483 | name = Release; 484 | }; 485 | A10ADD1B19BB7BC60081601C /* Debug */ = { 486 | isa = XCBuildConfiguration; 487 | baseConfigurationReference = 28C411DEDEC34C38461EF59F /* Pods-SeleneTests.debug.xcconfig */; 488 | buildSettings = { 489 | GCC_PREFIX_HEADER = "Selene/Selene-Prefix.pch"; 490 | GCC_PREPROCESSOR_DEFINITIONS = ( 491 | "DEBUG=1", 492 | "$(inherited)", 493 | ); 494 | INFOPLIST_FILE = SeleneTests/Info.plist; 495 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 496 | PRODUCT_BUNDLE_IDENTIFIER = "com.linkedin.$(PRODUCT_NAME:rfc1034identifier)"; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | }; 499 | name = Debug; 500 | }; 501 | A10ADD1C19BB7BC60081601C /* Release */ = { 502 | isa = XCBuildConfiguration; 503 | baseConfigurationReference = CC084F3BCF5971FB30A918E3 /* Pods-SeleneTests.release.xcconfig */; 504 | buildSettings = { 505 | GCC_PREFIX_HEADER = "Selene/Selene-Prefix.pch"; 506 | INFOPLIST_FILE = SeleneTests/Info.plist; 507 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 508 | PRODUCT_BUNDLE_IDENTIFIER = "com.linkedin.$(PRODUCT_NAME:rfc1034identifier)"; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | }; 511 | name = Release; 512 | }; 513 | /* End XCBuildConfiguration section */ 514 | 515 | /* Begin XCConfigurationList section */ 516 | A10ADD0119BB7BC60081601C /* Build configuration list for PBXProject "Selene" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | A10ADD1519BB7BC60081601C /* Debug */, 520 | A10ADD1619BB7BC60081601C /* Release */, 521 | ); 522 | defaultConfigurationIsVisible = 0; 523 | defaultConfigurationName = Release; 524 | }; 525 | A10ADD1719BB7BC60081601C /* Build configuration list for PBXNativeTarget "Selene" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | A10ADD1819BB7BC60081601C /* Debug */, 529 | A10ADD1919BB7BC60081601C /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | A10ADD1A19BB7BC60081601C /* Build configuration list for PBXNativeTarget "SeleneTests" */ = { 535 | isa = XCConfigurationList; 536 | buildConfigurations = ( 537 | A10ADD1B19BB7BC60081601C /* Debug */, 538 | A10ADD1C19BB7BC60081601C /* Release */, 539 | ); 540 | defaultConfigurationIsVisible = 0; 541 | defaultConfigurationName = Release; 542 | }; 543 | /* End XCConfigurationList section */ 544 | }; 545 | rootObject = A10ADCFE19BB7BC60081601C /* Project object */; 546 | } 547 | --------------------------------------------------------------------------------