diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index bd01840..0e8642c 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 2206A0A2172CDEC3004A4006 /* SVHTTPRequest.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 2206A0A1172CDEC3004A4006 /* SVHTTPRequest.podspec */; }; + 2206A0A3172CDEC3004A4006 /* SVHTTPRequest.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 2206A0A1172CDEC3004A4006 /* SVHTTPRequest.podspec */; }; 220DF2EE150A7B6800E52072 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 220DF2ED150A7B6800E52072 /* Cocoa.framework */; }; 220DF2F8150A7B6800E52072 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 220DF2F6150A7B6800E52072 /* InfoPlist.strings */; }; 220DF2FA150A7B6800E52072 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 220DF2F9150A7B6800E52072 /* main.m */; }; @@ -31,6 +33,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 2206A0A1172CDEC3004A4006 /* SVHTTPRequest.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = SVHTTPRequest.podspec; path = ../SVHTTPRequest.podspec; sourceTree = ""; }; 220DF2EB150A7B6800E52072 /* MacDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 220DF2ED150A7B6800E52072 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; 220DF2F0150A7B6800E52072 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -63,7 +66,7 @@ 225389B114297AFE00856491 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/DemoViewController.xib; sourceTree = ""; }; 225389BE14297B0B00856491 /* SVHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SVHTTPRequest.h; sourceTree = ""; }; 225389BF14297B0B00856491 /* SVHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SVHTTPRequest.m; sourceTree = ""; }; - 22A748821630AA83004893A8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 22A748821630AA83004893A8 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -124,8 +127,8 @@ 2253898914297AFD00856491 = { isa = PBXGroup; children = ( - 22A748821630AA83004893A8 /* Default-568h@2x.png */, 220FDF34149AC4880021F43D /* README.md */, + 2206A0A1172CDEC3004A4006 /* SVHTTPRequest.podspec */, 2253899E14297AFE00856491 /* Demo */, 225389B814297B0B00856491 /* SVHTTPRequest */, 220DF2F3150A7B6800E52072 /* MacDemo */, @@ -172,6 +175,7 @@ 2253899F14297AFE00856491 /* Supporting Files */ = { isa = PBXGroup; children = ( + 22A748821630AA83004893A8 /* Default-568h@2x.png */, 225389A014297AFE00856491 /* Demo-Info.plist */, 225389A114297AFE00856491 /* InfoPlist.strings */, 225389A414297AFE00856491 /* main.m */, @@ -263,6 +267,7 @@ 220DF2F8150A7B6800E52072 /* InfoPlist.strings in Resources */, 220DF2FE150A7B6800E52072 /* Credits.rtf in Resources */, 220DF304150A7B6800E52072 /* MainMenu.xib in Resources */, + 2206A0A3172CDEC3004A4006 /* SVHTTPRequest.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -275,6 +280,7 @@ 225389B214297AFE00856491 /* DemoViewController.xib in Resources */, 220FDF35149AC4880021F43D /* README.md in Resources */, 22A748831630AA83004893A8 /* Default-568h@2x.png in Resources */, + 2206A0A2172CDEC3004A4006 /* SVHTTPRequest.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Demo/Demo/DemoViewController.m b/Demo/Demo/DemoViewController.m index 51fcb11..c648115 100644 --- a/Demo/Demo/DemoViewController.m +++ b/Demo/Demo/DemoViewController.m @@ -12,7 +12,6 @@ @implementation DemoViewController - (IBAction)watchersRequest { - watchersLabel.text = nil; [SVHTTPRequest GET:@"https://api.github.com/repos/samvermette/SVHTTPRequest" @@ -23,12 +22,9 @@ - (IBAction)watchersRequest { } - (IBAction)twitterRequest { - twitterImageView.image = nil; - followersLabel.text = nil; [[SVHTTPClient sharedClient] setBasePath:@"http://api.twitter.com/1/"]; - [[SVHTTPClient sharedClient] GET:@"users/profile_image" parameters:[NSDictionary dictionaryWithObjectsAndKeys: @"samvermette", @"screen_name", @@ -37,16 +33,9 @@ - (IBAction)twitterRequest { completion:^(id response, NSHTTPURLResponse *urlResponse, NSError *error) { twitterImageView.image = [UIImage imageWithData:response]; }]; - - [[SVHTTPClient sharedClient] GET:@"users/show.json" - parameters:[NSDictionary dictionaryWithObject:@"samvermette" forKey:@"screen_name"] - completion:^(id response, NSHTTPURLResponse *urlResponse, NSError *error) { - followersLabel.text = [NSString stringWithFormat:@"@samvermette has %@ followers", [response valueForKey:@"followers_count"]]; - }]; } - (IBAction)progressRequest { - progressLabel.text = nil; [SVHTTPRequest GET:@"http://sanjosetransit.com/extras/SJTransit_Icons.zip" diff --git a/Demo/Demo/en.lproj/DemoViewController.xib b/Demo/Demo/en.lproj/DemoViewController.xib index b7ba3e1..3cf5578 100644 --- a/Demo/Demo/en.lproj/DemoViewController.xib +++ b/Demo/Demo/en.lproj/DemoViewController.xib @@ -1,22 +1,22 @@ - 1280 - 11D50b - 2182 - 1138.32 - 568.00 + 1552 + 12D78 + 3084 + 1187.37 + 626.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1179 + 2083 YES + IBProxyObject IBUIButton IBUIImageView - IBUIView IBUILabel - IBProxyObject + IBUIView YES @@ -44,8 +44,9 @@ 292 - {{73, 18}, {171, 44}} + {{77, 48}, {164, 44}} + _NS:222 NO @@ -53,7 +54,7 @@ 0 0 1 - Github GET Request + GET JSON Request 3 MQA @@ -81,8 +82,9 @@ 292 - {{23, 70}, {269, 39}} + {{23, 90}, {269, 39}} + _NS:129 NO @@ -94,6 +96,7 @@ 1 MCAwIDAAA + darkTextColor 1 @@ -112,8 +115,9 @@ 292 - {{71, 124}, {173, 44}} + {{71, 164}, {173, 44}} + _NS:222 NO @@ -121,7 +125,7 @@ 0 0 1 - Twitter GET Request + GET Image Request 1 @@ -134,39 +138,20 @@ 292 - {{109, 192}, {96, 96}} + {{109, 222}, {96, 96}} - + + _NS:363 NO IBCocoaTouchFramework - - - 292 - {{23, 297}, {269, 39}} - - - _NS:129 - NO - YES - 7 - NO - IBCocoaTouchFramework - - - - 1 - 10 - 1 - - - 292 - {{60, 357}, {195, 44}} + {{60, 367}, {195, 44}} + _NS:225 NO @@ -190,8 +175,9 @@ 292 - {{23, 410}, {269, 39}} + {{23, 408}, {269, 39}} + _NS:328 NO YES @@ -210,6 +196,7 @@ {{0, 20}, {320, 460}} + NO @@ -244,14 +231,6 @@ 20 - - - followersLabel - - - - 24 - progressLabel @@ -319,7 +298,6 @@ - @@ -345,11 +323,6 @@ - - 21 - - - 25 @@ -373,7 +346,6 @@ 12.IBPluginDependency 15.IBPluginDependency 18.IBPluginDependency - 21.IBPluginDependency 25.IBPluginDependency 26.IBPluginDependency 6.IBPluginDependency @@ -392,7 +364,6 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -409,7 +380,104 @@ 28 - + + + YES + + DemoViewController + UIViewController + + YES + + YES + progressRequest + twitterRequest + watchersRequest + + + YES + id + id + id + + + + YES + + YES + progressRequest + twitterRequest + watchersRequest + + + YES + + progressRequest + id + + + twitterRequest + id + + + watchersRequest + id + + + + + YES + + YES + followersLabel + progressLabel + twitterImageView + watchersLabel + + + YES + UILabel + UILabel + UIImageView + UILabel + + + + YES + + YES + followersLabel + progressLabel + twitterImageView + watchersLabel + + + YES + + followersLabel + UILabel + + + progressLabel + UILabel + + + twitterImageView + UIImageView + + + watchersLabel + UILabel + + + + + IBProjectSource + ./Classes/DemoViewController.h + + + + 0 IBCocoaTouchFramework @@ -418,6 +486,6 @@ YES 3 - 1179 + 2083 diff --git a/README.md b/README.md index 7fb15e4..f67a51a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ -_**Important note if your project doesn't use ARC**: you must add the `-fobjc-arc` compiler flag to `SVHTTPRequest.m` in Target Settings > Build Phases > Compile Sources._ - # SVHTTPRequest -SVHTTPRequest is a simple and extremely straightforward way to communicate with RESTful web APIs for iOS and Mac. It's a simpler and cleaner alternative to bulky [ASIHTTPRequest](https://github.com/pokeb/asi-http-request/tree), [AFNetworking](https://github.com/AFNetworking/AFNetworking) and [RESTKit](https://github.com/RestKit/RestKit). It is blocked-based, uses `NSURLConnection`, ARC, as well as `NSJSONSerialization` to automatically parse JSON responses (making it only compatible with iOS 5 and Mac OS X Lion). +SVHTTPRequest lets you easily interact with RESTful (GET, POST, DELETE, PUT) web APIs. It is blocked-based, uses `NSURLConnection`, ARC, as well as `NSJSONSerialization` to automatically parse JSON responses. **SVHTTPRequest features:** -* straightforward singleton convenience methods for making `GET`, `POST`, `PUT`, `DELETE`, `HEAD` and download requests. +* class methods for quickly making `GET`, `POST`, `PUT`, `DELETE`, `HEAD` and download requests. * completion block handler returning `response` (`NSObject` if JSON, otherwise `NSData`), `NSHTTPURLResponse` and `NSError` objects. * persistent `basePath` and basic authentication signing when using `SVHTTPClient`. * support for `multipart/form-data` parameters in POST and PUT requests. @@ -14,6 +12,14 @@ SVHTTPRequest is a simple and extremely straightforward way to communicate with ## Installation +### From CocoaPods + +Add `pod 'SVHTTPRequest'` to your Podfile or `pod 'SVHTTPRequest', :head` if you're feeling adventurous. + +### Manually + +_**If your project doesn't use ARC**: you must add the `-fobjc-arc` compiler flag to `SVHTTPRequest.m` and `SVHTTPClient.m` in Target Settings > Build Phases > Compile Sources._ + * Drag the `SVHTTPRequest/SVHTTPRequest` folder into your project. * `#import "SVHTTPRequest.h"` (this will import `SVHTTPClient` as well) diff --git a/SVHTTPRequest.podspec b/SVHTTPRequest.podspec index d5beaf2..38b0e2e 100644 --- a/SVHTTPRequest.podspec +++ b/SVHTTPRequest.podspec @@ -1,15 +1,12 @@ Pod::Spec.new do |s| - s.name = 'SVHTTPRequest' - s.version = '0.4' - s.license = 'MIT' - s.summary = 'Simple REST client for iOS and Mac.' - s.homepage = 'http://samvermette.com/310' - s.author = { 'Sam Vermette' => 'hello@samvermette.com' } - s.source = { :git => 'https://github.com/samvermette/SVHTTPRequest.git', :tag => s.version.to_s } - - s.description = 'SVHTTPRequest is a simple and extremely straightforward way to communicate with RESTful web APIs for iOS and Mac. It is blocked-based, uses NSURLConnection, ARC, as well as NSJSONSerialization to automatically parse JSON responses (making it only compatible with iOS 5+ and OS X 10.7+).' - - s.source_files = 'SVHTTPRequest/*.{h,m}' - s.preserve_paths = 'Demo' - s.requires_arc = true + s.name = 'SVHTTPRequest' + s.version = '0.5.1' + s.license = 'MIT' + s.summary = 'Simple REST client for iOS and Mac.' + s.homepage = 'http://samvermette.com/310' + s.author = { 'Sam Vermette' => 'hello@samvermette.com' } + s.source = { :git => 'https://github.com/samvermette/SVHTTPRequest.git', :tag => s.version.to_s } + s.description = 'SVHTTPRequest lets you easily interact with RESTful (GET, POST, DELETE, PUT) web APIs. It is blocked-based, uses NSURLConnection, ARC, as well as NSJSONSerialization to automatically parse JSON responses.' + s.source_files = 'SVHTTPRequest/*.{h,m}' + s.requires_arc = true end diff --git a/SVHTTPRequest/SVHTTPClient.h b/SVHTTPRequest/SVHTTPClient.h index 093bfad..5c94860 100644 --- a/SVHTTPRequest/SVHTTPClient.h +++ b/SVHTTPRequest/SVHTTPClient.h @@ -15,27 +15,26 @@ typedef void (^SVHTTPRequestCompletionHandler)(id response, NSHTTPURLResponse *u @interface SVHTTPClient : NSObject -+ (SVHTTPClient*)sharedClient; -+ (SVHTTPClient*)sharedClientWithIdentifier:(NSString*)identifier; ++ (instancetype)sharedClient; ++ (instancetype)sharedClientWithIdentifier:(NSString*)identifier; - (void)setBasicAuthWithUsername:(NSString*)username password:(NSString*)password; +- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field; - (SVHTTPRequest*)GET:(NSString*)path parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; - (SVHTTPRequest*)GET:(NSString*)path parameters:(NSDictionary*)parameters saveToPath:(NSString*)savePath progress:(void (^)(float progress))progressBlock completion:(SVHTTPRequestCompletionHandler)completionBlock; -- (SVHTTPRequest*)POST:(NSString*)path parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; -- (SVHTTPRequest*)POST:(NSString*)path parameters:(NSDictionary*)parameters progress:(void (^)(float progress))progressBlock completion:(void (^)(id response, NSHTTPURLResponse *urlResponse, NSError *error))completionBlock; +- (SVHTTPRequest*)POST:(NSString*)path parameters:(NSObject*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; +- (SVHTTPRequest*)POST:(NSString*)path parameters:(NSObject*)parameters progress:(void (^)(float progress))progressBlock completion:(void (^)(id response, NSHTTPURLResponse *urlResponse, NSError *error))completionBlock; +- (SVHTTPRequest*)PUT:(NSString*)path parameters:(NSObject*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; -- (SVHTTPRequest*)PUT:(NSString*)path parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; - (SVHTTPRequest*)DELETE:(NSString*)path parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; - - (SVHTTPRequest*)HEAD:(NSString*)path parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; - (void)cancelRequestsWithPath:(NSString*)path; - (void)cancelAllRequests; +- (SVHTTPRequest*)queueRequest:(SVHTTPRequest*)requestOperation; -// header values common to all requests, e.g. API keys -- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field; @property (nonatomic, strong) NSDictionary *baseParameters; @property (nonatomic, strong) NSString *username; diff --git a/SVHTTPRequest/SVHTTPClient.m b/SVHTTPRequest/SVHTTPClient.m index 0997e1b..eb561af 100644 --- a/SVHTTPRequest/SVHTTPClient.m +++ b/SVHTTPRequest/SVHTTPClient.m @@ -28,15 +28,11 @@ - (SVHTTPRequest*)queueRequest:(NSString*)path @implementation SVHTTPClient -@synthesize username, password, basePath, baseParameters, userAgent, sendParametersAsJSON, cachePolicy, timeoutInterval; -@synthesize operationQueue, HTTPHeaderFields; - - -+ (id)sharedClient { ++ (instancetype)sharedClient { return [self sharedClientWithIdentifier:@"master"]; } -+ (id)sharedClientWithIdentifier:(NSString *)identifier { ++ (instancetype)sharedClientWithIdentifier:(NSString *)identifier { SVHTTPClient *sharedClient = [[self sharedClients] objectForKey:identifier]; if(!sharedClient) { @@ -68,17 +64,8 @@ - (id)init { - (void)setBasicAuthWithUsername:(NSString *)newUsername password:(NSString *)newPassword { - - if(username) - username = nil; - - if(password) - password = nil; - - if(newUsername && newPassword) { - username = newUsername; - password = newPassword; - } + self.username = newUsername; + self.password = newPassword; } #pragma mark - Request Methods @@ -128,10 +115,10 @@ - (void)cancelAllRequests { #pragma mark - - (NSMutableDictionary *)HTTPHeaderFields { - if(HTTPHeaderFields == nil) - HTTPHeaderFields = [NSMutableDictionary new]; + if(_HTTPHeaderFields == nil) + _HTTPHeaderFields = [NSMutableDictionary new]; - return HTTPHeaderFields; + return _HTTPHeaderFields; } - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field { @@ -146,26 +133,40 @@ - (SVHTTPRequest*)queueRequest:(NSString*)path completion:(SVHTTPRequestCompletionHandler)completionBlock { NSString *completeURLString = [NSString stringWithFormat:@"%@%@", self.basePath, path]; + id mergedParameters; - NSMutableDictionary *mergedParameters = [NSMutableDictionary dictionary]; - [mergedParameters addEntriesFromDictionary:parameters]; - [mergedParameters addEntriesFromDictionary:self.baseParameters]; + if((method == SVHTTPRequestMethodPOST || method == SVHTTPRequestMethodPUT) && self.sendParametersAsJSON && ![parameters isKindOfClass:[NSDictionary class]]) + mergedParameters = parameters; + else { + mergedParameters = [NSMutableDictionary dictionary]; + [mergedParameters addEntriesFromDictionary:parameters]; + [mergedParameters addEntriesFromDictionary:self.baseParameters]; + } - SVHTTPRequest *requestOperation = [(id)[SVHTTPRequest alloc] initWithAddress:completeURLString method:method parameters:mergedParameters saveToPath:savePath progress:progressBlock completion:completionBlock]; + SVHTTPRequest *requestOperation = [(id)[SVHTTPRequest alloc] initWithAddress:completeURLString + method:method + parameters:mergedParameters + saveToPath:savePath + progress:progressBlock + completion:completionBlock]; + return [self queueRequest:requestOperation]; +} + +- (SVHTTPRequest*)queueRequest:(SVHTTPRequest*)requestOperation { requestOperation.sendParametersAsJSON = self.sendParametersAsJSON; requestOperation.cachePolicy = self.cachePolicy; requestOperation.userAgent = self.userAgent; + requestOperation.timeoutInterval = self.timeoutInterval; [(id)requestOperation setClient:self]; [self.HTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { - [(id)requestOperation setValue:value forHTTPHeaderField:field]; + [requestOperation setValue:value forHTTPHeaderField:field]; }]; if(self.username && self.password) [(id)requestOperation signRequestWithUsername:self.username password:self.password]; - [(id)requestOperation setRequestPath:path]; [self.operationQueue addOperation:requestOperation]; return requestOperation; diff --git a/SVHTTPRequest/SVHTTPRequest.h b/SVHTTPRequest/SVHTTPRequest.h index 22df5f0..8403c52 100644 --- a/SVHTTPRequest/SVHTTPRequest.h +++ b/SVHTTPRequest/SVHTTPRequest.h @@ -19,7 +19,6 @@ enum { SVHTTPRequestMethodDELETE, SVHTTPRequestMethodHEAD }; - typedef NSUInteger SVHTTPRequestMethod; @interface SVHTTPRequest : NSOperation @@ -27,19 +26,21 @@ typedef NSUInteger SVHTTPRequestMethod; + (SVHTTPRequest*)GET:(NSString*)address parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)block; + (SVHTTPRequest*)GET:(NSString*)address parameters:(NSDictionary*)parameters saveToPath:(NSString*)savePath progress:(void (^)(float progress))progressBlock completion:(SVHTTPRequestCompletionHandler)completionBlock; -+ (SVHTTPRequest*)POST:(NSString*)address parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)block; -+ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSDictionary *)parameters progress:(void (^)(float))progressBlock completion:(void (^)(id, NSHTTPURLResponse*, NSError *))completionBlock; ++ (SVHTTPRequest*)POST:(NSString*)address parameters:(NSObject*)parameters completion:(SVHTTPRequestCompletionHandler)block; ++ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSObject *)parameters progress:(void (^)(float))progressBlock completion:(SVHTTPRequestCompletionHandler)completionBlock; ++ (SVHTTPRequest*)PUT:(NSString*)address parameters:(NSObject*)parameters completion:(SVHTTPRequestCompletionHandler)block; -+ (SVHTTPRequest*)PUT:(NSString*)address parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)block; + (SVHTTPRequest*)DELETE:(NSString*)address parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)block; - + (SVHTTPRequest*)HEAD:(NSString*)address parameters:(NSDictionary*)parameters completion:(SVHTTPRequestCompletionHandler)block; - (SVHTTPRequest*)initWithAddress:(NSString*)urlString method:(SVHTTPRequestMethod)method - parameters:(NSDictionary*)parameters + parameters:(NSObject*)parameters completion:(SVHTTPRequestCompletionHandler)completionBlock; +- (void)preprocessParameters; +- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field; + + (void)setDefaultTimeoutInterval:(NSTimeInterval)interval; + (void)setDefaultUserAgent:(NSString*)userAgent; @@ -47,6 +48,7 @@ typedef NSUInteger SVHTTPRequestMethod; @property (nonatomic, readwrite) BOOL sendParametersAsJSON; @property (nonatomic, readwrite) NSURLRequestCachePolicy cachePolicy; @property (nonatomic, readwrite) NSUInteger timeoutInterval; +@property (nonatomic, strong) NSMutableURLRequest *operationRequest; @end @@ -60,12 +62,11 @@ typedef NSUInteger SVHTTPRequestMethod; - (SVHTTPRequest*)initWithAddress:(NSString*)urlString method:(SVHTTPRequestMethod)method - parameters:(NSDictionary*)parameters + parameters:(NSObject*)parameters saveToPath:(NSString*)savePath progress:(void (^)(float))progressBlock completion:(SVHTTPRequestCompletionHandler)completionBlock; - (void)signRequestWithUsername:(NSString*)username password:(NSString*)password; -- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field; @end \ No newline at end of file diff --git a/SVHTTPRequest/SVHTTPRequest.m b/SVHTTPRequest/SVHTTPRequest.m index d78b6d0..7731675 100644 --- a/SVHTTPRequest/SVHTTPRequest.m +++ b/SVHTTPRequest/SVHTTPRequest.m @@ -8,9 +8,14 @@ // #import "SVHTTPRequest.h" +#import @interface NSData (Base64) - (NSString*)base64EncodingWithLineLength:(unsigned int)lineLength; +- (NSString *)getImageType; +- (BOOL)isJPG; +- (BOOL)isPNG; +- (BOOL)isGIF; @end @interface NSString (OAURLEncodingAdditions) @@ -25,17 +30,16 @@ - (NSString*)encodedURLParameterString; typedef NSUInteger SVHTTPRequestState; -static NSUInteger taskCount = 0; -static NSTimeInterval defaultTimeoutInterval = 20; +static NSInteger SVHTTPRequestTaskCount = 0; static NSString *defaultUserAgent; +static NSTimeInterval SVHTTPRequestTimeoutInterval = 20; @interface SVHTTPRequest () -@property (nonatomic, strong) NSMutableURLRequest *operationRequest; +@property (nonatomic, strong) NSDictionary *parameters; @property (nonatomic, strong) NSMutableData *operationData; @property (nonatomic, strong) NSFileHandle *operationFileHandle; @property (nonatomic, strong) NSURLConnection *operationConnection; -@property (nonatomic, strong) NSDictionary *operationParameters; @property (nonatomic, strong) NSHTTPURLResponse *operationURLResponse; @property (nonatomic, strong) NSString *operationSavePath; @property (nonatomic, assign) CFRunLoopRef operationRunLoop; @@ -44,7 +48,7 @@ @interface SVHTTPRequest () @property (nonatomic, readwrite) UIBackgroundTaskIdentifier backgroundTaskIdentifier; #endif -#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 +#if !OS_OBJECT_USE_OBJC @property (nonatomic, assign) dispatch_queue_t saveDataDispatchQueue; @property (nonatomic, assign) dispatch_group_t saveDataDispatchGroup; #else @@ -75,45 +79,44 @@ - (void)callCompletionBlockWithResponse:(id)response error:(NSError *)error; @implementation SVHTTPRequest -// public properties -@synthesize sendParametersAsJSON, cachePolicy, timeoutInterval; - -// private properties -@synthesize operationRequest, operationData, operationConnection, operationParameters, operationURLResponse, operationFileHandle, state; -@synthesize operationSavePath, operationCompletionBlock, operationProgressBlock, timeoutTimer; -@synthesize expectedContentLength, receivedContentLength, saveDataDispatchGroup, saveDataDispatchQueue; -@synthesize requestPath, userAgent, client; +@synthesize state = _state; - (void)dealloc { - [operationConnection cancel]; -#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000 - dispatch_release(saveDataDispatchGroup); - dispatch_release(saveDataDispatchQueue); + [_operationConnection cancel]; +#if !OS_OBJECT_USE_OBJC + dispatch_release(_saveDataDispatchGroup); + dispatch_release(_saveDataDispatchQueue); #endif } + (void)setDefaultTimeoutInterval:(NSTimeInterval)interval { - defaultTimeoutInterval = interval; + SVHTTPRequestTimeoutInterval = interval; } + (void)setDefaultUserAgent:(NSString *)userAgent { defaultUserAgent = userAgent; } -- (void)increaseTaskCount { - taskCount++; +- (NSUInteger)timeoutInterval { + if(_timeoutInterval == 0) + return SVHTTPRequestTimeoutInterval; + return _timeoutInterval; +} + +- (void)increaseSVHTTPRequestTaskCount { + SVHTTPRequestTaskCount++; [self toggleNetworkActivityIndicator]; } -- (void)decreaseTaskCount { - taskCount--; +- (void)decreaseSVHTTPRequestTaskCount { + SVHTTPRequestTaskCount = MAX(0, SVHTTPRequestTaskCount-1); [self toggleNetworkActivityIndicator]; } - (void)toggleNetworkActivityIndicator { -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE && !__has_feature(attribute_availability_app_extension) dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(taskCount > 0)]; + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(SVHTTPRequestTaskCount > 0)]; }); #endif } @@ -134,21 +137,21 @@ + (SVHTTPRequest*)GET:(NSString *)address parameters:(NSDictionary *)parameters return requestObject; } -+ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSDictionary *)parameters completion:(SVHTTPRequestCompletionHandler)block { ++ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSObject *)parameters completion:(SVHTTPRequestCompletionHandler)block { SVHTTPRequest *requestObject = [[self alloc] initWithAddress:address method:SVHTTPRequestMethodPOST parameters:parameters saveToPath:nil progress:nil completion:block]; [requestObject start]; return requestObject; } -+ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSDictionary *)parameters progress:(void (^)(float))progressBlock completion:(void (^)(id, NSHTTPURLResponse*, NSError *))completionBlock { ++ (SVHTTPRequest*)POST:(NSString *)address parameters:(NSObject *)parameters progress:(void (^)(float))progressBlock completion:(void (^)(id, NSHTTPURLResponse*, NSError *))completionBlock { SVHTTPRequest *requestObject = [[self alloc] initWithAddress:address method:SVHTTPRequestMethodPOST parameters:parameters saveToPath:nil progress:progressBlock completion:completionBlock]; [requestObject start]; return requestObject; } -+ (SVHTTPRequest*)PUT:(NSString *)address parameters:(NSDictionary *)parameters completion:(SVHTTPRequestCompletionHandler)block { ++ (SVHTTPRequest*)PUT:(NSString *)address parameters:(NSObject *)parameters completion:(SVHTTPRequestCompletionHandler)block { SVHTTPRequest *requestObject = [[self alloc] initWithAddress:address method:SVHTTPRequestMethodPUT parameters:parameters saveToPath:nil progress:nil completion:block]; [requestObject start]; @@ -180,13 +183,18 @@ - (SVHTTPRequest*)initWithAddress:(NSString*)urlString method:(SVHTTPRequestMeth self.operationCompletionBlock = completionBlock; self.operationProgressBlock = progressBlock; self.operationSavePath = savePath; - self.operationParameters = parameters; - self.timeoutInterval = defaultTimeoutInterval; self.saveDataDispatchGroup = dispatch_group_create(); self.saveDataDispatchQueue = dispatch_queue_create("com.samvermette.SVHTTPRequest", DISPATCH_QUEUE_SERIAL); - self.operationRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]]; + NSURL *url = [[NSURL alloc] initWithString:urlString]; + self.operationRequest = [[NSMutableURLRequest alloc] initWithURL:url]; + + NSString *path = url.path; + if ([path hasPrefix:@"/"]) { + path = [path substringFromIndex:1]; + } + [self setRequestPath:path]; // pipeline all but POST and downloads if(method != SVHTTPRequestMethodPOST && !savePath) @@ -202,31 +210,41 @@ - (SVHTTPRequest*)initWithAddress:(NSString*)urlString method:(SVHTTPRequestMeth [self.operationRequest setHTTPMethod:@"DELETE"]; else if(method == SVHTTPRequestMethodHEAD) [self.operationRequest setHTTPMethod:@"HEAD"]; + self.state = SVHTTPRequestStateReady; + self.parameters = parameters; + return self; } +- (void)preprocessParameters { + if(self.parameters) + [self addParametersToRequest:self.parameters]; + self.parameters = nil; +} -- (void)addParametersToRequest:(NSDictionary*)paramsDict { +- (void)addParametersToRequest:(NSObject*)parameters { NSString *method = self.operationRequest.HTTPMethod; if([method isEqualToString:@"POST"] || [method isEqualToString:@"PUT"]) { if(self.sendParametersAsJSON) { - [self.operationRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - NSError *jsonError; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:paramsDict options:0 error:&jsonError]; - - if(jsonData && jsonError) - [NSException raise:NSInvalidArgumentException format:@"Request parameters couldn't be serialized into JSON."]; - - [self.operationRequest setHTTPBody:jsonData]; - } else { + if([parameters isKindOfClass:[NSArray class]] || [parameters isKindOfClass:[NSDictionary class]]) { + [self.operationRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + NSError *jsonError; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&jsonError]; + [self.operationRequest setHTTPBody:jsonData]; + } + else + [NSException raise:NSInvalidArgumentException format:@"POST and PUT parameters must be provided as NSDictionary or NSArray when sendParametersAsJSON is set to YES."]; + } + else if([parameters isKindOfClass:[NSDictionary class]]) { __block BOOL hasData = NO; - + NSDictionary *paramsDict = (NSDictionary*)parameters; + [paramsDict.allValues enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - if([obj isKindOfClass:[NSData class]]) + if([obj isKindOfClass:[NSData class]] || [obj isKindOfClass:[NSURL class]]) hasData = YES; else if(![obj isKindOfClass:[NSString class]] && ![obj isKindOfClass:[NSNumber class]]) [NSException raise:NSInvalidArgumentException format:@"%@ requests only accept NSString and NSNumber parameters.", self.operationRequest.HTTPMethod]; @@ -235,6 +253,7 @@ - (void)addParametersToRequest:(NSDictionary*)paramsDict { if(!hasData) { const char *stringData = [[self parameterStringForDictionary:paramsDict] UTF8String]; NSMutableData *postData = [NSMutableData dataWithBytes:stringData length:strlen(stringData)]; + [self.operationRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; //added by uzys [self.operationRequest setHTTPBody:postData]; } else { @@ -243,20 +262,43 @@ - (void)addParametersToRequest:(NSDictionary*)paramsDict { [self.operationRequest setValue:contentType forHTTPHeaderField: @"Content-Type"]; __block NSMutableData *postData = [NSMutableData data]; - + __block int dataIdx = 0; // add string parameters [paramsDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - if(![obj isKindOfClass:[NSData class]]) { + if(![obj isKindOfClass:[NSData class]] && ![obj isKindOfClass:[NSURL class]]) { [postData appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; [postData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; [postData appendData:[[NSString stringWithFormat:@"%@", obj] dataUsingEncoding:NSUTF8StringEncoding]]; [postData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; } else { + NSString *fileName = nil; + NSData *data = nil; + NSString *imageExtension = nil; + if ([obj isKindOfClass:[NSURL class]]) { + fileName = [obj lastPathComponent]; + data = [NSData dataWithContentsOfURL:obj]; + } + else { + imageExtension = [obj getImageType]; + fileName = [NSString stringWithFormat:@"userfile%d%x", dataIdx, (int)[[NSDate date] timeIntervalSince1970]]; + if (imageExtension != nil) + fileName = [fileName stringByAppendingPathExtension:imageExtension]; + data = obj; + } + [postData appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - [postData appendData:[[NSString stringWithFormat:@"Content-Disposition: attachment; name=\"%@\"; filename=\"userfile\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; - [postData appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - [postData appendData:obj]; + [postData appendData:[[NSString stringWithFormat:@"Content-Disposition: attachment; name=\"%@\"; filename=\"%@\"\r\n", key, fileName] dataUsingEncoding:NSUTF8StringEncoding]]; + + if(imageExtension != nil) { + [postData appendData:[[NSString stringWithFormat:@"Content-Type: image/%@\r\n\r\n",imageExtension] dataUsingEncoding:NSUTF8StringEncoding]]; + } + else { + [postData appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + } + + [postData appendData:data]; [postData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + dataIdx++; } }]; @@ -264,12 +306,18 @@ - (void)addParametersToRequest:(NSDictionary*)paramsDict { [self.operationRequest setHTTPBody:postData]; } } - } else { + else + [NSException raise:NSInvalidArgumentException format:@"POST and PUT parameters must be provided as NSDictionary when sendParametersAsJSON is set to NO."]; + } + else if([parameters isKindOfClass:[NSDictionary class]]) { + NSDictionary *paramsDict = (NSDictionary*)parameters; NSString *baseAddress = self.operationRequest.URL.absoluteString; if(paramsDict.count > 0) baseAddress = [baseAddress stringByAppendingFormat:@"?%@", [self parameterStringForDictionary:paramsDict]]; [self.operationRequest setURL:[NSURL URLWithString:baseAddress]]; } + else + [NSException raise:NSInvalidArgumentException format:@"GET and DELETE parameters must be provided as NSDictionary."]; } - (NSString*)parameterStringForDictionary:(NSDictionary*)parameters { @@ -303,11 +351,11 @@ - (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field { - (void)setTimeoutTimer:(NSTimer *)newTimer { - if(timeoutTimer) - [timeoutTimer invalidate], timeoutTimer = nil; + if(_timeoutTimer) + [_timeoutTimer invalidate], _timeoutTimer = nil; if(newTimer) - timeoutTimer = newTimer; + _timeoutTimer = newTimer; } #pragma mark - NSOperation methods @@ -319,7 +367,9 @@ - (void)start { return; } -#if TARGET_OS_IPHONE + [self preprocessParameters]; + +#if TARGET_OS_IPHONE && !__has_feature(attribute_availability_app_extension) // all requests should complete and run completion block unless we explicitely cancel them. self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ if(self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) { @@ -330,14 +380,11 @@ - (void)start { #endif dispatch_async(dispatch_get_main_queue(), ^{ - [self increaseTaskCount]; + [self increaseSVHTTPRequestTaskCount]; }); - if(self.operationParameters) - [self addParametersToRequest:self.operationParameters]; - if(self.userAgent) - [self.operationRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + [self.operationRequest setValue:self.userAgent forHTTPHeaderField:@"User-Agent"]; else if(defaultUserAgent) [self.operationRequest setValue:defaultUserAgent forHTTPHeaderField:@"User-Agent"]; @@ -382,11 +429,11 @@ - (void)start { // private method; not part of NSOperation - (void)finish { [self.operationConnection cancel]; - operationConnection = nil; + self.operationConnection = nil; - [self decreaseTaskCount]; + [self decreaseSVHTTPRequestTaskCount]; -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE && !__has_feature(attribute_availability_app_extension) if(self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier]; self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; @@ -423,14 +470,14 @@ - (BOOL)isExecuting { - (SVHTTPRequestState)state { @synchronized(self) { - return state; + return _state; } } - (void)setState:(SVHTTPRequestState)newState { @synchronized(self) { [self willChangeValueForKey:@"state"]; - state = newState; + _state = newState; [self didChangeValueForKey:@"state"]; } } @@ -497,9 +544,16 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { id response = [NSData dataWithData:self.operationData]; NSError *error = nil; - if ([[operationURLResponse MIMEType] isEqualToString:@"application/json"]) { + if ([[self.operationURLResponse MIMEType] isEqualToString:@"application/json"]) { if(self.operationData && self.operationData.length > 0) { - NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:response options:NSJSONReadingAllowFragments error:&error]; + //We parse the string before, because we need it to be UTF-8 in NSJSONSerialization + NSString *utf8String = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding]; + if (utf8String == nil) { + utf8String = [[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding]; + } + + NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:[utf8String dataUsingEncoding:NSUTF8StringEncoding] + options:NSJSONReadingAllowFragments error:&error]; if(jsonObject) response = jsonObject; @@ -523,13 +577,23 @@ - (void)callCompletionBlockWithResponse:(id)response error:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ NSError *serverError = error; - if(!serverError && self.operationURLResponse.statusCode == 500) { - serverError = [NSError errorWithDomain:NSURLErrorDomain - code:NSURLErrorBadServerResponse - userInfo:[NSDictionary dictionaryWithObjectsAndKeys: - @"Bad Server Response.", NSLocalizedDescriptionKey, - self.operationRequest.URL, NSURLErrorFailingURLErrorKey, - self.operationRequest.URL.absoluteString, NSURLErrorFailingURLStringErrorKey, nil]]; + if(!serverError) { + if(self.operationURLResponse.statusCode == 500) { + serverError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorBadServerResponse + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + @"Bad Server Response.", NSLocalizedDescriptionKey, + self.operationRequest.URL, NSURLErrorFailingURLErrorKey, + self.operationRequest.URL.absoluteString, NSURLErrorFailingURLStringErrorKey, nil]]; + } + else if(self.operationURLResponse.statusCode > 299) { + serverError = [NSError errorWithDomain:NSURLErrorDomain + code:self.operationURLResponse.statusCode + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + self.operationRequest.URL, NSURLErrorFailingURLErrorKey, + self.operationRequest.URL.absoluteString, NSURLErrorFailingURLStringErrorKey, nil]]; + + } } if(self.operationCompletionBlock && !self.isCancelled) @@ -621,6 +685,63 @@ - (NSString *)base64EncodingWithLineLength:(unsigned int) lineLength { return result; } +- (BOOL)isJPG { + if (self.length > 4) { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0xff && + buffer[1]==0xd8 && + buffer[2]==0xff && + buffer[3]==0xe0; + } + + return NO; +} + +- (BOOL)isPNG { + if (self.length > 4) { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0x89 && + buffer[1]==0x50 && + buffer[2]==0x4e && + buffer[3]==0x47; + } + + return NO; +} + +- (BOOL)isGIF { + if(self.length >3) { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0x47 && + buffer[1]==0x49 && + buffer[2]==0x46; //Signature ASCII 'G','I','F' + } + return NO; +} + +- (NSString *)getImageType { + NSString *ret; + if([self isJPG]) { + ret=@"jpg"; + } + else if([self isGIF]) { + ret=@"gif"; + } + else if([self isPNG]) { + ret=@"png"; + } + else { + ret=nil; + } + return ret; +} + @end