diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8d73b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# OS X +.DS_Store + +# Xcode +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +profile +*.moved-aside +DerivedData +*.hmap +*.ipa + +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? +# +# Pods/ + diff --git a/Classes/ABX.h b/Classes/ABX.h new file mode 100644 index 0000000..3b2d041 --- /dev/null +++ b/Classes/ABX.h @@ -0,0 +1,12 @@ +#import "ABXApiClient.h" + +#import "ABXFaq.h" +#import "ABXNotification.h" +#import "ABXVersion.h" +#import "ABXIssue.h" + +#import "ABXFAQsViewController.h" +#import "ABXVersionsViewController.h" +#import "ABXFeedbackViewController.h" + +#import "ABXNotificationView.h" \ No newline at end of file diff --git a/Classes/Classes/ABXApiClient.h b/Classes/Classes/ABXApiClient.h new file mode 100644 index 0000000..f62a9d3 --- /dev/null +++ b/Classes/Classes/ABXApiClient.h @@ -0,0 +1,35 @@ +// +// ABXApiClient.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +// Error codes +typedef enum { + ABXResponseCodeSuccess, // Request completed successfully + ABXResponseCodeErrorAuth, // Check your bundle identifier and API key + ABXResponseCodeErrorExpired, // Account requires payment + ABXResponseCodeErrorDecoding, // Error decoding the JSON data + ABXResponseCodeErrorEncoding, // Error encoding the post/put request + ABXResponseCodeErrorNotFound, // Not found + ABXResponseCodeErrorUnknown // Unknown error +} ABXResponseCode; + +typedef void (^ABXRequestCompletion)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON); + +@interface ABXApiClient : NSObject + ++ (ABXApiClient*)instance; + +- (void)setApiKey:(NSString *)apiKey; + +- (NSURLSessionDataTask*)GET:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; + +- (NSURLSessionDataTask*)POST:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; + +- (NSURLSessionDataTask*)PUT:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete; + +@end diff --git a/Classes/Classes/ABXApiClient.m b/Classes/Classes/ABXApiClient.m new file mode 100644 index 0000000..cda1ece --- /dev/null +++ b/Classes/Classes/ABXApiClient.m @@ -0,0 +1,227 @@ +// +// ABXApiClient.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXApiClient.h" + +#import "NSDictionary+ABXQueryString.h" +#import "NSDictionary+ABXNSNullAsNull.h" + +@interface ABXApiClient () + +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, strong) NSOperationQueue *queue; + +@property (nonatomic, copy) NSString *apiKey; + +@end + +@implementation ABXApiClient + +static NSString *kAppbotUrl = @"https://api.appbot.co/v1"; + ++ (ABXApiClient*)instance +{ + static dispatch_once_t onceToken; + static ABXApiClient *client = nil; + dispatch_once(&onceToken, ^{ + client = [[ABXApiClient alloc] init]; + }); + return client; +} + +#pragma mark - Init + +- (id)init +{ + self = [super init]; + if (self) { + // Setup the request queue + self.queue = [[NSOperationQueue alloc] init]; + _queue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; + + // Setup our session + self.session = [NSURLSession sessionWithConfiguration:nil delegate:nil delegateQueue:_queue]; + } + return self; +} + +#pragma mark - Requests + +- (NSURLSessionDataTask*)GET:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete +{ + NSDictionary *parameters = [self combineDefaultParamsWith:params]; + + // Create our URL + NSURL *url = [[NSURL URLWithString:kAppbotUrl] URLByAppendingPathComponent:path]; + NSString *query = [parameters queryStringValue]; + url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:url.query ? @"&%@" : @"?%@", query]]; + + // Create the request + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.allowsCellularAccess = YES; + request.HTTPMethod = @"GET"; + return [self performRequest:request complete:complete]; +} + +- (NSURLSessionDataTask*)POST:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete +{ + return [self httpBodyRequest:@"POST" path:path params:params complete:complete]; +} + +- (NSURLSessionDataTask*)PUT:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete +{ + return [self httpBodyRequest:@"PUT" path:path params:params complete:complete]; +} + +- (NSURLSessionDataTask*)httpBodyRequest:(NSString*)method path:(NSString*)path params:(NSDictionary*)params complete:(ABXRequestCompletion)complete +{ + NSDictionary *parameters = [self combineDefaultParamsWith:params]; + + // Create our URL + NSURL *url = [[NSURL URLWithString:kAppbotUrl] URLByAppendingPathComponent:path]; + NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); + + // Create the request + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setValue:[NSString stringWithFormat:@"application/json; charset=%@", charset] forHTTPHeaderField:@"Content-Type"]; + NSError *error = nil; + [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters options:0 error:&error]]; + if (error) { + // Error setting the HTTP body + if (complete) { + complete(ABXResponseCodeErrorEncoding, -1, error, nil); + } + return nil; + } + else { + request.allowsCellularAccess = YES; + request.HTTPMethod = method; + return [self performRequest:request complete:complete]; + } +} + +- (NSURLSessionDataTask*)performRequest:(NSURLRequest*)request complete:(ABXRequestCompletion)complete +{ + // https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html#//apple_ref/doc/uid/TP40013174-CH14-SW1 + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { + // iOS 6.1 and below + [NSURLConnection sendAsynchronousRequest:request + queue:self.queue + completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + [self handleResponse:response data:data error:error complete:complete]; + }]; + return nil; + } + else { + // iOS 7 + NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [self handleResponse:response data:data error:error complete:complete]; }]; + [task resume]; + return task; + } +} + +- (void)handleResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error complete:(ABXRequestCompletion)complete +{ + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; + NSInteger httpCode = [httpResponse statusCode]; + if (httpCode >= 200 && httpCode < 300) { + [self handleRequestSuccess:httpCode data:data complete:complete]; + } + else { + [self handleRequestFailure:httpCode error:error complete:complete]; + } +} + +- (void)handleRequestSuccess:(NSInteger)httpCode data:(NSData*)data complete:(ABXRequestCompletion)complete +{ + NSError *jsonError = nil; + NSDictionary *json = nil; + + if (data != nil && data.length > 0) { + json = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&jsonError]; + } + + if (jsonError) { + // JSON error + if (complete) { + dispatch_async(dispatch_get_main_queue(), ^{ + complete(ABXResponseCodeErrorDecoding, httpCode, jsonError, nil); + }); + } + } + else { + // Success! + if (complete) { + dispatch_async(dispatch_get_main_queue(), ^{ + complete(ABXResponseCodeSuccess, httpCode, nil, json); + }); + } + } +} + +- (void)handleRequestFailure:(NSInteger)httpCode error:(NSError*)error complete:(ABXRequestCompletion)complete +{ + // Work out which error code + ABXResponseCode responseCode = ABXResponseCodeErrorUnknown; + switch (httpCode) { + case 401: + responseCode = ABXResponseCodeErrorAuth; + break; + + case 402: + responseCode = ABXResponseCodeErrorExpired; + break; + } + + if (complete) { + dispatch_async(dispatch_get_main_queue(), ^{ + complete(responseCode, httpCode, error, nil); + }); + } +} + +#pragma mark - Key + +- (void)validateApiKey +{ + // The API key must always be set + assert(_apiKey); + if (_apiKey == nil || _apiKey.length == 0) { + NSException* myException = [NSException + exceptionWithName:@"InvalidKeyException" + reason:@"Key is not valid." + userInfo:nil]; + @throw myException; + } +} + +#pragma mark - Params + +- (NSDictionary*)combineDefaultParamsWith:(NSDictionary*)params +{ + [self validateApiKey]; + + NSDictionary *defaultParams = @{ @"bundle_identifier" : [[NSBundle mainBundle] bundleIdentifier], + @"key" : _apiKey }; + if (params == nil) { + // If there are no other params just use they key and bundle + return defaultParams; + } + else { + // Append the default values + NSMutableDictionary *mutableParams = [params mutableCopy]; + [mutableParams addEntriesFromDictionary:defaultParams]; + return mutableParams; + } +} + + +@end diff --git a/Classes/Classes/ABXKeychain.h b/Classes/Classes/ABXKeychain.h new file mode 100644 index 0000000..d1fc9b2 --- /dev/null +++ b/Classes/Classes/ABXKeychain.h @@ -0,0 +1,89 @@ +// +// Altered Version of https://github.com/nicklockwood/FXKeychain +// Altered to not conflict with any existing uses of the library +// +// FXKeychain.h +// +// Version 1.5 beta +// +// Created by Nick Lockwood on 29/12/2012. +// Copyright 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/FXKeychain +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import + +#import + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis" + + +#ifndef APPBOTKEYCHAIN_USE_NSCODING +#if TARGET_OS_IPHONE +#define APPBOTKEYCHAIN_USE_NSCODING 1 +#else +#define APPBOTKEYCHAIN_USE_NSCODING 0 +#endif +#endif + + +typedef NS_ENUM(NSInteger, ABXKeychainAccess) +{ + ABXKeychainAccessibleWhenUnlocked = 0, + ABXKeychainAccessibleAfterFirstUnlock, + ABXKeychainAccessibleAlways, + ABXKeychainAccessibleWhenUnlockedThisDeviceOnly, + ABXKeychainAccessibleAfterFirstUnlockThisDeviceOnly, + ABXKeychainAccessibleAlwaysThisDeviceOnly +}; + + +@interface ABXKeychain : NSObject + ++ (instancetype)defaultKeychain; + +@property (nonatomic, readonly) NSString *service; +@property (nonatomic, readonly) NSString *accessGroup; +@property (nonatomic, assign) ABXKeychainAccess accessibility; + +- (id)initWithService:(NSString *)service + accessGroup:(NSString *)accessGroup + accessibility:(ABXKeychainAccess)accessibility; + +- (id)initWithService:(NSString *)service + accessGroup:(NSString *)accessGroup; + +- (BOOL)setObject:(id)object forKey:(id)key; +- (BOOL)setObject:(id)object forKeyedSubscript:(id)key; +- (BOOL)removeObjectForKey:(id)key; +- (id)objectForKey:(id)key; +- (id)objectForKeyedSubscript:(id)key; + +@end + + +#pragma GCC diagnostic pop diff --git a/Classes/Classes/ABXKeychain.m b/Classes/Classes/ABXKeychain.m new file mode 100644 index 0000000..d67469c --- /dev/null +++ b/Classes/Classes/ABXKeychain.m @@ -0,0 +1,341 @@ +// +// Altered Version of https://github.com/nicklockwood/FXKeychain +// Altered to not conflict with any existing uses of the library +// +// FXKeychain.m +// +// Version 1.5 beta +// +// Created by Nick Lockwood on 29/12/2012. +// Copyright 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/FXKeychain +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import "ABXKeychain.h" + +#import +#if !__has_feature(objc_arc) +#error This class requires automatic reference counting +#endif + + +@implementation NSObject (AppBotKeychainPropertyListCoding) + +- (id)AppBotKeychain_propertyListRepresentation +{ + return self; +} + +@end + +#if !APPBOTKEYCHAIN_USE_NSCODING + +@implementation NSNull (AppBotKeychainPropertyListCoding) + +- (id)AppBotKeychain_propertyListRepresentation +{ + return nil; +} + +@end + + +@implementation NSArray (BMPropertyListCoding) + +- (id)AppBotKeychain_propertyListRepresentation +{ + NSMutableArray *copy = [NSMutableArray arrayWithCapacity:[self count]]; + [self enumerateObjectsUsingBlock:^(__unsafe_unretained id obj, __unused NSUInteger idx, __unused BOOL *stop) { + id value = [obj AppBotKeychain_propertyListRepresentation]; + if (value) [copy addObject:value]; + }]; + return copy; +} + +@end + + +@implementation NSDictionary (BMPropertyListCoding) + +- (id)AppBotKeychain_propertyListRepresentation +{ + NSMutableDictionary *copy = [NSMutableDictionary dictionaryWithCapacity:[self count]]; + [self enumerateKeysAndObjectsUsingBlock:^(__unsafe_unretained id key, __unsafe_unretained id obj, __unused BOOL *stop) { + id value = [obj AppBotKeychain_propertyListRepresentation]; + if (value) copy[key] = value; + }]; + return copy; +} + +@end + +#endif + +@implementation ABXKeychain + ++ (instancetype)defaultKeychain +{ + static id sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]; + sharedInstance = [[ABXKeychain alloc] initWithService:bundleID + accessGroup:nil]; + }); + + return sharedInstance; +} + +- (id)init +{ + return [self initWithService:nil accessGroup:nil]; +} + +- (id)initWithService:(NSString *)service + accessGroup:(NSString *)accessGroup +{ + return [self initWithService:service + accessGroup:accessGroup + accessibility:ABXKeychainAccessibleWhenUnlocked]; +} + +- (id)initWithService:(NSString *)service + accessGroup:(NSString *)accessGroup + accessibility:(ABXKeychainAccess)accessibility +{ + if ((self = [super init])) + { + _service = [service copy]; + _accessGroup = [accessGroup copy]; + _accessibility = accessibility; + } + return self; +} + +- (NSData *)dataForKey:(id)key +{ + //generate query + NSMutableDictionary *query = [NSMutableDictionary dictionary]; + if ([self.service length]) query[(__bridge NSString *)kSecAttrService] = self.service; + query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword; + query[(__bridge NSString *)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; + query[(__bridge NSString *)kSecReturnData] = (__bridge id)kCFBooleanTrue; + query[(__bridge NSString *)kSecAttrAccount] = [key description]; + +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + if ([_accessGroup length]) query[(__bridge NSString *)kSecAttrAccessGroup] = _accessGroup; +#endif + + //recover data + CFDataRef data = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&data); + if (status != errSecSuccess && status != errSecItemNotFound) + { + NSLog(@"AppBotKeychain failed to retrieve data for key '%@', error: %ld", key, (long)status); + } + return CFBridgingRelease(data); +} + +- (BOOL)setObject:(id)object forKey:(id)key +{ + NSParameterAssert(key); + + //generate query + NSMutableDictionary *query = [NSMutableDictionary dictionary]; + if ([self.service length]) query[(__bridge NSString *)kSecAttrService] = self.service; + query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword; + query[(__bridge NSString *)kSecAttrAccount] = [key description]; + +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + if ([_accessGroup length]) query[(__bridge NSString *)kSecAttrAccessGroup] = _accessGroup; +#endif + + //encode object + NSData *data = nil; + NSError *error = nil; + if ([(id)object isKindOfClass:[NSString class]]) + { + //check that string data does not represent a binary plist + NSPropertyListFormat format = NSPropertyListBinaryFormat_v1_0; + if (![object hasPrefix:@"bplist"] || ![NSPropertyListSerialization propertyListWithData:[object dataUsingEncoding:NSUTF8StringEncoding] + options:NSPropertyListImmutable + format:&format + error:NULL]) + { + //safe to encode as a string + data = [object dataUsingEncoding:NSUTF8StringEncoding]; + } + } + + //if not encoded as a string, encode as plist + if (object && !data) + { + data = [NSPropertyListSerialization dataWithPropertyList:[object AppBotKeychain_propertyListRepresentation] + format:NSPropertyListBinaryFormat_v1_0 + options:0 + error:&error]; +#if APPBOTKEYCHAIN_USE_NSCODING + + //property list encoding failed. try NSCoding + if (!data) + { + data = [NSKeyedArchiver archivedDataWithRootObject:object]; + } + +#endif + + } + + //fail if object is invalid + NSAssert(!object || (object && data), @"AppBotKeychain failed to encode object for key '%@', error: %@", key, error); + + if (data) + { + //update values + NSMutableDictionary *update = [@{(__bridge NSString *)kSecValueData: data} mutableCopy]; + +#if TARGET_OS_IPHONE || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 + + update[(__bridge NSString *)kSecAttrAccessible] = @[(__bridge id)kSecAttrAccessibleWhenUnlocked, + (__bridge id)kSecAttrAccessibleAfterFirstUnlock, + (__bridge id)kSecAttrAccessibleAlways, + (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly][self.accessibility]; +#endif + + //write data + OSStatus status = errSecSuccess; + if ([self dataForKey:key]) + { + //there's already existing data for this key, update it + status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update); + } + else + { + //no existing data, add a new item + [query addEntriesFromDictionary:update]; + status = SecItemAdd ((__bridge CFDictionaryRef)query, NULL); + } + if (status != errSecSuccess) + { + NSLog(@"AppBotKeychain failed to store data for key '%@', error: %ld", key, (long)status); + return NO; + } + } + else + { + //delete existing data + +#if TARGET_OS_IPHONE + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); +#else + CFTypeRef result = NULL; + query[(__bridge id)kSecReturnRef] = (__bridge id)kCFBooleanTrue; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); + if (status == errSecSuccess) + { + status = SecKeychainItemDelete((SecKeychainItemRef) result); + CFRelease(result); + } +#endif + if (status != errSecSuccess) + { + NSLog(@"AppBotKeychain failed to delete data for key '%@', error: %ld", key, (long)status); + return NO; + } + } + return YES; +} + +- (BOOL)setObject:(id)object forKeyedSubscript:(id)key +{ + return [self setObject:object forKey:key]; +} + +- (BOOL)removeObjectForKey:(id)key +{ + return [self setObject:nil forKey:key]; +} + +- (id)objectForKey:(id)key +{ + NSData *data = [self dataForKey:key]; + if (data) + { + id object = nil; + NSError *error = nil; + NSPropertyListFormat format = NSPropertyListBinaryFormat_v1_0; + + //check if data is a binary plist + if ([data length] >= 6 && !strncmp("bplist", data.bytes, 6)) + { + //attempt to decode as a plist + object = [NSPropertyListSerialization propertyListWithData:data + options:NSPropertyListImmutable + format:&format + error:&error]; + + if ([object respondsToSelector:@selector(objectForKey:)] && object[@"$archiver"]) + { + //data represents an NSCoded archive + +#if APPBOTKEYCHAIN_USE_NSCODING + + //parse as archive + object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; +#else + //don't trust it + object = nil; +#endif + + } + } + if (!object || format != NSPropertyListBinaryFormat_v1_0) + { + //may be a string + object = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } + if (!object) + { + NSLog(@"AppBotKeychain failed to decode data for key '%@', error: %@", key, error); + } + return object; + } + else + { + //no value found + return nil; + } +} + +- (id)objectForKeyedSubscript:(id)key +{ + return [self objectForKey:key]; +} + +@end diff --git a/Classes/Classes/NSDictionary+ABXNSNullAsNull.h b/Classes/Classes/NSDictionary+ABXNSNullAsNull.h new file mode 100644 index 0000000..80042df --- /dev/null +++ b/Classes/Classes/NSDictionary+ABXNSNullAsNull.h @@ -0,0 +1,14 @@ +// +// NSDictionary+ABXNSNullAsNull.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface NSDictionary (ABXNSNullAsNull) + +- (id)objectForKeyNulled:(id)aKey; + +@end diff --git a/Classes/Classes/NSDictionary+ABXNSNullAsNull.m b/Classes/Classes/NSDictionary+ABXNSNullAsNull.m new file mode 100644 index 0000000..9564dac --- /dev/null +++ b/Classes/Classes/NSDictionary+ABXNSNullAsNull.m @@ -0,0 +1,21 @@ +// +// NSDictionary+ABXNSNullAsNull.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "NSDictionary+ABXNSNullAsNull.h" + +@implementation NSDictionary (ABXNSNullAsNull) + +- (id)objectForKeyNulled:(id)aKey +{ + id value = [self objectForKey:aKey]; + if (!value || [value isKindOfClass:[NSNull class]]) { + return nil; + } + return value; +} + +@end diff --git a/Classes/Classes/NSDictionary+ABXQueryString.h b/Classes/Classes/NSDictionary+ABXQueryString.h new file mode 100644 index 0000000..1ae402e --- /dev/null +++ b/Classes/Classes/NSDictionary+ABXQueryString.h @@ -0,0 +1,14 @@ +// +// NSDictionary+ABXQueryString.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface NSDictionary (ABXQueryString) + +- (NSString *)queryStringValue; + +@end diff --git a/Classes/Classes/NSDictionary+ABXQueryString.m b/Classes/Classes/NSDictionary+ABXQueryString.m new file mode 100644 index 0000000..dc707ae --- /dev/null +++ b/Classes/Classes/NSDictionary+ABXQueryString.m @@ -0,0 +1,27 @@ +// +// NSDictionary+ABXQueryString.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "NSDictionary+ABXQueryString.h" + +#import "NSString+ABXURLEncoding.h" + +@implementation NSDictionary (ABXQueryString) + +- (NSString *)queryStringValue +{ + NSMutableArray *pairs = [NSMutableArray array]; + for (NSString *key in [self keyEnumerator]) + { + id value = [self objectForKey:key]; + NSString *escapedValue = [value urlEncodedString]; + [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escapedValue]]; + } + + return [pairs componentsJoinedByString:@"&"]; +} + +@end diff --git a/Classes/Classes/NSString+ABXSizing.h b/Classes/Classes/NSString+ABXSizing.h new file mode 100644 index 0000000..f43c15b --- /dev/null +++ b/Classes/Classes/NSString+ABXSizing.h @@ -0,0 +1,15 @@ +// +// NSString+ABXSizing.h +// Sample Project +// +// Created by Stuart Hall on 12/06/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface NSString (ABXSizing) + +- (CGFloat)heightForWidth:(CGFloat)width andFont:(UIFont*)font; + +@end diff --git a/Classes/Classes/NSString+ABXSizing.m b/Classes/Classes/NSString+ABXSizing.m new file mode 100644 index 0000000..3322473 --- /dev/null +++ b/Classes/Classes/NSString+ABXSizing.m @@ -0,0 +1,42 @@ +// +// NSString+ABXSizing.m +// +// Created by Stuart Hall on 12/06/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "NSString+ABXSizing.h" + +@implementation NSString (ABXSizing) + +- (CGFloat)heightForWidth:(CGFloat)width andFont:(UIFont*)font +{ + CGSize size; + CGSize constraintSize = CGSizeMake(width, CGFLOAT_MAX); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { + size = [self boundingRectWithSize:constraintSize + options:NSStringDrawingUsesLineFragmentOrigin + attributes:@{NSFontAttributeName:font} + context:nil].size; + } + else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + + size = [self sizeWithFont:font + constrainedToSize:constraintSize + lineBreakMode:NSLineBreakByWordWrapping]; + +#pragma clang diagnostic pop + } +#else + size = [self sizeWithFont:font + constrainedToSize:constraintSize + lineBreakMode:UILineBreakModeWordWrap]; +#endif + + return ceil(size.height); +} + +@end diff --git a/Classes/Classes/NSString+ABXURLEncoding.h b/Classes/Classes/NSString+ABXURLEncoding.h new file mode 100644 index 0000000..ac5a14e --- /dev/null +++ b/Classes/Classes/NSString+ABXURLEncoding.h @@ -0,0 +1,15 @@ +// +// NSString+ABXURLEncoding.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface NSString (ABXURLEncoding) + +- (NSString *)urlEncodedString; +- (NSString *)urlDecodedString; + +@end diff --git a/Classes/Classes/NSString+ABXURLEncoding.m b/Classes/Classes/NSString+ABXURLEncoding.m new file mode 100644 index 0000000..d678fb2 --- /dev/null +++ b/Classes/Classes/NSString+ABXURLEncoding.m @@ -0,0 +1,31 @@ +// +// NSString+ABXURLEncoding.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "NSString+ABXURLEncoding.h" + +@implementation NSString (ABXURLEncoding) + +- (NSString *)urlEncodedString +{ + CFStringRef ref = CFURLCreateStringByAddingPercentEscapes(NULL, + (__bridge CFStringRef)self, + NULL, + (CFStringRef)@":!*();@/&?#[]+$,='%’\"", + kCFStringEncodingUTF8); + return (__bridge_transfer NSString *)(ref); +} + +- (NSString *)urlDecodedString +{ + CFStringRef ref = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, + (__bridge CFStringRef)self, + CFSTR(""), + kCFStringEncodingUTF8); + return (__bridge_transfer NSString *)(ref); +} + +@end diff --git a/Classes/Controllers/ABXBaseListViewController.h b/Classes/Controllers/ABXBaseListViewController.h new file mode 100644 index 0000000..21b5b0a --- /dev/null +++ b/Classes/Controllers/ABXBaseListViewController.h @@ -0,0 +1,21 @@ +// +// ABXBaseListViewController.h +// Sample Project +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface ABXBaseListViewController : UIViewController + +@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, strong) UIActivityIndicatorView *activityView; +@property (nonatomic, strong) UILabel *errorLabel; + ++ (void)showFromController:(UIViewController*)controller; + +- (void)showError:(NSString*)error; + +@end diff --git a/Classes/Controllers/ABXBaseListViewController.m b/Classes/Controllers/ABXBaseListViewController.m new file mode 100644 index 0000000..55f96bf --- /dev/null +++ b/Classes/Controllers/ABXBaseListViewController.m @@ -0,0 +1,125 @@ +// +// ABXBaseListViewController.m +// Sample Project +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXBaseListViewController.h" + +#import "ABXNavigationController.h" + +@interface ABXBaseListViewController() + +@end + +@implementation ABXBaseListViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor whiteColor]; + + [self setupUI]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + ++ (void)showFromController:(UIViewController*)controller +{ + ABXBaseListViewController *viewController = [[self alloc] init]; + UINavigationController *nav = [[ABXNavigationController alloc] initWithRootViewController:viewController]; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // Show as a sheet on iPad + nav.modalPresentationStyle = UIModalPresentationFormSheet; + } + [controller presentViewController:nav animated:YES completion:nil]; +} + +#pragma mark - UI + +- (void)setupUI +{ + // Table view + self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds]; + self.tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + self.tableView.dataSource = (id)self; + self.tableView.delegate = (id)self; + self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectZero]; + self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 44)]; + self.tableView.tableFooterView.backgroundColor = [UIColor clearColor]; + [self.view addSubview:self.tableView]; + + // Powered by + UIButton *appbotButton = [UIButton buttonWithType:UIButtonTypeCustom]; + appbotButton.frame = CGRectMake(0, CGRectGetHeight(self.view.frame) - 33, CGRectGetWidth(self.view.frame), 33); + appbotButton.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1]; + [appbotButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + [appbotButton setTitle:@"Powered by Appbot" forState:UIControlStateNormal]; + appbotButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + appbotButton.titleLabel.font = [UIFont systemFontOfSize:13]; + [appbotButton addTarget:self action:@selector(onAppbot) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:appbotButton]; + + // Powered by seperator + UIView *seperator = [[UIView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 33, CGRectGetWidth(self.view.frame), 1)]; + seperator.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + seperator.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1]; + [self.view addSubview:seperator]; + + // Activity Indicator + self.activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.activityView.center = CGPointMake(CGRectGetMidX(self.view.bounds), 100); + [self.activityView startAnimating]; + self.activityView.hidesWhenStopped = YES; + self.activityView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.view addSubview:self.activityView]; + + // Close button + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(onDone)]; +} + +#pragma mark - Buttons + +- (void)onDone +{ + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)onAppbot +{ + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://appbot.co"]]; +} + +#pragma mark - Errors + +- (void)showError:(NSString*)error +{ + if (!self.errorLabel) { + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, CGRectGetWidth(self.tableView.bounds) - 20, 100)]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.text = error; + label.font = [UIFont systemFontOfSize:15]; + label.textColor = [UIColor blackColor]; + label.backgroundColor = [UIColor clearColor]; + label.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + [self.view addSubview:label]; + self.errorLabel = label; + } + else { + self.errorLabel.text = error; + self.errorLabel.hidden = NO; + } +} + +@end diff --git a/Classes/Controllers/ABXFAQViewController.h b/Classes/Controllers/ABXFAQViewController.h new file mode 100644 index 0000000..24a1cdf --- /dev/null +++ b/Classes/Controllers/ABXFAQViewController.h @@ -0,0 +1,16 @@ +// +// ABXFAQViewController.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@class ABXFaq; + +@interface ABXFAQViewController : UIViewController + +@property (nonatomic, strong) ABXFaq *faq; + +@end diff --git a/Classes/Controllers/ABXFAQViewController.m b/Classes/Controllers/ABXFAQViewController.m new file mode 100644 index 0000000..99e8b5a --- /dev/null +++ b/Classes/Controllers/ABXFAQViewController.m @@ -0,0 +1,211 @@ +// +// ABXFAQViewController.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXFAQViewController.h" + +#import "ABXFaq.h" +#import "ABXFeedbackViewController.h" + +@interface ABXFAQViewController () + +@property (nonatomic, strong) UIWebView *webview; +@property (nonatomic, strong) UIView *bottom; + +@end + +@implementation ABXFAQViewController + +- (void)dealloc +{ + self.webview.delegate = nil; + self.webview = nil; +} + + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = NSLocalizedString(@"FAQ", nil); + + // Webview + CGRect bounds = self.view.bounds; + bounds.size.height -= 44; + self.webview = [[UIWebView alloc] initWithFrame:bounds]; + self.webview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.webview.clipsToBounds = NO; + self.webview.delegate = self; + [self.view addSubview:self.webview]; + + [self addToolbar]; + + // Nav buttons + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithTitle:NSLocalizedString(@"Contact", nil) + style:UIBarButtonItemStylePlain + target:self + action:@selector(onContact)]; + + // Load the HTML + NSString *html = [NSString stringWithFormat: + @"" + @"" + @"" + @"" + @"" + @"

