diff --git a/.travis.yml b/.travis.yml index ab1deda2..1f54173b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,9 @@ language: objective-c -xcode_workspace: InstagramKit-Example/InstagramKit-Example.xcworkspace -xcode_scheme: InstagramKit-Example -xcode_sdk: iphonesimulator8.1 -podfile: InstagramKit-Example/Podfile +xcode_workspace: InstagramKit.xcworkspace +xcode_scheme: InstagramKit +osx_image: xcode6.4 +xcode_sdk: iphonesimulator8.4 # before_install: -# - gem install cocoapods --quiet -# - pod --version +# - pod repo remove master # - pod setup -# - pod install - # - pod repo update --silent - -# script: - #- pod lib lint - # - xctool -xcode_workspace InstagramKit-Example/InstagramKit-Example.xcode_workspace -xcode_scheme 'InstagramKit-Example' -configuration Release -sdk iphonesimulator -arch i386 build diff --git a/InstagramKit-Example/InstagramKit-Example.xcodeproj/project.pbxproj b/InstagramKit-Example/InstagramKit-Example.xcodeproj/project.pbxproj index 61e2a976..c0953cec 100644 --- a/InstagramKit-Example/InstagramKit-Example.xcodeproj/project.pbxproj +++ b/InstagramKit-Example/InstagramKit-Example.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ /* Begin PBXFileReference section */ 17B839D94B3A182F8D8DBFBE /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; - 8203CC4F1B48D5C8007FA65B /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; 82DA94FF1B483C1C00A6303B /* InstagramKit-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "InstagramKit-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 82DA95031B483C1C00A6303B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 82DA95041B483C1C00A6303B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -99,7 +98,6 @@ 82DA95291B483CA900A6303B /* AppDelegate.h */, 82DA952A1B483CA900A6303B /* AppDelegate.m */, 82DA952B1B483CA900A6303B /* View */, - 8203CC4F1B48D5C8007FA65B /* Constants.h */, ); path = Classes; sourceTree = ""; diff --git a/InstagramKit-Example/InstagramKit-Example/Base.lproj/Main.storyboard b/InstagramKit-Example/InstagramKit-Example/Base.lproj/Main.storyboard index ffe0b04f..48e8e41e 100644 --- a/InstagramKit-Example/InstagramKit-Example/Base.lproj/Main.storyboard +++ b/InstagramKit-Example/InstagramKit-Example/Base.lproj/Main.storyboard @@ -157,6 +157,11 @@ + + + + + diff --git a/InstagramKit-Example/InstagramKit-Example/Classes/Constants.h b/InstagramKit-Example/InstagramKit-Example/Classes/Constants.h deleted file mode 100644 index 7b589ccb..00000000 --- a/InstagramKit-Example/InstagramKit-Example/Classes/Constants.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) 2015 Shyam Bhat -// -// 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. - -#ifndef InstagramKit_Example_Constants_h -#define InstagramKit_Example_Constants_h - -#endif - - -#define kInstagramUserAuthenticatedNotification @"InstagramUserAuthenticatedNotification" \ No newline at end of file diff --git a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKCollectionViewController.m b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKCollectionViewController.m index 5c1e2985..52a60952 100644 --- a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKCollectionViewController.m +++ b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKCollectionViewController.m @@ -24,7 +24,6 @@ #import "IKCell.h" #import "InstagramMedia.h" #import "IKMediaViewController.h" -#import "Constants.h" #import "IKLoginViewController.h" #define kNumberOfCellsInARow 3 @@ -32,9 +31,9 @@ @interface IKCollectionViewController () -@property (nonatomic, strong) NSMutableArray *mediaArray; -@property (nonatomic, strong) InstagramPaginationInfo *currentPaginationInfo; -@property (nonatomic, weak) InstagramEngine *instagramEngine; +@property (nonatomic, strong) NSMutableArray *mediaArray; +@property (nonatomic, strong) InstagramPaginationInfo *currentPaginationInfo; +@property (nonatomic, weak) InstagramEngine *instagramEngine; @end @@ -52,8 +51,8 @@ - (void)viewDidLoad [self loadMedia]; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(userAuthenticated:) - name:kInstagramUserAuthenticatedNotification + selector:@selector(userAuthenticationChanged:) + name:InstagtamKitUserAuthenticationChangedNotification object:nil]; } @@ -66,22 +65,19 @@ - (void)viewDidLoad - (void)loadMedia { self.currentPaginationInfo = nil; + BOOL isSessionValid = [self.instagramEngine isSessionValid]; + [self setTitle: (isSessionValid) ? @"My Feed" : @"Popular Media"]; + [self.navigationItem.leftBarButtonItem setTitle: (isSessionValid) ? @"Log out" : @"Log in"]; + [self.navigationItem.rightBarButtonItem setEnabled: isSessionValid]; + [self.mediaArray removeAllObjects]; + [self.collectionView reloadData]; + if (isSessionValid) { - [self setTitle:@"My Feed"]; - [self.navigationItem.leftBarButtonItem setTitle:@"Log out"]; - [self.navigationItem.rightBarButtonItem setEnabled:YES]; - [self.mediaArray removeAllObjects]; - [self.collectionView reloadData]; [self requestSelfFeed]; } else { - [self setTitle:@"Popular Media"]; - [self.navigationItem.leftBarButtonItem setTitle:@"Log in"]; - [self.navigationItem.rightBarButtonItem setEnabled:NO]; - [self.mediaArray removeAllObjects]; - [self.collectionView reloadData]; [self requestPopularMedia]; } } @@ -90,8 +86,7 @@ - (void)loadMedia #pragma mark - API Requests - /** - - requestPopularMedia - Calls InstagramKit's Helper method to fetch Popular Instagram Media. + Calls InstagramKit's helper method to fetch Popular Instagram Media. */ - (void)requestPopularMedia { @@ -107,8 +102,7 @@ - (void)requestPopularMedia /** - - requestSelfFeed - Calls InstagramKit's Helper method to fetch Media in logged in Users own feed. + Calls InstagramKit's helper method to fetch Media in the authenticated user's feed. @discussion The self.currentPaginationInfo object is updated on each successful call and it's updated nextMaxId is passed as a parameter to the next paginated request. */ @@ -133,7 +127,6 @@ - (void)requestSelfFeed /** - - moreTapped: Invoked when user taps the 'More' navigation item. @discussion The requestSelfFeed method is called with updated pagination parameters (nextMaxId). */ @@ -143,9 +136,8 @@ - (IBAction)moreTapped:(id)sender { /** - - loginTapped: - Invoked when user taps the left navigation item. - @discussion Either directs to the Login ViewController or logs out. + Invoked when user taps the left navigation item. + @discussion Either directs to the Login ViewController or logs out. */ - (IBAction)loginTapped:(id)sender { @@ -157,10 +149,8 @@ - (IBAction)loginTapped:(id)sender { [self.instagramEngine logout]; - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Logged out" message:@"The user is now logged out." delegate:nil cancelButtonTitle:@"Okay" otherButtonTitles:nil, nil]; + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"InstagramKit" message:@"You are now logged out." delegate:nil cancelButtonTitle:@"Okay" otherButtonTitles:nil, nil]; [alert show]; - - [self loadMedia]; } } @@ -168,7 +158,7 @@ - (IBAction)loginTapped:(id)sender #pragma mark - User Authenticated Notification - -- (void)userAuthenticated:(NSNotification *)notification +- (void)userAuthenticationChanged:(NSNotification *)notification { [self loadMedia]; } diff --git a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKLoginViewController.m b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKLoginViewController.m index 4a320d44..69ebe34c 100644 --- a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKLoginViewController.m +++ b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKLoginViewController.m @@ -20,7 +20,6 @@ #import "IKLoginViewController.h" #import "InstagramKit.h" -#import "Constants.h" @interface IKLoginViewController () @@ -34,8 +33,9 @@ - (void)viewDidLoad { [super viewDidLoad]; self.webView.scrollView.bounces = NO; - - NSURL *authURL = [[InstagramEngine sharedEngine] authorizarionURLForScope:InstagramKitLoginScopeBasic]; + [self.navigationItem.rightBarButtonItem setEnabled:NO]; + + NSURL *authURL = [[InstagramEngine sharedEngine] authorizarionURL]; [self.webView loadRequest:[NSURLRequest requestWithURL:authURL]]; } @@ -44,22 +44,17 @@ - (void)viewDidLoad - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSError *error; - if ([[InstagramEngine sharedEngine] extractValidAccessTokenFromURL:request.URL error:&error]) + if ([[InstagramEngine sharedEngine] receivedValidAccessTokenFromURL:request.URL error:&error]) { - if (!error) { - [self dismissViewControllerAnimated:YES - completion:nil]; - - [[NSNotificationCenter defaultCenter] postNotificationName:kInstagramUserAuthenticatedNotification - object:nil]; - } - else - { - NSLog(@"%@",error); - } + [self authenticationSuccess]; } - return YES; } +- (void)authenticationSuccess +{ + [self.navigationItem setLeftBarButtonItem:nil]; + [self.navigationItem.rightBarButtonItem setEnabled:YES]; +} + @end diff --git a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKMediaViewController.m b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKMediaViewController.m index 6da1b5ef..e211d317 100644 --- a/InstagramKit-Example/InstagramKit-Example/Classes/View/IKMediaViewController.m +++ b/InstagramKit-Example/InstagramKit-Example/Classes/View/IKMediaViewController.m @@ -26,9 +26,9 @@ @interface IKMediaViewController () -@property (weak, nonatomic) IBOutlet UIImageView *imageView; -@property (weak, nonatomic) IBOutlet UILabel *captionLabel; -@property (nonatomic, strong) InstagramMedia *media; +@property (weak, nonatomic) IBOutlet UIImageView *imageView; +@property (weak, nonatomic) IBOutlet UILabel *captionLabel; +@property (nonatomic, strong) InstagramMedia *media; @end diff --git a/InstagramKit-Example/Podfile b/InstagramKit-Example/Podfile index 0176f8fb..13d1c0a9 100644 --- a/InstagramKit-Example/Podfile +++ b/InstagramKit-Example/Podfile @@ -1 +1,2 @@ -pod 'InstagramKit', :path => '../' +pod "InstagramKit", :path => '../' +pod "InstagramKit/UICKeyChainStore", :path => '../' diff --git a/InstagramKit.podspec b/InstagramKit.podspec index d5365035..a37e2ea4 100644 --- a/InstagramKit.podspec +++ b/InstagramKit.podspec @@ -10,11 +10,21 @@ Pod::Spec.new do |s| s.homepage = 'https://github.com/shyambhat/InstagramKit' s.license = 'MIT' s.author = { "Shyam Bhat" => "shyambhat@me.com" } - s.platform = :ios, '6.0' + s.platform = :ios, '7.0' s.source = { :git => "https://github.com/shyambhat/InstagramKit.git", :tag => '3.6.5' } s.source_files = 'InstagramKit', 'InstagramKit/**/*.{h,m}' s.exclude_files = 'InstagramKitDemo' s.requires_arc = true s.dependency 'AFNetworking', '~>2.0' + s.default_subspec = 'Exclude-UICKeyChainStore' + s.subspec 'Exclude-UICKeyChainStore' do |exclude_uickeychainstore| + # default lean subspec for users who don't need UICKeyChainStore + end + + s.subspec 'UICKeyChainStore' do |uickeychainstore| + uickeychainstore.xcconfig = + { 'OTHER_CFLAGS' => '$(inherited) -INSTAGRAMKIT_INCLUDE_UICKEYCHAINSTORE' } + uickeychainstore.dependency 'UICKeyChainStore', '~>2.0' + end end diff --git a/InstagramKit.xcodeproj/project.pbxproj b/InstagramKit.xcodeproj/project.pbxproj index 8ea34d44..93f3ac6c 100644 --- a/InstagramKit.xcodeproj/project.pbxproj +++ b/InstagramKit.xcodeproj/project.pbxproj @@ -16,9 +16,23 @@ 822466881B4C2FD000F8B5EA /* InstagramTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 822466791B4C2FD000F8B5EA /* InstagramTag.m */; }; 8224668A1B4C2FD000F8B5EA /* InstagramUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 8224667B1B4C2FD000F8B5EA /* InstagramUser.m */; }; 8289FCAC1B51C419000903FD /* InstagramKitConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8289FCAB1B51C419000903FD /* InstagramKitConstants.m */; }; + 82D75EE71B5BF8000028EA29 /* InstagramKitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 82D75EE61B5BF8000028EA29 /* InstagramKitTests.m */; }; + 82D75EE81B5BF8000028EA29 /* libInstagramKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8224664F1B4C2D8A00F8B5EA /* libInstagramKit.a */; }; + 82D75EF71B5BFE0D0028EA29 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82D75EF61B5BFE0D0028EA29 /* SystemConfiguration.framework */; }; + 82D75EFE1B5BFEF90028EA29 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82D75EFA1B5BFEE40028EA29 /* MobileCoreServices.framework */; }; CB701B571A290E15ED62FF1E /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D71ABA66AA07709C96A1148 /* libPods.a */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 82D75EE91B5BF8000028EA29 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 822466471B4C2D8A00F8B5EA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8224664E1B4C2D8A00F8B5EA; + remoteInfo = InstagramKit; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 8224664D1B4C2D8A00F8B5EA /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -55,6 +69,16 @@ 8224667B1B4C2FD000F8B5EA /* InstagramUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstagramUser.m; sourceTree = ""; }; 8289FCAB1B51C419000903FD /* InstagramKitConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstagramKitConstants.m; sourceTree = ""; }; 8289FCAD1B51C42E000903FD /* InstagramKitConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InstagramKitConstants.h; sourceTree = ""; }; + 82D75EE21B5BF8000028EA29 /* InstagramKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstagramKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 82D75EE51B5BF8000028EA29 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 82D75EE61B5BF8000028EA29 /* InstagramKitTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstagramKitTests.m; sourceTree = ""; }; + 82D75EF21B5BFDEF0028EA29 /* libPods-AFNetworking.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-AFNetworking.a"; path = "Pods/../build/Debug-iphoneos/libPods-AFNetworking.a"; sourceTree = ""; }; + 82D75EF41B5BFDFA0028EA29 /* libPods.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libPods.a; path = "Pods/../build/Debug-iphoneos/libPods.a"; sourceTree = ""; }; + 82D75EF61B5BFE0D0028EA29 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 82D75EF81B5BFEDF0028EA29 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 82D75EFA1B5BFEE40028EA29 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 82D75EFC1B5BFEE80028EA29 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 82D75F031B5C56C10028EA29 /* InstagramEngine+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "InstagramEngine+Internal.h"; sourceTree = ""; }; 8D71ABA66AA07709C96A1148 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; A62AD4BB7D5AE07DF04ACAD7 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; CAAFACF6E29667CA9EEF7C20 /* libPods-InstagramKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-InstagramKit.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -69,6 +93,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 82D75EDF1B5BF8000028EA29 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 82D75EFE1B5BFEF90028EA29 /* MobileCoreServices.framework in Frameworks */, + 82D75EF71B5BFE0D0028EA29 /* SystemConfiguration.framework in Frameworks */, + 82D75EE81B5BF8000028EA29 /* libInstagramKit.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -87,6 +121,7 @@ isa = PBXGroup; children = ( 822466511B4C2D8A00F8B5EA /* InstagramKit */, + 82D75EE31B5BF8000028EA29 /* InstagramKitTests */, 822466501B4C2D8A00F8B5EA /* Products */, 61AE4BABF7E3863CA217EAEF /* Pods */, B7153832B971838CED8AEC18 /* Frameworks */, @@ -97,6 +132,7 @@ isa = PBXGroup; children = ( 8224664F1B4C2D8A00F8B5EA /* libInstagramKit.a */, + 82D75EE21B5BF8000028EA29 /* InstagramKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -143,9 +179,41 @@ path = Models; sourceTree = ""; }; + 82D75EE31B5BF8000028EA29 /* InstagramKitTests */ = { + isa = PBXGroup; + children = ( + 82D75EE61B5BF8000028EA29 /* InstagramKitTests.m */, + 82D75F021B5C56C10028EA29 /* Categories */, + 82D75EE41B5BF8000028EA29 /* Supporting Files */, + ); + path = InstagramKitTests; + sourceTree = ""; + }; + 82D75EE41B5BF8000028EA29 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 82D75EE51B5BF8000028EA29 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 82D75F021B5C56C10028EA29 /* Categories */ = { + isa = PBXGroup; + children = ( + 82D75F031B5C56C10028EA29 /* InstagramEngine+Internal.h */, + ); + path = Categories; + sourceTree = ""; + }; B7153832B971838CED8AEC18 /* Frameworks */ = { isa = PBXGroup; children = ( + 82D75EFC1B5BFEE80028EA29 /* Security.framework */, + 82D75EFA1B5BFEE40028EA29 /* MobileCoreServices.framework */, + 82D75EF81B5BFEDF0028EA29 /* CoreGraphics.framework */, + 82D75EF61B5BFE0D0028EA29 /* SystemConfiguration.framework */, + 82D75EF41B5BFDFA0028EA29 /* libPods.a */, + 82D75EF21B5BFDEF0028EA29 /* libPods-AFNetworking.a */, CAAFACF6E29667CA9EEF7C20 /* libPods-InstagramKit.a */, 8D71ABA66AA07709C96A1148 /* libPods.a */, ); @@ -174,6 +242,24 @@ productReference = 8224664F1B4C2D8A00F8B5EA /* libInstagramKit.a */; productType = "com.apple.product-type.library.static"; }; + 82D75EE11B5BF8000028EA29 /* InstagramKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 82D75EED1B5BF8000028EA29 /* Build configuration list for PBXNativeTarget "InstagramKitTests" */; + buildPhases = ( + 82D75EDE1B5BF8000028EA29 /* Sources */, + 82D75EDF1B5BF8000028EA29 /* Frameworks */, + 82D75EE01B5BF8000028EA29 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 82D75EEA1B5BF8000028EA29 /* PBXTargetDependency */, + ); + name = InstagramKitTests; + productName = InstagramKitTests; + productReference = 82D75EE21B5BF8000028EA29 /* InstagramKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -186,6 +272,9 @@ 8224664E1B4C2D8A00F8B5EA = { CreatedOnToolsVersion = 6.4; }; + 82D75EE11B5BF8000028EA29 = { + CreatedOnToolsVersion = 6.4; + }; }; }; buildConfigurationList = 8224664A1B4C2D8A00F8B5EA /* Build configuration list for PBXProject "InstagramKit" */; @@ -201,10 +290,21 @@ projectRoot = ""; targets = ( 8224664E1B4C2D8A00F8B5EA /* InstagramKit */, + 82D75EE11B5BF8000028EA29 /* InstagramKitTests */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 82D75EE01B5BF8000028EA29 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXShellScriptBuildPhase section */ BEF61D59C959607747D3FFB5 /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -255,8 +355,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 82D75EDE1B5BF8000028EA29 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 82D75EE71B5BF8000028EA29 /* InstagramKitTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 82D75EEA1B5BF8000028EA29 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8224664E1B4C2D8A00F8B5EA /* InstagramKit */; + targetProxy = 82D75EE91B5BF8000028EA29 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 822466611B4C2D8A00F8B5EA /* Debug */ = { isa = XCBuildConfiguration; @@ -354,6 +470,40 @@ }; name = Release; }; + 82D75EEB1B5BF8000028EA29 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = InstagramKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 82D75EEC1B5BF8000028EA29 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = InstagramKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -375,6 +525,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 82D75EED1B5BF8000028EA29 /* Build configuration list for PBXNativeTarget "InstagramKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 82D75EEB1B5BF8000028EA29 /* Debug */, + 82D75EEC1B5BF8000028EA29 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 822466471B4C2D8A00F8B5EA /* Project object */; diff --git a/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKit.xcscheme b/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKit.xcscheme index 4b004d11..5eefa0d7 100644 --- a/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKit.xcscheme +++ b/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKit.xcscheme @@ -1,10 +1,30 @@ - + version = "1.7"> + + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + + + + + + + + + + + + + buildConfiguration = "Debug" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + allowLocationSimulation = "NO"> + + + + + + + + + + + + + + + + + + diff --git a/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKitTests.xcscheme b/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKitTests.xcscheme new file mode 100644 index 00000000..c4ab40e8 --- /dev/null +++ b/InstagramKit.xcodeproj/xcshareddata/xcschemes/InstagramKitTests.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InstagramKit/Engine/InstagramEngine.h b/InstagramKit/Engine/InstagramEngine.h index df7d0ff5..b4a4bab1 100644 --- a/InstagramKit/Engine/InstagramEngine.h +++ b/InstagramKit/Engine/InstagramEngine.h @@ -49,6 +49,16 @@ #pragma mark - Authentication - + +/** + * A convenience method to generate an authentication URL with Basic permissions + * to direct user to Instagram's login screen. + * + * @return URL to direct user to Instagram's login screen. + */ +- (NSURL *)authorizarionURL; + + /** * A convenience method to generate an authentication URL to direct user to Instagram's login screen. * @@ -67,8 +77,8 @@ * * @return YES if valid token extracted and saved, otherwise NO. */ -- (BOOL)extractValidAccessTokenFromURL:(NSURL *)url - error:(NSError *__autoreleasing *)error; +- (BOOL)receivedValidAccessTokenFromURL:(NSURL *)url + error:(NSError *__autoreleasing *)error; /** * Validate if authorization is done. @@ -93,7 +103,7 @@ * @param failure Provides an error and a server status code. */ - (void)getMedia:(NSString *)mediaId - withSuccess:(InstagramMediaDetailBlock)success + withSuccess:(InstagramMediaObjectBlock)success failure:(InstagramFailureBlock)failure; @@ -199,11 +209,11 @@ /** * Get basic information about a user. * - * @param user An partially populated User object. + * @param userId Id of a User object. * @param success Provides a fully populated User object. * @param failure Provides an error and a server status code. */ -- (void)getUserDetails:(InstagramUser *)user +- (void)getUserDetails:(NSString *)userId withSuccess:(InstagramUserBlock)success failure:(InstagramFailureBlock)failure; @@ -463,7 +473,7 @@ */ - (void)createComment:(NSString *)commentText onMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure; @@ -483,7 +493,7 @@ */ - (void)removeComment:(NSString *)commentId onMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure; @@ -514,7 +524,7 @@ * @param failure Provides an error and a server status code. */ - (void)likeMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure; @@ -530,7 +540,7 @@ * @param failure Provides an error and a server status code. */ - (void)unlikeMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure; @@ -702,7 +712,7 @@ * @param failure Provides an error and a server status code. */ - (void)getPaginatedItemsForInfo:(InstagramPaginationInfo *)paginationInfo - withSuccess:(InstagramObjectsBlock)success + withSuccess:(InstagramPaginatiedResponseBlock)success failure:(InstagramFailureBlock)failure; diff --git a/InstagramKit/Engine/InstagramEngine.m b/InstagramKit/Engine/InstagramEngine.m index 03ffa1e0..058259a3 100644 --- a/InstagramKit/Engine/InstagramEngine.m +++ b/InstagramKit/Engine/InstagramEngine.m @@ -27,12 +27,18 @@ #import "InstagramPaginationInfo.h" #import "InstagramLocation.h" +#if INSTAGRAMKIT_UICKEYCHAINSTORE +#import "UICKeyChainStore.h" +#endif + @interface InstagramEngine() -{ - dispatch_queue_t mBackgroundQueue; -} @property (nonatomic, strong) AFHTTPSessionManager *httpManager; +@property (nonatomic, strong) dispatch_queue_t backgroundQueue; + +#if INSTAGRAMKIT_UICKEYCHAINSTORE +@property (nonatomic, strong) UICKeyChainStore *keychainStore; +#endif @end @@ -53,42 +59,32 @@ + (instancetype)sharedEngine { } -- (NSDictionary*)clientConfiguration { - - NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; - NSMutableDictionary *configuration = [NSMutableDictionary dictionary]; - if (info[kInstagramAppClientIdConfigurationKey]) { - configuration[kInstagramAppClientIdConfigurationKey] = info[kInstagramAppClientIdConfigurationKey]; - } - if (info[kInstagramAppRedirectURLConfigurationKey]) { - configuration[kInstagramAppRedirectURLConfigurationKey] = info[kInstagramAppRedirectURLConfigurationKey]; - } - configuration[kInstagramKitBaseUrlConfigurationKey] = kInstagramKitBaseUrl; - configuration[kInstagramKitAuthorizationUrlConfigurationKey] = kInstagramKitAuthorizationUrl; - - return [NSDictionary dictionaryWithDictionary:configuration]; -} - - - (instancetype)init { if (self = [super init]) { - NSURL *baseURL = [NSURL URLWithString:kInstagramKitBaseUrl]; + NSURL *baseURL = [NSURL URLWithString:kInstagramKitBaseURL]; self.httpManager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL]; self.httpManager.responseSerializer = [[AFJSONResponseSerializer alloc] init]; - NSDictionary *configuration = [self clientConfiguration]; - self.appClientID = configuration[kInstagramAppClientIdConfigurationKey]; - self.appRedirectURL = configuration[kInstagramAppRedirectURLConfigurationKey]; - - mBackgroundQueue = dispatch_queue_create("background", NULL); + NSDictionary *info = [[NSBundle bundleForClass:[self class]] infoDictionary]; + self.appClientID = info[kInstagramAppClientIdConfigurationKey]; + self.appRedirectURL = info[kInstagramAppRedirectURLConfigurationKey]; - NSAssert(IKNotNull(self.appClientID) && ![self.appClientID isEqualToString:@""] && ![self.appClientID isEqualToString:@""], @"Invalid Instagram Client ID. Please set a valid value for the key \"%@\" in Info.plist",kInstagramAppClientIdConfigurationKey); + self.backgroundQueue = dispatch_queue_create("instagramkit.response.queue", NULL); - NSAssert(IKNotNull(self.appRedirectURL) && ![self.appRedirectURL isEqualToString:@""] && ![self.appRedirectURL isEqualToString:@""], @"Invalid Redirect URL. Please set a valid value for the key \"%@\" in Info.plist", kInstagramAppRedirectURLConfigurationKey); + if (!IKNotNull(self.appClientID) || [self.appClientID isEqualToString:@""]) { + NSLog(@"ERROR : InstagramKit - Invalid Client ID. Please set a valid value for the key \"%@\" in the App's Info.plist file",kInstagramAppClientIdConfigurationKey); + } + + if (!IKNotNull(self.appRedirectURL) || [self.appRedirectURL isEqualToString:@""]) { + NSLog(@"ERROR : InstagramKit - Invalid Redirect URL. Please set a valid value for the key \"%@\" in the App's Info.plist file",kInstagramAppRedirectURLConfigurationKey); + } - [self retrieveAccessToken]; +#if INSTAGRAMKIT_UICKEYCHAINSTORE + self.keychainStore = [UICKeyChainStore keyChainStoreWithService:InstagtamKitKeychainStore]; + _accessToken = self.keychainStore[@"token"]; +#endif } return self; } @@ -97,28 +93,34 @@ - (instancetype)init { #pragma mark - Authentication - +- (NSURL *)authorizarionURL +{ + return [self authorizarionURLForScope:InstagramKitLoginScopeBasic]; +} + + - (NSURL *)authorizarionURLForScope:(InstagramKitLoginScope)scope { NSDictionary *parameters = [self authorizationParametersWithScope:scope]; - NSURLRequest *authRequest = (NSURLRequest *)[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:kInstagramKitAuthorizationUrl parameters:parameters error:nil]; + NSURLRequest *authRequest = (NSURLRequest *)[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:kInstagramKitAuthorizationURL parameters:parameters error:nil]; return authRequest.URL; } -- (BOOL)extractValidAccessTokenFromURL:(NSURL *)url - error:(NSError *__autoreleasing *)error +- (BOOL)receivedValidAccessTokenFromURL:(NSURL *)url + error:(NSError *__autoreleasing *)error { NSURL *appRedirectURL = [NSURL URLWithString:self.appRedirectURL]; - if (![appRedirectURL.scheme isEqual:url.scheme] || ![appRedirectURL.host isEqual:url.host]) { return NO; } - NSString* accessToken = [self queryStringParametersFromString:url.fragment][@"access_token"]; - if (accessToken) + BOOL success = YES; + NSString *token = [self queryStringParametersFromString:url.fragment][@"access_token"]; + if (token) { - self.accessToken = accessToken; + self.accessToken = token; } else { @@ -126,9 +128,9 @@ - (BOOL)extractValidAccessTokenFromURL:(NSURL *)url *error = [NSError errorWithDomain:InstagtamKitErrorDomain code:InstagramKitAuthenticationFailedError userInfo:@{NSLocalizedDescriptionKey: localizedDescription}]; + success = NO; } - return YES; - + return success; } @@ -140,12 +142,11 @@ - (BOOL)isSessionValid - (void)logout { - NSHTTPCookie *cookie; NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - for (cookie in [storage cookies]) { + [[storage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { [storage deleteCookie:cookie]; - } - [[NSUserDefaults standardUserDefaults] synchronize]; + }]; + self.accessToken = nil; } @@ -156,176 +157,174 @@ - (void)logout - (void)setAccessToken:(NSString *)accessToken { _accessToken = accessToken; - [self storeAccessToken]; -} -- (void)storeAccessToken -{ - [[NSUserDefaults standardUserDefaults] setObject:self.accessToken forKey:@"com.instagramkit.token"]; -} +#if INSTAGRAMKIT_UICKEYCHAINSTORE + self.keychainStore[@"token"] = self.accessToken; +#endif -- (void)retrieveAccessToken -{ - _accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:@"com.instagramkit.token"]; + [[NSNotificationCenter defaultCenter] postNotificationName:InstagtamKitUserAuthenticationChangedNotification object:nil]; } + #pragma mark - + - (NSDictionary *)authorizationParametersWithScope:(InstagramKitLoginScope)scope { - NSDictionary *configuration = [self clientConfiguration]; NSString *scopeString = [self stringForScope:scope]; - NSDictionary *parameters = @{ - @"client_id": configuration[kInstagramAppClientIdConfigurationKey], - @"redirect_uri": configuration[kInstagramAppRedirectURLConfigurationKey], - @"response_type": @"token", - @"scope": scopeString - }; + NSDictionary *parameters = @{ @"client_id": self.appClientID, + @"redirect_uri": self.appRedirectURL, + @"response_type": @"token", + @"scope": scopeString }; return parameters; } - (NSString *)stringForScope:(InstagramKitLoginScope)scope { - NSArray *typeStrings = @[@"basic",@"comments",@"relationships",@"likes"]; - NSMutableArray *strings = [NSMutableArray arrayWithCapacity:4]; + NSArray *typeStrings = @[@"basic", @"comments", @"relationships", @"likes"]; - for (NSUInteger i=0; i < typeStrings.count; i++) - { - NSUInteger enumBitValueToCheck = 1 << i; - if (scope & enumBitValueToCheck) - { - [strings addObject:[typeStrings objectAtIndex:i]]; - } - } - if (!strings.count) { - return @"basic"; - } - return [strings componentsJoinedByString:@"+"]; + NSMutableArray *strings = [NSMutableArray arrayWithCapacity:typeStrings.count]; + [typeStrings enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSUInteger enumBitValueToCheck = 1 << idx; + (scope & enumBitValueToCheck) ? [strings addObject:obj] : 0; + }]; + + return (strings.count) ? [strings componentsJoinedByString:@"+"] : typeStrings[0]; } --(NSDictionary*)queryStringParametersFromString:(NSString*)string { +- (NSDictionary *)queryStringParametersFromString:(NSString*)string { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; - for (NSString * param in [string componentsSeparatedByString:@"&"]) - { + [[string componentsSeparatedByString:@"&"] enumerateObjectsUsingBlock:^(NSString * param, NSUInteger idx, BOOL *stop) { NSArray *pairs = [param componentsSeparatedByString:@"="]; - if ([pairs count] != 2) continue; + if ([pairs count] != 2) return; + NSString *key = [pairs[0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSString *value = [pairs[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; [dict setObject:value forKey:key]; - } + }]; return dict; } +- (NSDictionary *)dictionaryWithAccessTokenAndParameters:(NSDictionary *)params +{ + NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:params]; + if (self.accessToken) { + [mutableDictionary setObject:self.accessToken forKey:kKeyAccessToken]; + } + else + { + [mutableDictionary setObject:self.appClientID forKey:kKeyClientID]; + } + return [NSDictionary dictionaryWithDictionary:mutableDictionary]; +} + + +- (NSDictionary *)parametersFromCount:(NSInteger)count maxId:(NSString *)maxId andPaginationKey:(NSString *)key +{ + NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSString stringWithFormat:@"%ld",(long)count], kCount, nil]; + if (maxId) { + [params setObject:maxId forKey:key]; + } + return params ? [NSDictionary dictionaryWithDictionary:params] : nil; +} + + #pragma mark - Base Calls - - (void)getPath:(NSString *)path parameters:(NSDictionary *)parameters responseModel:(Class)modelClass - success:(void (^)(id response, InstagramPaginationInfo *paginationInfo))success - failure:(void (^)(NSError* error, NSInteger statusCode))failure + success:(InstagramObjectBlock)success + failure:(InstagramFailureBlock)failure { - - NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:parameters]; - - if (self.accessToken) { - [params setObject:self.accessToken forKey:kKeyAccessToken]; - } - else - { - [params setObject:self.appClientID forKey:kKeyClientID]; - } - + NSDictionary *params = [self dictionaryWithAccessTokenAndParameters:parameters]; NSString *percentageEscapedPath = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - [self.httpManager GET:percentageEscapedPath - parameters:params - success:^(NSURLSessionDataTask *task, id responseObject) { - NSDictionary *responseDictionary = (NSDictionary *)responseObject; - NSDictionary *pInfo = responseDictionary[kPagination]; - InstagramPaginationInfo *paginationInfo = (pInfo)?[[InstagramPaginationInfo alloc] initWithInfo:pInfo andObjectType:modelClass]: nil; - BOOL multiple = ([responseDictionary[kData] isKindOfClass:[NSArray class]]); - if (multiple) { - NSArray *responseObjects = responseDictionary[kData]; - NSMutableArray*objects = [NSMutableArray arrayWithCapacity:responseObjects.count]; - dispatch_async(mBackgroundQueue, ^{ - if (modelClass) { - for (NSDictionary *info in responseObjects) { - id model = [[modelClass alloc] initWithInfo:info]; - [objects addObject:model]; - } - } - dispatch_async(dispatch_get_main_queue(), ^{ - (success)? success(objects, paginationInfo) : 0; - }); - }); - } - else { - id model = nil; - if (modelClass && IKNotNull(responseDictionary[kData])) - { - if (modelClass == [NSDictionary class]) { - model = [[NSDictionary alloc] initWithDictionary:responseDictionary[kData]]; - } - else - { - model = [[modelClass alloc] initWithInfo:responseDictionary[kData]]; - } - } - (success)? success(model, paginationInfo) : 0; - } - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - (failure)? failure(error, ((NSHTTPURLResponse *)[task response]).statusCode) : failure; - }]; + parameters:params + success:^(NSURLSessionDataTask *task, id responseObject) { + if (!success) return; + NSDictionary *responseDictionary = (NSDictionary *)responseObject; + NSDictionary *dataDictionary = IKNotNull(responseDictionary[kData]) ? responseDictionary[kData] : nil; + id model = (modelClass == [NSDictionary class]) ? [dataDictionary copy] : [[modelClass alloc] initWithInfo:dataDictionary]; + success(model); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + (failure)? failure(error, ((NSHTTPURLResponse *)[task response]).statusCode) : 0; + }]; +} + + +- (void)getPaginatedPath:(NSString *)path + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramPaginatiedResponseBlock)success + failure:(InstagramFailureBlock)failure +{ + NSDictionary *params = [self dictionaryWithAccessTokenAndParameters:parameters]; + NSString *percentageEscapedPath = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + [self.httpManager GET:percentageEscapedPath + parameters:params + success:^(NSURLSessionDataTask *task, id responseObject) { + if (!success) return; + NSDictionary *responseDictionary = (NSDictionary *)responseObject; + + NSDictionary *pInfo = responseDictionary[kPagination]; + InstagramPaginationInfo *paginationInfo = IKNotNull(pInfo)?[[InstagramPaginationInfo alloc] initWithInfo:pInfo andObjectType:modelClass]: nil; + + NSArray *responseObjects = IKNotNull(responseDictionary[kData]) ? responseDictionary[kData] : nil; + + NSMutableArray *objects = [NSMutableArray arrayWithCapacity:responseObjects.count]; + dispatch_async(self.backgroundQueue, ^{ + [responseObjects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSDictionary *info = obj; + id model = [[modelClass alloc] initWithInfo:info]; + [objects addObject:model]; + }]; + dispatch_async(dispatch_get_main_queue(), ^{ + success(objects, paginationInfo); + }); + }); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + (failure)? failure(error, ((NSHTTPURLResponse *)[task response]).statusCode) : 0; + }]; } - (void)postPath:(NSString *)path - parameters:(NSDictionary *)parameters - responseModel:(Class)modelClass - success:(void (^)(NSDictionary *responseObject))success - failure:(void (^)(NSError* error, NSInteger statusCode))failure + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramResponseBlock)success + failure:(InstagramFailureBlock)failure { - NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:parameters]; - if (self.accessToken) { - [params setObject:self.accessToken forKey:kKeyAccessToken]; - } - else - [params setObject:self.appClientID forKey:kKeyClientID]; - + NSDictionary *params = [self dictionaryWithAccessTokenAndParameters:parameters]; [self.httpManager POST:path - parameters:params - success:^(NSURLSessionDataTask *task, id responseObject) { - NSDictionary *responseDictionary = (NSDictionary *)responseObject; - (success)? success(responseDictionary) : 0; - } - failure:^(NSURLSessionDataTask *task, NSError *error) { - (failure) ? failure(error,((NSHTTPURLResponse*)[task response]).statusCode) : 0; - }]; + parameters:params + success:^(NSURLSessionDataTask *task, id responseObject) { + (success)? success((NSDictionary *)responseObject) : 0; + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + (failure) ? failure(error,((NSHTTPURLResponse*)[task response]).statusCode) : 0; + }]; } - (void)deletePath:(NSString *)path parameters:(NSDictionary *)parameters responseModel:(Class)modelClass - success:(void (^)(void))success - failure:(void (^)(NSError* error, NSInteger statusCode))failure + success:(InstagramResponseBlock)success + failure:(InstagramFailureBlock)failure { - NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:parameters]; - if (self.accessToken) { - [params setObject:self.accessToken forKey:kKeyAccessToken]; - } - else - [params setObject:self.appClientID forKey:kKeyClientID]; + NSDictionary *params = [self dictionaryWithAccessTokenAndParameters:parameters]; [self.httpManager DELETE:path parameters:params success:^(NSURLSessionDataTask *task, id responseObject) { - (success)? success():0; + (success)? success((NSDictionary *)responseObject) : 0; } failure:^(NSURLSessionDataTask *task, NSError *error) { (failure) ? failure(error,((NSHTTPURLResponse*)[task response]).statusCode) : 0; @@ -333,30 +332,17 @@ - (void)deletePath:(NSString *)path } -- (NSDictionary *)parametersFromCount:(NSInteger)count maxId:(NSString *)maxId andPaginationKey:(NSString *)key -{ - NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSString stringWithFormat:@"%ld",(long)count], kCount, nil]; - if (maxId) { - [params setObject:maxId forKey:key]; - } - return params ? [NSDictionary dictionaryWithDictionary:params] : nil; -} - - #pragma mark - Media - - (void)getMedia:(NSString *)mediaId - withSuccess:(InstagramMediaDetailBlock)success + withSuccess:(InstagramMediaObjectBlock)success failure:(InstagramFailureBlock)failure { [self getPath:[NSString stringWithFormat:@"media/%@",mediaId] parameters:nil responseModel:[InstagramMedia class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - InstagramMedia *media = response; - success(media); - } + success:success failure:failure]; } @@ -364,11 +350,11 @@ - (void)getMedia:(NSString *)mediaId - (void)getPopularMediaWithSuccess:(InstagramMediaBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:@"media/popular" - parameters:nil - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:@"media/popular" + parameters:nil + responseModel:[InstagramMedia class] + success:success + failure:failure]; } @@ -391,26 +377,22 @@ - (void)getMediaAtLocation:(CLLocationCoordinate2D)location failure:(InstagramFailureBlock)failure { NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxId]; - [self getPath:[NSString stringWithFormat:@"media/search?lat=%f&lng=%f",location.latitude,location.longitude] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"media/search?lat=%f&lng=%f",location.latitude,location.longitude] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } - (void)searchLocationsAtLocation:(CLLocationCoordinate2D)loction withSuccess:(InstagramLocationsBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"locations/search?lat=%f&lng=%f", loction.latitude, loction.longitude] - parameters:nil - responseModel:[InstagramLocation class] - success:^(id response, InstagramPaginationInfo *paginationInfo) - { - NSArray *objects = response; - success(objects); - } - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"locations/search?lat=%f&lng=%f", loction.latitude, loction.longitude] + parameters:nil + responseModel:[InstagramLocation class] + success:success + failure:failure]; } @@ -419,14 +401,11 @@ - (void)searchLocationsAtLocation:(CLLocationCoordinate2D)loction withSuccess:(InstagramLocationsBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"locations/search?lat=%f&lng=%f&distance=%ld", loction.latitude, loction.longitude, (long)distance] - parameters:nil - responseModel:[InstagramLocation class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - NSArray *objects = response; - success(objects); - } - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"locations/search?lat=%f&lng=%f&distance=%ld", loction.latitude, loction.longitude, (long)distance] + parameters:nil + responseModel:[InstagramLocation class] + success:success + failure:failure]; } @@ -437,9 +416,7 @@ - (void)getLocationWithId:(NSString*)locationId [self getPath:[NSString stringWithFormat:@"locations/%@", locationId] parameters:nil responseModel:[InstagramLocation class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - success(response); - } + success:success failure:failure]; } @@ -448,29 +425,25 @@ - (void)getMediaAtLocationWithId:(NSString*)locationId withSuccess:(InstagramMediaBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"locations/%@/media/recent", locationId] parameters:nil responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"locations/%@/media/recent", locationId] + parameters:nil + responseModel:[InstagramMedia class] + success:success + failure:failure]; } #pragma mark - Users - -- (void)getUserDetails:(InstagramUser *)user +- (void)getUserDetails:(NSString *)userId withSuccess:(InstagramUserBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/%@",user.Id] + [self getPath:[NSString stringWithFormat:@"users/%@",userId] parameters:nil - responseModel:[NSDictionary class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - if(IKNotNull(response)) - { - [user updateDetails:response]; - success(user); - } - } + responseModel:[InstagramUser class] + success:success failure:failure]; } @@ -496,11 +469,11 @@ - (void)getMediaForUser:(NSString *)userId NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxId]; - [self getPath:[NSString stringWithFormat:@"users/%@/media/recent",userId] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/%@/media/recent",userId] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } @@ -508,11 +481,11 @@ - (void)searchUsersWithString:(NSString *)name withSuccess:(InstagramUsersBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/search?q=%@",name] - parameters:nil - responseModel:[InstagramUser class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/search?q=%@",name] + parameters:nil + responseModel:[InstagramUser class] + success:success + failure:failure]; } @@ -525,10 +498,7 @@ - (void)getSelfUserDetailsWithSuccess:(InstagramUserBlock)success [self getPath:@"users/self" parameters:nil responseModel:[InstagramUser class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - InstagramUser *userDetail = response; - success(userDetail); - } + success:success failure:failure]; } @@ -549,11 +519,11 @@ - (void)getSelfFeedWithCount:(NSInteger)count failure:(InstagramFailureBlock)failure { NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxId]; - [self getPath:[NSString stringWithFormat:@"users/self/feed"] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/self/feed"] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } @@ -575,11 +545,11 @@ - (void)getMediaLikedBySelfWithCount:(NSInteger)count NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxLikeId]; - [self getPath:[NSString stringWithFormat:@"users/self/media/liked"] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/self/media/liked"] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } - (void)getSelfRecentMediaWithSuccess:(InstagramMediaBlock)success @@ -600,11 +570,11 @@ - (void)getSelfRecentMediaWithCount:(NSInteger)count NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxId]; - [self getPath:[NSString stringWithFormat:@"users/self/media/recent"] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/self/media/recent"] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } #pragma mark - Tags - @@ -617,10 +587,8 @@ - (void)getTagDetailsWithName:(NSString *)name [self getPath:[NSString stringWithFormat:@"tags/%@",name] parameters:nil responseModel:[InstagramTag class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - InstagramTag *tag = response; - success(tag); - } failure:failure]; + success:success + failure:failure]; } @@ -643,11 +611,11 @@ - (void)getMediaWithTagName:(NSString *)tag failure:(InstagramFailureBlock)failure { NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxTagId]; - [self getPath:[NSString stringWithFormat:@"tags/%@/media/recent",tag] - parameters:params - responseModel:[InstagramMedia class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"tags/%@/media/recent",tag] + parameters:params + responseModel:[InstagramMedia class] + success:success + failure:failure]; } @@ -670,11 +638,11 @@ - (void)searchTagsWithName:(NSString *)name failure:(InstagramFailureBlock)failure { NSDictionary *params = [self parametersFromCount:count maxId:maxId andPaginationKey:kPaginationKeyMaxId]; - [self getPath:[NSString stringWithFormat:@"tags/search?q=%@",name] - parameters:params - responseModel:[InstagramTag class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"tags/search?q=%@",name] + parameters:params + responseModel:[InstagramTag class] + success:success + failure:failure]; } @@ -685,37 +653,31 @@ - (void)getCommentsOnMedia:(NSString *)mediaId withSuccess:(InstagramCommentsBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"media/%@/comments",mediaId] - parameters:nil - responseModel:[InstagramComment class] - success:^(id response, InstagramPaginationInfo *paginationInfo) { - NSArray *objects = response; - success(objects); - } - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"media/%@/comments",mediaId] + parameters:nil + responseModel:[InstagramComment class] + success:success + failure:failure]; } - (void)createComment:(NSString *)commentText onMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure { NSDictionary *params = [NSDictionary dictionaryWithObjects:@[commentText] forKeys:@[@"text"]]; [self postPath:[NSString stringWithFormat:@"media/%@/comments",mediaId] parameters:params responseModel:nil - success:^(NSDictionary *responseObject) - { - success(); - } + success:success failure:failure]; } - (void)removeComment:(NSString *)commentId onMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure { [self deletePath:[NSString stringWithFormat:@"media/%@/comments/%@",mediaId,commentId] @@ -733,38 +695,34 @@ - (void)getLikesOnMedia:(NSString *)mediaId withSuccess:(InstagramUsersBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"media/%@/likes",mediaId] - parameters:nil - responseModel:[InstagramUser class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"media/%@/likes",mediaId] + parameters:nil + responseModel:[InstagramUser class] + success:success + failure:failure]; } - (void)likeMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure { [self postPath:[NSString stringWithFormat:@"media/%@/likes",mediaId] parameters:nil responseModel:nil - success:^(NSDictionary *responseObject) { - success(); - } + success:success failure:failure]; } - (void)unlikeMedia:(NSString *)mediaId - withSuccess:(dispatch_block_t)success + withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure { [self deletePath:[NSString stringWithFormat:@"media/%@/likes",mediaId] parameters:nil responseModel:nil - success:^{ - success(); - } + success:success failure:failure]; } @@ -776,13 +734,11 @@ - (void)getRelationshipStatusOfUser:(NSString *)userId withSuccess:(InstagramResponseBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/%@/relationship",userId] parameters:nil responseModel:[NSDictionary class] success:^(id response, InstagramPaginationInfo *paginationInfo) { - if(success) - { - NSDictionary *responseDictionary = response; - success(responseDictionary); - } - } failure:failure]; + [self getPath:[NSString stringWithFormat:@"users/%@/relationship",userId] + parameters:nil + responseModel:[NSDictionary class] + success:success + failure:failure]; } @@ -790,11 +746,11 @@ - (void)getUsersFollowedByUser:(NSString *)userId withSuccess:(InstagramUsersBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/%@/follows",userId] - parameters:nil - responseModel:[InstagramUser class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/%@/follows",userId] + parameters:nil + responseModel:[InstagramUser class] + success:success + failure:failure]; } @@ -802,22 +758,22 @@ - (void)getFollowersOfUser:(NSString *)userId withSuccess:(InstagramUsersBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/%@/followed-by",userId] - parameters:nil - responseModel:[InstagramUser class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/%@/followed-by",userId] + parameters:nil + responseModel:[InstagramUser class] + success:success + failure:failure]; } - (void)getFollowRequestsWithSuccess:(InstagramUsersBlock)success failure:(InstagramFailureBlock)failure { - [self getPath:[NSString stringWithFormat:@"users/self/requested-by"] - parameters:nil - responseModel:[InstagramUser class] - success:success - failure:failure]; + [self getPaginatedPath:[NSString stringWithFormat:@"users/self/requested-by"] + parameters:nil + responseModel:[InstagramUser class] + success:success + failure:failure]; } @@ -878,19 +834,11 @@ - (void)approveUser:(NSString *)userId failure:(InstagramFailureBlock)failure { NSDictionary *params = @{kRelationshipActionKey:kRelationshipActionApprove}; - [self postPath:[NSString stringWithFormat:@"users/%@/relationship",userId] parameters:params responseModel:nil success:^(NSDictionary *responseObject) - { - if(success) - { - success(responseObject); - } - } failure:^(NSError *error, NSInteger statusCode) { - if(failure) - { - failure(error, statusCode); - } - NSLog(@"%@", [error description]); - }]; + [self postPath:[NSString stringWithFormat:@"users/%@/relationship",userId] + parameters:params + responseModel:nil + success:success + failure:failure]; } @@ -911,16 +859,16 @@ - (void)ignoreUser:(NSString *)userId - (void)getPaginatedItemsForInfo:(InstagramPaginationInfo *)paginationInfo - withSuccess:(InstagramObjectsBlock)success + withSuccess:(InstagramPaginatiedResponseBlock)success failure:(InstagramFailureBlock)failure { NSString *relativePath = [[paginationInfo.nextURL absoluteString] stringByReplacingOccurrencesOfString:[self.httpManager.baseURL absoluteString] withString:@""]; - [self getPath:relativePath - parameters:nil - responseModel:paginationInfo.type - success:success - failure:failure]; + [self getPaginatedPath:relativePath + parameters:nil + responseModel:paginationInfo.type + success:success + failure:failure]; } -@end +@end \ No newline at end of file diff --git a/InstagramKit/InstagramKitConstants.h b/InstagramKit/InstagramKitConstants.h index c24ed5d8..f31a0cfc 100644 --- a/InstagramKit/InstagramKitConstants.h +++ b/InstagramKit/InstagramKitConstants.h @@ -26,25 +26,27 @@ #define INSTAGRAMKIT_EXTERN extern __attribute__((visibility ("default"))) #endif +#define INSTAGRAMKIT_UICKEYCHAINSTORE __has_include("UICKeyChainStore.h") + /** * Configuration Key for the Instagram API's Base URL. */ -INSTAGRAMKIT_EXTERN NSString *const kInstagramKitBaseUrlConfigurationKey; +INSTAGRAMKIT_EXTERN NSString *const kInstagramKitBaseURLConfigurationKey; /** * Configuration Key for the Instagram API's Authorization URL. */ -INSTAGRAMKIT_EXTERN NSString *const kInstagramKitAuthorizationUrlConfigurationKey; +INSTAGRAMKIT_EXTERN NSString *const kInstagramKitAuthorizationURLConfigurationKey; /** * Instagram API's Base URL. */ -INSTAGRAMKIT_EXTERN NSString *const kInstagramKitBaseUrl; +INSTAGRAMKIT_EXTERN NSString *const kInstagramKitBaseURL; /** * Instagram API's Authorization URL. */ -INSTAGRAMKIT_EXTERN NSString *const kInstagramKitAuthorizationUrl; +INSTAGRAMKIT_EXTERN NSString *const kInstagramKitAuthorizationURL; /** * Configuration Key for the Client Id of your App, registered with Instagram. @@ -58,10 +60,9 @@ INSTAGRAMKIT_EXTERN NSString *const kInstagramAppRedirectURLConfigurationKey; /*! - @typedef InstagrmaKitLoginScope enum + @typedef InstagrmaKitLoginScope enum - @abstract - Passed to indicate the scope of the access you are requesting from the user. + @abstract Passed to indicate the scope of the access you are requesting from the user. @discussion All apps have basic read access by default, but if you plan on asking for extended access such as liking, commenting, or managing friendships, you need to specify these scopes in your authorization request. @@ -83,17 +84,28 @@ typedef NS_ENUM(NSUInteger, InstagramKitLoginScope) /*! - @abstract The error domain for all errors from InstagramKit. - @discussion Error codes in the range 0-99 are reserved for this domain. + @abstract The notification posted on changing the authentication token. */ +INSTAGRAMKIT_EXTERN NSString *const InstagtamKitUserAuthenticationChangedNotification; + +/*! + @abstract The error domain for all errors from InstagramKit. + @discussion Error codes in the range 0-99 are reserved for this domain. + */ INSTAGRAMKIT_EXTERN NSString *const InstagtamKitErrorDomain; + /*! - @typedef NS_ENUM(NSInteger, InstagtamKitErrorCode) - @abstract Error codes for InstagtamKitErrorDomain. + @abstract The Keychain Store service from InstagramKit to securely store credentials. */ +INSTAGRAMKIT_EXTERN NSString *const InstagtamKitKeychainStore; + +/*! + @typedef NS_ENUM(NSInteger, InstagtamKitErrorCode) + @abstract Error codes for InstagtamKitErrorDomain. + */ typedef NS_ENUM(NSInteger, InstagtamKitErrorCode) { /*! @@ -118,21 +130,107 @@ typedef NS_ENUM(NSInteger, InstagtamKitErrorCode) @class InstagramPaginationInfo; @class InstagramTag; @class InstagramLocation; +@class InstagramModel; -typedef void (^InstagramLoginBlock)(NSError *error); -typedef void (^InstagramUserBlock)(InstagramUser *user); -typedef void (^InstagramMediaDetailBlock)(InstagramMedia *media); +/** + * A generic block used as a callback for receiving a collection of objects. + * + * @param paginatedObjects Array of Instagram model objects. + * @param paginationInfo A PaginationInfo object. + */ +typedef void (^InstagramPaginatiedResponseBlock)(NSArray *paginatedObjects, InstagramPaginationInfo *paginationInfo); + +/** + * A generic block used as a callback for receiving a single object. + * + * @param model An Instagram model object. + */ +typedef void (^InstagramObjectBlock)(id object); + +/** + * A callback block providing a collection of Media objects. + * + * @param media An array of InstagramMedia objects. + * @param paginationInfo A PaginationInfo object. + */ typedef void (^InstagramMediaBlock)(NSArray *media, InstagramPaginationInfo *paginationInfo); -typedef void (^InstagramObjectsBlock)(NSArray *objects, InstagramPaginationInfo *paginationInfo); + +/** + * A callback block providing a collection of User objects. + * + * @param users An array of User objects. + * @param paginationInfo A PaginationInfo object. + */ +typedef void (^InstagramUsersBlock)(NSArray *users, InstagramPaginationInfo *paginationInfo); + +/** + * A callback block providing a collection of Location objects. + * + * @param locations An array of InstagramLocation objects. + * @param paginationInfo A PaginationInfo object. + */ +typedef void (^InstagramLocationsBlock)(NSArray *locations, InstagramPaginationInfo *paginationInfo); + +/** + * A callback block providing a collection of Comment objects. + * + * @param comments An array of InstagramComment objects. + * @param paginationInfo A PaginationInfo object. + */ +typedef void (^InstagramCommentsBlock)(NSArray *comments, InstagramPaginationInfo *paginationInfo); + +/** + * A callback block providing a collection of Tag objects. + * + * @param tags An array of Tag objects. + * @param paginationInfo A PaginationInfo object. + */ typedef void (^InstagramTagsBlock)(NSArray *tags, InstagramPaginationInfo *paginationInfo); + +/** + * A callback block providing a User object. + * + * @param user An InstagramUser object. + */ +typedef void (^InstagramUserBlock)(InstagramUser *user); + +/** + * A callback block providing a Media object. + * + * @param media An InstagraMedia object. + */ +typedef void (^InstagramMediaObjectBlock)(InstagramMedia *media); + +/** + * A callback block providing a Tag object. + * + * @param tag An InstagramTag object. + */ typedef void (^InstagramTagBlock)(InstagramTag *tag); -typedef void (^InstagramCommentsBlock)(NSArray *comments); -typedef void (^InstagramUsersBlock)(NSArray *users, InstagramPaginationInfo *paginationInfo); -typedef void (^InstagramResponseBlock)(NSDictionary *serverResponse); -typedef void (^InstagramFailureBlock)(NSError* error, NSInteger serverStatusCode); -typedef void (^InstagramLocationsBlock)(NSArray *locations); + +/** + * A callback block providing a Location object. + * + * @param location An InstagramLocation object. + */ typedef void (^InstagramLocationBlock)(InstagramLocation *location); +/** + * A generic failure block for handling server errors. + * + * @param error + * @param serverStatusCode + */ +typedef void (^InstagramFailureBlock)(NSError* error, NSInteger serverStatusCode); + +/** + * A generic response block providing the server response dictionary, as is. + * + * @param serverResponse Response JSON in dictionary form. + */ +typedef void (^InstagramResponseBlock)(NSDictionary *serverResponse); + + /** * String constants as represented in JSON. */ diff --git a/InstagramKit/InstagramKitConstants.m b/InstagramKit/InstagramKitConstants.m index 7128211b..fc96706d 100644 --- a/InstagramKit/InstagramKitConstants.m +++ b/InstagramKit/InstagramKitConstants.m @@ -20,15 +20,17 @@ #import "InstagramKitConstants.h" -NSString *const kInstagramKitBaseUrlConfigurationKey = @"InstagramKitBaseUrl"; -NSString *const kInstagramKitAuthorizationUrlConfigurationKey = @"InstagramKitAuthorizationUrl"; -NSString *const kInstagramKitBaseUrl = @"https://api.instagram.com/v1/"; -NSString *const kInstagramKitAuthorizationUrl = @"https://api.instagram.com/oauth/authorize/"; +NSString *const kInstagramKitBaseURLConfigurationKey = @"InstagramKitBaseUrl"; +NSString *const kInstagramKitAuthorizationURLConfigurationKey = @"InstagramKitAuthorizationUrl"; +NSString *const kInstagramKitBaseURL = @"https://api.instagram.com/v1/"; +NSString *const kInstagramKitAuthorizationURL = @"https://api.instagram.com/oauth/authorize/"; NSString *const kInstagramAppClientIdConfigurationKey = @"InstagramAppClientId"; NSString *const kInstagramAppRedirectURLConfigurationKey = @"InstagramAppRedirectURL"; +NSString *const InstagtamKitUserAuthenticationChangedNotification = @"com.instagramkit.token.change"; NSString *const InstagtamKitErrorDomain = @"com.instagramkit"; +NSString *const InstagtamKitKeychainStore = @"com.instagramkit.secure"; NSString *const kKeyClientID = @"client_id"; NSString *const kKeyAccessToken = @"access_token"; diff --git a/InstagramKit/Models/InstagramComment.m b/InstagramKit/Models/InstagramComment.m index 494bb485..c317c0d8 100644 --- a/InstagramKit/Models/InstagramComment.m +++ b/InstagramKit/Models/InstagramComment.m @@ -49,7 +49,7 @@ + (BOOL)supportsSecureCoding - (id)initWithCoder:(NSCoder *)decoder { - if ((self = [self init])) { + if ((self = [super initWithCoder:decoder])) { _user = [decoder decodeObjectOfClass:[InstagramUser class] forKey:kCreator]; _text = [decoder decodeObjectOfClass:[NSString class] forKey:kText]; _createdDate = [decoder decodeObjectOfClass:[NSDate class] forKey:kCreatedDate]; @@ -59,6 +59,8 @@ - (id)initWithCoder:(NSCoder *)decoder - (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeObject:_user forKey:kCreator]; [encoder encodeObject:_text forKey:kText]; [encoder encodeObject:_createdDate forKey:kCreatedDate]; diff --git a/InstagramKit/Models/InstagramLocation.m b/InstagramKit/Models/InstagramLocation.m index 877191aa..03b32e04 100644 --- a/InstagramKit/Models/InstagramLocation.m +++ b/InstagramKit/Models/InstagramLocation.m @@ -54,7 +54,7 @@ + (BOOL)supportsSecureCoding - (id)initWithCoder:(NSCoder *)decoder { - if ((self = [self init])) { + if ((self = [super initWithCoder:decoder])) { CLLocationCoordinate2D coordinates; coordinates.latitude = [decoder decodeDoubleForKey:kLocationLatitude]; coordinates.longitude = [decoder decodeDoubleForKey:kLocationLongitude]; @@ -66,6 +66,8 @@ - (id)initWithCoder:(NSCoder *)decoder - (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeDouble:_coordinates.latitude forKey:kLocationLatitude]; [encoder encodeDouble:_coordinates.longitude forKey:kLocationLongitude]; [encoder encodeObject:_name forKey:kLocationName]; diff --git a/InstagramKit/Models/InstagramMedia.m b/InstagramKit/Models/InstagramMedia.m index 4396cf53..7d4d96e7 100755 --- a/InstagramKit/Models/InstagramMedia.m +++ b/InstagramKit/Models/InstagramMedia.m @@ -121,7 +121,7 @@ + (BOOL)supportsSecureCoding - (id)initWithCoder:(NSCoder *)decoder { - if ((self = [self init])) { + if ((self = [super initWithCoder:decoder])) { _user = [decoder decodeObjectOfClass:[InstagramUser class] forKey:kUser]; _userHasLiked = [decoder decodeBoolForKey:kUserHasLiked]; _createdDate = [decoder decodeObjectOfClass:[NSDate class] forKey:kCreatedDate]; @@ -165,6 +165,8 @@ - (id)initWithCoder:(NSCoder *)decoder - (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeObject:_user forKey:kUser]; [encoder encodeBool:_userHasLiked forKey:kUserHasLiked]; [encoder encodeObject:_createdDate forKey:kCreatedDate]; diff --git a/InstagramKit/Models/InstagramUser.m b/InstagramKit/Models/InstagramUser.m index 8904379d..23224ff8 100644 --- a/InstagramKit/Models/InstagramUser.m +++ b/InstagramKit/Models/InstagramUser.m @@ -75,7 +75,7 @@ + (BOOL)supportsSecureCoding - (id)initWithCoder:(NSCoder *)decoder { - if ((self = [self init])) { + if ((self = [super initWithCoder:decoder])) { _username = [decoder decodeObjectOfClass:[NSString class] forKey:kUsername]; _fullName = [decoder decodeObjectOfClass:[NSString class] forKey:kFullName]; _profilePictureURL = [decoder decodeObjectOfClass:[NSString class] forKey:kProfilePictureURL]; @@ -87,6 +87,8 @@ - (id)initWithCoder:(NSCoder *)decoder - (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeObject:_username forKey:kUsername]; [encoder encodeObject:_fullName forKey:kFullName]; [encoder encodeObject:_profilePictureURL forKey:kProfilePictureURL]; diff --git a/InstagramKitTests/Categories/InstagramEngine+Internal.h b/InstagramKitTests/Categories/InstagramEngine+Internal.h new file mode 100644 index 00000000..416e585c --- /dev/null +++ b/InstagramKitTests/Categories/InstagramEngine+Internal.h @@ -0,0 +1,66 @@ +// +// Copyright (c) 2015 Shyam Bhat +// +// 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 "InstagramEngine.h" + +@interface InstagramEngine (Internal) + +- (NSURL *)authorizarionURLForScope:(InstagramKitLoginScope)scope; + +- (BOOL)receivedValidAccessTokenFromURL:(NSURL *)url + error:(NSError *__autoreleasing *)error; + +- (void)logout; + +- (NSString *)stringForScope:(InstagramKitLoginScope)scope; + +- (NSDictionary *)queryStringParametersFromString:(NSString*)string; + + + +- (void)getPath:(NSString *)path + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramObjectBlock)success + failure:(InstagramFailureBlock)failure; + + +- (void)getPaginatedPath:(NSString *)path + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramPaginatiedResponseBlock)success + failure:(InstagramFailureBlock)failure; + + +- (void)postPath:(NSString *)path + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramResponseBlock)success + failure:(InstagramFailureBlock)failure; + + +- (void)deletePath:(NSString *)path + parameters:(NSDictionary *)parameters + responseModel:(Class)modelClass + success:(InstagramResponseBlock)success + failure:(InstagramFailureBlock)failure; + + +@end diff --git a/InstagramKitTests/Info.plist b/InstagramKitTests/Info.plist new file mode 100644 index 00000000..31db8616 --- /dev/null +++ b/InstagramKitTests/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.InstagramKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + InstagramAppClientId + 830f11f01cb34d6ab29666a7ea1052ac + InstagramAppRedirectURL + http://test.com + CFBundleVersion + 1 + + diff --git a/InstagramKitTests/InstagramKitTests.m b/InstagramKitTests/InstagramKitTests.m new file mode 100644 index 00000000..caa7869f --- /dev/null +++ b/InstagramKitTests/InstagramKitTests.m @@ -0,0 +1,130 @@ +// +// Copyright (c) 2015 Shyam Bhat +// +// 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 +#import +#import "InstagramKit.h" +#import "InstagramEngine+Internal.h" + +#define kTestRequestTimeout 15 + +static NSString *const kTestAccessToken = @"InstagramKitBaseUrl"; + +@interface InstagramKitTests : XCTestCase + +@property (nonatomic, strong) InstagramEngine *engine; + +@end + +@implementation InstagramKitTests + +- (void)setUp { + [super setUp]; + self.engine = [InstagramEngine sharedEngine]; +} + +- (void)tearDown { + self.engine = nil; +} + + +- (void)testInitialization { + + InstagramEngine *testEngine = [InstagramEngine sharedEngine]; + XCTAssert(testEngine, @"Pass"); + + NSDictionary *info = [[NSBundle bundleForClass:[self class]] infoDictionary]; + XCTAssertEqual(testEngine.appClientID,info[kInstagramAppClientIdConfigurationKey]); + XCTAssert(testEngine.appClientID); + + XCTAssertEqual(testEngine.appRedirectURL,info[kInstagramAppRedirectURLConfigurationKey]); + XCTAssert(testEngine.appRedirectURL); +} + + +- (void)testUnauthorizedGetMediaRequest +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"completed request"]; + Class modelClass = [InstagramMedia class]; + self.engine.accessToken = nil; + + [self.engine getPath:@"media/1032802639895336381_1194245772" + parameters:nil + responseModel:modelClass + success:^(id object) { + XCTAssertNotNil(object); + XCTAssertTrue([object isKindOfClass:modelClass]); + [expectation fulfill]; + } + failure:nil]; + + [self waitForExpectationsWithTimeout:kTestRequestTimeout + handler:^(NSError *error) { + XCTAssertNil(error, @"expectation not fulfilled: %@", error); + }]; +} + + +- (void)testUnauthorizedGetUserRequest +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"completed request"]; + Class modelClass = [InstagramUser class]; + self.engine.accessToken = nil; + + [self.engine getPath:@"users/1194245772" + parameters:nil + responseModel:modelClass + success:^(id object) { + XCTAssertNotNil(object); + XCTAssertTrue([object isKindOfClass:modelClass]); + [expectation fulfill]; + } + failure:nil]; + + [self waitForExpectationsWithTimeout:kTestRequestTimeout + handler:^(NSError *error) { + XCTAssertNil(error, @"expectation not fulfilled: %@", error); + }]; +} + + +- (void)testUnauthorizedPaginatedRequest +{ + XCTestExpectation *expectation = [self expectationWithDescription:@"completed request"]; + Class modelClass = [InstagramMedia class]; + self.engine.accessToken = nil; + [self.engine getPaginatedPath:@"media/popular" + parameters:nil + responseModel:modelClass + success:^(NSArray *paginatedObjects, InstagramPaginationInfo *paginationInfo) { + XCTAssertNotNil(paginatedObjects); + XCTAssertTrue([paginatedObjects[0] isKindOfClass:modelClass]); + [expectation fulfill]; + } + failure:nil]; + + [self waitForExpectationsWithTimeout:kTestRequestTimeout + handler:^(NSError *error) { + XCTAssertNil(error, @"expectation not fulfilled: %@", error); + }]; +} + + +@end diff --git a/Podfile b/Podfile index 21780a97..b2cc3172 100644 --- a/Podfile +++ b/Podfile @@ -1,3 +1,3 @@ -platform :ios, "6.0" +platform :ios, "7.0" pod 'AFNetworking', '~>2.0' diff --git a/README.md b/README.md index ae64d3b5..5ad609f6 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,20 @@ Getting started is easy. Just include the files from the directory 'InstagramKit ```ruby pod 'InstagramKit', '~> 3.0' ``` +If your App uses authorization and you'd like the storage and retrieval of the access token to and from the Keychain to be automatically handled for you by InstagramKit, include the following pods instead - + +```ruby +pod 'InstagramKit', '~> 3.0' +pod 'InstagramKit/UICKeyChainStore', '~> 2.0' +``` + +InstagramKit uses [UICKeyChainStore](https://github.com/kishikawakatsumi/UICKeyChainStore) as an optional sub-dependency for Keychain access. +If you opt to use the optional pod, InstagramKit resumes your authenticated sessions across App launches, without needing any additional code. ####Compatibility -Earliest supported deployment target = iOS 6 +Earliest supported deployment target = iOS 7.0 -####Instagram Developer Registration +#### Instagram Developer Registration Head over to http://instagram.com/developer/clients/manage/ to register your app with Instagram and set the right credentials for ```InstagramAppClientId``` and ```InstagramAppRedirectURL``` in your App's Info.plist file. ```InstagramAppClientId``` is your App's Client Id and ```InstagramAppRedirectURL```, the redirect URI which is obtained on registering your App on Instagram's Developer Dashboard. @@ -46,12 +55,16 @@ The redirect URI specifies where Instagram should redirect users after they have In order to make Authenticated calls to the API, you need an Access Token and often times a User ID. To get your Access Token, the user needs to authenticate your app to access his Instagram account. -To do so, redirect the user to -```https://instagram.com/oauth/authorize/?client_id=[Client ID]&redirect_uri=[Redirect URI]&response_type=token``` +To do so, redirect the user to ```https://instagram.com/oauth/authorize/?client_id=[Client ID]&redirect_uri=[Redirect URI]&response_type=token``` +or allow InstagramKitEngine's helper method do the hard work for you - +```Objective-C +NSURL *authURL = [[InstagramEngine sharedEngine] authorizarionURL]; +[self.webView loadRequest:[NSURLRequest requestWithURL:authURL]]; +``` -#### Scope -All apps have basic read access by default, but if you plan on asking for extended access such as liking, commenting, or managing friendships, you need to specify these scopes in your authorization request using the IKLoginScope enum. +#### Scopes +All apps have basic read access by default, but if you plan on asking for extended access such as liking, commenting, or managing friendships, you need to specify these scopes in your authorization request using the InstagramKitScope enum. _Note that in order to use these extended permissions, first you need to submit your app for review to Instagram._ @@ -59,12 +72,12 @@ _For your app to POST or DELETE likes, comments or follows, you must apply to In ```Objective-C // Set scope depending on permissions your App has been granted from Instagram -// IKLoginScopeBasic is included by default. +// InstagramKitScopeBasic is included by default. -IKLoginScope scope = IKLoginScopeRelationships | IKLoginScopeComments | IKLoginScopeLikes; +InstagramKitScope scope = InstagramKitScopeRelationships | InstagramKitScopeComments | InstagramKitScopeLikes; NSURL *authURL = [[InstagramEngine sharedEngine] authorizarionURLForScope:scope]; -[mWebView loadRequest:[NSURLRequest requestWithURL:authURL]]; +[self.webView loadRequest:[NSURLRequest requestWithURL:authURL]]; ``` Once the user grants your app permission, they will be redirected to a url in the form of something like ```http://localhost/#access_token=[access_token]``` and ```[access_token]``` will be split by a period like ```[userID].[rest of access token]```. @@ -75,7 +88,7 @@ InstagramKit includes a helper method to validate this token. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSError *error; - if ([[InstagramEngine sharedEngine] receivedValidAccessTokenWithURL:request.URL error:&error]) { + if ([[InstagramEngine sharedEngine] receivedValidAccessTokenFromURL:request.URL error:&error]) { if (!error) { //success! ...