This commit is contained in:
watrabi
2025-09-18 17:55:52 -04:00
commit 977f1ff4b8
15030 changed files with 17324420 additions and 0 deletions
@@ -0,0 +1,70 @@
// AFHTTPRequestOperation.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import "AFURLConnectionOperation.h"
NS_ASSUME_NONNULL_BEGIN
/**
`AFHTTPRequestOperation` is a subclass of `AFURLConnectionOperation` for requests using the HTTP or HTTPS protocols. It encapsulates the concept of acceptable status codes and content types, which determine the success or failure of a request.
*/
@interface AFHTTPRequestOperation : AFURLConnectionOperation
///------------------------------------------------
/// @name Getting HTTP URL Connection Information
///------------------------------------------------
/**
The last HTTP response received by the operation's connection.
*/
@property (readonly, nonatomic, strong, nullable) NSHTTPURLResponse *response;
/**
Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to an AFHTTPResponse serializer, which uses the raw data as its response object. The serializer validates the status code to be in the `2XX` range, denoting success. If the response serializer generates an error in `-responseObjectForResponse:data:error:`, the `failure` callback of the session task or request operation will be executed; otherwise, the `success` callback will be executed.
@warning `responseSerializer` must not be `nil`. Setting a response serializer will clear out any cached value
*/
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> * responseSerializer;
/**
An object constructed by the `responseSerializer` from the response and response data. Returns `nil` unless the operation `isFinished`, has a `response`, and has `responseData` with non-zero content length. If an error occurs during serialization, `nil` will be returned, and the `error` property will be populated with the serialization error.
*/
@property (readonly, nonatomic, strong, nullable) id responseObject;
///-----------------------------------------------------------
/// @name Setting Completion Block Success / Failure Callbacks
///-----------------------------------------------------------
/**
Sets the `completionBlock` property with a block that executes either the specified success or failure block, depending on the state of the request on completion. If `error` returns a value, which can be caused by an unacceptable status code or content type, then `failure` is executed. Otherwise, `success` is executed.
This method should be overridden in subclasses in order to specify the response object passed into the success block.
@param success The block to be executed on the completion of a successful request. This block has no return value and takes two arguments: the receiver operation and the object constructed from the response data of the request.
@param failure The block to be executed on the completion of an unsuccessful request. This block has no return value and takes two arguments: the receiver operation and the error that occurred during the request.
*/
- (void)setCompletionBlockWithSuccess:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,207 @@
// AFHTTPRequestOperation.m
//
// Copyright (c) 2013-2015 AFNetworking (http://afnetworking.com)
//
// 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.
#import "AFHTTPRequestOperation.h"
static dispatch_queue_t http_request_operation_processing_queue() {
static dispatch_queue_t af_http_request_operation_processing_queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_http_request_operation_processing_queue = dispatch_queue_create("com.alamofire.networking.http-request.processing", DISPATCH_QUEUE_CONCURRENT);
});
return af_http_request_operation_processing_queue;
}
static dispatch_group_t http_request_operation_completion_group() {
static dispatch_group_t af_http_request_operation_completion_group;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_http_request_operation_completion_group = dispatch_group_create();
});
return af_http_request_operation_completion_group;
}
#pragma mark -
@interface AFURLConnectionOperation ()
@property (readwrite, nonatomic, strong) NSURLRequest *request;
@property (readwrite, nonatomic, strong) NSURLResponse *response;
@end
@interface AFHTTPRequestOperation ()
@property (readwrite, nonatomic, strong) NSHTTPURLResponse *response;
@property (readwrite, nonatomic, strong) id responseObject;
@property (readwrite, nonatomic, strong) NSError *responseSerializationError;
@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
@end
@implementation AFHTTPRequestOperation
@dynamic response;
@dynamic lock;
- (instancetype)initWithRequest:(NSURLRequest *)urlRequest {
self = [super initWithRequest:urlRequest];
if (!self) {
return nil;
}
self.responseSerializer = [AFHTTPResponseSerializer serializer];
return self;
}
- (void)setResponseSerializer:(AFHTTPResponseSerializer <AFURLResponseSerialization> *)responseSerializer {
NSParameterAssert(responseSerializer);
[self.lock lock];
_responseSerializer = responseSerializer;
self.responseObject = nil;
self.responseSerializationError = nil;
[self.lock unlock];
}
- (id)responseObject {
[self.lock lock];
if (!_responseObject && [self isFinished] && !self.error) {
NSError *error = nil;
self.responseObject = [self.responseSerializer responseObjectForResponse:self.response data:self.responseData error:&error];
if (error) {
self.responseSerializationError = error;
}
}
[self.lock unlock];
return _responseObject;
}
- (NSError *)error {
if (_responseSerializationError) {
return _responseSerializationError;
} else {
return [super error];
}
}
#pragma mark - AFHTTPRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
// completionBlock is manually nilled out in AFURLConnectionOperation to break the retain cycle.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
#pragma clang diagnostic ignored "-Wgnu"
self.completionBlock = ^{
if (self.completionGroup) {
dispatch_group_enter(self.completionGroup);
}
dispatch_async(http_request_operation_processing_queue(), ^{
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
id responseObject = self.responseObject;
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
if (success) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
success(self, responseObject);
});
}
}
}
if (self.completionGroup) {
dispatch_group_leave(self.completionGroup);
}
});
};
#pragma clang diagnostic pop
}
#pragma mark - AFURLRequestOperation
- (void)pause {
[super pause];
u_int64_t offset = 0;
if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
offset = [(NSNumber *)[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
} else {
offset = [(NSData *)[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
}
NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
[mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
}
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFHTTPRequestOperation *operation = [super copyWithZone:zone];
operation.responseSerializer = [self.responseSerializer copyWithZone:zone];
operation.completionQueue = self.completionQueue;
operation.completionGroup = self.completionGroup;
return operation;
}
@end
@@ -0,0 +1,326 @@
// AFHTTPRequestOperationManager.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <Availability.h>
#if __IPHONE_OS_VERSION_MIN_REQUIRED
#import <MobileCoreServices/MobileCoreServices.h>
#else
#import <CoreServices/CoreServices.h>
#endif
#import "AFHTTPRequestOperation.h"
#import "AFURLResponseSerialization.h"
#import "AFURLRequestSerialization.h"
#import "AFSecurityPolicy.h"
#import "AFNetworkReachabilityManager.h"
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
NS_ASSUME_NONNULL_BEGIN
/**
`AFHTTPRequestOperationManager` encapsulates the common patterns of communicating with a web application over HTTP, including request creation, response serialization, network reachability monitoring, and security, as well as request operation management.
## Subclassing Notes
Developers targeting iOS 7 or Mac OS X 10.9 or later that deal extensively with a web service are encouraged to subclass `AFHTTPSessionManager`, providing a class method that returns a shared singleton object on which authentication and other configuration can be shared across the application.
For developers targeting iOS 6 or Mac OS X 10.8 or earlier, `AFHTTPRequestOperationManager` may be used to similar effect.
## Methods to Override
To change the behavior of all request operation construction for an `AFHTTPRequestOperationManager` subclass, override `HTTPRequestOperationWithRequest:success:failure`.
## Serialization
Requests created by an HTTP client will contain default headers and encode parameters according to the `requestSerializer` property, which is an object conforming to `<AFURLRequestSerialization>`.
Responses received from the server are automatically validated and serialized by the `responseSerializers` property, which is an object conforming to `<AFURLResponseSerialization>`
## URL Construction Using Relative Paths
For HTTP convenience methods, the request serializer constructs URLs from the path relative to the `-baseURL`, using `NSURL +URLWithString:relativeToURL:`, when provided. If `baseURL` is `nil`, `path` needs to resolve to a valid `NSURL` object using `NSURL +URLWithString:`.
Below are a few examples of how `baseURL` and relative paths interact:
NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"];
[NSURL URLWithString:@"foo" relativeToURL:baseURL]; // http://example.com/v1/foo
[NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL]; // http://example.com/v1/foo?bar=baz
[NSURL URLWithString:@"/foo" relativeToURL:baseURL]; // http://example.com/foo
[NSURL URLWithString:@"foo/" relativeToURL:baseURL]; // http://example.com/v1/foo
[NSURL URLWithString:@"/foo/" relativeToURL:baseURL]; // http://example.com/foo/
[NSURL URLWithString:@"http://example2.com/" relativeToURL:baseURL]; // http://example2.com/
Also important to note is that a trailing slash will be added to any `baseURL` without one. This would otherwise cause unexpected behavior when constructing URLs using paths without a leading slash.
## Network Reachability Monitoring
Network reachability status and change monitoring is available through the `reachabilityManager` property. Applications may choose to monitor network reachability conditions in order to prevent or suspend any outbound requests. See `AFNetworkReachabilityManager` for more details.
## NSSecureCoding & NSCopying Caveats
`AFHTTPRequestOperationManager` conforms to the `NSSecureCoding` and `NSCopying` protocols, allowing operations to be archived to disk, and copied in memory, respectively. There are a few minor caveats to keep in mind, however:
- Archives and copies of HTTP clients will be initialized with an empty operation queue.
- NSSecureCoding cannot serialize / deserialize block properties, so an archive of an HTTP client will not include any reachability callback block that may be set.
*/
@interface AFHTTPRequestOperationManager : NSObject <NSSecureCoding, NSCopying>
/**
The URL used to monitor reachability, and construct requests from relative paths in methods like `requestWithMethod:URLString:parameters:`, and the `GET` / `POST` / et al. convenience methods.
*/
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
/**
Requests created with `requestWithMethod:URLString:parameters:` & `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:` are constructed with a set of default headers using a parameter serialization specified by this property. By default, this is set to an instance of `AFHTTPRequestSerializer`, which serializes query string parameters for `GET`, `HEAD`, and `DELETE` requests, or otherwise URL-form-encodes HTTP message bodies.
@warning `requestSerializer` must not be `nil`.
*/
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;
/**
Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to a JSON serializer, which serializes data from responses with a `application/json` MIME type, and falls back to the raw data object. The serializer validates the status code to be in the `2XX` range, denoting success. If the response serializer generates an error in `-responseObjectForResponse:data:error:`, the `failure` callback of the session task or request operation will be executed; otherwise, the `success` callback will be executed.
@warning `responseSerializer` must not be `nil`.
*/
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> * responseSerializer;
/**
The operation queue on which request operations are scheduled and run.
*/
@property (nonatomic, strong) NSOperationQueue *operationQueue;
///-------------------------------
/// @name Managing URL Credentials
///-------------------------------
/**
Whether request operations should consult the credential storage for authenticating the connection. `YES` by default.
@see AFURLConnectionOperation -shouldUseCredentialStorage
*/
@property (nonatomic, assign) BOOL shouldUseCredentialStorage;
/**
The credential used by request operations for authentication challenges.
@see AFURLConnectionOperation -credential
*/
@property (nonatomic, strong, nullable) NSURLCredential *credential;
///-------------------------------
/// @name Managing Security Policy
///-------------------------------
/**
The security policy used by created request operations to evaluate server trust for secure connections. `AFHTTPRequestOperationManager` uses the `defaultPolicy` unless otherwise specified.
*/
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
///------------------------------------
/// @name Managing Network Reachability
///------------------------------------
/**
The network reachability manager. `AFHTTPRequestOperationManager` uses the `sharedManager` by default.
*/
@property (readwrite, nonatomic, strong) AFNetworkReachabilityManager *reachabilityManager;
///-------------------------------
/// @name Managing Callback Queues
///-------------------------------
/**
The dispatch queue for the `completionBlock` of request operations. If `NULL` (default), the main queue is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;
#else
@property (nonatomic, assign, nullable) dispatch_queue_t completionQueue;
#endif
/**
The dispatch group for the `completionBlock` of request operations. If `NULL` (default), a private dispatch group is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_group_t completionGroup;
#else
@property (nonatomic, assign, nullable) dispatch_group_t completionGroup;
#endif
///---------------------------------------------
/// @name Creating and Initializing HTTP Clients
///---------------------------------------------
/**
Creates and returns an `AFHTTPRequestOperationManager` object.
*/
+ (instancetype)manager;
/**
Initializes an `AFHTTPRequestOperationManager` object with the specified base URL.
This is the designated initializer.
@param url The base URL for the HTTP client.
@return The newly-initialized HTTP client
*/
- (instancetype)initWithBaseURL:(nullable NSURL *)url NS_DESIGNATED_INITIALIZER;
///---------------------------------------
/// @name Managing HTTP Request Operations
///---------------------------------------
/**
Creates an `AFHTTPRequestOperation`, and sets the response serializers to that of the HTTP client.
@param request The request object to be loaded asynchronously during execution of the operation.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created request operation and the object created from the response data of request.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred.
*/
- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
///---------------------------
/// @name Making HTTP Requests
///---------------------------
/**
Creates and runs an `AFHTTPRequestOperation` with a `GET` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)GET:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a `HEAD` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes a single arguments: the request operation.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)HEAD:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a `POST` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a multipart `POST` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a `PUT` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)PUT:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a `PATCH` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)PATCH:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
/**
Creates and runs an `AFHTTPRequestOperation` with a `DELETE` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the request operation, and the response object created by the client response serializer.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the request operation and the error describing the network or parsing error that occurred.
@see -HTTPRequestOperationWithRequest:success:failure:
*/
- (nullable AFHTTPRequestOperation *)DELETE:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(nullable void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,284 @@
// AFHTTPRequestOperationManager.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import "AFHTTPRequestOperationManager.h"
#import "AFHTTPRequestOperation.h"
#import <Availability.h>
#import <Security/Security.h>
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <UIKit/UIKit.h>
#endif
@interface AFHTTPRequestOperationManager ()
@property (readwrite, nonatomic, strong) NSURL *baseURL;
@end
@implementation AFHTTPRequestOperationManager
+ (instancetype)manager {
return [[self alloc] initWithBaseURL:nil];
}
- (instancetype)init {
return [self initWithBaseURL:nil];
}
- (instancetype)initWithBaseURL:(NSURL *)url {
self = [super init];
if (!self) {
return nil;
}
// Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
self.baseURL = url;
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.securityPolicy = [AFSecurityPolicy defaultPolicy];
self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
self.operationQueue = [[NSOperationQueue alloc] init];
self.shouldUseCredentialStorage = YES;
return self;
}
#pragma mark -
#ifdef _SYSTEMCONFIGURATION_H
#endif
- (void)setRequestSerializer:(AFHTTPRequestSerializer <AFURLRequestSerialization> *)requestSerializer {
NSParameterAssert(requestSerializer);
_requestSerializer = requestSerializer;
}
- (void)setResponseSerializer:(AFHTTPResponseSerializer <AFURLResponseSerialization> *)responseSerializer {
NSParameterAssert(responseSerializer);
_responseSerializer = responseSerializer;
}
#pragma mark -
- (AFHTTPRequestOperation *)HTTPRequestOperationWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
return [self HTTPRequestOperationWithRequest:request success:success failure:failure];
}
- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = self.responseSerializer;
operation.shouldUseCredentialStorage = self.shouldUseCredentialStorage;
operation.credential = self.credential;
operation.securityPolicy = self.securityPolicy;
[operation setCompletionBlockWithSuccess:success failure:failure];
operation.completionQueue = self.completionQueue;
operation.completionGroup = self.completionGroup;
return operation;
}
#pragma mark -
- (AFHTTPRequestOperation *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"GET" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)HEAD:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"HEAD" URLString:URLString parameters:parameters success:^(AFHTTPRequestOperation *requestOperation, __unused id responseObject) {
if (success) {
success(requestOperation);
}
} failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:request success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)PUT:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"PUT" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)PATCH:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"PATCH" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
- (AFHTTPRequestOperation *)DELETE:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"DELETE" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
#pragma mark - NSObject
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, baseURL: %@, operationQueue: %@>", NSStringFromClass([self class]), self, [self.baseURL absoluteString], self.operationQueue];
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (id)initWithCoder:(NSCoder *)decoder {
NSURL *baseURL = [decoder decodeObjectForKey:NSStringFromSelector(@selector(baseURL))];
self = [self initWithBaseURL:baseURL];
if (!self) {
return nil;
}
self.requestSerializer = [decoder decodeObjectOfClass:[AFHTTPRequestSerializer class] forKey:NSStringFromSelector(@selector(requestSerializer))];
self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.baseURL forKey:NSStringFromSelector(@selector(baseURL))];
[coder encodeObject:self.requestSerializer forKey:NSStringFromSelector(@selector(requestSerializer))];
[coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFHTTPRequestOperationManager *HTTPClient = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL];
HTTPClient.requestSerializer = [self.requestSerializer copyWithZone:zone];
HTTPClient.responseSerializer = [self.responseSerializer copyWithZone:zone];
return HTTPClient;
}
@end
@@ -0,0 +1,251 @@
// AFHTTPSessionManager.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <Availability.h>
#if __IPHONE_OS_VERSION_MIN_REQUIRED
#import <MobileCoreServices/MobileCoreServices.h>
#else
#import <CoreServices/CoreServices.h>
#endif
#import "AFURLSessionManager.h"
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
/**
`AFHTTPSessionManager` is a subclass of `AFURLSessionManager` with convenience methods for making HTTP requests. When a `baseURL` is provided, requests made with the `GET` / `POST` / et al. convenience methods can be made with relative paths.
## Subclassing Notes
Developers targeting iOS 7 or Mac OS X 10.9 or later that deal extensively with a web service are encouraged to subclass `AFHTTPSessionManager`, providing a class method that returns a shared singleton object on which authentication and other configuration can be shared across the application.
For developers targeting iOS 6 or Mac OS X 10.8 or earlier, `AFHTTPRequestOperationManager` may be used to similar effect.
## Methods to Override
To change the behavior of all data task operation construction, which is also used in the `GET` / `POST` / et al. convenience methods, override `dataTaskWithRequest:completionHandler:`.
## Serialization
Requests created by an HTTP client will contain default headers and encode parameters according to the `requestSerializer` property, which is an object conforming to `<AFURLRequestSerialization>`.
Responses received from the server are automatically validated and serialized by the `responseSerializers` property, which is an object conforming to `<AFURLResponseSerialization>`
## URL Construction Using Relative Paths
For HTTP convenience methods, the request serializer constructs URLs from the path relative to the `-baseURL`, using `NSURL +URLWithString:relativeToURL:`, when provided. If `baseURL` is `nil`, `path` needs to resolve to a valid `NSURL` object using `NSURL +URLWithString:`.
Below are a few examples of how `baseURL` and relative paths interact:
NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"];
[NSURL URLWithString:@"foo" relativeToURL:baseURL]; // http://example.com/v1/foo
[NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL]; // http://example.com/v1/foo?bar=baz
[NSURL URLWithString:@"/foo" relativeToURL:baseURL]; // http://example.com/foo
[NSURL URLWithString:@"foo/" relativeToURL:baseURL]; // http://example.com/v1/foo
[NSURL URLWithString:@"/foo/" relativeToURL:baseURL]; // http://example.com/foo/
[NSURL URLWithString:@"http://example2.com/" relativeToURL:baseURL]; // http://example2.com/
Also important to note is that a trailing slash will be added to any `baseURL` without one. This would otherwise cause unexpected behavior when constructing URLs using paths without a leading slash.
@warning Managers for background sessions must be owned for the duration of their use. This can be accomplished by creating an application-wide or shared singleton instance.
*/
#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090)
NS_ASSUME_NONNULL_BEGIN
@interface AFHTTPSessionManager : AFURLSessionManager <NSSecureCoding, NSCopying>
/**
The URL used to construct requests from relative paths in methods like `requestWithMethod:URLString:parameters:`, and the `GET` / `POST` / et al. convenience methods.
*/
@property (readonly, nonatomic, strong, nullable) NSURL *baseURL;
/**
Requests created with `requestWithMethod:URLString:parameters:` & `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:` are constructed with a set of default headers using a parameter serialization specified by this property. By default, this is set to an instance of `AFHTTPRequestSerializer`, which serializes query string parameters for `GET`, `HEAD`, and `DELETE` requests, or otherwise URL-form-encodes HTTP message bodies.
@warning `requestSerializer` must not be `nil`.
*/
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;
/**
Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to an instance of `AFJSONResponseSerializer`.
@warning `responseSerializer` must not be `nil`.
*/
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> * responseSerializer;
///---------------------
/// @name Initialization
///---------------------
/**
Creates and returns an `AFHTTPSessionManager` object.
*/
+ (instancetype)manager;
/**
Initializes an `AFHTTPSessionManager` object with the specified base URL.
@param url The base URL for the HTTP client.
@return The newly-initialized HTTP client
*/
- (instancetype)initWithBaseURL:(nullable NSURL *)url;
/**
Initializes an `AFHTTPSessionManager` object with the specified base URL.
This is the designated initializer.
@param url The base URL for the HTTP client.
@param configuration The configuration used to create the managed session.
@return The newly-initialized HTTP client
*/
- (instancetype)initWithBaseURL:(nullable NSURL *)url
sessionConfiguration:(nullable NSURLSessionConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
///---------------------------
/// @name Making HTTP Requests
///---------------------------
/**
Creates and runs an `NSURLSessionDataTask` with a `GET` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a `HEAD` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes a single arguments: the data task.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)HEAD:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a `POST` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a multipart `POST` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a `PUT` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)PUT:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a `PATCH` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)PATCH:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a `DELETE` request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:completionHandler:
*/
- (nullable NSURLSessionDataTask *)DELETE:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
@end
NS_ASSUME_NONNULL_END
#endif
@@ -0,0 +1,321 @@
// AFHTTPSessionManager.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import "AFHTTPSessionManager.h"
#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090)
#import "AFURLRequestSerialization.h"
#import "AFURLResponseSerialization.h"
#import <Availability.h>
#import <Security/Security.h>
#ifdef _SYSTEMCONFIGURATION_H
#import <netinet/in.h>
#import <netinet6/in6.h>
#import <arpa/inet.h>
#import <ifaddrs.h>
#import <netdb.h>
#endif
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <UIKit/UIKit.h>
#endif
@interface AFHTTPSessionManager ()
@property (readwrite, nonatomic, strong) NSURL *baseURL;
@end
@implementation AFHTTPSessionManager
@dynamic responseSerializer;
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}
- (instancetype)init {
return [self initWithBaseURL:nil];
}
- (instancetype)initWithBaseURL:(NSURL *)url {
return [self initWithBaseURL:url sessionConfiguration:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
return [self initWithBaseURL:nil sessionConfiguration:configuration];
}
- (instancetype)initWithBaseURL:(NSURL *)url
sessionConfiguration:(NSURLSessionConfiguration *)configuration
{
self = [super initWithSessionConfiguration:configuration];
if (!self) {
return nil;
}
// Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected
if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
self.baseURL = url;
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
return self;
}
#pragma mark -
#ifdef _SYSTEMCONFIGURATION_H
#endif
- (void)setRequestSerializer:(AFHTTPRequestSerializer <AFURLRequestSerialization> *)requestSerializer {
NSParameterAssert(requestSerializer);
_requestSerializer = requestSerializer;
}
- (void)setResponseSerializer:(AFHTTPResponseSerializer <AFURLResponseSerialization> *)responseSerializer {
NSParameterAssert(responseSerializer);
[super setResponseSerializer:responseSerializer];
}
#pragma mark -
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET" URLString:URLString parameters:parameters success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)HEAD:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"HEAD" URLString:URLString parameters:parameters success:^(NSURLSessionDataTask *task, __unused id responseObject) {
if (success) {
success(task);
}
} failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:nil completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
return task;
}
- (NSURLSessionDataTask *)PUT:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"PUT" URLString:URLString parameters:parameters success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)PATCH:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"PATCH" URLString:URLString parameters:parameters success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)DELETE:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"DELETE" URLString:URLString parameters:parameters success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [self dataTaskWithRequest:request completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(dataTask, error);
}
} else {
if (success) {
success(dataTask, responseObject);
}
}
}];
return dataTask;
}
#pragma mark - NSObject
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, baseURL: %@, session: %@, operationQueue: %@>", NSStringFromClass([self class]), self, [self.baseURL absoluteString], self.session, self.operationQueue];
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (id)initWithCoder:(NSCoder *)decoder {
NSURL *baseURL = [decoder decodeObjectOfClass:[NSURL class] forKey:NSStringFromSelector(@selector(baseURL))];
NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"];
if (!configuration) {
NSString *configurationIdentifier = [decoder decodeObjectOfClass:[NSString class] forKey:@"identifier"];
if (configurationIdentifier) {
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1100)
configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:configurationIdentifier];
#else
configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:configurationIdentifier];
#endif
}
}
self = [self initWithBaseURL:baseURL sessionConfiguration:configuration];
if (!self) {
return nil;
}
self.requestSerializer = [decoder decodeObjectOfClass:[AFHTTPRequestSerializer class] forKey:NSStringFromSelector(@selector(requestSerializer))];
self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:self.baseURL forKey:NSStringFromSelector(@selector(baseURL))];
if ([self.session.configuration conformsToProtocol:@protocol(NSCoding)]) {
[coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"];
} else {
[coder encodeObject:self.session.configuration.identifier forKey:@"identifier"];
}
[coder encodeObject:self.requestSerializer forKey:NSStringFromSelector(@selector(requestSerializer))];
[coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFHTTPSessionManager *HTTPClient = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL sessionConfiguration:self.session.configuration];
HTTPClient.requestSerializer = [self.requestSerializer copyWithZone:zone];
HTTPClient.responseSerializer = [self.responseSerializer copyWithZone:zone];
return HTTPClient;
}
@end
#endif
@@ -0,0 +1,204 @@
// AFNetworkReachabilityManager.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) {
AFNetworkReachabilityStatusUnknown = -1,
AFNetworkReachabilityStatusNotReachable = 0,
AFNetworkReachabilityStatusReachableViaWWAN = 1,
AFNetworkReachabilityStatusReachableViaWiFi = 2,
};
NS_ASSUME_NONNULL_BEGIN
/**
`AFNetworkReachabilityManager` monitors the reachability of domains, and addresses for both WWAN and WiFi network interfaces.
Reachability can be used to determine background information about why a network operation failed, or to trigger a network operation retrying when a connection is established. It should not be used to prevent a user from initiating a network request, as it's possible that an initial request may be required to establish reachability.
See Apple's Reachability Sample Code (https://developer.apple.com/library/ios/samplecode/reachability/)
@warning Instances of `AFNetworkReachabilityManager` must be started with `-startMonitoring` before reachability status can be determined.
*/
@interface AFNetworkReachabilityManager : NSObject
/**
The current network reachability status.
*/
@property (readonly, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus;
/**
Whether or not the network is currently reachable.
*/
@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable;
/**
Whether or not the network is currently reachable via WWAN.
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWWAN) BOOL reachableViaWWAN;
/**
Whether or not the network is currently reachable via WiFi.
*/
@property (readonly, nonatomic, assign, getter = isReachableViaWiFi) BOOL reachableViaWiFi;
///---------------------
/// @name Initialization
///---------------------
/**
Returns the shared network reachability manager.
*/
+ (instancetype)sharedManager;
/**
Creates and returns a network reachability manager for the specified domain.
@param domain The domain used to evaluate network reachability.
@return An initialized network reachability manager, actively monitoring the specified domain.
*/
+ (instancetype)managerForDomain:(NSString *)domain;
/**
Creates and returns a network reachability manager for the socket address.
@param address The socket address (`sockaddr_in`) used to evaluate network reachability.
@return An initialized network reachability manager, actively monitoring the specified socket address.
*/
+ (instancetype)managerForAddress:(const void *)address;
/**
Initializes an instance of a network reachability manager from the specified reachability object.
@param reachability The reachability object to monitor.
@return An initialized network reachability manager, actively monitoring the specified reachability.
*/
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability NS_DESIGNATED_INITIALIZER;
///--------------------------------------------------
/// @name Starting & Stopping Reachability Monitoring
///--------------------------------------------------
/**
Starts monitoring for changes in network reachability status.
*/
- (void)startMonitoring;
/**
Stops monitoring for changes in network reachability status.
*/
- (void)stopMonitoring;
///-------------------------------------------------
/// @name Getting Localized Reachability Description
///-------------------------------------------------
/**
Returns a localized string representation of the current network reachability status.
*/
- (NSString *)localizedNetworkReachabilityStatusString;
///---------------------------------------------------
/// @name Setting Network Reachability Change Callback
///---------------------------------------------------
/**
Sets a callback to be executed when the network availability of the `baseURL` host changes.
@param block A block object to be executed when the network availability of the `baseURL` host changes.. This block has no return value and takes a single argument which represents the various reachability states from the device to the `baseURL`.
*/
- (void)setReachabilityStatusChangeBlock:(nullable void (^)(AFNetworkReachabilityStatus status))block;
@end
///----------------
/// @name Constants
///----------------
/**
## Network Reachability
The following constants are provided by `AFNetworkReachabilityManager` as possible network reachability statuses.
enum {
AFNetworkReachabilityStatusUnknown,
AFNetworkReachabilityStatusNotReachable,
AFNetworkReachabilityStatusReachableViaWWAN,
AFNetworkReachabilityStatusReachableViaWiFi,
}
`AFNetworkReachabilityStatusUnknown`
The `baseURL` host reachability is not known.
`AFNetworkReachabilityStatusNotReachable`
The `baseURL` host cannot be reached.
`AFNetworkReachabilityStatusReachableViaWWAN`
The `baseURL` host can be reached via a cellular connection, such as EDGE or GPRS.
`AFNetworkReachabilityStatusReachableViaWiFi`
The `baseURL` host can be reached via a Wi-Fi connection.
### Keys for Notification UserInfo Dictionary
Strings that are used as keys in a `userInfo` dictionary in a network reachability status change notification.
`AFNetworkingReachabilityNotificationStatusItem`
A key in the userInfo dictionary in a `AFNetworkingReachabilityDidChangeNotification` notification.
The corresponding value is an `NSNumber` object representing the `AFNetworkReachabilityStatus` value for the current reachability status.
*/
///--------------------
/// @name Notifications
///--------------------
/**
Posted when network reachability changes.
This notification assigns no notification object. The `userInfo` dictionary contains an `NSNumber` object under the `AFNetworkingReachabilityNotificationStatusItem` key, representing the `AFNetworkReachabilityStatus` value for the current network reachability.
@warning In order for network reachability to be monitored, include the `SystemConfiguration` framework in the active target's "Link Binary With Library" build phase, and add `#import <SystemConfiguration/SystemConfiguration.h>` to the header prefix of the project (`Prefix.pch`).
*/
extern NSString * const AFNetworkingReachabilityDidChangeNotification;
extern NSString * const AFNetworkingReachabilityNotificationStatusItem;
///--------------------
/// @name Functions
///--------------------
/**
Returns a localized string representation of an `AFNetworkReachabilityStatus` value.
*/
extern NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status);
NS_ASSUME_NONNULL_END
@@ -0,0 +1,260 @@
// AFNetworkReachabilityManager.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import "AFNetworkReachabilityManager.h"
#import <netinet/in.h>
#import <netinet6/in6.h>
#import <arpa/inet.h>
#import <ifaddrs.h>
#import <netdb.h>
NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change";
NSString * const AFNetworkingReachabilityNotificationStatusItem = @"AFNetworkingReachabilityNotificationStatusItem";
typedef void (^AFNetworkReachabilityStatusBlock)(AFNetworkReachabilityStatus status);
typedef NS_ENUM(NSUInteger, AFNetworkReachabilityAssociation) {
AFNetworkReachabilityForAddress = 1,
AFNetworkReachabilityForAddressPair = 2,
AFNetworkReachabilityForName = 3,
};
NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusNotReachable:
return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil);
case AFNetworkReachabilityStatusReachableViaWWAN:
return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil);
case AFNetworkReachabilityStatusReachableViaWiFi:
return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil);
case AFNetworkReachabilityStatusUnknown:
default:
return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil);
}
}
static AFNetworkReachabilityStatus AFNetworkReachabilityStatusForFlags(SCNetworkReachabilityFlags flags) {
BOOL isReachable = ((flags & kSCNetworkReachabilityFlagsReachable) != 0);
BOOL needsConnection = ((flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0);
BOOL canConnectionAutomatically = (((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || ((flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0));
BOOL canConnectWithoutUserInteraction = (canConnectionAutomatically && (flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0);
BOOL isNetworkReachable = (isReachable && (!needsConnection || canConnectWithoutUserInteraction));
AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusUnknown;
if (isNetworkReachable == NO) {
status = AFNetworkReachabilityStatusNotReachable;
}
#if TARGET_OS_IPHONE
else if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) {
status = AFNetworkReachabilityStatusReachableViaWWAN;
}
#endif
else {
status = AFNetworkReachabilityStatusReachableViaWiFi;
}
return status;
}
static void AFNetworkReachabilityCallback(SCNetworkReachabilityRef __unused target, SCNetworkReachabilityFlags flags, void *info) {
AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusForFlags(flags);
AFNetworkReachabilityStatusBlock block = (__bridge AFNetworkReachabilityStatusBlock)info;
if (block) {
block(status);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSDictionary *userInfo = @{ AFNetworkingReachabilityNotificationStatusItem: @(status) };
[notificationCenter postNotificationName:AFNetworkingReachabilityDidChangeNotification object:nil userInfo:userInfo];
});
}
static const void * AFNetworkReachabilityRetainCallback(const void *info) {
return Block_copy(info);
}
static void AFNetworkReachabilityReleaseCallback(const void *info) {
if (info) {
Block_release(info);
}
}
@interface AFNetworkReachabilityManager ()
@property (readwrite, nonatomic, strong) id networkReachability;
@property (readwrite, nonatomic, assign) AFNetworkReachabilityAssociation networkReachabilityAssociation;
@property (readwrite, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus;
@property (readwrite, nonatomic, copy) AFNetworkReachabilityStatusBlock networkReachabilityStatusBlock;
@end
@implementation AFNetworkReachabilityManager
+ (instancetype)sharedManager {
static AFNetworkReachabilityManager *_sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_len = sizeof(address);
address.sin_family = AF_INET;
_sharedManager = [self managerForAddress:&address];
});
return _sharedManager;
}
+ (instancetype)managerForDomain:(NSString *)domain {
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [domain UTF8String]);
AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];
manager.networkReachabilityAssociation = AFNetworkReachabilityForName;
return manager;
}
+ (instancetype)managerForAddress:(const void *)address {
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)address);
AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];
manager.networkReachabilityAssociation = AFNetworkReachabilityForAddress;
return manager;
}
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability {
self = [super init];
if (!self) {
return nil;
}
self.networkReachability = CFBridgingRelease(reachability);
self.networkReachabilityStatus = AFNetworkReachabilityStatusUnknown;
return self;
}
- (instancetype)init NS_UNAVAILABLE
{
return nil;
}
- (void)dealloc {
[self stopMonitoring];
}
#pragma mark -
- (BOOL)isReachable {
return [self isReachableViaWWAN] || [self isReachableViaWiFi];
}
- (BOOL)isReachableViaWWAN {
return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN;
}
- (BOOL)isReachableViaWiFi {
return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi;
}
#pragma mark -
- (void)startMonitoring {
[self stopMonitoring];
if (!self.networkReachability) {
return;
}
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
id networkReachability = self.networkReachability;
SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL};
SCNetworkReachabilitySetCallback((__bridge SCNetworkReachabilityRef)networkReachability, AFNetworkReachabilityCallback, &context);
SCNetworkReachabilityScheduleWithRunLoop((__bridge SCNetworkReachabilityRef)networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
switch (self.networkReachabilityAssociation) {
case AFNetworkReachabilityForName:
break;
case AFNetworkReachabilityForAddress:
case AFNetworkReachabilityForAddressPair:
default: {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{
SCNetworkReachabilityFlags flags;
SCNetworkReachabilityGetFlags((__bridge SCNetworkReachabilityRef)networkReachability, &flags);
AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusForFlags(flags);
dispatch_async(dispatch_get_main_queue(), ^{
callback(status);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:AFNetworkingReachabilityDidChangeNotification object:nil userInfo:@{ AFNetworkingReachabilityNotificationStatusItem: @(status) }];
});
});
}
break;
}
}
- (void)stopMonitoring {
if (!self.networkReachability) {
return;
}
SCNetworkReachabilityUnscheduleFromRunLoop((__bridge SCNetworkReachabilityRef)self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
}
#pragma mark -
- (NSString *)localizedNetworkReachabilityStatusString {
return AFStringFromNetworkReachabilityStatus(self.networkReachabilityStatus);
}
#pragma mark -
- (void)setReachabilityStatusChangeBlock:(void (^)(AFNetworkReachabilityStatus status))block {
self.networkReachabilityStatusBlock = block;
}
#pragma mark - NSKeyValueObserving
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"reachable"] || [key isEqualToString:@"reachableViaWWAN"] || [key isEqualToString:@"reachableViaWiFi"]) {
return [NSSet setWithObject:@"networkReachabilityStatus"];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
@end
@@ -0,0 +1,44 @@
// AFNetworking.h
//
// Copyright (c) 2013 AFNetworking (http://afnetworking.com/)
//
// 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.
#import <Foundation/Foundation.h>
#import <Availability.h>
#ifndef _AFNETWORKING_
#define _AFNETWORKING_
#import "AFURLRequestSerialization.h"
#import "AFURLResponseSerialization.h"
#import "AFSecurityPolicy.h"
#import "AFNetworkReachabilityManager.h"
#import "AFURLConnectionOperation.h"
#import "AFHTTPRequestOperation.h"
#import "AFHTTPRequestOperationManager.h"
#if ( ( defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) || \
( defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 ) )
#import "AFURLSessionManager.h"
#import "AFHTTPSessionManager.h"
#endif
#endif /* _AFNETWORKING_ */
@@ -0,0 +1,142 @@
// AFSecurityPolicy.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <Security/Security.h>
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey,
AFSSLPinningModeCertificate,
};
/**
`AFSecurityPolicy` evaluates server trust against pinned X.509 certificates and public keys over secure connections.
Adding pinned SSL certificates to your app helps prevent man-in-the-middle attacks and other vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged to route all communication over an HTTPS connection with SSL pinning configured and enabled.
*/
NS_ASSUME_NONNULL_BEGIN
@interface AFSecurityPolicy : NSObject
/**
The criteria by which server trust should be evaluated against the pinned SSL certificates. Defaults to `AFSSLPinningModeNone`.
*/
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
/**
The certificates used to evaluate server trust according to the SSL pinning mode. By default, this property is set to any (`.cer`) certificates included in the app bundle. Note that if you create an array with duplicate certificates, the duplicate certificates will be removed. Note that if pinning is enabled, `evaluateServerTrust:forDomain:` will return true if any pinned certificate matches.
*/
@property (nonatomic, strong, nullable) NSArray *pinnedCertificates;
/**
Whether or not to trust servers with an invalid or expired SSL certificates. Defaults to `NO`.
*/
@property (nonatomic, assign) BOOL allowInvalidCertificates;
/**
Whether or not to validate the domain name in the certificate's CN field. Defaults to `YES`.
*/
@property (nonatomic, assign) BOOL validatesDomainName;
///-----------------------------------------
/// @name Getting Specific Security Policies
///-----------------------------------------
/**
Returns the shared default security policy, which does not allow invalid certificates, validates domain name, and does not validate against pinned certificates or public keys.
@return The default security policy.
*/
+ (instancetype)defaultPolicy;
///---------------------
/// @name Initialization
///---------------------
/**
Creates and returns a security policy with the specified pinning mode.
@param pinningMode The SSL pinning mode.
@return A new security policy.
*/
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode;
///------------------------------
/// @name Evaluating Server Trust
///------------------------------
/**
Whether or not the specified server trust should be accepted, based on the security policy.
This method should be used when responding to an authentication challenge from a server.
@param serverTrust The X.509 certificate trust of the server.
@return Whether or not to trust the server.
@warning This method has been deprecated in favor of `-evaluateServerTrust:forDomain:`.
*/
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust DEPRECATED_ATTRIBUTE;
/**
Whether or not the specified server trust should be accepted, based on the security policy.
This method should be used when responding to an authentication challenge from a server.
@param serverTrust The X.509 certificate trust of the server.
@param domain The domain of serverTrust. If `nil`, the domain will not be validated.
@return Whether or not to trust the server.
*/
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(nullable NSString *)domain;
@end
NS_ASSUME_NONNULL_END
///----------------
/// @name Constants
///----------------
/**
## SSL Pinning Modes
The following constants are provided by `AFSSLPinningMode` as possible SSL pinning modes.
enum {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey,
AFSSLPinningModeCertificate,
}
`AFSSLPinningModeNone`
Do not used pinned certificates to validate servers.
`AFSSLPinningModePublicKey`
Validate host certificates against public keys of pinned certificates.
`AFSSLPinningModeCertificate`
Validate host certificates against pinned certificates.
*/
@@ -0,0 +1,311 @@
// AFSecurityPolicy.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import "AFSecurityPolicy.h"
#import <AssertMacros.h>
#if !defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
static NSData * AFSecKeyGetData(SecKeyRef key) {
CFDataRef data = NULL;
__Require_noErr_Quiet(SecItemExport(key, kSecFormatUnknown, kSecItemPemArmour, NULL, &data), _out);
return (__bridge_transfer NSData *)data;
_out:
if (data) {
CFRelease(data);
}
return nil;
}
#endif
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
return [(__bridge id)key1 isEqual:(__bridge id)key2];
#else
return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
#endif
}
static id AFPublicKeyForCertificate(NSData *certificate) {
id allowedPublicKey = nil;
SecCertificateRef allowedCertificate;
SecCertificateRef allowedCertificates[1];
CFArrayRef tempCertificates = nil;
SecPolicyRef policy = nil;
SecTrustRef allowedTrust = nil;
SecTrustResultType result;
allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
__Require_Quiet(allowedCertificate != NULL, _out);
allowedCertificates[0] = allowedCertificate;
tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
policy = SecPolicyCreateBasicX509();
__Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);
__Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
_out:
if (allowedTrust) {
CFRelease(allowedTrust);
}
if (policy) {
CFRelease(policy);
}
if (tempCertificates) {
CFRelease(tempCertificates);
}
if (allowedCertificate) {
CFRelease(allowedCertificate);
}
return allowedPublicKey;
}
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
[trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
}
return [NSArray arrayWithArray:trustChain];
}
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
SecPolicyRef policy = SecPolicyCreateBasicX509();
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
SecTrustRef trust;
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
_out:
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
return [NSArray arrayWithArray:trustChain];
}
#pragma mark -
@interface AFSecurityPolicy()
@property (readwrite, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
@property (readwrite, nonatomic, strong) NSArray *pinnedPublicKeys;
@end
@implementation AFSecurityPolicy
+ (NSArray *)defaultPinnedCertificates {
static NSArray *_defaultPinnedCertificates = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
NSMutableArray *certificates = [NSMutableArray arrayWithCapacity:[paths count]];
for (NSString *path in paths) {
NSData *certificateData = [NSData dataWithContentsOfFile:path];
[certificates addObject:certificateData];
}
_defaultPinnedCertificates = [[NSArray alloc] initWithArray:certificates];
});
return _defaultPinnedCertificates;
}
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
return securityPolicy;
}
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = pinningMode;
[securityPolicy setPinnedCertificates:[self defaultPinnedCertificates]];
return securityPolicy;
}
- (id)init {
self = [super init];
if (!self) {
return nil;
}
self.validatesDomainName = YES;
return self;
}
- (void)setPinnedCertificates:(NSArray *)pinnedCertificates {
_pinnedCertificates = [[NSOrderedSet orderedSetWithArray:pinnedCertificates] array];
if (self.pinnedCertificates) {
NSMutableArray *mutablePinnedPublicKeys = [NSMutableArray arrayWithCapacity:[self.pinnedCertificates count]];
for (NSData *certificate in self.pinnedCertificates) {
id publicKey = AFPublicKeyForCertificate(certificate);
if (!publicKey) {
continue;
}
[mutablePinnedPublicKeys addObject:publicKey];
}
self.pinnedPublicKeys = [NSArray arrayWithArray:mutablePinnedPublicKeys];
} else {
self.pinnedPublicKeys = nil;
}
}
#pragma mark -
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust {
return [self evaluateServerTrust:serverTrust forDomain:nil];
}
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
if (self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust)){
return YES;
} else {
return NO;
}
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
NSUInteger trustedCertificateCount = 0;
for (NSData *trustChainCertificate in serverCertificates) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
trustedCertificateCount++;
}
}
return trustedCertificateCount > 0;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}
return NO;
}
#pragma mark - NSKeyValueObserving
+ (NSSet *)keyPathsForValuesAffectingPinnedPublicKeys {
return [NSSet setWithObject:@"pinnedCertificates"];
}
@end
@@ -0,0 +1,344 @@
// AFURLConnectionOperation.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <Availability.h>
#import "AFURLRequestSerialization.h"
#import "AFURLResponseSerialization.h"
#import "AFSecurityPolicy.h"
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
/**
`AFURLConnectionOperation` is a subclass of `NSOperation` that implements `NSURLConnection` delegate methods.
## Subclassing Notes
This is the base class of all network request operations. You may wish to create your own subclass in order to implement additional `NSURLConnection` delegate methods (see "`NSURLConnection` Delegate Methods" below), or to provide additional properties and/or class constructors.
If you are creating a subclass that communicates over the HTTP or HTTPS protocols, you may want to consider subclassing `AFHTTPRequestOperation` instead, as it supports specifying acceptable content types or status codes.
## NSURLConnection Delegate Methods
`AFURLConnectionOperation` implements the following `NSURLConnection` delegate methods:
- `connection:didReceiveResponse:`
- `connection:didReceiveData:`
- `connectionDidFinishLoading:`
- `connection:didFailWithError:`
- `connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite:`
- `connection:willCacheResponse:`
- `connectionShouldUseCredentialStorage:`
- `connection:needNewBodyStream:`
- `connection:willSendRequestForAuthenticationChallenge:`
If any of these methods are overridden in a subclass, they _must_ call the `super` implementation first.
## Callbacks and Completion Blocks
The built-in `completionBlock` provided by `NSOperation` allows for custom behavior to be executed after the request finishes. It is a common pattern for class constructors in subclasses to take callback block parameters, and execute them conditionally in the body of its `completionBlock`. Make sure to handle cancelled operations appropriately when setting a `completionBlock` (i.e. returning early before parsing response data). See the implementation of any of the `AFHTTPRequestOperation` subclasses for an example of this.
Subclasses are strongly discouraged from overriding `setCompletionBlock:`, as `AFURLConnectionOperation`'s implementation includes a workaround to mitigate retain cycles, and what Apple rather ominously refers to as ["The Deallocation Problem"](http://developer.apple.com/library/ios/#technotes/tn2109/).
## SSL Pinning
Relying on the CA trust model to validate SSL certificates exposes your app to security vulnerabilities, such as man-in-the-middle attacks. For applications that connect to known servers, SSL certificate pinning provides an increased level of security, by checking server certificate validity against those specified in the app bundle.
SSL with certificate pinning is strongly recommended for any application that transmits sensitive information to an external webservice.
Connections will be validated on all matching certificates with a `.cer` extension in the bundle root.
## NSCoding & NSCopying Conformance
`AFURLConnectionOperation` conforms to the `NSCoding` and `NSCopying` protocols, allowing operations to be archived to disk, and copied in memory, respectively. However, because of the intrinsic limitations of capturing the exact state of an operation at a particular moment, there are some important caveats to keep in mind:
### NSCoding Caveats
- Encoded operations do not include any block or stream properties. Be sure to set `completionBlock`, `outputStream`, and any callback blocks as necessary when using `-initWithCoder:` or `NSKeyedUnarchiver`.
- Operations are paused on `encodeWithCoder:`. If the operation was encoded while paused or still executing, its archived state will return `YES` for `isReady`. Otherwise, the state of an operation when encoding will remain unchanged.
### NSCopying Caveats
- `-copy` and `-copyWithZone:` return a new operation with the `NSURLRequest` of the original. So rather than an exact copy of the operation at that particular instant, the copying mechanism returns a completely new instance, which can be useful for retrying operations.
- A copy of an operation will not include the `outputStream` of the original.
- Operation copies do not include `completionBlock`, as it often strongly captures a reference to `self`, which would otherwise have the unintuitive side-effect of pointing to the _original_ operation when copied.
*/
NS_ASSUME_NONNULL_BEGIN
@interface AFURLConnectionOperation : NSOperation <NSURLConnectionDelegate, NSURLConnectionDataDelegate, NSSecureCoding, NSCopying>
///-------------------------------
/// @name Accessing Run Loop Modes
///-------------------------------
/**
The run loop modes in which the operation will run on the network thread. By default, this is a single-member set containing `NSRunLoopCommonModes`.
*/
@property (nonatomic, strong) NSSet *runLoopModes;
///-----------------------------------------
/// @name Getting URL Connection Information
///-----------------------------------------
/**
The request used by the operation's connection.
*/
@property (readonly, nonatomic, strong) NSURLRequest *request;
/**
The last response received by the operation's connection.
*/
@property (readonly, nonatomic, strong, nullable) NSURLResponse *response;
/**
The error, if any, that occurred in the lifecycle of the request.
*/
@property (readonly, nonatomic, strong, nullable) NSError *error;
///----------------------------
/// @name Getting Response Data
///----------------------------
/**
The data received during the request.
*/
@property (readonly, nonatomic, strong, nullable) NSData *responseData;
/**
The string representation of the response data.
*/
@property (readonly, nonatomic, copy, nullable) NSString *responseString;
/**
The string encoding of the response.
If the response does not specify a valid string encoding, `responseStringEncoding` will return `NSUTF8StringEncoding`.
*/
@property (readonly, nonatomic, assign) NSStringEncoding responseStringEncoding;
///-------------------------------
/// @name Managing URL Credentials
///-------------------------------
/**
Whether the URL connection should consult the credential storage for authenticating the connection. `YES` by default.
This is the value that is returned in the `NSURLConnectionDelegate` method `-connectionShouldUseCredentialStorage:`.
*/
@property (nonatomic, assign) BOOL shouldUseCredentialStorage;
/**
The credential used for authentication challenges in `-connection:didReceiveAuthenticationChallenge:`.
This will be overridden by any shared credentials that exist for the username or password of the request URL, if present.
*/
@property (nonatomic, strong, nullable) NSURLCredential *credential;
///-------------------------------
/// @name Managing Security Policy
///-------------------------------
/**
The security policy used to evaluate server trust for secure connections.
*/
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
///------------------------
/// @name Accessing Streams
///------------------------
/**
The input stream used to read data to be sent during the request.
This property acts as a proxy to the `HTTPBodyStream` property of `request`.
*/
@property (nonatomic, strong) NSInputStream *inputStream;
/**
The output stream that is used to write data received until the request is finished.
By default, data is accumulated into a buffer that is stored into `responseData` upon completion of the request, with the intermediary `outputStream` property set to `nil`. When `outputStream` is set, the data will not be accumulated into an internal buffer, and as a result, the `responseData` property of the completed request will be `nil`. The output stream will be scheduled in the network thread runloop upon being set.
*/
@property (nonatomic, strong, nullable) NSOutputStream *outputStream;
///---------------------------------
/// @name Managing Callback Queues
///---------------------------------
/**
The dispatch queue for `completionBlock`. If `NULL` (default), the main queue is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;
#else
@property (nonatomic, assign, nullable) dispatch_queue_t completionQueue;
#endif
/**
The dispatch group for `completionBlock`. If `NULL` (default), a private dispatch group is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_group_t completionGroup;
#else
@property (nonatomic, assign, nullable) dispatch_group_t completionGroup;
#endif
///---------------------------------------------
/// @name Managing Request Operation Information
///---------------------------------------------
/**
The user info dictionary for the receiver.
*/
@property (nonatomic, strong) NSDictionary *userInfo;
// FIXME: It doesn't seem that this userInfo is used anywhere in the implementation.
///------------------------------------------------------
/// @name Initializing an AFURLConnectionOperation Object
///------------------------------------------------------
/**
Initializes and returns a newly allocated operation object with a url connection configured with the specified url request.
This is the designated initializer.
@param urlRequest The request object to be used by the operation connection.
*/
- (instancetype)initWithRequest:(NSURLRequest *)urlRequest NS_DESIGNATED_INITIALIZER;
///----------------------------------
/// @name Pausing / Resuming Requests
///----------------------------------
/**
Pauses the execution of the request operation.
A paused operation returns `NO` for `-isReady`, `-isExecuting`, and `-isFinished`. As such, it will remain in an `NSOperationQueue` until it is either cancelled or resumed. Pausing a finished, cancelled, or paused operation has no effect.
*/
- (void)pause;
/**
Whether the request operation is currently paused.
@return `YES` if the operation is currently paused, otherwise `NO`.
*/
- (BOOL)isPaused;
/**
Resumes the execution of the paused request operation.
Pause/Resume behavior varies depending on the underlying implementation for the operation class. In its base implementation, resuming a paused requests restarts the original request. However, since HTTP defines a specification for how to request a specific content range, `AFHTTPRequestOperation` will resume downloading the request from where it left off, instead of restarting the original request.
*/
- (void)resume;
///----------------------------------------------
/// @name Configuring Backgrounding Task Behavior
///----------------------------------------------
/**
Specifies that the operation should continue execution after the app has entered the background, and the expiration handler for that background task.
@param handler A handler to be called shortly before the applications remaining background time reaches 0. The handler is wrapped in a block that cancels the operation, and cleans up and marks the end of execution, unlike the `handler` parameter in `UIApplication -beginBackgroundTaskWithExpirationHandler:`, which expects this to be done in the handler itself. The handler is called synchronously on the main thread, thus blocking the applications suspension momentarily while the application is notified.
*/
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
- (void)setShouldExecuteAsBackgroundTaskWithExpirationHandler:(nullable void (^)(void))handler NS_EXTENSION_UNAVAILABLE_IOS("Not available in app extensions.");
#endif
///---------------------------------
/// @name Setting Progress Callbacks
///---------------------------------
/**
Sets a callback to be called when an undetermined number of bytes have been uploaded to the server.
@param block A block object to be called when an undetermined number of bytes have been uploaded to the server. This block has no return value and takes three arguments: the number of bytes written since the last time the upload progress block was called, the total bytes written, and the total bytes expected to be written during the request, as initially determined by the length of the HTTP body. This block may be called multiple times, and will execute on the main thread.
*/
- (void)setUploadProgressBlock:(nullable void (^)(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite))block;
/**
Sets a callback to be called when an undetermined number of bytes have been downloaded from the server.
@param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes three arguments: the number of bytes read since the last time the download progress block was called, the total bytes read, and the total bytes expected to be read during the request, as initially determined by the expected content size of the `NSHTTPURLResponse` object. This block may be called multiple times, and will execute on the main thread.
*/
- (void)setDownloadProgressBlock:(nullable void (^)(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead))block;
///-------------------------------------------------
/// @name Setting NSURLConnection Delegate Callbacks
///-------------------------------------------------
/**
Sets a block to be executed when the connection will authenticate a challenge in order to download its request, as handled by the `NSURLConnectionDelegate` method `connection:willSendRequestForAuthenticationChallenge:`.
@param block A block object to be executed when the connection will authenticate a challenge in order to download its request. The block has no return type and takes two arguments: the URL connection object, and the challenge that must be authenticated. This block must invoke one of the challenge-responder methods (NSURLAuthenticationChallengeSender protocol).
If `allowsInvalidSSLCertificate` is set to YES, `connection:willSendRequestForAuthenticationChallenge:` will attempt to have the challenge sender use credentials with invalid SSL certificates.
*/
- (void)setWillSendRequestForAuthenticationChallengeBlock:(nullable void (^)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge))block;
/**
Sets a block to be executed when the server redirects the request from one URL to another URL, or when the request URL changed by the `NSURLProtocol` subclass handling the request in order to standardize its format, as handled by the `NSURLConnectionDataDelegate` method `connection:willSendRequest:redirectResponse:`.
@param block A block object to be executed when the request URL was changed. The block returns an `NSURLRequest` object, the URL request to redirect, and takes three arguments: the URL connection object, the the proposed redirected request, and the URL response that caused the redirect.
*/
- (void)setRedirectResponseBlock:(nullable NSURLRequest * (^)(NSURLConnection *connection, NSURLRequest *request, NSURLResponse *redirectResponse))block;
/**
Sets a block to be executed to modify the response a connection will cache, if any, as handled by the `NSURLConnectionDelegate` method `connection:willCacheResponse:`.
@param block A block object to be executed to determine what response a connection will cache, if any. The block returns an `NSCachedURLResponse` object, the cached response to store in memory or `nil` to prevent the response from being cached, and takes two arguments: the URL connection object, and the cached response provided for the request.
*/
- (void)setCacheResponseBlock:(nullable NSCachedURLResponse * (^)(NSURLConnection *connection, NSCachedURLResponse *cachedResponse))block;
///
/**
*/
+ (NSArray *)batchOfRequestOperations:(nullable NSArray *)operations
progressBlock:(nullable void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(nullable void (^)(NSArray *operations))completionBlock;
@end
///--------------------
/// @name Notifications
///--------------------
/**
Posted when an operation begins executing.
*/
extern NSString * const AFNetworkingOperationDidStartNotification;
/**
Posted when an operation finishes.
*/
extern NSString * const AFNetworkingOperationDidFinishNotification;
NS_ASSUME_NONNULL_END
@@ -0,0 +1,792 @@
// AFURLConnectionOperation.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import "AFURLConnectionOperation.h"
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <UIKit/UIKit.h>
#endif
#if !__has_feature(objc_arc)
#error AFNetworking must be built with ARC.
// You can turn on ARC for only AFNetworking files by adding -fobjc-arc to the build phase for each of its files.
#endif
typedef NS_ENUM(NSInteger, AFOperationState) {
AFOperationPausedState = -1,
AFOperationReadyState = 1,
AFOperationExecutingState = 2,
AFOperationFinishedState = 3,
};
static dispatch_group_t url_request_operation_completion_group() {
static dispatch_group_t af_url_request_operation_completion_group;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_url_request_operation_completion_group = dispatch_group_create();
});
return af_url_request_operation_completion_group;
}
static dispatch_queue_t url_request_operation_completion_queue() {
static dispatch_queue_t af_url_request_operation_completion_queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue", DISPATCH_QUEUE_CONCURRENT );
});
return af_url_request_operation_completion_queue;
}
static NSString * const kAFNetworkingLockName = @"com.alamofire.networking.operation.lock";
NSString * const AFNetworkingOperationDidStartNotification = @"com.alamofire.networking.operation.start";
NSString * const AFNetworkingOperationDidFinishNotification = @"com.alamofire.networking.operation.finish";
typedef void (^AFURLConnectionOperationProgressBlock)(NSUInteger bytes, long long totalBytes, long long totalBytesExpected);
typedef void (^AFURLConnectionOperationAuthenticationChallengeBlock)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge);
typedef NSCachedURLResponse * (^AFURLConnectionOperationCacheResponseBlock)(NSURLConnection *connection, NSCachedURLResponse *cachedResponse);
typedef NSURLRequest * (^AFURLConnectionOperationRedirectResponseBlock)(NSURLConnection *connection, NSURLRequest *request, NSURLResponse *redirectResponse);
typedef void (^AFURLConnectionOperationBackgroundTaskCleanupBlock)();
static inline NSString * AFKeyPathFromOperationState(AFOperationState state) {
switch (state) {
case AFOperationReadyState:
return @"isReady";
case AFOperationExecutingState:
return @"isExecuting";
case AFOperationFinishedState:
return @"isFinished";
case AFOperationPausedState:
return @"isPaused";
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
return @"state";
#pragma clang diagnostic pop
}
}
}
static inline BOOL AFStateTransitionIsValid(AFOperationState fromState, AFOperationState toState, BOOL isCancelled) {
switch (fromState) {
case AFOperationReadyState:
switch (toState) {
case AFOperationPausedState:
case AFOperationExecutingState:
return YES;
case AFOperationFinishedState:
return isCancelled;
default:
return NO;
}
case AFOperationExecutingState:
switch (toState) {
case AFOperationPausedState:
case AFOperationFinishedState:
return YES;
default:
return NO;
}
case AFOperationFinishedState:
return NO;
case AFOperationPausedState:
return toState == AFOperationReadyState;
default: {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
switch (toState) {
case AFOperationPausedState:
case AFOperationReadyState:
case AFOperationExecutingState:
case AFOperationFinishedState:
return YES;
default:
return NO;
}
}
#pragma clang diagnostic pop
}
}
@interface AFURLConnectionOperation ()
@property (readwrite, nonatomic, assign) AFOperationState state;
@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
@property (readwrite, nonatomic, strong) NSURLConnection *connection;
@property (readwrite, nonatomic, strong) NSURLRequest *request;
@property (readwrite, nonatomic, strong) NSURLResponse *response;
@property (readwrite, nonatomic, strong) NSError *error;
@property (readwrite, nonatomic, strong) NSData *responseData;
@property (readwrite, nonatomic, copy) NSString *responseString;
@property (readwrite, nonatomic, assign) NSStringEncoding responseStringEncoding;
@property (readwrite, nonatomic, assign) long long totalBytesRead;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationBackgroundTaskCleanupBlock backgroundTaskCleanup;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationProgressBlock uploadProgress;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationProgressBlock downloadProgress;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationAuthenticationChallengeBlock authenticationChallenge;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationCacheResponseBlock cacheResponse;
@property (readwrite, nonatomic, copy) AFURLConnectionOperationRedirectResponseBlock redirectResponse;
- (void)operationDidStart;
- (void)finish;
- (void)cancelConnection;
@end
@implementation AFURLConnectionOperation
@synthesize outputStream = _outputStream;
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
- (instancetype)initWithRequest:(NSURLRequest *)urlRequest {
NSParameterAssert(urlRequest);
self = [super init];
if (!self) {
return nil;
}
_state = AFOperationReadyState;
self.lock = [[NSRecursiveLock alloc] init];
self.lock.name = kAFNetworkingLockName;
self.runLoopModes = [NSSet setWithObject:NSRunLoopCommonModes];
self.request = urlRequest;
self.shouldUseCredentialStorage = YES;
self.securityPolicy = [AFSecurityPolicy defaultPolicy];
return self;
}
- (instancetype)init NS_UNAVAILABLE
{
return nil;
}
- (void)dealloc {
if (_outputStream) {
[_outputStream close];
_outputStream = nil;
}
if (_backgroundTaskCleanup) {
_backgroundTaskCleanup();
}
}
#pragma mark -
- (void)setResponseData:(NSData *)responseData {
[self.lock lock];
if (!responseData) {
_responseData = nil;
} else {
_responseData = [NSData dataWithBytes:responseData.bytes length:responseData.length];
}
[self.lock unlock];
}
- (NSString *)responseString {
[self.lock lock];
if (!_responseString && self.response && self.responseData) {
self.responseString = [[NSString alloc] initWithData:self.responseData encoding:self.responseStringEncoding];
}
[self.lock unlock];
return _responseString;
}
- (NSStringEncoding)responseStringEncoding {
[self.lock lock];
if (!_responseStringEncoding && self.response) {
NSStringEncoding stringEncoding = NSUTF8StringEncoding;
if (self.response.textEncodingName) {
CFStringEncoding IANAEncoding = CFStringConvertIANACharSetNameToEncoding((__bridge CFStringRef)self.response.textEncodingName);
if (IANAEncoding != kCFStringEncodingInvalidId) {
stringEncoding = CFStringConvertEncodingToNSStringEncoding(IANAEncoding);
}
}
self.responseStringEncoding = stringEncoding;
}
[self.lock unlock];
return _responseStringEncoding;
}
- (NSInputStream *)inputStream {
return self.request.HTTPBodyStream;
}
- (void)setInputStream:(NSInputStream *)inputStream {
NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
mutableRequest.HTTPBodyStream = inputStream;
self.request = mutableRequest;
}
- (NSOutputStream *)outputStream {
if (!_outputStream) {
self.outputStream = [NSOutputStream outputStreamToMemory];
}
return _outputStream;
}
- (void)setOutputStream:(NSOutputStream *)outputStream {
[self.lock lock];
if (outputStream != _outputStream) {
if (_outputStream) {
[_outputStream close];
}
_outputStream = outputStream;
}
[self.lock unlock];
}
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
- (void)setShouldExecuteAsBackgroundTaskWithExpirationHandler:(void (^)(void))handler {
[self.lock lock];
if (!self.backgroundTaskCleanup) {
UIApplication *application = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier __block backgroundTaskIdentifier = UIBackgroundTaskInvalid;
__weak __typeof(self)weakSelf = self;
self.backgroundTaskCleanup = ^(){
if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:backgroundTaskIdentifier];
backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
};
backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (handler) {
handler();
}
if (strongSelf) {
[strongSelf cancel];
strongSelf.backgroundTaskCleanup();
}
}];
}
[self.lock unlock];
}
#endif
#pragma mark -
- (void)setState:(AFOperationState)state {
if (!AFStateTransitionIsValid(self.state, state, [self isCancelled])) {
return;
}
[self.lock lock];
NSString *oldStateKey = AFKeyPathFromOperationState(self.state);
NSString *newStateKey = AFKeyPathFromOperationState(state);
[self willChangeValueForKey:newStateKey];
[self willChangeValueForKey:oldStateKey];
_state = state;
[self didChangeValueForKey:oldStateKey];
[self didChangeValueForKey:newStateKey];
[self.lock unlock];
}
- (void)pause {
if ([self isPaused] || [self isFinished] || [self isCancelled]) {
return;
}
[self.lock lock];
if ([self isExecuting]) {
[self performSelector:@selector(operationDidPause) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
});
}
self.state = AFOperationPausedState;
[self.lock unlock];
}
- (void)operationDidPause {
[self.lock lock];
[self.connection cancel];
[self.lock unlock];
}
- (BOOL)isPaused {
return self.state == AFOperationPausedState;
}
- (void)resume {
if (![self isPaused]) {
return;
}
[self.lock lock];
self.state = AFOperationReadyState;
[self start];
[self.lock unlock];
}
#pragma mark -
- (void)setUploadProgressBlock:(void (^)(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite))block {
self.uploadProgress = block;
}
- (void)setDownloadProgressBlock:(void (^)(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead))block {
self.downloadProgress = block;
}
- (void)setWillSendRequestForAuthenticationChallengeBlock:(void (^)(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge))block {
self.authenticationChallenge = block;
}
- (void)setCacheResponseBlock:(NSCachedURLResponse * (^)(NSURLConnection *connection, NSCachedURLResponse *cachedResponse))block {
self.cacheResponse = block;
}
- (void)setRedirectResponseBlock:(NSURLRequest * (^)(NSURLConnection *connection, NSURLRequest *request, NSURLResponse *redirectResponse))block {
self.redirectResponse = block;
}
#pragma mark - NSOperation
- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
block();
});
dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}
- (BOOL)isReady {
return self.state == AFOperationReadyState && [super isReady];
}
- (BOOL)isExecuting {
return self.state == AFOperationExecutingState;
}
- (BOOL)isFinished {
return self.state == AFOperationFinishedState;
}
- (BOOL)isConcurrent {
return YES;
}
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
- (void)operationDidStart {
[self.lock lock];
if (![self isCancelled]) {
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for (NSString *runLoopMode in self.runLoopModes) {
[self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
[self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
}
[self.outputStream open];
[self.connection start];
}
[self.lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
});
}
- (void)finish {
[self.lock lock];
self.state = AFOperationFinishedState;
[self.lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
});
}
- (void)cancel {
[self.lock lock];
if (![self isFinished] && ![self isCancelled]) {
[super cancel];
if ([self isExecuting]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
}
[self.lock unlock];
}
- (void)cancelConnection {
NSDictionary *userInfo = nil;
if ([self.request URL]) {
userInfo = @{NSURLErrorFailingURLErrorKey : [self.request URL]};
}
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
if (![self isFinished]) {
if (self.connection) {
[self.connection cancel];
[self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
} else {
// Accommodate race condition where `self.connection` has not yet been set before cancellation
self.error = error;
[self finish];
}
}
}
#pragma mark -
+ (NSArray *)batchOfRequestOperations:(NSArray *)operations
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(void (^)(NSArray *operations))completionBlock
{
if (!operations || [operations count] == 0) {
return @[[NSBlockOperation blockOperationWithBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(@[]);
}
});
}]];
}
__block dispatch_group_t group = dispatch_group_create();
NSBlockOperation *batchedOperation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(operations);
}
});
}];
for (AFURLConnectionOperation *operation in operations) {
operation.completionGroup = group;
void (^originalCompletionBlock)(void) = [operation.completionBlock copy];
__weak __typeof(operation)weakOperation = operation;
operation.completionBlock = ^{
__strong __typeof(weakOperation)strongOperation = weakOperation;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_queue_t queue = strongOperation.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
if (originalCompletionBlock) {
originalCompletionBlock();
}
NSUInteger numberOfFinishedOperations = [[operations indexesOfObjectsPassingTest:^BOOL(id op, NSUInteger __unused idx, BOOL __unused *stop) {
return [op isFinished];
}] count];
if (progressBlock) {
progressBlock(numberOfFinishedOperations, [operations count]);
}
dispatch_group_leave(group);
});
};
dispatch_group_enter(group);
[batchedOperation addDependency:operation];
}
return [operations arrayByAddingObject:batchedOperation];
}
#pragma mark - NSObject
- (NSString *)description {
[self.lock lock];
NSString *description = [NSString stringWithFormat:@"<%@: %p, state: %@, cancelled: %@ request: %@, response: %@>", NSStringFromClass([self class]), self, AFKeyPathFromOperationState(self.state), ([self isCancelled] ? @"YES" : @"NO"), self.request, self.response];
[self.lock unlock];
return description;
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if (self.authenticationChallenge) {
self.authenticationChallenge(connection, challenge);
return;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
} else {
if ([challenge previousFailureCount] == 0) {
if (self.credential) {
[[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
return self.shouldUseCredentialStorage;
}
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
{
if (self.redirectResponse) {
return self.redirectResponse(connection, request, redirectResponse);
} else {
return request;
}
}
- (void)connection:(NSURLConnection __unused *)connection
didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
dispatch_async(dispatch_get_main_queue(), ^{
if (self.uploadProgress) {
self.uploadProgress((NSUInteger)bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}
});
}
- (void)connection:(NSURLConnection __unused *)connection
didReceiveResponse:(NSURLResponse *)response
{
self.response = response;
}
- (void)connection:(NSURLConnection __unused *)connection
didReceiveData:(NSData *)data
{
NSUInteger length = [data length];
while (YES) {
NSInteger totalNumberOfBytesWritten = 0;
if ([self.outputStream hasSpaceAvailable]) {
const uint8_t *dataBuffer = (uint8_t *)[data bytes];
NSInteger numberOfBytesWritten = 0;
while (totalNumberOfBytesWritten < (NSInteger)length) {
numberOfBytesWritten = [self.outputStream write:&dataBuffer[(NSUInteger)totalNumberOfBytesWritten] maxLength:(length - (NSUInteger)totalNumberOfBytesWritten)];
if (numberOfBytesWritten == -1) {
break;
}
totalNumberOfBytesWritten += numberOfBytesWritten;
}
break;
}
if (self.outputStream.streamError) {
[self.connection cancel];
[self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:self.outputStream.streamError];
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
self.totalBytesRead += (long long)length;
if (self.downloadProgress) {
self.downloadProgress(length, self.totalBytesRead, self.response.expectedContentLength);
}
});
}
- (void)connectionDidFinishLoading:(NSURLConnection __unused *)connection {
self.responseData = [self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
[self.outputStream close];
if (self.responseData) {
self.outputStream = nil;
}
self.connection = nil;
[self finish];
}
- (void)connection:(NSURLConnection __unused *)connection
didFailWithError:(NSError *)error
{
self.error = error;
[self.outputStream close];
if (self.responseData) {
self.outputStream = nil;
}
self.connection = nil;
[self finish];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
if (self.cacheResponse) {
return self.cacheResponse(connection, cachedResponse);
} else {
if ([self isCancelled]) {
return nil;
}
return cachedResponse;
}
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (id)initWithCoder:(NSCoder *)decoder {
NSURLRequest *request = [decoder decodeObjectOfClass:[NSURLRequest class] forKey:NSStringFromSelector(@selector(request))];
self = [self initWithRequest:request];
if (!self) {
return nil;
}
self.state = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(state))] integerValue];
self.response = [decoder decodeObjectOfClass:[NSHTTPURLResponse class] forKey:NSStringFromSelector(@selector(response))];
self.error = [decoder decodeObjectOfClass:[NSError class] forKey:NSStringFromSelector(@selector(error))];
self.responseData = [decoder decodeObjectOfClass:[NSData class] forKey:NSStringFromSelector(@selector(responseData))];
self.totalBytesRead = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(totalBytesRead))] longLongValue];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[self pause];
[coder encodeObject:self.request forKey:NSStringFromSelector(@selector(request))];
switch (self.state) {
case AFOperationExecutingState:
case AFOperationPausedState:
[coder encodeInteger:AFOperationReadyState forKey:NSStringFromSelector(@selector(state))];
break;
default:
[coder encodeInteger:self.state forKey:NSStringFromSelector(@selector(state))];
break;
}
[coder encodeObject:self.response forKey:NSStringFromSelector(@selector(response))];
[coder encodeObject:self.error forKey:NSStringFromSelector(@selector(error))];
[coder encodeObject:self.responseData forKey:NSStringFromSelector(@selector(responseData))];
[coder encodeInt64:self.totalBytesRead forKey:NSStringFromSelector(@selector(totalBytesRead))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFURLConnectionOperation *operation = [(AFURLConnectionOperation *)[[self class] allocWithZone:zone] initWithRequest:self.request];
operation.uploadProgress = self.uploadProgress;
operation.downloadProgress = self.downloadProgress;
operation.authenticationChallenge = self.authenticationChallenge;
operation.cacheResponse = self.cacheResponse;
operation.redirectResponse = self.redirectResponse;
operation.completionQueue = self.completionQueue;
operation.completionGroup = self.completionGroup;
return operation;
}
@end
@@ -0,0 +1,471 @@
// AFURLRequestSerialization.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <UIKit/UIKit.h>
#endif
NS_ASSUME_NONNULL_BEGIN
/**
The `AFURLRequestSerialization` protocol is adopted by an object that encodes parameters for a specified HTTP requests. Request serializers may encode parameters as query strings, HTTP bodies, setting the appropriate HTTP header fields as necessary.
For example, a JSON request serializer may set the HTTP body of the request to a JSON representation, and set the `Content-Type` HTTP header field value to `application/json`.
*/
@protocol AFURLRequestSerialization <NSObject, NSSecureCoding, NSCopying>
/**
Returns a request with the specified parameters encoded into a copy of the original request.
@param request The original request.
@param parameters The parameters to be encoded.
@param error The error that occurred while attempting to encode the request parameters.
@return A serialized request.
*/
- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(nullable id)parameters
error:(NSError * __nullable __autoreleasing *)error;
@end
#pragma mark -
/**
*/
typedef NS_ENUM(NSUInteger, AFHTTPRequestQueryStringSerializationStyle) {
AFHTTPRequestQueryStringDefaultStyle = 0,
};
@protocol AFMultipartFormData;
/**
`AFHTTPRequestSerializer` conforms to the `AFURLRequestSerialization` & `AFURLResponseSerialization` protocols, offering a concrete base implementation of query string / URL form-encoded parameter serialization and default request headers, as well as response status code and content type validation.
Any request or response serializer dealing with HTTP is encouraged to subclass `AFHTTPRequestSerializer` in order to ensure consistent default behavior.
*/
@interface AFHTTPRequestSerializer : NSObject <AFURLRequestSerialization>
/**
The string encoding used to serialize parameters. `NSUTF8StringEncoding` by default.
*/
@property (nonatomic, assign) NSStringEncoding stringEncoding;
/**
Whether created requests can use the devices cellular radio (if present). `YES` by default.
@see NSMutableURLRequest -setAllowsCellularAccess:
*/
@property (nonatomic, assign) BOOL allowsCellularAccess;
/**
The cache policy of created requests. `NSURLRequestUseProtocolCachePolicy` by default.
@see NSMutableURLRequest -setCachePolicy:
*/
@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy;
/**
Whether created requests should use the default cookie handling. `YES` by default.
@see NSMutableURLRequest -setHTTPShouldHandleCookies:
*/
@property (nonatomic, assign) BOOL HTTPShouldHandleCookies;
/**
Whether created requests can continue transmitting data before receiving a response from an earlier transmission. `NO` by default
@see NSMutableURLRequest -setHTTPShouldUsePipelining:
*/
@property (nonatomic, assign) BOOL HTTPShouldUsePipelining;
/**
The network service type for created requests. `NSURLNetworkServiceTypeDefault` by default.
@see NSMutableURLRequest -setNetworkServiceType:
*/
@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;
/**
The timeout interval, in seconds, for created requests. The default timeout interval is 60 seconds.
@see NSMutableURLRequest -setTimeoutInterval:
*/
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
///---------------------------------------
/// @name Configuring HTTP Request Headers
///---------------------------------------
/**
Default HTTP header field values to be applied to serialized requests. By default, these include the following:
- `Accept-Language` with the contents of `NSLocale +preferredLanguages`
- `User-Agent` with the contents of various bundle identifiers and OS designations
@discussion To add or remove default request headers, use `setValue:forHTTPHeaderField:`.
*/
@property (readonly, nonatomic, strong) NSDictionary *HTTPRequestHeaders;
/**
Creates and returns a serializer with default configuration.
*/
+ (instancetype)serializer;
/**
Sets the value for the HTTP headers set in request objects made by the HTTP client. If `nil`, removes the existing value for that header.
@param field The HTTP header to set a default value for
@param value The value set as default for the specified header, or `nil`
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
/**
Returns the value for the HTTP headers set in the request serializer.
@param field The HTTP header to retrieve the default value for
@return The value set as default for the specified header, or `nil`
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
/**
Sets the "Authorization" HTTP header set in request objects made by the HTTP client to a basic authentication value with Base64-encoded username and password. This overwrites any existing value for this header.
@param username The HTTP basic auth username
@param password The HTTP basic auth password
*/
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
/**
@deprecated This method has been deprecated. Use -setValue:forHTTPHeaderField: instead.
*/
- (void)setAuthorizationHeaderFieldWithToken:(NSString *)token DEPRECATED_ATTRIBUTE;
/**
Clears any existing value for the "Authorization" HTTP header.
*/
- (void)clearAuthorizationHeader;
///-------------------------------------------------------
/// @name Configuring Query String Parameter Serialization
///-------------------------------------------------------
/**
HTTP methods for which serialized requests will encode parameters as a query string. `GET`, `HEAD`, and `DELETE` by default.
*/
@property (nonatomic, strong) NSSet *HTTPMethodsEncodingParametersInURI;
/**
Set the method of query string serialization according to one of the pre-defined styles.
@param style The serialization style.
@see AFHTTPRequestQueryStringSerializationStyle
*/
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
/**
Set the a custom method of query string serialization according to the specified block.
@param block A block that defines a process of encoding parameters into a query string. This block returns the query string and takes three arguments: the request, the parameters to encode, and the error that occurred when attempting to encode parameters for the given request.
*/
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
///-------------------------------
/// @name Creating Request Objects
///-------------------------------
/**
@deprecated This method has been deprecated. Use -requestWithMethod:URLString:parameters:error: instead.
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters DEPRECATED_ATTRIBUTE;
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URL string.
If the HTTP method is `GET`, `HEAD`, or `DELETE`, the parameters will be used to construct a url-encoded query string that is appended to the request's URL. Otherwise, the parameters will be encoded according to the value of the `parameterEncoding` property, and set as the request body.
@param method The HTTP method for the request, such as `GET`, `POST`, `PUT`, or `DELETE`. This parameter must not be `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be either set as a query string for `GET` requests, or the request HTTP body.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object.
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable id)parameters
error:(NSError * __nullable __autoreleasing *)error;
/**
@deprecated This method has been deprecated. Use -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error: instead.
*/
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block DEPRECATED_ATTRIBUTE;
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URLString, and constructs a `multipart/form-data` HTTP body, using the specified parameters and multipart form data block. See http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2
Multipart form requests are automatically streamed, reading files directly from disk along with in-memory data in a single HTTP body. The resulting `NSMutableURLRequest` object has an `HTTPBodyStream` property, so refrain from setting `HTTPBodyStream` or `HTTPBody` on this request object, as it will clear out the multipart form body stream.
@param method The HTTP method for the request. This parameter must not be `GET` or `HEAD`, or `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded and set in the request HTTP body.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object
*/
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable NSDictionary *)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * __nullable __autoreleasing *)error;
/**
Creates an `NSMutableURLRequest` by removing the `HTTPBodyStream` from a request, and asynchronously writing its contents into the specified file, invoking the completion handler when finished.
@param request The multipart form request. The `HTTPBodyStream` property of `request` must not be `nil`.
@param fileURL The file URL to write multipart form contents to.
@param handler A handler block to execute.
@discussion There is a bug in `NSURLSessionTask` that causes requests to not send a `Content-Length` header when streaming contents from an HTTP body, which is notably problematic when interacting with the Amazon S3 webservice. As a workaround, this method takes a request constructed with `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:`, or any other request with an `HTTPBodyStream`, writes the contents to the specified file and returns a copy of the original request with the `HTTPBodyStream` property set to `nil`. From here, the file can either be passed to `AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler:`, or have its contents read into an `NSData` that's assigned to the `HTTPBody` property of the request.
@see https://github.com/AFNetworking/AFNetworking/issues/1398
*/
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(nullable void (^)(NSError *error))handler;
@end
#pragma mark -
/**
The `AFMultipartFormData` protocol defines the methods supported by the parameter in the block argument of `AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:`.
*/
@protocol AFMultipartFormData
/**
Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary.
The filename and MIME type for this data in the form will be automatically generated, using the last path component of the `fileURL` and system associated MIME type for the `fileURL` extension, respectively.
@param fileURL The URL corresponding to the file whose content will be appended to the form. This parameter must not be `nil`.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
@param error If an error occurs, upon return contains an `NSError` object that describes the problem.
@return `YES` if the file data was successfully appended, otherwise `NO`.
*/
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * __nullable __autoreleasing *)error;
/**
Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary.
@param fileURL The URL corresponding to the file whose content will be appended to the form. This parameter must not be `nil`.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
@param fileName The file name to be used in the `Content-Disposition` header. This parameter must not be `nil`.
@param mimeType The declared MIME type of the file data. This parameter must not be `nil`.
@param error If an error occurs, upon return contains an `NSError` object that describes the problem.
@return `YES` if the file data was successfully appended otherwise `NO`.
*/
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * __nullable __autoreleasing *)error;
/**
Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary.
@param inputStream The input stream to be appended to the form data
@param name The name to be associated with the specified input stream. This parameter must not be `nil`.
@param fileName The filename to be associated with the specified input stream. This parameter must not be `nil`.
@param length The length of the specified input stream in bytes.
@param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
*/
- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream
name:(NSString *)name
fileName:(NSString *)fileName
length:(int64_t)length
mimeType:(NSString *)mimeType;
/**
Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary.
@param data The data to be encoded and appended to the form data.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
@param fileName The filename to be associated with the specified data. This parameter must not be `nil`.
@param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`.
*/
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType;
/**
Appends the HTTP headers `Content-Disposition: form-data; name=#{name}"`, followed by the encoded data and the multipart form boundary.
@param data The data to be encoded and appended to the form data.
@param name The name to be associated with the specified data. This parameter must not be `nil`.
*/
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name;
/**
Appends HTTP headers, followed by the encoded data and the multipart form boundary.
@param headers The HTTP headers to be appended to the form data.
@param body The data to be encoded and appended to the form data. This parameter must not be `nil`.
*/
- (void)appendPartWithHeaders:(nullable NSDictionary *)headers
body:(NSData *)body;
/**
Throttles request bandwidth by limiting the packet size and adding a delay for each chunk read from the upload stream.
When uploading over a 3G or EDGE connection, requests may fail with "request body stream exhausted". Setting a maximum packet size and delay according to the recommended values (`kAFUploadStream3GSuggestedPacketSize` and `kAFUploadStream3GSuggestedDelay`) lowers the risk of the input stream exceeding its allocated bandwidth. Unfortunately, there is no definite way to distinguish between a 3G, EDGE, or LTE connection over `NSURLConnection`. As such, it is not recommended that you throttle bandwidth based solely on network reachability. Instead, you should consider checking for the "request body stream exhausted" in a failure block, and then retrying the request with throttled bandwidth.
@param numberOfBytes Maximum packet size, in number of bytes. The default packet size for an input stream is 16kb.
@param delay Duration of delay each time a packet is read. By default, no delay is set.
*/
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
@end
#pragma mark -
/**
`AFJSONRequestSerializer` is a subclass of `AFHTTPRequestSerializer` that encodes parameters as JSON using `NSJSONSerialization`, setting the `Content-Type` of the encoded request to `application/json`.
*/
@interface AFJSONRequestSerializer : AFHTTPRequestSerializer
/**
Options for writing the request JSON data from Foundation objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONWritingOptions". `0` by default.
*/
@property (nonatomic, assign) NSJSONWritingOptions writingOptions;
/**
Creates and returns a JSON serializer with specified reading and writing options.
@param writingOptions The specified JSON writing options.
*/
+ (instancetype)serializerWithWritingOptions:(NSJSONWritingOptions)writingOptions;
@end
#pragma mark -
/**
`AFPropertyListRequestSerializer` is a subclass of `AFHTTPRequestSerializer` that encodes parameters as JSON using `NSPropertyListSerializer`, setting the `Content-Type` of the encoded request to `application/x-plist`.
*/
@interface AFPropertyListRequestSerializer : AFHTTPRequestSerializer
/**
The property list format. Possible values are described in "NSPropertyListFormat".
*/
@property (nonatomic, assign) NSPropertyListFormat format;
/**
@warning The `writeOptions` property is currently unused.
*/
@property (nonatomic, assign) NSPropertyListWriteOptions writeOptions;
/**
Creates and returns a property list serializer with a specified format, read options, and write options.
@param format The property list format.
@param writeOptions The property list write options.
@warning The `writeOptions` property is currently unused.
*/
+ (instancetype)serializerWithFormat:(NSPropertyListFormat)format
writeOptions:(NSPropertyListWriteOptions)writeOptions;
@end
#pragma mark -
///----------------
/// @name Constants
///----------------
/**
## Error Domains
The following error domain is predefined.
- `NSString * const AFURLRequestSerializationErrorDomain`
### Constants
`AFURLRequestSerializationErrorDomain`
AFURLRequestSerializer errors. Error codes for `AFURLRequestSerializationErrorDomain` correspond to codes in `NSURLErrorDomain`.
*/
extern NSString * const AFURLRequestSerializationErrorDomain;
/**
## User info dictionary keys
These keys may exist in the user info dictionary, in addition to those defined for NSError.
- `NSString * const AFNetworkingOperationFailingURLRequestErrorKey`
### Constants
`AFNetworkingOperationFailingURLRequestErrorKey`
The corresponding value is an `NSURLRequest` containing the request of the operation associated with an error. This key is only present in the `AFURLRequestSerializationErrorDomain`.
*/
extern NSString * const AFNetworkingOperationFailingURLRequestErrorKey;
/**
## Throttling Bandwidth for HTTP Request Input Streams
@see -throttleBandwidthWithPacketSize:delay:
### Constants
`kAFUploadStream3GSuggestedPacketSize`
Maximum packet size, in number of bytes. Equal to 16kb.
`kAFUploadStream3GSuggestedDelay`
Duration of delay each time a packet is read. Equal to 0.2 seconds.
*/
extern NSUInteger const kAFUploadStream3GSuggestedPacketSize;
extern NSTimeInterval const kAFUploadStream3GSuggestedDelay;
NS_ASSUME_NONNULL_END
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
// AFURLResponseSerialization.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
NS_ASSUME_NONNULL_BEGIN
/**
The `AFURLResponseSerialization` protocol is adopted by an object that decodes data into a more useful object representation, according to details in the server response. Response serializers may additionally perform validation on the incoming response and data.
For example, a JSON response serializer may check for an acceptable status code (`2XX` range) and content type (`application/json`), decoding a valid JSON response into an object.
*/
@protocol AFURLResponseSerialization <NSObject, NSSecureCoding, NSCopying>
/**
The response object decoded from the data associated with a specified response.
@param response The response to be processed.
@param data The response data to be decoded.
@param error The error that occurred while attempting to decode the response data.
@return The object decoded from the specified response data.
*/
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response
data:(nullable NSData *)data
error:(NSError * __nullable __autoreleasing *)error;
@end
#pragma mark -
/**
`AFHTTPResponseSerializer` conforms to the `AFURLRequestSerialization` & `AFURLResponseSerialization` protocols, offering a concrete base implementation of query string / URL form-encoded parameter serialization and default request headers, as well as response status code and content type validation.
Any request or response serializer dealing with HTTP is encouraged to subclass `AFHTTPResponseSerializer` in order to ensure consistent default behavior.
*/
@interface AFHTTPResponseSerializer : NSObject <AFURLResponseSerialization>
- (instancetype)init;
/**
The string encoding used to serialize data received from the server, when no string encoding is specified by the response. `NSUTF8StringEncoding` by default.
*/
@property (nonatomic, assign) NSStringEncoding stringEncoding;
/**
Creates and returns a serializer with default configuration.
*/
+ (instancetype)serializer;
///-----------------------------------------
/// @name Configuring Response Serialization
///-----------------------------------------
/**
The acceptable HTTP status codes for responses. When non-`nil`, responses with status codes not contained by the set will result in an error during validation.
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
*/
@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;
/**
The acceptable MIME types for responses. When non-`nil`, responses with a `Content-Type` with MIME types that do not intersect with the set will result in an error during validation.
*/
@property (nonatomic, copy, nullable) NSSet *acceptableContentTypes;
/**
Validates the specified response and data.
In its base implementation, this method checks for an acceptable status code and content type. Subclasses may wish to add other domain-specific checks.
@param response The response to be validated.
@param data The data associated with the response.
@param error The error that occurred while attempting to validate the response.
@return `YES` if the response is valid, otherwise `NO`.
*/
- (BOOL)validateResponse:(nullable NSHTTPURLResponse *)response
data:(nullable NSData *)data
error:(NSError * __nullable __autoreleasing *)error;
@end
#pragma mark -
/**
`AFJSONResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes JSON responses.
By default, `AFJSONResponseSerializer` accepts the following MIME types, which includes the official standard, `application/json`, as well as other commonly-used types:
- `application/json`
- `text/json`
- `text/javascript`
*/
@interface AFJSONResponseSerializer : AFHTTPResponseSerializer
- (instancetype)init;
/**
Options for reading the response JSON data and creating the Foundation objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONReadingOptions". `0` by default.
*/
@property (nonatomic, assign) NSJSONReadingOptions readingOptions;
/**
Whether to remove keys with `NSNull` values from response JSON. Defaults to `NO`.
*/
@property (nonatomic, assign) BOOL removesKeysWithNullValues;
/**
Creates and returns a JSON serializer with specified reading and writing options.
@param readingOptions The specified JSON reading options.
*/
+ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions;
@end
#pragma mark -
/**
`AFXMLParserResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLParser` objects.
By default, `AFXMLParserResponseSerializer` accepts the following MIME types, which includes the official standard, `application/xml`, as well as other commonly-used types:
- `application/xml`
- `text/xml`
*/
@interface AFXMLParserResponseSerializer : AFHTTPResponseSerializer
@end
#pragma mark -
#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
/**
`AFXMLDocumentResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLDocument` objects.
By default, `AFXMLDocumentResponseSerializer` accepts the following MIME types, which includes the official standard, `application/xml`, as well as other commonly-used types:
- `application/xml`
- `text/xml`
*/
@interface AFXMLDocumentResponseSerializer : AFHTTPResponseSerializer
- (instancetype)init;
/**
Input and output options specifically intended for `NSXMLDocument` objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONReadingOptions". `0` by default.
*/
@property (nonatomic, assign) NSUInteger options;
/**
Creates and returns an XML document serializer with the specified options.
@param mask The XML document options.
*/
+ (instancetype)serializerWithXMLDocumentOptions:(NSUInteger)mask;
@end
#endif
#pragma mark -
/**
`AFPropertyListResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLDocument` objects.
By default, `AFPropertyListResponseSerializer` accepts the following MIME types:
- `application/x-plist`
*/
@interface AFPropertyListResponseSerializer : AFHTTPResponseSerializer
- (instancetype)init;
/**
The property list format. Possible values are described in "NSPropertyListFormat".
*/
@property (nonatomic, assign) NSPropertyListFormat format;
/**
The property list reading options. Possible values are described in "NSPropertyListMutabilityOptions."
*/
@property (nonatomic, assign) NSPropertyListReadOptions readOptions;
/**
Creates and returns a property list serializer with a specified format, read options, and write options.
@param format The property list format.
@param readOptions The property list reading options.
*/
+ (instancetype)serializerWithFormat:(NSPropertyListFormat)format
readOptions:(NSPropertyListReadOptions)readOptions;
@end
#pragma mark -
/**
`AFImageResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes image responses.
By default, `AFImageResponseSerializer` accepts the following MIME types, which correspond to the image formats supported by UIImage or NSImage:
- `image/tiff`
- `image/jpeg`
- `image/gif`
- `image/png`
- `image/ico`
- `image/x-icon`
- `image/bmp`
- `image/x-bmp`
- `image/x-xbitmap`
- `image/x-win-bitmap`
*/
@interface AFImageResponseSerializer : AFHTTPResponseSerializer
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
/**
The scale factor used when interpreting the image data to construct `responseImage`. Specifying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the size property. This is set to the value of scale of the main screen by default, which automatically scales images for retina displays, for instance.
*/
@property (nonatomic, assign) CGFloat imageScale;
/**
Whether to automatically inflate response image data for compressed formats (such as PNG or JPEG). Enabling this can significantly improve drawing performance on iOS when used with `setCompletionBlockWithSuccess:failure:`, as it allows a bitmap representation to be constructed in the background rather than on the main thread. `YES` by default.
*/
@property (nonatomic, assign) BOOL automaticallyInflatesResponseImage;
#endif
@end
#pragma mark -
/**
`AFCompoundSerializer` is a subclass of `AFHTTPResponseSerializer` that delegates the response serialization to the first `AFHTTPResponseSerializer` object that returns an object for `responseObjectForResponse:data:error:`, falling back on the default behavior of `AFHTTPResponseSerializer`. This is useful for supporting multiple potential types and structures of server responses with a single serializer.
*/
@interface AFCompoundResponseSerializer : AFHTTPResponseSerializer
/**
The component response serializers.
*/
@property (readonly, nonatomic, copy) NSArray *responseSerializers;
/**
Creates and returns a compound serializer comprised of the specified response serializers.
@warning Each response serializer specified must be a subclass of `AFHTTPResponseSerializer`, and response to `-validateResponse:data:error:`.
*/
+ (instancetype)compoundSerializerWithResponseSerializers:(NSArray *)responseSerializers;
@end
///----------------
/// @name Constants
///----------------
/**
## Error Domains
The following error domain is predefined.
- `NSString * const AFURLResponseSerializationErrorDomain`
### Constants
`AFURLResponseSerializationErrorDomain`
AFURLResponseSerializer errors. Error codes for `AFURLResponseSerializationErrorDomain` correspond to codes in `NSURLErrorDomain`.
*/
extern NSString * const AFURLResponseSerializationErrorDomain;
/**
## User info dictionary keys
These keys may exist in the user info dictionary, in addition to those defined for NSError.
- `NSString * const AFNetworkingOperationFailingURLResponseErrorKey`
- `NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey`
### Constants
`AFNetworkingOperationFailingURLResponseErrorKey`
The corresponding value is an `NSURLResponse` containing the response of the operation associated with an error. This key is only present in the `AFURLResponseSerializationErrorDomain`.
`AFNetworkingOperationFailingURLResponseDataErrorKey`
The corresponding value is an `NSData` containing the original data of the operation associated with an error. This key is only present in the `AFURLResponseSerializationErrorDomain`.
*/
extern NSString * const AFNetworkingOperationFailingURLResponseErrorKey;
extern NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey;
NS_ASSUME_NONNULL_END
@@ -0,0 +1,820 @@
// AFURLResponseSerialization.m
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import "AFURLResponseSerialization.h"
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <UIKit/UIKit.h>
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
#import <Cocoa/Cocoa.h>
#endif
NSString * const AFURLResponseSerializationErrorDomain = @"com.alamofire.error.serialization.response";
NSString * const AFNetworkingOperationFailingURLResponseErrorKey = @"com.alamofire.serialization.response.error.response";
NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey = @"com.alamofire.serialization.response.error.data";
static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) {
if (!error) {
return underlyingError;
}
if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) {
return error;
}
NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy];
mutableUserInfo[NSUnderlyingErrorKey] = underlyingError;
return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo];
}
static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) {
if ([error.domain isEqualToString:domain] && error.code == code) {
return YES;
} else if (error.userInfo[NSUnderlyingErrorKey]) {
return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain);
}
return NO;
}
static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) {
if ([JSONObject isKindOfClass:[NSArray class]]) {
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
for (id value in (NSArray *)JSONObject) {
[mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)];
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
} else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
id value = (NSDictionary *)JSONObject[key];
if (!value || [value isEqual:[NSNull null]]) {
[mutableDictionary removeObjectForKey:key];
} else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions);
}
}
return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
}
return JSONObject;
}
@implementation AFHTTPResponseSerializer
+ (instancetype)serializer {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.stringEncoding = NSUTF8StringEncoding;
self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
self.acceptableContentTypes = nil;
return self;
}
#pragma mark -
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError * __autoreleasing *)error
{
BOOL responseIsValid = YES;
NSError *validationError = nil;
if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]]) {
if ([data length] > 0 && [response URL]) {
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
}
responseIsValid = NO;
}
if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);
responseIsValid = NO;
}
}
if (error && !responseIsValid) {
*error = validationError;
}
return responseIsValid;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
[self validateResponse:(NSHTTPURLResponse *)response data:data error:error];
return data;
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [self init];
if (!self) {
return nil;
}
self.acceptableStatusCodes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
self.acceptableContentTypes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableContentTypes))];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.acceptableStatusCodes forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
[coder encodeObject:self.acceptableContentTypes forKey:NSStringFromSelector(@selector(acceptableContentTypes))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFHTTPResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.acceptableStatusCodes = [self.acceptableStatusCodes copyWithZone:zone];
serializer.acceptableContentTypes = [self.acceptableContentTypes copyWithZone:zone];
return serializer;
}
@end
#pragma mark -
@implementation AFJSONResponseSerializer
+ (instancetype)serializer {
return [self serializerWithReadingOptions:(NSJSONReadingOptions)0];
}
+ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions {
AFJSONResponseSerializer *serializer = [[self alloc] init];
serializer.readingOptions = readingOptions;
return serializer;
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
return self;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
// Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization.
// See https://github.com/rails/rails/issues/1742
NSStringEncoding stringEncoding = self.stringEncoding;
if (response.textEncodingName) {
CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (encoding != kCFStringEncodingInvalidId) {
stringEncoding = CFStringConvertEncodingToNSStringEncoding(encoding);
}
}
id responseObject = nil;
NSError *serializationError = nil;
@autoreleasepool {
NSString *responseString = [[NSString alloc] initWithData:data encoding:stringEncoding];
if (responseString && ![responseString isEqualToString:@" "]) {
// Workaround for a bug in NSJSONSerialization when Unicode character escape codes are used instead of the actual character
// See http://stackoverflow.com/a/12843465/157142
data = [responseString dataUsingEncoding:NSUTF8StringEncoding];
if (data) {
if ([data length] > 0) {
responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
} else {
return nil;
}
} else {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Data failed decoding as a UTF-8 string", @"AFNetworking", nil),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Could not decode string: %@", @"AFNetworking", nil), responseString]
};
serializationError = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
}
}
}
if (self.removesKeysWithNullValues && responseObject) {
responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}
if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
}
return responseObject;
}
#pragma mark - NSSecureCoding
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.readingOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(readingOptions))] unsignedIntegerValue];
self.removesKeysWithNullValues = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(removesKeysWithNullValues))] boolValue];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:@(self.readingOptions) forKey:NSStringFromSelector(@selector(readingOptions))];
[coder encodeObject:@(self.removesKeysWithNullValues) forKey:NSStringFromSelector(@selector(removesKeysWithNullValues))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFJSONResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.readingOptions = self.readingOptions;
serializer.removesKeysWithNullValues = self.removesKeysWithNullValues;
return serializer;
}
@end
#pragma mark -
@implementation AFXMLParserResponseSerializer
+ (instancetype)serializer {
AFXMLParserResponseSerializer *serializer = [[self alloc] init];
return serializer;
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/xml", @"text/xml", nil];
return self;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
return [[NSXMLParser alloc] initWithData:data];
}
@end
#pragma mark -
#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
@implementation AFXMLDocumentResponseSerializer
+ (instancetype)serializer {
return [self serializerWithXMLDocumentOptions:0];
}
+ (instancetype)serializerWithXMLDocumentOptions:(NSUInteger)mask {
AFXMLDocumentResponseSerializer *serializer = [[self alloc] init];
serializer.options = mask;
return serializer;
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/xml", @"text/xml", nil];
return self;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
NSError *serializationError = nil;
NSXMLDocument *document = [[NSXMLDocument alloc] initWithData:data options:self.options error:&serializationError];
if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
}
return document;
}
#pragma mark - NSSecureCoding
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.options = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(options))] unsignedIntegerValue];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:@(self.options) forKey:NSStringFromSelector(@selector(options))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFXMLDocumentResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.options = self.options;
return serializer;
}
@end
#endif
#pragma mark -
@implementation AFPropertyListResponseSerializer
+ (instancetype)serializer {
return [self serializerWithFormat:NSPropertyListXMLFormat_v1_0 readOptions:0];
}
+ (instancetype)serializerWithFormat:(NSPropertyListFormat)format
readOptions:(NSPropertyListReadOptions)readOptions
{
AFPropertyListResponseSerializer *serializer = [[self alloc] init];
serializer.format = format;
serializer.readOptions = readOptions;
return serializer;
}
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/x-plist", nil];
return self;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
id responseObject;
NSError *serializationError = nil;
if (data) {
responseObject = [NSPropertyListSerialization propertyListWithData:data options:self.readOptions format:NULL error:&serializationError];
}
if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
}
return responseObject;
}
#pragma mark - NSSecureCoding
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.format = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(format))] unsignedIntegerValue];
self.readOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(readOptions))] unsignedIntegerValue];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:@(self.format) forKey:NSStringFromSelector(@selector(format))];
[coder encodeObject:@(self.readOptions) forKey:NSStringFromSelector(@selector(readOptions))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFPropertyListResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.format = self.format;
serializer.readOptions = self.readOptions;
return serializer;
}
@end
#pragma mark -
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
#import <CoreGraphics/CoreGraphics.h>
@interface UIImage (AFNetworkingSafeImageLoading)
+ (UIImage *)af_safeImageWithData:(NSData *)data;
@end
static NSLock* imageLock = nil;
@implementation UIImage (AFNetworkingSafeImageLoading)
+ (UIImage *)af_safeImageWithData:(NSData *)data {
UIImage* image = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageLock = [[NSLock alloc] init];
});
[imageLock lock];
image = [UIImage imageWithData:data];
[imageLock unlock];
return image;
}
@end
static UIImage * AFImageWithDataAtScale(NSData *data, CGFloat scale) {
UIImage *image = [UIImage af_safeImageWithData:data];
if (image.images) {
return image;
}
return [[UIImage alloc] initWithCGImage:[image CGImage] scale:scale orientation:image.imageOrientation];
}
static UIImage * AFInflatedImageFromResponseWithDataAtScale(NSHTTPURLResponse *response, NSData *data, CGFloat scale) {
if (!data || [data length] == 0) {
return nil;
}
CGImageRef imageRef = NULL;
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
if ([response.MIMEType isEqualToString:@"image/png"]) {
imageRef = CGImageCreateWithPNGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault);
} else if ([response.MIMEType isEqualToString:@"image/jpeg"]) {
imageRef = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault);
if (imageRef) {
CGColorSpaceRef imageColorSpace = CGImageGetColorSpace(imageRef);
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(imageColorSpace);
// CGImageCreateWithJPEGDataProvider does not properly handle CMKY, so fall back to AFImageWithDataAtScale
if (imageColorSpaceModel == kCGColorSpaceModelCMYK) {
CGImageRelease(imageRef);
imageRef = NULL;
}
}
}
CGDataProviderRelease(dataProvider);
UIImage *image = AFImageWithDataAtScale(data, scale);
if (!imageRef) {
if (image.images || !image) {
return image;
}
imageRef = CGImageCreateCopy([image CGImage]);
if (!imageRef) {
return nil;
}
}
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
if (width * height > 1024 * 1024 || bitsPerComponent > 8) {
CGImageRelease(imageRef);
return image;
}
// CGImageGetBytesPerRow() calculates incorrectly in iOS 5.0, so defer to CGBitmapContextCreate
size_t bytesPerRow = 0;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
if (colorSpaceModel == kCGColorSpaceModelRGB) {
uint32_t alpha = (bitmapInfo & kCGBitmapAlphaInfoMask);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wassign-enum"
if (alpha == kCGImageAlphaNone) {
bitmapInfo &= ~kCGBitmapAlphaInfoMask;
bitmapInfo |= kCGImageAlphaNoneSkipFirst;
} else if (!(alpha == kCGImageAlphaNoneSkipFirst || alpha == kCGImageAlphaNoneSkipLast)) {
bitmapInfo &= ~kCGBitmapAlphaInfoMask;
bitmapInfo |= kCGImageAlphaPremultipliedFirst;
}
#pragma clang diagnostic pop
}
CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo);
CGColorSpaceRelease(colorSpace);
if (!context) {
CGImageRelease(imageRef);
return image;
}
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), imageRef);
CGImageRef inflatedImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
UIImage *inflatedImage = [[UIImage alloc] initWithCGImage:inflatedImageRef scale:scale orientation:image.imageOrientation];
CGImageRelease(inflatedImageRef);
CGImageRelease(imageRef);
return inflatedImage;
}
#endif
@implementation AFImageResponseSerializer
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"image/tiff", @"image/jpeg", @"image/gif", @"image/png", @"image/ico", @"image/x-icon", @"image/bmp", @"image/x-bmp", @"image/x-xbitmap", @"image/x-win-bitmap", nil];
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
self.imageScale = [[UIScreen mainScreen] scale];
self.automaticallyInflatesResponseImage = YES;
#endif
return self;
}
#pragma mark - AFURLResponseSerializer
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
if (self.automaticallyInflatesResponseImage) {
return AFInflatedImageFromResponseWithDataAtScale((NSHTTPURLResponse *)response, data, self.imageScale);
} else {
return AFImageWithDataAtScale(data, self.imageScale);
}
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
// Ensure that the image is set to it's correct pixel width and height
NSBitmapImageRep *bitimage = [[NSBitmapImageRep alloc] initWithData:data];
NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize([bitimage pixelsWide], [bitimage pixelsHigh])];
[image addRepresentation:bitimage];
return image;
#endif
return nil;
}
#pragma mark - NSSecureCoding
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
NSNumber *imageScale = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(imageScale))];
#if CGFLOAT_IS_DOUBLE
self.imageScale = [imageScale doubleValue];
#else
self.imageScale = [imageScale floatValue];
#endif
self.automaticallyInflatesResponseImage = [decoder decodeBoolForKey:NSStringFromSelector(@selector(automaticallyInflatesResponseImage))];
#endif
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
[coder encodeObject:@(self.imageScale) forKey:NSStringFromSelector(@selector(imageScale))];
[coder encodeBool:self.automaticallyInflatesResponseImage forKey:NSStringFromSelector(@selector(automaticallyInflatesResponseImage))];
#endif
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFImageResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
serializer.imageScale = self.imageScale;
serializer.automaticallyInflatesResponseImage = self.automaticallyInflatesResponseImage;
#endif
return serializer;
}
@end
#pragma mark -
@interface AFCompoundResponseSerializer ()
@property (readwrite, nonatomic, copy) NSArray *responseSerializers;
@end
@implementation AFCompoundResponseSerializer
+ (instancetype)compoundSerializerWithResponseSerializers:(NSArray *)responseSerializers {
AFCompoundResponseSerializer *serializer = [[self alloc] init];
serializer.responseSerializers = responseSerializers;
return serializer;
}
#pragma mark - AFURLResponseSerialization
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
for (id <AFURLResponseSerialization> serializer in self.responseSerializers) {
if (![serializer isKindOfClass:[AFHTTPResponseSerializer class]]) {
continue;
}
NSError *serializerError = nil;
id responseObject = [serializer responseObjectForResponse:response data:data error:&serializerError];
if (responseObject) {
if (error) {
*error = AFErrorWithUnderlyingError(serializerError, *error);
}
return responseObject;
}
}
return [super responseObjectForResponse:response data:data error:error];
}
#pragma mark - NSSecureCoding
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (!self) {
return nil;
}
self.responseSerializers = [decoder decodeObjectOfClass:[NSArray class] forKey:NSStringFromSelector(@selector(responseSerializers))];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
[coder encodeObject:self.responseSerializers forKey:NSStringFromSelector(@selector(responseSerializers))];
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
AFCompoundResponseSerializer *serializer = [[[self class] allocWithZone:zone] init];
serializer.responseSerializers = self.responseSerializers;
return serializer;
}
@end
@@ -0,0 +1,550 @@
// AFURLSessionManager.h
// Copyright (c) 20112015 Alamofire Software Foundation (http://alamofire.org/)
//
// 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.
#import <Foundation/Foundation.h>
#import "AFURLResponseSerialization.h"
#import "AFURLRequestSerialization.h"
#import "AFSecurityPolicy.h"
#import "AFNetworkReachabilityManager.h"
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif
/**
`AFURLSessionManager` creates and manages an `NSURLSession` object based on a specified `NSURLSessionConfiguration` object, which conforms to `<NSURLSessionTaskDelegate>`, `<NSURLSessionDataDelegate>`, `<NSURLSessionDownloadDelegate>`, and `<NSURLSessionDelegate>`.
## Subclassing Notes
This is the base class for `AFHTTPSessionManager`, which adds functionality specific to making HTTP requests. If you are looking to extend `AFURLSessionManager` specifically for HTTP, consider subclassing `AFHTTPSessionManager` instead.
## NSURLSession & NSURLSessionTask Delegate Methods
`AFURLSessionManager` implements the following delegate methods:
### `NSURLSessionDelegate`
- `URLSession:didBecomeInvalidWithError:`
- `URLSession:didReceiveChallenge:completionHandler:`
- `URLSessionDidFinishEventsForBackgroundURLSession:`
### `NSURLSessionTaskDelegate`
- `URLSession:willPerformHTTPRedirection:newRequest:completionHandler:`
- `URLSession:task:didReceiveChallenge:completionHandler:`
- `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`
- `URLSession:task:didCompleteWithError:`
### `NSURLSessionDataDelegate`
- `URLSession:dataTask:didReceiveResponse:completionHandler:`
- `URLSession:dataTask:didBecomeDownloadTask:`
- `URLSession:dataTask:didReceiveData:`
- `URLSession:dataTask:willCacheResponse:completionHandler:`
### `NSURLSessionDownloadDelegate`
- `URLSession:downloadTask:didFinishDownloadingToURL:`
- `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:`
- `URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:`
If any of these methods are overridden in a subclass, they _must_ call the `super` implementation first.
## Network Reachability Monitoring
Network reachability status and change monitoring is available through the `reachabilityManager` property. Applications may choose to monitor network reachability conditions in order to prevent or suspend any outbound requests. See `AFNetworkReachabilityManager` for more details.
## NSCoding Caveats
- Encoded managers do not include any block properties. Be sure to set delegate callback blocks when using `-initWithCoder:` or `NSKeyedUnarchiver`.
## NSCopying Caveats
- `-copy` and `-copyWithZone:` return a new manager with a new `NSURLSession` created from the configuration of the original.
- Operation copies do not include any delegate callback blocks, as they often strongly captures a reference to `self`, which would otherwise have the unintuitive side-effect of pointing to the _original_ session manager when copied.
@warning Managers for background sessions must be owned for the duration of their use. This can be accomplished by creating an application-wide or shared singleton instance.
*/
NS_ASSUME_NONNULL_BEGIN
#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090)
@interface AFURLSessionManager : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSSecureCoding, NSCopying>
/**
The managed session.
*/
@property (readonly, nonatomic, strong) NSURLSession *session;
/**
The operation queue on which delegate callbacks are run.
*/
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;
/**
Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to an instance of `AFJSONResponseSerializer`.
@warning `responseSerializer` must not be `nil`.
*/
@property (nonatomic, strong) id <AFURLResponseSerialization> responseSerializer;
///-------------------------------
/// @name Managing Security Policy
///-------------------------------
/**
The security policy used by created request operations to evaluate server trust for secure connections. `AFURLSessionManager` uses the `defaultPolicy` unless otherwise specified.
*/
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
///--------------------------------------
/// @name Monitoring Network Reachability
///--------------------------------------
/**
The network reachability manager. `AFURLSessionManager` uses the `sharedManager` by default.
*/
@property (readwrite, nonatomic, strong) AFNetworkReachabilityManager *reachabilityManager;
///----------------------------
/// @name Getting Session Tasks
///----------------------------
/**
The data, upload, and download tasks currently run by the managed session.
*/
@property (readonly, nonatomic, strong) NSArray *tasks;
/**
The data tasks currently run by the managed session.
*/
@property (readonly, nonatomic, strong) NSArray *dataTasks;
/**
The upload tasks currently run by the managed session.
*/
@property (readonly, nonatomic, strong) NSArray *uploadTasks;
/**
The download tasks currently run by the managed session.
*/
@property (readonly, nonatomic, strong) NSArray *downloadTasks;
///-------------------------------
/// @name Managing Callback Queues
///-------------------------------
/**
The dispatch queue for `completionBlock`. If `NULL` (default), the main queue is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;
#else
@property (nonatomic, assign, nullable) dispatch_queue_t completionQueue;
#endif
/**
The dispatch group for `completionBlock`. If `NULL` (default), a private dispatch group is used.
*/
#if OS_OBJECT_HAVE_OBJC_SUPPORT
@property (nonatomic, strong, nullable) dispatch_group_t completionGroup;
#else
@property (nonatomic, assign, nullable) dispatch_group_t completionGroup;
#endif
///---------------------------------
/// @name Working Around System Bugs
///---------------------------------
/**
Whether to attempt to retry creation of upload tasks for background sessions when initial call returns `nil`. `NO` by default.
@bug As of iOS 7.0, there is a bug where upload tasks created for background tasks are sometimes `nil`. As a workaround, if this property is `YES`, AFNetworking will follow Apple's recommendation to try creating the task again.
@see https://github.com/AFNetworking/AFNetworking/issues/1675
*/
@property (nonatomic, assign) BOOL attemptsToRecreateUploadTasksForBackgroundSessions;
///---------------------
/// @name Initialization
///---------------------
/**
Creates and returns a manager for a session created with the specified configuration. This is the designated initializer.
@param configuration The configuration used to create the managed session.
@return A manager for a newly-created session.
*/
- (instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
/**
Invalidates the managed session, optionally canceling pending tasks.
@param cancelPendingTasks Whether or not to cancel pending tasks.
*/
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks;
///-------------------------
/// @name Running Data Tasks
///-------------------------
/**
Creates an `NSURLSessionDataTask` with the specified request.
@param request The HTTP request for the request.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(nullable void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
///---------------------------
/// @name Running Upload Tasks
///---------------------------
/**
Creates an `NSURLSessionUploadTask` with the specified request for a local file.
@param request The HTTP request for the request.
@param fileURL A URL to the local file to be uploaded.
@param progress A progress object monitoring the current upload progress.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
@see `attemptsToRecreateUploadTasksForBackgroundSessions`
*/
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromFile:(NSURL *)fileURL
progress:(NSProgress * __nullable __autoreleasing * __nullable)progress
completionHandler:(nullable void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
/**
Creates an `NSURLSessionUploadTask` with the specified request for an HTTP body.
@param request The HTTP request for the request.
@param bodyData A data object containing the HTTP body to be uploaded.
@param progress A progress object monitoring the current upload progress.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromData:(nullable NSData *)bodyData
progress:(NSProgress * __nullable __autoreleasing * __nullable)progress
completionHandler:(nullable void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
/**
Creates an `NSURLSessionUploadTask` with the specified streaming request.
@param request The HTTP request for the request.
@param progress A progress object monitoring the current upload progress.
@param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any.
*/
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request
progress:(NSProgress * __nullable __autoreleasing * __nullable)progress
completionHandler:(nullable void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler;
///-----------------------------
/// @name Running Download Tasks
///-----------------------------
/**
Creates an `NSURLSessionDownloadTask` with the specified request.
@param request The HTTP request for the request.
@param progress A progress object monitoring the current download progress.
@param destination A block object to be executed in order to determine the destination of the downloaded file. This block takes two arguments, the target path & the server response, and returns the desired file URL of the resulting download. The temporary file used during the download will be automatically deleted after being moved to the returned URL.
@param completionHandler A block to be executed when a task finishes. This block has no return value and takes three arguments: the server response, the path of the downloaded file, and the error describing the network or parsing error that occurred, if any.
@warning If using a background `NSURLSessionConfiguration` on iOS, these blocks will be lost when the app is terminated. Background sessions may prefer to use `-setDownloadTaskDidFinishDownloadingBlock:` to specify the URL for saving the downloaded file, rather than the destination block of this method.
*/
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
progress:(NSProgress * __nullable __autoreleasing * __nullable)progress
destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
completionHandler:(nullable void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;
/**
Creates an `NSURLSessionDownloadTask` with the specified resume data.
@param resumeData The data used to resume downloading.
@param progress A progress object monitoring the current download progress.
@param destination A block object to be executed in order to determine the destination of the downloaded file. This block takes two arguments, the target path & the server response, and returns the desired file URL of the resulting download. The temporary file used during the download will be automatically deleted after being moved to the returned URL.
@param completionHandler A block to be executed when a task finishes. This block has no return value and takes three arguments: the server response, the path of the downloaded file, and the error describing the network or parsing error that occurred, if any.
*/
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
progress:(NSProgress * __nullable __autoreleasing * __nullable)progress
destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
completionHandler:(nullable void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;
///---------------------------------
/// @name Getting Progress for Tasks
///---------------------------------
/**
Returns the upload progress of the specified task.
@param uploadTask The session upload task. Must not be `nil`.
@return An `NSProgress` object reporting the upload progress of a task, or `nil` if the progress is unavailable.
*/
- (nullable NSProgress *)uploadProgressForTask:(NSURLSessionUploadTask *)uploadTask;
/**
Returns the download progress of the specified task.
@param downloadTask The session download task. Must not be `nil`.
@return An `NSProgress` object reporting the download progress of a task, or `nil` if the progress is unavailable.
*/
- (nullable NSProgress *)downloadProgressForTask:(NSURLSessionDownloadTask *)downloadTask;
///-----------------------------------------
/// @name Setting Session Delegate Callbacks
///-----------------------------------------
/**
Sets a block to be executed when the managed session becomes invalid, as handled by the `NSURLSessionDelegate` method `URLSession:didBecomeInvalidWithError:`.
@param block A block object to be executed when the managed session becomes invalid. The block has no return value, and takes two arguments: the session, and the error related to the cause of invalidation.
*/
- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block;
/**
Sets a block to be executed when a connection level authentication challenge has occurred, as handled by the `NSURLSessionDelegate` method `URLSession:didReceiveChallenge:completionHandler:`.
@param block A block object to be executed when a connection level authentication challenge has occurred. The block returns the disposition of the authentication challenge, and takes three arguments: the session, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge.
*/
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __nullable __autoreleasing * __nullable credential))block;
///--------------------------------------
/// @name Setting Task Delegate Callbacks
///--------------------------------------
/**
Sets a block to be executed when a task requires a new request body stream to send to the remote server, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:needNewBodyStream:`.
@param block A block object to be executed when a task requires a new request body stream.
*/
- (void)setTaskNeedNewBodyStreamBlock:(nullable NSInputStream * (^)(NSURLSession *session, NSURLSessionTask *task))block;
/**
Sets a block to be executed when an HTTP request is attempting to perform a redirection to a different URL, as handled by the `NSURLSessionTaskDelegate` method `URLSession:willPerformHTTPRedirection:newRequest:completionHandler:`.
@param block A block object to be executed when an HTTP request is attempting to perform a redirection to a different URL. The block returns the request to be made for the redirection, and takes four arguments: the session, the task, the redirection response, and the request corresponding to the redirection response.
*/
- (void)setTaskWillPerformHTTPRedirectionBlock:(nullable NSURLRequest * (^)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request))block;
/**
Sets a block to be executed when a session task has received a request specific authentication challenge, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didReceiveChallenge:completionHandler:`.
@param block A block object to be executed when a session task has received a request specific authentication challenge. The block returns the disposition of the authentication challenge, and takes four arguments: the session, the task, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge.
*/
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __nullable __autoreleasing * __nullable credential))block;
/**
Sets a block to be executed periodically to track upload progress, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`.
@param block A block object to be called when an undetermined number of bytes have been uploaded to the server. This block has no return value and takes five arguments: the session, the task, the number of bytes written since the last time the upload progress block was called, the total bytes written, and the total bytes expected to be written during the request, as initially determined by the length of the HTTP body. This block may be called multiple times, and will execute on the main thread.
*/
- (void)setTaskDidSendBodyDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend))block;
/**
Sets a block to be executed as the last message related to a specific task, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didCompleteWithError:`.
@param block A block object to be executed when a session task is completed. The block has no return value, and takes three arguments: the session, the task, and any error that occurred in the process of executing the task.
*/
- (void)setTaskDidCompleteBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, NSError *error))block;
///-------------------------------------------
/// @name Setting Data Task Delegate Callbacks
///-------------------------------------------
/**
Sets a block to be executed when a data task has received a response, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didReceiveResponse:completionHandler:`.
@param block A block object to be executed when a data task has received a response. The block returns the disposition of the session response, and takes three arguments: the session, the data task, and the received response.
*/
- (void)setDataTaskDidReceiveResponseBlock:(nullable NSURLSessionResponseDisposition (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response))block;
/**
Sets a block to be executed when a data task has become a download task, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didBecomeDownloadTask:`.
@param block A block object to be executed when a data task has become a download task. The block has no return value, and takes three arguments: the session, the data task, and the download task it has become.
*/
- (void)setDataTaskDidBecomeDownloadTaskBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask))block;
/**
Sets a block to be executed when a data task receives data, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didReceiveData:`.
@param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes three arguments: the session, the data task, and the data received. This block may be called multiple times, and will execute on the session manager operation queue.
*/
- (void)setDataTaskDidReceiveDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data))block;
/**
Sets a block to be executed to determine the caching behavior of a data task, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:willCacheResponse:completionHandler:`.
@param block A block object to be executed to determine the caching behavior of a data task. The block returns the response to cache, and takes three arguments: the session, the data task, and the proposed cached URL response.
*/
- (void)setDataTaskWillCacheResponseBlock:(nullable NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block;
/**
Sets a block to be executed once all messages enqueued for a session have been delivered, as handled by the `NSURLSessionDataDelegate` method `URLSessionDidFinishEventsForBackgroundURLSession:`.
@param block A block object to be executed once all messages enqueued for a session have been delivered. The block has no return value and takes a single argument: the session.
*/
- (void)setDidFinishEventsForBackgroundURLSessionBlock:(nullable void (^)(NSURLSession *session))block;
///-----------------------------------------------
/// @name Setting Download Task Delegate Callbacks
///-----------------------------------------------
/**
Sets a block to be executed when a download task has completed a download, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didFinishDownloadingToURL:`.
@param block A block object to be executed when a download task has completed. The block returns the URL the download should be moved to, and takes three arguments: the session, the download task, and the temporary location of the downloaded file. If the file manager encounters an error while attempting to move the temporary file to the destination, an `AFURLSessionDownloadTaskDidFailToMoveFileNotification` will be posted, with the download task as its object, and the user info of the error.
*/
- (void)setDownloadTaskDidFinishDownloadingBlock:(nullable NSURL * (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location))block;
/**
Sets a block to be executed periodically to track download progress, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:`.
@param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes five arguments: the session, the download task, the number of bytes read since the last time the download progress block was called, the total bytes read, and the total bytes expected to be read during the request, as initially determined by the expected content size of the `NSHTTPURLResponse` object. This block may be called multiple times, and will execute on the session manager operation queue.
*/
- (void)setDownloadTaskDidWriteDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite))block;
/**
Sets a block to be executed when a download task has been resumed, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:`.
@param block A block object to be executed when a download task has been resumed. The block has no return value and takes four arguments: the session, the download task, the file offset of the resumed download, and the total number of bytes expected to be downloaded.
*/
- (void)setDownloadTaskDidResumeBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes))block;
@end
#endif
///--------------------
/// @name Notifications
///--------------------
/**
Posted when a task begins executing.
@deprecated Use `AFNetworkingTaskDidResumeNotification` instead.
*/
extern NSString * const AFNetworkingTaskDidStartNotification DEPRECATED_ATTRIBUTE;
/**
Posted when a task resumes.
*/
extern NSString * const AFNetworkingTaskDidResumeNotification;
/**
Posted when a task finishes executing. Includes a userInfo dictionary with additional information about the task.
@deprecated Use `AFNetworkingTaskDidCompleteNotification` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishNotification DEPRECATED_ATTRIBUTE;
/**
Posted when a task finishes executing. Includes a userInfo dictionary with additional information about the task.
*/
extern NSString * const AFNetworkingTaskDidCompleteNotification;
/**
Posted when a task suspends its execution.
*/
extern NSString * const AFNetworkingTaskDidSuspendNotification;
/**
Posted when a session is invalidated.
*/
extern NSString * const AFURLSessionDidInvalidateNotification;
/**
Posted when a session download task encountered an error when moving the temporary download file to a specified destination.
*/
extern NSString * const AFURLSessionDownloadTaskDidFailToMoveFileNotification;
/**
The raw response data of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if response data exists for the task.
@deprecated Use `AFNetworkingTaskDidCompleteResponseDataKey` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishResponseDataKey DEPRECATED_ATTRIBUTE;
/**
The raw response data of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if response data exists for the task.
*/
extern NSString * const AFNetworkingTaskDidCompleteResponseDataKey;
/**
The serialized response object of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if the response was serialized.
@deprecated Use `AFNetworkingTaskDidCompleteSerializedResponseKey` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishSerializedResponseKey DEPRECATED_ATTRIBUTE;
/**
The serialized response object of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if the response was serialized.
*/
extern NSString * const AFNetworkingTaskDidCompleteSerializedResponseKey;
/**
The response serializer used to serialize the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if the task has an associated response serializer.
@deprecated Use `AFNetworkingTaskDidCompleteResponseSerializerKey` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishResponseSerializerKey DEPRECATED_ATTRIBUTE;
/**
The response serializer used to serialize the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if the task has an associated response serializer.
*/
extern NSString * const AFNetworkingTaskDidCompleteResponseSerializerKey;
/**
The file path associated with the download task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if an the response data has been stored directly to disk.
@deprecated Use `AFNetworkingTaskDidCompleteAssetPathKey` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishAssetPathKey DEPRECATED_ATTRIBUTE;
/**
The file path associated with the download task. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if an the response data has been stored directly to disk.
*/
extern NSString * const AFNetworkingTaskDidCompleteAssetPathKey;
/**
Any error associated with the task, or the serialization of the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if an error exists.
@deprecated Use `AFNetworkingTaskDidCompleteErrorKey` instead.
*/
extern NSString * const AFNetworkingTaskDidFinishErrorKey DEPRECATED_ATTRIBUTE;
/**
Any error associated with the task, or the serialization of the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidFinishNotification` if an error exists.
*/
extern NSString * const AFNetworkingTaskDidCompleteErrorKey;
NS_ASSUME_NONNULL_END
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
//
// GameThumbnailCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@class RBXGameData;
@class RobloxImageView;
@interface CarouselThumbnailCell : UICollectionViewCell
-(void) setGameData:(RBXGameData*)data;
@end
@@ -0,0 +1,75 @@
//
// GameThumbnailCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "CarouselThumbnailCell.h"
#import "RobloxImageView.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#define THUMBNAIL_SIZE RBX_SCALED_DEVICE_SIZE(455, 256)
#define VIEWCELL_CORNER_RADIUS 5.0
@interface CarouselThumbnailCell()
@end
@implementation CarouselThumbnailCell
{
IBOutlet RobloxImageView *_image;
IBOutlet UILabel *_title;
IBOutlet UIView *_titleBackground;
IBOutlet UIActivityIndicatorView *_activityIndicator;
NSString* _placeID;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self setTitleStyle];
}
- (void)setTitleStyle
{
_title.backgroundColor = [UIColor clearColor];
[RobloxTheme applyToCarouselThumbnailTitle:_title];
_titleBackground.layer.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.95].CGColor;
_titleBackground.layer.cornerRadius = VIEWCELL_CORNER_RADIUS - 1.0;
_titleBackground.layer.masksToBounds = NO;
_titleBackground.layer.shouldRasterize = NO;
_titleBackground.frame = CGRectIntegral(_titleBackground.frame);
_titleBackground.frame = CGRectIntegral(_titleBackground.frame);
}
-(void)setGameData:(RBXGameData*)data
{
if(_placeID == nil || ![_placeID isEqualToString:data.placeID])
{
_placeID = data.placeID;
[_activityIndicator startAnimating];
_image.hidden = YES;
_image.animateInOptions = RBXImageViewAnimateInAlways;
_title.text = data.title;
self.layer.cornerRadius = VIEWCELL_CORNER_RADIUS;
UIActivityIndicatorView* indicator = _activityIndicator;
RobloxImageView* image = _image;
[_image loadThumbnailForGame:data withSize:THUMBNAIL_SIZE completion:^
{
[indicator stopAnimating];
image.hidden = NO;
}];
}
}
@end
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5056" systemVersion="13D65" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment defaultVersion="1552" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell multipleTouchEnabled="YES" contentMode="center" id="UyC-rE-64E" customClass="CarouselThumbnailCell">
<rect key="frame" x="0.0" y="0.0" width="455" height="256"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="455" height="256"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zbS-gq-Dcy" customClass="RobloxImageView">
<rect key="frame" x="0.0" y="0.0" width="455" height="256"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</imageView>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" hidesWhenStopped="YES" animating="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="5BC-sU-pQe">
<rect key="frame" x="217" y="118" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<view opaque="NO" alpha="0.85000000000000009" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lFi-jK-nqA">
<rect key="frame" x="0.0" y="216" width="454" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="__GAME_TITLE__" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oTO-bq-MpL">
<rect key="frame" x="20" y="0.0" width="414" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="customSize" width="461" height="151"/>
<connections>
<outlet property="_activityIndicator" destination="5BC-sU-pQe" id="sXv-eK-1Gg"/>
<outlet property="_image" destination="zbS-gq-Dcy" id="QtM-I4-sH0"/>
<outlet property="_title" destination="oTO-bq-MpL" id="WSu-fm-kh4"/>
<outlet property="_titleBackground" destination="lFi-jK-nqA" id="W1v-zz-mQI"/>
</connections>
</collectionViewCell>
</objects>
</document>
@@ -0,0 +1,15 @@
//
// ExtendedSegmentedControl.h
// RobloxMobile
//
// Created by Ariel Lichtin on 6/5/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ExtendedSegmentedControl : UISegmentedControl
@property NSInteger maxVisibleItems;
@end
@@ -0,0 +1,170 @@
//
// ExtendedSegmentedControl.m
// RobloxMobile
//
// Created by Ariel Lichtin on 6/5/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "ExtendedSegmentedControl.h"
#import <UIKit/UIPopoverController.h>
#define CELL_SIZE CGSizeMake(320.0f, 44.0f)
//-----------------------------------------------------------------------------------------------------------------------------
@interface ExtendedSegmentedControlCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *title;
@property (weak, nonatomic) IBOutlet UIImageView *checkImage;
@end
@implementation ExtendedSegmentedControlCell
@end
//-----------------------------------------------------------------------------------------------------------------------------
@interface ExtendedSegmentedControl () <UITableViewDelegate, UITableViewDataSource>
@end
@implementation ExtendedSegmentedControl
{
NSMutableArray* _elements;
NSInteger _extendedSelectedIndex;
UIViewController* _tableController;
UITableView* _tableView;
UIPopoverController* _popOver;
}
- (void)awakeFromNib
{
[super awakeFromNib];
_elements = [NSMutableArray array];
_tableView = [[UITableView alloc] init];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.scrollEnabled = NO;
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[_tableView registerNib:[UINib nibWithNibName:@"ExtendedSegmentedControlCell" bundle:nil] forCellReuseIdentifier:@"ReuseCell"];
_tableController = [[UIViewController alloc] init];
[_tableController.view addSubview:_tableView];
}
-(void) showPopOver
{
if(!_popOver.isPopoverVisible)
{
[_tableView reloadData];
CGRect fromRect = self.bounds;
float itemWidth = fromRect.size.width / (float)self.numberOfSegments;
fromRect.origin.x = fromRect.size.width - itemWidth;
fromRect.size.width = itemWidth;
CGRect tableRect;
tableRect.origin = CGPointZero;
tableRect.size = CELL_SIZE;
tableRect.size.height *= [_tableView numberOfRowsInSection:0];
_tableView.frame = tableRect;
_tableController.preferredContentSize = tableRect.size;
//_tableController.contentSizeForViewInPopover = tableRect.size;
_popOver = [[UIPopoverController alloc] initWithContentViewController:_tableController];
[_popOver presentPopoverFromRect:fromRect inView:self permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
}
}
- (void)insertSegmentWithTitle:(NSString *)title atIndex:(NSUInteger)segment animated:(BOOL)animated
{
[_elements addObject:title];
if(self.maxVisibleItems > 0 && _elements.count > self.maxVisibleItems)
{
[self setTitle:NSLocalizedString(@"MoreWord", nil) forSegmentAtIndex:(self.maxVisibleItems - 1)];
}
else
{
[super insertSegmentWithTitle:title atIndex:segment animated:animated];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSInteger previousSelectedSegmentIndex = [super selectedSegmentIndex];
[super touchesEnded:touches withEvent:event];
if(_elements.count > self.maxVisibleItems && [super selectedSegmentIndex] == self.maxVisibleItems-1)
{
if(previousSelectedSegmentIndex == [super selectedSegmentIndex])
[self showPopOver];
else
[super setSelectedSegmentIndex:previousSelectedSegmentIndex];
}
}
- (NSInteger)selectedSegmentIndex
{
return _extendedSelectedIndex;
}
- (void)setSelectedSegmentIndex:(NSInteger)selectedSegmentIndex
{
_extendedSelectedIndex = selectedSegmentIndex;
if(_elements.count > self.maxVisibleItems && selectedSegmentIndex >= self.maxVisibleItems)
{
[super setSelectedSegmentIndex:self.maxVisibleItems-1];
[_tableView reloadData];
}
else
[super setSelectedSegmentIndex:selectedSegmentIndex];
}
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents
{
if(controlEvents == UIControlEventValueChanged)
{
if(_elements.count > self.maxVisibleItems && [super selectedSegmentIndex] == self.maxVisibleItems-1)
{
[self showPopOver];
}
else
{
_extendedSelectedIndex = [super selectedSegmentIndex];
}
}
[super sendActionsForControlEvents:controlEvents];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return MAX(0, (_elements.count - self.maxVisibleItems));
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ExtendedSegmentedControlCell* cell = [tableView dequeueReusableCellWithIdentifier:@"ReuseCell" forIndexPath:indexPath];
cell.title.text = _elements[self.maxVisibleItems + indexPath.row];
cell.checkImage.hidden = (_extendedSelectedIndex - self.maxVisibleItems) != indexPath.row;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
_extendedSelectedIndex = indexPath.row + self.maxVisibleItems;
[super setSelectedSegmentIndex:(self.maxVisibleItems - 1)];
[self sendActionsForControlEvents:UIControlEventValueChanged];
[_popOver dismissPopoverAnimated:YES];
}
@end
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5056" systemVersion="13D65" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment defaultVersion="1536" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="H3i-QG-flo" customClass="ExtendedSegmentedControlCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="H3i-QG-flo" id="Byq-gH-7H4">
<rect key="frame" x="0.0" y="0.0" width="320" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ez9-UX-Ob4">
<rect key="frame" x="20" y="11" width="253" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="Check.png" translatesAutoresizingMaskIntoConstraints="NO" id="M4I-a6-1N8">
<rect key="frame" x="281" y="11" width="35" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="checkImage" destination="M4I-a6-1N8" id="phT-sd-FdG"/>
<outlet property="title" destination="ez9-UX-Ob4" id="QrN-zJ-JsR"/>
</connections>
</tableViewCell>
</objects>
<resources>
<image name="Check.png" width="15" height="12"/>
</resources>
</document>
@@ -0,0 +1,818 @@
//
// Flurry.h
// Flurry iOS Analytics Agent
//
// Copyright 2009-2012 Flurry, Inc. All rights reserved.
//
// Methods in this header file are for use with Flurry Analytics
#import <UIKit/UIKit.h>
/*!
* @brief Provides all available methods for defining and reporting Analytics from use
* of your app.
*
* Set of methods that allow developers to capture detailed, aggregate information
* regarding the use of their app by end users.
*
* @note This class provides methods necessary for correct function of FlurryAds.h.
* For information on how to use Flurry's Ads SDK to
* attract high-quality users and monetize your user base see <a href="http://support.flurry.com/index.php?title=Publishers">Support Center - Publishers</a>.
*
* @author 2009 - 2013 Flurry, Inc. All Rights Reserved.
* @version 4.3.0
*
*/
/*!
* @brief Enum for setting up log output level.
* @since 4.2.0
*
*/
typedef enum {
FlurryLogLevelNone = 0, //No output
FlurryLogLevelCriticalOnly, //Default, outputs only critical log events
FlurryLogLevelDebug, //Debug level, outputs critical and main log events
FlurryLogLevelAll //Highest level, outputs all log events
} FlurryLogLevel;
@interface Flurry : NSObject {
}
/** @name Pre-Session Calls
* Optional sdk settings that should be called before start session.
*/
//@{
/*!
* @brief Explicitly specifies the App Version that Flurry will use to group Analytics data.
* @since 2.7
*
* This is an optional method that overrides the App Version Flurry uses for reporting. Flurry will
* use the CFBundleVersion in your info.plist file when this method is not invoked.
*
* @note There is a maximum of 605 versions allowed for a single app. \n
* This method must be called prior to invoking #startSession:.
*
* @param version The custom version name.
*/
+ (void)setAppVersion:(NSString *)version;
/*!
* @brief Retrieves the Flurry Agent Build Version.
* @since 2.7
*
* This is an optional method that retrieves the Flurry Agent Version the app is running under.
* It is most often used if reporting an unexpected behavior of the SDK to <a href="mailto:iphonesupport@flurry.com">
* Flurry Support</a>
*
* @note This method must be called prior to invoking #startSession:. \n
* FAQ for the iPhone SDK is located at <a href="http://wiki.flurry.com/index.php?title=IPhone_FAQ">
* Support Center - iPhone FAQ</a>.
*
* @see #setLogLevel: for information on how to view debugging information on your console.
*
* @return The agent version of the Flurry SDK.
*
*/
+ (NSString *)getFlurryAgentVersion;
/*!
* @brief Displays an exception in the debug log if thrown during a Session.
* @since 2.7
*
* This is an optional method that augments the debug logs with exceptions that occur during the session.
* You must both capture exceptions to Flurry and set debug logging to enabled for this method to
* display information to the console. The default setting for this method is @c NO.
*
* @note This method must be called prior to invoking #startSession:.
*
* @see #setLogLevel: for information on how to view debugging information on your console. \n
* #logError:message:exception: for details on logging exceptions. \n
* #logError:message:error: for details on logging errors.
*
* @param value @c YES to show errors in debug logs, @c NO to omit errors in debug logs.
*/
+ (void)setShowErrorInLogEnabled:(BOOL)value;
/*!
* @brief Generates debug logs to console.
* @since 2.7
*
* This is an optional method that displays debug information related to the Flurry SDK.
* display information to the console. The default setting for this method is @c NO
* which sets the log level to @c FlurryLogLevelCriticalOnly.
* When set to @c YES the debug log level is set to @c FlurryLogLevelDebug
*
* @note This method must be called prior to invoking #startSession:. If the method, setLogLevel is called later in the code, debug logging will be automatically enabled.
*
* @param value @c YES to show debug logs, @c NO to omit debug logs.
*
*/
+ (void)setDebugLogEnabled:(BOOL)value;
/*!
* @brief Generates debug logs to console.
* @since 4.2.2
*
* This is an optional method that displays debug information related to the Flurry SDK.
* display information to the console. The default setting for this method is @c FlurryLogLevelCritycalOnly.
*
* @note Its good practice to call this method prior to invoking #startSession:. If debug logging is disabled earlier, this method will enable it.
*
* @param value Log level
*
*/
+ (void)setLogLevel:(FlurryLogLevel)value;
/*!
* @brief Set the timeout for expiring a Flurry session.
* @since 2.7
*
* This is an optional method that sets the time the app may be in the background before
* starting a new session upon resume. The default value for the session timeout is 10
* seconds in the background.
*
* @note This method must be called prior to invoking #startSession:.
*
* @param seconds The time in seconds to set the session timeout to.
*/
+ (void)setSessionContinueSeconds:(int)seconds;
/*!
* @brief Send data over a secure transport.
* @since 3.0
*
* This is an optional method that sends data over an SSL connection when enabled. The
* default value is @c NO.
*
* @note This method must be called prior to invoking #startSession:.
*
* @param value @c YES to send data over secure connection.
*/
+ (void)setSecureTransportEnabled:(BOOL)value;
/*!
* @brief Enable automatic collection of crash reports.
* @since 4.1
*
* This is an optional method that collects crash reports when enabled. The
* default value is @c NO.
*
* @note This method must be called prior to invoking #startSession:.
*
* @param value @c YES to enable collection of crash reports.
*/
+ (void)setCrashReportingEnabled:(BOOL)value;
//@}
/*!
* @brief Start a Flurry session for the project denoted by @c apiKey.
* @since 2.6
*
* This method serves as the entry point to Flurry Analytics collection. It must be
* called in the scope of @c applicationDidFinishLaunching. The session will continue
* for the period the app is in the foreground until your app is backgrounded for the
* time specified in #setSessionContinueSeconds:. If the app is resumed in that period
* the session will continue, otherwise a new session will begin.
*
* Crash reporting will not be enabled. See #setCrashReportingEnabled: for
* more information.
*
* @note If testing on a simulator, please be sure to send App to background via home
* button. Flurry depends on the iOS lifecycle to be complete for full reporting.
*
* @see #setSessionContinueSeconds: for details on setting a custom session timeout.
*
* @code
* - (void)applicationDidFinishLaunching:(UIApplication *)application
{
// Optional Flurry startup methods
[Flurry startSession:@"YOUR_API_KEY"];
// ....
}
* @endcode
*
* @param apiKey The API key for this project.
*/
+ (void)startSession:(NSString *)apiKey;
/*!
* @brief Start a Flurry session for the project denoted by @c apiKey.
* @since 4.0.8
*
* This method serves as the entry point to Flurry Analytics collection. It must be
* called in the scope of @c applicationDidFinishLaunching passing in the launchOptions param.
* The session will continue
* for the period the app is in the foreground until your app is backgrounded for the
* time specified in #setSessionContinueSeconds:. If the app is resumed in that period
* the session will continue, otherwise a new session will begin.
*
* @note If testing on a simulator, please be sure to send App to background via home
* button. Flurry depends on the iOS lifecycle to be complete for full reporting.
*
* @see #setSessionContinueSeconds: for details on setting a custom session timeout.
*
* @code
* - (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Optional Flurry startup methods
[Flurry startSession:@"YOUR_API_KEY" withOptions:launchOptions];
// ....
}
* @endcode
*
* @param apiKey The API key for this project.
* @param options passed launchOptions from the applicatin's didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
*/
+ (void) startSession:(NSString *)apiKey withOptions:(id)options;
/*!
* @brief Pauses a Flurry session left running in background.
* @since 4.2.2
*
* This method should be used in case of #setBackgroundSessionEnabled: set to YES. It can be
* called when application finished all background tasks (such as playing music) to pause session.
*
* @see #setBackgroundSessionEnabled: for details on setting a custom behaviour on resigning activity.
*
* @code
* - (void)allBackgroundTasksFinished
{
// ....
[Flurry pauseBackgroundSession];
// ....
}
* @endcode
*
*/
+ (void)pauseBackgroundSession;
/*!
* @brief Adds an SDK origin specified by @c originName and @c originVersion.
* @since 5.0.0
*
* This method allows you to specify origin within your Flurry SDK wrapper. As a general rule
* you should capture all the origin info related to your wrapper for Flurry SDK after every session start.
*
* @see #addOrigin:withVersion:withParameters: for details on reporting origin info with parameters. \n
*
* @code
* - (void)interestingSDKWrapperLibraryfunction
{
// ... after calling startSession
[Flurry addOrigin:@"Interesting_Wrapper" withVersion:@"1.0.0"];
// more code ...
}
* @endcode
*
* @param originName Name of the origin.
* @param originVersion Version string of the origin wrapper
*/
+ (void)addOrigin:(NSString *)originName withVersion:(NSString*)originVersion;
/*!
* @brief Adds a custom parameterized origin specified by @c originName with @c originVersion and @c parameters.
* @since 5.0.0
*
* This method overrides #addOrigin to allow you to associate parameters with an origin attribute. Parameters
* are valuable as they allow you to store characteristics of an origin.
*
* @note You should not pass private or confidential information about your origin info in a
* custom origin. \n
* A maximum of 9 parameter names may be associated with any origin. Sending
* over 10 parameter names with a single origin will result in no parameters being logged
* for that origin.
*
*
* @code
* - (void)userPurchasedSomethingCool
{
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"Origin Info Item", // Parameter Value
@"Origin Info Item Key", // Parameter Name
nil];
// ... after calling startSession
[Flurry addOrigin:@"Interesting_Wrapper" withVersion:@"1.0.0"];
// more code ...
}
* @endcode
*
* @param originName Name of the origin.
* @param originVersion Version string of the origin wrapper
* @param parameters An immutable copy of map containing Name-Value pairs of parameters.
*/
+ (void)addOrigin:(NSString *)originName withVersion:(NSString*)originVersion withParameters:(NSDictionary *)parameters;
/** @name Event and Error Logging
* Methods for reporting custom events and errors during the session.
*/
//@{
/*!
* @brief Records a custom event specified by @c eventName.
* @since 2.8.4
*
* This method allows you to specify custom events within your app. As a general rule
* you should capture events related to user navigation within your app, any action
* around monetization, and other events as they are applicable to tracking progress
* towards your business goals.
*
* @note You should not pass private or confidential information about your users in a
* custom event. \n
* Where applicable, you should make a concerted effort to use timed events with
* parameters (#logEvent:withParameters:timed:) or events with parameters
* (#logEvent:withParameters:). This provides valuable information around the time the user
* spends within an action (e.g. - time spent on a level or viewing a page) or characteristics
* of an action (e.g. - Buy Event that has a Parameter of Widget with Value Golden Sword).
*
* @see #logEvent:withParameters: for details on storing events with parameters. \n
* #logEvent:timed: for details on storing timed events. \n
* #logEvent:withParameters:timed: for details on storing timed events with parameters. \n
* #endTimedEvent:withParameters: for details on stopping a timed event and (optionally) updating
* parameters.
*
* @code
* - (void)interestingAppAction
{
[Flurry logEvent:@"Interesting_Action"];
// Perform interesting action
}
* @endcode
*
* @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme
* that can be easily understood by non-technical people in your business domain.
*/
+ (void)logEvent:(NSString *)eventName;
/*!
* @brief Records a custom parameterized event specified by @c eventName with @c parameters.
* @since 2.8.4
*
* This method overrides #logEvent to allow you to associate parameters with an event. Parameters
* are extremely valuable as they allow you to store characteristics of an action. For example,
* if a user purchased an item it may be helpful to know what level that user was on.
* By setting this parameter you will be able to view a distribution of levels for the purcahsed
* event on the <a href="http://dev.flurry.com">Flurrly Dev Portal</a>.
*
* @note You should not pass private or confidential information about your users in a
* custom event. \n
* A maximum of 10 parameter names may be associated with any event. Sending
* over 10 parameter names with a single event will result in no parameters being logged
* for that event. You may specify an infinite number of Parameter values. For example,
* a Search Box would have 1 parameter name (e.g. - Search Box) and many values, which would
* allow you to see what values users look for the most in your app. \n
* Where applicable, you should make a concerted effort to use timed events with
* parameters (#logEvent:withParameters:timed:). This provides valuable information
* around the time the user spends within an action (e.g. - time spent on a level or
* viewing a page).
*
* @see #logEvent:withParameters:timed: for details on storing timed events with parameters. \n
* #endTimedEvent:withParameters: for details on stopping a timed event and (optionally) updating
* parameters.
*
* @code
* - (void)userPurchasedSomethingCool
{
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"Cool Item", // Parameter Value
@"Item Purchased", // Parameter Name
nil];
[Flurry logEvent:@"Something Cool Purchased" withParameters:params];
// Give user cool item
}
* @endcode
*
* @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme
* that can be easily understood by non-technical people in your business domain.
* @param parameters An immutable copy of map containing Name-Value pairs of parameters.
*/
+ (void)logEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters;
/*!
* @brief Records an app exception. Commonly used to catch unhandled exceptions.
* @since 2.7
*
* This method captures an exception for reporting to Flurry. We recommend adding an uncaught
* exception listener to capture any exceptions that occur during usage that is not
* anticipated by your app.
*
* @see #logError:message:error: for details on capturing errors.
*
* @code
* - (void) uncaughtExceptionHandler(NSException *exception)
{
[Flurry logError:@"Uncaught" message:@"Crash!" exception:exception];
}
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
[Flurry startSession:@"YOUR_API_KEY"];
// ....
}
* @endcode
*
* @param errorID Name of the error.
* @param message The message to associate with the error.
* @param exception The exception object to report.
*/
+ (void)logError:(NSString *)errorID message:(NSString *)message exception:(NSException *)exception;
/*!
* @brief Records an app error.
* @since 2.7
*
* This method captures an error for reporting to Flurry.
*
* @see #logError:message:exception: for details on capturing exceptions.
*
* @code
* - (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
[Flurry logError:@"WebView No Load" message:[error localizedDescription] error:error];
}
* @endcode
*
* @param errorID Name of the error.
* @param message The message to associate with the error.
* @param error The error object to report.
*/
+ (void)logError:(NSString *)errorID message:(NSString *)message error:(NSError *)error;
/*!
* @brief Records a timed event specified by @c eventName.
* @since 2.8.4
*
* This method overrides #logEvent to allow you to capture the length of an event. This can
* be extremely valuable to understand the level of engagement with a particular action. For
* example, you can capture how long a user spends on a level or reading an article.
*
* @note You should not pass private or confidential information about your users in a
* custom event. \n
* Where applicable, you should make a concerted effort to use parameters with your timed
* events (#logEvent:withParameters:timed:). This provides valuable information
* around the characteristics of an action (e.g. - Buy Event that has a Parameter of Widget with
* Value Golden Sword).
*
* @see #logEvent:withParameters:timed: for details on storing timed events with parameters. \n
* #endTimedEvent:withParameters: for details on stopping a timed event and (optionally) updating
* parameters.
*
* @code
* - (void)startLevel
{
[Flurry logEvent:@"Level Played" timed:YES];
// Start user on level
}
- (void)endLevel
{
[Flurry endTimedEvent:@"Level Played" withParameters:nil];
// User done with level
}
* @endcode
*
* @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme
* that can be easily understood by non-technical people in your business domain.
* @param timed Specifies the event will be timed.
*/
+ (void)logEvent:(NSString *)eventName timed:(BOOL)timed;
/*!
* @brief Records a custom parameterized timed event specified by @c eventName with @c parameters.
* @since 2.8.4
*
* This method overrides #logEvent to allow you to capture the length of an event with parameters.
* This can be extremely valuable to understand the level of engagement with a particular action
* and the characteristics associated with that action. For example, you can capture how long a user
* spends on a level or reading an article. Parameters can be used to capture, for example, the
* author of an article or if something was purchased while on the level.
*
* @note You should not pass private or confidential information about your users in a
* custom event.
*
* @see #endTimedEvent:withParameters: for details on stopping a timed event and (optionally) updating
* parameters.
*
* @code
* - (void)startLevel
{
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"100", // Parameter Value
@"Current Points", // Parameter Name
nil];
[Flurry logEvent:@"Level Played" withParameters:params timed:YES];
// Start user on level
}
- (void)endLevel
{
// User gained additional 100 points in Level
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"200", // Parameter Value
@"Current Points", // Parameter Name
nil];
[Flurry endTimedEvent:@"Level Played" withParameters:params];
// User done with level
}
* @endcode
*
* @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme
* that can be easily understood by non-technical people in your business domain.
* @param parameters An immutable copy of map containing Name-Value pairs of parameters.
* @param timed Specifies the event will be timed.
*/
+ (void)logEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters timed:(BOOL)timed;
/*!
* @brief Ends a timed event specified by @c eventName and optionally updates parameters with @c parameters.
* @since 2.8.4
*
* This method ends an existing timed event. If parameters are provided, this will overwrite existing
* parameters with the same name or create new parameters if the name does not exist in the parameter
* map set by #logEvent:withParameters:timed:.
*
* @note You should not pass private or confidential information about your users in a
* custom event. \n
* If the app is backgrounded prior to ending a timed event, the Flurry SDK will automatically
* end the timer on the event. \n
* #endTimedEvent:withParameters: is ignored if called on a previously
* terminated event.
*
* @see #logEvent:withParameters:timed: for details on starting a timed event with parameters.
*
* @code
* - (void)startLevel
{
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"100", // Parameter Value
@"Current Points", // Parameter Name
nil];
[Flurry logEvent:@"Level Played" withParameters:params timed:YES];
// Start user on level
}
- (void)endLevel
{
// User gained additional 100 points in Level
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:@"200", // Parameter Value
@"Current Points", // Parameter Name
nil];
[Flurry endTimedEvent:@"Level Played" withParameters:params];
// User done with level
}
* @endcode
*
* @param eventName Name of the event. For maximum effectiveness, we recommend using a naming scheme
* that can be easily understood by non-technical people in your business domain.
* @param parameters An immutable copy of map containing Name-Value pairs of parameters.
*/
+ (void)endTimedEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters; // non-nil parameters will update the parameters
//@}
/** @name Page View Methods
* Count page views.
*/
//@{
/*!
* @deprecated
* @brief see +(void)logAllPageViewsForTarget:(id)target; for details
* @since 2.7
* This method does the same as +(void)logAllPageViewsForTarget:(id)target method and is left for backward compatibility
*/
+ (void)logAllPageViews:(id)target __attribute__ ((deprecated));
/*!
* @brief Automatically track page views on a @c UINavigationController or @c UITabBarController.
* @since 4.3
*
* This method increments the page view count for a session based on traversing a UINavigationController
* or UITabBarController. The page view count is only a counter for the number of transitions in your
* app. It does not associate a name with the page count. To associate a name with a count of occurences
* see #logEvent:.
*
* @note If you need to release passed target, you should call counterpart method + (void)stopLogPageViewsForTarget:(id)target before;
*
* @see #logPageView for details on explictly incrementing page view count.
*
* @code
* -(void) trackViewsFromTabBar:(UITabBarController*) tabBar
{
[Flurry logAllPageViewsForTarget:tabBar];
}
* @endcode
*
* @param target The navigation or tab bar controller.
*/
+ (void)logAllPageViewsForTarget:(id)target;
/*!
* @brief Stops logging page views on previously observed with logAllPageViewsForTarget: @c UINavigationController or @c UITabBarController.
* @since 4.3
*
* Call this method before instance of @c UINavigationController or @c UITabBarController observed with logAllPageViewsForTarget: is released.
*
* @code
* -(void) dealloc
{
[Flurry stopLogPageViewsForTarget:_tabBarController];
[_tabBarController release];
[super dealloc];
}
* @endcode
*
* @param target The navigation or tab bar controller.
*/
+ (void)stopLogPageViewsForTarget:(id)target;
/*!
* @brief Explicitly track a page view during a session.
* @since 2.7
*
* This method increments the page view count for a session when invoked. It does not associate a name
* with the page count. To associate a name with a count of occurences see #logEvent:.
*
* @see #logAllPageViews for details on automatically incrementing page view count based on user
* traversing navigation or tab bar controller.
*
* @code
* -(void) trackView
{
[Flurry logPageView];
}
* @endcode
*
*/
+ (void)logPageView;
//@}
/** @name User Info
* Methods to set user information.
*/
//@{
/*!
* @brief Assign a unique id for a user in your app.
* @since 2.7
*
* @note Please be sure not to use this method to pass any private or confidential information
* about the user.
*
* @param userID The app id for a user.
*/
+ (void)setUserID:(NSString *)userID;
/*!
* @brief Set your user's age in years.
* @since 2.7
*
* Use this method to capture the age of your user. Only use this method if you collect this
* information explictly from your user (i.e. - there is no need to set a default value).
*
* @note The age is aggregated across all users of your app and not available on a per user
* basis.
*
* @param age Reported age of user.
*
*/
+ (void)setAge:(int)age;
/*!
* @brief Set your user's gender.
* @since 2.7
*
* Use this method to capture the gender of your user. Only use this method if you collect this
* information explictly from your user (i.e. - there is no need to set a default value). Allowable
* values are @c @"m" or @c @"f"
*
* @note The gender is aggregated across all users of your app and not available on a per user
* basis.
*
* @param gender Reported gender of user.
*
*/
+ (void)setGender:(NSString *)gender; // user's gender m or f
//@}
/** @name Location Reporting
* Methods for setting location information.
*/
//@{
/*!
* @brief Set the location of the session.
* @since 2.7
*
* Use information from the CLLocationManager to specify the location of the session. Flurry does not
* automatically track this information or include the CLLocation framework.
*
* @note Only the last location entered is captured per session. \n
* Regardless of accuracy specified, the Flurry SDK will only report location at city level or higher. \n
* Location is aggregated across all users of your app and not available on a per user basis. \n
* This information should only be captured if it is germaine to the use of your app.
*
* @code
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
[locationManager startUpdatingLocation];
* @endcode
*
* After starting the location manager, you can set the location with Flurry. You can implement
* CLLocationManagerDelegate to be aware of when the location is updated. Below is an example
* of how to use this method, after you have recieved a location update from the locationManager.
*
* @code
CLLocation *location = locationManager.location;
[Flurry setLatitude:location.coordinate.latitude
longitude:location.coordinate.longitude
horizontalAccuracy:location.horizontalAccuracy
verticalAccuracy:location.verticalAccuracy];
* @endcode
* @param latitude The latitude.
* @param longitude The longitude.
* @param horizontalAccuracy The radius of uncertainty for the location in meters.
* @param verticalAccuracy The accuracy of the altitude value in meters.
*
*/
+ (void)setLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(float)horizontalAccuracy verticalAccuracy:(float)verticalAccuracy;
//@}
/** @name Session Reporting Calls
* Optional methods that can be called at any point to control session reporting.
*/
//@{
/*!
* @brief Set session to report when app closes.
* @since 2.7
*
* Use this method report session data when the app is closed. The default value is @c YES.
*
* @note This method is rarely invoked in iOS >= 3.2 due to the updated iOS lifecycle.
*
* @see #setSessionReportsOnPauseEnabled:
*
* @param sendSessionReportsOnClose YES to send on close, NO to omit reporting on close.
*
*/
+ (void)setSessionReportsOnCloseEnabled:(BOOL)sendSessionReportsOnClose;
/*!
* @brief Set session to report when app is sent to the background.
* @since 2.7
*
* Use this method report session data when the app is paused. The default value is @c YES.
*
* @param setSessionReportsOnPauseEnabled YES to send on pause, NO to omit reporting on pause.
*
*/
+ (void)setSessionReportsOnPauseEnabled:(BOOL)setSessionReportsOnPauseEnabled;
/*!
* @brief Set session to support background execution.
* @since 4.2.2
*
* Use this method to enable reporting of errors and events when application is
* running in backgorund (such applications have UIBackgroundModes in Info.plist).
* You should call #pauseBackgroundSession when appropriate in background mode to
* pause the session (for example when played song completed in background)
*
* Default value is @c NO
*
* @see #pauseBackgroundSession for details
*
* @param setBackgroundSessionEnabled YES to enbale background support and
* continue log events and errors for running session.
*/
+ (void)setBackgroundSessionEnabled:(BOOL)setBackgroundSessionEnabled;
/*!
* @brief Enable custom event logging.
* @since 2.7
*
* Use this method to allow the capture of custom events. The default value is @c YES.
*
* @param value YES to enable event logging, NO to stop custom logging.
*
*/
+ (void)setEventLoggingEnabled:(BOOL)value;
//@}
@end
@@ -0,0 +1,19 @@
//
// GameThumbnailCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@class RBXGameGear;
@class RBXGamePass;
@interface GameConsumableCollectionViewCell : UICollectionViewCell
-(void) setGameGearData:(RBXGameGear*)gameGearData;
-(void) setGamePassData:(RBXGamePass*)gamePassData;
@end
@@ -0,0 +1,120 @@
//
// GameThumbnailCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GameConsumableCollectionViewCell.h"
#import "RobloxImageView.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#import "RBActivityIndicatorView.h"
#define THUMBNAIL_SIZE CGSizeMake(420, 230)
#define VIEWCELL_CORNER_RADIUS 5.0
@interface GameConsumableCollectionViewCell()
@end
@implementation GameConsumableCollectionViewCell
{
IBOutlet RobloxImageView *_image;
IBOutlet UILabel *_title;
IBOutlet UILabel *_priceLabel;
NSNumber* _assetID;
}
- (void)layoutSubviews
{
[super layoutSubviews];
_title.backgroundColor = [UIColor clearColor];
[RobloxTheme applyToConsumablePriceLabel:_priceLabel];
[RobloxTheme applyToConsumableTitleLabel:_title];
}
-(void) setGameGearData:(RBXGameGear*)data
{
if(data == nil)
{
_assetID = nil;
_image.hidden = YES;
_title.hidden = YES;
_priceLabel.hidden = YES;
}
else if(_assetID == nil || ![_assetID isEqualToNumber:data.assetID])
{
_assetID = data.assetID;
_image.hidden = NO;
_title.hidden = NO;
_priceLabel.hidden = NO;
_image.hidden = YES;
_image.animateInOptions = RBXImageViewAnimateInIfNotCached;
_title.text = data.name;
_priceLabel.text = data.userOwns
? [NSString stringWithFormat:NSLocalizedString(@"Purchased", nil)]
: (data.priceInRobux > 0 ? [NSString stringWithFormat:@"%lu", (unsigned long)data.priceInRobux] : @"-");
self.layer.cornerRadius = VIEWCELL_CORNER_RADIUS;
RobloxImageView* image = _image;
[_image loadWithAssetID:[data.assetID stringValue] withSize:THUMBNAIL_SIZE completion:^
{
dispatch_async(dispatch_get_main_queue(), ^
{
image.hidden = NO;
});
}];
}
}
-(void) setGamePassData:(RBXGamePass*)data
{
if(data == nil)
{
_assetID = nil;
_image.hidden = YES;
_title.hidden = YES;
_priceLabel.hidden = YES;
}
else if(_assetID == nil || ![_assetID isEqualToNumber:data.passID])
{
_assetID = data.passID;
_image.hidden = NO;
_title.hidden = NO;
_priceLabel.hidden = NO;
_image.hidden = YES;
_image.animateInOptions = RBXImageViewAnimateInIfNotCached;
_title.text = data.passName;
_priceLabel.text = data.userOwns
? [NSString stringWithFormat:NSLocalizedString(@"Purchased", nil)]
: [NSString stringWithFormat:@"%lu", (unsigned long)data.priceInRobux];
self.layer.cornerRadius = VIEWCELL_CORNER_RADIUS;
RobloxImageView* image = _image;
[_image loadWithAssetID:[data.passID stringValue] withSize:THUMBNAIL_SIZE completion:^
{
dispatch_async(dispatch_get_main_queue(), ^
{
image.hidden = NO;
});
}];
}
}
@end
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="6245" systemVersion="14A379a" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="UyC-rE-64E" customClass="GameConsumableCollectionViewCell">
<rect key="frame" x="0.0" y="0.0" width="150" height="207"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="150" height="207"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zbS-gq-Dcy" customClass="RobloxImageView">
<rect key="frame" x="0.0" y="0.0" width="150" height="150"/>
</imageView>
<label userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_CONSUMABLE_TITLE_" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oTO-bq-MpL">
<rect key="frame" x="12" y="157" width="133" height="18"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="ROBUX" translatesAutoresizingMaskIntoConstraints="NO" id="ZAO-EB-Adg">
<rect key="frame" x="12" y="177" width="22" height="22"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="__PRICE__" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ABz-e2-yzd">
<rect key="frame" x="42" y="177" width="92" height="23"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" fixedFrame="YES" image="Separator" translatesAutoresizingMaskIntoConstraints="NO" id="HgN-Ni-TdP">
<rect key="frame" x="5" y="154" width="140" height="1"/>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
<size key="customSize" width="461" height="151"/>
<connections>
<outlet property="_image" destination="zbS-gq-Dcy" id="QtM-I4-sH0"/>
<outlet property="_priceLabel" destination="ABz-e2-yzd" id="YjW-fy-D8g"/>
<outlet property="_title" destination="oTO-bq-MpL" id="WSu-fm-kh4"/>
</connections>
<point key="canvasLocation" x="540" y="373.5"/>
</collectionViewCell>
</objects>
<resources>
<image name="ROBUX" width="24" height="24"/>
<image name="Separator" width="491" height="1"/>
</resources>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<simulatedStatusBarMetrics key="statusBar"/>
<simulatedOrientationMetrics key="orientation"/>
<simulatedScreenMetrics key="destination"/>
</simulatedMetricsContainer>
</document>
@@ -0,0 +1,19 @@
//
// GameConsumableCollectionViewController.h
// RobloxMobile
//
// Created by Ariel Lichtin on 5/28/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
@interface GameConsumableCollectionViewController : UIViewController
-(void) fetchGearForPlaceID:(NSString*)placeID gameTitle:(NSString*)gameTitle completion:(void(^)())completionHandler;
-(void) fetchPassesForPlaceID:(NSString*)placeID gameTitle:(NSString*)gameTitle completion:(void(^)())completionHandler;
@property(nonatomic, readonly) NSUInteger numItems;
@end
@@ -0,0 +1,181 @@
//
// GameSortHorizontalViewController.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/28/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GameConsumableCollectionViewController.h"
#import "GameConsumableCollectionViewCell.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#import "RobloxNotifications.h"
#import "RBConsumablePurchaseViewController.h"
#define NUM_GAMES_IN_CONTROLLER 10
#define ITEM_SIZE CGSizeMake(150, 207)
#define THUMBNAIL_SIZE CGSizeMake(420, 420)
@interface GameConsumableCollectionViewController ()
@end
@implementation GameConsumableCollectionViewController
{
IBOutlet UILabel *_title;
IBOutlet UICollectionView* _collectionView;
NSString* _gameTitle;
NSArray* _passes;
NSArray* _gear;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onGameItemsUpdated:) name:RBX_NOTIFY_GAME_ITEMS_UPDATED object:nil];
if(self)
{
}
return self;
}
- (void)onGameItemsUpdated:(NSNotification*)notification
{
[_collectionView reloadData];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[RobloxTheme applyToGameSortTitle:_title];
// Setup the collection view
[_collectionView registerNib:[UINib nibWithNibName:@"GameConsumableCollectionViewCell" bundle: nil] forCellWithReuseIdentifier:@"ReuseCell"];
[_collectionView setBackgroundView:nil];
[_collectionView setBackgroundColor:[UIColor clearColor]];
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:ITEM_SIZE];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[flowLayout setMinimumLineSpacing:14.0f];
[flowLayout setSectionInset:UIEdgeInsetsMake(0.0, 29.0, 0.0, 29.0)];
[_collectionView setCollectionViewLayout:flowLayout];
}
- (NSUInteger)numItems
{
if(_passes != nil)
return _passes.count;
else if(_gear != nil)
return _gear.count;
else
return 0;
}
-(void) fetchGearForPlaceID:(NSString*)placeID gameTitle:(NSString*)gameTitle completion:(void(^)())completionHandler
{
_title.text = [NSLocalizedString(@"GearTitlePhrase", nil) uppercaseString];
_gear = nil;
_passes = nil;
_gameTitle = gameTitle;
[_collectionView reloadData];
[RobloxData fetchGameGear:placeID startIndex:0 maxRows:10 completion:^(NSArray *gear)
{
dispatch_async(dispatch_get_main_queue(), ^
{
_gear = gear;
[_collectionView reloadData];
if(completionHandler != nil)
completionHandler();
});
}];
}
-(void) fetchPassesForPlaceID:(NSString*)placeID gameTitle:(NSString*)gameTitle completion:(void(^)())completionHandler
{
_title.text = [NSLocalizedString(@"PassesTitlePhrase", nil) uppercaseString];
_gear = nil;
_passes = nil;
_gameTitle = gameTitle;
[_collectionView reloadData];
[RobloxData fetchGamePasses:placeID startIndex:0 maxRows:10 completion:^(NSArray *passes)
{
dispatch_async(dispatch_get_main_queue(), ^
{
_passes = passes;
[_collectionView reloadData];
if(completionHandler != nil)
completionHandler();
});
}];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
if(_passes != nil)
return _passes.count;
else if(_gear != nil)
return _gear.count;
else
return 0;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GameConsumableCollectionViewCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:@"ReuseCell" forIndexPath:indexPath];
cell.layer.cornerRadius = 2;
if(_passes != nil)
{
RBXGamePass* pass = _passes[indexPath.row];
[cell setGamePassData:pass];
}
else if(_gear != nil)
{
RBXGameGear* gear = _gear[indexPath.row];
[cell setGameGearData:gear];
}
else
{
[cell setGameGearData:nil];
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
RBConsumablePurchaseViewController* controller = [[RBConsumablePurchaseViewController alloc] initWithNibName:@"RBConsumablePurchaseViewController" bundle:nil];
controller.modalPresentationStyle = UIModalPresentationFormSheet;
controller.gameTitle = _gameTitle;
if(_passes != nil)
{
controller.passData = _passes[indexPath.row];
}
else if(_gear != nil)
{
controller.gearData = _gear[indexPath.row];
}
[self.navigationController presentViewController:controller animated:YES completion:nil];
}
@end
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="6245" systemVersion="14A379a" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment defaultVersion="1792" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="GameConsumableCollectionViewController">
<connections>
<outlet property="_collectionView" destination="Ekr-22-cfr" id="PVH-SE-dac"/>
<outlet property="_title" destination="mxl-Lt-fu4" id="hFR-R7-02z"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="1024" height="245"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mxl-Lt-fu4">
<rect key="frame" x="29" y="0.0" width="337" height="25"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.1764705926" green="0.32156863810000003" blue="0.56078433989999998" alpha="1" colorSpace="deviceRGB"/>
<nil key="highlightedColor"/>
</label>
<collectionView contentMode="scaleToFill" fixedFrame="YES" alwaysBounceHorizontal="YES" showsVerticalScrollIndicator="NO" minimumZoomScale="0.0" maximumZoomScale="0.0" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="Ekr-22-cfr">
<rect key="frame" x="0.0" y="38" width="1024" height="207"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="BKz-jl-6P0">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
<connections>
<outlet property="dataSource" destination="-1" id="6Nb-XY-6iZ"/>
<outlet property="delegate" destination="-1" id="g5i-Oh-WGP"/>
</connections>
</collectionView>
</subviews>
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="simulatedStatusBarMetrics"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="460" y="465"/>
</view>
</objects>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<simulatedStatusBarMetrics key="statusBar"/>
<simulatedOrientationMetrics key="orientation"/>
<simulatedScreenMetrics key="destination"/>
</simulatedMetricsContainer>
</document>
@@ -0,0 +1,20 @@
//
// GameSearchResultCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 6/10/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
@interface GameSearchResultCell : UICollectionViewCell
@property(strong, nonatomic) RBXGameData* gameData;
+(CGSize) getCellSize;
+(NSString*) getNibName;
@end
@@ -0,0 +1,70 @@
//
// GameSearchResultCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 6/10/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GameSearchResultCell.h"
#import "RobloxImageView.h"
#import "RobloxTheme.h"
#import "RobloxInfo.h"
#import "RBActivityIndicatorView.h"
@implementation GameSearchResultCell
{
IBOutlet RobloxImageView* _thumbnail;
IBOutlet UILabel *_title;
IBOutlet RBActivityIndicatorView *_activityIndicator;
}
+(CGSize)getCellSize
{
return [RobloxInfo thisDeviceIsATablet] ? CGSizeMake(160, 220) : CGSizeMake(140, 215);
}
+(NSString*)getNibName
{
return [RobloxInfo thisDeviceIsATablet] ? @"GameSearchResultCell" : @"GameSearchResultCell_iPhone";
}
- (void)awakeFromNib
{
[RobloxTheme applyToGameSearchToolbarCell:_title];
_thumbnail.animateInOptions = RBXImageViewAnimateInIfNotCached;
_thumbnail.layer.cornerRadius = 5;
_thumbnail.layer.masksToBounds = YES;
_thumbnail.layer.borderColor = [UIColor whiteColor].CGColor;
_thumbnail.layer.borderWidth = 1;
_title.hidden = YES;
_activityIndicator.hidden = NO;
}
- (void)setGameData:(RBXGameData *)gameData
{
if (gameData)
{
_gameData = gameData;
_title.text = gameData.title;
_title.hidden = NO;
[_activityIndicator startAnimating];
[_thumbnail loadThumbnailForGame:gameData withSize:[RobloxTheme sizeGameCoverSquare] completion:^{
dispatch_async(dispatch_get_main_queue(), ^
{
[_activityIndicator stopAnimating];
_thumbnail.hidden = NO;
});
}];
}
else
{
_title.hidden = YES;
_thumbnail.hidden = YES;
[_activityIndicator startAnimating];
}
}
@end
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="8152.3" systemVersion="14E46" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8124.4"/>
</dependencies>
<customFonts key="customFonts">
<mutableArray key="SourceSansPro-Semibold.ttf">
<string>SourceSansPro-Semibold</string>
</mutableArray>
</customFonts>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell multipleTouchEnabled="YES" contentMode="center" id="HH9-gH-d9I" customClass="GameSearchResultCell">
<rect key="frame" x="0.0" y="0.0" width="160" height="220"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="160" height="220"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GYf-6s-fCf" customClass="RobloxImageView">
<rect key="frame" x="10" y="5" width="140" height="140"/>
</imageView>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_MAX TWENTY CHARACTERS_" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tZ2-Op-Tg9">
<rect key="frame" x="10" y="153" width="140" height="67"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name="SourceSansPro-Semibold" family="Source Sans Pro" pointSize="20"/>
<nil key="highlightedColor"/>
</label>
<view hidden="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2Jq-zQ-GAz" customClass="RBActivityIndicatorView">
<rect key="frame" x="45" y="40" width="70" height="70"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<size key="customSize" width="426" height="186"/>
<connections>
<outlet property="_activityIndicator" destination="2Jq-zQ-GAz" id="R8c-bi-8D3"/>
<outlet property="_thumbnail" destination="GYf-6s-fCf" id="QNI-Jh-3Rd"/>
<outlet property="_title" destination="tZ2-Op-Tg9" id="cgP-WQ-gAI"/>
</connections>
<point key="canvasLocation" x="173" y="398.5"/>
</collectionViewCell>
</objects>
</document>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8152.3" systemVersion="14E46" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8124.4"/>
</dependencies>
<customFonts key="customFonts">
<mutableArray key="SourceSansPro-Semibold.ttf">
<string>SourceSansPro-Semibold</string>
</mutableArray>
</customFonts>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell multipleTouchEnabled="YES" contentMode="center" id="tHy-Xk-7BH" customClass="GameSearchResultCell">
<rect key="frame" x="0.0" y="0.0" width="140" height="215"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="140" height="215"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pHN-h4-7Cx" customClass="RobloxImageView">
<rect key="frame" x="0.0" y="0.0" width="140" height="140"/>
</imageView>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_MAX TWENTY CHARACTERS_" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7gK-MI-Jge">
<rect key="frame" x="0.0" y="148" width="140" height="67"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name="SourceSansPro-Semibold" family="Source Sans Pro" pointSize="20"/>
<nil key="highlightedColor"/>
</label>
<view hidden="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1IZ-7P-CgI" customClass="RBActivityIndicatorView">
<rect key="frame" x="35" y="35" width="70" height="70"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<size key="customSize" width="426" height="186"/>
<connections>
<outlet property="_activityIndicator" destination="1IZ-7P-CgI" id="ACA-ec-nbs"/>
<outlet property="_thumbnail" destination="pHN-h4-7Cx" id="HdM-sR-1Sr"/>
<outlet property="_title" destination="7gK-MI-Jge" id="QHX-F6-LIj"/>
</connections>
<point key="canvasLocation" x="173" y="398.5"/>
</collectionViewCell>
</objects>
</document>
@@ -0,0 +1,22 @@
//
// GameSortCarouselViewController
//
// Created by Huy Le on 4/29/14.
// Copyright (c) 2014 2359Media. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
#import "iCarousel/iCarousel.h"
@interface GameSortCarouselViewController : UIViewController
@property(strong, nonatomic) RBXGameSort* gameSort;
@property(nonatomic) NSUInteger maxNumItems;
@property(nonatomic, strong) iCarousel *carousel;
@property(copy) void (^gameSelectedHandler)(RBXGameData* gameData);
-(id) initWithFrame:(CGRect)frame;
@end
@@ -0,0 +1,170 @@
//
// GameSortCarouselViewController.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/28/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GameSortCarouselViewController.h"
#import "CarouselThumbnailCell.h"
#import "RobloxTheme.h"
#import "RobloxData.h"
#import "NSTimer+Blocks.h"
#define THUMBNAIL_SIZE RBX_SCALED_DEVICE_SIZE(455, 256)
#define ITEM_FRAME CGRectMake(0,0,455,256)
@interface GameSortCarouselViewController () <iCarouselDataSource, iCarouselDelegate>
@end
@implementation GameSortCarouselViewController
{
CGRect _customFrame;
// iCarousel* _carousel;
NSTimer* _autoScrollTimer;
NSArray* _games;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super init];
if(self)
{
_games = nil;
_customFrame = frame;
_maxNumItems = 10;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.frame = CGRectZero;
_carousel = [[iCarousel alloc] initWithFrame:_customFrame];
_carousel.autoresizingMask = UIViewAutoresizingNone;
_carousel.type = iCarouselTypeCoverFlow;
_carousel.delegate = self;
_carousel.dataSource = self;
_carousel.decelerationRate = 0.7f;
[self.view addSubview:_carousel];
[self initAutoScrollTimer];
}
- (void)viewWillUnload
{
[self killAutoScrollTimer];
[super viewWillUnload];
_carousel = nil;
}
- (void)setGameSort:(RBXGameSort *)gameSort
{
_gameSort = gameSort;
[RobloxData fetchGameListWithSortID:_gameSort.sortID genreID:nil fromIndex:0 numGames:self.maxNumItems thumbSize:THUMBNAIL_SIZE completion:^(NSArray *games)
{
_games = games;
dispatch_async(dispatch_get_main_queue(), ^
{
[_carousel reloadData];
});
}];
}
-(void) initAutoScrollTimer
{
_autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:7.0f block:^
{
[_carousel scrollByNumberOfItems:1 duration:1.0f];
}
repeats:YES];
}
-(void) killAutoScrollTimer
{
if(_autoScrollTimer)
{
[_autoScrollTimer invalidate];
_autoScrollTimer = nil;
}
}
#pragma mark -
#pragma mark iCarousel methods
- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
return _games != nil ? _games.count : 0;
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
{
// Create new view if no view is available for recycling
if (view == nil)
{
view = [[[NSBundle mainBundle] loadNibNamed:@"CarouselThumbnailCell" owner:self options:nil] firstObject];
//[view setFrame:ITEM_FRAME];
}
CarouselThumbnailCell* cell = (CarouselThumbnailCell*) view;
cell.layer.cornerRadius = 5;
[cell setGameData:_games[index]];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
//customize carousel display
switch (option)
{
case iCarouselOptionWrap:
{
return YES;
}
case iCarouselOptionSpacing:
{
return 0.7f;
}
case iCarouselOptionTilt:
{
return 0.5f;
}
default:
{
return value;
}
}
}
- (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
if(_gameSelectedHandler != nil)
{
RBXGameData* gameData = _games[index];
_gameSelectedHandler(gameData);
}
}
- (void)carouselWillBeginDragging:(__unused iCarousel *)carousel
{
[self killAutoScrollTimer];
}
- (void)carouselDidEndDragging:(__unused iCarousel *)carousel willDecelerate:(__unused BOOL)decelerate
{
[self initAutoScrollTimer];
}
@end
@@ -0,0 +1,31 @@
//
// GameSortHorizontalViewController.h
// RobloxMobile
//
// Created by Ariel Lichtin on 5/28/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
#import "RBXEventReporter.h"
@interface GameSortHorizontalViewController : UIViewController
// Initialize the sort
-(void) setSort:(RBXGameSort*)sort;
- (void) setSort:(RBXGameSort *)sort andGenre:(RBXGameGenre *)genre;
// Initialize with a prefetched game list
-(void) setSort:(RBXGameSort*)sort withGames:(NSArray*)games;
-(void) setAnalyticsLocation:(RBXAnalyticsGameLocations)location andContext:(RBXAnalyticsContextName)context;
@property (strong, nonatomic) NSString* sortTitle;
@property (nonatomic) NSUInteger startIndex;
// Callback handlers
@property (copy) void (^seeAllHandler)(NSNumber* sortID);
@property (copy) void (^gameSelectedHandler)(RBXGameData* gameData);
@end
@@ -0,0 +1,165 @@
//
// GameSortHorizontalViewController.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/28/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GameSortHorizontalViewController.h"
#import "GameThumbnailCell.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#import "RobloxNotifications.h"
#import "UIView+Position.h"
#define NUM_GAMES_IN_CONTROLLER 30
#define ITEM_SIZE CGSizeMake(166, 220)
#define THUMBNAIL_SIZE CGSizeMake(420, 230)
@interface GameSortHorizontalViewController ()
@end
@implementation GameSortHorizontalViewController
{
IBOutlet UILabel *_title;
IBOutlet UIButton *_seeAllButton;
IBOutlet UICollectionView* _collectionView;
NSNumber* _sortID;
NSArray* _games;
RBXAnalyticsGameLocations _gameLocation;
RBXAnalyticsContextName _gameContext;
}
- (instancetype)init
{
self = [super init];
if(self)
{
self.startIndex = 0;
_gameContext = RBXAContextMain;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
if(_sortTitle != nil)
_title.text = _sortTitle;
[RobloxTheme applyToGameSortSeeAllButton:_seeAllButton];
[RobloxTheme applyToGameSortTitle:_title];
[_seeAllButton setTitle:NSLocalizedString(@"SeeAllPhrase", nil) forState:UIControlStateNormal];
[_seeAllButton setShowsTouchWhenHighlighted:YES];
// Setup the collection view
[_collectionView registerNib:[UINib nibWithNibName:@"GameThumbnailCell" bundle: nil] forCellWithReuseIdentifier:@"ReuseCell"];
[_collectionView setBackgroundView:nil];
[_collectionView setBackgroundColor:[UIColor clearColor]];
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:ITEM_SIZE];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
[flowLayout setMinimumLineSpacing:6.0f];
[flowLayout setSectionInset:UIEdgeInsetsMake(0.0, 29.0, 0.0, 29.0)];
[_collectionView setCollectionViewLayout:flowLayout];
}
- (void)setSortTitle:(NSString *)sortTitle
{
_sortTitle = [sortTitle uppercaseString];
_title.text = _sortTitle;
}
-(void) setSort:(RBXGameSort*)sort
{
[self setSort:sort andGenre:nil];
}
- (void) setSort:(RBXGameSort *)sort andGenre:(RBXGameGenre *)genre {
_sortTitle = [sort.title uppercaseString];
_sortID = sort.sortID;
_games = nil;
_title.text = _sortTitle;
[_collectionView reloadData];
[RobloxData fetchGameListWithSortID:sort.sortID genreID:genre.genreID fromIndex:self.startIndex numGames:NUM_GAMES_IN_CONTROLLER thumbSize:THUMBNAIL_SIZE completion:^(NSArray *games)
{
dispatch_async(dispatch_get_main_queue(), ^
{
_games = games;
[_collectionView reloadData];
});
}];
}
-(void) setSort:(RBXGameSort*)sort withGames:(NSArray*)games
{
_sortTitle = [sort.title uppercaseString];
_sortID = sort.sortID;
_games = games;
_title.text = _sortTitle;
[_collectionView reloadData];
}
-(void) setAnalyticsLocation:(RBXAnalyticsGameLocations)location andContext:(RBXAnalyticsContextName)context
{
_gameLocation = location;
_gameContext = context;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _games != nil ? _games.count : 0;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GameThumbnailCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:@"ReuseCell" forIndexPath:indexPath];
RBXGameData* gameData = _games[indexPath.row];
[cell setGameData:gameData];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if(_gameSelectedHandler != nil)
{
RBXGameData* gameData = _games[indexPath.row];
[[RBXEventReporter sharedInstance] reportOpenGameDetailFromSort:[NSNumber numberWithInteger:gameData.placeID.integerValue]
fromPage:_gameLocation
inSort:_sortID
atIndex:[NSNumber numberWithInteger:indexPath.row]
totalItemsInSort:[NSNumber numberWithInteger:_games.count]];
_gameSelectedHandler(gameData);
}
}
- (IBAction)seeAllCategoryTouchUpInside:(id)sender
{
if (_gameContext)
[[RBXEventReporter sharedInstance] reportButtonClick:RBXAButtonSeeAll withContext:_gameContext withCustomDataString:_sortID.stringValue];
if(_seeAllHandler != nil)
{
_seeAllHandler(_sortID);
}
}
@end
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="7706" systemVersion="14E46" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
</dependencies>
<customFonts key="customFonts">
<mutableArray key="SourceSansPro-Regular.ttf">
<string>SourceSansPro-Regular</string>
</mutableArray>
</customFonts>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="GameSortHorizontalViewController">
<connections>
<outlet property="_collectionView" destination="Ekr-22-cfr" id="PVH-SE-dac"/>
<outlet property="_seeAllButton" destination="vr9-hM-bas" id="gyo-VV-kPb"/>
<outlet property="_title" destination="mxl-Lt-fu4" id="hFR-R7-02z"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="1024" height="254"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mxl-Lt-fu4">
<rect key="frame" x="29" y="0.0" width="337" height="25"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.1764705926" green="0.32156863810000003" blue="0.56078433989999998" alpha="1" colorSpace="deviceRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vr9-hM-bas">
<rect key="frame" x="920" y="0.0" width="80" height="30"/>
<color key="backgroundColor" red="0.12941177189350128" green="0.74901962280273438" blue="0.98431378602981567" alpha="1" colorSpace="deviceRGB"/>
<fontDescription key="fontDescription" name="SourceSansPro-Regular" family="Source Sans Pro" pointSize="18"/>
<state key="normal" title="_See_All_">
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="seeAllCategoryTouchUpInside:" destination="-1" eventType="touchUpInside" id="851-Jz-eol"/>
</connections>
</button>
<collectionView opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" fixedFrame="YES" alwaysBounceHorizontal="YES" showsVerticalScrollIndicator="NO" minimumZoomScale="0.0" maximumZoomScale="0.0" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="Ekr-22-cfr">
<rect key="frame" x="0.0" y="34" width="1024" height="220"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="BKz-jl-6P0">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
<connections>
<outlet property="dataSource" destination="-1" id="6Nb-XY-6iZ"/>
<outlet property="delegate" destination="-1" id="g5i-Oh-WGP"/>
</connections>
</collectionView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<nil key="simulatedStatusBarMetrics"/>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="460" y="497"/>
</view>
</objects>
<simulatedMetricsContainer key="defaultSimulatedMetrics">
<simulatedStatusBarMetrics key="statusBar"/>
<simulatedOrientationMetrics key="orientation"/>
<simulatedScreenMetrics key="destination"/>
</simulatedMetricsContainer>
</document>
@@ -0,0 +1,21 @@
//
// GameThumbnailCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@class RBXGameData;
@class RobloxImageView;
@interface GameThumbnailCell : UICollectionViewCell
+ (CGSize) getCellSize;
+ (NSString*) getNibName;
-(void) setGameData:(RBXGameData*)data;
@end
@@ -0,0 +1,226 @@
//
// GameThumbnailCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 5/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <QuartzCore/QuartzCore.h>
#import "GameThumbnailCell.h"
#import "RobloxImageView.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#import "RBActivityIndicatorView.h"
#import "RobloxInfo.h"
#define NUM_CELLS 5
@interface GameVotesView : UIView
-(void) setLikes:(uint)numLikes andDislikes:(uint)numDislikes;
@end
@implementation GameVotesView
{
uint likes;
uint dislikes;
CALayer* likesLayer;
CALayer* dislikesLayer;
NSMutableArray* margins;
}
-(void) initialize
{
likes = 1;
dislikes = 1;
//draw the likes
likesLayer = [CALayer layer];
[likesLayer setBackgroundColor:[RobloxTheme colorGray2].CGColor];
[likesLayer setFrame:CGRectZero];
[self.layer addSublayer:likesLayer];
//draw the dislikes
dislikesLayer = [CALayer layer];
[dislikesLayer setBackgroundColor:[RobloxTheme colorGray3].CGColor];
[dislikesLayer setFrame:CGRectZero];
[self.layer addSublayer:dislikesLayer];
//create the margin layers
margins = [NSMutableArray arrayWithCapacity:NUM_CELLS-1];
for (int i = 1; i < NUM_CELLS; i++)
{
CALayer* aMargin = [CALayer layer];
[aMargin setBackgroundColor:[UIColor whiteColor].CGColor];
[aMargin setFrame:CGRectZero];
[self.layer addSublayer:aMargin];
[margins addObject:aMargin];
}
self.backgroundColor = [UIColor clearColor];
}
-(id) init
{
self = [super init];
if (self)
[self initialize];
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
[self initialize];
return self;
}
-(id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
[self initialize];
return self;
}
-(void) setLikes:(uint)numLikes andDislikes:(uint)numDislikes
{
likes = numLikes;
dislikes = numDislikes;
[self drawAllLayers];
}
-(void) drawAllLayers
{
//draw the likes and dislikes
int totalVotes = likes + dislikes;
float percentLikes = (float) likes / (float) MAX(totalVotes, 1);
[likesLayer setFrame:CGRectMake(0, 0, self.frame.size.width * percentLikes, self.frame.size.height)];
[dislikesLayer setFrame:CGRectMake(likesLayer.frame.size.width, 0, self.frame.size.width - likesLayer.frame.size.width, self.frame.size.height)];
}
-(void) layoutSubviews
{
//draw the margin dividers
float margin = 2;
float cellWidth = (self.frame.size.width - (margin * margins.count)) / NUM_CELLS;
for (int i = 1; i <= margins.count; i++)
{
CGRect marginDivider = CGRectMake((cellWidth + margin) * i, 0, margin, self.frame.size.height);
[(CALayer*)margins[i-1] setFrame:marginDivider];
}
}
@end
@interface GameThumbnailCell()
@end
@implementation GameThumbnailCell
{
IBOutlet RobloxImageView *_image;
IBOutlet UILabel *_title;
IBOutlet UILabel *_numPlayers;
IBOutlet GameVotesView *_votes;
IBOutlet UIImageView *_playerVote;
IBOutlet RBActivityIndicatorView *_activityIndicator;
NSString* _placeID;
}
+ (CGSize) getCellSize
{
return [RobloxInfo thisDeviceIsATablet] ? CGSizeMake(166, 220) : CGSizeMake(150, 220);
}
+ (NSString*) getNibName
{
return [RobloxInfo thisDeviceIsATablet] ? @"GameThumbnailCell" : @"GameThumbnailCell_iPhone";
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
[RobloxTheme applyRoundedBorderToView:self];
_title.hidden = YES;
_image.hidden = YES;
_numPlayers.hidden = YES;
_votes.hidden = YES;
_playerVote.hidden = YES;
[RobloxTheme applyToGameThumbnailTitle:_title];
_image.animateInOptions = RBXImageViewAnimateInIfNotCached;
}
return self;
}
-(void)setGameData:(RBXGameData*)data
{
if (data) // if(![_placeID isEqualToString:data.placeID])
{
dispatch_async(dispatch_get_main_queue(), ^
{
//save the placeID
_placeID = data.placeID;
//update the title
_title.text = data.title;
_title.hidden = NO;
//set the number of players currently playing
_numPlayers.text = [NSString stringWithFormat:@"%@ %@", data.players, NSLocalizedString(@"NumberPlayersPhrase", nil)];
_numPlayers.hidden = NO;
//update the player voted image
switch (data.userVote)
{
case RBXUserVotePositive: { [_playerVote setImage:[UIImage imageNamed:@"Thumbs Down Filled"]]; } break;
case RBXUserVoteNegative: { [_playerVote setImage:[UIImage imageNamed:@"Thumbs Up Filled"]]; } break;
default: { [_playerVote setImage:[UIImage imageNamed:@"Thumbs Up Greyed"]]; } break;
}
_playerVote.hidden = NO;
//draw the total player votes
//[self drawVotes:data.upVotes withNumberOfLikes:(data.upVotes + data.downVotes)];
[_votes setLikes:data.upVotes andDislikes:data.downVotes];
_votes.hidden = NO;
//load the image icon
[_activityIndicator startAnimating];
[_image loadThumbnailForGame:data withSize:[RobloxTheme sizeProfilePictureMedium] completion:^
{
dispatch_async(dispatch_get_main_queue(), ^
{
[_activityIndicator stopAnimating];
_image.hidden = NO;
});
}];
});
}
else
{
_placeID = nil;
_image.hidden = YES;
_title.hidden = YES;
_votes.hidden = YES;
_playerVote.hidden = YES;
_numPlayers.hidden = YES;
}
}
@end
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="8152.3" systemVersion="14E46" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8124.4"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell multipleTouchEnabled="YES" contentMode="center" id="UyC-rE-64E" customClass="GameThumbnailCell">
<rect key="frame" x="0.0" y="0.0" width="166" height="220"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="166" height="220"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zbS-gq-Dcy" customClass="RobloxImageView">
<rect key="frame" x="13" y="13" width="140" height="140"/>
</imageView>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="__GAME_TITLE__" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oTO-bq-MpL">
<rect key="frame" x="12" y="153" width="142" height="24"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="highlightedColor"/>
</label>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_XYZ_Players_Online" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8Ei-Fh-MWe">
<rect key="frame" x="12" y="173" width="142" height="24"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NnN-RL-hma" customClass="RBActivityIndicatorView">
<rect key="frame" x="48" y="40" width="70" height="70"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="Thumbs Up" translatesAutoresizingMaskIntoConstraints="NO" id="HUl-WQ-zgb">
<rect key="frame" x="8" y="198" width="16" height="16"/>
<color key="tintColor" red="0.023529414087533951" green="0.035294119268655777" blue="0.050980396568775177" alpha="1" colorSpace="deviceRGB"/>
</imageView>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zbS-aG-hrN" customClass="GameVotesView">
<rect key="frame" x="28" y="203" width="110" height="6"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="customSize" width="461" height="191"/>
<connections>
<outlet property="_activityIndicator" destination="NnN-RL-hma" id="5aG-B9-JYp"/>
<outlet property="_image" destination="zbS-gq-Dcy" id="QtM-I4-sH0"/>
<outlet property="_numPlayers" destination="8Ei-Fh-MWe" id="jwS-ur-z6G"/>
<outlet property="_playerVote" destination="HUl-WQ-zgb" id="k9C-i0-EdC"/>
<outlet property="_title" destination="oTO-bq-MpL" id="WSu-fm-kh4"/>
<outlet property="_votes" destination="zbS-aG-hrN" id="pMW-jI-MzV"/>
</connections>
<point key="canvasLocation" x="271" y="314"/>
</collectionViewCell>
</objects>
<resources>
<image name="Thumbs Up" width="22" height="22"/>
</resources>
</document>
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8152.3" systemVersion="14E46" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8124.4"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell multipleTouchEnabled="YES" contentMode="center" id="hOD-7h-uRm" customClass="GameThumbnailCell">
<rect key="frame" x="0.0" y="0.0" width="150" height="220"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="150" height="220"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1XX-7d-WGy" customClass="RobloxImageView">
<rect key="frame" x="6" y="13" width="140" height="140"/>
</imageView>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="__GAME_TITLE__" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="08S-Ow-0ot">
<rect key="frame" x="6" y="153" width="140" height="24"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="highlightedColor"/>
</label>
<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_XYZ_Players_Online" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KSE-OQ-gd0">
<rect key="frame" x="6" y="173" width="140" height="24"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<color key="tintColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qMs-nz-vaH" customClass="RBActivityIndicatorView">
<rect key="frame" x="41" y="40" width="70" height="70"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="Thumbs Up" translatesAutoresizingMaskIntoConstraints="NO" id="Zsy-79-gll">
<rect key="frame" x="5" y="198" width="16" height="16"/>
<color key="tintColor" red="0.023529414089999999" green="0.03529411927" blue="0.050980396570000003" alpha="1" colorSpace="deviceRGB"/>
</imageView>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ciO-ZY-kC9" customClass="GameVotesView">
<rect key="frame" x="29" y="203" width="104" height="6"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="customSize" width="436" height="191"/>
<connections>
<outlet property="_activityIndicator" destination="qMs-nz-vaH" id="y0s-Cc-lQC"/>
<outlet property="_image" destination="1XX-7d-WGy" id="E4h-gu-Ri9"/>
<outlet property="_numPlayers" destination="KSE-OQ-gd0" id="fE4-e1-in6"/>
<outlet property="_playerVote" destination="Zsy-79-gll" id="eWe-gY-egX"/>
<outlet property="_title" destination="08S-Ow-0ot" id="fdE-GK-STn"/>
<outlet property="_votes" destination="ciO-ZY-kC9" id="azZ-le-9p9"/>
</connections>
<point key="canvasLocation" x="270.5" y="314"/>
</collectionViewCell>
</objects>
<resources>
<image name="Thumbs Up" width="22" height="22"/>
</resources>
</document>
@@ -0,0 +1,18 @@
//
// GamesCollectionViewController.h
// RobloxMobile
//
// Created by alichtin on 6/2/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface GamesCollectionView : UIView
- (void) loadGamesForKeywords:(NSString*)keywords;
- (void) loadGamesForSort:(NSNumber *)sortID playerID:(NSNumber *)playerID;
- (void) loadGamesForSort:(NSNumber *)sortID playerID:(NSNumber *)playerID genreID:(NSNumber *)genreID;
@end
@@ -0,0 +1,200 @@
//
// GamesCollectionView.m
// RobloxMobile
//
// Created by alichtin on 6/2/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "GamesCollectionView.h"
#import "GameThumbnailCell.h"
#import "RobloxNotifications.h"
#import "RobloxData.h"
#import "RBInfiniteCollectionView.h"
#import "RobloxTheme.h"
@interface GamesCollectionView () <RBInfiniteCollectionViewDelegate>
@end
@implementation GamesCollectionView
{
RBInfiniteCollectionView* _collectionView;
NSNumber* _playerID; // Some sorts require the player ID
NSNumber* _sortID;
NSNumber* _genreID;
NSString* _keywords;
NSMutableArray* _games;
NSUInteger _numItemsInCollectionView;
}
- (instancetype)init
{
self = [super init];
if(self)
{
_games = [[NSMutableArray alloc] init];
self.backgroundColor = [UIColor clearColor];
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:[GameThumbnailCell getCellSize]];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
[flowLayout setMinimumLineSpacing:6.0f]; //32.0f];
[flowLayout setMinimumInteritemSpacing:0.0f];
[flowLayout setSectionInset:UIEdgeInsetsMake(6,0,6,0)]; //(20, 20, 20, 20)];
_collectionView = [[RBInfiniteCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
[_collectionView registerNib:[UINib nibWithNibName:@"GameThumbnailCell" bundle: nil] forCellWithReuseIdentifier:@"ReuseCell"];
_collectionView.backgroundView = nil;
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.infiniteDelegate = self;
[self addSubview:_collectionView];
[self clearCollectionView];
}
return self;
}
- (instancetype) initWithFrame:(CGRect)frame {
if (self == [super initWithFrame:frame]) {
// init
self.backgroundColor = [UIColor clearColor];
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:[GameThumbnailCell getCellSize]];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
[flowLayout setMinimumLineSpacing:6.0f]; //32.0f];
[flowLayout setMinimumInteritemSpacing:0.0f];
[flowLayout setSectionInset:UIEdgeInsetsMake(6,0,6,0)]; //(20, 20, 20, 20)];
_collectionView = [[RBInfiniteCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout];
[_collectionView registerNib:[UINib nibWithNibName:@"GameThumbnailCell" bundle: nil] forCellWithReuseIdentifier:@"ReuseCell"];
_collectionView.backgroundView = nil;
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.infiniteDelegate = self;
[self addSubview:_collectionView];
[self clearCollectionView];
}
return self;
}
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
[_collectionView setFrame:self.bounds];
}
- (void) clearCollectionView
{
[_games removeAllObjects];
[_collectionView reloadData];
}
- (void) loadGamesForKeywords:(NSString*)keywords
{
if(_keywords == nil || ![_keywords isEqualToString:keywords])
{
_sortID = nil;
_keywords = keywords;
[self clearCollectionView];
}
[_collectionView loadElementsAsync];
}
- (void) loadGamesForSort:(NSNumber *)sortID playerID:(NSNumber *)playerID
{
[self loadGamesForSort:sortID playerID:playerID genreID:nil];
}
- (void) loadGamesForSort:(NSNumber *)sortID playerID:(NSNumber *)playerID genreID:(NSNumber *)genreID {
if(_sortID == nil || ![_sortID isEqualToNumber:sortID])
{
_sortID = sortID;
_keywords = nil;
[self clearCollectionView];
}
if (_genreID == nil || ![_genreID isEqualToNumber:genreID]) {
_genreID = genreID;
}
if (_playerID == nil || ![_playerID isEqualToNumber:playerID]) {
_playerID = playerID;
}
[_collectionView loadElementsAsync];
}
#pragma mark -
#pragma mark Infinite scroll delegates
- (void) asyncRequestItemsForCollectionView:(RBInfiniteCollectionView*)collectionView numItemsToRequest:(NSUInteger)itemsToRequest completionHandler:(void(^)())completionHandler
{
if(_collectionView.numItems > _games.count)
{
void(^block)(NSArray*) = ^(NSArray* games)
{
dispatch_async(dispatch_get_main_queue(), ^
{
[_games addObjectsFromArray:games];
completionHandler();
});
};
// If _sortID is set, this collection view displays the games belonging to a sort
// If _keywords is set, this collection view is used to display search results
if(_sortID != nil)
[RobloxData fetchGameListWithSortID:_sortID genreID:_genreID playerID:_playerID fromIndex:_games.count numGames:itemsToRequest thumbSize:[RobloxTheme sizeGameCoverSquare] completion:block];
else // if(_keywords != nil)
[RobloxData searchGames:_keywords fromIndex:_games.count numGames:itemsToRequest thumbSize:[RobloxTheme sizeGameCoverSquare] completion:block];
}
}
- (NSUInteger)numItemsInInfiniteCollectionView:(RBInfiniteCollectionView*)collectionView
{
return _games.count;
}
- (UICollectionViewCell*)infiniteCollectionView:(RBInfiniteCollectionView*)collectionView cellForItemAtIndexPath:(NSIndexPath*)indexPath;
{
GameThumbnailCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:@"ReuseCell" forIndexPath:indexPath];
if(indexPath.row < _games.count)
{
RBXGameData* gameData = _games[indexPath.row];
if([gameData isKindOfClass:[NSNull class]])
[cell setGameData:nil];
else
[cell setGameData:gameData];
}
else
{
[cell setGameData:nil];
}
return cell;
}
- (void)infiniteCollectionView:(RBInfiniteCollectionView*)collectionView didSelectItemAtIndexPath:(NSIndexPath*)indexPath;
{
if(indexPath.row < _games.count)
{
RBXGameData* gameData = _games[indexPath.row];
NSDictionary* userInfo = @{ @"gameData" : gameData,
@"gameIndex" : [NSNumber numberWithInteger:indexPath.row],
@"totalGames" : [NSNumber numberWithInteger:_games.count] };
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_SELECTED object:nil userInfo:userInfo];
}
}
@end
@@ -0,0 +1,515 @@
//
// MBProgressHUD.h
// Version 0.9
// Created by Matej Bukovinski on 2.4.09.
//
// This code is distributed under the terms and conditions of the MIT license.
// Copyright (c) 2013 Matej Bukovinski
//
// 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.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreGraphics/CoreGraphics.h>
@protocol MBProgressHUDDelegate;
typedef enum {
/** Progress is shown using an UIActivityIndicatorView. This is the default. */
MBProgressHUDModeIndeterminate,
/** Progress is shown using a round, pie-chart like, progress view. */
MBProgressHUDModeDeterminate,
/** Progress is shown using a horizontal progress bar */
MBProgressHUDModeDeterminateHorizontalBar,
/** Progress is shown using a ring-shaped progress view. */
MBProgressHUDModeAnnularDeterminate,
/** Shows a custom view */
MBProgressHUDModeCustomView,
/** Shows only labels */
MBProgressHUDModeText
} MBProgressHUDMode;
typedef enum {
/** Opacity animation */
MBProgressHUDAnimationFade,
/** Opacity + scale animation */
MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomIn
} MBProgressHUDAnimation;
#ifndef MB_INSTANCETYPE
#if __has_feature(objc_instancetype)
#define MB_INSTANCETYPE instancetype
#else
#define MB_INSTANCETYPE id
#endif
#endif
#ifndef MB_STRONG
#if __has_feature(objc_arc)
#define MB_STRONG strong
#else
#define MB_STRONG retain
#endif
#endif
#ifndef MB_WEAK
#if __has_feature(objc_arc_weak)
#define MB_WEAK weak
#elif __has_feature(objc_arc)
#define MB_WEAK unsafe_unretained
#else
#define MB_WEAK assign
#endif
#endif
#if NS_BLOCKS_AVAILABLE
typedef void (^MBProgressHUDCompletionBlock)();
#endif
/**
* Displays a simple HUD window containing a progress indicator and two optional labels for short messages.
*
* This is a simple drop-in class for displaying a progress HUD view similar to Apple's private UIProgressHUD class.
* The MBProgressHUD window spans over the entire space given to it by the initWithFrame constructor and catches all
* user input on this region, thereby preventing the user operations on components below the view. The HUD itself is
* drawn centered as a rounded semi-transparent view which resizes depending on the user specified content.
*
* This view supports four modes of operation:
* - MBProgressHUDModeIndeterminate - shows a UIActivityIndicatorView
* - MBProgressHUDModeDeterminate - shows a custom round progress indicator
* - MBProgressHUDModeAnnularDeterminate - shows a custom annular progress indicator
* - MBProgressHUDModeCustomView - shows an arbitrary, user specified view (@see customView)
*
* All three modes can have optional labels assigned:
* - If the labelText property is set and non-empty then a label containing the provided content is placed below the
* indicator view.
* - If also the detailsLabelText property is set then another label is placed below the first label.
*/
@interface MBProgressHUD : UIView
/**
* Creates a new HUD, adds it to provided view and shows it. The counterpart to this method is hideHUDForView:animated:.
*
* @param view The view that the HUD will be added to
* @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use
* animations while appearing.
* @return A reference to the created HUD.
*
* @see hideHUDForView:animated:
* @see animationType
*/
+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
/**
* Finds the top-most HUD subview and hides it. The counterpart to this method is showHUDAddedTo:animated:.
*
* @param view The view that is going to be searched for a HUD subview.
* @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use
* animations while disappearing.
* @return YES if a HUD was found and removed, NO otherwise.
*
* @see showHUDAddedTo:animated:
* @see animationType
*/
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
/**
* Finds all the HUD subviews and hides them.
*
* @param view The view that is going to be searched for HUD subviews.
* @param animated If set to YES the HUDs will disappear using the current animationType. If set to NO the HUDs will not use
* animations while disappearing.
* @return the number of HUDs found and removed.
*
* @see hideHUDForView:animated:
* @see animationType
*/
+ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated;
/**
* Finds the top-most HUD subview and returns it.
*
* @param view The view that is going to be searched.
* @return A reference to the last HUD subview discovered.
*/
+ (MB_INSTANCETYPE)HUDForView:(UIView *)view;
/**
* Finds all HUD subviews and returns them.
*
* @param view The view that is going to be searched.
* @return All found HUD views (array of MBProgressHUD objects).
*/
+ (NSArray *)allHUDsForView:(UIView *)view;
/**
* A convenience constructor that initializes the HUD with the window's bounds. Calls the designated constructor with
* window.bounds as the parameter.
*
* @param window The window instance that will provide the bounds for the HUD. Should be the same instance as
* the HUD's superview (i.e., the window that the HUD will be added to).
*/
- (id)initWithWindow:(UIWindow *)window;
/**
* A convenience constructor that initializes the HUD with the view's bounds. Calls the designated constructor with
* view.bounds as the parameter
*
* @param view The view instance that will provide the bounds for the HUD. Should be the same instance as
* the HUD's superview (i.e., the view that the HUD will be added to).
*/
- (id)initWithView:(UIView *)view;
/**
* Display the HUD. You need to make sure that the main thread completes its run loop soon after this method call so
* the user interface can be updated. Call this method when your task is already set-up to be executed in a new thread
* (e.g., when using something like NSOperation or calling an asynchronous call like NSURLRequest).
*
* @param animated If set to YES the HUD will appear using the current animationType. If set to NO the HUD will not use
* animations while appearing.
*
* @see animationType
*/
- (void)show:(BOOL)animated;
/**
* Hide the HUD. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to
* hide the HUD when your task completes.
*
* @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use
* animations while disappearing.
*
* @see animationType
*/
- (void)hide:(BOOL)animated;
/**
* Hide the HUD after a delay. This still calls the hudWasHidden: delegate. This is the counterpart of the show: method. Use it to
* hide the HUD when your task completes.
*
* @param animated If set to YES the HUD will disappear using the current animationType. If set to NO the HUD will not use
* animations while disappearing.
* @param delay Delay in seconds until the HUD is hidden.
*
* @see animationType
*/
- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay;
/**
* Shows the HUD while a background task is executing in a new thread, then hides the HUD.
*
* This method also takes care of autorelease pools so your method does not have to be concerned with setting up a
* pool.
*
* @param method The method to be executed while the HUD is shown. This method will be executed in a new thread.
* @param target The object that the target method belongs to.
* @param object An optional object to be passed to the method.
* @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will not use
* animations while (dis)appearing.
*/
- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated;
#if NS_BLOCKS_AVAILABLE
/**
* Shows the HUD while a block is executing on a background queue, then hides the HUD.
*
* @see showAnimated:whileExecutingBlock:onQueue:completionBlock:
*/
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block;
/**
* Shows the HUD while a block is executing on a background queue, then hides the HUD.
*
* @see showAnimated:whileExecutingBlock:onQueue:completionBlock:
*/
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion;
/**
* Shows the HUD while a block is executing on the specified dispatch queue, then hides the HUD.
*
* @see showAnimated:whileExecutingBlock:onQueue:completionBlock:
*/
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue;
/**
* Shows the HUD while a block is executing on the specified dispatch queue, executes completion block on the main queue, and then hides the HUD.
*
* @param animated If set to YES the HUD will (dis)appear using the current animationType. If set to NO the HUD will
* not use animations while (dis)appearing.
* @param block The block to be executed while the HUD is shown.
* @param queue The dispatch queue on which the block should be executed.
* @param completion The block to be executed on completion.
*
* @see completionBlock
*/
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue
completionBlock:(MBProgressHUDCompletionBlock)completion;
/**
* A block that gets called after the HUD was completely hidden.
*/
@property (copy) MBProgressHUDCompletionBlock completionBlock;
#endif
/**
* MBProgressHUD operation mode. The default is MBProgressHUDModeIndeterminate.
*
* @see MBProgressHUDMode
*/
@property (assign) MBProgressHUDMode mode;
/**
* The animation type that should be used when the HUD is shown and hidden.
*
* @see MBProgressHUDAnimation
*/
@property (assign) MBProgressHUDAnimation animationType;
/**
* The UIView (e.g., a UIImageView) to be shown when the HUD is in MBProgressHUDModeCustomView.
* For best results use a 37 by 37 pixel view (so the bounds match the built in indicator bounds).
*/
@property (MB_STRONG) UIView *customView;
/**
* The HUD delegate object.
*
* @see MBProgressHUDDelegate
*/
@property (MB_WEAK) id<MBProgressHUDDelegate> delegate;
/**
* An optional short message to be displayed below the activity indicator. The HUD is automatically resized to fit
* the entire text. If the text is too long it will get clipped by displaying "..." at the end. If left unchanged or
* set to @"", then no message is displayed.
*/
@property (copy) NSString *labelText;
/**
* An optional details message displayed below the labelText message. This message is displayed only if the labelText
* property is also set and is different from an empty string (@""). The details text can span multiple lines.
*/
@property (copy) NSString *detailsLabelText;
/**
* The opacity of the HUD window. Defaults to 0.8 (80% opacity).
*/
@property (assign) float opacity;
/**
* The color of the HUD window. Defaults to black. If this property is set, color is set using
* this UIColor and the opacity property is not used. using retain because performing copy on
* UIColor base colors (like [UIColor greenColor]) cause problems with the copyZone.
*/
@property (MB_STRONG) UIColor *color;
/**
* The x-axis offset of the HUD relative to the centre of the superview.
*/
@property (assign) float xOffset;
/**
* The y-axis offset of the HUD relative to the centre of the superview.
*/
@property (assign) float yOffset;
/**
* The amount of space between the HUD edge and the HUD elements (labels, indicators or custom views).
* Defaults to 20.0
*/
@property (assign) float margin;
/**
* The corner radius for the HUD
* Defaults to 10.0
*/
@property (assign) float cornerRadius;
/**
* Cover the HUD background view with a radial gradient.
*/
@property (assign) BOOL dimBackground;
/*
* Grace period is the time (in seconds) that the invoked method may be run without
* showing the HUD. If the task finishes before the grace time runs out, the HUD will
* not be shown at all.
* This may be used to prevent HUD display for very short tasks.
* Defaults to 0 (no grace time).
* Grace time functionality is only supported when the task status is known!
* @see taskInProgress
*/
@property (assign) float graceTime;
/**
* The minimum time (in seconds) that the HUD is shown.
* This avoids the problem of the HUD being shown and than instantly hidden.
* Defaults to 0 (no minimum show time).
*/
@property (assign) float minShowTime;
/**
* Indicates that the executed operation is in progress. Needed for correct graceTime operation.
* If you don't set a graceTime (different than 0.0) this does nothing.
* This property is automatically set when using showWhileExecuting:onTarget:withObject:animated:.
* When threading is done outside of the HUD (i.e., when the show: and hide: methods are used directly),
* you need to set this property when your task starts and completes in order to have normal graceTime
* functionality.
*/
@property (assign) BOOL taskInProgress;
/**
* Removes the HUD from its parent view when hidden.
* Defaults to NO.
*/
@property (assign) BOOL removeFromSuperViewOnHide;
/**
* Font to be used for the main label. Set this property if the default is not adequate.
*/
@property (MB_STRONG) UIFont* labelFont;
/**
* Color to be used for the main label. Set this property if the default is not adequate.
*/
@property (MB_STRONG) UIColor* labelColor;
/**
* Font to be used for the details label. Set this property if the default is not adequate.
*/
@property (MB_STRONG) UIFont* detailsLabelFont;
/**
* Color to be used for the details label. Set this property if the default is not adequate.
*/
@property (MB_STRONG) UIColor* detailsLabelColor;
/**
* The color of the activity indicator. Defaults to [UIColor whiteColor]
* Does nothing on pre iOS 5.
*/
@property (MB_STRONG) UIColor *activityIndicatorColor;
/**
* The progress of the progress indicator, from 0.0 to 1.0. Defaults to 0.0.
*/
@property (assign) float progress;
/**
* The minimum size of the HUD bezel. Defaults to CGSizeZero (no minimum size).
*/
@property (assign) CGSize minSize;
/**
* The actual size of the HUD bezel.
* You can use this to limit touch handling on the bezel aria only.
* @see https://github.com/jdg/MBProgressHUD/pull/200
*/
@property (atomic, assign, readonly) CGSize size;
/**
* Force the HUD dimensions to be equal if possible.
*/
@property (assign, getter = isSquare) BOOL square;
@end
@protocol MBProgressHUDDelegate <NSObject>
@optional
/**
* Called after the HUD was fully hidden from the screen.
*/
- (void)hudWasHidden:(MBProgressHUD *)hud;
@end
/**
* A progress view for showing definite progress by filling up a circle (pie chart).
*/
@interface MBRoundProgressView : UIView
/**
* Progress (0.0 to 1.0)
*/
@property (nonatomic, assign) float progress;
/**
* Indicator progress color.
* Defaults to white [UIColor whiteColor]
*/
@property (nonatomic, MB_STRONG) UIColor *progressTintColor;
/**
* Indicator background (non-progress) color.
* Defaults to translucent white (alpha 0.1)
*/
@property (nonatomic, MB_STRONG) UIColor *backgroundTintColor;
/*
* Display mode - NO = round or YES = annular. Defaults to round.
*/
@property (nonatomic, assign, getter = isAnnular) BOOL annular;
@end
/**
* A flat bar progress view.
*/
@interface MBBarProgressView : UIView
/**
* Progress (0.0 to 1.0)
*/
@property (nonatomic, assign) float progress;
/**
* Bar border line color.
* Defaults to white [UIColor whiteColor].
*/
@property (nonatomic, MB_STRONG) UIColor *lineColor;
/**
* Bar background color.
* Defaults to clear [UIColor clearColor];
*/
@property (nonatomic, MB_STRONG) UIColor *progressRemainingColor;
/**
* Bar progress color.
* Defaults to white [UIColor whiteColor].
*/
@property (nonatomic, MB_STRONG) UIColor *progressColor;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
//
// LeaderboardCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 6/6/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
#import "RobloxImageView.h"
@interface LeaderboardCell : UITableViewCell
@property (strong, nonatomic) IBOutlet UILabel* rankLabel;
@property (strong, nonatomic) IBOutlet UILabel *titleLabel;
@property (strong, nonatomic) IBOutlet UILabel *subtitleLabel;
@property (strong, nonatomic) IBOutlet UILabel *pointsLabel;
@property (strong, nonatomic) IBOutlet RobloxImageView *avatar;
- (void) showPlayerInfo:(RBXLeaderboardEntry*)data;
- (void) showClanInfo:(RBXLeaderboardEntry*)data;
@end
@@ -0,0 +1,49 @@
//
// LeaderboardCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 6/6/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "LeaderboardCell.h"
#import "RobloxImageView.h"
#define AVATAR_SIZE CGSizeMake(110, 110)
@implementation LeaderboardCell
{
}
- (void) showPlayerInfo:(RBXLeaderboardEntry*)data
{
_rankLabel.text = data.displayRank;
_titleLabel.text = data.name;
if(data.clanName != (id)[NSNull null])
{
_subtitleLabel.text = data.clanName;
}
else
{
_subtitleLabel.text = @"";
}
_avatar.animateInOptions = RBXImageViewAnimateInAlways;
[_avatar loadAvatarForUserID:data.userID prefetchedURL:data.userAvatarURL urlIsFinal:data.userAvatarIsFinal withSize:AVATAR_SIZE completion:nil];
_pointsLabel.text = data.displayPoints;
}
- (void) showClanInfo:(RBXLeaderboardEntry*)data
{
_rankLabel.text = data.displayRank;
_titleLabel.text = data.clanName;
_subtitleLabel.text = data.name;
_pointsLabel.text = data.displayPoints;
_avatar.animateInOptions = RBXImageViewAnimateInAlways;
[_avatar loadAvatarForUserID:data.clanEmblemID prefetchedURL:data.clanAvatarURL urlIsFinal:data.clanAvatarIsFinal withSize:AVATAR_SIZE completion:nil];
}
@end
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5056" systemVersion="13D65" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment defaultVersion="1536" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="80" id="KGk-i7-Jjw" customClass="LeaderboardCell">
<rect key="frame" x="0.0" y="0.0" width="490" height="63"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="490" height="62"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fEN-gm-uTF">
<rect key="frame" x="107" y="11" width="290" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="19"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VFL-fS-QzH">
<rect key="frame" x="107" y="35" width="290" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="rJP-qe-KGs" customClass="RobloxImageView">
<rect key="frame" x="51" y="11" width="44" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f9w-nw-pdb">
<rect key="frame" x="358" y="0.0" width="105" height="63"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="25"/>
<nil key="highlightedColor"/>
</label>
<label appearanceType="aqua" opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KSw-xh-nUM">
<rect key="frame" x="0.0" y="0.0" width="51" height="63"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="avatar" destination="rJP-qe-KGs" id="z0P-bq-4dV"/>
<outlet property="pointsLabel" destination="f9w-nw-pdb" id="iLw-vx-cfb"/>
<outlet property="rankLabel" destination="KSw-xh-nUM" id="LXC-nQ-kIR"/>
<outlet property="subtitleLabel" destination="VFL-fS-QzH" id="C8N-JY-qKW"/>
<outlet property="titleLabel" destination="fEN-gm-uTF" id="foG-Ar-fmG"/>
</connections>
</tableViewCell>
</objects>
</document>
@@ -0,0 +1,22 @@
//
// LeaderboardsViewController.h
// RobloxMobile
//
// Created by Ariel Lichtin on 6/16/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
@interface LeaderboardsViewController : UIViewController
@property (nonatomic) RBXLeaderboardsTimeFilter timeFilter;
@property (strong, nonatomic) NSNumber* distributorID;
// Callback
@property (copy, nonatomic) void (^onLeaderboardsLoaded)(BOOL hasLeaderboards);
- (void) updateLeaderboards;
@end
@@ -0,0 +1,420 @@
//
// LeaderboardViewController.m
// RobloxMobile
//
// Created by Ariel Lichtin on 6/16/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "LeaderboardsViewController.h"
#import "LeaderboardCell.h"
#import "RobloxData.h"
#import "UserInfo.h"
#import "RobloxTheme.h"
#import "LeaderboardsViewController.h"
#import "RBProfileViewController.h"
#import "RobloxNotifications.h"
#import "Flurry.h"
#define NUM_ITEMS_PER_REQUEST 20
#define START_REQUEST_THRESHOLD 10 // The next request will start when there are 10 elements left
#define CELL_SIZE CGSizeMake(490, 63)
#define AVATAR_SIZE CGSizeMake(110, 110)
#define LBDC_didPressLeaderboardPlayerItem @"LEADERBOARDS SCREEN - Leaderboard Player Item Pressed"
#define LBDC_didPressLeaderboardClaneItem @"LEADERBOARDS SCREEN - Leaderboard Clan Item Pressed"
@interface LeaderboardsViewController () <UITableViewDelegate, UITableViewDataSource>
@end
@implementation LeaderboardsViewController
{
IBOutlet UIActivityIndicatorView *_playersSpinner;
IBOutlet UIActivityIndicatorView *_clansSpinner;
IBOutlet UITableView* _playersTable;
IBOutlet UITableView* _clansTable;
IBOutlet UILabel* _playersTitle;
IBOutlet UILabel* _clansTitle;
LeaderboardCell* _currentUserPlayerCell;
LeaderboardCell* _currentUserClanCell;
NSMutableArray* _players;
NSMutableArray* _clans;
RBXLeaderboardEntry* _currentUserPlayerEntry;
RBXLeaderboardEntry* _currentUserClanEntry;
BOOL _initialized;
NSInteger _lastPlayerRequested;
NSInteger _lastClanRequested;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLeaderboards) name:RBX_NOTIFY_DID_LEAVE_GAME object:nil];
// Load the current user player header cell
{
_currentUserPlayerCell = [[[NSBundle mainBundle] loadNibNamed:@"LeaderboardCell" owner:nil options:nil] firstObject];
_currentUserPlayerCell.hidden = YES;
CGRect cellRect = _currentUserPlayerCell.frame;
cellRect.origin = _playersTable.frame.origin;
_currentUserPlayerCell.frame = cellRect;
[self applyThemeToLeaderboardUserCell:_currentUserPlayerCell];
[self.view insertSubview:_currentUserPlayerCell aboveSubview:_playersTable];
}
// Load the current user clan header cell
{
_currentUserClanCell = [[[NSBundle mainBundle] loadNibNamed:@"LeaderboardCell" owner:nil options:nil] firstObject];
_currentUserClanCell.hidden = YES;
CGRect cellRect = _currentUserClanCell.frame;
cellRect.origin = _clansTable.frame.origin;
_currentUserClanCell.frame = cellRect;
[self applyThemeToLeaderboardUserCell:_currentUserClanCell];
[self.view insertSubview:_currentUserClanCell aboveSubview:_clansTable];
}
_playersTitle.text = NSLocalizedString(@"PlayersWord", nil);
[RobloxTheme applyToTableHeaderTitle:_playersTitle];
_clansTitle.text = NSLocalizedString(@"ClansWord", nil);
[RobloxTheme applyToTableHeaderTitle:_clansTitle];
[_playersTable registerNib:[UINib nibWithNibName:@"LeaderboardCell" bundle:nil] forCellReuseIdentifier:@"ReuseCell"];
[_playersTable setAllowsSelection:YES];
[_playersTable setSeparatorStyle:UITableViewCellSeparatorStyleNone];
[_clansTable registerNib:[UINib nibWithNibName:@"LeaderboardCell" bundle:nil] forCellReuseIdentifier:@"ReuseCell"];
[_clansTable setAllowsSelection:YES];
[_clansTable setSeparatorStyle:UITableViewCellSeparatorStyleNone];
_players = [NSMutableArray array];
_clans = [NSMutableArray array];
_initialized = NO;
_lastPlayerRequested = 0;
_lastClanRequested = 0;
}
- (void) updateLeaderboards
{
if(!_initialized)
{
_initialized = YES;
[self fetchInitialPlayers];
[self fetchInitialClans];
}
}
- (void) fetchInitialPlayers
{
// Hide the tables and show the spinner
[_currentUserPlayerCell setHidden:YES];
[_playersSpinner setHidden:NO];
[_playersSpinner startAnimating];
[_playersTable setHidden:YES];
// Group the full leaderboard and user info request
dispatch_group_t group = dispatch_group_create();
// If a user is logged in, fetch the current user LB entry
UserInfo* currentPlayer = [UserInfo CurrentPlayer];
if(currentPlayer.userLoggedIn)
{
dispatch_group_enter(group);
[RobloxData fetchUserLeaderboardInfo:RBXLeaderboardsTargetUser
userID:currentPlayer.userId
distributorID:self.distributorID
timeFilter:self.timeFilter
avatarSize:AVATAR_SIZE
completion:^(RBXLeaderboardEntry *userEntry)
{
_currentUserPlayerEntry = userEntry;
dispatch_group_leave(group);
}];
}
// Fetch the first N leaderboard entries
dispatch_group_enter(group);
[RobloxData fetchLeaderboardsFor:RBXLeaderboardsTargetUser
timeFilter:self.timeFilter
distributorID:self.distributorID
startIndex:0
numItems:NUM_ITEMS_PER_REQUEST
lastEntry:nil
avatarSize:AVATAR_SIZE
completion:^(NSArray *leaderboard)
{
if(leaderboard != nil)
{
[_players addObjectsFromArray:leaderboard];
}
dispatch_group_leave(group);
}];
// Execute the following block when both operations are complete.
dispatch_group_notify(group, dispatch_get_main_queue(), ^
{
if(_currentUserPlayerEntry != nil)
{
// Show the user entry and resize the table
[_currentUserPlayerCell showPlayerInfo:_currentUserPlayerEntry];
_currentUserPlayerCell.hidden = NO;
CGRect rect = _playersTable.frame;
rect.origin.y += _currentUserPlayerCell.frame.size.height;
rect.size.height -= _currentUserPlayerCell.frame.size.height;
_playersTable.frame = rect;
}
// Show the table, hide the spinner
[_playersTable reloadData];
[_playersTable setHidden:NO];
[_playersSpinner stopAnimating];
[_playersSpinner setHidden:YES];
});
}
- (void) fetchInitialClans
{
// Hide the tables and show the spinner
[_currentUserClanCell setHidden:YES];
[_clansSpinner setHidden:NO];
[_clansSpinner startAnimating];
[_clansTable setHidden:YES];
// Group the full leaderboard and user info request
dispatch_group_t group = dispatch_group_create();
// If a user is logged in, fetch the current user LB entry
UserInfo* currentPlayer = [UserInfo CurrentPlayer];
if(currentPlayer.userLoggedIn)
{
dispatch_group_enter(group);
[RobloxData fetchUserLeaderboardInfo:RBXLeaderboardsTargetClan
userID:currentPlayer.userId
distributorID:self.distributorID
timeFilter:self.timeFilter
avatarSize:AVATAR_SIZE
completion:^(RBXLeaderboardEntry *userEntry)
{
_currentUserClanEntry = userEntry;
dispatch_group_leave(group);
}];
}
// Fetch the first N leaderboard entries
dispatch_group_enter(group);
[RobloxData fetchLeaderboardsFor:RBXLeaderboardsTargetClan
timeFilter:self.timeFilter
distributorID:self.distributorID
startIndex:0
numItems:NUM_ITEMS_PER_REQUEST
lastEntry:nil
avatarSize:AVATAR_SIZE
completion:^(NSArray *leaderboard)
{
if(leaderboard != nil)
{
[_clans addObjectsFromArray:leaderboard];
}
dispatch_group_leave(group);
}];
// Execute the following block when both operations are complete.
dispatch_group_notify(group, dispatch_get_main_queue(), ^
{
if(_currentUserClanEntry != nil)
{
// Show the user entry and resize the table
[_currentUserClanCell showPlayerInfo:_currentUserClanEntry];
_currentUserClanCell.hidden = NO;
CGRect rect = _clansTable.frame;
rect.origin.y += _currentUserClanCell.frame.size.height;
rect.size.height -= _currentUserClanCell.frame.size.height;
_clansTable.frame = rect;
}
// Show the table, hide the spinner
[_clansTable reloadData];
[_clansTable setHidden:NO];
[_clansSpinner stopAnimating];
[_clansSpinner setHidden:YES];
if(self.onLeaderboardsLoaded)
{
BOOL hasLeaderboards = _players != nil && _players.count > 0;
self.onLeaderboardsLoaded(hasLeaderboards);
}
});
}
- (void) fetchMorePlayers
{
_lastPlayerRequested = _players.count;
[RobloxData fetchLeaderboardsFor:RBXLeaderboardsTargetUser
timeFilter:self.timeFilter
distributorID:self.distributorID
startIndex:_players.count
numItems:NUM_ITEMS_PER_REQUEST
lastEntry:[_players lastObject]
avatarSize:AVATAR_SIZE
completion:^(NSArray *leaderboard)
{
if(leaderboard != nil)
{
[_players addObjectsFromArray:leaderboard];
dispatch_async(dispatch_get_main_queue(), ^
{
NSMutableArray* items = [NSMutableArray array];
for(int i = _players.count - leaderboard.count; i < _players.count; ++i)
{
[items addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[_playersTable insertRowsAtIndexPaths:items withRowAnimation:UITableViewRowAnimationNone];
});
}
}];
}
- (void) fetchMoreClans
{
_lastClanRequested = _clans.count;
[RobloxData fetchLeaderboardsFor:RBXLeaderboardsTargetClan
timeFilter:self.timeFilter
distributorID:self.distributorID
startIndex:_clans.count
numItems:NUM_ITEMS_PER_REQUEST
lastEntry:[_clans lastObject]
avatarSize:AVATAR_SIZE
completion:^(NSArray *leaderboard)
{
if(leaderboard != nil)
{
[_clans addObjectsFromArray:leaderboard];
dispatch_async(dispatch_get_main_queue(), ^
{
NSMutableArray* items = [NSMutableArray array];
for(int i = _clans.count - leaderboard.count; i < _clans.count; ++i)
{
[items addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[_clansTable insertRowsAtIndexPaths:items withRowAnimation:UITableViewRowAnimationNone];
});
}
}];
}
#pragma odd style functions
-(void) applyThemeToLeaderboardUserCell:(LeaderboardCell*)cell
{
cell.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:14];
cell.titleLabel.textColor = [UIColor colorWithRed:(0x41/255.0f) green:(0x63/255.0f) blue:(0x99/255.0f) alpha:1.0f];
cell.subtitleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:12];
cell.subtitleLabel.textColor = [UIColor colorWithWhite:(0x97/255.0f) alpha:1.0f];
cell.rankLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:13];
cell.rankLabel.textColor = [UIColor colorWithWhite:(0x97/255.0f) alpha:1.0f];
cell.pointsLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:14];
cell.pointsLabel.textColor = [UIColor colorWithRed:(0x41/255.0f) green:(0x63/255.0f) blue:(0x99/255.0f) alpha:1.0f];
// Add a separator line for this "header" cell
UIImageView* separator = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Separator"]];
CGRect frame = cell.contentView.bounds;
frame.origin.y = frame.size.height - 1;
frame.size.height = 1;
separator.frame = frame;
[cell.contentView addSubview:separator];
}
#pragma Delegate functions
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSArray* elements = tableView == _playersTable ? _players : _clans;
return elements != nil ? elements.count : 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
LeaderboardCell* cell = [tableView dequeueReusableCellWithIdentifier:@"ReuseCell" forIndexPath:indexPath];
if([tableView isEqual:_playersTable])
{
[cell showPlayerInfo:_players[indexPath.row]];
if(_lastPlayerRequested < _players.count && indexPath.row + START_REQUEST_THRESHOLD > _players.count)
[self fetchMorePlayers];
}
else if([tableView isEqual:_clansTable])
{
[cell showClanInfo:_clans[indexPath.row]];
if(_lastClanRequested < _clans.count && indexPath.row + START_REQUEST_THRESHOLD > _clans.count)
[self fetchMoreClans];
}
//[RobloxTheme applyToLeaderboardCell:cell];
cell.titleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:14];
cell.titleLabel.textColor = [UIColor colorWithWhite:(0x34/255.0f) alpha:1.0f];
cell.subtitleLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:12];
cell.subtitleLabel.textColor = [UIColor colorWithWhite:(0x97/255.0f) alpha:1.0f];
cell.rankLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:13];
cell.rankLabel.textColor = [UIColor colorWithWhite:(0x97/255.0f) alpha:1.0f];
cell.pointsLabel.font = [UIFont fontWithName:@"SourceSansPro-Regular" size:14];
cell.pointsLabel.textColor = [UIColor colorWithWhite:(0x34/255.0f) alpha:1.0f];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
RBProfileViewController *viewController = (RBProfileViewController*)
[self.storyboard instantiateViewControllerWithIdentifier:@"RBOthersProfileViewController"];
if ([tableView isEqual:_playersTable]) {
[Flurry logEvent:LBDC_didPressLeaderboardPlayerItem];
RBXLeaderboardEntry* player = _players[indexPath.row];
viewController.userId = @(player.userID);
} else if ([tableView isEqual:_clansTable]) {
[Flurry logEvent:LBDC_didPressLeaderboardClaneItem];
RBXLeaderboardEntry* player = _clans[indexPath.row];
viewController.userId = @(player.userID);
}
[self.navigationController pushViewController:viewController animated:YES];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return CELL_SIZE.height;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
@@ -0,0 +1,24 @@
//
// MoreTileButton.h
// RobloxMobile
//
// Created by Kyler Mulherin on 12/9/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface MoreTileButton : UIButton
@property (nonatomic) IBInspectable NSInteger layoutStyle;
@property (nonatomic) IBInspectable UIImage* imageName;
@property (nonatomic) IBInspectable UIImage* imageDownName;
@property (nonatomic) IBInspectable NSString* iconLabel;
@property (nonatomic) IBInspectable BOOL isEnabled;
//for use with Sponsored Event posts, don't forget to cast it
@property (nonatomic) id extraInfo;
-(void) setBadgeValue:(NSString*)value;
@end
@@ -0,0 +1,218 @@
//
// MoreTileButton.m
// RobloxMobile
//
// Created by Kyler Mulherin on 12/9/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "MoreTileButton.h"
#import "RobloxTheme.h"
#import "RobloxInfo.h"
#import "UIView+Position.h"
#define IPHONE_ICON_SIZE CGRectMake(0,0,28,28)
#define IPAD_ICON_SIZE CGRectMake(0,0,40,40)
#define LABEL_HEIGHT 30
#define DEFAULT_IMAGE_NAME @"Icon Home Off"
#define DEFAULT_LABEL_TEXT @"_BUTTON_TEXT_"
@implementation MoreTileButton
{
UIImageView* imgIcon;
UILabel* lblTitle;
UILabel* lblBadge;
}
-(id) initWithFrame:(CGRect)frame
{
//this gets called by Interface Builder, and in code if you're stupid
self = [super initWithFrame:frame];
if (self)
{
[self initDefaults];
}
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
[self initDefaults];
}
return self;
}
-(void) initDefaults
{
self.enabled = YES;
//initialize the elements
imgIcon = [[UIImageView alloc] initWithFrame:([RobloxInfo thisDeviceIsATablet] ? IPAD_ICON_SIZE : IPHONE_ICON_SIZE)];
[imgIcon setTintColor:[UIColor blackColor]];
[imgIcon setHighlighted:YES];
[imgIcon setContentMode:UIViewContentModeScaleAspectFit];
[self addSubview:imgIcon];
lblTitle = [[UILabel alloc] initWithFrame:CGRectMake(0,0,self.frame.size.width, LABEL_HEIGHT)];
[lblTitle setTextAlignment:NSTextAlignmentCenter];
[RobloxTheme applyToGamePreviewDetailText:lblTitle];
[lblTitle setTextColor:[UIColor blackColor]];
[self addSubview:lblTitle];
[self setBackgroundColor:[UIColor whiteColor]];
self.clipsToBounds = NO;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(0,5);
self.layer.shadowOpacity = 0.5;
[self addTarget:self action:@selector(setToDownState) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(setToUpState) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(setToUpState) forControlEvents:UIControlEventTouchUpOutside];
lblBadge = [[UILabel alloc]initWithFrame:CGRectMake(23,0, 13, 13)];
lblBadge.textColor = [UIColor whiteColor];
lblBadge.textAlignment = NSTextAlignmentCenter;
lblBadge.layer.borderWidth = 1;
lblBadge.layer.cornerRadius = 8;
lblBadge.layer.masksToBounds = YES;
lblBadge.layer.borderColor =[[UIColor clearColor] CGColor];
lblBadge.layer.shadowColor = [[UIColor clearColor] CGColor];
lblBadge.layer.shadowOffset = CGSizeMake(0.0, 0.0);
lblBadge.layer.shadowOpacity = 0.0;
lblBadge.backgroundColor = [UIColor redColor];
lblBadge.font = [UIFont fontWithName:@"ArialMT" size:11];
}
-(void) layoutSubviews
{
[super layoutSubviews];
[imgIcon centerInFrame:CGRectMake(0, 0, self.width, self.height)];
switch (_layoutStyle)
{
case 1:
{
//square, main buttons
[imgIcon setY:imgIcon.y - (LABEL_HEIGHT * 0.5)];
[lblTitle setY:imgIcon.bottom];
[lblTitle setWidth:self.width];
[lblBadge setX:(imgIcon.x + imgIcon.width * 0.8)];
[lblBadge setY:(imgIcon.y - lblBadge.height * 0.5)];
} break;
case 2:
{
//long, horizontal buttons
[imgIcon setX:20];
[lblTitle centerInFrame:CGRectMake(0, 0, self.width, self.height)];
[lblBadge setX:(imgIcon.x + imgIcon.width * 0.8)];
[lblBadge setY:(imgIcon.y - lblBadge.height * 0.5)];
} break;
case 3:
{
//scale the event icon image to fit the button
CGSize imgSize = CGSizeMake(117, 30);
float wideTimes = floorf(self.width / imgSize.width);
float tallTimes = floorf(self.height / imgSize.height);
CGSize iconSize = (wideTimes > tallTimes) ? CGSizeMake(imgSize.width * tallTimes, imgSize.height * tallTimes)
: CGSizeMake(imgSize.width * wideTimes, imgSize.height * wideTimes);
[imgIcon setFrame:CGRectMake(0, 0, iconSize.width, iconSize.height)];
[imgIcon centerInFrame:self.bounds];
lblTitle.hidden = YES;
self.layer.shadowOpacity = 0.25;
} break;
}
}
-(void) prepareForInterfaceBuilder
{
[self.layer setBorderColor:[UIColor blackColor].CGColor];
[self.layer setBorderWidth:1];
}
//Mutators
-(void)setImageName:(UIImage *)imageName
{
_imageName = imageName;
[imgIcon setImage:_imageName];
}
-(void)setIconLabel:(NSString *)iconLabel
{
_iconLabel = iconLabel;
[lblTitle setText:_iconLabel];
}
-(void)setLayoutStyle:(NSInteger)layoutStyle
{
_layoutStyle = layoutStyle;
[self layoutSubviews];
}
-(void)setFrame:(CGRect)frame
{
[super setFrame:frame];
[self setLayoutStyle:_layoutStyle];
}
-(void)setIsEnabled:(BOOL)isEnabled
{
if (isEnabled)
{
[self setToUpState];
self.layer.opacity = 1.0;
}
else
{
self.backgroundColor = [UIColor lightGrayColor];
self.layer.opacity = 0.3;
}
self.enabled = isEnabled;
_isEnabled = isEnabled;
}
-(void)setBadgeValue:(NSString *)value
{
if (value)
{
lblBadge.text = value;
[lblBadge setWidth: (10 + [value sizeWithAttributes:@{NSFontAttributeName: lblBadge.font}].width)];
//add the badge
if (![self.subviews containsObject:lblBadge])
[self addSubview:lblBadge];
}
else
{
if ([self.subviews containsObject:lblBadge])
[lblBadge removeFromSuperview];
}
}
//Button Stylings
-(void) setToDownState
{
if (_layoutStyle == 3 || !self.enabled)
return;
self.backgroundColor = [UIColor lightGrayColor];
[imgIcon setImage:_imageDownName];
}
-(void) setToUpState
{
if (_layoutStyle == 3)
return;
self.backgroundColor = [UIColor whiteColor];
[imgIcon setImage:_imageName];
}
@end
@@ -0,0 +1,25 @@
//
// NativeSearchNavItem.h
// RobloxMobile
//
// Created by Kyler Mulherin on 11/10/14.
// Modified from code by Ariel Lichten.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
@interface NativeSearchNavItem : UIBarButtonItem
typedef NS_ENUM(NSInteger, SearchStatus)
{
SearchStatusOpen,
SearchStatusClosed
};
- (id)initWithSearchType:(SearchResultType)searchType
andContainer:(UIViewController*)target
compactMode:(BOOL)compactMode;
@end
@@ -0,0 +1,364 @@
//
// NativeSearchNavItem.m
// RobloxMobile
//
// Created by Kyler Mulherin on 11/10/14.
// Modified from code by Ariel Lichtin.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "NativeSearchNavItem.h"
#import "GameSearchResultCell.h"
#import "UserSearchResultCell.h"
#import "RobloxData.h"
#import "RobloxTheme.h"
#import "RobloxInfo.h"
#import "RobloxNotifications.h"
#import "RBMobileWebViewController.h"
#import "UIViewController+Helpers.h"
#import <UIKit/UIKit.h>
#define GAME_THUMBNAIL_SIZE CGSizeMake(110, 110)
#define GAME_ITEM_SIZE CGSizeMake(312, 64)
#define USER_THUMBNAIL_SIZE CGSizeMake(110, 110)
#define USER_ITEM_SIZE CGSizeMake(312, 64)
@interface NativeSearchNavItem () <UISearchBarDelegate, UICollectionViewDelegate, UICollectionViewDataSource>
@end
@implementation NativeSearchNavItem
{
CGRect _baseFrame;
UISearchBar* _searchBar;
SearchType _searchType;
BOOL _compactMode;
// Parent ViewController
__weak UIViewController* _containerController;
UIView* _customView;
UIButton* _iconButton;
// Navigation items that are hidden while the search bar is active
NSArray* _navLeftItems;
NSArray* _navRightItems;
NSString* _navTitle;
UICollectionView* _collectionView;
SearchStatus _status;
NSArray* _searchData; //could be games, users, etc.
}
- (id)initWithSearchType:(SearchType)searchType
andContainer:(UIViewController*)target
compactMode:(BOOL)compactMode
{
self = [super init];
if (self)
{
_compactMode = compactMode;
UIImage* image = [UIImage imageNamed:@"Search"];
_iconButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_iconButton setImage:image forState:UIControlStateNormal];
[_iconButton setFrame:CGRectMake(0, 0, image.size.width, image.size.height)];
[_iconButton addTarget:self action:@selector(closeButtonTouched) forControlEvents:UIControlEventTouchUpInside];
_status = SearchStatusClosed;
_searchType = searchType;
if(_compactMode)
_baseFrame = _iconButton.frame;
else
_baseFrame = CGRectMake(0, 0, 160, 26);
_containerController = target;
// Initialize search bar
_searchBar = [[UISearchBar alloc] initWithFrame:_baseFrame];
_searchBar.delegate = self;
_searchBar.backgroundColor = [UIColor clearColor];
_searchBar.spellCheckingType = UITextSpellCheckingTypeNo;
_searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
_searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
if( [_searchBar respondsToSelector:@selector(setSearchBarStyle:)] )
{
_searchBar.searchBarStyle = UISearchBarStyleMinimal;
}
_customView = [[UIView alloc] initWithFrame:_baseFrame];
[_customView addSubview:_iconButton];
[_customView addSubview:_searchBar];
[self setCustomView:_customView];
_searchBar.hidden = _compactMode;
_iconButton.hidden = !_compactMode;
// Initialize game results view
UICollectionViewFlowLayout* layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = 50.0f;
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.sectionInset = UIEdgeInsetsMake(45.0f, 26.0f, 20.0f, 26.0f);
_collectionView = [[UICollectionView alloc] initWithFrame:_containerController.view.bounds collectionViewLayout:(UICollectionViewLayout *)layout];
_collectionView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.8f];
_collectionView.delegate = self;
_collectionView.dataSource = self;
_collectionView.scrollEnabled = NO;
//initialize some context specific items
if (_searchType == SearchTypeUsers)
{
_searchBar.placeholder = NSLocalizedString(@"SearchUsersPhrase", nil);
layout.itemSize = USER_ITEM_SIZE;
[_collectionView registerNib:[UINib nibWithNibName:@"UserSearchResultCell" bundle:nil] forCellWithReuseIdentifier:@"ReuseCell"];
}
else if (_searchType == SearchTypeGames)
{
_searchBar.placeholder = NSLocalizedString(@"SearchGamesPhrase", nil);
layout.itemSize = GAME_ITEM_SIZE;
[_collectionView registerNib:[UINib nibWithNibName:@"GameSearchResultCell" bundle:nil] forCellWithReuseIdentifier:@"ReuseCell"];
}
[RobloxTheme applyToSearchNavItem:_searchBar];
}
return self;
}
- (void)openSearch
{
if(_status == SearchStatusOpen)
return;
_searchBar.hidden = NO;
_iconButton.hidden = YES;
_status = SearchStatusOpen;
[_searchBar setShowsCancelButton:YES];
CGRect visibleFrame = _containerController.view.bounds;
CGFloat windowWidth = visibleFrame.size.width;
// Animate the search bar in
[UIView animateWithDuration:0.3f
animations:^
{
// Make space in the navigation bar for the search bar
_containerController.navigationItem.hidesBackButton = YES;
_navLeftItems = _containerController.navigationItem.leftBarButtonItems;
_navRightItems = _containerController.navigationItem.rightBarButtonItems;
_navTitle = _containerController.navigationItem.title;
_containerController.navigationItem.titleView.hidden = YES;
_containerController.navigationItem.title = @"";
[_containerController.navigationItem setLeftBarButtonItems:[NSArray array]];
[_containerController.navigationItem setRightBarButtonItems:[NSArray arrayWithObject:self]];
_customView.frame = CGRectMake(0, 0, windowWidth - 30.0f, _baseFrame.size.height);
_searchBar.frame = CGRectMake(0, 0, windowWidth - 30.0f, _baseFrame.size.height);
}
completion:^(BOOL finished)
{
[_searchBar becomeFirstResponder];
}];
_collectionView.frame = visibleFrame;
[_containerController.view addSubview:_collectionView];
}
- (void)closeSearch
{
if(_status == SearchStatusClosed)
return;
_status = SearchStatusClosed;
[_searchBar resignFirstResponder];
[UIView animateWithDuration:0.3f
animations:^
{
_customView.frame = _baseFrame;
_searchBar.frame = _baseFrame;
[_searchBar setShowsCancelButton:NO animated:YES];
}
completion:^(BOOL finished)
{
// Restore the navigation bar
[_containerController.navigationItem setHidesBackButton: NO];
[_containerController.navigationItem setLeftBarButtonItems:_navLeftItems animated:YES];
[_containerController.navigationItem setRightBarButtonItems:_navRightItems animated:YES];
[_containerController.navigationItem setTitle:_navTitle];
_containerController.navigationItem.titleView.hidden = NO;
// Animate and release the focus from the search bar
[_searchBar endEditing:YES];
[_searchBar resignFirstResponder];
_searchBar.hidden = _compactMode;
_iconButton.hidden = !_compactMode;
}
];
[_collectionView removeFromSuperview];
}
- (void) closeSearchFast
{
if(_status == SearchStatusClosed)
return;
_status = SearchStatusClosed;
[_searchBar resignFirstResponder];
// Restore the navigation bar
[_containerController.navigationItem setHidesBackButton: NO];
[_containerController.navigationItem setLeftBarButtonItems:_navLeftItems animated:YES];
[_containerController.navigationItem setRightBarButtonItems:_navRightItems animated:YES];
[_containerController.navigationItem setTitle:_navTitle];
_containerController.navigationItem.titleView.hidden = NO;
// Animate and release the focus from the search bar
_customView.frame = _baseFrame;
_searchBar.frame = _baseFrame;
[_searchBar setShowsCancelButton:NO animated:YES];
[_searchBar endEditing:YES];
[_searchBar resignFirstResponder];
_searchBar.hidden = _compactMode;
_iconButton.hidden = !_compactMode;
}
- (void)closeButtonTouched
{
[self openSearch];
}
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
[self openSearch];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[self closeSearch];
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:_searchBar.text forKey:@"keywords"];
if (_searchType == SearchTypeGames)
{
[[NSNotificationCenter defaultCenter] postNotificationName:RBXNotificationSearchGames object:nil userInfo:userInfo];
}
else if (_searchType == SearchTypeUsers)
{
//Open a webview with the results
CGRect currentScreenRect = CGRectMake(_containerController.view.frame.origin.x, _containerController.view.frame.origin.y,
_containerController.view.frame.size.width, _containerController.view.frame.size.height);
RBMobileWebViewController* results = [[RBMobileWebViewController alloc] initWithNavButtons:NO];
[results.view setFrame:currentScreenRect];
NSString* endpointFormat = [RobloxInfo thisDeviceIsATablet] ? @"%@/users/search?keyword=%@" : @"%@people?search=%@";
[results setUrl:[NSString stringWithFormat:endpointFormat, [RobloxInfo getBaseUrl], _searchBar.text]];
[self closeSearchFast];
[_containerController.navigationController pushViewController:results animated:YES];
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *) searchBar
{
searchBar.text = nil;
[self closeSearch];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if(searchText.length == 0)
{
_searchData = nil;
[_collectionView reloadData];
}
else
{
if (_searchType == SearchTypeGames)
{
[RobloxData searchGames:searchText fromIndex:0 numGames:9 thumbSize:GAME_THUMBNAIL_SIZE completion:^(NSArray *games)
{
dispatch_async(dispatch_get_main_queue(), ^
{
if([searchText length] && [searchText isEqualToString:searchBar.text] )
{
_searchData = games;
[_collectionView reloadData];
}
});
}];
}
else if (_searchType == SearchTypeUsers)
{
/*[RobloxData searchUsers:searchText fromIndex:0 numGames:9 thumbSize:USER_THUMBNAIL_SIZE completion:^(NSArray *users)
{
dispatch_async(dispatch_get_main_queue(), ^
{
if([searchText length] && [searchText isEqualToString:searchBar.text] )
{
_searchData = users;
[_collectionView reloadData];
}
});
}];*/
}
}
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
[self closeSearch];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _searchData ? _searchData.count : 0;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//grab the generic cell
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ReuseCell" forIndexPath:indexPath];
if (_searchType == SearchTypeGames)
{
RBXGameData* gameData = _searchData[indexPath.row];
[(GameSearchResultCell*)cell setGameData:gameData];
}
else if (_searchType == SearchTypeUsers)
{
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self closeSearch];
//[collectionView deselectItemAtIndexPath:indexPath animated:YES];
if (_searchType == SearchTypeGames)
{
RBXGameData* gameData = _searchData[indexPath.row];
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:gameData forKey:@"gameData"];
[[NSNotificationCenter defaultCenter] postNotificationName:RBXNotificationGameSelected object:nil userInfo:userInfo];
}
else if (_searchType == SearchTypeUsers)
{
}
}
@end
@@ -0,0 +1,586 @@
//
// NativeSearchNavItem.m
// RobloxMobile
//
// Created by Kyler Mulherin on 11/10/14.
// Modified from code by Ariel Lichtin.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "NativeSearchNavItem.h"
#import "GameSearchResultCell.h"
#import "UserSearchResultCell.h"
#import "RobloxTheme.h"
#import "RobloxInfo.h"
#import "RobloxNotifications.h"
#import "RBMobileWebViewController.h"
#import "UIViewController+Helpers.h"
#import "FastLog.h"
#import "RBXEventReporter.h"
#import "NSDictionary+Parsing.h"
#import "UIView+Position.h"
#import <UIKit/UIKit.h>
//flags
FASTFLAGVARIABLE(NativeSearchFastResultsEnabled, false)
FASTINTVARIABLE(NativeSearchFastResultsWaitTime, 1000)
#define FAST_RESULT_TIMER_INTERVAL 500.0f
#define NUM_TO_DISPLAY_PHONE 2
#define NUM_TO_DISPLAY_TABLET 5
@interface NativeSearchNavItem () <UISearchBarDelegate, UICollectionViewDelegate, UICollectionViewDataSource>
@end
@implementation NativeSearchNavItem
{
CGRect _baseFrame;
UISearchBar* _searchBar;
SearchResultType _searchType;
BOOL _compactMode;
// Parent ViewController
__weak UIViewController* _containerController;
UIView* _customView;
UIButton* _iconButton;
UILabel* _lblAreYouSearching;
UILabel* _lblForMoreSuggestions;
// Navigation items that are hidden while the search bar is active
NSArray* _navLeftItems;
NSArray* _navRightItems;
NSString* _navTitle;
UICollectionView* _collectionView;
SearchStatus _status;
NSNumber* _keyboardHeight;
NSArray* _searchData; //could be games, users, etc.
bool _searchCompleted; //used to check if we've already fired a search
int _timeBeforeSearch; //a counter for searching
NSTimer* _searchWaitTimer; //a timer for counting how long to wait
}
- (id)initWithSearchType:(SearchResultType)searchType
andContainer:(UIViewController*)target
compactMode:(BOOL)compactMode
{
self = [super init];
if (self)
{
//initialize some properties
_compactMode = compactMode;
_status = SearchStatusClosed;
_searchType = searchType;
_containerController = target;
_keyboardHeight = nil;
//Initialize the nav-bar icon
UIImage* image = [UIImage imageNamed:@"Icon Search White"];
_iconButton = [UIButton buttonWithType:UIButtonTypeCustom];
_iconButton.hidden = !_compactMode;
[_iconButton setImage:image forState:UIControlStateNormal];
[_iconButton setFrame:CGRectMake(0, 0, image.size.width, image.size.height)];
[_iconButton addTarget:self action:@selector(openSearch) forControlEvents:UIControlEventTouchUpInside];
// Initialize the base frame
_baseFrame = _compactMode ? _iconButton.frame : CGRectMake(0, 0, 160, 26);
// Initialize search bar
_searchBar = [[UISearchBar alloc] initWithFrame:_baseFrame];
_searchBar.hidden = _compactMode;
_searchBar.delegate = self;
_searchBar.backgroundColor = [UIColor clearColor];
_searchBar.spellCheckingType = UITextSpellCheckingTypeNo;
_searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
_searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
if( [_searchBar respondsToSelector:@selector(setSearchBarStyle:)] )
_searchBar.searchBarStyle = UISearchBarStyleMinimal;
// Initialize the custom nav-bar view
_customView = [[UIView alloc] initWithFrame:_baseFrame];
[_customView addSubview:_iconButton];
[_customView addSubview:_searchBar];
[self setCustomView:_customView];
//initialize some layout properties
float edgeInsetSidesAmount = [RobloxInfo thisDeviceIsATablet] ? 26.0f : 4.0f;
UICollectionViewFlowLayout* layout = [[UICollectionViewFlowLayout alloc] init];
layout.minimumLineSpacing = [RobloxInfo thisDeviceIsATablet] ? 50.0f : 20.0f;
layout.minimumInteritemSpacing = [RobloxInfo thisDeviceIsATablet] ? 10.0f : 0.0f;
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.sectionInset = UIEdgeInsetsMake(45.0f, edgeInsetSidesAmount, 20.0f, edgeInsetSidesAmount);
//initialize some context specific items
NSString* cellTypeName;
switch (_searchType)
{
case (SearchResultUsers) :
{
_searchBar.placeholder = NSLocalizedString(@"SearchUsersPhrase", nil);
cellTypeName = [UserSearchResultCell getNibName];
layout.itemSize = [UserSearchResultCell getCellSize];
} break;
case (SearchResultGames) :
{
_searchBar.placeholder = NSLocalizedString(@"SearchGamesPhrase", nil);
layout.itemSize = [GameSearchResultCell getCellSize];
cellTypeName = [GameSearchResultCell getNibName];
} break;
case (SearchResultGroups) : {} break;
case (SearchResultCatalog) : {} break;
}
if (layout.itemSize.width != 0)
{
CGSize cellSize = layout.itemSize;
NSInteger numberOfCells = _containerController.view.frame.size.width / cellSize.width ;
NSInteger contentWidth = numberOfCells * cellSize.width;
NSInteger edgeInsets = (_containerController.view.frame.size.width - contentWidth) / (numberOfCells + 1);
layout.sectionInset = UIEdgeInsetsMake(45.0f,edgeInsets,layout.minimumLineSpacing,edgeInsets);
}
// Initialize results view
_collectionView = [[UICollectionView alloc] initWithFrame:_containerController.view.bounds collectionViewLayout:layout];
_collectionView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.8f];
_collectionView.delegate = self;
_collectionView.dataSource = self;
_collectionView.scrollEnabled = NO;
if (cellTypeName)
[_collectionView registerNib:[UINib nibWithNibName:cellTypeName bundle:nil] forCellWithReuseIdentifier:@"ReuseCell"];
//Initialize the helper text
int labelHeight = 20;
UIFont* fontToUse = [RobloxInfo thisDeviceIsATablet] ? [RobloxTheme fontBody] : [RobloxTheme fontBodySmall];
_lblAreYouSearching = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, _collectionView.frame.size.width, labelHeight)];
[_lblAreYouSearching setFont:fontToUse];
[_lblAreYouSearching setTextColor:[RobloxTheme colorGray4]];
[_lblAreYouSearching setText:NSLocalizedString(@"SearchSuggestionPart1", nil)];
[_lblAreYouSearching setHidden:YES];
_lblForMoreSuggestions = [[UILabel alloc] initWithFrame:CGRectMake(0, _collectionView.frame.size.height - labelHeight, _collectionView.frame.size.width, labelHeight)];
[_lblForMoreSuggestions setFont:fontToUse];
[_lblForMoreSuggestions setTextColor:[RobloxTheme colorGray4]];
[_lblForMoreSuggestions setText:NSLocalizedString(@"SearchSuggestionPart2", nil)];
[_lblForMoreSuggestions setTextAlignment:NSTextAlignmentRight];
[_lblForMoreSuggestions setHidden:YES];
[RobloxTheme applyToSearchNavItem:_searchBar];
}
return self;
}
//flags
- (BOOL) isFastResultsEnabled
{
return FFlag::NativeSearchFastResultsEnabled;
}
- (int) getSearchWaitTime
{
return FInt::NativeSearchFastResultsWaitTime;
}
//Accessors
-(RBXAnalyticsCustomData) getDataForSortType
{
RBXAnalyticsCustomData searchDataType = RBXACustomGames;
switch (_searchType)
{
case SearchResultGames: searchDataType = RBXACustomGames; break;
case SearchResultUsers: searchDataType = RBXACustomUsers; break;
case SearchResultCatalog: searchDataType = RBXACustomCatalog; break;
case SearchResultGroups: break;
}
return searchDataType;
}
//Navigation Bar functions
- (void) openSearch
{
if(_status == SearchStatusOpen)
return;
//add the observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardOpened:) name:UIKeyboardDidShowNotification object:nil];
[[RBXEventReporter sharedInstance] reportButtonClick:RBXAButtonSearchOpen
withContext:RBXAContextMain
withCustomData:[self getDataForSortType]];
_searchBar.hidden = NO;
_iconButton.hidden = YES;
_status = SearchStatusOpen;
[_searchBar setShowsCancelButton:YES];
CGRect visibleFrame = _containerController.view.bounds;
CGFloat windowWidth = visibleFrame.size.width;
// Animate the search bar in
[UIView animateWithDuration:0.3f
animations:^
{
// Make space in the navigation bar for the search bar
_containerController.navigationItem.hidesBackButton = YES;
_navLeftItems = _containerController.navigationItem.leftBarButtonItems;
_navRightItems = _containerController.navigationItem.rightBarButtonItems;
_navTitle = _containerController.navigationItem.title;
_containerController.navigationItem.titleView.hidden = YES;
_containerController.navigationItem.title = @"";
[_containerController.navigationItem setLeftBarButtonItems:[NSArray array]];
[_containerController.navigationItem setRightBarButtonItems:[NSArray arrayWithObject:self]];
_customView.frame = CGRectMake(0, 0, windowWidth - 30.0f, _baseFrame.size.height);
_searchBar.frame = CGRectMake(0, 0, windowWidth - 30.0f, _baseFrame.size.height);
}
completion:^(BOOL finished)
{
[_searchBar becomeFirstResponder];
}];
//only display the results if the flag is on
if ([self isFastResultsEnabled])
{
_collectionView.frame = visibleFrame;
[_containerController.view addSubview:_collectionView];
[_containerController.view addSubview:_lblAreYouSearching];
[_containerController.view addSubview:_lblForMoreSuggestions];
}
}
- (void) closeSearch
{
if(_status == SearchStatusClosed)
return;
_status = SearchStatusClosed;
[_searchBar resignFirstResponder];
[self deactivateWaitTimer];
[UIView animateWithDuration:0.3f
animations:^
{
_customView.frame = _baseFrame;
_searchBar.frame = _baseFrame;
[_searchBar setShowsCancelButton:NO animated:YES];
}
completion:^(BOOL finished)
{
// Restore the navigation bar
[_containerController.navigationItem setHidesBackButton: NO];
[_containerController.navigationItem setLeftBarButtonItems:_navLeftItems animated:YES];
[_containerController.navigationItem setRightBarButtonItems:_navRightItems animated:YES];
[_containerController.navigationItem setTitle:_navTitle];
_containerController.navigationItem.titleView.hidden = NO;
// Animate and release the focus from the search bar
[_searchBar endEditing:YES];
[_searchBar resignFirstResponder];
_searchBar.hidden = _compactMode;
_iconButton.hidden = !_compactMode;
}
];
[_collectionView removeFromSuperview];
[_lblAreYouSearching removeFromSuperview];
[_lblForMoreSuggestions removeFromSuperview];
//remove the notification listener
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
//Other functions
- (void) showSuggestionText:(BOOL)isShown
{
_lblAreYouSearching.hidden = !isShown;
_lblForMoreSuggestions.hidden = !isShown;
}
- (void) searchForResultsWithText:(NSString*)searchText
{
//This number clearly doesn't matter as the returned amount is constant. GG Web Team.
int numToSearch = [RobloxInfo thisDeviceIsATablet] ? NUM_TO_DISPLAY_TABLET: NUM_TO_DISPLAY_PHONE;
switch(_searchType)
{
case (SearchResultGames):
{
[RobloxData searchGames:searchText
fromIndex:0
numGames:numToSearch
thumbSize:[RobloxTheme sizeGameCoverRectangle]
completion:^(NSArray *games)
{
dispatch_async(dispatch_get_main_queue(), ^
{
if([searchText isEqualToString:_searchBar.text] )
{
_searchData = games;
[_collectionView reloadData];
[self showSuggestionText:(games.count > 0)];
}
});
}];
} break;
case (SearchResultUsers) :
{
[RobloxData searchUsers:searchText
fromIndex:0
numUsers:numToSearch
thumbSize:[RobloxTheme sizeProfilePictureMedium]
completion:^(NSArray *users)
{
dispatch_async(dispatch_get_main_queue(), ^
{
if([searchText isEqualToString:_searchBar.text] )
{
_searchData = users;
[_collectionView reloadData];
[self showSuggestionText:(users.count > 0)];
}
});
}];
} break;
case (SearchResultCatalog) : {} break;
case (SearchResultGroups) : {} break;
}
}
- (void) initWaitTimer
{
if (_searchWaitTimer)
return;
_searchCompleted = NO;
_searchWaitTimer = [NSTimer timerWithTimeInterval:(FAST_RESULT_TIMER_INTERVAL / 1000.0f)
target:self
selector:@selector(ticTimer)
userInfo:nil
repeats:YES];
}
- (void) activateWaitTimer
{
if (![_searchWaitTimer isValid])
{
[self initWaitTimer];
[[NSRunLoop mainRunLoop] addTimer:_searchWaitTimer forMode:NSDefaultRunLoopMode];
}
}
- (void) deactivateWaitTimer
{
if ([_searchWaitTimer isValid])
{
[_searchWaitTimer invalidate];
_searchWaitTimer = nil;
}
}
- (void) ticTimer
{
if (_searchCompleted)
return;
_timeBeforeSearch -= FAST_RESULT_TIMER_INTERVAL;
if (_timeBeforeSearch <= 0)
{
[self searchForResultsWithText:_searchBar.text];
_searchCompleted = YES;
}
}
//Notification functions
-(void) keyboardOpened:(NSNotification*)notification
{
//NSLog(@"Keyboard Opened : %@", notification.userInfo);
if (notification && notification.userInfo)
{
//get the height of the keyboard
NSDictionary* info = notification.userInfo;
CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
if ([RobloxInfo thisDeviceIsATablet])
_keyboardHeight = MIN([NSNumber numberWithFloat:keyboardFrame.size.height], [NSNumber numberWithFloat:keyboardFrame.size.width]);
else
_keyboardHeight = [NSNumber numberWithFloat:keyboardFrame.size.height];
//move the helper text to the top of the label
if (_keyboardHeight)
dispatch_async(dispatch_get_main_queue(), ^{
float navBarTopOffset = _containerController.view.bottom - (_keyboardHeight.floatValue + _lblForMoreSuggestions.height + _lblForMoreSuggestions.height);
[_lblForMoreSuggestions setY:navBarTopOffset];
[_lblForMoreSuggestions setRight:_containerController.view.right];
});
}
}
//Text Editing Delegate Functions
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
[self openSearch];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[[RBXEventReporter sharedInstance] reportButtonClick:RBXAButtonSubmit withContext:RBXAContextSearch withCustomData:[self getDataForSortType]];
[self closeSearch];
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:_searchBar.text forKey:@"keywords"];
switch (_searchType)
{
case (SearchResultGames): [[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_SEARCH_GAMES object:nil userInfo:userInfo]; break;
case (SearchResultCatalog): break;
case (SearchResultGroups): break;
case (SearchResultUsers): [[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_SEARCH_USERS object:nil userInfo:userInfo]; break;
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *) searchBar
{
[[RBXEventReporter sharedInstance] reportButtonClick:RBXAButtonClose withContext:RBXAContextMain withCustomData:[self getDataForSortType]];
searchBar.text = nil;
[self closeSearch];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if (![self isFastResultsEnabled]) return;
[self showSuggestionText:NO];
if(searchText.length == 0)
{
_searchData = nil;
[_collectionView reloadData];
return;
}
NSString* lastCharacter = [searchText substringFromIndex:(searchText.length-1)];
if ([lastCharacter isEqualToString:@" "])
{
if (!_searchCompleted)
{
[self searchForResultsWithText:searchText];
_searchCompleted = YES;
}
}
else
{
_searchCompleted = NO;
//reset the timer
_timeBeforeSearch = [self getSearchWaitTime];
[self activateWaitTimer];
}
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
[self closeSearch];
}
//Collection view delegate functions
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
int numItems = 0;
if (_searchData)
numItems = MIN(_searchData.count, [RobloxInfo thisDeviceIsATablet] ? NUM_TO_DISPLAY_TABLET : NUM_TO_DISPLAY_PHONE);
return numItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//grab the generic cell
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ReuseCell" forIndexPath:indexPath];
switch(_searchType)
{
case (SearchResultGames):
{
RBXGameData* gameData = _searchData[indexPath.row];
[(GameSearchResultCell*)cell setGameData:gameData];
} break;
case (SearchResultCatalog):
{
} break;
case (SearchResultGroups):
{
} break;
case (SearchResultUsers):
{
RBXUserSearchInfo* searchData = _searchData[indexPath.row];
[(UserSearchResultCell*)cell setInfo:searchData];
} break;
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self closeSearch];
//[collectionView deselectItemAtIndexPath:indexPath animated:YES];
switch(_searchType)
{
case (SearchResultGames):
{
RBXGameData* gameData = _searchData[indexPath.row];
if (!gameData)
return;
[[RBXEventReporter sharedInstance] reportOpenGameDetailFromSearch:[NSNumber numberWithInteger:gameData.placeID.integerValue]
fromPage:RBXALocationGameSearch
atIndex:[NSNumber numberWithInteger:indexPath.row]
totalItemsInSort:[NSNumber numberWithInteger:_searchData.count]];
NSDictionary* userInfo = @{ @"gameData" : gameData,
@"gameIndex" : [NSNumber numberWithInteger:indexPath.row],
@"totalGames" : [NSNumber numberWithInteger:_searchData.count] };
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_GAME_SELECTED object:nil userInfo:userInfo];
} break;
case (SearchResultCatalog):
{
} break;
case (SearchResultGroups):
{
} break;
case (SearchResultUsers):
{
RBXUserSearchInfo* userData = _searchData[indexPath.row];
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:userData forKey:@"userData"];
[[NSNotificationCenter defaultCenter] postNotificationName:RBX_NOTIFY_USER_SELECTED object:nil userInfo:userInfo];
} break;
}
}
@end
@@ -0,0 +1,13 @@
//
// NonRotatableNavigationController.h
// RobloxMobile
//
// Created by Kyler Mulherin on 3/10/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface NonRotatableNavigationController : UINavigationController
@end
@@ -0,0 +1,36 @@
//
// NonRotatableNavigationController.m
// RobloxMobile
//
// Created by Kyler Mulherin on 3/10/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "NonRotatableNavigationController.h"
@interface NonRotatableNavigationController ()
@end
@implementation NonRotatableNavigationController
#ifdef __IPHONE_9_0
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
#else
- (NSUInteger)supportedInterfaceOrientations
#endif
{
switch ([UIDevice currentDevice].userInterfaceIdiom)
{
case UIUserInterfaceIdiomPad: return UIInterfaceOrientationMaskLandscape;
case UIUserInterfaceIdiomPhone: return UIInterfaceOrientationMaskPortrait;
case UIUserInterfaceIdiomUnspecified: return UIInterfaceOrientationMaskPortrait;
default: return UIInterfaceOrientationMaskPortrait;
}
}
- (BOOL)shouldAutorotate
{
return NO;
}
@end
@@ -0,0 +1,13 @@
//
// NonRotatableNavigationController.h
// RobloxMobile
//
// Created by Kyler Mulherin on 3/10/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface NonRotatableViewController : UIViewController
@end
@@ -0,0 +1,39 @@
//
// NonRotatableViewController.m
// -A subclass of UIViewController to prevent the screen from rotating
// RobloxMobile
//
// Created by Kyler Mulherin on 3/10/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "NonRotatableViewController.h"
@interface NonRotatableViewController ()
@end
@implementation NonRotatableViewController
#ifdef __IPHONE_9_0
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
#else
- (NSUInteger)supportedInterfaceOrientations
#endif
{
switch ([UIDevice currentDevice].userInterfaceIdiom)
{
case UIUserInterfaceIdiomPad: return UIInterfaceOrientationMaskLandscape;
case UIUserInterfaceIdiomPhone: return UIInterfaceOrientationMaskPortrait;
case UIUserInterfaceIdiomUnspecified: return UIInterfaceOrientationMaskPortrait;
default: return UIInterfaceOrientationMaskPortrait;
}
}
- (BOOL)shouldAutorotate
{
return NO;
}
@end
@@ -0,0 +1,16 @@
//
// RBActivityIndicatorView.h
// RobloxMobile
//
// Created by Ariel Lichtin on 8/26/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBActivityIndicatorView : UIView
-(void) startAnimating;
-(void) stopAnimating;
@end
@@ -0,0 +1,81 @@
//
// RBActivityIndicatorView.m
// RobloxMobile
//
// Created by Ariel Lichtin on 8/26/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBActivityIndicatorView.h"
#import <UIKit/UIKit.h>
#define SPINNER_ICON_FRAME CGRectMake(0, 0, 32, 32)
@implementation RBActivityIndicatorView
{
UIImageView* _imageView;
}
- (id)initWithFrame:(CGRect)frame
{
if (frame.size.width == 0 && frame.size.height == 0) {
frame = SPINNER_ICON_FRAME;
}
self = [super initWithFrame:frame];
if (self)
{
[self initUI];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
[self initUI];
}
return self;
}
- (void) initUI
{
self.backgroundColor = [UIColor clearColor];
UIImage* loadingIcon = [UIImage imageNamed:@"Loading Icon"];
_imageView = [[UIImageView alloc] initWithImage:loadingIcon];
_imageView.frame = self.bounds;
_imageView.contentMode = UIViewContentModeCenter;
[self addSubview:_imageView];
self.hidden = YES;
}
-(void) startAnimating
{
self.hidden = NO;
if(_imageView.layer.animationKeys.count == 0)
{
CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 /* full rotation*/];
rotationAnimation.duration = 1.0;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = HUGE_VALF;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[_imageView.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}
}
-(void) stopAnimating
{
self.hidden = YES;
[_imageView.layer removeAllAnimations];
}
@end
@@ -0,0 +1,19 @@
//
// RBBadgesViewController.h
// RobloxMobile
//
// Created by Ariel Lichtin on 10/7/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBBadgesViewController : UIViewController
@property(strong, nonatomic) NSString* gameID;
@property(nonatomic, readonly) NSUInteger numItems;
@property(copy, nonatomic) void (^completionHandler)();
@end
@@ -0,0 +1,152 @@
//
// RBBadgesViewController.m
// RobloxMobile
//
// Created by Ariel Lichtin on 10/7/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBBadgesViewController.h"
#import "RobloxImageView.h"
#import "RobloxData.h"
#import "UIView+Position.h"
#import "RobloxTheme.h"
#import "GameBadgeViewController.h"
#define REUSE_IDENTIFIER @"ReuseCell"
#define BADGE_SIZE CGSizeMake(110, 110)
#define ITEM_SIZE CGSizeMake(75, 75)
//--------------------------------------------
@interface RBBadgeCollectionViewCell : UICollectionViewCell
@property(strong, nonatomic) RBXBadgeInfo* badge;
@end
@implementation RBBadgeCollectionViewCell
{
RobloxImageView* _badgeImage;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self initElements];
}
return self;
}
- (void) initElements
{
_badgeImage = [[RobloxImageView alloc] initWithFrame:self.bounds];
[self addSubview:_badgeImage];
}
- (void)setBadge:(RBXBadgeInfo *)badge
{
[_badgeImage loadBadgeWithURL:badge.imageURL withSize:BADGE_SIZE completion:nil];
}
@end
//--------------------------------------------
@interface RBBadgesViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
@end
@implementation RBBadgesViewController
{
IBOutlet UILabel* _title;
IBOutlet UICollectionView* _collectionView;
NSArray* _badges;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if(self)
{
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
_title.text = [NSLocalizedString(@"GameBadgesPhrase", nil) uppercaseString];
[RobloxTheme applyToGameSortTitle:_title];
[_collectionView registerClass:[RBBadgeCollectionViewCell class] forCellWithReuseIdentifier:REUSE_IDENTIFIER];
_collectionView.backgroundView = nil;
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.delegate = self;
_collectionView.dataSource = self;
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
[flowLayout setItemSize:ITEM_SIZE];
[flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical];
[flowLayout setMinimumInteritemSpacing:36.0f];
[flowLayout setMinimumLineSpacing:25.0f];
[_collectionView setCollectionViewLayout:flowLayout];
[RobloxData fetchGameBadges:_gameID completion:^(NSArray *badges)
{
dispatch_async(dispatch_get_main_queue(), ^
{
_badges = badges;
[_collectionView reloadData];
// Adjust size
_collectionView.size = _collectionView.collectionViewLayout.collectionViewContentSize;
_collectionView.height = _collectionView.height;
self.view.height = CGRectGetMaxY(_collectionView.frame);
if(self.completionHandler != nil)
{
self.completionHandler();
}
});
}];
}
- (NSUInteger)numItems
{
return _badges != nil ? _badges.count : 0;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.numItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
RBBadgeCollectionViewCell* cell = [_collectionView dequeueReusableCellWithReuseIdentifier:REUSE_IDENTIFIER forIndexPath:indexPath];
cell.badge = _badges[indexPath.row];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
RBXBadgeInfo* badge = _badges[indexPath.row];
GameBadgeViewController* badgePopup = [[GameBadgeViewController alloc] initWithBadgeInfo:badge];
[self.navigationController presentViewController:badgePopup animated:YES completion:nil];
}
@end
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6245" systemVersion="14A379a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment defaultVersion="1792" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="RBBadgesViewController">
<connections>
<outlet property="_collectionView" destination="kdd-Z7-r0b" id="nl3-Nn-fhZ"/>
<outlet property="_title" destination="Ncr-dY-3z6" id="Znp-8F-UcR"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="968" height="110"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_TITLE_" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ncr-dY-3z6">
<rect key="frame" x="0.0" y="0.0" width="193" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="kdd-Z7-r0b">
<rect key="frame" x="0.0" y="34" width="968" height="76"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="2sl-9N-kzf">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
</collectionView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<nil key="simulatedStatusBarMetrics"/>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="378" y="343"/>
</view>
</objects>
</document>
@@ -0,0 +1,18 @@
//
// RBBarButtonMenu.h
// RobloxMobile
//
// Created by Ariel Lichtin on 9/4/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBBarButtonMenu : UIBarButtonItem
- (instancetype) initWithTitle:(NSString*)title style:(UIBarButtonItemStyle)style;
- (instancetype) initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style;
- (void)addItemWithTitle:(NSString *)title target:(id)target action:(SEL)action;
@end
@@ -0,0 +1,145 @@
//
// RBBarButtonMenu.m
// RobloxMobile
//
// Created by Ariel Lichtin on 9/4/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBBarButtonMenu.h"
#define CELL_SIZE CGSizeMake(320.0f, 44.0f)
//-----------------------------------------------------------------------------------------------------------------------------
@interface RBBarButtonMenuItemData: NSObject
@property(strong, nonatomic) NSString* title;
@property(nonatomic) SEL action;
@property(nonatomic,assign) id target;
@end
@implementation RBBarButtonMenuItemData
@end
//-----------------------------------------------------------------------------------------------------------------------------
@interface RBBarButtonMenuCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *title;
@property (weak, nonatomic) IBOutlet UIImageView *checkImage;
@end
@implementation RBBarButtonMenuCell
@end
//-----------------------------------------------------------------------------------------------------------------------------
@interface RBBarButtonMenu () <UITableViewDelegate, UITableViewDataSource>
@end
@implementation RBBarButtonMenu
{
NSMutableArray* _elements;
UIViewController* _tableController;
UITableView* _tableView;
UIPopoverController* _popOver;
}
- (void)initButton
{
self.target = self;
self.action = @selector(mainButtonTouched);
_elements = [NSMutableArray array];
_tableView = [[UITableView alloc] init];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.scrollEnabled = NO;
_tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[_tableView registerNib:[UINib nibWithNibName:@"RBBarButtonMenuCell" bundle:nil] forCellReuseIdentifier:@"ReuseCell"];
_tableController = [[UIViewController alloc] init];
[_tableController.view addSubview:_tableView];
}
- (instancetype)initWithTitle:(NSString*)title style:(UIBarButtonItemStyle)style
{
self = [super initWithTitle:title style:UIBarButtonItemStyleDone target:nil action:nil];
if(self)
{
[self initButton];
}
return self;
}
- (instancetype)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style
{
self = [super initWithImage:image style:style target:nil action:nil];
if(self)
{
[self initButton];
}
return self;
}
-(void) showPopOver
{
if(_popOver == nil || !_popOver.isPopoverVisible)
{
[_tableView reloadData];
CGRect tableRect;
tableRect.origin = CGPointZero;
tableRect.size = CELL_SIZE;
tableRect.size.height *= [_tableView numberOfRowsInSection:0];
_tableView.frame = tableRect;
_tableController.preferredContentSize = tableRect.size;
_popOver = [[UIPopoverController alloc] initWithContentViewController:_tableController];
[_popOver presentPopoverFromBarButtonItem:self permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
}
}
- (void)addItemWithTitle:(NSString *)title target:(id)target action:(SEL)action
{
RBBarButtonMenuItemData* item = [[RBBarButtonMenuItemData alloc] init];
item.title = title;
item.target = target;
item.action = action;
[_elements addObject:item];
}
- (void) mainButtonTouched
{
[self showPopOver];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _elements.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
RBBarButtonMenuCell* cell = [tableView dequeueReusableCellWithIdentifier:@"ReuseCell" forIndexPath:indexPath];
RBBarButtonMenuItemData* itemData = _elements[indexPath.row];
cell.title.text = itemData.title;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[_popOver dismissPopoverAnimated:YES];
RBBarButtonMenuItemData* itemData = _elements[indexPath.row];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[itemData.target performSelector:itemData.action];
#pragma clang diagnostic pop
}
@end
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5056" systemVersion="13E28" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment defaultVersion="1792" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="H3i-QG-flo" customClass="RBBarButtonMenuCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="H3i-QG-flo" id="Byq-gH-7H4">
<rect key="frame" x="0.0" y="0.0" width="320" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ez9-UX-Ob4">
<rect key="frame" x="20" y="11" width="280" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="title" destination="ez9-UX-Ob4" id="QrN-zJ-JsR"/>
</connections>
</tableViewCell>
</objects>
</document>
@@ -0,0 +1,27 @@
//
// RBBirthdayPicker.h
// RobloxMobile
//
// Created by Kyler Mulherin on 7/15/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBBirthdayPicker : UIView <UIPickerViewDelegate, UIPickerViewDataSource>
@property (nonatomic, getter=getBirthday, readonly) NSString* playerBirthday;
@property (nonatomic, getter=getWasBornToday, readonly) BOOL wasBornToday;
-(BOOL) isValid;
-(BOOL) userUnder13;
-(void) setValidationBlock:(void(^)())callback;
-(void) markAsValid;
-(void) markAsNormal;
-(void) markAsInvalid;
- (BOOL)becomeFirstResponder;
- (BOOL)resignFirstResponder;
@end
@@ -0,0 +1,481 @@
//
// RBBirthdayPicker.m
// RobloxMobile
//
// Created by Kyler Mulherin on 7/15/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBBirthdayPicker.h"
#import "RobloxTheme.h"
#import "RobloxInfo.h"
#import "UIView+Position.h"
@implementation RBBirthdayPicker
{
NSString* _birthdayString;
UILabel* _birthdayLabel;
UILabel* _birthdayMonth;
UILabel* _birthdayDay;
UILabel* _birthdayYear;
UIButton* _dateButton;
UIView* _dimView;
UIView* _datePickerContainer;
UINavigationBar* _datePickerNavBar;
UIButton* _datePickerAcceptButton;
UIButton* _datePickerCancelButton;
UIPickerView* _dateStringPicker;
CGRect _datePosOffScreen;
CGRect _datePosOnScreen;
bool isValid;
void (^validationBlock)();
NSMutableArray* _dataMonths;
NSMutableArray* _dataDays;
NSMutableArray* _dataYears;
NSMutableArray* _lastDateIndexes;
}
//Constructors and Initializers
-(void) initialize {
isValid = NO;
validationBlock = nil;
//initialize the data for some date pickers
//Days
NSString* placeHolder = @"----";
_dataDays = [NSMutableArray arrayWithCapacity:32];
[_dataDays addObject:placeHolder];
for (NSInteger i = 1; i <= 31; i++)
[_dataDays addObject:[[NSNumber numberWithInteger:i] stringValue]];
//Months
_dataMonths = [NSMutableArray arrayWithArray:@[placeHolder,
NSLocalizedString(@"MonthJanuary", nil),
NSLocalizedString(@"MonthFebruary", nil),
NSLocalizedString(@"MonthMarch", nil),
NSLocalizedString(@"MonthApril", nil),
NSLocalizedString(@"MonthMay", nil),
NSLocalizedString(@"MonthJune", nil),
NSLocalizedString(@"MonthJuly", nil),
NSLocalizedString(@"MonthAugust", nil),
NSLocalizedString(@"MonthSeptember", nil),
NSLocalizedString(@"MonthOctober", nil),
NSLocalizedString(@"MonthNovember", nil),
NSLocalizedString(@"MonthDecember", nil)]];
//Years
NSDateComponents* dateComps = [[NSCalendar currentCalendar] components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:[NSDate date]];
_dataYears = [NSMutableArray arrayWithCapacity:((dateComps.year - 1900) + 1)];
int currentYear = 1900;
while (currentYear <= dateComps.year)
[_dataYears addObject:[NSNumber numberWithInt:(currentYear++)].stringValue];
[_dataYears addObject:placeHolder];
_lastDateIndexes = [NSMutableArray arrayWithCapacity:3];
_lastDateIndexes[0] = [NSNumber numberWithInt:_dataMonths.count * 250];
_lastDateIndexes[1] = [NSNumber numberWithInt:_dataDays.count * 250];
_lastDateIndexes[2] = [NSNumber numberWithInt:_dataYears.count - 1];
_dateStringPicker = [[UIPickerView alloc] init];
[_dateStringPicker setDelegate:self];
[_dateStringPicker setDataSource:self];
[_dateStringPicker selectRow:[_lastDateIndexes[0] integerValue] inComponent:0 animated:NO];
[_dateStringPicker selectRow:[_lastDateIndexes[1] integerValue] inComponent:1 animated:NO];
[_dateStringPicker selectRow:[_lastDateIndexes[2] integerValue] inComponent:2 animated:NO];
_birthdayString = @"";
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// UI ELEMENTS
[self.layer setBorderWidth:1];
[self.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[self.layer setCornerRadius:5.0];
[self setClipsToBounds:YES];
_birthdayLabel = [[UILabel alloc] init];
[_birthdayLabel setText:NSLocalizedString(@"BirthdayWord", nil)];
[_birthdayLabel setFont:[RobloxTheme fontBodyBold]];
[_birthdayLabel setTextColor:[RobloxTheme colorGray1]];
_birthdayMonth = [[UILabel alloc] init];
[_birthdayMonth setTextAlignment:NSTextAlignmentCenter];
[_birthdayMonth setText: placeHolder];
[_birthdayMonth.layer setBorderWidth:1];
[_birthdayMonth.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[RobloxTheme applyToModalLoginTitleLabel:_birthdayMonth];
_birthdayDay = [[UILabel alloc] init];
[_birthdayDay setTextAlignment:NSTextAlignmentCenter];
[_birthdayDay setText: placeHolder];
[_birthdayDay.layer setBorderWidth:1];
[_birthdayDay.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[RobloxTheme applyToModalLoginTitleLabel:_birthdayDay];
_birthdayYear = [[UILabel alloc] init];
[_birthdayYear setTextAlignment:NSTextAlignmentCenter];
[_birthdayYear setText: placeHolder];
[_birthdayYear.layer setBorderWidth:1];
[_birthdayYear.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[RobloxTheme applyToModalLoginTitleLabel:_birthdayYear];
_dateButton = [[UIButton alloc] init];
[_dateButton addTarget:self action:@selector(openBirthday:) forControlEvents:UIControlEventTouchUpInside];
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// DATE PICKER
if (NSClassFromString(@"UIVisualEffectView") && NSClassFromString(@"UIBlurEffect"))
{
_dimView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]];
[_dimView setAlpha:0.0];
}
else
{
_dimView = [[UIView alloc] init];
[_dimView setBackgroundColor:[UIColor colorWithRed:160.0/255.0 green:160.0/255.0 blue:160.0/255.0 alpha:0.85]];
[_dimView setAlpha:0.0];
}
_datePickerContainer = [[UIView alloc] init];
[_datePickerContainer setOpaque:YES];
[_datePickerContainer setBackgroundColor:[UIColor whiteColor]];
[_datePickerContainer setClipsToBounds:YES];
[_datePickerContainer.layer setBorderColor:[[RobloxTheme colorGray4] CGColor]];
[_datePickerContainer.layer setBorderWidth:1];
[_datePickerContainer.layer setCornerRadius:5.0];
[_datePickerContainer.layer setShadowColor:[UIColor blackColor].CGColor];
[_datePickerContainer.layer setShadowOpacity:0.6];
[_datePickerContainer.layer setShadowOffset:CGSizeMake(0, 2)];
[_datePickerContainer.layer setShadowRadius:4];
[_datePickerContainer setClipsToBounds:YES];
_datePickerNavBar = [[UINavigationBar alloc] init];
[_datePickerNavBar pushNavigationItem:[[UINavigationItem alloc] init] animated:NO];
[_datePickerNavBar.topItem setTitle:NSLocalizedString(@"SelectBirthday", nil)];
[RobloxTheme applyToModalPopupNavBar:_datePickerNavBar];
_datePickerAcceptButton = [[UIButton alloc] init];
[_datePickerAcceptButton setTitle:NSLocalizedString(@"AcceptWord", nil) forState:UIControlStateNormal];
[_datePickerAcceptButton addTarget:self action:@selector(closeBirthdayAccept:) forControlEvents:UIControlEventTouchUpInside];
[_datePickerAcceptButton setEnabled:NO];
[RobloxTheme applyToDisabledModalSubmitButton:_datePickerAcceptButton];
_datePickerCancelButton = [[UIButton alloc] init];
[_datePickerCancelButton setTitle:NSLocalizedString(@"CancelWord", nil) forState:UIControlStateNormal];
[_datePickerCancelButton addTarget:self action:@selector(closeBirthday:) forControlEvents:UIControlEventTouchUpInside];
[RobloxTheme applyToModalCancelButton:_datePickerCancelButton];
if ([RobloxInfo thisDeviceIsATablet])
{
//insert the buttons into the navigation bar
[_datePickerNavBar.topItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:_datePickerAcceptButton]];
[_datePickerNavBar.topItem setLeftBarButtonItem:[[UIBarButtonItem alloc] initWithCustomView:_datePickerCancelButton] ];
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// ADD THE SUBVIEWS
[self addSubview:_birthdayLabel];
[self addSubview:_birthdayMonth];
[self addSubview:_birthdayDay];
[self addSubview:_birthdayYear];
[self addSubview:_dateButton];
[_datePickerContainer addSubview:_dateStringPicker];
[_datePickerContainer addSubview:_datePickerNavBar];
if (![RobloxInfo thisDeviceIsATablet])
{
[_datePickerContainer addSubview:_datePickerAcceptButton];
[_datePickerContainer addSubview:_datePickerCancelButton];
}
}
-(void) layoutSubviews {
[_dateButton setFrame:CGRectMake(0, 0, self.width, self.height)];
int margin = 12; //self.width * 0.05;
[_birthdayLabel setFrame:CGRectMake(margin, 0, (self.width / 3) - margin, self.height)];
[_birthdayMonth setFrame:CGRectMake(_birthdayLabel.right, -1, self.width * 0.33, self.height+2)];
[_birthdayDay setFrame:CGRectMake(_birthdayMonth.right-1, -1, self.width * 0.15, self.height+2)];
[_birthdayYear setFrame:CGRectMake(_birthdayDay.right-1, -1, (self.width - _birthdayDay.right)+2,self.height+2)];
CGFloat datePickerNavHeight = 40;
CGFloat datePickerHeight = 220;
CGFloat dateButtonWidth = [RobloxInfo thisDeviceIsATablet] ? 80 : 100;
CGFloat containerHeight = datePickerHeight + datePickerNavHeight + ([RobloxInfo thisDeviceIsATablet] ? 0 : datePickerNavHeight + 16);
_datePosOffScreen = CGRectMake(0, self.superview.height + 300, self.superview.width, containerHeight);
_datePosOnScreen = CGRectMake(0, MIN((self.superview.height * 0.5) - (containerHeight * 0.5), self.superview.height - containerHeight), self.superview.width, containerHeight);
[_datePickerContainer setFrame:CGRectMake(0, _datePosOffScreen.origin.y, self.superview.width, containerHeight)];
[_datePickerNavBar setFrame:CGRectMake(0, 6, _datePickerContainer.width, datePickerNavHeight)];
[_dateStringPicker setFrame:CGRectMake(0, datePickerNavHeight, _datePickerContainer.width, datePickerHeight)];
[_dateStringPicker setNeedsLayout];
if ([RobloxInfo thisDeviceIsATablet])
{
[_datePickerNavBar.topItem.rightBarButtonItem.customView setFrame:CGRectMake(0, 0, dateButtonWidth, datePickerNavHeight)];
[_datePickerNavBar.topItem.leftBarButtonItem.customView setFrame:_datePickerNavBar.topItem.rightBarButtonItem.customView.frame];
}
else
{
float containerMargin = 10;
float center = _datePickerContainer.center.x;
[_datePickerAcceptButton setFrame:CGRectMake(center + (containerMargin * 0.5), _dateStringPicker.bottom, dateButtonWidth, datePickerNavHeight)];
[_datePickerCancelButton setFrame:CGRectMake(_datePickerAcceptButton.x - _datePickerAcceptButton.width - containerMargin,
_datePickerAcceptButton.y,
_datePickerAcceptButton.width,
_datePickerAcceptButton.height)];
}
int edgeOffset = 400;
[_dimView setFrame:CGRectMake(-edgeOffset, -edgeOffset, self.superview.width + (edgeOffset * 2), self.superview.height + (edgeOffset * 2))];
}
-(id) init {
self = [super init];
if (self)
[self initialize];
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self)
[self initialize];
return self;
}
-(id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
[self initialize];
return self;
}
//Accessors
-(NSDate*) getDate {
int selectedDay = [_dateStringPicker selectedRowInComponent:1] % _dataDays.count;
int selectedMonth = [_dateStringPicker selectedRowInComponent:0] % _dataMonths.count;
int selectedYear= [_dataYears[[_dateStringPicker selectedRowInComponent:2]] integerValue];
NSDateComponents* dateConstructor = [[NSDateComponents alloc] init];
[dateConstructor setDay:selectedDay];
[dateConstructor setMonth:selectedMonth];
[dateConstructor setYear:selectedYear];
NSDate* validDate = [[NSCalendar currentCalendar] dateFromComponents:dateConstructor];
if (validDate)
{
//Verify the validity of the created date,
//If the components of the created date match those of the inputted date, it must be correct
//Ex: Feb 31st, 2000 as input yields 2/31/2000
// The created date would result in something like 3/3/2000
// Thus, 2/31/2000 is not a valid date
NSDateComponents* dateComps = [[NSCalendar currentCalendar] components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:validDate];
bool matchesDay = (selectedDay == [dateComps day]);
bool matchesMonth = (selectedMonth == [dateComps month]);
bool matchesYear = (selectedYear == [dateComps year]);
if (!matchesDay || !matchesMonth || !matchesYear)
return nil;
}
//check to see if the created date is in the future
NSTimeInterval time = [validDate timeIntervalSinceDate:[NSDate date]];
if (time > 0)
{
//The date is in the future
return nil;
}
return validDate;
}
-(NSString*) getBirthday { return _birthdayString; }
-(BOOL) getWasBornToday {
NSDate* pickedDate = [self getDate];
if (!pickedDate)
return NO;
return [pickedDate isEqualToDate:[NSDate date]];
}
-(BOOL) userUnder13 {
//getDate is guaranteed to return a valid date, as it is only called after a successful validation
NSDate* dateToday = [NSDate date];
NSTimeInterval secondsSince = [dateToday timeIntervalSinceDate:[self getDate]];
float numberOfYears = secondsSince / (60.0f * 60.0f * 24.0f * 365.25f);
return (numberOfYears < 13.0f);
}
-(BOOL) isValid { return isValid; }
//Mutators
-(void) setValidationBlock:(void(^)())callback { validationBlock = callback; }
//Delegate Functions
-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{
NSDate* currentDate = [self getDate];
[_datePickerAcceptButton setEnabled:(currentDate != nil)];
if (_datePickerAcceptButton.enabled)
[RobloxTheme applyToModalSubmitButton:_datePickerAcceptButton];
else
[RobloxTheme applyToDisabledModalSubmitButton:_datePickerAcceptButton];
}
-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
NSString* title = @"";
switch (component)
{
case (0): title = _dataMonths[row % _dataMonths.count]; break;
case (1): title = _dataDays[row % _dataDays.count]; break;
case (2): title = _dataYears[row % _dataYears.count]; break;
}
return title;
}
-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 3;
}
-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
NSInteger numRows = 0;
switch (component)
{
case 0: numRows = _dataMonths.count * 500; break;
case 1: numRows = _dataDays.count * 500; break;
case 2: numRows = _dataYears.count; break;
}
return numRows;
}
-(CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
CGFloat compWidth = pickerView.width;
switch (component)
{
case (0) : compWidth *= 0.40f; break;
case (1) : compWidth *= 0.20f; break;
case (2) : compWidth *= 0.40f; break;
}
return compWidth;
}
//Button Actions
- (void)openBirthday:(id)sender {
[self.superview addSubview:_dimView];
[self.superview addSubview:_datePickerContainer];
//set the date of the last date that was accepted
[_dateStringPicker selectRow:[_lastDateIndexes[0] integerValue] inComponent:0 animated:NO];
[_dateStringPicker selectRow:[_lastDateIndexes[1] integerValue] inComponent:1 animated:NO];
[_dateStringPicker selectRow:[_lastDateIndexes[2] integerValue] inComponent:2 animated:NO];
[_dateStringPicker becomeFirstResponder];
[UIView animateWithDuration:0.3
animations:^
{
_datePickerContainer.frame = _datePosOnScreen;
_dimView.alpha = 1.0;
}
completion:nil];
}
- (void)closeBirthday:(id)sender {
[_dateStringPicker resignFirstResponder];
[UIView animateWithDuration:0.3
animations:^
{
_datePickerContainer.frame = _datePosOffScreen;
_dimView.alpha = 0.0;
}
completion:^(BOOL finished)
{
[_datePickerContainer removeFromSuperview];
[_dimView removeFromSuperview];
}];
}
- (void)closeBirthdayAccept:(id)sender {
isValid = YES;
[self updateBirthdate];
[self closeBirthday:sender];
[self markAsValid];
}
- (void)updateBirthdate {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setDateStyle:NSDateFormatterShortStyle];
//save the string for the signup string
NSDate* acceptedDate = [self getDate];
_birthdayString = [dateFormatter stringFromDate:acceptedDate];
//update the indexes of the last dates
_lastDateIndexes[0] = [NSNumber numberWithInteger:[_dateStringPicker selectedRowInComponent:0]];
_lastDateIndexes[1] = [NSNumber numberWithInteger:[_dateStringPicker selectedRowInComponent:1]];
_lastDateIndexes[2] = [NSNumber numberWithInteger:[_dateStringPicker selectedRowInComponent:2]];
//update the UI
NSDateComponents* dateComps = [[NSCalendar currentCalendar] components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:acceptedDate];
[_birthdayMonth setText:[dateFormatter.monthSymbols objectAtIndex:(dateComps.month - 1)]];
[_birthdayDay setText:[NSString stringWithFormat:@"%li", (long)dateComps.day]];
[_birthdayYear setText:[NSString stringWithFormat:@"%li", (long)dateComps.year]];
//run some code for some other controllers
if (validationBlock != nil)
validationBlock();
}
//Miscellaneous functions
- (BOOL)becomeFirstResponder {
[super becomeFirstResponder];
return [_dateStringPicker becomeFirstResponder];
}
- (BOOL)resignFirstResponder {
[super resignFirstResponder];
if (_datePickerContainer.superview)
[self closeBirthday:nil];
return [_dateStringPicker resignFirstResponder];
}
//Colors and crap
-(void) changeBorderColor:(UIColor*)color {
dispatch_async(dispatch_get_main_queue(), ^
{
self.layer.borderColor = [color CGColor];
_birthdayMonth.layer.borderColor = [color CGColor];
_birthdayDay.layer.borderColor = [color CGColor];
_birthdayYear.layer.borderColor = [color CGColor];
});
}
-(void) markAsValid {
[self changeBorderColor:[RobloxTheme colorGreen1]];
[_birthdayLabel setTextColor:[RobloxTheme colorGray1]];
[_birthdayMonth setTextColor:[RobloxTheme colorGray1]];
[_birthdayDay setTextColor:[RobloxTheme colorGray1]];
[_birthdayYear setTextColor:[RobloxTheme colorGray1]];
}
-(void) markAsNormal {
[self changeBorderColor:[RobloxTheme colorGray4]];
[_birthdayLabel setTextColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f]];
[_birthdayMonth setTextColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f]];
[_birthdayDay setTextColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f]];
[_birthdayYear setTextColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f]];
}
-(void) markAsInvalid {
[self changeBorderColor:[RobloxTheme colorRed1]];
//[_birthdayLabel setTextColor:[RobloxTheme colorGray1]];
//[_birthdayMonth setTextColor:[RobloxTheme colorGray1]];
//[_birthdayDay setTextColor:[RobloxTheme colorGray1]];
//[_birthdayYear setTextColor:[RobloxTheme colorGray1]];
}
@end
@@ -0,0 +1,23 @@
//
// RBCaptchaV2ViewController.h
// RobloxMobile
//
// Created by Ashish Jain on 12/17/15.
// Copyright © 2015 ROBLOX. All rights reserved.
//
#import "RBModalPopUpViewController.h"
typedef void(^CaptchaCompletionHandler)(NSError *captchaError);
@interface RBCaptchaV2ViewController : RBModalPopUpViewController <UIGestureRecognizerDelegate, UITextFieldDelegate>
+ (instancetype) CaptchaV2ForLoginWithUsername:(NSString*)username
completionHandler:(CaptchaCompletionHandler)completionHandler;
+ (instancetype) CaptchaV2ForSignupWithUsername:(NSString *)username
andCompletion:(CaptchaCompletionHandler)completionHandler;
+ (instancetype) CaptchaV2ForSocialSignupWithUsername:(NSString *)username
andCompletion:(CaptchaCompletionHandler)completionHandler;
@end
@@ -0,0 +1,453 @@
//
// RBCaptchaV2ViewController.m
// RobloxMobile
//
// Created by Ashish Jain on 12/17/15.
// Copyright © 2015 ROBLOX. All rights reserved.
//
#import "RBCaptchaV2ViewController.h"
#import "LoginManager.h"
#import "RobloxInfo.h"
#import "RobloxTheme.h"
#import "RBXFunctions.h"
#import "UIView+Position.h"
@interface RBCaptchaV2ViewController ()
typedef NS_ENUM(NSInteger, RBCaptchaType)
{
RBCaptchaLogin,
RBCaptchaSignup,
RBCaptchaSocialSignup
};
@property UITapGestureRecognizer* touches;
@property (nonatomic, strong) NSString *challenge;
@property (nonatomic, strong) NSString *imageToken;
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) UILabel *instructions;
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UIButton *submitButton;
@property (nonatomic, strong) UIButton *reloadButton;
@property (nonatomic, strong) UITextField *userInput;
@property (nonatomic, strong) NSString *username;
@property RBCaptchaType cType;
@property (nonatomic, copy) CaptchaCompletionHandler captchaCompletionHandler;
@end
static NSString *publicKey = @"6Le88gcTAAAAALG04IFgQDENWlQmc_hy_3tdF1yY";
static NSInteger captchaAttempts = 0;
#define MARGIN_WIDTH 20
#define VIEW_BACKGROUND_COLOR [UIColor colorWithRed:(238.0/255.0) green:(238.0/255.0) blue:(238.0/255.0) alpha:1.0]
@implementation RBCaptchaV2ViewController
+ (instancetype) CaptchaV2ForLoginWithUsername:(NSString*)username
completionHandler:(CaptchaCompletionHandler)completionHandler
{
RBCaptchaV2ViewController *vc = [[RBCaptchaV2ViewController alloc] init];
vc.username = username;
vc.captchaCompletionHandler = completionHandler;
vc.cType = RBCaptchaLogin;
return vc;
}
+ (instancetype) CaptchaV2ForSignupWithUsername:(NSString *)username
andCompletion:(CaptchaCompletionHandler)completionHandler
{
RBCaptchaV2ViewController *vc = [[RBCaptchaV2ViewController alloc] init];
vc.username = username;
vc.captchaCompletionHandler = completionHandler;
vc.cType = RBCaptchaSignup;
return vc;
}
+ (instancetype) CaptchaV2ForSocialSignupWithUsername:(NSString *)username
andCompletion:(CaptchaCompletionHandler)completionHandler
{
RBCaptchaV2ViewController *vc = [[RBCaptchaV2ViewController alloc] init];
vc.username = username;
vc.captchaCompletionHandler = completionHandler;
vc.cType = RBCaptchaSocialSignup;
return vc;
}
#pragma mark Lifecycle functions
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.touches = [[UITapGestureRecognizer alloc] init];
[self.touches setNumberOfTouchesRequired:1];
[self.touches setNumberOfTapsRequired:1];
[self.touches setDelegate:self];
[self.touches setEnabled:YES];
[self.view addGestureRecognizer:self.touches];
self.title = NSLocalizedString(@"CaptchaTitleQuestion", nil);
[self.view setBackgroundColor:VIEW_BACKGROUND_COLOR];
self.instructions = [[UILabel alloc] initWithFrame:CGRectZero];
[self.instructions setTextColor:[UIColor grayColor]];
[self.instructions setFont:[RobloxTheme fontBodyLarge]];
[self.instructions setNumberOfLines:0];
[self.instructions setText:NSLocalizedString(@"CaptchaVerifyHuman", nil)];
[self.instructions setTextAlignment:NSTextAlignmentCenter];
[self.view addSubview:self.instructions];
self.imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
[self.imageView setBackgroundColor:[UIColor whiteColor]];
[self.imageView setContentMode:UIViewContentModeScaleAspectFit];
[self.imageView.layer setBorderColor:[UIColor lightGrayColor].CGColor];
[self.imageView.layer setBorderWidth:0.5];
[self.view addSubview:self.imageView];
self.userInput = [[UITextField alloc] initWithFrame:CGRectZero];
[self.userInput setDelegate:self];
[self.userInput setBackgroundColor:[UIColor whiteColor]];
[self.userInput.layer setBorderColor:[UIColor lightGrayColor].CGColor];
[self.userInput.layer setBorderWidth:0.5];
[self.userInput setTextAlignment:NSTextAlignmentCenter];
[self.userInput setPlaceholder:NSLocalizedString(@"CaptchaTextPlaceholder", nil)];
[self.userInput setReturnKeyType:UIReturnKeyDone];
[self.userInput setAutocapitalizationType:UITextAutocapitalizationTypeNone];
[self.userInput setAutocorrectionType:UITextAutocorrectionTypeNo];
[self.view addSubview:self.userInput];
self.submitButton = [[UIButton alloc] initWithFrame:CGRectZero];
[self.submitButton setTitle:NSLocalizedString(@"SubmitWord", nil).uppercaseString forState:UIControlStateNormal];
[self.submitButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
[self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[RobloxTheme applyToModalSubmitButton:self.submitButton];
[self.view addSubview:self.submitButton];
self.reloadButton = [[UIButton alloc] initWithFrame:CGRectZero];
[self.reloadButton setTitle:NSLocalizedString(@"ReloadWord", nil).uppercaseString forState:UIControlStateNormal];
[self.reloadButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
[self.reloadButton addTarget:self action:@selector(reloadButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[RobloxTheme applyToModalCancelButton:self.reloadButton];
[self.reloadButton setBackgroundColor:[UIColor whiteColor]];
[self.view addSubview:self.reloadButton];
}
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGRect bounds = self.view.bounds;
NSInteger navBarHeight = self.navigationController.navigationBar.frame.size.height;
[self.instructions setPosition:CGPointMake(MARGIN_WIDTH, navBarHeight + MARGIN_WIDTH)];
[self.instructions setSize:CGSizeMake(bounds.size.width - MARGIN_WIDTH - MARGIN_WIDTH, bounds.size.height / 7)];
[self.imageView setPosition:CGPointMake(MARGIN_WIDTH, CGRectGetMaxY(self.instructions.frame) + MARGIN_WIDTH)];
[self.imageView setSize:CGSizeMake(bounds.size.width - MARGIN_WIDTH - MARGIN_WIDTH, bounds.size.height / 4)];
[self.userInput setPosition:CGPointMake(MARGIN_WIDTH, CGRectGetMaxY(self.imageView.frame) + MARGIN_WIDTH)];
[self.userInput setWidth:bounds.size.width - MARGIN_WIDTH - MARGIN_WIDTH];
[self.userInput setHeight:40];
// This button width takes into account for two buttons side by side separated by MARGIN_WIDTH
NSInteger buttonWidth = (bounds.size.width - MARGIN_WIDTH - MARGIN_WIDTH - MARGIN_WIDTH) / 2;
[self.reloadButton setPosition:CGPointMake(MARGIN_WIDTH, CGRectGetMaxY(self.userInput.frame) + MARGIN_WIDTH)];
[self.reloadButton setWidth:buttonWidth];
[self.reloadButton setHeight:40];
[self.submitButton setPosition:CGPointMake(CGRectGetMaxX(self.reloadButton.frame) + MARGIN_WIDTH, CGRectGetMaxY(self.userInput.frame) + MARGIN_WIDTH)];
[self.submitButton setWidth:buttonWidth];
[self.submitButton setHeight:40];
}
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self getCaptchaChallengeImageWithCompletionHandler:^(UIImage *image, NSError *captchaError) {
[RBXFunctions dispatchOnMainThread:^{
[self.imageView setImage:image];
[self.userInput becomeFirstResponder];
}];
}];
}
#pragma mark Accessors and Mutators
- (BOOL) disablesAutomaticKeyboardDismissal
{
return NO;
}
#pragma mark UI Functions
- (void) submitButtonTapped:(id)sender {
if (![RBXFunctions isEmptyString:self.userInput.text]) {
[self verifyCaptchaForUsername:self.username challenge:self.imageToken response:self.userInput.text completionHandler:^(NSError *rbxRecaptchaError) {
if ([RBXFunctions isEmpty:rbxRecaptchaError]) {
// Captcha Validation Succeeded
[self dismissAndResetCaptchaControllerWithError:rbxRecaptchaError];
}
else
{
// Captcha Validation Failed
if (captchaAttempts < 3) {
[self reDisplayCaptchaAndIncrementAttempts:YES];
}
else
{
[self dismissAndResetCaptchaControllerWithError:rbxRecaptchaError];
}
}
}];
}
}
- (void) reloadButtonTapped:(id)sender
{
[self reDisplayCaptchaAndIncrementAttempts:NO];
}
- (void) reDisplayCaptchaAndIncrementAttempts:(BOOL)incrementAttempts {
[self getCaptchaChallengeImageWithCompletionHandler:^(UIImage *image, NSError *captchaError) {
[RBXFunctions dispatchOnMainThread:^{
if (incrementAttempts) {
captchaAttempts++;
}
[self.userInput setText:@""];
[self.imageView setImage:image];
[self.userInput becomeFirstResponder];
}];
}];
}
- (void) dismissAndResetCaptchaControllerWithError:(NSError *)rbxRecaptchaError
{
captchaAttempts = 0;
[RBXFunctions dispatchOnMainThread:^{
[self dismissViewControllerAnimated:YES completion:nil];
if (nil != self.captchaCompletionHandler) {
self.captchaCompletionHandler(rbxRecaptchaError);
}
}];
}
- (void) getCaptchaChallengeImageWithCompletionHandler:(void(^)(UIImage *image, NSError *captchaError))completionHandler
{
[self getChallengeWithCompletionHandler:completionHandler];
}
- (void) getChallengeWithCompletionHandler:(void(^)(UIImage *image, NSError *captchaError))completionHandler
{
NSString *challengeUrl = [NSString stringWithFormat:@"http://www.google.com/recaptcha/api/challenge?k=%@", publicKey];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:challengeUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if ([RBXFunctions isEmpty:error]) {
if (![RBXFunctions isEmpty:data]) {
NSString *obj = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([obj isKindOfClass:[NSString class]]) {
self.challenge = [self parseChallengeFromRecaptchaState:obj];
[self getImageTokenWithCompletionHandler:completionHandler];
return;
}
}
}
if (nil != completionHandler) {
completionHandler(nil, error);
}
}] resume];
}
- (NSString *) parseChallengeFromRecaptchaState:(NSString *)recaptchaState
{
//NSLog(@"recaptcha state: %@", recaptchaState);
NSString *challenge = nil;
NSArray *components = [recaptchaState componentsSeparatedByString:@","];
//NSLog(@"components: %@", components);
for (NSString *component in components) {
if ([component rangeOfString:@"challenge"].length != 0) {
NSArray *subComponents = [component componentsSeparatedByString:@":"];
//NSLog(@"subComponents: %@", subComponents);
for (NSString *subComponent in subComponents) {
if ([subComponent rangeOfString:@"challenge"].length == 0) {
//NSLog(@"challenge:%@", subComponent);
challenge = [subComponent stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
//NSLog(@"removed white space; challenge:%@", challenge);
challenge = [challenge stringByTrimmingCharactersInSet:[NSCharacterSet punctuationCharacterSet]];
//NSLog(@"removed apostrophes; challenge:%@", challenge);
break;
}
}
}
}
return challenge;
}
- (void) getImageTokenWithCompletionHandler:(void(^)(UIImage *image, NSError *captchaError))completionHandler
{
NSString *imageTokenUrl = [NSString stringWithFormat:@"http://www.google.com/recaptcha/api/reload?c=%@&k=%@&type=image", self.challenge, publicKey];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageTokenUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if ([RBXFunctions isEmpty:error]) {
if (![RBXFunctions isEmpty:data]) {
NSString *obj = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([obj isKindOfClass:[NSString class]]) {
self.imageToken = [self parseImageTokenFromResponse:obj];
[self getImageWithCompletionHandler:completionHandler];
return;
}
}
}
if (nil != completionHandler) {
completionHandler(nil, error);
}
}] resume];
}
- (NSString *) parseImageTokenFromResponse:(NSString *)response
{
NSString *imageToken = nil;
NSString *substring = [response substringFromIndex:[response rangeOfString:@"('"].location];
NSArray *components = [substring componentsSeparatedByString:@","];
for (NSString *component in components) {
if (component.length > 50) {
imageToken = component;
imageToken = [imageToken stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
imageToken = [imageToken stringByTrimmingCharactersInSet:[NSCharacterSet punctuationCharacterSet]];
NSLog(@"imageToken: %@", imageToken);
break;
}
}
return imageToken;
}
- (void) getImageWithCompletionHandler:(void(^)(UIImage *image, NSError *captchaError))completionHandler
{
NSString *imageUrl = [NSString stringWithFormat:@"http://www.google.com/recaptcha/api/image?c=%@", self.imageToken];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
UIImage *responseImage = nil;
if ([RBXFunctions isEmpty:error]) {
if (![RBXFunctions isEmpty:data]) {
responseImage = [UIImage imageWithData:data];
if ([responseImage isKindOfClass:[UIImage class]]) {
self.image = responseImage;
captchaAttempts++;
}
}
}
if (nil != completionHandler) {
completionHandler(responseImage, error);
}
}] resume];
}
- (void) verifyCaptchaForUsername:(NSString *)username challenge:(NSString *)challenge response:(NSString *)response completionHandler:(CaptchaCompletionHandler)completionHandler
{
NSString* validateURL;
switch (self.cType)
{
case (RBCaptchaLogin): validateURL = [[RobloxInfo getApiBaseUrl] stringByAppendingString:@"/captcha/validate/login/"]; break;
case (RBCaptchaSignup): validateURL = [[RobloxInfo getApiBaseUrl] stringByAppendingString:@"/captcha/validate/signup/"]; break;
case (RBCaptchaSocialSignup): validateURL = [[RobloxInfo getApiBaseUrl] stringByAppendingString:@"/captcha/validate/signup/"]; break; //maybe this will matter one day
default: validateURL = [[RobloxInfo getApiBaseUrl] stringByAppendingString:@"/captcha/validate/login/"]; break;
}
NSURL *url = [NSURL URLWithString: validateURL];
//configure the request
NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60 * 7];
[theRequest setHTTPMethod:@"POST"];
// NSDictionary *bodyParams = @{
// @"username":username,
// @"recaptcha_challenge_field":challenge,
// @"recaptcha_response_field":response
// };
// [theRequest setHTTPBody:[NSKeyedArchiver archivedDataWithRootObject:bodyParams]];
NSString* args = [NSString stringWithFormat:@"username=%@&recaptcha_challenge_field=%@&recaptcha_response_field=%@", username, challenge, response];
NSLog(@"recaptcha args: %@", args);
[theRequest setHTTPBody:[args dataUsingEncoding:NSUTF8StringEncoding]];
[RobloxInfo setDefaultHTTPHeadersForRequest:theRequest];
[[[NSURLSession sharedSession] dataTaskWithRequest:theRequest
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"HTTP Response Status: %@", @(httpResponse.statusCode).stringValue);
if ([RBXFunctions isEmpty:error]) {
// collect user info and store in user object
if (httpResponse.statusCode != 200)
{
error = [NSError errorWithDomain:@"CaptchaFailed" code:httpResponse.statusCode userInfo:@{@"captchainfo":args}];
}
}
if (nil != completionHandler) {
completionHandler(error);
}
}] resume];
}
#pragma mark Delegate functions
- (void) resignAllResponders
{
[self.view endEditing:YES];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//NSLog(@"GestureRecognizer : %@", touch);
UIView* touchedView = touch.view;
if (touchedView == self.view)
{
[self resignAllResponders];
return YES;
}
return NO;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self resignAllResponders];
if (textField.text.length > 0)
[self submitButtonTapped:nil];
return YES;
}
@end
@@ -0,0 +1,20 @@
//
// RBCaptchaViewController.h
// RobloxMobile
//
// Created by Kyler Mulherin on 6/8/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBModalPopUpViewController.h"
typedef void(^AttemptedCompletionHandler)(bool success, NSString *message);
@interface RBCaptchaViewController : RBModalPopUpViewController <UIWebViewDelegate>
@property (nonatomic, copy) AttemptedCompletionHandler attemptedCompletionHandler;
// Constructors
+ (instancetype) CaptchaWithCompletionHandler:(AttemptedCompletionHandler)completionHandler;
@end
@@ -0,0 +1,154 @@
//
// RBCaptchaViewController.m
// RobloxMobile
//
// Created by Kyler Mulherin on 6/8/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBCaptchaViewController.h"
#import "RobloxInfo.h"
#import "UIView+Position.h"
#import "RBXEventReporter.h"
#import "RBXFunctions.h"
@interface RBCaptchaViewController ()
@property (nonatomic, retain) UIWebView* webview;
@property (nonatomic, retain) NSString* captchaURLString;
@property (nonatomic, retain) NSString* captchaSolvedURLString;
@property (nonatomic) BOOL solved;
@end
@implementation RBCaptchaViewController
{
void* _captchaContext;
}
+ (instancetype) CaptchaWithCompletionHandler:(AttemptedCompletionHandler)completionHandler
{
RBCaptchaViewController *vc = [[RBCaptchaViewController alloc] init];
vc.solved = NO;
if (nil != completionHandler) {
vc.attemptedCompletionHandler = completionHandler;
}
return vc;
}
// View Lifecycle functions
- (void) viewDidLoad
{
[self shouldAddCloseButton:YES];
[self shouldApplyModalTheme:YES];
//disable the tap recognizer for versions higher than iOS7
if (floor(NSFoundationVersionNumber) >= NSFoundationVersionNumber_iOS_7_1)
[self disableTapRecognizer];
[super viewDidLoad];
self.view.clipsToBounds = YES;
self.view.backgroundColor = [UIColor whiteColor];
_captchaURLString = [[RobloxInfo getWWWBaseUrl] stringByAppendingString:@"mobile-captcha"];
_captchaSolvedURLString = [_captchaURLString stringByAppendingString:@"-solved"];
[self.navigationController.navigationItem setTitle:NSLocalizedString(@"CaptchaWord", nil)];
[RobloxTheme applyToModalPopupNavBar:self.navigationController.navigationBar];
_webview = [[UIWebView alloc] initWithFrame:self.view.frame];
[_webview setDelegate:self];
[_webview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:_captchaURLString]]];
[_webview setScalesPageToFit:NO];
_webview.scrollView.scrollEnabled = NO;
_webview.scrollView.pagingEnabled = NO;
[self.view addSubview:_webview];
[_webview.scrollView addObserver:self forKeyPath:@"contentSize" options:0 context:_captchaContext];
[[RBXEventReporter sharedInstance] reportScreenLoaded:RBXAContextCaptcha];
}
- (void) viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[_webview setSize:self.view.frame.size];
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self becomeFirstResponder];
}
- (void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self resignFirstResponder];
[_webview stopLoading];
[_webview.scrollView removeObserver:self forKeyPath:@"contentSize"];
if (!self.solved)
{
[[RBXEventReporter sharedInstance] reportButtonClick:RBXAButtonClose
withContext:RBXAContextCaptcha];
if (nil != self.attemptedCompletionHandler) {
self.attemptedCompletionHandler(NO, nil);
}
}
}
- (void) dealloc
{
[_webview setDelegate:nil];
}
// Override functions
-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == _captchaContext)
{
//make it so that the view scrolls if and ONLY if it needs to. Scrolling looks dumb -Kyler
_webview.scrollView.scrollEnabled = (_webview.scrollView.contentSize.height > self.view.frame.size.height);
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// Delegate Functions
-(void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
[[RBXEventReporter sharedInstance] reportFormFieldValidation:RBXAFieldCaptcha
withContext:RBXAContextCaptcha
withError:RBXAErrorMiscWebErrors];
[self dismissViewControllerAnimated:NO completion:nil];
}
-(void) webViewDidFinishLoad:(UIWebView *)webView
{
NSString* url = webView.request.URL.absoluteString;
if ([url isEqualToString:_captchaSolvedURLString])
{
//The successful response event reporting is handled by the web
if (nil != self.attemptedCompletionHandler) {
self.attemptedCompletionHandler(YES, nil);
}
//mark the captcha as solved and go back to whatever we were doing
self.solved = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
@end
@@ -0,0 +1,18 @@
//
// RBFavoritesView
// RobloxMobile
//
// Created by Ariel Lichtin on 10/1/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxData.h"
@interface RBFavoritesView : UIView
- (void) setFavoritesForGame:(RBXGameData*)gameData;
- (void) setFavoritesForPass:(RBXGamePass*)gamePass;
- (void) setFavoritesForGear:(RBXGameGear*)gameGear;
@end
@@ -0,0 +1,177 @@
//
// RBFavoritesView
// RobloxMobile
//
// Created by Ariel Lichtin on 10/1/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBFavoritesView.h"
#import "UIView+Position.h"
#import "RobloxData.h"
#import "RobloxHUD.h"
#import "RobloxTheme.h"
#define BAR_HEIGHT 4.0f
#define HORIZONTAL_MARGIN 33.0f
#define RADIUS 2.0f
#define FAVORITES_NORMAL @"Favorites"
#define FAVORITES_FILLED @"Favorites Filled"
@implementation RBFavoritesView
{
NSString* _assetID;
BOOL _userFavorited;
NSUInteger _favoriteCount;
UILabel* _label;
UIButton* _button;
}
- (instancetype)init
{
self = [super init];
if(self)
{
[self initElements];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
[self initElements];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self initElements];
}
return self;
}
- (void) setFavoritesForGame:(RBXGameData*)gameData
{
_assetID = gameData.placeID;
_userFavorited = gameData.userFavorited;
_favoriteCount = gameData.favorited;
[self updateFavorites];
}
- (void) setFavoritesForPass:(RBXGamePass*)gamePass
{
_assetID = [gamePass.passID stringValue];
_userFavorited = gamePass.userFavorited;
_favoriteCount = gamePass.favoriteCount;
[self updateFavorites];
}
- (void) setFavoritesForGear:(RBXGameGear*)gameGear
{
_assetID = [gameGear.assetID stringValue];
_userFavorited = gameGear.userFavorited;
_favoriteCount = gameGear.favoriteCount;
[self updateFavorites];
}
- (void) initElements
{
self.backgroundColor = [UIColor clearColor];
UIImage* favoritesImage = [UIImage imageNamed:FAVORITES_NORMAL];
_button = [UIButton buttonWithType:UIButtonTypeCustom];
_button.size = favoritesImage.size;
_button.position = CGPointMake(0, self.height - favoritesImage.size.height);
_button.contentMode = UIViewContentModeScaleToFill;
[_button setImage:favoritesImage forState:UIControlStateNormal];
[_button addTarget:self action:@selector(favoritedTouched) forControlEvents:UIControlEventTouchDown];
[self addSubview:_button];
CGRect labelRect;
labelRect.size = CGSizeMake(70.0, 23.0);
labelRect.origin = CGPointMake(30.0, self.height - labelRect.size.height);
_label = [[UILabel alloc] initWithFrame:labelRect];
[RobloxTheme applyToFavoriteLabel:_label];
[self addSubview:_label];
}
- (void) updateFavorites
{
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[numberFormatter setMaximumFractionDigits:0];
_label.text = [numberFormatter stringFromNumber:[NSNumber numberWithUnsignedInteger:_favoriteCount]];
if(_userFavorited)
[_button setImage:[UIImage imageNamed:FAVORITES_FILLED] forState:UIControlStateNormal];
else
[_button setImage:[UIImage imageNamed:FAVORITES_NORMAL] forState:UIControlStateNormal];
}
- (void) favoritedTouched
{
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^
{
_button.transform = CGAffineTransformMakeScale(2.0, 2.0);
}
completion:^(BOOL finished)
{
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^
{
_button.transform = CGAffineTransformIdentity;
}
completion:nil];
}];
[RobloxData favoriteToggleForAssetID:_assetID
completion:^(BOOL success, NSString *message)
{
dispatch_async(dispatch_get_main_queue(), ^
{
if(success)
{
if(_userFavorited)
{
if(_favoriteCount > 0)
{
_favoriteCount--;
}
}
else
{
_favoriteCount++;
}
_userFavorited = !_userFavorited;
[self updateFavorites];
}
else if(message != nil)
{
[RobloxHUD showMessage:message];
}
});
}];
}
@end
@@ -0,0 +1,18 @@
//
// RBGenderView.h
// RobloxMobile
//
// Created by Kyler Mulherin on 7/13/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "SignupVerifier.h"
@interface RBGenderView : UIView
@property (nonatomic, setter=setGender:) Gender playerGender;
-(void) setTouchBlock:(void (^)())block;
@end
@@ -0,0 +1,169 @@
//
// RBGenderView.m
// RobloxMobile
//
// Created by Kyler Mulherin on 7/13/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBGenderView.h"
#import "RobloxTheme.h"
#import "UIView+Position.h"
@implementation RBGenderView
{
UIButton* _btnMale;
UIButton* _btnFemale;
UILabel* _lblGender;
void (^touchBlock)();
}
//Constructors and Initialization stuff
-(void) initialize
{
_playerGender = GENDER_DEFAULT;
[self.layer setBorderWidth:1];
[self.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[self.layer setCornerRadius:5.0];
[self setClipsToBounds:YES];
_btnMale = [[UIButton alloc] init];
[_btnMale addTarget:self action:@selector(didPressButtonMale:) forControlEvents:UIControlEventTouchUpInside];
[_btnMale setImage:[UIImage imageNamed:@"Gender Male Selected"] forState:UIControlStateSelected];
[_btnMale setImage:[UIImage imageNamed:@"Gender Male Unselected"] forState:UIControlStateNormal];
[_btnMale.titleLabel setFont:[UIFont fontWithName:@"SourceSansPro-Regular" size:18]];
[_btnMale setTitleColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f] forState:UIControlStateNormal];
[_btnMale setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[_btnMale setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
[_btnMale setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected | UIControlStateHighlighted];
[_btnMale.layer setBorderWidth:1];
[_btnMale.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
_btnFemale = [[UIButton alloc] init];
[_btnFemale addTarget:self action:@selector(didPressButtonFemale:) forControlEvents:UIControlEventTouchUpInside];
[_btnFemale setImage:[UIImage imageNamed:@"Gender Female Selected"] forState:UIControlStateSelected];
[_btnFemale setImage:[UIImage imageNamed:@"Gender Female Unselected"] forState:UIControlStateNormal];
[_btnFemale setTitleColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f] forState:UIControlStateNormal];
[_btnFemale setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[_btnFemale setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
[_btnFemale setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected | UIControlStateHighlighted];
//[_btnFemale.layer setBorderWidth:1];
//[_btnFemale.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
_lblGender = [[UILabel alloc] init];
[_lblGender setText:NSLocalizedString(@"GenderWord", nil)];
//[RobloxTheme applyToModalLoginTitleLabel:_lblGender];
[_lblGender setFont:[RobloxTheme fontBodyBold]];
[_lblGender setTextColor:[RobloxTheme colorGray1]];
//[_lblGender setTextColor:[RobloxTheme colorGray1]];
//add the objects to the view
[self addSubview:_lblGender];
[self addSubview:_btnMale];
[self addSubview:_btnFemale];
}
-(void) layoutSubviews
{
//layout the view
CGFloat cellWidth = self.width / 3;
int margin = 12; //self.width * 0.05;
[_lblGender setFrame:CGRectMake(margin, 0, (cellWidth - margin), self.height)];
[_btnMale setFrame:CGRectMake(_lblGender.right, -1, cellWidth, self.height+2)];
[_btnFemale setFrame:CGRectMake(_btnMale.right-1, -1, cellWidth+1, self.height+2)];
}
-(id) init
{
self = [super init];
if (self)
[self initialize];
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
[self initialize];
return self;
}
-(id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
[self initialize];
return self;
}
//Mutator
-(void)setGender:(Gender)gender { _playerGender = gender; }
-(void)setTouchBlock:(void (^)())block { touchBlock = block; }
//Actions
-(void)highlightButton:(UIButton*)aButton
{
aButton.selected = YES;
}
-(void)resetButton:(UIButton*)aButton
{
aButton.selected = NO;
}
-(void)didPressButtonMale:(id)sender
{
if (touchBlock)
touchBlock();
if (_playerGender == GENDER_GIRL)
[self resetButton:_btnFemale];
else if (_playerGender == GENDER_BOY)
{
[self resetButton:_btnMale];
_playerGender = GENDER_DEFAULT;
[self markAsNormal];
return;
}
_playerGender = GENDER_BOY;
[self highlightButton:_btnMale];
[self markAsValid];
}
-(void)didPressButtonFemale:(id)sender
{
if (touchBlock)
touchBlock();
if (_playerGender == GENDER_BOY)
[self resetButton:_btnMale];
else if (_playerGender == GENDER_GIRL)
{
[self resetButton:_btnFemale];
_playerGender = GENDER_DEFAULT;
[self markAsNormal];
return;
}
_playerGender = GENDER_GIRL;
[self highlightButton:_btnFemale];
[self markAsValid];
}
//Colors and crap
-(void) changeBorderColor:(UIColor*)color {
dispatch_async(dispatch_get_main_queue(), ^
{
self.layer.borderColor=[color CGColor];
_btnMale.layer.borderColor = [color CGColor];
//_btnFemale.layer.borderColor = [color CGColor];
});
}
-(void) markAsValid { [self changeBorderColor:[RobloxTheme colorGreen1]]; /*[_lblGender setTextColor:[RobloxTheme colorGray1]];*/ }
-(void) markAsNormal { [self changeBorderColor:[RobloxTheme colorGray4]]; /*[_lblGender setTextColor:[UIColor colorWithWhite:(0xA9/255.f) alpha:1.0f]];*/ }
@end
@@ -0,0 +1,43 @@
//
// RBInfiniteCollectionView.h
// RobloxMobile
//
// Created by Ariel Lichtin on 10/14/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
// Forward declarations
@class RBInfiniteCollectionView;
// Infinite scroll view delagate
@protocol RBInfiniteCollectionViewDelegate <UICollectionViewDelegate>
@required
//
- (void) asyncRequestItemsForCollectionView:(RBInfiniteCollectionView*)collectionView numItemsToRequest:(NSUInteger)itemsToRequest completionHandler:(void(^)())completionHandler;
- (NSUInteger)numItemsInInfiniteCollectionView:(RBInfiniteCollectionView*)collectionView;
- (UICollectionViewCell*) infiniteCollectionView:(RBInfiniteCollectionView*)collectionView cellForItemAtIndexPath:(NSIndexPath*)indexPath;
@optional
- (void) infiniteCollectionView:(RBInfiniteCollectionView*)collectionView didSelectItemAtIndexPath:(NSIndexPath*)indexPath;
@end
// Infinite Collection View
@interface RBInfiniteCollectionView : UICollectionView
// Retrieves the total number of items in the collection view (real cells + placeholders)
@property(nonatomic, readonly) NSUInteger numItems;
// Delegate
@property(weak, nonatomic) id<RBInfiniteCollectionViewDelegate> infiniteDelegate;
// Start
- (void) loadElementsAsync;
@end
@@ -0,0 +1,167 @@
//
// RBInfiniteCollectionView.m
// RobloxMobile
//
// Created by Ariel Lichtin on 10/14/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBInfiniteCollectionView.h"
#define NUM_ITEMS_PER_REQUEST 40
#define START_REQUEST_THRESHOLD 10 // The next request will start when there are 10 elements left
@interface RBInfiniteCollectionView () <UICollectionViewDelegate, UICollectionViewDataSource>
@end
@implementation RBInfiniteCollectionView
{
NSUInteger _numItemsInCollectionView;
BOOL _requestInProgress;
BOOL _listComplete;
}
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
self = [super initWithFrame:frame collectionViewLayout:layout];
if(self)
{
_numItemsInCollectionView = NUM_ITEMS_PER_REQUEST;
_listComplete = NO;
_requestInProgress = NO;
__weak RBInfiniteCollectionView* weakSelf = self;
self.delegate = weakSelf;
self.dataSource = weakSelf;
[self reloadData];
}
return self;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Get the index of the last visible item
NSArray* indexes = [self indexPathsForVisibleItems];
NSIndexPath* maxIndex = nil;
for(NSIndexPath* index in indexes)
{
if(maxIndex == nil || maxIndex.row < index.row)
{
maxIndex = index;
}
}
if(_listComplete == NO && maxIndex.row + START_REQUEST_THRESHOLD > _numItemsInCollectionView)
{
_numItemsInCollectionView += NUM_ITEMS_PER_REQUEST;
[self performBatchUpdates:^
{
NSMutableArray* indexes = [NSMutableArray array];
for(NSUInteger i = _numItemsInCollectionView - NUM_ITEMS_PER_REQUEST; i < _numItemsInCollectionView; ++i)
{
[indexes addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self insertItemsAtIndexPaths:indexes];
}
completion:nil];
[self loadElementsAsync];
}
}
- (void)loadElementsAsync
{
if(_infiniteDelegate == nil)
return;
NSUInteger itemCount = [_infiniteDelegate numItemsInInfiniteCollectionView:self];
if(!_listComplete && !_requestInProgress && _numItemsInCollectionView > itemCount)
{
_requestInProgress = YES;
NSUInteger itemsToRequest = _numItemsInCollectionView - itemCount;
void(^block)() = ^()
{
NSUInteger newItemCount = [_infiniteDelegate numItemsInInfiniteCollectionView:self];
NSInteger rangeFrom = itemCount;
NSInteger rangeTo = newItemCount;
// Update visible elements
NSArray* visibleCells = [self indexPathsForVisibleItems];
NSMutableArray* cellsToUpdate = [NSMutableArray array];
for(NSIndexPath* indexPath in visibleCells)
{
BOOL inRange = indexPath.row >= rangeFrom && indexPath.row < rangeTo;
if(inRange)
[cellsToUpdate addObject:indexPath];
}
[self reloadItemsAtIndexPaths:cellsToUpdate];
NSUInteger numItemsRetrieved = newItemCount - itemCount;
_listComplete = numItemsRetrieved < itemsToRequest;
if(_listComplete)
{
// If the list is already complete,
// remove the remaining placeholder empty cells
NSUInteger totalElements = _numItemsInCollectionView;
_numItemsInCollectionView = newItemCount;
[self performBatchUpdates:^
{
NSMutableArray* indexes = [NSMutableArray array];
for(NSUInteger i = newItemCount; i < totalElements; ++i)
{
[indexes addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self deleteItemsAtIndexPaths:indexes];
}
completion:nil];
}
_requestInProgress = NO;
[self loadElementsAsync];
};
[_infiniteDelegate asyncRequestItemsForCollectionView:self numItemsToRequest:itemsToRequest completionHandler:block];
}
}
- (NSUInteger)numItems
{
return _numItemsInCollectionView;
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _numItemsInCollectionView;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
if(_infiniteDelegate)
return [_infiniteDelegate infiniteCollectionView:self cellForItemAtIndexPath:indexPath];
else
return nil;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if(_infiniteDelegate)
{
[_infiniteDelegate infiniteCollectionView:self didSelectItemAtIndexPath:indexPath];
}
}
@end
@@ -0,0 +1,43 @@
//
// RBInfiniteTableView.h
// RobloxMobile
//
// Created by Ariel Lichtin on 10/14/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
// Forward declarations
@class RBInfiniteTableView;
// Infinite scroll view delagate
@protocol RBInfiniteTableViewDelegate <UICollectionViewDelegate>
@required
//
- (void) asyncRequestItemsForTableView:(RBInfiniteTableView*)tableView numItemsToRequest:(NSUInteger)itemsToRequest completionHandler:(void(^)())completionHandler;
- (NSUInteger)numItemsInInfiniteTableView:(RBInfiniteTableView*)tableView;
- (UITableViewCell*) infiniteTableView:(RBInfiniteTableView*)tableView cellForItemAtIndexPath:(NSIndexPath*)indexPath;
@optional
- (void) infiniteTableView:(RBInfiniteTableView*)tableView didSelectItemAtIndexPath:(NSIndexPath*)indexPath;
@end
// Infinite Collection View
@interface RBInfiniteTableView : UITableView
// Retrieves the total number of items in the collection view (real cells + placeholders)
@property(nonatomic, readonly) NSUInteger numItems;
// Delegate
@property(weak, nonatomic) id<RBInfiniteTableViewDelegate> infiniteDelegate;
// Start
- (void) loadElementsAsync;
@end
@@ -0,0 +1,193 @@
//
// RBInfiniteTableView.m
// RobloxMobile
//
// Created by Ariel Lichtin on 10/14/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBInfiniteTableView.h"
#define NUM_ITEMS_PER_REQUEST 20
#define START_REQUEST_THRESHOLD 10 // The next request will start when there are 10 elements left
@interface RBInfiniteTableView () <UITableViewDelegate, UITableViewDataSource>
@end
@implementation RBInfiniteTableView
{
NSUInteger _numItemsInView;
BOOL _requestInProgress;
BOOL _listComplete;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
[self initTableView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self initTableView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
{
self = [super initWithFrame:frame style:style];
if(self)
{
[self initTableView];
}
return self;
}
- (void)initTableView
{
_numItemsInView = NUM_ITEMS_PER_REQUEST;
_listComplete = NO;
_requestInProgress = NO;
__weak RBInfiniteTableView* weakSelf = self;
self.delegate = weakSelf;
self.dataSource = weakSelf;
[self reloadData];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Get the index of the last visible item
NSArray* indexes = [self indexPathsForVisibleRows];
NSIndexPath* maxIndex = nil;
for(NSIndexPath* index in indexes)
{
if(maxIndex == nil || maxIndex.row < index.row)
{
maxIndex = index;
}
}
if(_listComplete == NO && maxIndex.row + START_REQUEST_THRESHOLD > _numItemsInView)
{
_numItemsInView += NUM_ITEMS_PER_REQUEST;
NSMutableArray* indexes = [NSMutableArray array];
for(NSUInteger i = _numItemsInView - NUM_ITEMS_PER_REQUEST; i < _numItemsInView; ++i)
{
[indexes addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self beginUpdates];
[self insertRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationAutomatic];
[self endUpdates];
[self loadElementsAsync];
}
}
- (void)loadElementsAsync
{
if(_infiniteDelegate == nil)
return;
NSUInteger itemCount = [_infiniteDelegate numItemsInInfiniteTableView:self];
if(!_listComplete && !_requestInProgress && _numItemsInView > itemCount)
{
_requestInProgress = YES;
NSUInteger itemsToRequest = _numItemsInView - itemCount;
void(^block)() = ^()
{
NSUInteger newItemCount = [_infiniteDelegate numItemsInInfiniteTableView:self];
NSInteger rangeFrom = itemCount;
NSInteger rangeTo = newItemCount;
// Update visible elements
NSArray* visibleCells = [self indexPathsForVisibleRows];
if(visibleCells.count > 0)
{
NSMutableArray* cellsToUpdate = [NSMutableArray array];
for(NSIndexPath* indexPath in visibleCells)
{
BOOL inRange = indexPath.row >= rangeFrom && indexPath.row < rangeTo;
if(inRange)
[cellsToUpdate addObject:indexPath];
}
[self reloadRowsAtIndexPaths:cellsToUpdate withRowAnimation:UITableViewRowAnimationAutomatic];
}
NSUInteger numItemsRetrieved = newItemCount - itemCount;
_listComplete = numItemsRetrieved < itemsToRequest;
if(_listComplete)
{
// If the list is already complete,
// remove the remaining placeholder empty cells
NSUInteger totalElements = _numItemsInView;
_numItemsInView = newItemCount;
NSMutableArray* indexes = [NSMutableArray array];
for(NSUInteger i = newItemCount; i < totalElements; ++i)
{
[indexes addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self beginUpdates];
[self deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationAutomatic];
[self endUpdates];
}
_requestInProgress = NO;
[self loadElementsAsync];
};
[_infiniteDelegate asyncRequestItemsForTableView:self numItemsToRequest:itemsToRequest completionHandler:block];
}
}
- (NSUInteger)numItems
{
return _numItemsInView;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _numItemsInView;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(_infiniteDelegate)
return [_infiniteDelegate infiniteTableView:self cellForItemAtIndexPath:indexPath];
else
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(_infiniteDelegate)
{
[_infiniteDelegate infiniteTableView:self didSelectItemAtIndexPath:indexPath];
}
}
@end
@@ -0,0 +1,17 @@
//
// RBModalPopUpViewController.h
// RobloxMobile
//
// Created by Kyler Mulherin on 10/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#include "RobloxTheme.h"
@interface RBModalPopUpViewController : UIViewController <UIGestureRecognizerDelegate>
-(void) disableTapRecognizer;
-(void) shouldAddCloseButton:(BOOL)shouldAdd;
-(void) shouldApplyModalTheme:(BOOL)shouldApply;
@end
@@ -0,0 +1,111 @@
//
// RBModalPopUpViewController.m
// A CLASS THAT SHOULD DISMISS ITSELF WHEN A TAP IS REGISTERED OUTSIDE ITS OWN BOUNDS
// RobloxMobile
//
// Created by Kyler Mulherin on 10/15/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBModalPopUpViewController.h"
@interface RBModalPopUpViewController ()
@property UITapGestureRecognizer* rbOutsideTapRecognizer;
@end
@implementation RBModalPopUpViewController
{
bool _tapsEnabled;
bool _addCloseButton;
bool _applyTheme;
}
- (id) init
{
self = [super init];
if (self)
{
_tapsEnabled = YES;
_addCloseButton = YES;
_applyTheme = YES;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_rbOutsideTapRecognizer = [self getTapRecognizer];
if (_addCloseButton)
{
UIButton* close = [RobloxTheme applyCloseButtonToUINavigationItem:self.navigationItem];
[close addTarget:self action:@selector(dismissView:) forControlEvents:UIControlEventTouchUpInside];
}
if (_applyTheme)
[RobloxTheme applyToModalPopupNavBar:self.navigationController.navigationBar];
}
- (UITapGestureRecognizer*) getTapRecognizer
{
if (_rbOutsideTapRecognizer == nil)
{
_rbOutsideTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOutside:)];
[_rbOutsideTapRecognizer setCancelsTouchesInView:NO];
[_rbOutsideTapRecognizer setDelegate:self];
}
return _rbOutsideTapRecognizer;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.view.window addGestureRecognizer:[self getTapRecognizer]];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.view.window removeGestureRecognizer:_rbOutsideTapRecognizer];
}
-(void)disableTapRecognizer
{
_tapsEnabled = NO;
}
-(void)shouldAddCloseButton:(BOOL)shouldAdd
{
_addCloseButton = shouldAdd;
}
-(void)shouldApplyModalTheme:(BOOL)shouldApply
{
_applyTheme = shouldApply;
}
//Gesture Recognition
//Tap Recognition
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { return YES; }
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { return YES; }
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; }
- (void) didTapOutside:(UIGestureRecognizer*)sender
{
if (!_tapsEnabled)
return;
//check the position of the tap and dismiss the view if it lies outside the bounds of the view
if (sender.state == UIGestureRecognizerStateEnded)
{
UIView* root = self.view.window.rootViewController.view;
CGPoint location = [sender locationInView:root];
location = [self.view convertPoint:location fromView:root];
if (![self.view pointInside:location withEvent:nil])
{
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
//close button
- (void)dismissView:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; }
@end
@@ -0,0 +1,22 @@
//
// PlayerThumbnailCell.h
// RobloxMobile
//
// Created by Ariel Lichtin on 9/23/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RobloxImageView.h"
#import "RobloxData.h"
@interface RBPlayerThumbnailCell : UICollectionViewCell
@property(strong, nonatomic) RBXFriendInfo* friendInfo;
@property(weak, nonatomic) IBOutlet RobloxImageView* avatar;
@property(weak, nonatomic) IBOutlet UILabel* nameLabel;
@property(weak, nonatomic) IBOutlet UIImageView* isOnlineMarker;
@end
@@ -0,0 +1,51 @@
//
// PlayerThumbnailCell.m
// RobloxMobile
//
// Created by Ariel Lichtin on 9/23/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBPlayerThumbnailCell.h"
#import "RobloxTheme.h"
#define FRIEND_AVATAR_SIZE CGSizeMake(110, 110)
@implementation RBPlayerThumbnailCell
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
[RobloxTheme applyRoundedBorderToView:self];
}
return self;
}
- (void)setFriendInfo:(RBXFriendInfo *)friendInfo
{
_friendInfo = friendInfo;
[RobloxTheme applyToFriendCellLabel:self.nameLabel];
self.nameLabel.backgroundColor = [UIColor whiteColor];
if(_friendInfo)
{
self.nameLabel.text = friendInfo.username;
self.avatar.animateInOptions = RBXImageViewAnimateInAlways;
[self.avatar loadAvatarForUserID:[friendInfo.userID integerValue] prefetchedURL:friendInfo.avatarURL urlIsFinal:friendInfo.avatarIsFinal withSize:FRIEND_AVATAR_SIZE completion:nil];
NSString* imageName = friendInfo.isOnline ? @"User Online" : @"User Offline";
[self.isOnlineMarker setImage:[UIImage imageNamed:imageName]];
}
else
{
self.nameLabel.text = @"";
self.avatar.image = nil;
self.isOnlineMarker.image = nil;
}
}
@end
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6250" systemVersion="14A389" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="RBPlayerThumbnailCell">
<rect key="frame" x="0.0" y="0.0" width="90" height="108"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="90" height="108"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wxV-AT-NyI" customClass="RobloxImageView">
<rect key="frame" x="0.0" y="0.0" width="90" height="90"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Friend name" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j38-cH-eU1">
<rect key="frame" x="0.0" y="87" width="90" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Akd-eS-gjx">
<rect key="frame" x="75" y="75" width="9" height="9"/>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<size key="customSize" width="155" height="157"/>
<connections>
<outlet property="avatar" destination="wxV-AT-NyI" id="wHS-0A-9MB"/>
<outlet property="isOnlineMarker" destination="Akd-eS-gjx" id="zPi-V5-uc6"/>
<outlet property="nameLabel" destination="j38-cH-eU1" id="lQl-W6-981"/>
</connections>
<point key="canvasLocation" x="564.5" y="584.5"/>
</collectionViewCell>
</objects>
</document>
@@ -0,0 +1,13 @@
//
// RBRoundBorder.h
// RobloxMobile
//
// Created by Ariel Lichtin on 10/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBRoundedeBorder : UIView
@end
@@ -0,0 +1,94 @@
//
// RBRoundBorder.m
// RobloxMobile
//
// Created by Ariel Lichtin on 10/27/14.
// Copyright (c) 2014 ROBLOX. All rights reserved.
//
#import "RBRoundedeBorder.h"
#import "UIView+Position.h"
@implementation RBRoundedeBorder
{
UIImageView* _left;
UIImageView* _right;
UIImageView* _top;
UIImageView* _bottom;
UIImageView* _topLeft;
UIImageView* _topRight;
UIImageView* _bottomLeft;
UIImageView* _bottomRight;
}
- (instancetype)init
{
self = [super init];
if(self)
{
_left = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-left-middle"]];
_right = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-right-middle"]];
_top = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-top-middle"]];
_bottom = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-bottom-middle"]];
_topLeft = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-top-left"]];
_topRight = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-top-right"]];
_bottomLeft = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-bottom-left"]];
_bottomRight = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"rounded-border-bottom-right"]];
[self addSubview:_left];
[self addSubview:_right];
[self addSubview:_top];
[self addSubview:_bottom];
[self addSubview:_topLeft];
[self addSubview:_topRight];
[self addSubview:_bottomLeft];
[self addSubview:_bottomRight];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
if(self.superview != nil)
{
CGRect bounds = self.superview.bounds;
CGFloat borderWidth = _left.frame.size.width;
CGFloat borderOffset = borderWidth * 0.5;
bounds = CGRectInset(bounds, -borderOffset, -borderOffset);
// Set corners
_topLeft.x = bounds.origin.x;
_topLeft.y = bounds.origin.y;
_bottomLeft.x = bounds.origin.x;
_bottomLeft.bottom = CGRectGetMaxY(bounds);
_topRight.right = CGRectGetMaxX(bounds);
_topRight.y = bounds.origin.y;
_bottomRight.right = CGRectGetMaxX(bounds);
_bottomRight.bottom = CGRectGetMaxY(bounds);
// Set borders
_left.x = _topLeft.x;
_left.y = _topLeft.bottom;
_left.height = _bottomLeft.y - _topLeft.bottom;
_top.x = _topLeft.right;
_top.y = _topLeft.y;
_top.width = _topRight.x - _topLeft.right;
_right.x = _topRight.x;
_right.y = _topRight.bottom;
_right.height = _bottomRight.y - _topRight.bottom;
_bottom.x = _bottomLeft.right;
_bottom.y = _bottomLeft.y;
_bottom.width = _bottomRight.x - _bottomLeft.right;
}
}
@end
@@ -0,0 +1,30 @@
//
// RBSearchUserCell.h
// RobloxMobile
//
// Created by Kyler Mulherin on 1/14/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#include <UIKit/UIKit.h>
#include "RobloxImageView.h"
#include "RobloxData.h"
@interface RBSearchUserCell : UICollectionViewCell
//cell visual elements
@property IBOutlet UILabel* lblName;
@property IBOutlet UITextView* lblBlurb;
@property IBOutlet UITextView* lblOtherNames;
@property IBOutlet UILabel* lblOtherNamesWord;
@property IBOutlet RobloxImageView* imgAvatar;
@property IBOutlet UIImageView* imgIsOnline;
//data values
@property (strong, nonatomic) RBXUserSearchInfo* searchInfo;
+ (CGSize) getCellSize;
+ (NSString*) getNibName;
-(void) setInfo:(RBXUserSearchInfo *)searchInfo;
@end
@@ -0,0 +1,111 @@
//
// RBSearchUserCell.m
// RobloxMobile
//
// Created by Kyler Mulherin on 1/14/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBSearchUserCell.h"
#import "RobloxTheme.h"
#import "RBActivityIndicatorView.h"
#import "UIView+Position.h"
#import "RobloxInfo.h"
#define DEFAULT_CELL_HEIGHT 154
@interface RBSearchUserCell ()
@end
@implementation RBSearchUserCell
+ (CGSize) getCellSize
{
return [RobloxInfo thisDeviceIsATablet] ? CGSizeMake(514, 154) : CGSizeMake(300, 200);
}
+ (NSString*) getNibName
{
return [RobloxInfo thisDeviceIsATablet] ? @"RBSearchUserCell" : @"RBSearchUserCell_iPhone";
}
- (void)awakeFromNib
{
dispatch_async(dispatch_get_main_queue(), ^
{
[RobloxTheme applyRoundedBorderToView:self];
self.backgroundColor = [UIColor whiteColor];
[_lblName setText:@""];
[_lblBlurb setText:@""];
[_lblOtherNames setText:@""];
_imgAvatar.image = nil;
_imgIsOnline.image = nil;
self.clipsToBounds = YES;
});
}
-(void) setInfo:(RBXUserSearchInfo *)searchInfo
{
_searchInfo = searchInfo;
if(_searchInfo)
{
dispatch_async(dispatch_get_main_queue(), ^
{
[_lblName setText:_searchInfo.userName];
[_lblBlurb setText:_searchInfo.blurb];
[_lblOtherNames setText:_searchInfo.previousNames];
[_lblOtherNamesWord setText:NSLocalizedString(@"UsersPreviousNamesWord", nil)];
NSString* imageName = _searchInfo.isOnline ? @"User Online" : @"User Offline";
[_imgIsOnline setImage:[UIImage imageNamed:imageName]];
_imgIsOnline.hidden = NO;
//check if there are other names, hide them if there are not
bool shouldHideOtherNames = _searchInfo.previousNames.length < 3;
_lblOtherNames.hidden = shouldHideOtherNames;
_lblOtherNamesWord.hidden = shouldHideOtherNames;
_lblName.hidden = NO;
_lblBlurb.hidden = NO;
if ([RobloxInfo thisDeviceIsATablet])
{
//extend the text areas if there are no other names
CGFloat margin = _lblBlurb.y - (_lblOtherNamesWord.y + _lblOtherNamesWord.height);
CGFloat blurbHeight = 90;
[_lblBlurb setY:shouldHideOtherNames ? _lblOtherNamesWord.y : _lblOtherNamesWord.y + _lblOtherNamesWord.height + margin];
[_lblBlurb setHeight:shouldHideOtherNames ? (blurbHeight + _lblOtherNamesWord.height + margin) : blurbHeight];
}
});
//load the avatar image
RBActivityIndicatorView* spinner = [[RBActivityIndicatorView alloc] initWithFrame:_imgAvatar.frame];
[self addSubview:spinner];
[spinner startAnimating];
_imgAvatar.animateInOptions = RBXImageViewAnimateInAlways;
[_imgAvatar loadAvatarForUserID:_searchInfo.userId
prefetchedURL:_searchInfo.avatarURL
urlIsFinal:_searchInfo.avatarIsFinal
withSize:[RobloxTheme sizeProfilePictureLarge]
completion:^
{
dispatch_async(dispatch_get_main_queue(), ^
{
_imgAvatar.hidden = NO;
[spinner stopAnimating];
[spinner removeFromSuperview];
});
}];
}
}
@end
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6254" systemVersion="14A389" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
</dependencies>
<customFonts key="customFonts">
<mutableArray key="SourceSansPro-Bold.ttf">
<string>SourceSansPro-Bold</string>
<string>SourceSansPro-Bold</string>
</mutableArray>
<mutableArray key="SourceSansPro-Regular.ttf">
<string>SourceSansPro-Regular</string>
<string>SourceSansPro-Regular</string>
</mutableArray>
</customFonts>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="7T5-aR-zwc" customClass="RBSearchUserCell">
<rect key="frame" x="0.0" y="0.0" width="514" height="154"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="514" height="154"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="POr-U9-Z60" customClass="RobloxImageView">
<rect key="frame" x="8" y="22" width="110" height="110"/>
</imageView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="MAX_TWENTY_CHARACTERS" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nCb-cE-455">
<rect key="frame" x="148" y="8" width="336" height="21"/>
<fontDescription key="fontDescription" name="SourceSansPro-Bold" family="Source Sans Pro" pointSize="16"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="User Offline" translatesAutoresizingMaskIntoConstraints="NO" id="Izv-Sl-B6P">
<rect key="frame" x="129" y="15" width="9" height="9"/>
</imageView>
<textView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" scrollEnabled="NO" pagingEnabled="YES" showsVerticalScrollIndicator="NO" editable="NO" text="ONE_NAME, ANOTHER_NAME" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KEZ-Tp-fcw">
<rect key="frame" x="284" y="22" width="230" height="26"/>
<fontDescription key="fontDescription" name="SourceSansPro-Regular" family="Source Sans Pro" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_PREVIOUS_NAMES_" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sIK-iD-Mhr">
<rect key="frame" x="128" y="28" width="166" height="20"/>
<fontDescription key="fontDescription" name="SourceSansPro-Bold" family="Source Sans Pro" pointSize="16"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<textView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" showsHorizontalScrollIndicator="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0bN-gt-qfP">
<rect key="frame" x="129" y="56" width="377" height="90"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<fontDescription key="fontDescription" name="SourceSansPro-Regular" family="Source Sans Pro" pointSize="16"/>
<textInputTraits key="textInputTraits"/>
</textView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<size key="customSize" width="514" height="154"/>
<connections>
<outlet property="imgAvatar" destination="POr-U9-Z60" id="uGc-Tb-JsP"/>
<outlet property="imgIsOnline" destination="Izv-Sl-B6P" id="Q2p-Yr-eMT"/>
<outlet property="lblBlurb" destination="0bN-gt-qfP" id="7Or-1p-hN6"/>
<outlet property="lblName" destination="nCb-cE-455" id="otv-Ef-bbR"/>
<outlet property="lblOtherNames" destination="KEZ-Tp-fcw" id="M8e-9a-ViT"/>
<outlet property="lblOtherNamesWord" destination="sIK-iD-Mhr" id="odB-6X-jqx"/>
</connections>
<point key="canvasLocation" x="522" y="337"/>
</collectionViewCell>
</objects>
<resources>
<image name="User Offline" width="9" height="9"/>
</resources>
</document>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6254" systemVersion="14A389" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
</dependencies>
<customFonts key="customFonts">
<mutableArray key="SourceSansPro-Bold.ttf">
<string>SourceSansPro-Bold</string>
<string>SourceSansPro-Bold</string>
</mutableArray>
<mutableArray key="SourceSansPro-Regular.ttf">
<string>SourceSansPro-Regular</string>
<string>SourceSansPro-Regular</string>
</mutableArray>
</customFonts>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="4vP-59-jbs" customClass="RBSearchUserCell">
<rect key="frame" x="0.0" y="0.0" width="300" height="200"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="300" height="200"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="607-2L-bgL" customClass="RobloxImageView">
<rect key="frame" x="8" y="8" width="90" height="90"/>
</imageView>
<textView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" text="ONE_NAME, ANOTHER_NAME" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VJf-Nr-fgH">
<rect key="frame" x="106" y="71" width="186" height="27"/>
<fontDescription key="fontDescription" name="SourceSansPro-Regular" family="Source Sans Pro" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_PREVIOUS_NAMES_" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ttY-br-z2t">
<rect key="frame" x="106" y="51" width="186" height="20"/>
<fontDescription key="fontDescription" name="SourceSansPro-Bold" family="Source Sans Pro" pointSize="16"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<textView hidden="YES" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" showsHorizontalScrollIndicator="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H8D-GF-JaB">
<rect key="frame" x="8" y="106" width="284" height="86"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<fontDescription key="fontDescription" name="SourceSansPro-Regular" family="Source Sans Pro" pointSize="16"/>
<textInputTraits key="textInputTraits"/>
</textView>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="User Offline" translatesAutoresizingMaskIntoConstraints="NO" id="IYi-9n-GNb">
<rect key="frame" x="104" y="25" width="9" height="9"/>
</imageView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="_TWENTY_CHARACTERS_" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nju-fQ-xks">
<rect key="frame" x="118" y="19" width="182" height="21"/>
<fontDescription key="fontDescription" name="SourceSansPro-Bold" family="Source Sans Pro" pointSize="16"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<size key="customSize" width="365" height="206"/>
<connections>
<outlet property="imgAvatar" destination="607-2L-bgL" id="i87-f7-fg5"/>
<outlet property="imgIsOnline" destination="IYi-9n-GNb" id="nMU-EZ-TkB"/>
<outlet property="lblBlurb" destination="H8D-GF-JaB" id="Ald-p1-5n3"/>
<outlet property="lblName" destination="Nju-fQ-xks" id="TXR-oH-ahZ"/>
<outlet property="lblOtherNames" destination="VJf-Nr-fgH" id="3ME-2M-zXK"/>
<outlet property="lblOtherNamesWord" destination="ttY-br-z2t" id="tNL-H5-j9W"/>
</connections>
<point key="canvasLocation" x="447" y="363"/>
</collectionViewCell>
</objects>
<resources>
<image name="User Offline" width="9" height="9"/>
</resources>
</document>
@@ -0,0 +1,16 @@
//
// RBTabBarController.h
// RobloxMobile
//
// Created by Ashish Jain on 12/8/15.
// Copyright © 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "RBXEventReporter.h"
@interface RBTabBarController : UITabBarController
-(RBXAnalyticsCustomData) getCurrentTabContext;
@end
@@ -0,0 +1,232 @@
//
// RBTabBarController.m
// RobloxMobile
//
// Created by Ashish Jain on 12/8/15.
// Copyright © 2015 ROBLOX. All rights reserved.
//
#import "RBTabBarController.h"
#import "ABTestManager.h"
#import "Flurry.h"
#import "LoginManager.h"
#import "RBMobileWebViewController.h"
#import "RBMoreViewController.h"
#import "RBXFunctions.h"
#import "RobloxAlert.h"
#import "RobloxInfo.h"
#import "RobloxNotifications.h"
#import "SignUpScreenController.h"
#import "UserInfo.h"
//---METRICS---
#define HSC_signUpControllerFromPlayNow @"HOME SCREEN - Sign Up Pressed While Guest"
@interface RBTabBarController () <UITabBarControllerDelegate>
@property (nonatomic) NSInteger selectedIndexLogin;
@end
@implementation RBTabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: [RobloxInfo getUserAgentString], @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
//check if the user is logged in, if not make the Games Page the landing page
if ([UserInfo CurrentPlayer].userLoggedIn == NO)
[self setSelectedIndex:1];
//set the images
if ([RobloxInfo thisDeviceIsATablet])
{
[((UIViewController*)self.viewControllers[0]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Home Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[0]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Home On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[1]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Game Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[1]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Game On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[2]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Catalog Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[2]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Catalog On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[3]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Friends Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[3]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Friends On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[4]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Messages Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[4]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Messages On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[5]).tabBarItem setImage:[[UIImage imageNamed:@"Icon More Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[5]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon More On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
}
else
{
[((UIViewController*)self.viewControllers[0]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Home Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[0]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Home On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[1]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Game Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[1]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Game On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[2]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Friends Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[2]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Friends On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[3]).tabBarItem setImage:[[UIImage imageNamed:@"Icon Messages Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[3]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon Messages On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[4]).tabBarItem setImage:[[UIImage imageNamed:@"Icon More Off"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
[((UIViewController*)self.viewControllers[4]).tabBarItem setSelectedImage:[[UIImage imageNamed:@"Icon More On"] imageWithRenderingMode:[RobloxTheme iconColoringMode]]];
}
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gotLoginSuccessfulNotification:)
name:RBX_NOTIFY_LOGIN_SUCCEEDED
object:nil ];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(gotLogoutNotification:)
name:RBX_NOTIFY_LOGGED_OUT
object:nil ];
}
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(RBXAnalyticsCustomData) getCurrentTabContext
{
RBXAnalyticsCustomData currentTab = RBXACustomTabGames;
if ([RobloxInfo thisDeviceIsATablet])
{
switch (self.selectedIndex)
{
case 0 : { currentTab = RBXACustomTabHome; } break;
case 1 : { currentTab = RBXACustomTabGames; } break;
case 2 : { currentTab = RBXACustomTabCatalog; } break;
case 3 : { currentTab = RBXACustomTabFriends; } break;
case 4 : { currentTab = RBXACustomTabMessages; } break;
case 5 : {
currentTab = RBXACustomTabMore;
if ([self.viewControllers[5] respondsToSelector:@selector(getMostRecentTab)])
currentTab = [((RBMoreViewController*)self.viewControllers[5]) getMostRecentTab];
} break;
}
}
else
{
switch (self.selectedIndex)
{
case 0 : { currentTab = RBXACustomTabHome; } break;
case 1 : { currentTab = RBXACustomTabGames; } break;
case 2 : { currentTab = RBXACustomTabFriends; } break;
case 3 : { currentTab = RBXACustomTabMessages; } break;
case 4 : {
currentTab = RBXACustomTabMore;
if ([self.viewControllers[4] respondsToSelector:@selector(getMostRecentTab)])
currentTab = [((RBMoreViewController*)self.viewControllers[4]) getMostRecentTab];
} break;
}
}
return currentTab;
}
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
NSUInteger indexOfTab = [self.viewControllers indexOfObject:viewController];
if (self.selectedIndex == indexOfTab && self.viewControllers.count > 1)
{
//drill back to the original screen
UINavigationController* tappedVC = (UINavigationController*)viewController;
UIViewController* rootVC = tappedVC.viewControllers[0];
[tappedVC popToViewController:rootVC animated:YES];
//if it is a webview, reload the original url
if ([rootVC isKindOfClass:[RBMobileWebViewController class]])
{
[(RBMobileWebViewController*)rootVC reloadWebPage];
}
else if ([viewController isEqual:[[self viewControllers] objectAtIndex:1]] && [RobloxInfo thisDeviceIsATablet])
{
//if we are on the games page, reload the games
if ([rootVC respondsToSelector:@selector(loadGames)])
[rootVC performSelector:@selector(loadGames)];
}
//return NO;
}
UserInfo* userInfo = [UserInfo CurrentPlayer];
if(!userInfo.userLoggedIn && ![viewController isEqual:[[self viewControllers] objectAtIndex:1]])
{
NSString* controllerName;
if ([[LoginManager sharedInstance] isFacebookEnabled])
controllerName = @"SignUpScreenControllerWithSocial";
else if ([LoginManager apiProxyEnabled])
controllerName = @"SignUpAPIScreenController";
else
controllerName = @"SignUpScreenController";
//ask the user to sign up or log in to see these pages
[Flurry logEvent:HSC_signUpControllerFromPlayNow];
NSString* storyboardName = [RobloxInfo getStoryboardName];
UIStoryboard* storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
SignUpScreenController* controller = (SignUpScreenController*)[storyboard instantiateViewControllerWithIdentifier:controllerName];
controller.modalPresentationStyle = UIModalPresentationFormSheet;
[self.navigationController presentViewController:controller animated:YES completion:nil];
self.selectedIndexLogin = [self.viewControllers indexOfObject:viewController];
return NO;
}
return YES;
}
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
[[RBXEventReporter sharedInstance] reportTabButtonClick:[self getCurrentTabContext]];
}
-(void) gotLoginSuccessfulNotification:(NSNotification*) notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[self setSelectedIndex:self.selectedIndexLogin];
});
}
-(void) gotLogoutNotification:(NSNotification*) notification
{
if ([[ABTestManager sharedInstance] IsInTestMobileGuestMode])
{
dispatch_async(dispatch_get_main_queue(), ^
{
[self setSelectedIndex:1];
});
}
else
{
// Let's look inside the notification and see if there's a treat
if ([notification.object isKindOfClass:[NSDictionary class]]) {
id obj = [(NSDictionary *)notification.object objectForKey:@"object"];
if ([obj isKindOfClass:[NSError class]]) {
// Oh that's too bad, its not a treat.
NSError *error = (NSError *)obj;
if ([error.domain.lowercaseString isEqualToString:@"httperror".lowercaseString] && error.code >= 400) {
[RBXFunctions dispatchOnMainThread:^{
if (self.selectedIndex != 1) {
[self setSelectedIndex:1];
}
}];
}
}
}
}
}
@end
@@ -0,0 +1,41 @@
//
// RBValidTextField.h
// RobloxMobile
//
// Created by Kyler Mulherin on 7/9/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface RBValidTextField : UIView <UITextFieldDelegate>
@property (nonatomic, getter=getText, setter=setText:) NSString* text;
@property (nonatomic, getter=getTitle, setter=setTitle:) NSString* titleText;
@property (nonatomic, getter=getHint, setter=setHint:) NSString* hintText;
@property (nonatomic, getter=getError, setter=setError:) NSString* errorMessage;
//mutators
-(void) setProtectedTextEntry:(bool)isProtected;
-(void) setKeyboardType:(UIKeyboardType)type;
-(void) setValidationBlock:(void (^)())block;
-(void) setExitOnEnterBlock:(void (^)())block;
-(void) setNextResponder:(id)aResponder;
-(void) forceUpdate;
-(void) markAsValid;
-(void) markAsNormal;
-(void) markAsInvalid;
-(void) showError:(NSString*)errorMessage;
-(void) hideError;
//accessors
-(bool) isValidated;
-(bool) isEditing;
//odd functions
-(BOOL) becomeFirstResponder;
-(BOOL) resignFirstResponder;
@end
@@ -0,0 +1,341 @@
//
// RBValidTextField.m
// RobloxMobile
//
// Created by Kyler Mulherin on 7/9/15.
// Copyright (c) 2015 ROBLOX. All rights reserved.
//
#import "RBValidTextField.h"
#import "RobloxTheme.h"
#import "UIView+Position.h"
#import <QuartzCore/QuartzCore.h>
@implementation RBValidTextField
{
//UI elements
UITextField* _txtInput;
UILabel* _lblError;
UILabel* _lblTitle;
UILabel* _lblHelper;
//private variables
bool _isValidated;
bool _shouldRestrictCharacters;
void (^validationBlock)();
void (^exitOnEnterBlock)();
id _nextResponder;
CGSize _helperSize;
CGSize _inputSize;
}
//Initializization and view layout stuff
-(void) initialize {
validationBlock = nil;
exitOnEnterBlock = nil;
_isValidated = false;
_nextResponder = nil;
_shouldRestrictCharacters = YES;
[self setClipsToBounds:NO];
_txtInput = [[UITextField alloc] init];
[RobloxTheme applyToModalLoginTextField:_txtInput];
[_txtInput.layer setBorderWidth:1];
[_txtInput.layer setBorderColor:[RobloxTheme colorGray4].CGColor];
[_txtInput.layer setCornerRadius:5.0];
[_txtInput setDelegate:self];
[_txtInput setFont:[RobloxTheme fontBody]];
[_txtInput setKeyboardType:UIKeyboardTypeDefault];
[_txtInput setText:@""];
[_txtInput setTextAlignment:NSTextAlignmentLeft];
[_txtInput setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
[_txtInput setTextColor:[RobloxTheme colorGray1]];
[_txtInput setLeftViewMode:UITextFieldViewModeAlways];
[_txtInput setOpaque:YES];
[_txtInput setAutocapitalizationType:UITextAutocapitalizationTypeNone];
[_txtInput setAutocorrectionType:UITextAutocorrectionTypeNo];
[_txtInput setReturnKeyType:UIReturnKeyDone];
[_txtInput setClearButtonMode:UITextFieldViewModeWhileEditing];
_lblTitle = [[UILabel alloc] init];
[_lblTitle setText:@""];
[_lblTitle setOpaque:YES];
[RobloxTheme applyToModalLoginTitleLabel:_lblTitle];
//_lblHint = [[UILabel alloc] init];
//[_lblHint setText:@""];
//[_lblHint setOpaque:YES];
//[_lblHint setLineBreakMode:NSLineBreakByWordWrapping];
//[_lblHint setNumberOfLines:2];
//[RobloxTheme applyToModalLoginHintLabel:_lblHint];
_lblHelper = [[UILabel alloc] init];
[_lblHelper setText:@""];
[_lblHelper setOpaque:YES];
[_lblHelper setHidden:YES];
[RobloxTheme applyToModalLoginHintLabel:_lblHelper];
[_lblHelper setFont:[RobloxTheme fontBodySmall]];
//[_lblHelper setLineBreakMode:NSLineBreakByTruncatingTail];
_lblError = [[UILabel alloc] init];
[_lblError setFont:[RobloxTheme fontBodySmall]];
[_lblError setTextColor:[RobloxTheme colorRed1]];
[_lblError setTextAlignment:NSTextAlignmentLeft];
[_lblError setText:@""];
[_lblError setOpaque:YES];
[_lblError setHidden:YES];
[_lblError setNumberOfLines:1];
[_lblError setAdjustsFontSizeToFitWidth:YES];
//add the objects to the view
[self addSubview:_txtInput];
[self addSubview:_lblTitle];
//[self addSubview:_lblHint];
[self addSubview:_lblError];
[self addSubview:_lblHelper];
[_txtInput setFrame:CGRectZero];
[_lblHelper setFrame:CGRectZero];
[_lblError setFrame:CGRectZero];
}
-(void) layoutSubviews {
//layout the views
int margin = 12; //self.width * 0.05;
_helperSize= CGSizeMake(self.width, 14);
_inputSize = CGSizeMake(self.width, self.height);
[_txtInput setFrame:CGRectMake(0, _helperSize.height, _inputSize.width, _inputSize.height)];
[_lblHelper setFrame:CGRectMake(0, self.height, _helperSize.width, _helperSize.height)];
[_lblError setFrame:CGRectMake(0, self.height, _helperSize.width, _helperSize.height)];
[_txtInput setLeftView:[[UIView alloc] initWithFrame:CGRectMake(0, 0, margin, _txtInput.height)]];
if (_lblError.hidden)
{
if (_lblHelper.hidden)
[_txtInput setY:0];
else
[_txtInput setY:_lblHelper.bottom];
}
if (_lblTitle.text.length > 0)
{
//int margin = self.width * 0.05;
CGSize textSize = [_lblTitle.text sizeWithAttributes:@{NSFontAttributeName:_lblTitle.font}];
[_lblTitle setFrame:CGRectMake(margin, _txtInput.y, textSize.width, _txtInput.height)];
//[_lblHint setFrame:CGRectMake(_lblTitle.right + margin, _txtInput.y, MAX(0, _txtInput.width - (_lblTitle.right + margin + margin)), _txtInput.height)];
}
//if (_lblHelper.text.length > 0) //(_lblHint.text.length > 0)
//{
// CGSize textSize = [_lblHelper.text sizeWithAttributes:@{NSFontAttributeName:_lblHelper.font}];
// if (textSize.width >= _lblHelper.width)
// [_lblHelper setText:_lblHint.text];
//}
}
-(id) init {
self = [super init];
if (self)
[self initialize];
return self;
}
-(id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
[self initialize];
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self)
[self initialize];
return self;
}
//Mutators
-(void) setText:(NSString *)text { _txtInput.text = text; [self forceUpdate]; }
-(void) setTitle:(NSString*)titleText {
[_lblTitle setText:titleText];
//[_lblHelper setText:titleText];
int margin = self.width * 0.05;
CGSize textSize = [_lblTitle.text sizeWithAttributes:@{NSFontAttributeName:_lblTitle.font}];
[_lblTitle setFrame:CGRectMake(margin, _txtInput.y, textSize.width, _txtInput.height)];
//[_lblHint setFrame:CGRectMake(_lblTitle.right + margin, _txtInput.y, MAX(0, (_txtInput.width - _lblTitle.width) - margin), _txtInput.height)];
}
-(void) setHint:(NSString*)hintText {
//[_lblHint setText:hintText];
//[_lblHelper setText:[NSString stringWithFormat:@"%@ (%@)", _lblHelper.text, hintText]];
[_lblHelper setText:hintText];
}
-(void) setError:(NSString*)errorMessage { [_txtInput setText:errorMessage]; }
-(void) setNextResponder:(id)aResponder {
_nextResponder = aResponder;
[_txtInput setReturnKeyType: (_nextResponder != nil) ? UIReturnKeyNext : UIReturnKeyDone];
}
-(void) setValidationBlock:(void(^)())block { validationBlock = block; }
-(void) setExitOnEnterBlock:(void (^)())block { exitOnEnterBlock = block; }
-(void) forceUpdate {
if (!self)
return;
if (_txtInput.text && _txtInput.text.length > 0)
{
//the user has entered text, so keep the helpers hidden
_lblTitle.hidden = YES;
//_lblHint.hidden = YES;
}
else
{
//the user has not entered any text yet, show the helper hints again
_lblTitle.hidden = NO;
//_lblHint.hidden = NO;
}
_lblHelper.hidden = YES;
[_txtInput setY:0];
if (_lblError.hidden)
{
//everything is normal
[self markAsNormal];
}
}
-(void) hideError {
dispatch_async(dispatch_get_main_queue(), ^
{
[_txtInput setY:0];
_lblError.hidden = YES;
});
}
-(void) showError:(NSString*)errorMessage {
if (errorMessage && errorMessage.length > 0)
{
dispatch_async(dispatch_get_main_queue(), ^
{
//dynamically size the labels according to the size of the frame provided
[_txtInput setY:0];
_lblError.text = errorMessage;
_lblError.hidden = NO;
_lblHelper.hidden = YES;
});
}
else
{
[self hideError];
}
}
-(void) setProtectedTextEntry:(bool)isProtected { _txtInput.secureTextEntry = isProtected; }
-(void) setKeyboardType:(UIKeyboardType)type { [_txtInput setKeyboardType:type]; }
//color changing
-(void) changeBorderColor:(UIColor*)color { dispatch_async(dispatch_get_main_queue(), ^ { _txtInput.layer.borderColor=[color CGColor]; }); }
-(void) markAsValid { [self changeBorderColor:[RobloxTheme colorGreen1]]; _isValidated = YES; [self hideError];}
-(void) markAsNormal { [self changeBorderColor:[RobloxTheme colorGray4]]; }
-(void) markAsInvalid { [self changeBorderColor:[RobloxTheme colorRed1]]; _isValidated = NO; }
//Accessors
-(NSString*) getText { return _txtInput.text; }
-(NSString*) getError { return _lblError.text; }
-(bool) isValidated { return _isValidated; }
-(bool) isEditing { return _txtInput.isEditing; }
//Delegate functions
-(void) textFieldDidBeginEditing:(UITextField *)textField {
if (textField == _txtInput)
{
_lblTitle.hidden = YES;
//_lblHint.hidden = YES;
//only reveal the helper text if the error isn't visible
if (_lblError.hidden)
{
_lblHelper.hidden = NO;
//[_txtInput setY:_lblHelper.bottom];
}
else
{
_lblHelper.hidden = YES;
}
}
}
-(void) textFieldDidEndEditing:(UITextField *)textField {
if (textField == _txtInput)
{
_isValidated = NO;
_lblHelper.hidden = YES;
_lblError.hidden = YES;
//[_txtInput setY:0];
[self markAsNormal];
if (textField.text.length > 0)
{
//the user has entered text, so keep the helpers hidden
_lblTitle.hidden = YES;
//_lblHint.hidden = YES;
if (validationBlock)
validationBlock();
}
else
{
//the user has not entered any text yet, show the helper hints again
_lblTitle.hidden = NO;
//_lblHint.hidden = NO;
}
}
}
-(BOOL) textFieldShouldReturn:(UITextField *)textField {
if (_nextResponder)
[_nextResponder becomeFirstResponder];
else
[self.superview endEditing:YES]; //[self resignFirstResponder]; //<-- THIS LINE IS SUSPECT
if (exitOnEnterBlock)
exitOnEnterBlock();
return (_nextResponder == nil);
}
//Misc functions
-(BOOL) becomeFirstResponder {
[super becomeFirstResponder];
dispatch_async(dispatch_get_main_queue(), ^{
[self textFieldDidBeginEditing:_txtInput];
});
return [_txtInput becomeFirstResponder];
}
-(BOOL) resignFirstResponder {
[super resignFirstResponder];
dispatch_async(dispatch_get_main_queue(), ^{
[self textFieldDidEndEditing:_txtInput];
_lblHelper.hidden = YES;
_lblError.hidden = YES;
});
return [_txtInput resignFirstResponder];
}
@end

Some files were not shown because too many files have changed in this diff Show More