%@

" + @"
%@
" + @"" + "", self.faq.question, self.faq.answer]; + [self.webview loadHTMLString:html + baseURL:nil]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated.r +} + +#pragma mark - Buttons + +- (void)onContact +{ + [ABXFeedbackViewController showFromController:self + placeholder:NSLocalizedString(@"How can we help?", nil)]; +} + +#pragma mark - UI + +- (void)addToolbar +{ + // Toolbar + UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(self.view.frame) - 44, CGRectGetWidth(self.view.frame), 44)]; + bottom.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + bottom.backgroundColor = [UIColor clearColor]; + [self.view addSubview:bottom]; + self.bottom = bottom; + UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:bottom.bounds]; + toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [bottom addSubview:toolbar]; + + // Voting label + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 100, 44)]; + label.font = [UIFont systemFontOfSize:15]; + label.text = NSLocalizedString(@"Helpful?", nil); + label.backgroundColor = [UIColor clearColor]; + [bottom addSubview:label]; + + // Upvote button + UIButton *yesButton = [[UIButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bottom.bounds) - 190, 7, 80, 30)]; + yesButton.backgroundColor = [UIColor colorWithWhite:0.97 alpha:1]; + yesButton.layer.cornerRadius = 4; + yesButton.layer.masksToBounds = YES; + [yesButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + [yesButton setTitle:NSLocalizedString(@"Yes", nil) forState:UIControlStateNormal]; + yesButton.titleLabel.font = [UIFont systemFontOfSize:15]; + yesButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + [yesButton addTarget:self action:@selector(onUpVote) forControlEvents:UIControlEventTouchUpInside]; + [bottom addSubview:yesButton]; + + // Downvote button + UIButton *noButton = [[UIButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bottom.bounds) - 100, 7, 80, 30)]; + noButton.backgroundColor = [UIColor colorWithWhite:0.97 alpha:1]; + noButton.layer.cornerRadius = 4; + noButton.layer.masksToBounds = YES; + [noButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + [noButton setTitle:NSLocalizedString(@"No", nil) forState:UIControlStateNormal]; + noButton.titleLabel.font = [UIFont systemFontOfSize:15]; + noButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + [noButton addTarget:self action:@selector(onDownVote) forControlEvents:UIControlEventTouchUpInside]; + [bottom addSubview:noButton]; +} + +#pragma mark - Buttons + +- (void)onDone +{ + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - Voting + +- (void)clearBottomBar +{ + // Remove the existing views + for (UIView *v in self.bottom.subviews) { + if (![v isKindOfClass:[UIToolbar class]]) { + [v removeFromSuperview]; + } + } +} + +- (void)showVoteLoading +{ + [self clearBottomBar]; + + // Spinner + UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [activity startAnimating]; + activity.center = CGPointMake(32, 22); + [self.bottom addSubview:activity]; + + // Label + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 0, 200, 44)]; + label.font = [UIFont systemFontOfSize:15]; + label.text = NSLocalizedString(@"One moment please...", nil); + label.backgroundColor = [UIColor clearColor]; + [self.bottom addSubview:label]; +} + +- (void)completeVoting +{ + [self clearBottomBar]; + + // Label + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 200, 44)]; + label.font = [UIFont systemFontOfSize:15]; + label.text = NSLocalizedString(@"Thanks for your feedback.", nil); + label.backgroundColor = [UIColor clearColor]; + [self.bottom addSubview:label]; +} + +- (void)onDownVote +{ + // Thumbs down + [self showVoteLoading]; + [self.faq downvote:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + [self completeVoting]; + }]; +} + +- (void)onUpVote +{ + // Thumbsup + [self showVoteLoading]; + [self.faq upvote:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + [self completeVoting]; + }]; + +} + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + if (navigationType == UIWebViewNavigationTypeLinkClicked) { + [[UIApplication sharedApplication] openURL:request.URL]; + return NO; + } + + return YES; +} + +@end diff --git a/Classes/Controllers/ABXFAQsViewController.h b/Classes/Controllers/ABXFAQsViewController.h new file mode 100644 index 0000000..716e193 --- /dev/null +++ b/Classes/Controllers/ABXFAQsViewController.h @@ -0,0 +1,14 @@ +// +// ABXFAQsViewController.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXBaseListViewController.h" + +@interface ABXFAQsViewController : ABXBaseListViewController + +@end diff --git a/Classes/Controllers/ABXFAQsViewController.m b/Classes/Controllers/ABXFAQsViewController.m new file mode 100644 index 0000000..02449dc --- /dev/null +++ b/Classes/Controllers/ABXFAQsViewController.m @@ -0,0 +1,190 @@ +// +// ABXFAQsViewController.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXFAQsViewController.h" + +#import "ABXFaq.h" +#import "ABXFAQViewController.h" +#import "ABXFeedbackViewController.h" + +@interface ABXFAQsViewController () + +@property (nonatomic, strong) NSArray *faqs; +@property (nonatomic, strong) NSArray *filteredFaqs; + +@property (nonatomic, strong) UISearchBar *searchBar; + +@end + +@implementation ABXFAQsViewController + +- (void)dealloc +{ + self.tableView.delegate = nil; + self.tableView.dataSource= nil; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Title + self.title = NSLocalizedString(@"FAQs", nil); + + // Setup our UI components + [self setupFaqUI]; + + // Fetch + [self fetchFAQs]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - UI + +- (void)setupFaqUI +{ + // Search bar + self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), 44)]; + self.searchBar.delegate = self; + self.searchBar.placeholder = NSLocalizedString(@"Search...", nil); + self.tableView.tableHeaderView = self.searchBar; + + // Nav buttons + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithTitle:NSLocalizedString(@"Contact", nil) + style:UIBarButtonItemStylePlain + target:self + action:@selector(onContact)]; +} + +#pragma mark - Fetching + +- (void)fetchFAQs +{ + self.tableView.hidden = YES; + [self.activityView startAnimating]; + [ABXFaq fetch:^(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + [self.activityView stopAnimating]; + if (responseCode == ABXResponseCodeSuccess) { + self.faqs = faqs; + self.filteredFaqs = faqs; + [self.tableView reloadData]; + + if (faqs.count == 0) { + [self showError:NSLocalizedString(@"No FAQs found.", nil)]; + } + else { + self.tableView.hidden = NO; + } + } + else { + [self showError:NSLocalizedString(@"Unable to fetch FAQs.\r\nplease try again later", nil)]; + } + }]; +} + +#pragma mark - Buttons + +- (void)onContact +{ + [ABXFeedbackViewController showFromController:self + placeholder:NSLocalizedString(@"How can we help?", nil)]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.filteredFaqs.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + static NSString *CellIdentifier = @"FAQCell"; + + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + cell.textLabel.font = [UIFont systemFontOfSize:15]; + } + + if (indexPath.row < self.filteredFaqs.count) { + cell.textLabel.text = [[self.filteredFaqs objectAtIndex:indexPath.row] question]; + } + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 60; +} + +#pragma mark - UITableViewDelegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + + ABXFAQViewController* controller = [[ABXFAQViewController alloc] init]; + controller.faq = self.faqs[indexPath.row]; + [self.navigationController pushViewController:controller animated:YES]; +} + +#pragma mark - Search Bar + +- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar +{ + [searchBar setShowsCancelButton:YES animated:YES]; +} + +- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar +{ + [searchBar setShowsCancelButton:NO animated:YES]; + + self.errorLabel.hidden = YES; + self.filteredFaqs = self.faqs; + [self.tableView reloadData]; +} + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText +{ + if (searchText.length > 0) { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.question contains[cd] %@ OR SELF.answer contains[cd] %@", searchText, searchText]; + self.filteredFaqs = [self.faqs filteredArrayUsingPredicate:predicate]; + + if (self.filteredFaqs.count > 0) { + self.errorLabel.hidden = YES; + } + else { + [self showError:NSLocalizedString(@"No matches found", nil)]; + } + } + else { + self.filteredFaqs = self.faqs; + } + [self.tableView reloadData]; +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar +{ + [searchBar resignFirstResponder]; + searchBar.text = @""; +} + +@end diff --git a/Classes/Controllers/ABXFeedbackViewController.h b/Classes/Controllers/ABXFeedbackViewController.h new file mode 100644 index 0000000..c6de62d --- /dev/null +++ b/Classes/Controllers/ABXFeedbackViewController.h @@ -0,0 +1,17 @@ +// +// ABXFeedbackViewController.h +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface ABXFeedbackViewController : UIViewController + +@property (nonatomic, copy) NSString *placeholder; + ++ (void)showFromController:(UIViewController*)controller placeholder:(NSString*)placeholder; + +@end diff --git a/Classes/Controllers/ABXFeedbackViewController.m b/Classes/Controllers/ABXFeedbackViewController.m new file mode 100644 index 0000000..f9e05c7 --- /dev/null +++ b/Classes/Controllers/ABXFeedbackViewController.m @@ -0,0 +1,338 @@ +// +// ABXFeedbackViewController.m +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXFeedbackViewController.h" + +#import "ABXKeychain.h" +#import "ABXTextView.h" +#import "ABXIssue.h" + +@interface ABXFeedbackViewController () + +@property (nonatomic, strong) ABXTextView *textView; +@property (nonatomic, strong) UITextField *textField; +@property (nonatomic, strong) ABXKeychain *keychain; + +@end + +@implementation ABXFeedbackViewController + +static NSInteger const kEmailAlert = 0; +static NSInteger const kCloseAlert = 1; + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Listen for keyboard events + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onKeyboard:) + name:UIKeyboardWillShowNotification + object:nil]; + + self.view.backgroundColor = [UIColor whiteColor]; + + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) { + self.edgesForExtendedLayout = UIRectEdgeNone; + } + + // Email label + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, 90, 50)]; + label.textColor = [UIColor grayColor]; + label.text = NSLocalizedString(@"Your Email:", nil); + label.font = [UIFont systemFontOfSize:15]; + [self.view addSubview:label]; + + // Field for their email + CGRect tfRect = CGRectMake(110, 0, CGRectGetWidth(self.view.frame) - 120, 50); + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] == NSOrderedAscending) { + tfRect = CGRectMake(110, 15, CGRectGetWidth(self.view.frame) - 120, 31); + } + UITextField *textField = [[UITextField alloc] initWithFrame:tfRect]; + textField.placeholder = @"e.g. yourname@icloud.com"; + textField.font = [UIFont systemFontOfSize:15]; + textField.keyboardType = UIKeyboardTypeEmailAddress; + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + textField.autoresizingMask = UIViewAutoresizingFlexibleWidth; + textField.returnKeyType = UIReturnKeyNext; + textField.delegate = self; + [self.view addSubview:textField]; + self.textField = textField; + + UIView *seperator = [[UIView alloc] initWithFrame:CGRectMake(20, 50, CGRectGetWidth(self.view.frame), [UIScreen mainScreen].scale >= 2.0f ? 0.5 : 1)]; + seperator.backgroundColor = [UIColor lightGrayColor]; + seperator.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self.view addSubview:seperator]; + + // Text view + self.textView = [[ABXTextView alloc] initWithFrame:CGRectMake(15, 51, CGRectGetWidth(self.view.frame) - 30, CGRectGetHeight(self.view.frame) - 51)]; + self.textView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.textView.font = [UIFont systemFontOfSize:15]; + self.textView.placeholder = self.placeholder ?: NSLocalizedString(@"How can we help?", nil); + self.textView.delegate = self; + [self.view addSubview:self.textView]; + + // Title + self.title = NSLocalizedString(@"Contact", nil); + + // Buttons + [self showButtons]; + + // Set the email from the keychain if has been entered before + self.keychain = [[ABXKeychain alloc] initWithService:@"appbot.co" accessGroup:nil accessibility:ABXKeychainAccessibleWhenUnlocked]; + self.textField.text = self.keychain[@"FeedbackEmail"]; + + if (self.textField.text.length > 0) { + [self.textView becomeFirstResponder]; + } + else { + [self.textField becomeFirstResponder]; + } +} + ++ (void)showFromController:(UIViewController*)controller placeholder:(NSString*)placeholder +{ + ABXFeedbackViewController *viewController = [[self alloc] init]; + viewController.placeholder = placeholder; + UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:viewController]; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // Show as a sheet on iPad + nav.modalPresentationStyle = UIModalPresentationFormSheet; + } + [controller presentViewController:nav animated:YES completion:nil]; +} + +#pragma mark Keyboard + +- (void)onKeyboard:(NSNotification*)notification +{ + CGRect keyboardWinRect = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + + // Convert it to suit us + CGRect keyboardRect = [self.view convertRect:keyboardWinRect fromView:self.view.window]; + + // Move the textView so the bottom doesn't extend beyound the keyboard + CGRect tvFrame = self.textView.frame; + tvFrame.size.height = CGRectGetHeight(self.view.bounds) - (CGRectGetHeight(keyboardRect) + CGRectGetMinY(tvFrame)); + self.textView.frame = tvFrame; +} + +- (void)showButtons +{ + if (self.navigationItem.leftBarButtonItem == nil) { + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(onDone)]; + } + + if (self.textView.text.length > 0 && self.navigationItem.rightBarButtonItem == nil) { + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Send", nil) + style:UIBarButtonItemStyleDone + target:self + action:@selector(onSend)]; + } + else if (self.textView.text.length == 0 && self.navigationItem.rightBarButtonItem != nil) { + self.navigationItem.rightBarButtonItem = nil; + } +} + +- (void)onDone +{ + if (self.textView.text.length > 0) { + // Prompt to ensure they want to close + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Are you sure?", nil) + message:NSLocalizedString(@"Your message will be lost.", nil) + delegate:self + cancelButtonTitle:NSLocalizedString(@"Cancel", nil) + otherButtonTitles:NSLocalizedString(@"Close", nil), nil]; + alert.tag = kCloseAlert; + [alert show]; + } + else { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (void)onSend +{ + [self validateAndSend]; +} + +#pragma mark - Validation + +- (BOOL)validateEmail +{ + NSString *regex1 = @"\\A[a-z0-9]+([-._][a-z0-9]+)*@([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,4}\\z"; + NSString *regex2 = @"^(?=.{1,64}@.{4,64}$)(?=.{6,100}$).*"; + NSPredicate *test1 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex1]; + NSPredicate *test2 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex2]; + return [test1 evaluateWithObject:self.textField.text] && [test2 evaluateWithObject:self.textField.text]; +} + +- (void)validateAndSend +{ + [self.textView resignFirstResponder]; + [self.textField resignFirstResponder]; + + if (self.textView.text.length == 0) { + // Needs a body + [self.textView becomeFirstResponder]; + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"No Message.", nil) + message:NSLocalizedString(@"Please enter a message.", nil) + delegate:nil + cancelButtonTitle:NSLocalizedString(@"OK", nil) + otherButtonTitles:nil] show]; + + } + else if (self.textField.text.length == 0) { + // Ensure they want to submit without an email + UIAlertView * alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"No Email.", nil) + message:NSLocalizedString(@"Are you sure you want to send without your email? We won't be able reply to you.", nil) + delegate:self + cancelButtonTitle:NSLocalizedString(@"Cancel", nil) + otherButtonTitles:NSLocalizedString(@"Send", nil), nil]; + alert.tag = kEmailAlert; + [alert show]; + } + else if (![self validateEmail]) { + // Invalid email + [self.textField becomeFirstResponder]; + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Invalid Email Address.", nil) + message:NSLocalizedString(@"Please check your email address, it appears to be invalid.", nil) + delegate:nil + cancelButtonTitle:NSLocalizedString(@"OK", nil) + otherButtonTitles:nil] show]; + } + else { + // All good! + [self send]; + } +} + +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + switch (alertView.tag) { + case kEmailAlert: { + if (buttonIndex == 1) { + [self send]; + } + else { + [self.textField becomeFirstResponder]; + } + } + break; + + case kCloseAlert: { + if (buttonIndex == 1) { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; + } + } + } + +} + +#pragma mark - Submission + +- (void)send +{ + if (self.textField.text.length > 0) { + // Save the email in the keychain + self.keychain[@"FeedbackEmail"] = self.textField.text; + } + + self.navigationItem.leftBarButtonItem = nil; + self.navigationItem.rightBarButtonItem = nil; + + UIView *overlay = [[UIView alloc] initWithFrame:self.view.bounds]; + overlay.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + overlay.backgroundColor = [UIColor clearColor]; + [self.view addSubview:overlay]; + + UIView *smoke = [[UIView alloc] initWithFrame:self.view.bounds]; + smoke.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + smoke.backgroundColor = [UIColor blackColor]; + smoke.alpha = 0.5; + [overlay addSubview:smoke]; + + UIView *content = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(overlay.frame), 50)]; + content.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + content.center = smoke.center; + [overlay addSubview:content]; + + UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activity.center = CGPointMake(CGRectGetMidX(content.bounds), CGRectGetMidY(content.bounds) - 10); + [activity startAnimating]; + [content addSubview:activity]; + + UILabel *label = [[UILabel alloc] initWithFrame:content.bounds]; + label.center = CGPointMake(CGRectGetMidX(content.bounds), CGRectGetMidY(content.bounds) + 30); + label.textColor = [UIColor whiteColor]; + label.text = @"Sending..."; + label.textAlignment = NSTextAlignmentCenter; + label.font = [UIFont systemFontOfSize:15]; + label.backgroundColor = [UIColor clearColor]; + [content addSubview:label]; + + [ABXIssue submit:self.textField.text + feedback:self.textView.text + complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + switch (responseCode) { + case ABXResponseCodeSuccess: { + [self.navigationController dismissViewControllerAnimated:YES + completion:^{ + [self showConfirm]; + }]; + } + break; + + default: { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", nil) + message:NSLocalizedString(@"There was an error sending your feedback, please try again.", nil) + delegate:nil + cancelButtonTitle:NSLocalizedString(@"Cancel", nil) + otherButtonTitles:nil]; + [alert show]; + [self showButtons]; + [overlay removeFromSuperview]; + } + break; + } + }]; +} + +- (void)showConfirm +{ + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Thanks", nil) + message:NSLocalizedString(@"We have received your feedback and will be in contact soon.", nil) + delegate:nil + cancelButtonTitle:NSLocalizedString(@"OK", nil) + otherButtonTitles:nil]; + [alert show]; +} + +#pragma mark - UITextViewDelegate + +- (void)textViewDidChange:(UITextView *)textView +{ + [self showButtons]; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + [self.textView becomeFirstResponder]; + + return NO; +} + +@end + diff --git a/Classes/Controllers/ABXNavigationController.h b/Classes/Controllers/ABXNavigationController.h new file mode 100644 index 0000000..a85fe87 --- /dev/null +++ b/Classes/Controllers/ABXNavigationController.h @@ -0,0 +1,12 @@ +// +// ABXNavigationController.h +// +// Created by Stuart Hall on 11/06/2014. +// Copyright (c) 2014 Stuart Hall. All rights reserved. +// + +#import + +@interface ABXNavigationController : UINavigationController + +@end diff --git a/Classes/Controllers/ABXNavigationController.m b/Classes/Controllers/ABXNavigationController.m new file mode 100644 index 0000000..3443135 --- /dev/null +++ b/Classes/Controllers/ABXNavigationController.m @@ -0,0 +1,33 @@ +// +// ABXNavigationController.m +// Realtime +// +// Created by Stuart Hall on 11/06/2014. +// Copyright (c) 2014 Stuart Hall. All rights reserved. +// + +#import "ABXNavigationController.h" + +@interface ABXNavigationController () + +@end + +@implementation ABXNavigationController + +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return UIStatusBarStyleDefault; +} + +@end diff --git a/Classes/Controllers/ABXVersionsViewController.h b/Classes/Controllers/ABXVersionsViewController.h new file mode 100644 index 0000000..204b86f --- /dev/null +++ b/Classes/Controllers/ABXVersionsViewController.h @@ -0,0 +1,14 @@ +// +// ABXVersionsViewController.h +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXBaseListViewController.h" + +@interface ABXVersionsViewController : ABXBaseListViewController + +@end diff --git a/Classes/Controllers/ABXVersionsViewController.m b/Classes/Controllers/ABXVersionsViewController.m new file mode 100644 index 0000000..acfc565 --- /dev/null +++ b/Classes/Controllers/ABXVersionsViewController.m @@ -0,0 +1,101 @@ +// +// ABXVersionsViewController.m +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXVersionsViewController.h" + +#import "ABXVersion.h" +#import "ABXVersionTableViewCell.h" + +@interface ABXVersionsViewController () + +@property (nonatomic, strong) NSArray *versions; + +@end + +@implementation ABXVersionsViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.title = NSLocalizedString(@"Versions", nil); + + [self fetchVersions]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Buttons + +- (void)onDone +{ + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - Fetching + +- (void)fetchVersions +{ + [ABXVersion fetch:^(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + [self.activityView stopAnimating]; + if (responseCode == ABXResponseCodeSuccess) { + self.versions = versions; + [self.tableView reloadData]; + + if (versions.count == 0) { + [self showError:NSLocalizedString(@"No versions found.", nil)]; + } + } + else { + [self showError:NSLocalizedString(@"Unable to fetch versions.\r\nPlease try again later", nil)]; + } + }]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.versions.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + static NSString *CellIdentifier = @"FAQCell"; + + ABXVersionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (!cell) { + cell = [[ABXVersionTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; + } + + if (indexPath.row < self.versions.count) { + [cell setVersion:self.versions[indexPath.row]]; + } + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.row < self.versions.count) { + return [ABXVersionTableViewCell heightForVersion:self.versions[indexPath.row] + withWidth:CGRectGetWidth(self.tableView.frame)]; + } + return 0; +} + + +@end diff --git a/Classes/Models/ABXFaq.h b/Classes/Models/ABXFaq.h new file mode 100644 index 0000000..d515516 --- /dev/null +++ b/Classes/Models/ABXFaq.h @@ -0,0 +1,23 @@ +// +// ABXFaq.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXModel.h" + +@interface ABXFaq : ABXModel + +@property (nonatomic, copy) NSNumber *identifier; +@property (nonatomic, copy) NSString *question; +@property (nonatomic, copy) NSString *answer; + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + +- (NSURLSessionDataTask*)upvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; +- (NSURLSessionDataTask*)downvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + +@end diff --git a/Classes/Models/ABXFaq.m b/Classes/Models/ABXFaq.m new file mode 100644 index 0000000..59ef160 --- /dev/null +++ b/Classes/Models/ABXFaq.m @@ -0,0 +1,58 @@ +// +// ABXFaq.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXFaq.h" + +#import "NSDictionary+ABXNSNullAsNull.h" + +PROTECTED_ABXMODEL + +@implementation ABXFaq + +- (id)initWithAttributes:(NSDictionary*)attributes +{ + self = [super init]; + if (self) { + self.identifier = [attributes objectForKeyNulled:@"id"]; + self.question = [attributes objectForKeyNulled:@"question"]; + self.answer = [attributes objectForKeyNulled:@"answer"]; + } + return self; +} + ++ (id)createWithAttributes:(NSDictionary*)attributes +{ + return [[ABXFaq alloc] initWithAttributes:attributes]; +} + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [self fetchList:@"faqs" params:nil complete:complete]; +} + +- (NSURLSessionDataTask*)upvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [self vote:@"upvote" complete:complete]; +} + +- (NSURLSessionDataTask*)downvote:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [self vote:@"downvote" complete:complete]; +} + +- (NSURLSessionDataTask*)vote:(NSString*)action complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [[ABXApiClient instance] PUT:[NSString stringWithFormat:@"faqs/%@/%@", _identifier, action] + params:nil + complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { + if (complete) { + complete(responseCode, httpCode, error); + } + }]; +} + +@end diff --git a/Classes/Models/ABXIssue.h b/Classes/Models/ABXIssue.h new file mode 100644 index 0000000..fe7919a --- /dev/null +++ b/Classes/Models/ABXIssue.h @@ -0,0 +1,16 @@ +// +// ABXIssue.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXModel.h" + +@interface ABXIssue : ABXModel + ++ (NSURLSessionDataTask*)submit:(NSString*)email + feedback:(NSString*)feedback + complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + +@end diff --git a/Classes/Models/ABXIssue.m b/Classes/Models/ABXIssue.m new file mode 100644 index 0000000..24b3aab --- /dev/null +++ b/Classes/Models/ABXIssue.m @@ -0,0 +1,145 @@ +// +// ABXIssue.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXIssue.h" + +#include +#include +#import +#import + +@implementation ABXIssue + ++ (NSURLSessionDataTask*)submit:(NSString*)email + feedback:(NSString*)feedback + complete:(void(^)(ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:[self systemInfo]]; + if (feedback.length > 0) { + [params setObject:feedback forKey:@"issue"]; + } + if (email.length > 0) { + [params setObject:email forKey:@"email"]; + } + + return [[ABXApiClient instance] POST:@"issues" + params:params + complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { + if (complete) { + complete(responseCode, httpCode, error); + } + }]; +} + +#pragma mark - System Info + ++ (NSDictionary*)systemInfo +{ + NSUInteger totalMemory; + NSUInteger freeMemory = [self freeMemory:&totalMemory]; + + uint64_t totalSpace; + uint64_t freeSpace = [self freeDiskspace:&totalSpace]; + + return @{ @"os_version" : [@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion] ?: @""], + @"app_version" : NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"] ?: @"", + @"app_name" : NSBundle.mainBundle.infoDictionary[@"CFBundleDisplayName"] ?: @"", + @"device" : [self platform] ?: @"", + @"language" : [[NSLocale preferredLanguages] firstObject] ?: @"", + @"locale" : [[NSLocale currentLocale] localeIdentifier] ?: @"", + @"jailbroken" : @([self isJailbroken]), + @"free_memory" : @(freeMemory), + @"total_memory" : @(totalMemory), + @"free_space" : @(freeSpace), + @"total_space" : @(totalSpace) }; +} + ++ (NSString *)platform +{ + size_t size; + sysctlbyname("hw.machine", NULL, &size, NULL, 0); + char *machine = malloc(size); + sysctlbyname("hw.machine", machine, &size, NULL, 0); + NSString *platform = [NSString stringWithUTF8String:machine]; + free(machine); + return platform; +} + ++ (NSUInteger)freeMemory:(NSUInteger*)totalMemory +{ + mach_port_t host_port = mach_host_self(); + mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); + vm_size_t pagesize; + vm_statistics_data_t vm_stat; + + host_page_size(host_port, &pagesize); + + host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); + + NSUInteger mem_used = (vm_stat.active_count + vm_stat.inactive_count + vm_stat.wire_count) * pagesize; + NSUInteger mem_free = vm_stat.free_count * pagesize; + NSUInteger mem_total = mem_used + mem_free; + + *totalMemory = mem_total; + + return mem_free; +} + ++ (uint64_t)freeDiskspace:(uint64_t*)totalDiskspace +{ + uint64_t totalSpace = 0; + uint64_t totalFreeSpace = 0; + NSError *error = nil; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error: &error]; + + if (dictionary) { + NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize]; + NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize]; + totalSpace = [fileSystemSizeInBytes unsignedLongLongValue]; + totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue]; + } + + *totalDiskspace = totalSpace; + + return totalFreeSpace; +} + +// All credit to https://github.com/itruf/crackify ++ (BOOL)isJailbroken +{ +#if !TARGET_IPHONE_SIMULATOR + //Check for Cydia.app + BOOL yes; + if ([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@%@", @"App", @"lic",@"ati", @"ons/", @"Cyd", @"ia.", @"app"]] + || [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@", @"pr", @"iva",@"te/v", @"ar/l", @"ib/a", @"pt/"] isDirectory:&yes] + || [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"/%@%@%@%@%@%@", @"us", @"r/l",@"ibe", @"xe", @"c/cy", @"dia"] isDirectory:&yes]) { + //Cydia installed + return YES; + } + + //Try to write file in private + NSError *error; + + static NSString *str = @"Jailbreak test string"; + + [str writeToFile:@"/private/test_jail.txt" atomically:YES + encoding:NSUTF8StringEncoding error:&error]; + + if (error == nil) { + // Writed + return YES; + } + else { + [[NSFileManager defaultManager] removeItemAtPath:@"/private/test_jail.txt" error:nil]; + } +#endif + return NO; +} + + +@end diff --git a/Classes/Models/ABXModel.h b/Classes/Models/ABXModel.h new file mode 100644 index 0000000..b14f93b --- /dev/null +++ b/Classes/Models/ABXModel.h @@ -0,0 +1,22 @@ +// +// ABXModel.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXApiClient.h" + +// Protected methods +#define PROTECTED_ABXMODEL \ +@interface ABXModel () \ ++ (id)createWithAttributes:(NSDictionary*)attributes; \ ++ (NSURLSessionDataTask*)fetchList:(NSString*)path \ + params:(NSDictionary*)params \ + complete:(void(^)(NSArray *objects, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; \ +@end \ + +@interface ABXModel : NSObject +@end diff --git a/Classes/Models/ABXModel.m b/Classes/Models/ABXModel.m new file mode 100644 index 0000000..8c10f23 --- /dev/null +++ b/Classes/Models/ABXModel.m @@ -0,0 +1,60 @@ +// +// ABXModel.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXModel.h" + +#import "NSDictionary+ABXNSNullAsNull.h" + +@implementation ABXModel + ++ (id)createWithAttributes:(NSDictionary*)attributes +{ + // Virtual + assert(false); +} + ++ (NSURLSessionDataTask*)fetchList:(NSString*)path + params:(NSDictionary*)params + complete:(void(^)(NSArray *objects, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + // Generic list fetch + return [[ABXApiClient instance] GET:path + params:params + complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { + if (responseCode == ABXResponseCodeSuccess) { + NSArray *results = [JSON objectForKeyNulled:@"results"]; + if (results && [results isKindOfClass:[NSArray class]]) { + // Convert into objects + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[results count]]; + for (NSDictionary *attrs in results) { + if ([attrs isKindOfClass:[NSDictionary class]]) { + [objects addObject:[self createWithAttributes:attrs]]; + } + } + + // Success! + if (complete) { + complete(objects, responseCode, httpCode, error); + } + } + else { + // Decoding error, pass the values through + if (complete) { + complete(nil, ABXResponseCodeErrorDecoding, httpCode, error); + } + } + } + else { + // Error, pass the values through + if (complete) { + complete(nil, responseCode, httpCode, error); + } + } + }]; +} + +@end diff --git a/Classes/Models/ABXNotification.h b/Classes/Models/ABXNotification.h new file mode 100644 index 0000000..ba7d706 --- /dev/null +++ b/Classes/Models/ABXNotification.h @@ -0,0 +1,24 @@ +// +// ABXNotification.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXModel.h" + +@interface ABXNotification : ABXModel + +@property (nonatomic, copy) NSNumber *identifier; +@property (nonatomic, copy) NSString *message; +@property (nonatomic, copy) NSString *actionLabel; +@property (nonatomic, copy) NSString *actionUrl; + +- (void)markAsSeen; +- (BOOL)hasSeen; + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + +@end diff --git a/Classes/Models/ABXNotification.m b/Classes/Models/ABXNotification.m new file mode 100644 index 0000000..8a1f8f6 --- /dev/null +++ b/Classes/Models/ABXNotification.m @@ -0,0 +1,54 @@ +// +// ABXNotification.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXNotification.h" + +#import "NSDictionary+ABXNSNullAsNull.h" + +PROTECTED_ABXMODEL + +@implementation ABXNotification + +- (id)initWithAttributes:(NSDictionary*)attributes +{ + self = [super init]; + if (self) { + self.identifier = [attributes objectForKeyNulled:@"id"]; + self.message = [attributes objectForKeyNulled:@"message"]; + self.actionLabel = [attributes objectForKeyNulled:@"action_label"]; + self.actionUrl = [attributes objectForKeyNulled:@"action_url"]; + } + return self; +} + ++ (id)createWithAttributes:(NSDictionary*)attributes +{ + return [[ABXNotification alloc] initWithAttributes:attributes]; +} + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [self fetchList:@"notifications" params:nil complete:complete]; +} + +- (BOOL)hasAction +{ + return self.actionUrl.length > 0 && self.actionLabel.length > 0; +} + +- (void)markAsSeen +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[@"Notification" stringByAppendingString:[self.identifier stringValue]]]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (BOOL)hasSeen +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:[@"Notification" stringByAppendingString:[self.identifier stringValue]]]; +} + +@end diff --git a/Classes/Models/ABXVersion.h b/Classes/Models/ABXVersion.h new file mode 100644 index 0000000..3f12464 --- /dev/null +++ b/Classes/Models/ABXVersion.h @@ -0,0 +1,26 @@ +// +// ABXVersion.h +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXModel.h" + +@interface ABXVersion : ABXModel + +@property (nonatomic, strong) NSDate *releaseDate; +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSString *version; + +- (void)markAsSeen; +- (BOOL)hasSeen; +- (BOOL)isNewerThanCurrent; + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + ++ (NSURLSessionDataTask*)fetchCurrentVersion:(void(^)(ABXVersion *currentVersion, ABXVersion *latestVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete; + +@end diff --git a/Classes/Models/ABXVersion.m b/Classes/Models/ABXVersion.m new file mode 100644 index 0000000..4c04712 --- /dev/null +++ b/Classes/Models/ABXVersion.m @@ -0,0 +1,108 @@ +// +// ABXVersion.m +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXVersion.h" + +#import "NSDictionary+ABXNSNullAsNull.h" +#import "ABXKeychain.h" + +PROTECTED_ABXMODEL + +@implementation ABXVersion + +- (id)initWithAttributes:(NSDictionary*)attributes +{ + self = [super init]; + if (self) { + // Date formatter, cache as they are expensive to create + static dispatch_once_t onceToken; + static NSDateFormatter *formatter = nil; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setDateFormat:@"yyyy-MM-dd"]; + }); + + // Convert the string to a date + NSString *releaseDateString = [attributes objectForKeyNulled:@"release_date"]; + if (releaseDateString) { + self.releaseDate = [formatter dateFromString:releaseDateString]; + } + + self.text = [attributes objectForKeyNulled:@"change_text"]; + self.version = [attributes objectForKeyNulled:@"version"]; + } + return self; +} + ++ (id)createWithAttributes:(NSDictionary*)attributes +{ + return [[ABXVersion alloc] initWithAttributes:attributes]; +} + ++ (NSURLSessionDataTask*)fetch:(void(^)(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [self fetchList:@"versions" params:nil complete:complete]; +} + ++ (NSURLSessionDataTask*)fetchCurrentVersion:(void(^)(ABXVersion *version, ABXVersion *latestVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error))complete +{ + return [[ABXApiClient instance] GET:@"versions" + params:@{ @"version" : NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"] } + complete:^(ABXResponseCode responseCode, NSInteger httpCode, NSError *error, id JSON) { + if (responseCode == ABXResponseCodeSuccess) { + NSDictionary *results = [JSON objectForKeyNulled:@"results"]; + if (results && [results isKindOfClass:[NSDictionary class]]) { + // Convert into objects + ABXVersion *version = nil; + ABXVersion *currentVersion = nil; + + if ([results objectForKeyNulled:@"version"]) { + version = [self createWithAttributes:[results objectForKeyNulled:@"version"]]; + } + + if ([results objectForKeyNulled:@"current_version"]) { + currentVersion = [self createWithAttributes:[results objectForKeyNulled:@"current_version"]]; + } + + // Success! + if (complete) { + complete(version, currentVersion, responseCode, httpCode, error); + } + } + else { + // Decoding error, pass the values through + if (complete) { + complete(nil, nil, ABXResponseCodeErrorDecoding, httpCode, error); + } + } + } + else { + // Error, pass the values through + if (complete) { + complete(nil, nil, responseCode, httpCode, error); + } + } + }]; +} + +- (void)markAsSeen +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[@"Version" stringByAppendingString:self.version]]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (BOOL)hasSeen +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:[@"Version" stringByAppendingString:self.version]]; +} + +- (BOOL)isNewerThanCurrent +{ + return [self.version compare:NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"] options:NSNumericSearch] == NSOrderedDescending; +} + +@end diff --git a/Classes/Views/ABXNotificationView.h b/Classes/Views/ABXNotificationView.h new file mode 100644 index 0000000..55f097b --- /dev/null +++ b/Classes/Views/ABXNotificationView.h @@ -0,0 +1,28 @@ +// +// ABXNotificationView.h +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@class ABXNotificationView; + +typedef void (^ABXNotificationViewCallback)(ABXNotificationView *view); + +@interface ABXNotificationView : UIView + ++ (ABXNotificationView*)show:(NSString*)text + actionText:(NSString*)actionText + backgroundColor:(UIColor*)backgroundColor + textColor:(UIColor*)textColor + buttonColor:(UIColor*)buttonColor + inController:(UIViewController*)controller + actionBlock:(ABXNotificationViewCallback)actionBlock + dismissBlock:(ABXNotificationViewCallback)dismissBlock; + +- (void)dismiss; + +@end diff --git a/Classes/Views/ABXNotificationView.m b/Classes/Views/ABXNotificationView.m new file mode 100644 index 0000000..21b522a --- /dev/null +++ b/Classes/Views/ABXNotificationView.m @@ -0,0 +1,146 @@ +// +// ABXNotificationView.m +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXNotificationView.h" + +#import "ABXNotification.h" + +#import "NSString+ABXSizing.h" + +@interface ABXNotificationView () + +@property (nonatomic, strong) ABXNotificationViewCallback actionCallback; +@property (nonatomic, strong) ABXNotificationViewCallback dismissCallback; + +@end + +@implementation ABXNotificationView + ++ (ABXNotificationView*)show:(NSString*)text + actionText:(NSString*)actionText + backgroundColor:(UIColor*)backgroundColor + textColor:(UIColor*)textColor + buttonColor:(UIColor*)buttonColor + inController:(UIViewController*)controller + actionBlock:(ABXNotificationViewCallback)actionBlock + dismissBlock:(ABXNotificationViewCallback)dismissBlock +{ + static NSInteger const kMaxWidth = 300; + + + // Calculate the label height + UIFont *font = [UIFont systemFontOfSize:15]; + CGFloat labelHeight = [text heightForWidth:kMaxWidth andFont:font]; + + NSUInteger topPadding = [self topOffsetForController:controller]; + + // Create the view + CGFloat totalHeight = labelHeight + 50 + topPadding; + ABXNotificationView *view = [[ABXNotificationView alloc] initWithFrame:CGRectMake(0, -totalHeight, CGRectGetWidth(controller.view.bounds), totalHeight)]; + view.backgroundColor = backgroundColor; + view.autoresizingMask = UIViewAutoresizingFlexibleWidth; + view.actionCallback = actionBlock; + view.dismissCallback = dismissBlock; + [controller.view addSubview:view]; + + // Label for the text + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake((CGRectGetWidth(view.bounds) - kMaxWidth)/2, 15 + topPadding, kMaxWidth, labelHeight)]; + label.numberOfLines = 0; + label.text = text; + label.textAlignment = NSTextAlignmentCenter; + label.textColor = textColor; + label.font = font; + label.backgroundColor = [UIColor clearColor]; + label.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [view addSubview:label]; + + if (actionText) { + // Action button + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.tintColor = buttonColor; + [button setTitle:actionText forState:UIControlStateNormal]; + button.frame = CGRectMake((CGRectGetWidth(view.bounds) - kMaxWidth)/2, totalHeight - 40, kMaxWidth/2, 40); + button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + button.titleLabel.font = [UIFont boldSystemFontOfSize:15]; + [button addTarget:view action:@selector(onAction:) forControlEvents:UIControlEventTouchUpInside]; + [view addSubview:button]; + } + + // Close Button + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.tintColor = buttonColor; + [button setTitle:NSLocalizedString(@"Close", nil) forState:UIControlStateNormal]; + if (actionText) { + button.frame = CGRectMake(CGRectGetWidth(view.bounds) / 2, totalHeight - 40, kMaxWidth / 2, 40); + } + else { + button.frame = CGRectMake((CGRectGetWidth(view.bounds) - kMaxWidth)/2, totalHeight - 40, CGRectGetWidth(view.bounds) / 2, 40); + } + button.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + button.titleLabel.font = [UIFont boldSystemFontOfSize:15]; + [button addTarget:view action:@selector(onClose:) forControlEvents:UIControlEventTouchUpInside]; + [view addSubview:button]; + + // Slide it down + [UIView animateWithDuration:0.3 + animations:^{ + CGFloat topPadding = 0; + CGRect r = view.frame; + r.origin.y = topPadding; + view.frame = r; + }]; + + return view; +} + +- (void)onAction:(UIButton*)button +{ + if (self.actionCallback) { + self.actionCallback(self); + } +} + +- (void)onClose:(UIButton*)button +{ + if (self.dismissCallback) { + self.dismissCallback(self); + } + [self dismiss]; +} + +- (void)dismiss +{ + // Slide it away + [UIView animateWithDuration:0.3 + animations:^{ + CGRect r = self.frame; + r.origin.y = -CGRectGetHeight(r); + self.frame = r; + } + completion:^(BOOL finished) { + [self removeFromSuperview]; + }]; +} + ++ (NSInteger)topOffsetForController:(UIViewController*)controller +{ + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { + // Determine the status bar size + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + CGRect statusBarWindowRect = [controller.view.window convertRect:statusBarFrame fromWindow: nil]; + CGRect statusBarViewRect = [controller.view convertRect:statusBarWindowRect fromView: nil]; + + // Determine the navigation bar size + CGFloat navbarHeight = CGRectGetHeight(controller.navigationController.navigationBar.frame); + + return CGRectGetHeight(statusBarViewRect) + navbarHeight; + } + return 0; +} + +@end diff --git a/Classes/Views/ABXPromptView.h b/Classes/Views/ABXPromptView.h new file mode 100644 index 0000000..a4cc48e --- /dev/null +++ b/Classes/Views/ABXPromptView.h @@ -0,0 +1,26 @@ +// +// ABXPromptView.h +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@protocol ABXPromptViewDelegate + +- (void)appbotPromptForReview; +- (void)appbotPromptForFeedback; + +@end + +@interface ABXPromptView : UIView + +@property (weak) id delegate; + ++ (BOOL)hasHadInteractionForCurrentVersion; + +@property (nonatomic, strong) UIColor *backgroundColor; + +@end \ No newline at end of file diff --git a/Classes/Views/ABXPromptView.m b/Classes/Views/ABXPromptView.m new file mode 100644 index 0000000..4f90da3 --- /dev/null +++ b/Classes/Views/ABXPromptView.m @@ -0,0 +1,157 @@ +// +// ABXPromptView.m +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXPromptView.h" + +@interface ABXPromptView () + +@property (nonatomic, strong) UIView *container; +@property (nonatomic, strong) UILabel *label; +@property (nonatomic, strong) UIButton *closeButton; +@property (nonatomic, strong) UIButton *leftButton; +@property (nonatomic, strong) UIButton *rightButton; +@property (nonatomic, strong) UIButton *largeButton; + +@property (nonatomic, assign) BOOL liked; + +@end + +@implementation ABXPromptView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self initialise]; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (self) { + [self initialise]; + } + return self; +} + +#pragma mark - Setup + +- (void)initialise +{ + self.container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 280, 100)]; + self.container.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + self.container.backgroundColor = [UIColor clearColor]; + [self addSubview:self.container]; + self.container.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); + + self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.container.bounds), 52)]; + self.label.textColor = [UIColor colorWithWhite:0.1 alpha:1]; + self.label.textAlignment = NSTextAlignmentCenter; + self.label.numberOfLines = 0; + self.label.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:15]; + self.label.text = [[NSLocalizedString(@"What do you think about ", nil) stringByAppendingString:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]] stringByAppendingString:@"?"]; + [self.container addSubview:self.label]; + + + self.leftButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.leftButton.frame = CGRectMake(CGRectGetMidX(self.container.bounds) - 135, 50, 130, 30); + self.leftButton.backgroundColor = [UIColor colorWithWhite:0.6 alpha:1]; + self.leftButton.layer.cornerRadius = 4; + self.leftButton.layer.masksToBounds = YES; + [self.leftButton setTitle:NSLocalizedString(@"I Love It!", nil) forState:UIControlStateNormal]; + [self.leftButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + self.leftButton.titleLabel.font = [UIFont systemFontOfSize:15]; + [self.leftButton addTarget:self action:@selector(onLove) forControlEvents:UIControlEventTouchUpInside]; + [self.container addSubview:self.leftButton]; + + self.rightButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.rightButton.frame = CGRectMake(CGRectGetMidX(self.container.bounds) + 5, 50, 130, 30); + self.rightButton.backgroundColor = [UIColor colorWithWhite:0.6 alpha:1]; + self.rightButton.layer.cornerRadius = 4; + self.rightButton.layer.masksToBounds = YES; + [self.rightButton setTitle:NSLocalizedString(@"Could Be Better", nil) forState:UIControlStateNormal]; + [self.rightButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + self.rightButton.titleLabel.font = [UIFont systemFontOfSize:15]; + [self.rightButton addTarget:self action:@selector(onImprove) forControlEvents:UIControlEventTouchUpInside]; + [self.container addSubview:self.rightButton]; + + self.largeButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.largeButton.frame = CGRectMake(CGRectGetMidX(self.container.bounds) - 100, 50, 200, 30); + self.largeButton.backgroundColor = [UIColor colorWithWhite:0.6 alpha:1]; + self.largeButton.layer.cornerRadius = 4; + self.largeButton.layer.masksToBounds = YES; + [self.largeButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + self.largeButton.titleLabel.font = [UIFont systemFontOfSize:15]; + [self.largeButton addTarget:self action:@selector(onLargeButton) forControlEvents:UIControlEventTouchUpInside]; + self.largeButton.alpha = 0; + [self.container addSubview:self.largeButton]; +} + +#pragma mark - Buttons + +- (void)onLove +{ + self.liked = YES; + [UIView animateWithDuration:0.3 + animations:^{ + self.label.text = NSLocalizedString(@"Great! Could you leave us a nice review?\r\nIt really helps.", nil); + [self.largeButton setTitle:NSLocalizedString(@"Leave a Review", nil) forState:UIControlStateNormal]; + [self.closeButton setTitle:NSLocalizedString(@"no thanks", nil) forState:UIControlStateNormal]; + self.leftButton.alpha = 0; + self.rightButton.alpha = 0; + self.largeButton.alpha = 1; + }]; +} + +- (void)onImprove +{ + self.liked = NO; + [UIView animateWithDuration:0.3 + animations:^{ + self.label.text = NSLocalizedString(@"Could you tell us how we could improve?", nil); + [self.largeButton setTitle:NSLocalizedString(@"Send Feedback", nil) forState:UIControlStateNormal]; + [self.closeButton setTitle:NSLocalizedString(@"no thanks", nil) forState:UIControlStateNormal]; + self.leftButton.alpha = 0; + self.rightButton.alpha = 0; + self.largeButton.alpha = 1; + }]; +} + +- (void)onLargeButton +{ + [[self class] setHasHadInteractionForCurrentVersion]; + + if (self.liked && self.delegate && [self.delegate respondsToSelector:@selector(appbotPromptForReview)]) { + [self.delegate appbotPromptForReview]; + } + else if (!self.liked && self.delegate && [self.delegate respondsToSelector:@selector(appbotPromptForFeedback)]) { + [self.delegate appbotPromptForFeedback]; + } +} + +static NSString* const kInteractionKey = @"ABXPromptViewInteraction"; + ++ (NSString*)keyForCurrentVersion +{ + return [kInteractionKey stringByAppendingString:NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]]; +} + ++ (BOOL)hasHadInteractionForCurrentVersion +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:[self keyForCurrentVersion]]; +} + ++ (void)setHasHadInteractionForCurrentVersion +{ + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:[self keyForCurrentVersion]]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +@end diff --git a/Classes/Views/ABXTextView.h b/Classes/Views/ABXTextView.h new file mode 100644 index 0000000..e904e66 --- /dev/null +++ b/Classes/Views/ABXTextView.h @@ -0,0 +1,15 @@ +// +// ABXTextView.h +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface ABXTextView : UITextView + +@property (nonatomic, strong) NSString *placeholder; + +@end diff --git a/Classes/Views/ABXTextView.m b/Classes/Views/ABXTextView.m new file mode 100644 index 0000000..9896917 --- /dev/null +++ b/Classes/Views/ABXTextView.m @@ -0,0 +1,98 @@ +// +// ABXTextView.m +// Sample Project +// +// Created by Stuart Hall on 30/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXTextView.h" + +@interface ABXTextView () + +@property (nonatomic, assign) BOOL didDrawPlaceholder; + +@end + +@implementation ABXTextView + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + if ((self = [super initWithCoder:aDecoder])) { + [self setup]; + } + return self; +} + + +- (id)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) + { + [self setup]; + } + return self; +} + +- (void)setup +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(textDidChange:) + name:UITextViewTextDidChangeNotification object:self]; +} + +- (void)textDidChange:(NSNotification*)notification +{ + if (self.didDrawPlaceholder || self.text.length == 0) { + [self setNeedsDisplay]; + } +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UITextViewTextDidChangeNotification + object:self]; +} + +- (void)drawRect:(CGRect)rect +{ + [super drawRect:rect]; + + if (self.text.length == 0 && self.placeholder) { + self.didDrawPlaceholder = YES; + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] != NSOrderedAscending) { + CGRect rect = CGRectInset(self.bounds, 4, 8); + [self.placeholder drawInRect:rect + withAttributes:@{ NSFontAttributeName : self.font, + NSForegroundColorAttributeName : [UIColor colorWithWhite:0.8 alpha:1]}]; + } + else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + CGRect rect = CGRectInset(self.bounds, 8, 8); + [[UIColor colorWithWhite:0.8 alpha:1] set]; + [self.placeholder drawInRect:rect withFont:self.font]; +#pragma clang diagnostic pop + } + } + else { + self.didDrawPlaceholder = NO; + } +} + +- (void)setText:(NSString *)text +{ + [super setText:text]; + if (self.didDrawPlaceholder || self.text.length == 0) { + [self setNeedsDisplay]; + } +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + _placeholder = placeholder; + [self setNeedsDisplay]; +} + +@end diff --git a/Classes/Views/ABXVersionTableViewCell.h b/Classes/Views/ABXVersionTableViewCell.h new file mode 100644 index 0000000..56be1b1 --- /dev/null +++ b/Classes/Views/ABXVersionTableViewCell.h @@ -0,0 +1,19 @@ +// +// ABXVersionTableViewCell.h +// Sample Project +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@class ABXVersion; + +@interface ABXVersionTableViewCell : UITableViewCell + +- (void)setVersion:(ABXVersion*)version; + ++ (CGFloat)heightForVersion:(ABXVersion*)version withWidth:(CGFloat)width; + +@end diff --git a/Classes/Views/ABXVersionTableViewCell.m b/Classes/Views/ABXVersionTableViewCell.m new file mode 100644 index 0000000..111cfda --- /dev/null +++ b/Classes/Views/ABXVersionTableViewCell.m @@ -0,0 +1,89 @@ +// +// ABXVersionTableViewCell.m +// Sample Project +// +// Created by Stuart Hall on 22/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXVersionTableViewCell.h" + +#import "ABXVersion.h" +#import "NSString+ABXSizing.h" + +@interface ABXVersionTableViewCell () + +@property (nonatomic, strong) UILabel *versionLabel; +@property (nonatomic, strong) UILabel *dateLabel; +@property (nonatomic, strong) UILabel *textDetailsLabel; + +@end + +@implementation ABXVersionTableViewCell + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + self.layer.shouldRasterize = YES; + self.layer.rasterizationScale = [UIScreen mainScreen].scale; + self.selectionStyle = UITableViewCellSelectionStyleNone; + + // Version number + self.versionLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 10, (CGRectGetWidth(self.contentView.bounds) - 30)/2, 30)]; + self.versionLabel.textColor = [UIColor blackColor]; + self.versionLabel.font = [UIFont systemFontOfSize:15]; + [self.contentView addSubview:self.versionLabel]; + + // Release date + self.dateLabel = [[UILabel alloc] initWithFrame:CGRectMake(CGRectGetMidX(self.contentView.bounds), 10, (CGRectGetWidth(self.contentView.bounds) - 30)/2, 30)]; + self.dateLabel.textColor = [UIColor blackColor]; + self.dateLabel.textAlignment = NSTextAlignmentRight; + self.dateLabel.font = [UIFont systemFontOfSize:15]; + self.dateLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + [self.contentView addSubview:self.dateLabel]; + + // Text + self.textDetailsLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth(self.contentView.bounds) - 30, 0)]; + self.textDetailsLabel.textColor = [UIColor darkGrayColor]; + self.textDetailsLabel.font = [ABXVersionTableViewCell detailFont]; + self.textDetailsLabel.numberOfLines = 0; + self.textDetailsLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self.contentView addSubview:self.textDetailsLabel]; + } + return self; +} + +- (void)setVersion:(ABXVersion *)version +{ + self.versionLabel.text = [NSLocalizedString(@"Version ", nil) stringByAppendingString:version.version]; + + static dispatch_once_t onceToken; + static NSDateFormatter *dateFormatter = nil; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateStyle:NSDateFormatterLongStyle]; + }); + + self.dateLabel.text = [dateFormatter stringFromDate:version.releaseDate]; + + self.textDetailsLabel.text = version.text; + [self.textDetailsLabel sizeToFit]; +} + ++ (UIFont*)detailFont +{ + static dispatch_once_t onceToken; + static UIFont *font = nil; + dispatch_once(&onceToken, ^{ + font = [UIFont systemFontOfSize:14]; + }); + return font; +} + ++ (CGFloat)heightForVersion:(ABXVersion*)version withWidth:(CGFloat)width +{ + return [version.text heightForWidth:width - 30 andFont:[self detailFont]] + 60; +} + +@end diff --git a/Example/Sample Project.xcodeproj/project.pbxproj b/Example/Sample Project.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fa4ee42 --- /dev/null +++ b/Example/Sample Project.xcodeproj/project.pbxproj @@ -0,0 +1,604 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + E0369CE8193822F10082ADB7 /* ABXFeedbackViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0369CE7193822F10082ADB7 /* ABXFeedbackViewController.m */; }; + E0369CEB1938233F0082ADB7 /* ABXKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = E0369CEA1938233F0082ADB7 /* ABXKeychain.m */; }; + E0369CEE193825210082ADB7 /* ABXTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = E0369CED193825210082ADB7 /* ABXTextView.m */; }; + E0369CF41938324C0082ADB7 /* ABXNotificationView.m in Sources */ = {isa = PBXBuildFile; fileRef = E0369CF31938324C0082ADB7 /* ABXNotificationView.m */; }; + E0369CF9193869A90082ADB7 /* ABXPromptView.m in Sources */ = {isa = PBXBuildFile; fileRef = E0369CF8193869A90082ADB7 /* ABXPromptView.m */; }; + E039FD4C192C290E00BA0B76 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD4B192C290E00BA0B76 /* Foundation.framework */; }; + E039FD4E192C290E00BA0B76 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD4D192C290E00BA0B76 /* CoreGraphics.framework */; }; + E039FD50192C290E00BA0B76 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD4F192C290E00BA0B76 /* UIKit.framework */; }; + E039FD56192C290E00BA0B76 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E039FD54192C290E00BA0B76 /* InfoPlist.strings */; }; + E039FD58192C290E00BA0B76 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD57192C290E00BA0B76 /* main.m */; }; + E039FD5C192C290E00BA0B76 /* ABXAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD5B192C290E00BA0B76 /* ABXAppDelegate.m */; }; + E039FD5E192C290E00BA0B76 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E039FD5D192C290E00BA0B76 /* Images.xcassets */; }; + E039FD65192C290E00BA0B76 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD64192C290E00BA0B76 /* XCTest.framework */; }; + E039FD66192C290E00BA0B76 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD4B192C290E00BA0B76 /* Foundation.framework */; }; + E039FD67192C290E00BA0B76 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E039FD4F192C290E00BA0B76 /* UIKit.framework */; }; + E039FD84192C29E600BA0B76 /* ABXApiClient.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD80192C29E600BA0B76 /* ABXApiClient.m */; }; + E039FD87192C2E3100BA0B76 /* NSDictionary+ABXQueryString.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD86192C2E3100BA0B76 /* NSDictionary+ABXQueryString.m */; }; + E039FD8A192C2E6400BA0B76 /* NSString+ABXURLEncoding.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD89192C2E6400BA0B76 /* NSString+ABXURLEncoding.m */; }; + E039FD8D192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD8C192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.m */; }; + E039FD90192C4DF500BA0B76 /* ABXFaq.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD8F192C4DF500BA0B76 /* ABXFaq.m */; }; + E039FD93192C51FB00BA0B76 /* ABXViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD92192C51FB00BA0B76 /* ABXViewController.m */; }; + E039FD95192C521B00BA0B76 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E039FD94192C521B00BA0B76 /* Storyboard.storyboard */; }; + E039FD98192C58BE00BA0B76 /* ABXNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD97192C58BE00BA0B76 /* ABXNotification.m */; }; + E039FD9B192C5BFB00BA0B76 /* ABXVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD9A192C5BFB00BA0B76 /* ABXVersion.m */; }; + E039FD9E192C843000BA0B76 /* ABXModel.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FD9D192C843000BA0B76 /* ABXModel.m */; }; + E039FDA1192C8F7A00BA0B76 /* ABXIssue.m in Sources */ = {isa = PBXBuildFile; fileRef = E039FDA0192C8F7A00BA0B76 /* ABXIssue.m */; }; + E0B113D419481EB2000E873E /* ABXNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B113D319481EB2000E873E /* ABXNavigationController.m */; }; + E0B114881949293B000E873E /* NSString+ABXSizing.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B114871949293B000E873E /* NSString+ABXSizing.m */; }; + E0C5377F192CC7BB00858D01 /* ABXFAQsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C5377E192CC7BB00858D01 /* ABXFAQsViewController.m */; }; + E0C53782192CD74F00858D01 /* ABXFAQViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C53781192CD74F00858D01 /* ABXFAQViewController.m */; }; + E0C537C7192DCC7500858D01 /* ABXVersionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C537C6192DCC7500858D01 /* ABXVersionsViewController.m */; }; + E0C537CA192DCDCB00858D01 /* ABXBaseListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C537C9192DCDCB00858D01 /* ABXBaseListViewController.m */; }; + E0C537CD192E228D00858D01 /* ABXVersionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C537CC192E228D00858D01 /* ABXVersionTableViewCell.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + E039FD68192C290E00BA0B76 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E039FD40192C290E00BA0B76 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E039FD47192C290E00BA0B76; + remoteInfo = "Sample Project"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + E0369CE6193822F10082ADB7 /* ABXFeedbackViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXFeedbackViewController.h; sourceTree = ""; }; + E0369CE7193822F10082ADB7 /* ABXFeedbackViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXFeedbackViewController.m; sourceTree = ""; }; + E0369CE91938233F0082ADB7 /* ABXKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXKeychain.h; sourceTree = ""; }; + E0369CEA1938233F0082ADB7 /* ABXKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXKeychain.m; sourceTree = ""; }; + E0369CEC193825210082ADB7 /* ABXTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXTextView.h; sourceTree = ""; }; + E0369CED193825210082ADB7 /* ABXTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXTextView.m; sourceTree = ""; }; + E0369CF21938324C0082ADB7 /* ABXNotificationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXNotificationView.h; sourceTree = ""; }; + E0369CF31938324C0082ADB7 /* ABXNotificationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXNotificationView.m; sourceTree = ""; }; + E0369CF5193852BD0082ADB7 /* ABX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABX.h; sourceTree = ""; }; + E0369CF7193869A90082ADB7 /* ABXPromptView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXPromptView.h; sourceTree = ""; }; + E0369CF8193869A90082ADB7 /* ABXPromptView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXPromptView.m; sourceTree = ""; }; + E039FD48192C290E00BA0B76 /* Sample Project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sample Project.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + E039FD4B192C290E00BA0B76 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + E039FD4D192C290E00BA0B76 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + E039FD4F192C290E00BA0B76 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + E039FD53192C290E00BA0B76 /* Sample Project-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Sample Project-Info.plist"; sourceTree = ""; }; + E039FD55192C290E00BA0B76 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E039FD57192C290E00BA0B76 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + E039FD59192C290E00BA0B76 /* Sample Project-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Sample Project-Prefix.pch"; sourceTree = ""; }; + E039FD5A192C290E00BA0B76 /* ABXAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ABXAppDelegate.h; sourceTree = ""; }; + E039FD5B192C290E00BA0B76 /* ABXAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ABXAppDelegate.m; sourceTree = ""; }; + E039FD5D192C290E00BA0B76 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + E039FD63192C290E00BA0B76 /* Sample ProjectTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Sample ProjectTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + E039FD64192C290E00BA0B76 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + E039FD7F192C29E600BA0B76 /* ABXApiClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXApiClient.h; sourceTree = ""; }; + E039FD80192C29E600BA0B76 /* ABXApiClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXApiClient.m; sourceTree = ""; }; + E039FD85192C2E3100BA0B76 /* NSDictionary+ABXQueryString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+ABXQueryString.h"; sourceTree = ""; }; + E039FD86192C2E3100BA0B76 /* NSDictionary+ABXQueryString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+ABXQueryString.m"; sourceTree = ""; }; + E039FD88192C2E6400BA0B76 /* NSString+ABXURLEncoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+ABXURLEncoding.h"; sourceTree = ""; }; + E039FD89192C2E6400BA0B76 /* NSString+ABXURLEncoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+ABXURLEncoding.m"; sourceTree = ""; }; + E039FD8B192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+ABXNSNullAsNull.h"; sourceTree = ""; }; + E039FD8C192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+ABXNSNullAsNull.m"; sourceTree = ""; }; + E039FD8E192C4DF500BA0B76 /* ABXFaq.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXFaq.h; sourceTree = ""; }; + E039FD8F192C4DF500BA0B76 /* ABXFaq.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXFaq.m; sourceTree = ""; }; + E039FD91192C51FB00BA0B76 /* ABXViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXViewController.h; sourceTree = ""; }; + E039FD92192C51FB00BA0B76 /* ABXViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXViewController.m; sourceTree = ""; }; + E039FD94192C521B00BA0B76 /* Storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = ""; }; + E039FD96192C58BE00BA0B76 /* ABXNotification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXNotification.h; sourceTree = ""; }; + E039FD97192C58BE00BA0B76 /* ABXNotification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXNotification.m; sourceTree = ""; }; + E039FD99192C5BFB00BA0B76 /* ABXVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXVersion.h; sourceTree = ""; }; + E039FD9A192C5BFB00BA0B76 /* ABXVersion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXVersion.m; sourceTree = ""; }; + E039FD9C192C843000BA0B76 /* ABXModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXModel.h; sourceTree = ""; }; + E039FD9D192C843000BA0B76 /* ABXModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXModel.m; sourceTree = ""; }; + E039FD9F192C8F7A00BA0B76 /* ABXIssue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXIssue.h; sourceTree = ""; }; + E039FDA0192C8F7A00BA0B76 /* ABXIssue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXIssue.m; sourceTree = ""; }; + E0B113D219481EB2000E873E /* ABXNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXNavigationController.h; sourceTree = ""; }; + E0B113D319481EB2000E873E /* ABXNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXNavigationController.m; sourceTree = ""; }; + E0B114861949293B000E873E /* NSString+ABXSizing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+ABXSizing.h"; sourceTree = ""; }; + E0B114871949293B000E873E /* NSString+ABXSizing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+ABXSizing.m"; sourceTree = ""; }; + E0C5377D192CC7BB00858D01 /* ABXFAQsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXFAQsViewController.h; sourceTree = ""; }; + E0C5377E192CC7BB00858D01 /* ABXFAQsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXFAQsViewController.m; sourceTree = ""; }; + E0C53780192CD74F00858D01 /* ABXFAQViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXFAQViewController.h; sourceTree = ""; }; + E0C53781192CD74F00858D01 /* ABXFAQViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXFAQViewController.m; sourceTree = ""; }; + E0C537C5192DCC7500858D01 /* ABXVersionsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXVersionsViewController.h; sourceTree = ""; }; + E0C537C6192DCC7500858D01 /* ABXVersionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXVersionsViewController.m; sourceTree = ""; }; + E0C537C8192DCDCB00858D01 /* ABXBaseListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXBaseListViewController.h; sourceTree = ""; }; + E0C537C9192DCDCB00858D01 /* ABXBaseListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXBaseListViewController.m; sourceTree = ""; }; + E0C537CB192E228D00858D01 /* ABXVersionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ABXVersionTableViewCell.h; sourceTree = ""; }; + E0C537CC192E228D00858D01 /* ABXVersionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ABXVersionTableViewCell.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E039FD45192C290E00BA0B76 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E039FD4E192C290E00BA0B76 /* CoreGraphics.framework in Frameworks */, + E039FD50192C290E00BA0B76 /* UIKit.framework in Frameworks */, + E039FD4C192C290E00BA0B76 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E039FD60192C290E00BA0B76 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E039FD65192C290E00BA0B76 /* XCTest.framework in Frameworks */, + E039FD67192C290E00BA0B76 /* UIKit.framework in Frameworks */, + E039FD66192C290E00BA0B76 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E039FD3F192C290E00BA0B76 = { + isa = PBXGroup; + children = ( + E039FD7A192C292C00BA0B76 /* AppbotX */, + E039FD51192C290E00BA0B76 /* Sample Project */, + E039FD4A192C290E00BA0B76 /* Frameworks */, + E039FD49192C290E00BA0B76 /* Products */, + ); + sourceTree = ""; + }; + E039FD49192C290E00BA0B76 /* Products */ = { + isa = PBXGroup; + children = ( + E039FD48192C290E00BA0B76 /* Sample Project.app */, + E039FD63192C290E00BA0B76 /* Sample ProjectTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + E039FD4A192C290E00BA0B76 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E039FD4B192C290E00BA0B76 /* Foundation.framework */, + E039FD4D192C290E00BA0B76 /* CoreGraphics.framework */, + E039FD4F192C290E00BA0B76 /* UIKit.framework */, + E039FD64192C290E00BA0B76 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E039FD51192C290E00BA0B76 /* Sample Project */ = { + isa = PBXGroup; + children = ( + E039FD5A192C290E00BA0B76 /* ABXAppDelegate.h */, + E039FD5B192C290E00BA0B76 /* ABXAppDelegate.m */, + E039FD5D192C290E00BA0B76 /* Images.xcassets */, + E039FD52192C290E00BA0B76 /* Supporting Files */, + E039FD91192C51FB00BA0B76 /* ABXViewController.h */, + E039FD92192C51FB00BA0B76 /* ABXViewController.m */, + E039FD94192C521B00BA0B76 /* Storyboard.storyboard */, + ); + path = "Sample Project"; + sourceTree = ""; + }; + E039FD52192C290E00BA0B76 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + E039FD53192C290E00BA0B76 /* Sample Project-Info.plist */, + E039FD54192C290E00BA0B76 /* InfoPlist.strings */, + E039FD57192C290E00BA0B76 /* main.m */, + E039FD59192C290E00BA0B76 /* Sample Project-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E039FD7A192C292C00BA0B76 /* AppbotX */ = { + isa = PBXGroup; + children = ( + E039FD7E192C29E600BA0B76 /* Classes */, + E039FD81192C29E600BA0B76 /* Controllers */, + E039FD82192C29E600BA0B76 /* Models */, + E039FD83192C29E600BA0B76 /* Views */, + E0369CF5193852BD0082ADB7 /* ABX.h */, + ); + name = AppbotX; + path = ../Classes; + sourceTree = ""; + }; + E039FD7E192C29E600BA0B76 /* Classes */ = { + isa = PBXGroup; + children = ( + E039FD7F192C29E600BA0B76 /* ABXApiClient.h */, + E039FD80192C29E600BA0B76 /* ABXApiClient.m */, + E039FD85192C2E3100BA0B76 /* NSDictionary+ABXQueryString.h */, + E039FD86192C2E3100BA0B76 /* NSDictionary+ABXQueryString.m */, + E039FD88192C2E6400BA0B76 /* NSString+ABXURLEncoding.h */, + E039FD89192C2E6400BA0B76 /* NSString+ABXURLEncoding.m */, + E039FD8B192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.h */, + E039FD8C192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.m */, + E0369CE91938233F0082ADB7 /* ABXKeychain.h */, + E0369CEA1938233F0082ADB7 /* ABXKeychain.m */, + E0B114861949293B000E873E /* NSString+ABXSizing.h */, + E0B114871949293B000E873E /* NSString+ABXSizing.m */, + ); + path = Classes; + sourceTree = ""; + }; + E039FD81192C29E600BA0B76 /* Controllers */ = { + isa = PBXGroup; + children = ( + E0C537C8192DCDCB00858D01 /* ABXBaseListViewController.h */, + E0C537C9192DCDCB00858D01 /* ABXBaseListViewController.m */, + E0C5377D192CC7BB00858D01 /* ABXFAQsViewController.h */, + E0C5377E192CC7BB00858D01 /* ABXFAQsViewController.m */, + E0C53780192CD74F00858D01 /* ABXFAQViewController.h */, + E0C53781192CD74F00858D01 /* ABXFAQViewController.m */, + E0C537C5192DCC7500858D01 /* ABXVersionsViewController.h */, + E0C537C6192DCC7500858D01 /* ABXVersionsViewController.m */, + E0369CE6193822F10082ADB7 /* ABXFeedbackViewController.h */, + E0369CE7193822F10082ADB7 /* ABXFeedbackViewController.m */, + E0B113D219481EB2000E873E /* ABXNavigationController.h */, + E0B113D319481EB2000E873E /* ABXNavigationController.m */, + ); + path = Controllers; + sourceTree = ""; + }; + E039FD82192C29E600BA0B76 /* Models */ = { + isa = PBXGroup; + children = ( + E039FD9C192C843000BA0B76 /* ABXModel.h */, + E039FD9D192C843000BA0B76 /* ABXModel.m */, + E039FD8E192C4DF500BA0B76 /* ABXFaq.h */, + E039FD8F192C4DF500BA0B76 /* ABXFaq.m */, + E039FD96192C58BE00BA0B76 /* ABXNotification.h */, + E039FD97192C58BE00BA0B76 /* ABXNotification.m */, + E039FD99192C5BFB00BA0B76 /* ABXVersion.h */, + E039FD9A192C5BFB00BA0B76 /* ABXVersion.m */, + E039FD9F192C8F7A00BA0B76 /* ABXIssue.h */, + E039FDA0192C8F7A00BA0B76 /* ABXIssue.m */, + ); + path = Models; + sourceTree = ""; + }; + E039FD83192C29E600BA0B76 /* Views */ = { + isa = PBXGroup; + children = ( + E0C537CB192E228D00858D01 /* ABXVersionTableViewCell.h */, + E0C537CC192E228D00858D01 /* ABXVersionTableViewCell.m */, + E0369CEC193825210082ADB7 /* ABXTextView.h */, + E0369CED193825210082ADB7 /* ABXTextView.m */, + E0369CF21938324C0082ADB7 /* ABXNotificationView.h */, + E0369CF31938324C0082ADB7 /* ABXNotificationView.m */, + E0369CF7193869A90082ADB7 /* ABXPromptView.h */, + E0369CF8193869A90082ADB7 /* ABXPromptView.m */, + ); + path = Views; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E039FD47192C290E00BA0B76 /* Sample Project */ = { + isa = PBXNativeTarget; + buildConfigurationList = E039FD74192C290E00BA0B76 /* Build configuration list for PBXNativeTarget "Sample Project" */; + buildPhases = ( + E039FD44192C290E00BA0B76 /* Sources */, + E039FD45192C290E00BA0B76 /* Frameworks */, + E039FD46192C290E00BA0B76 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Sample Project"; + productName = "Sample Project"; + productReference = E039FD48192C290E00BA0B76 /* Sample Project.app */; + productType = "com.apple.product-type.application"; + }; + E039FD62192C290E00BA0B76 /* Sample ProjectTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E039FD77192C290E00BA0B76 /* Build configuration list for PBXNativeTarget "Sample ProjectTests" */; + buildPhases = ( + E039FD5F192C290E00BA0B76 /* Sources */, + E039FD60192C290E00BA0B76 /* Frameworks */, + E039FD61192C290E00BA0B76 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E039FD69192C290E00BA0B76 /* PBXTargetDependency */, + ); + name = "Sample ProjectTests"; + productName = "Sample ProjectTests"; + productReference = E039FD63192C290E00BA0B76 /* Sample ProjectTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E039FD40192C290E00BA0B76 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = ABX; + LastUpgradeCheck = 0510; + ORGANIZATIONNAME = Appbot; + TargetAttributes = { + E039FD62192C290E00BA0B76 = { + TestTargetID = E039FD47192C290E00BA0B76; + }; + }; + }; + buildConfigurationList = E039FD43192C290E00BA0B76 /* Build configuration list for PBXProject "Sample Project" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = E039FD3F192C290E00BA0B76; + productRefGroup = E039FD49192C290E00BA0B76 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E039FD47192C290E00BA0B76 /* Sample Project */, + E039FD62192C290E00BA0B76 /* Sample ProjectTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E039FD46192C290E00BA0B76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E039FD56192C290E00BA0B76 /* InfoPlist.strings in Resources */, + E039FD5E192C290E00BA0B76 /* Images.xcassets in Resources */, + E039FD95192C521B00BA0B76 /* Storyboard.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E039FD61192C290E00BA0B76 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E039FD44192C290E00BA0B76 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E039FD93192C51FB00BA0B76 /* ABXViewController.m in Sources */, + E0369CE8193822F10082ADB7 /* ABXFeedbackViewController.m in Sources */, + E039FD8A192C2E6400BA0B76 /* NSString+ABXURLEncoding.m in Sources */, + E0B114881949293B000E873E /* NSString+ABXSizing.m in Sources */, + E039FD98192C58BE00BA0B76 /* ABXNotification.m in Sources */, + E039FD84192C29E600BA0B76 /* ABXApiClient.m in Sources */, + E0C537CD192E228D00858D01 /* ABXVersionTableViewCell.m in Sources */, + E039FD5C192C290E00BA0B76 /* ABXAppDelegate.m in Sources */, + E0369CEE193825210082ADB7 /* ABXTextView.m in Sources */, + E039FDA1192C8F7A00BA0B76 /* ABXIssue.m in Sources */, + E039FD90192C4DF500BA0B76 /* ABXFaq.m in Sources */, + E0C537CA192DCDCB00858D01 /* ABXBaseListViewController.m in Sources */, + E039FD9B192C5BFB00BA0B76 /* ABXVersion.m in Sources */, + E0C537C7192DCC7500858D01 /* ABXVersionsViewController.m in Sources */, + E039FD9E192C843000BA0B76 /* ABXModel.m in Sources */, + E0369CEB1938233F0082ADB7 /* ABXKeychain.m in Sources */, + E039FD8D192C486000BA0B76 /* NSDictionary+ABXNSNullAsNull.m in Sources */, + E039FD87192C2E3100BA0B76 /* NSDictionary+ABXQueryString.m in Sources */, + E0B113D419481EB2000E873E /* ABXNavigationController.m in Sources */, + E0C53782192CD74F00858D01 /* ABXFAQViewController.m in Sources */, + E0369CF9193869A90082ADB7 /* ABXPromptView.m in Sources */, + E0369CF41938324C0082ADB7 /* ABXNotificationView.m in Sources */, + E039FD58192C290E00BA0B76 /* main.m in Sources */, + E0C5377F192CC7BB00858D01 /* ABXFAQsViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E039FD5F192C290E00BA0B76 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E039FD69192C290E00BA0B76 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E039FD47192C290E00BA0B76 /* Sample Project */; + targetProxy = E039FD68192C290E00BA0B76 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + E039FD54192C290E00BA0B76 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + E039FD55192C290E00BA0B76 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E039FD72192C290E00BA0B76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E039FD73192C290E00BA0B76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E039FD75192C290E00BA0B76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Sample Project/Sample Project-Prefix.pch"; + INFOPLIST_FILE = "Sample Project/Sample Project-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + E039FD76192C290E00BA0B76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Sample Project/Sample Project-Prefix.pch"; + INFOPLIST_FILE = "Sample Project/Sample Project-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + E039FD78192C290E00BA0B76 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Sample Project.app/Sample Project"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Sample Project/Sample Project-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Sample ProjectTests/Sample ProjectTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + E039FD79192C290E00BA0B76 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Sample Project.app/Sample Project"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Sample Project/Sample Project-Prefix.pch"; + INFOPLIST_FILE = "Sample ProjectTests/Sample ProjectTests-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E039FD43192C290E00BA0B76 /* Build configuration list for PBXProject "Sample Project" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E039FD72192C290E00BA0B76 /* Debug */, + E039FD73192C290E00BA0B76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E039FD74192C290E00BA0B76 /* Build configuration list for PBXNativeTarget "Sample Project" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E039FD75192C290E00BA0B76 /* Debug */, + E039FD76192C290E00BA0B76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E039FD77192C290E00BA0B76 /* Build configuration list for PBXNativeTarget "Sample ProjectTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E039FD78192C290E00BA0B76 /* Debug */, + E039FD79192C290E00BA0B76 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E039FD40192C290E00BA0B76 /* Project object */; +} diff --git a/Example/Sample Project.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Sample Project.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..98c3ed9 --- /dev/null +++ b/Example/Sample Project.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Sample Project/ABXAppDelegate.h b/Example/Sample Project/ABXAppDelegate.h new file mode 100644 index 0000000..4235bd0 --- /dev/null +++ b/Example/Sample Project/ABXAppDelegate.h @@ -0,0 +1,15 @@ +// +// ABXAppDelegate.h +// Sample Project +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface ABXAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/Example/Sample Project/ABXAppDelegate.m b/Example/Sample Project/ABXAppDelegate.m new file mode 100644 index 0000000..9fde9f0 --- /dev/null +++ b/Example/Sample Project/ABXAppDelegate.m @@ -0,0 +1,52 @@ +// +// ABXAppDelegate.m +// Sample Project +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXAppDelegate.h" + +@implementation ABXAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // The test API key, you will need to change this to suit + [[ABXApiClient instance] setApiKey:@"bc2c1345090cee2262258834db71a1e9417365a7"]; + + if ([self.window respondsToSelector:@selector(setTintColor:)]) { + self.window.tintColor = [UIColor colorWithRed:0xf5/255.0f green:0x8c/255.0f blue:0x75/255.0f alpha:1]; + } + [self.window makeKeyAndVisible]; + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/Example/Sample Project/ABXViewController.h b/Example/Sample Project/ABXViewController.h new file mode 100644 index 0000000..2efec5c --- /dev/null +++ b/Example/Sample Project/ABXViewController.h @@ -0,0 +1,13 @@ +// +// ABXViewController.h +// Sample Project +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +@interface ABXViewController : UIViewController + +@end diff --git a/Example/Sample Project/ABXViewController.m b/Example/Sample Project/ABXViewController.m new file mode 100644 index 0000000..7347b3d --- /dev/null +++ b/Example/Sample Project/ABXViewController.m @@ -0,0 +1,234 @@ +// +// ABXViewController.m +// Sample Project +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import "ABXViewController.h" + +#import "ABX.h" +#import "ABXPromptView.h" + +@interface ABXViewController () + +@property (nonatomic, strong) IBOutlet ABXPromptView *promptView; + +@end + +@implementation ABXViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // The prompt view is an example workflow using AppbotX + // you could choose to hide it if it's been seen already + // [ABXPromptView hasHadInteractionForCurrentVersion] + // It's also good to only show it after a positive interaction + // or a number of usages of the app + self.promptView.delegate = self; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (BOOL)prefersStatusBarHidden +{ + return NO; +} + +#pragma mark - Buttons + +- (IBAction)onFetchNotifications:(id)sender +{ + // Fetch the notifications, there will only ever be one + [ABXNotification fetch:^(NSArray *notifications, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + switch (responseCode) { + case ABXResponseCodeSuccess: { + if (notifications.count > 0) { + ABXNotification *notification = [notifications firstObject]; + + if (![notification hasSeen]) { + // Show the view + [ABXNotificationView show:notification.message + actionText:notification.actionLabel + backgroundColor:[UIColor colorWithRed:0x86/255.0 green:0xcc/255.0 blue:0xf1/255.0 alpha:1] + textColor:[UIColor blackColor] + buttonColor:[UIColor whiteColor] + inController:self + actionBlock:^(ABXNotificationView *view) { + // Open the URL + // Here you could open it in your internal UIWebView or route accordingly + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:notification.actionUrl]]; + } dismissBlock:^(ABXNotificationView *view) { + // Here you can mark it as seen if you + // don't want it to appear again + // [notification markAsSeen]; + }]; + } + } + else { + [self showAlert:@"Notification" message:@"No notifications"]; + } + } + break; + + default: { + [self showAlert:@"Notification Error" message:[NSString stringWithFormat:@"%u", responseCode]]; + } + break; + } + }]; +} + +- (IBAction)onFetchVersions:(id)sender +{ + [ABXVersion fetch:^(NSArray *versions, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + switch (responseCode) { + case ABXResponseCodeSuccess: { + [self showAlert:@"Versions" message:[NSString stringWithFormat:@"Received %ld versions", (unsigned long)versions.count]]; + } + break; + + default: { + [self showAlert:@"Versions" message:[NSString stringWithFormat:@"%u", responseCode]]; + } + break; + } + }]; +} + +- (IBAction)onFetchCurrentVersion:(id)sender +{ + [ABXVersion fetchCurrentVersion:^(ABXVersion *version, ABXVersion *currentVersion, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + if (responseCode == ABXResponseCodeSuccess) { + if (currentVersion && [currentVersion isNewerThanCurrent]) { + // Show the view + [ABXNotificationView show:[NSString stringWithFormat:@"An update to version %@ is available", currentVersion.version] + actionText:NSLocalizedString(@"Update", nil) + backgroundColor:[UIColor colorWithRed:0xf4/255.0 green:0x7d/255.0 blue:0x67/255.0 alpha:1] + textColor:[UIColor blackColor] + buttonColor:[UIColor whiteColor] + inController:self + actionBlock:^(ABXNotificationView *view) { + // Throw them to the App Store + [view dismiss]; + [self openAppStoreForApp:@"650762525"]; + } + dismissBlock:^(ABXNotificationView *view) { + // Any action you want + }]; + } + else if (version) { + // We got a match! + if ([version hasSeen]) { + NSLog(@"Already shown this version"); + } + else { + // Show the view + [ABXNotificationView show:[NSString stringWithFormat:@"You've just updated to v%@", version.version] + actionText:NSLocalizedString(@"Learn More", nil) + backgroundColor:[UIColor colorWithRed:0xf4/255.0 green:0x7d/255.0 blue:0x67/255.0 alpha:1] + textColor:[UIColor blackColor] + buttonColor:[UIColor whiteColor] + inController:self + actionBlock:^(ABXNotificationView *view) { + // Take them to all the versions, or you could choose + // to just show the one version + [view dismiss]; + [ABXVersionsViewController showFromController:self]; + } + dismissBlock:^(ABXNotificationView *view) { + // Here you can mark it as seen if you + // don't want it to appear again + // [version markAsSeen]; + }]; + } + } + } + }]; +} + +- (IBAction)onFetchFAQs:(id)sender +{ + [ABXFaq fetch:^(NSArray *faqs, ABXResponseCode responseCode, NSInteger httpCode, NSError *error) { + switch (responseCode) { + case ABXResponseCodeSuccess: { + [self showAlert:@"FAQs" message:[NSString stringWithFormat:@"Received %ld faqs", (unsigned long)faqs.count]]; + } + break; + + default: { + [self showAlert:@"FAQs" message:[NSString stringWithFormat:@"%u", responseCode]]; + } + break; + } + }]; +} + +- (IBAction)showFAQs:(id)sender +{ + [ABXFAQsViewController showFromController:self]; +} + +- (IBAction)showVersions:(id)sender +{ + [ABXVersionsViewController showFromController:self]; +} + +- (IBAction)showFeedback:(id)sender +{ + [ABXFeedbackViewController showFromController:self placeholder:nil]; +} + +#pragma mark - Alert + +- (void)showAlert:(NSString*)title message:(NSString*)message +{ + [[[UIAlertView alloc] initWithTitle:title + message:message + delegate:nil + cancelButtonTitle:NSLocalizedString(@"OK", nil) + otherButtonTitles:nil] show]; +} + +#pragma mark - App Store + +- (void)openAppStoreReviewForApp:(NSString*)itunesId +{ + if ([[[UIDevice currentDevice] systemVersion] compare:@"7.1" options:NSNumericSearch] != NSOrderedAscending) { + // Since 7.1 we can throw to the review tab + NSString *url = [NSString stringWithFormat:@"http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=%@&pageNumber=0&ct=appbotReviewPrompt&at=11l4LZ&type=Purple%%252BSoftware&mt=8&sortOrdering=2", itunesId]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + } + else { + [self openAppStoreForApp:itunesId]; + } +} + +- (void)openAppStoreForApp:(NSString*)itunesId +{ + NSString *url = [NSString stringWithFormat:@"https://itunes.apple.com/au/app/app/id%@?mt=8", itunesId]; + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; +} + +#pragma mark - ABXPromptViewDelegate + +- (void)appbotPromptForReview +{ + [self openAppStoreReviewForApp:@"650762525"]; + self.promptView.hidden = YES; +} + +- (void)appbotPromptForFeedback +{ + [ABXFeedbackViewController showFromController:self placeholder:nil]; + self.promptView.hidden = YES; +} + +@end diff --git a/Example/Sample Project/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/Sample Project/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..91bf9c1 --- /dev/null +++ b/Example/Sample Project/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sample Project/Images.xcassets/Appbot.imageset/Contents.json b/Example/Sample Project/Images.xcassets/Appbot.imageset/Contents.json new file mode 100644 index 0000000..430e4f2 --- /dev/null +++ b/Example/Sample Project/Images.xcassets/Appbot.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "appbotx-logo.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sample Project/Images.xcassets/Appbot.imageset/appbotx-logo.png b/Example/Sample Project/Images.xcassets/Appbot.imageset/appbotx-logo.png new file mode 100644 index 0000000..e086096 Binary files /dev/null and b/Example/Sample Project/Images.xcassets/Appbot.imageset/appbotx-logo.png differ diff --git a/Example/Sample Project/Images.xcassets/LaunchImage.launchimage/Contents.json b/Example/Sample Project/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..6f870a4 --- /dev/null +++ b/Example/Sample Project/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sample Project/Sample Project-Info.plist b/Example/Sample Project/Sample Project-Info.plist new file mode 100644 index 0000000..6305941 --- /dev/null +++ b/Example/Sample Project/Sample Project-Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + co.appbot.sampleproject + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.2 + CFBundleSignature + ???? + CFBundleVersion + 1.2 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Storyboard + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/Sample Project/Sample Project-Prefix.pch b/Example/Sample Project/Sample Project-Prefix.pch new file mode 100644 index 0000000..e300054 --- /dev/null +++ b/Example/Sample Project/Sample Project-Prefix.pch @@ -0,0 +1,18 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#import + +#ifndef __IPHONE_3_0 +#warning "This project uses features only available in iOS SDK 3.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import + + #import "ABX.h" +#endif diff --git a/Example/Sample Project/Storyboard.storyboard b/Example/Sample Project/Storyboard.storyboard new file mode 100644 index 0000000..7a35a47 --- /dev/null +++ b/Example/Sample Project/Storyboard.storyboard @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Sample Project/en.lproj/InfoPlist.strings b/Example/Sample Project/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Example/Sample Project/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Example/Sample Project/main.m b/Example/Sample Project/main.m new file mode 100644 index 0000000..5449e09 --- /dev/null +++ b/Example/Sample Project/main.m @@ -0,0 +1,18 @@ +// +// main.m +// Sample Project +// +// Created by Stuart Hall on 21/05/2014. +// Copyright (c) 2014 Appbot. All rights reserved. +// + +#import + +#import "ABXAppDelegate.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([ABXAppDelegate class])); + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..433a7ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Appbot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8778044 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# AppbotX + +AppbotX is an iOS client library and sample application for the [AppbotX](http://appbot.co/appbotx) service. It is currently in limited beta. + +## Usage + +To run the example project; clone the repo, and open "Sample Project.xcodeproj" from the Example folder. + +Detailed usage documentation is coming, but please refer to the example for the time being. + +## Requirements + +The sample project includes a test key, but for you own application you will need an [Appbot](http://appbot.co) account and an API key. + +## Installation + +Appbotx will be available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile and run pod install. + + pod "AppbotX", :git => "https://github.com/appbotx/appbotx.git" + +Then initialize with your API key in your AppDelegate + + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions + { + [[ABXApiClient instance] setApiKey:@"API_KEY"]; + return YES; + } + +And import ABX.h into your precompiled header. Alternatively you can just include it within the files you require. + + #ifdef __OBJC__ + #import + #import + + #import "ABX.h" + #endif + +## License + +AppbotX is available under the MIT license. See the LICENSE file for more info. + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..242fb1d --- /dev/null +++ b/Rakefile @@ -0,0 +1,160 @@ +desc "Runs the specs [EMPTY]" +task :spec do + # Provide your own implementation +end + +task :version do + git_remotes = `git remote`.strip.split("\n") + + if git_remotes.count > 0 + puts "-- fetching version number from github" + sh 'git fetch' + + remote_version = remote_spec_version + end + + if remote_version.nil? + puts "There is no current released version. You're about to release a new Pod." + version = "0.0.1" + else + puts "The current released version of your pod is " + + remote_spec_version.to_s() + version = suggested_version_number + end + + puts "Enter the version you want to release (" + version + ") " + new_version_number = $stdin.gets.strip + if new_version_number == "" + new_version_number = version + end + + replace_version_number(new_version_number) +end + +desc "Release a new version of the Pod (append repo=name to push to a private spec repo)" +task :release do + # Allow override of spec repo name using `repo=private` after task name + repo = ENV["repo"] || "master" + + puts "* Running version" + sh "rake version" + + unless ENV['SKIP_CHECKS'] + if `git symbolic-ref HEAD 2>/dev/null`.strip.split('/').last != 'master' + $stderr.puts "[!] You need to be on the `master' branch in order to be able to do a release." + exit 1 + end + + if `git tag`.strip.split("\n").include?(spec_version) + $stderr.puts "[!] A tag for version `#{spec_version}' already exists. Change the version in the podspec" + exit 1 + end + + puts "You are about to release `#{spec_version}`, is that correct? [y/n]" + exit if $stdin.gets.strip.downcase != 'y' + end + + puts "* Running specs" + sh "rake spec" + + puts "* Linting the podspec" + sh "pod lib lint" + + # Then release + sh "git commit #{podspec_path} CHANGELOG.md -m 'Release #{spec_version}' --allow-empty" + sh "git tag -a #{spec_version} -m 'Release #{spec_version}'" + sh "git push origin master" + sh "git push origin --tags" + if repo == "master" + sh "pod trunk push #{podspec_path}" + else + sh "pod repo push #{repo} #{podspec_path}" + end +end + +# @return [Pod::Version] The version as reported by the Podspec. +# +def spec_version + require 'cocoapods' + spec = Pod::Specification.from_file(podspec_path) + spec.version +end + +# @return [Pod::Version] The version as reported by the Podspec from remote. +# +def remote_spec_version + require 'cocoapods-core' + + if spec_file_exist_on_remote? + remote_spec = eval(`git show origin/master:#{podspec_path}`) + remote_spec.version + else + nil + end +end + +# @return [Bool] If the remote repository has a copy of the podpesc file or not. +# +def spec_file_exist_on_remote? + test_condition = `if git rev-parse --verify --quiet origin/master:#{podspec_path} >/dev/null; + then + echo 'true' + else + echo 'false' + fi` + + 'true' == test_condition.strip +end + +# @return [String] The relative path of the Podspec. +# +def podspec_path + podspecs = Dir.glob('*.podspec') + if podspecs.count == 1 + podspecs.first + else + raise "Could not select a podspec" + end +end + +# @return [String] The suggested version number based on the local and remote +# version numbers. +# +def suggested_version_number + if spec_version != remote_spec_version + spec_version.to_s() + else + next_version(spec_version).to_s() + end +end + +# @param [Pod::Version] version +# the version for which you need the next version +# +# @note It is computed by bumping the last component of +# the version string by 1. +# +# @return [Pod::Version] The version that comes next after +# the version supplied. +# +def next_version(version) + version_components = version.to_s().split("."); + last = (version_components.last.to_i() + 1).to_s + version_components[-1] = last + Pod::Version.new(version_components.join(".")) +end + +# @param [String] new_version_number +# the new version number +# +# @note This methods replaces the version number in the podspec file +# with a new version number. +# +# @return void +# +def replace_version_number(new_version_number) + text = File.read(podspec_path) + text.gsub!(/(s.version( )*= ")#{spec_version}(")/, + "\\1#{new_version_number}\\3") + File.open(podspec_path, "w") { |file| file.puts text } +end diff --git a/appbotx.podspec b/appbotx.podspec new file mode 100644 index 0000000..d29bbe0 --- /dev/null +++ b/appbotx.podspec @@ -0,0 +1,27 @@ +# +# Be sure to run `pod lib lint NAME.podspec' to ensure this is a +# valid spec and remove all comments before submitting the spec. +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = "AppbotX" + s.version = "0.1.0" + s.summary = "A short description of appbotx." + s.description = <<-DESC + An optional longer description of appbotx + + * Markdown format. + * Don't worry about the indent, we strip it! + DESC + s.homepage = "http://appbot.co" + s.license = 'MIT' + s.author = { "Stuart Hall" => "stuartkhall@gmail.com" } + s.source = { :git => "http://github.com/appbotx/appbotx.git", :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/stuartkhall' + + s.platform = :ios, '6.0' + s.requires_arc = true + + s.source_files = 'Classes/*.{h,m}', 'Classes/Models/*.{h,m}', 'Classes/Views/*.{h,m}', 'Classes/Controllers/*.{h,m}', 'Classes/Classes/*.{h,m}' +end