+
+/*!
+ @protocol
+
+ @abstract
+ The `FBGraphObject` protocol is the base protocol which enables typed access to graph objects and
+ open graph objects. Inherit from this protocol or a sub-protocol in order to introduce custom types
+ for typed access to Facebook objects.
+
+ @discussion
+ The `FBGraphObject` protocol is the core type used by the Facebook SDK for iOS to
+ represent objects in the Facebook Social Graph and the Facebook Open Graph (OG).
+ The `FBGraphObject` class implements useful default functionality, but is rarely
+ used directly by applications. The `FBGraphObject` protocol, in contrast is the
+ base protocol for all graph object access via the SDK.
+
+ Goals of the FBGraphObject types:
+
+ - Lightweight/maintainable/robust
+ - Extensible and resilient to change, both by Facebook and third party (OG)
+ - Simple and natural extension to Objective-C
+
+
+ The FBGraphObject at its core is a duck typed (if it walks/swims/quacks...
+ its a duck) model which supports an optional static facade. Duck-typing achieves
+ the flexibility necessary for Social Graph and OG uses, and the static facade
+ increases discoverability, maintainability, robustness and simplicity.
+ The following excerpt from the PlacePickerSample shows a simple use of the
+ a facade protocol `FBGraphPlace` by an application:
+
+
+ ‐ (void)placePickerViewControllerSelectionDidChange:(FBPlacePickerViewController *)placePicker
+ {
+ id<FBGraphPlace> place = placePicker.selection;
+
+ // we'll use logging to show the simple typed property access to place and location info
+ NSLog(@"place=%@, city=%@, state=%@, lat long=%@ %@",
+ place.name,
+ place.location.city,
+ place.location.state,
+ place.location.latitude,
+ place.location.longitude);
+ }
+
+
+ Note that in this example, access to common place information is available through typed property
+ syntax. But if at some point places in the Social Graph supported additional fields "foo" and "bar", not
+ reflected in the `FBGraphPlace` protocol, the application could still access the values like so:
+
+
+ NSString *foo = [place objectForKey:@"foo"]; // perhaps located at the ... in the preceding example
+ NSNumber *bar = [place objectForKey:@"bar"]; // extensibility applies to Social and Open graph uses
+
+
+ In addition to untyped access, applications and future revisions of the SDK may add facade protocols by
+ declaring a protocol inheriting the `FBGraphObject` protocol, like so:
+
+
+ @protocol MyGraphThing<FBGraphObject>
+ @property (copy, nonatomic) NSString *id;
+ @property (copy, nonatomic) NSString *name;
+ @end
+
+
+ Important: facade implementations are inferred by graph objects returned by the methods of the SDK. This
+ means that no explicit implementation is required by application or SDK code. Any `FBGraphObject` instance
+ may be cast to any `FBGraphObject` facade protocol, and accessed via properties. If a field is not present
+ for a given facade property, the property will return nil.
+
+ The following layer diagram depicts some of the concepts discussed thus far:
+
+
+ *-------------* *------------* *-------------**--------------------------*
+ Facade --> | FBGraphUser | |FBGraphPlace| | MyGraphThing|| MyGraphPersonExtentension| ...
+ *-------------* *------------* *-------------**--------------------------*
+ *------------------------------------* *--------------------------------------*
+ Transparent impl --> | FBGraphObject (instances) | | CustomClass<FBGraphObject> |
+ *------------------------------------* *--------------------------------------*
+ *-------------------**------------------------* *-----------------------------*
+ Apparent impl --> |NSMutableDictionary||FBGraphObject (protocol)| |FBGraphObject (class methods)|
+ *-------------------**------------------------* *-----------------------------*
+
+
+ The *Facade* layer is meant for typed access to graph objects. The *Transparent impl* layer (more
+ specifically, the instance capabilities of `FBGraphObject`) are used by the SDK and app logic
+ internally, but are not part of the public interface between application and SDK. The *Apparent impl*
+ layer represents the lower-level "duck-typed" use of graph objects.
+
+ Implementation note: the SDK returns `NSMutableDictionary` derived instances with types declared like
+ one of the following:
+
+
+ NSMutableDictionary<FBGraphObject> *obj; // no facade specified (still castable by app)
+ NSMutableDictionary<FBGraphPlace> *person; // facade specified when possible
+
+
+ However, when passing a graph object to the SDK, `NSMutableDictionary` is not assumed; only the
+ FBGraphObject protocol is assumed, like so:
+
+
+ id<FBGraphObject> anyGraphObj;
+
+
+ As such, the methods declared on the `FBGraphObject` protocol represent the methods used by the SDK to
+ consume graph objects. While the `FBGraphObject` class implements the full `NSMutableDictionary` and KVC
+ interfaces, these are not consumed directly by the SDK, and are optional for custom implementations.
+ */
+@protocol FBGraphObject
+
+/*!
+ @method
+ @abstract
+ Returns the number of properties on this `FBGraphObject`.
+ */
+- (NSUInteger)count;
+/*!
+ @method
+ @abstract
+ Returns a property on this `FBGraphObject`.
+
+ @param aKey name of the property to return
+ */
+- (id)objectForKey:(id)aKey;
+/*!
+ @method
+ @abstract
+ Returns an enumerator of the property naems on this `FBGraphObject`.
+ */
+- (NSEnumerator *)keyEnumerator;
+/*!
+ @method
+ @abstract
+ Removes a property on this `FBGraphObject`.
+
+ @param aKey name of the property to remove
+ */
+- (void)removeObjectForKey:(id)aKey;
+/*!
+ @method
+ @abstract
+ Sets the value of a property on this `FBGraphObject`.
+
+ @param anObject the new value of the property
+ @param aKey name of the property to set
+ */
+- (void)setObject:(id)anObject forKey:(id)aKey;
+
+@end
+
+/*!
+ @class
+
+ @abstract
+ Static class with helpers for use with graph objects
+
+ @discussion
+ The public interface of this class is useful for creating objects that have the same graph characteristics
+ of those returned by methods of the SDK. This class also represents the internal implementation of the
+ `FBGraphObject` protocol, used by the Facebook SDK. Application code should not use the `FBGraphObject` class to
+ access instances and instance members, favoring the protocol.
+ */
+@interface FBGraphObject : NSMutableDictionary
+
+/*!
+ @method
+ @abstract
+ Used to create a graph object for, usually for use in posting a new graph object or action
+ */
++ (NSMutableDictionary*)graphObject;
+
+/*!
+ @method
+ @abstract
+ Used to wrap an existing dictionary with a `FBGraphObject` facade
+
+ @discussion
+ Normally you will not need to call this method, as the Facebook SDK already "FBGraphObject-ifys" json objects
+ fetch via `FBRequest` and `FBRequestConnection`. However, you may have other reasons to create json objects in your
+ application, which you would like to treat as a graph object. The pattern for doing this is that you pass the root
+ node of the json to this method, to retrieve a wrapper. From this point, if you traverse the graph, any other objects
+ deeper in the hierarchy will be wrapped as `FBGraphObject`'s in a lazy fashion.
+
+ This method is designed to avoid unnecessary memory allocations, and object copying. Due to this, the method does
+ not copy the source object if it can be avoided, but rather wraps and uses it as is. The returned object derives
+ callers shoudl use the returned object after calls to this method, rather than continue to call methods on the original
+ object.
+
+ @param jsonDictionary the dictionary representing the underlying object to wrap
+ */
++ (NSMutableDictionary*)graphObjectWrappingDictionary:(NSDictionary*)jsonDictionary;
+
+/*!
+ @method
+ @abstract
+ Used to compare two `FBGraphObject`s to determine if represent the same object. We do not overload
+ the concept of equality as there are various types of equality that may be important for an `FBGraphObject`
+ (for instance, two different `FBGraphObject`s could represent the same object, but contain different
+ subsets of fields).
+
+ @param anObject an `FBGraphObject` to test
+
+ @param anotherObject the `FBGraphObject` to compare it against
+ */
++ (BOOL)isGraphObjectID:(id)anObject sameAs:(id)anotherObject;
+
+
+@end
diff --git a/src/ios/facebook/FBGraphObject.m b/src/ios/facebook/FBGraphObject.m
new file mode 100644
index 000000000..446642af7
--- /dev/null
+++ b/src/ios/facebook/FBGraphObject.m
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBGraphObject.h"
+#import
+
+// Module Summary:
+// this is the module that does the heavy lifting to implement the public-facing
+// developer-model described in FBGraphObject.h, namely typed protocol
+// accessors over "duck-typed" dictionaries, for interating with Facebook Graph and
+// Open Graph objects and actions
+//
+// Message forwarding is used as described here:
+// https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html
+// in order to infer implementations for property getter
+// and setter selectors, backed by objectForKey: and setObject:forKey: respectively
+//
+// The basic flow is straightforward, though there are a number of details;
+// The flow is:
+// * System doesn't recognize a selector
+// * It calls methodSignatureForSelector:
+// * We return a new signature if we intend to handle the selector
+// * The system populates the invoke object with the old selector and the new signature
+// * The system passes the invocation to forwardInvocation:
+// * We swap out selectors and invoke
+//
+// Additional details include, deferred wrapping of objects as they are fetched by callers,
+// implementations for common methods such as respondsToSelector and conformsToProtocol, as
+// suggested in the previously referenced documentation
+
+static NSString *const FBIsGraphObjectKey = @"com.facebook.FBIsGraphObjectKey";
+
+// used internally by the category impl
+typedef enum _SelectorInferredImplType {
+ SelectorInferredImplTypeNone = 0,
+ SelectorInferredImplTypeGet = 1,
+ SelectorInferredImplTypeSet = 2
+} SelectorInferredImplType;
+
+
+// internal-only wrapper
+@interface FBGraphObjectArray : NSMutableArray
+
+- (id)initWrappingArray:(NSArray *)otherArray;
+- (id)graphObjectifyAtIndex:(NSUInteger)index;
+- (void)graphObjectifyAll;
+
+@end
+
+
+@interface FBGraphObject ()
+
+- (id)initWrappingDictionary:(NSDictionary *)otherDictionary;
+- (void)graphObjectifyAll;
+- (id)graphObjectifyAtKey:(id)key;
+
++ (id)graphObjectWrappingObject:(id)originalObject;
++ (SelectorInferredImplType)inferredImplTypeForSelector:(SEL)sel;
++ (BOOL)isProtocolImplementationInferable:(Protocol *)protocol checkFBGraphObjectAdoption:(BOOL)checkAdoption;
+
+@end
+
+@implementation FBGraphObject {
+ NSMutableDictionary *_jsonObject;
+}
+
+#pragma mark Lifecycle
+
+- (id)initWrappingDictionary:(NSDictionary *)jsonObject {
+ self = [super init];
+ if (self) {
+ if ([jsonObject isKindOfClass:[FBGraphObject class]]) {
+ // in this case, we prefer to return the original object,
+ // rather than allocate a wrapper
+
+ // we are about to return this, better make it the caller's
+ [jsonObject retain];
+
+ // we don't need self after all
+ [self release];
+
+ // no wrapper needed, returning the object that was provided
+ return (FBGraphObject*)jsonObject;
+ } else {
+ _jsonObject = [[NSMutableDictionary dictionaryWithDictionary:jsonObject] retain];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [_jsonObject release];
+ [super dealloc];
+}
+
+#pragma mark -
+#pragma mark Public Members
+
++ (NSMutableDictionary*)graphObject {
+ return [FBGraphObject graphObjectWrappingDictionary:[NSMutableDictionary dictionary]];
+}
+
++ (NSMutableDictionary*)graphObjectWrappingDictionary:(NSDictionary*)jsonDictionary {
+ return [FBGraphObject graphObjectWrappingObject:jsonDictionary];
+}
+
++ (BOOL)isGraphObjectID:(id)anObject sameAs:(id)anotherObject {
+ if (anObject != nil &&
+ anObject == anotherObject) {
+ return YES;
+ }
+ id anID = [anObject objectForKey:@"id"];
+ id anotherID = [anotherObject objectForKey:@"id"];
+ if ([anID isKindOfClass:[NSString class]] &&
+ [anotherID isKindOfClass:[NSString class]]) {
+ return [(NSString*)anID isEqualToString:anotherID];
+ }
+ return NO;
+}
+
+#pragma mark -
+#pragma mark NSObject overrides
+
+// make the respondsToSelector method do the right thing for the selectors we handle
+- (BOOL)respondsToSelector:(SEL)sel
+{
+ return [super respondsToSelector:sel] ||
+ ([FBGraphObject inferredImplTypeForSelector:sel] != SelectorInferredImplTypeNone);
+}
+
+- (BOOL)conformsToProtocol:(Protocol *)protocol {
+ return [super conformsToProtocol:protocol] ||
+ ([FBGraphObject isProtocolImplementationInferable:protocol
+ checkFBGraphObjectAdoption:YES]);
+}
+
+// returns the signature for the method that we will actually invoke
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
+ SEL alternateSelector = sel;
+
+ // if we should forward, to where?
+ switch ([FBGraphObject inferredImplTypeForSelector:sel]) {
+ case SelectorInferredImplTypeGet:
+ alternateSelector = @selector(objectForKey:);
+ break;
+ case SelectorInferredImplTypeSet:
+ alternateSelector = @selector(setObject:forKey:);
+ break;
+ case SelectorInferredImplTypeNone:
+ default:
+ break;
+ }
+
+ return [super methodSignatureForSelector:alternateSelector];
+}
+
+// forwards otherwise missing selectors that match the FBGraphObject convention
+- (void)forwardInvocation:(NSInvocation *)invocation {
+ // if we should forward, to where?
+ switch ([FBGraphObject inferredImplTypeForSelector:[invocation selector]]) {
+ case SelectorInferredImplTypeGet: {
+ // property getter impl uses the selector name as an argument...
+ NSString *propertyName = NSStringFromSelector([invocation selector]);
+ [invocation setArgument:&propertyName atIndex:2];
+ //... to the replacement method objectForKey:
+ invocation.selector = @selector(objectForKey:);
+ [invocation invokeWithTarget:self];
+ break;
+ }
+ case SelectorInferredImplTypeSet: {
+ // property setter impl uses the selector name as an argument...
+ NSMutableString *propertyName = [NSMutableString stringWithString:NSStringFromSelector([invocation selector])];
+ // remove 'set' and trailing ':', and lowercase the new first character
+ [propertyName deleteCharactersInRange:NSMakeRange(0, 3)]; // "set"
+ [propertyName deleteCharactersInRange:NSMakeRange(propertyName.length - 1, 1)]; // ":"
+
+ NSString *firstChar = [[propertyName substringWithRange:NSMakeRange(0,1)] lowercaseString];
+ [propertyName replaceCharactersInRange:NSMakeRange(0, 1) withString:firstChar];
+ // the object argument is already in the right place (2), but we need to set the key argument
+ [invocation setArgument:&propertyName atIndex:3];
+ // and replace the missing method with setObject:forKey:
+ invocation.selector = @selector(setObject:forKey:);
+ [invocation invokeWithTarget:self];
+ break;
+ }
+ case SelectorInferredImplTypeNone:
+ default:
+ [super forwardInvocation:invocation];
+ return;
+ }
+}
+
+- (id)graphObjectifyAtKey:(id)key {
+ id object = [_jsonObject objectForKey:key];
+ // make certain it is FBObjectGraph-ified
+ id possibleReplacement = [FBGraphObject graphObjectWrappingObject:object];
+ if (object != possibleReplacement) {
+ // and if not-yet, replace the original with the wrapped object
+ [_jsonObject setObject:possibleReplacement forKey:key];
+ object = possibleReplacement;
+ }
+ return object;
+}
+
+- (void)graphObjectifyAll {
+ NSArray *keys = [_jsonObject allKeys];
+ for (NSString *key in keys) {
+ [self graphObjectifyAtKey:key];
+ }
+}
+
+
+#pragma mark -
+
+#pragma mark NSDictionary and NSMutableDictionary overrides
+
+- (NSUInteger)count {
+ return _jsonObject.count;
+}
+
+- (id)objectForKey:(id)key {
+ return [self graphObjectifyAtKey:key];
+}
+
+- (NSEnumerator *)keyEnumerator {
+ [self graphObjectifyAll];
+ return _jsonObject.keyEnumerator;
+}
+
+- (void)setObject:(id)object forKey:(id)key {
+ return [_jsonObject setObject:object forKey:key];
+}
+
+- (void)removeObjectForKey:(id)key {
+ return [_jsonObject removeObjectForKey:key];
+}
+
+#pragma mark -
+#pragma mark Public Members
+
+#pragma mark -
+#pragma mark Private Class Members
+
++ (id)graphObjectWrappingObject:(id)originalObject {
+ // non-array and non-dictionary case, returns original object
+ id result = originalObject;
+
+ // array and dictionary wrap
+ if ([originalObject isKindOfClass:[NSDictionary class]]) {
+ result = [[[FBGraphObject alloc] initWrappingDictionary:originalObject] autorelease];
+ } else if ([originalObject isKindOfClass:[NSArray class]]) {
+ result = [[[FBGraphObjectArray alloc] initWrappingArray:originalObject] autorelease];
+ }
+
+ // return our object
+ return result;
+}
+
+// helper method used by the catgory implementation to determine whether a selector should be handled
++ (SelectorInferredImplType)inferredImplTypeForSelector:(SEL)sel {
+ // the overhead in this impl is high relative to the cost of a normal property
+ // accessor; if needed we will optimize by caching results of the following
+ // processing, indexed by selector
+
+ NSString *selectorName = NSStringFromSelector(sel);
+ int parameterCount = [[selectorName componentsSeparatedByString:@":"] count]-1;
+ // we will process a selector as a getter if paramCount == 0
+ if (parameterCount == 0) {
+ return SelectorInferredImplTypeGet;
+ // otherwise we consider a setter if...
+ } else if (parameterCount == 1 && // ... we have the correct arity
+ [selectorName hasPrefix:@"set"] && // ... we have the proper prefix
+ selectorName.length > 4) { // ... there are characters other than "set" & ":"
+ return SelectorInferredImplTypeSet;
+ }
+
+ return SelectorInferredImplTypeNone;
+}
+
++ (BOOL)isProtocolImplementationInferable:(Protocol*)protocol checkFBGraphObjectAdoption:(BOOL)checkAdoption {
+ // first handle base protocol questions
+ if (checkAdoption && !protocol_conformsToProtocol(protocol, @protocol(FBGraphObject))) {
+ return NO;
+ }
+
+ if ([protocol isEqual:@protocol(FBGraphObject)]) {
+ return YES; // by definition
+ }
+
+ unsigned int count = 0;
+ struct objc_method_description *methods = nil;
+
+ // then confirm that all methods are required
+ methods = protocol_copyMethodDescriptionList(protocol,
+ NO, // optional
+ YES, // instance
+ &count);
+ if (methods) {
+ free(methods);
+ return NO;
+ }
+
+ @try {
+ // fetch methods of the protocol and confirm that each can be implemented automatically
+ methods = protocol_copyMethodDescriptionList(protocol,
+ YES, // required
+ YES, // instance
+ &count);
+ for (int index = 0; index < count; index++) {
+ if ([FBGraphObject inferredImplTypeForSelector:methods[index].name] == SelectorInferredImplTypeNone) {
+ // we have a bad actor, short circuit
+ return NO;
+ }
+ }
+ } @finally {
+ if (methods) {
+ free(methods);
+ }
+ }
+
+ // fetch adopted protocols
+ Protocol **adopted = nil;
+ @try {
+ adopted = protocol_copyProtocolList(protocol, &count);
+ for (int index = 0; index < count; index++) {
+ // here we go again...
+ if (![FBGraphObject isProtocolImplementationInferable:adopted[index]
+ checkFBGraphObjectAdoption:NO]) {
+ return NO;
+ }
+ }
+ } @finally {
+ if (adopted) {
+ free(adopted);
+ }
+ }
+
+ // protocol ran the gauntlet
+ return YES;
+}
+
+#pragma mark -
+
+@end
+
+#pragma mark internal classes
+
+@implementation FBGraphObjectArray {
+ NSMutableArray *_jsonArray;
+}
+
+- (id)initWrappingArray:(NSArray *)jsonArray {
+ self = [super init];
+ if (self) {
+ if ([jsonArray isKindOfClass:[FBGraphObjectArray class]]) {
+ // in this case, we prefer to return the original object,
+ // rather than allocate a wrapper
+
+ // we are about to return this, better make it the caller's
+ [jsonArray retain];
+
+ // we don't need self after all
+ [self release];
+
+ // no wrapper needed, returning the object that was provided
+ return (FBGraphObjectArray*)jsonArray;
+ } else {
+ _jsonArray = [[NSMutableArray arrayWithArray:jsonArray] retain];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [_jsonArray release];
+ [super dealloc];
+}
+
+- (NSUInteger)count {
+ return _jsonArray.count;
+}
+
+- (id)graphObjectifyAtIndex:(NSUInteger)index {
+ id object = [_jsonArray objectAtIndex:index];
+ // make certain it is FBObjectGraph-ified
+ id possibleReplacement = [FBGraphObject graphObjectWrappingObject:object];
+ if (object != possibleReplacement) {
+ // and if not-yet, replace the original with the wrapped object
+ [_jsonArray replaceObjectAtIndex:index withObject:possibleReplacement];
+ object = possibleReplacement;
+ }
+ return object;
+}
+
+- (void)graphObjectifyAll {
+ int count = [_jsonArray count];
+ for (int i = 0; i < count; ++i) {
+ [self graphObjectifyAtIndex:i];
+ }
+}
+
+- (id)objectAtIndex:(NSUInteger)index {
+ return [self graphObjectifyAtIndex:index];
+}
+
+- (NSEnumerator *)objectEnumerator {
+ [self graphObjectifyAll];
+ return _jsonArray.objectEnumerator;
+}
+
+- (NSEnumerator *)reverseObjectEnumerator {
+ [self graphObjectifyAll];
+ return _jsonArray.reverseObjectEnumerator;
+}
+
+- (void)insertObject:(id)object atIndex:(NSUInteger)index {
+ [_jsonArray insertObject:object atIndex:index];
+}
+
+- (void)removeObjectAtIndex:(NSUInteger)index {
+ [_jsonArray removeObjectAtIndex:index];
+}
+
+- (void)addObject:(id)object {
+ [_jsonArray addObject:object];
+}
+
+- (void)removeLastObject {
+ [_jsonArray removeLastObject];
+}
+
+- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)object {
+ [_jsonArray replaceObjectAtIndex:index withObject:object];
+}
+
+@end
+
+#pragma mark -
diff --git a/src/ios/facebook/FBGraphObjectPagingLoader.h b/src/ios/facebook/FBGraphObjectPagingLoader.h
new file mode 100644
index 000000000..c79ded144
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectPagingLoader.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphObjectTableDataSource.h"
+
+@class FBSession;
+@class FBRequest;
+@protocol FBGraphObjectPagingLoaderDelegate;
+
+typedef enum {
+ // Paging links will be followed as soon as one set of results is loaded
+ FBGraphObjectPagingModeImmediate,
+ // Paging links will be followed as soon as one set of results is loaded, even without a view
+ FBGraphObjectPagingModeImmediateViewless,
+ // Paging links will be followed only when the user scrolls to the bottom of the table
+ FBGraphObjectPagingModeAsNeeded
+} FBGraphObjectPagingMode;
+
+@interface FBGraphObjectPagingLoader : NSObject
+
+@property (nonatomic, retain) UITableView *tableView;
+@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource;
+@property (nonatomic, retain) FBSession *session;
+@property (nonatomic, assign) id delegate;
+@property (nonatomic, readonly) FBGraphObjectPagingMode pagingMode;
+@property (nonatomic, readonly) BOOL isResultFromCache;
+
+- (id)initWithDataSource:(FBGraphObjectTableDataSource*)aDataSource
+ pagingMode:(FBGraphObjectPagingMode)pagingMode;
+- (void)startLoadingWithRequest:(FBRequest*)request
+ cacheIdentity:(NSString*)cacheIdentity
+ skipRoundtripIfCached:(BOOL)skipRoundtripIfCached;
+- (void)addResultsAndUpdateView:(NSDictionary*)results;
+- (void)cancel;
+- (void)reset;
+
+@end
+
+@protocol FBGraphObjectPagingLoaderDelegate
+
+@optional
+
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader willLoadURL:(NSString*)url;
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader didLoadData:(NSDictionary*)results;
+- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader*)pagingLoader;
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader handleError:(NSError*)error;
+- (void)pagingLoaderWasCancelled:(FBGraphObjectPagingLoader*)pagingLoader;
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectPagingLoader.m b/src/ios/facebook/FBGraphObjectPagingLoader.m
new file mode 100644
index 000000000..d81901b3f
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectPagingLoader.m
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBGraphObjectPagingLoader.h"
+#import "FBRequest.h"
+#import "FBError.h"
+#import "FBRequestConnection+Internal.h"
+
+@interface FBGraphObjectPagingLoader ()
+
+@property (nonatomic, retain) NSString *nextLink;
+@property (nonatomic, retain) FBRequestConnection *connection;
+@property (nonatomic, copy) NSString *cacheIdentity;
+@property (nonatomic, assign) BOOL skipRoundtripIfCached;
+@property (nonatomic) FBGraphObjectPagingMode pagingMode;
+
+- (void)followNextLink;
+- (void)requestCompleted:(FBRequestConnection *)connection
+ result:(id)result
+ error:(NSError *)error;
+
+@end
+
+
+@implementation FBGraphObjectPagingLoader
+
+@synthesize tableView = _tableView;
+@synthesize dataSource = _dataSource;
+@synthesize pagingMode = _pagingMode;
+@synthesize nextLink = _nextLink;
+@synthesize session = _session;
+@synthesize connection = _connection;
+@synthesize delegate = _delegate;
+@synthesize isResultFromCache = _isResultFromCache;
+@synthesize cacheIdentity = _cacheIdentity;
+@synthesize skipRoundtripIfCached = _skipRoundtripIfCached;
+
+#pragma mark Lifecycle methods
+
+- (id)initWithDataSource:(FBGraphObjectTableDataSource*)aDataSource
+ pagingMode:(FBGraphObjectPagingMode)pagingMode;{
+ if (self = [super init]) {
+ // Note that pagingMode must be set before dataSource.
+ self.pagingMode = pagingMode;
+ self.dataSource = aDataSource;
+ _isResultFromCache = NO;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [_tableView release];
+ [_dataSource release];
+ [_nextLink release];
+ [_session release];
+ [_connection release];
+ [_cacheIdentity release];
+
+ [super dealloc];
+}
+
+#pragma mark -
+
+- (void)setDataSource:(FBGraphObjectTableDataSource *)dataSource {
+ [dataSource retain];
+ [_dataSource release];
+ _dataSource = dataSource;
+ if (self.pagingMode == FBGraphObjectPagingModeAsNeeded) {
+ _dataSource.dataNeededDelegate = self;
+ } else {
+ _dataSource.dataNeededDelegate = nil;
+ }
+}
+
+- (void)setTableView:(UITableView*)tableView {
+ [tableView retain];
+ [_tableView release];
+ _tableView = tableView;
+
+ // If we already have a nextLink and we are in immediate paging mode, re-start
+ // loading when we are reconnected to a table view.
+ if (self.pagingMode == FBGraphObjectPagingModeImmediate &&
+ self.nextLink &&
+ self.tableView) {
+ [self followNextLink];
+ }
+}
+
+- (void)updateView
+{
+ [self.dataSource update];
+ [self.tableView reloadData];
+}
+
+// Adds new results to the table and attempts to preserve visual context in the table
+- (void)addResultsAndUpdateView:(NSDictionary*)results {
+ NSArray *data = (NSArray *)[results objectForKey:@"data"];
+ if (data.count == 0) {
+ // If we got no data, stop following paging links.
+ self.nextLink = nil;
+ // Tell the data source we're done.
+ [self.dataSource appendGraphObjects:nil];
+ [self updateView];
+
+ // notify of completion
+ if ([self.delegate respondsToSelector:@selector(pagingLoaderDidFinishLoading:)]) {
+ [self.delegate pagingLoaderDidFinishLoading:self];
+ }
+ return;
+ } else {
+ NSDictionary *paging = (NSDictionary *)[results objectForKey:@"paging"];
+ NSString *next = (NSString *)[paging objectForKey:@"next"];
+ self.nextLink = next;
+ }
+
+ if (!self.dataSource.hasGraphObjects) {
+ // If we don't have any data already, this is easy.
+ [self.dataSource appendGraphObjects:data];
+ [self updateView];
+ } else {
+ // As we fetch additional results and add them to the table, we do not
+ // want the table jumping around seemingly at random, frustrating the user's
+ // attempts at scrolling, etc. Since results may be added anywhere in
+ // the table, we choose to try to keep the first visible row in a fixed
+ // position (from the user's perspective). We try to keep it positioned at
+ // the same offset from the top of the screen so adding new items seems
+ // smoother, as opposed to having it "snap" to a multiple of row height
+ // (as would happen by simply calling [UITableView
+ // scrollToRowAtIndexPath:atScrollPosition:animated:].
+
+ // Which object is currently at the top of the table (the "anchor" object)?
+ // (If possible, we choose the second row, to give context above and below and avoid
+ // cases where the first row is only barely visible, thus providing little context.)
+ NSArray *visibleRowIndexPaths = [self.tableView indexPathsForVisibleRows];
+ if (visibleRowIndexPaths.count > 0) {
+ int anchorRowIndex = (visibleRowIndexPaths.count > 1) ? 1 : 0;
+ NSIndexPath *anchorIndexPath = [visibleRowIndexPaths objectAtIndex:anchorRowIndex];
+ id anchorObject = [self.dataSource itemAtIndexPath:anchorIndexPath];
+
+ // What is its rect, and what is the overall contentOffset of the table?
+ CGRect anchorRowRectBefore = [self.tableView rectForRowAtIndexPath:anchorIndexPath];
+ CGPoint contentOffset = self.tableView.contentOffset;
+
+ // Update with new data and reload the table.
+ [self.dataSource appendGraphObjects:data];
+ [self updateView];
+
+ // Where is the anchor object now?
+ anchorIndexPath = [self.dataSource indexPathForItem:anchorObject];
+ CGRect anchorRowRectAfter = [self.tableView rectForRowAtIndexPath:anchorIndexPath];
+
+ // Keep the content offset the same relative to the rect of the row (so if it was
+ // 1/4 scrolled off the top before, it still will be, etc.)
+ contentOffset.y += anchorRowRectAfter.origin.y - anchorRowRectBefore.origin.y;
+ self.tableView.contentOffset = contentOffset;
+ }
+ }
+
+ if ([self.delegate respondsToSelector:@selector(pagingLoader:didLoadData:)]) {
+ [self.delegate pagingLoader:self didLoadData:results];
+ }
+
+ // If we are supposed to keep paging, do so. But unless we are viewless, if we have lost
+ // our tableView, take that as a sign to stop (probably because the view was unloaded).
+ // If tableView is re-set, we will start again.
+ if ((self.pagingMode == FBGraphObjectPagingModeImmediate &&
+ self.tableView) ||
+ self.pagingMode == FBGraphObjectPagingModeImmediateViewless) {
+ [self followNextLink];
+ }
+}
+
+- (void)followNextLink {
+ if (self.nextLink &&
+ self.session) {
+ [self.connection cancel];
+ self.connection = nil;
+
+ if ([self.delegate respondsToSelector:@selector(pagingLoader:willLoadURL:)]) {
+ [self.delegate pagingLoader:self willLoadURL:self.nextLink];
+ }
+
+ FBRequest *request = [[FBRequest alloc] initWithSession:self.session
+ graphPath:nil];
+
+ FBRequestConnection *connection = [[FBRequestConnection alloc] init];
+ [connection addRequest:request completionHandler:
+ ^(FBRequestConnection *connection, id result, NSError *error) {
+ _isResultFromCache = _isResultFromCache || connection.isResultFromCache;
+ self.connection = nil;
+ [self requestCompleted:connection result:result error:error];
+ }];
+
+ // Override the URL using the one passed back in 'next'.
+ NSURL *url = [NSURL URLWithString:self.nextLink];
+ NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
+ connection.urlRequest = urlRequest;
+
+ self.nextLink = nil;
+
+ self.connection = connection;
+ [self.connection startWithCacheIdentity:self.cacheIdentity
+ skipRoundtripIfCached:self.skipRoundtripIfCached];
+
+ [request release];
+ [connection release];
+ }
+}
+
+- (void)startLoadingWithRequest:(FBRequest*)request
+ cacheIdentity:(NSString*)cacheIdentity
+ skipRoundtripIfCached:(BOOL)skipRoundtripIfCached {
+ [self.dataSource prepareForNewRequest];
+
+ [self.connection cancel];
+ _isResultFromCache = NO;
+
+ self.cacheIdentity = cacheIdentity;
+ self.skipRoundtripIfCached = skipRoundtripIfCached;
+
+ FBRequestConnection *connection = [[FBRequestConnection alloc] init];
+ [connection addRequest:request
+ completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
+ _isResultFromCache = _isResultFromCache || connection.isResultFromCache;
+ [self requestCompleted:connection result:result error:error];
+ }];
+
+ self.connection = connection;
+ [self.connection startWithCacheIdentity:self.cacheIdentity
+ skipRoundtripIfCached:self.skipRoundtripIfCached];
+
+ [connection release];
+
+ NSString *urlString = [[[self.connection urlRequest] URL] absoluteString];
+ if ([self.delegate respondsToSelector:@selector(pagingLoader:willLoadURL:)]) {
+ [self.delegate pagingLoader:self willLoadURL:urlString];
+ }
+}
+
+- (void)cancel {
+ [self.connection cancel];
+}
+
+- (void)reset {
+ [self cancel];
+ self.connection = nil;
+ self.nextLink = nil;
+}
+
+- (void)requestCompleted:(FBRequestConnection *)connection
+ result:(id)result
+ error:(NSError *)error {
+ self.connection = nil;
+
+ NSDictionary *resultDictionary = (NSDictionary *)result;
+
+ NSArray *data = nil;
+ if (!error && [result isKindOfClass:[NSDictionary class]]) {
+ id rawData = [resultDictionary objectForKey:@"data"];
+ if ([rawData isKindOfClass:[NSArray class]]) {
+ data = (NSArray *)rawData;
+ }
+ }
+
+ if (!error && !data) {
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:result
+ forKey:FBErrorParsedJSONResponseKey];
+ error = [[[NSError alloc] initWithDomain:FacebookSDKDomain
+ code:FBErrorProtocolMismatch
+ userInfo:userInfo]
+ autorelease];
+ }
+
+ if (error) {
+ // Cancellation is not really an error we want to bother the delegate with.
+ BOOL cancelled = [error.domain isEqualToString:FacebookSDKDomain] &&
+ error.code == FBErrorOperationCancelled;
+
+ if (cancelled) {
+ if ([self.delegate respondsToSelector:@selector(pagingLoaderWasCancelled:)]) {
+ [self.delegate pagingLoaderWasCancelled:self];
+ }
+ } else if ([self.delegate respondsToSelector:@selector(pagingLoader:handleError:)]) {
+ [self.delegate pagingLoader:self handleError:error];
+ }
+ } else {
+ [self addResultsAndUpdateView:resultDictionary];
+ }
+}
+
+#pragma mark FBGraphObjectDataSourceDataNeededDelegate methods
+
+- (void)graphObjectTableDataSourceNeedsData:(FBGraphObjectTableDataSource *)dataSource triggeredByIndexPath:(NSIndexPath*)indexPath {
+ if (self.pagingMode == FBGraphObjectPagingModeAsNeeded) {
+ [self followNextLink];
+ }
+}
+
+#pragma mark -
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectTableCell.h b/src/ios/facebook/FBGraphObjectTableCell.h
new file mode 100644
index 000000000..c2752d05a
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableCell.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+
+@interface FBGraphObjectTableCell : UITableViewCell
+
+// We allow the title to be split into two parts, with one (or both) optionally
+// bolded. titleSuffix will be appended to the end of title with a space in between.
+@property (copy, nonatomic) NSString *title;
+@property (copy, nonatomic) NSString *titleSuffix;
+@property (nonatomic) BOOL boldTitle;
+@property (nonatomic) BOOL boldTitleSuffix;
+
+@property (copy, nonatomic) NSString *subtitle;
+@property (retain, nonatomic) UIImage *picture;
+
++ (CGFloat)rowHeight;
+
+- (void)startAnimatingActivityIndicator;
+- (void)stopAnimatingActivityIndicator;
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectTableCell.m b/src/ios/facebook/FBGraphObjectTableCell.m
new file mode 100644
index 000000000..742b6da11
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableCell.m
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBGraphObjectTableCell.h"
+
+static const CGFloat titleFontHeight = 16;
+static const CGFloat subtitleFontHeight = 12;
+static const CGFloat pictureEdge = 40;
+static const CGFloat pictureMargin = 1;
+static const CGFloat horizontalMargin = 4;
+static const CGFloat titleTopNoSubtitle = 11;
+static const CGFloat titleTopWithSubtitle = 3;
+static const CGFloat subtitleTop = 23;
+static const CGFloat titleHeight = titleFontHeight * 1.25;
+static const CGFloat subtitleHeight = subtitleFontHeight * 1.25;
+
+@interface FBGraphObjectTableCell()
+
+@property (nonatomic, retain) UIImageView *pictureView;\
+@property (nonatomic, retain) UILabel* titleSuffixLabel;
+@property (nonatomic, retain) UIActivityIndicatorView *activityIndicator;
+
+- (void)updateFonts;
+
+@end
+
+@implementation FBGraphObjectTableCell
+
+@synthesize pictureView = _pictureView;
+@synthesize titleSuffixLabel = _titleSuffixLabel;
+@synthesize activityIndicator = _activityIndicator;
+@synthesize boldTitle = _boldTitle;
+@synthesize boldTitleSuffix = _boldTitleSuffix;
+
+#pragma mark - Lifecycle
+
+- (id)initWithStyle:(UITableViewCellStyle)style
+ reuseIdentifier:(NSString*)reuseIdentifier
+{
+ self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+ if (self) {
+ // Picture
+ UIImageView *pictureView = [[UIImageView alloc] init];
+ pictureView.clipsToBounds = YES;
+ pictureView.contentMode = UIViewContentModeScaleAspectFill;
+
+ self.pictureView = pictureView;
+ [self.contentView addSubview:pictureView];
+ [pictureView release];
+
+ // Subtitle
+ self.detailTextLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+ self.detailTextLabel.textColor = [UIColor colorWithRed:0.4 green:0.6 blue:0.8 alpha:1.0];
+ self.detailTextLabel.font = [UIFont systemFontOfSize:subtitleFontHeight];
+
+ // Title
+ self.textLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+ self.textLabel.font = [UIFont systemFontOfSize:titleFontHeight];
+
+ // Content View
+ self.contentView.clipsToBounds = YES;
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [_titleSuffixLabel release];
+ [_pictureView release];
+
+ [super dealloc];
+}
+
+#pragma mark -
+
+- (void)layoutSubviews
+{
+ [super layoutSubviews];
+
+ [self updateFonts];
+
+ BOOL hasPicture = (self.picture != nil);
+ BOOL hasSubtitle = (self.subtitle != nil);
+ BOOL hasTitleSuffix = (self.titleSuffix != nil);
+
+ CGFloat pictureWidth = hasPicture ? pictureEdge : 0;
+ CGSize cellSize = self.contentView.bounds.size;
+ CGFloat textLeft = (hasPicture ? ((2 * pictureMargin) + pictureWidth) : 0) + horizontalMargin;
+ CGFloat textWidth = cellSize.width - (textLeft + horizontalMargin);
+ CGFloat titleTop = hasSubtitle ? titleTopWithSubtitle : titleTopNoSubtitle;
+
+ self.pictureView.frame = CGRectMake(pictureMargin, pictureMargin, pictureEdge, pictureWidth);
+ self.detailTextLabel.frame = CGRectMake(textLeft, subtitleTop, textWidth, subtitleHeight);
+ if (!hasTitleSuffix) {
+ self.textLabel.frame = CGRectMake(textLeft, titleTop, textWidth, titleHeight);
+ } else {
+ CGSize titleSize = [self.textLabel.text sizeWithFont:self.textLabel.font];
+ CGSize spaceSize = [@" " sizeWithFont:self.textLabel.font];
+ CGFloat titleWidth = titleSize.width + spaceSize.width;
+ self.textLabel.frame = CGRectMake(textLeft, titleTop, titleWidth, titleHeight);
+
+ CGFloat titleSuffixLeft = textLeft + titleWidth;
+ CGFloat titleSuffixWidth = textWidth - titleWidth;
+ self.titleSuffixLabel.frame = CGRectMake(titleSuffixLeft, titleTop, titleSuffixWidth, titleHeight);
+ }
+
+ [self.pictureView setHidden:!(hasPicture)];
+ [self.detailTextLabel setHidden:!(hasSubtitle)];
+ [self.titleSuffixLabel setHidden:!(hasTitleSuffix)];
+}
+
++ (CGFloat)rowHeight
+{
+ return pictureEdge + (2 * pictureMargin) + 1;
+}
+
+- (void)startAnimatingActivityIndicator {
+ CGRect cellBounds = self.bounds;
+ if (!self.activityIndicator) {
+ UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
+ activityIndicator.hidesWhenStopped = YES;
+ activityIndicator.autoresizingMask =
+ (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
+
+ self.activityIndicator = activityIndicator;
+ [self addSubview:activityIndicator];
+ [activityIndicator release];
+ }
+
+ self.activityIndicator.center = CGPointMake(CGRectGetMidX(cellBounds), CGRectGetMidY(cellBounds));
+
+ [self.activityIndicator startAnimating];
+}
+
+- (void)stopAnimatingActivityIndicator {
+ if (self.activityIndicator) {
+ [self.activityIndicator stopAnimating];
+ }
+}
+
+- (void)updateFonts {
+ if (self.boldTitle) {
+ self.textLabel.font = [UIFont boldSystemFontOfSize:titleFontHeight];
+ } else {
+ self.textLabel.font = [UIFont systemFontOfSize:titleFontHeight];
+ }
+
+ if (self.boldTitleSuffix) {
+ self.titleSuffixLabel.font = [UIFont boldSystemFontOfSize:titleFontHeight];
+ } else {
+ self.titleSuffixLabel.font = [UIFont systemFontOfSize:titleFontHeight];
+ }
+}
+
+- (void)createTitleSuffixLabel {
+ if (!self.titleSuffixLabel) {
+ UILabel *titleSuffixLabel = [[UILabel alloc] init];
+ titleSuffixLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+ [self.contentView addSubview:titleSuffixLabel];
+
+ self.titleSuffixLabel = titleSuffixLabel;
+ [titleSuffixLabel release];
+ }
+}
+#pragma mark - Properties
+
+- (UIImage *)picture
+{
+ return self.pictureView.image;
+}
+
+- (void)setPicture:(UIImage *)picture
+{
+ self.pictureView.image = picture;
+ [self setNeedsLayout];
+}
+
+- (NSString*)subtitle
+{
+ return self.detailTextLabel.text;
+}
+
+- (void)setSubtitle:(NSString *)subtitle
+{
+ self.detailTextLabel.text = subtitle;
+ [self setNeedsLayout];
+}
+
+- (NSString*)title
+{
+ return self.textLabel.text;
+}
+
+- (void)setTitle:(NSString *)title
+{
+ self.textLabel.text = title;
+ [self setNeedsLayout];
+}
+
+- (NSString*)titleSuffix
+{
+ return self.titleSuffixLabel.text;
+}
+
+- (void)setTitleSuffix:(NSString *)titleSuffix
+{
+ if (titleSuffix) {
+ [self createTitleSuffixLabel];
+ self.titleSuffixLabel.text = titleSuffix;
+ }
+ [self setNeedsLayout];
+}
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectTableDataSource.h b/src/ios/facebook/FBGraphObjectTableDataSource.h
new file mode 100644
index 000000000..2267ddc3d
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableDataSource.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphObject.h"
+
+@protocol FBGraphObjectViewControllerDelegate;
+@protocol FBGraphObjectSelectionQueryDelegate;
+@protocol FBGraphObjectDataSourceDataNeededDelegate;
+@class FBGraphObjectTableCell;
+
+@interface FBGraphObjectTableDataSource : NSObject
+
+@property (nonatomic, retain) UIImage *defaultPicture;
+@property (nonatomic, assign) id controllerDelegate;
+@property (nonatomic, copy) NSString *groupByField;
+@property (nonatomic, assign) BOOL useCollation;
+@property (nonatomic) BOOL itemTitleSuffixEnabled;
+@property (nonatomic) BOOL itemPicturesEnabled;
+@property (nonatomic) BOOL itemSubtitleEnabled;
+@property (nonatomic, assign) id selectionDelegate;
+@property (nonatomic, assign) id dataNeededDelegate;
+@property (nonatomic, copy) NSArray *sortDescriptors;
+
+- (NSString *)fieldsForRequestIncluding:(NSSet *)customFields, ...;
+
+- (void)setSortingBySingleField:(NSString*)fieldName ascending:(BOOL)ascending;
+- (void)setSortingByFields:(NSArray*)fieldNames ascending:(BOOL)ascending;
+
+- (void)prepareForNewRequest;
+// Clears all graph objects from the data source.
+- (void)clearGraphObjects;
+// Adds additional graph objects (pass nil to indicate all objects have been added).
+- (void)appendGraphObjects:(NSArray *)data;
+- (BOOL)hasGraphObjects;
+
+- (void)bindTableView:(UITableView *)tableView;
+
+- (void)cancelPendingRequests;
+
+// Call this when updating any property or if
+// delegate.filterIncludesItem would return a different answer now.
+- (void)update;
+
+// Returns the graph object at a given indexPath.
+- (FBGraphObject *)itemAtIndexPath:(NSIndexPath *)indexPath;
+
+// Returns the indexPath for a given graph object.
+- (NSIndexPath *)indexPathForItem:(FBGraphObject *)item;
+
+@end
+
+@protocol FBGraphObjectViewControllerDelegate
+@required
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ titleOfItem:(id)graphObject;
+
+@optional
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ titleSuffixOfItem:(id)graphObject;
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ subtitleOfItem:(id)graphObject;
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ pictureUrlOfItem:(id)graphObject;
+
+- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ filterIncludesItem:(id)item;
+
+- (void)graphObjectTableDataSource:(FBGraphObjectTableDataSource*)dataSource
+ customizeTableCell:(FBGraphObjectTableCell*)cell;
+
+@end
+
+@protocol FBGraphObjectSelectionQueryDelegate
+
+- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ selectionIncludesItem:(id)item;
+
+@end
+
+@protocol FBGraphObjectDataSourceDataNeededDelegate
+
+- (void)graphObjectTableDataSourceNeedsData:(FBGraphObjectTableDataSource *)dataSource triggeredByIndexPath:(NSIndexPath*)indexPath;
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectTableDataSource.m b/src/ios/facebook/FBGraphObjectTableDataSource.m
new file mode 100644
index 000000000..322822377
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableDataSource.m
@@ -0,0 +1,585 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBGraphObjectTableDataSource.h"
+#import "FBGraphObjectTableCell.h"
+#import "FBGraphObject.h"
+#import "FBURLConnection.h"
+#import "FBUtility.h"
+
+// Magic number - iPhone address book doesn't show scrubber for less than 5 contacts
+static const NSInteger kMinimumCountToCollate = 6;
+
+@interface FBGraphObjectTableDataSource ()
+
+@property (nonatomic, retain) NSArray *data;
+@property (nonatomic, retain) NSArray *indexKeys;
+@property (nonatomic, retain) NSDictionary *indexMap;
+@property (nonatomic, retain) NSMutableSet *pendingURLConnections;
+@property (nonatomic, assign) BOOL expectingMoreGraphObjects;
+@property (nonatomic, retain) UILocalizedIndexedCollation *collation;
+@property (nonatomic, assign) BOOL showSections;
+
+- (BOOL)filterIncludesItem:(FBGraphObject *)item;
+- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView;
+- (NSString *)indexKeyOfItem:(FBGraphObject *)item;
+- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item;
+- (void)addOrRemovePendingConnection:(FBURLConnection *)connection;
+- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath;
+- (BOOL)isLastSection:(NSInteger)section;
+
+@end
+
+@implementation FBGraphObjectTableDataSource
+
+@synthesize data = _data;
+@synthesize defaultPicture = _defaultPicture;
+@synthesize controllerDelegate = _controllerDelegate;
+@synthesize groupByField = _groupByField;
+@synthesize useCollation = _useCollation;
+@synthesize showSections = _showSections;
+@synthesize indexKeys = _indexKeys;
+@synthesize indexMap = _indexMap;
+@synthesize itemTitleSuffixEnabled = _itemTitleSuffixEnabled;
+@synthesize itemPicturesEnabled = _itemPicturesEnabled;
+@synthesize itemSubtitleEnabled = _itemSubtitleEnabled;
+@synthesize pendingURLConnections = _pendingURLConnections;
+@synthesize selectionDelegate = _selectionDelegate;
+@synthesize sortDescriptors = _sortDescriptors;
+@synthesize dataNeededDelegate = _dataNeededDelegate;
+@synthesize expectingMoreGraphObjects = _expectingMoreGraphObjects;
+@synthesize collation = _collation;
+
+- (void)setUseCollation:(BOOL)useCollation
+{
+ if (_useCollation != useCollation) {
+ _useCollation = useCollation;
+ self.collation = _useCollation ? [UILocalizedIndexedCollation currentCollation] : nil;
+ }
+}
+
+- (id)init
+{
+ self = [super init];
+
+ if (self) {
+ NSMutableSet *pendingURLConnections = [[NSMutableSet alloc] init];
+ self.pendingURLConnections = pendingURLConnections;
+ [pendingURLConnections release];
+ self.expectingMoreGraphObjects = YES;
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ FBConditionalLog(![_pendingURLConnections count],
+ @"FBGraphObjectTableDataSource pending connection did not retain self");
+
+ [_collation release];
+ [_data release];
+ [_defaultPicture release];
+ [_groupByField release];
+ [_indexKeys release];
+ [_indexMap release];
+ [_pendingURLConnections release];
+ [_sortDescriptors release];
+
+ [super dealloc];
+}
+
+#pragma mark - Public Methods
+
+- (NSString *)fieldsForRequestIncluding:(NSSet *)customFields, ...
+{
+ // Start with custom fields.
+ NSMutableSet *nameSet = [[NSMutableSet alloc] initWithSet:customFields];
+
+ // Iterate through varargs after the initial set, and add them
+ id vaName;
+ va_list vaArguments;
+ va_start(vaArguments, customFields);
+ while ((vaName = va_arg(vaArguments, id))) {
+ [nameSet addObject:vaName];
+ }
+ va_end(vaArguments);
+
+ // Add fields needed for data source functionality.
+ if (self.groupByField) {
+ [nameSet addObject:self.groupByField];
+ }
+
+ // get a stable order for our fields, because we use the resulting URL as a cache ID
+ NSMutableArray *sortedFields = [[nameSet allObjects] mutableCopy];
+ [sortedFields sortUsingSelector:@selector(caseInsensitiveCompare:)];
+
+ [nameSet release];
+
+ // Build the comma-separated string
+ NSMutableString *fields = [[[NSMutableString alloc] init] autorelease];
+
+ for (NSString *field in sortedFields) {
+ if ([fields length]) {
+ [fields appendString:@","];
+ }
+ [fields appendString:field];
+ }
+
+ [sortedFields release];
+ return fields;
+}
+
+- (void)prepareForNewRequest {
+ self.data = nil;
+ self.expectingMoreGraphObjects = YES;
+}
+
+- (void)clearGraphObjects {
+ self.indexKeys = nil;
+ self.indexMap = nil;
+ [self prepareForNewRequest];
+}
+
+- (void)appendGraphObjects:(NSArray *)data
+{
+ if (self.data) {
+ self.data = [self.data arrayByAddingObjectsFromArray:data];
+ } else {
+ self.data = data;
+ }
+ if (data == nil) {
+ self.expectingMoreGraphObjects = NO;
+ }
+}
+
+- (BOOL)hasGraphObjects {
+ return self.data && self.data.count > 0;
+}
+
+- (void)bindTableView:(UITableView *)tableView
+{
+ tableView.dataSource = self;
+ tableView.rowHeight = [FBGraphObjectTableCell rowHeight];
+}
+
+- (void)cancelPendingRequests
+{
+ // Cancel all active connections.
+ for (FBURLConnection *connection in _pendingURLConnections) {
+ [connection cancel];
+ }
+}
+
+// Called after changing any properties. To simplify the code here,
+// since this class is internal, we do not auto-update on property
+// changes.
+//
+// This builds indexMap and indexKeys, the data structures used to
+// respond to UITableDataSource protocol requests. UITable expects
+// a list of section names, and then ask for items given a section
+// index and item index within that section. In addition, we need
+// to do reverse mapping from item to table location.
+//
+// To facilitate both of these, we build an array of section titles,
+// and a dictionary mapping title -> item array. We could consider
+// building a reverse-lookup map too, but this seems unnecessary.
+- (void)update
+{
+ NSInteger objectsShown = 0;
+ NSMutableDictionary *indexMap = [[[NSMutableDictionary alloc] init] autorelease];
+ NSMutableArray *indexKeys = [[[NSMutableArray alloc] init] autorelease];
+
+ for (FBGraphObject *item in self.data) {
+ if (![self filterIncludesItem:item]) {
+ continue;
+ }
+
+ NSString *key = [self indexKeyOfItem:item];
+ NSMutableArray *existingSection = [indexMap objectForKey:key];
+ NSMutableArray *section = existingSection;
+
+ if (!section) {
+ section = [[[NSMutableArray alloc] init] autorelease];
+ }
+ [section addObject:item];
+
+ if (!existingSection) {
+ [indexMap setValue:section forKey:key];
+ [indexKeys addObject:key];
+ }
+ objectsShown++;
+ }
+
+ if (self.sortDescriptors) {
+ for (NSString *key in indexKeys) {
+ [[indexMap objectForKey:key] sortUsingDescriptors:self.sortDescriptors];
+ }
+ }
+ if (!self.useCollation) {
+ [indexKeys sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
+ }
+
+ self.showSections = objectsShown >= kMinimumCountToCollate;
+ self.indexKeys = indexKeys;
+ self.indexMap = indexMap;
+}
+
+#pragma mark - Private Methods
+
+- (BOOL)filterIncludesItem:(FBGraphObject *)item
+{
+ if (![self.controllerDelegate respondsToSelector:
+ @selector(graphObjectTableDataSource:filterIncludesItem:)]) {
+ return YES;
+ }
+
+ return [self.controllerDelegate graphObjectTableDataSource:self
+ filterIncludesItem:item];
+}
+
+- (void)setSortingByFields:(NSArray*)fieldNames ascending:(BOOL)ascending {
+ NSMutableArray *sortDescriptors = [NSMutableArray arrayWithCapacity:fieldNames.count];
+ for (NSString *fieldName in fieldNames) {
+ NSSortDescriptor *sortBy = [NSSortDescriptor
+ sortDescriptorWithKey:fieldName
+ ascending:ascending
+ selector:@selector(localizedCaseInsensitiveCompare:)];
+ [sortDescriptors addObject:sortBy];
+ }
+ self.sortDescriptors = sortDescriptors;
+}
+
+- (void)setSortingBySingleField:(NSString*)fieldName ascending:(BOOL)ascending {
+ [self setSortingByFields:[NSArray arrayWithObject:fieldName] ascending:ascending];
+}
+
+- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView
+{
+ static NSString * const cellKey = @"fbTableCell";
+ FBGraphObjectTableCell *cell =
+ (FBGraphObjectTableCell*)[tableView dequeueReusableCellWithIdentifier:cellKey];
+
+ if (!cell) {
+ cell = [[FBGraphObjectTableCell alloc]
+ initWithStyle:UITableViewCellStyleSubtitle
+ reuseIdentifier:cellKey];
+ [cell autorelease];
+
+ cell.selectionStyle = UITableViewCellSelectionStyleNone;
+ }
+
+ return cell;
+}
+
+- (NSString *)indexKeyOfItem:(FBGraphObject *)item
+{
+ NSString *text = @"";
+
+ if (self.groupByField) {
+ text = [item objectForKey:self.groupByField];
+ }
+
+ if (self.useCollation) {
+ NSInteger collationSection = [self.collation sectionForObject:item collationStringSelector:NSSelectorFromString(self.groupByField)];
+ text = [[self.collation sectionTitles] objectAtIndex:collationSection];
+ } else {
+
+ if ([text length] > 1) {
+ text = [text substringToIndex:1];
+ }
+
+ text = [text uppercaseString];
+ }
+ return text;
+}
+
+- (FBGraphObject *)itemAtIndexPath:(NSIndexPath *)indexPath
+{
+ id key = nil;
+ if (self.useCollation) {
+ NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:indexPath.section];
+ key = sectionTitle;
+ } else if (indexPath.section >= 0 && indexPath.section < self.indexKeys.count) {
+ key = [self.indexKeys objectAtIndex:indexPath.section];
+ }
+ NSArray *sectionItems = [self.indexMap objectForKey:key];
+ if (indexPath.row >= 0 && indexPath.row < sectionItems.count) {
+ return [sectionItems objectAtIndex:indexPath.row];
+ }
+ return nil;
+}
+
+- (NSIndexPath *)indexPathForItem:(FBGraphObject *)item
+{
+ NSString *key = [self indexKeyOfItem:item];
+ NSMutableArray *sectionItems = [self.indexMap objectForKey:key];
+ if (!sectionItems) {
+ return nil;
+ }
+
+ NSInteger sectionIndex = 0;
+ if (self.useCollation) {
+ sectionIndex = [self.collation.sectionTitles indexOfObject:key];
+ } else {
+ sectionIndex = [self.indexKeys indexOfObject:key];
+ }
+ if (sectionIndex == NSNotFound) {
+ return nil;
+ }
+
+ id matchingObject = [FBUtility graphObjectInArray:sectionItems withSameIDAs:item];
+ if (matchingObject == nil) {
+ return nil;
+ }
+
+ NSInteger itemIndex = [sectionItems indexOfObject:matchingObject];
+ if (itemIndex == NSNotFound) {
+ return nil;
+ }
+
+ return [NSIndexPath indexPathForRow:itemIndex inSection:sectionIndex];
+}
+
+- (BOOL)isLastSection:(NSInteger)section {
+ if (self.useCollation) {
+ return section == self.collation.sectionTitles.count - 1;
+ } else {
+ return section == self.indexKeys.count - 1;
+ }
+}
+
+- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath {
+ if ([self isLastSection:indexPath.section]) {
+ NSArray *sectionItems = [self sectionItemsForSection:indexPath.section];
+
+ if (indexPath.row == sectionItems.count) {
+ // Last section has one more row that items if we are expecting more objects.
+ return YES;
+ }
+ }
+ return NO;
+}
+
+
+- (NSString *)titleForSection:(NSInteger)sectionIndex
+{
+ id key;
+ if (self.useCollation) {
+ NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:sectionIndex];
+ key = sectionTitle;
+ } else {
+ key = [self.indexKeys objectAtIndex:sectionIndex];
+ }
+ return key;
+}
+
+- (NSArray *)sectionItemsForSection:(NSInteger)sectionIndex
+{
+ id key = [self titleForSection:sectionIndex];
+ NSArray *sectionItems = [self.indexMap objectForKey:key];
+ return sectionItems;
+}
+- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item
+{
+ __block UIImage *image = nil;
+ NSString *urlString = [self.controllerDelegate graphObjectTableDataSource:self
+ pictureUrlOfItem:item];
+ if (urlString) {
+ FBURLConnectionHandler handler =
+ ^(FBURLConnection *connection, NSError *error, NSURLResponse *response, NSData *data) {
+ [self addOrRemovePendingConnection:connection];
+ if (!error) {
+ image = [UIImage imageWithData:data];
+
+ NSIndexPath *indexPath = [self indexPathForItem:item];
+ if (indexPath) {
+ FBGraphObjectTableCell *cell =
+ (FBGraphObjectTableCell*)[tableView cellForRowAtIndexPath:indexPath];
+
+ if (cell) {
+ cell.picture = image;
+ }
+ }
+ }
+ };
+
+ FBURLConnection *connection = [[[FBURLConnection alloc]
+ initWithURL:[NSURL URLWithString:urlString]
+ completionHandler:handler]
+ autorelease];
+
+ [self addOrRemovePendingConnection:connection];
+ }
+
+ // If the picture had not been fetched yet by this object, but is cached in the
+ // URL cache, we can complete synchronously above. In this case, we will not
+ // find the cell in the table because we are in the process of creating it. We can
+ // just return the object here.
+ if (image) {
+ return image;
+ }
+
+ return self.defaultPicture;
+}
+
+// In tableView:imageForItem:, there are two code-paths, and both always run.
+// Whichever runs first adds the connection to the collection of pending requests,
+// and whichever runs second removes it. This allows us to track all requests
+// for which one code-path has run and the other has not.
+- (void)addOrRemovePendingConnection:(FBURLConnection *)connection
+{
+ if ([self.pendingURLConnections containsObject:connection]) {
+ [self.pendingURLConnections removeObject:connection];
+ } else {
+ [self.pendingURLConnections addObject:connection];
+ }
+}
+
+#pragma mark - UITableViewDataSource
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+ if (self.useCollation) {
+ return self.collation.sectionTitles.count;
+ } else {
+ return [self.indexKeys count];
+ }
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+ NSArray *sectionItems = [self sectionItemsForSection:section];
+
+ int count = [sectionItems count];
+ // If we are expecting more objects to be loaded via paging, add 1 to the
+ // row count for the last section.
+ if (self.expectingMoreGraphObjects &&
+ self.dataNeededDelegate &&
+ [self isLastSection:section]) {
+ ++count;
+ }
+ return count;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
+{
+ if (!self.showSections) {
+ return nil;
+ }
+
+ NSArray *sectionItems = [self sectionItemsForSection:section];
+ return sectionItems.count > 0 ? [self titleForSection:section] : nil;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
+{
+ if (self.useCollation) {
+ return [self.collation sectionForSectionIndexTitleAtIndex:index];
+ } else {
+ return index;
+ }
+}
+
+- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
+{
+ if (!self.showSections) {
+ return nil;
+ }
+
+ if (self.useCollation) {
+ return self.collation.sectionIndexTitles;
+ } else {
+ return [self.indexKeys count] > 1 ? self.indexKeys : nil;
+ }
+}
+
+- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ return NO;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView
+ cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ FBGraphObjectTableCell *cell = [self cellWithTableView:tableView];
+
+ if ([self isActivityIndicatorIndexPath:indexPath]) {
+ cell.picture = nil;
+ cell.subtitle = nil;
+ cell.title = nil;
+ cell.accessoryType = UITableViewCellAccessoryNone;
+ cell.selected = NO;
+
+ [cell startAnimatingActivityIndicator];
+
+ [self.dataNeededDelegate graphObjectTableDataSourceNeedsData:self
+ triggeredByIndexPath:indexPath];
+ } else {
+ FBGraphObject *item = [self itemAtIndexPath:indexPath];
+
+ // This is a no-op if it doesn't have an activity indicator.
+ [cell stopAnimatingActivityIndicator];
+ if (item) {
+ if (self.itemPicturesEnabled) {
+ cell.picture = [self tableView:tableView imageForItem:item];
+ } else {
+ cell.picture = nil;
+ }
+
+ if (self.itemTitleSuffixEnabled) {
+ cell.titleSuffix = [self.controllerDelegate graphObjectTableDataSource:self
+ titleSuffixOfItem:item];
+ } else {
+ cell.titleSuffix = nil;
+ }
+
+ if (self.itemSubtitleEnabled) {
+ cell.subtitle = [self.controllerDelegate graphObjectTableDataSource:self
+ subtitleOfItem:item];
+ } else {
+ cell.subtitle = nil;
+ }
+
+ cell.title = [self.controllerDelegate graphObjectTableDataSource:self
+ titleOfItem:item];
+
+ if ([self.selectionDelegate graphObjectTableDataSource:self
+ selectionIncludesItem:item]) {
+ cell.accessoryType = UITableViewCellAccessoryCheckmark;
+ cell.selected = YES;
+ } else {
+ cell.accessoryType = UITableViewCellAccessoryNone;
+ cell.selected = NO;
+ }
+
+ if ([self.controllerDelegate respondsToSelector:@selector(graphObjectTableDataSource:customizeTableCell:)]) {
+ [self.controllerDelegate graphObjectTableDataSource:self
+ customizeTableCell:cell];
+ }
+ } else {
+ cell.picture = nil;
+ cell.subtitle = nil;
+ cell.title = nil;
+ cell.accessoryType = UITableViewCellAccessoryNone;
+ cell.selected = NO;
+ }
+ }
+
+ return cell;
+}
+
+@end
diff --git a/src/ios/facebook/FBGraphObjectTableSelection.h b/src/ios/facebook/FBGraphObjectTableSelection.h
new file mode 100644
index 000000000..ac62edeeb
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableSelection.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphObjectTableDataSource.h"
+
+@protocol FBGraphObjectSelectionChangedDelegate;
+
+@interface FBGraphObjectTableSelection : NSObject
+
+@property (nonatomic, assign) NSObject *delegate;
+@property (nonatomic, retain, readonly) NSArray *selection;
+@property (nonatomic) BOOL allowsMultipleSelection;
+
+- (id)initWithDataSource:(FBGraphObjectTableDataSource *)dataSource;
+- (void)clearSelectionInTableView:(UITableView*)tableView;
+
+
+@end
+
+@protocol FBGraphObjectSelectionChangedDelegate
+
+- (void)graphObjectTableSelectionDidChange:(FBGraphObjectTableSelection *)selection;
+
+@end
\ No newline at end of file
diff --git a/src/ios/facebook/FBGraphObjectTableSelection.m b/src/ios/facebook/FBGraphObjectTableSelection.m
new file mode 100644
index 000000000..f788f719e
--- /dev/null
+++ b/src/ios/facebook/FBGraphObjectTableSelection.m
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBGraphObjectTableSelection.h"
+#import "FBUtility.h"
+
+@interface FBGraphObjectTableSelection()
+
+@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource;
+@property (nonatomic, retain) NSArray *selection;
+
+- (void)selectItem:(FBGraphObject *)item
+ cell:(UITableViewCell *)cell;
+- (void)deselectItem:(FBGraphObject *)item
+ cell:(UITableViewCell *)cell;
+- (void)selectionChanged;
+
+@end
+
+@implementation FBGraphObjectTableSelection
+
+@synthesize dataSource = _dataSource;
+@synthesize delegate = _delegate;
+@synthesize selection = _selection;
+@synthesize allowsMultipleSelection = _allowMultipleSelection;
+
+- (id)initWithDataSource:(FBGraphObjectTableDataSource *)dataSource
+{
+ self = [super init];
+
+ if (self) {
+ dataSource.selectionDelegate = self;
+
+ self.dataSource = dataSource;
+ self.allowsMultipleSelection = YES;
+
+ NSArray *selection = [[NSArray alloc] init];
+ self.selection = selection;
+ [selection release];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ _dataSource.selectionDelegate = nil;
+
+ [_dataSource release];
+ [_selection release];
+
+ [super dealloc];
+}
+
+- (void)clearSelectionInTableView:(UITableView*)tableView {
+ [self deselectItems:self.selection tableView:tableView];
+}
+
+- (void)selectItem:(FBGraphObject *)item
+ cell:(UITableViewCell *)cell
+{
+ if ([FBUtility graphObjectInArray:self.selection withSameIDAs:item] == nil) {
+ NSMutableArray *selection = [[NSMutableArray alloc] initWithArray:self.selection];
+ [selection addObject:item];
+ self.selection = selection;
+ [selection release];
+ }
+ cell.accessoryType = UITableViewCellAccessoryCheckmark;
+ [self selectionChanged];
+}
+
+- (void)deselectItem:(FBGraphObject *)item
+ cell:(UITableViewCell *)cell
+{
+ id selectedItem = [FBUtility graphObjectInArray:self.selection withSameIDAs:item];
+ if (selectedItem) {
+ NSMutableArray *selection = [[NSMutableArray alloc] initWithArray:self.selection];
+ [selection removeObject:selectedItem];
+ self.selection = selection;
+ [selection release];
+ }
+ cell.accessoryType = UITableViewCellAccessoryNone;
+ [self selectionChanged];
+}
+
+- (void)deselectItems:(NSArray*)items tableView:(UITableView*)tableView
+{
+ // Copy this so it doesn't change from under us.
+ items = [NSArray arrayWithArray:items];
+
+ for (FBGraphObject *item in items) {
+ NSIndexPath *indexPath = [self.dataSource indexPathForItem:item];
+
+ UITableViewCell *cell = nil;
+ if (indexPath != nil) {
+ cell = [tableView cellForRowAtIndexPath:indexPath];
+ }
+
+ [self deselectItem:item cell:cell];
+ }
+}
+
+- (void)selectionChanged
+{
+ if ([self.delegate respondsToSelector:
+ @selector(graphObjectTableSelectionDidChange:)]) {
+ // Let the table view finish updating its UI before notifying the delegate.
+ [self.delegate performSelector:@selector(graphObjectTableSelectionDidChange:) withObject:self afterDelay:.1];
+ }
+}
+
+- (BOOL)selectionIncludesItem:(id)item
+{
+ return [FBUtility graphObjectInArray:self.selection withSameIDAs:item] != nil;
+}
+
+#pragma mark - FBGraphObjectSelectionDelegate
+
+- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ selectionIncludesItem:(id)item
+{
+ return [self selectionIncludesItem:item];
+}
+
+#pragma mark - UITableViewDelegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
+ // cell may be nil, which is okay, it will pick up the right selected state when it is created.
+
+ FBGraphObject *item = [self.dataSource itemAtIndexPath:indexPath];
+ if (item != nil) {
+ // We want to support multi-select on iOS <5.0, so rather than rely on the table view's notion
+ // of selection, just treat this as a toggle. If it is already selected, deselect it, and vice versa.
+ if (![self selectionIncludesItem:item]) {
+ if (self.allowsMultipleSelection == NO) {
+ // No multi-select allowed, deselect what is already selected.
+ [self deselectItems:self.selection tableView:tableView];
+ }
+ [self selectItem:item cell:cell];
+ } else {
+ [self deselectItem:item cell:cell];
+ }
+ }
+}
+
+- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ if (self.allowsMultipleSelection == NO) {
+ // Only deselect if we are not allowing multi select. Otherwise, the user will manually
+ // deselect this item by clicking on it again.
+
+ // cell may be nil, which is okay, it will pick up the right selected state when it is created.
+ UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
+
+ FBGraphObject *item = [self.dataSource itemAtIndexPath:indexPath];
+ [self deselectItem:item cell:cell];
+ }
+}
+
+#pragma mark Debugging helpers
+
+- (NSString*)description {
+ NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, allowsMultipleSelection: %@, delegate: %p, selection: [",
+ NSStringFromClass([self class]),
+ self,
+ self.allowsMultipleSelection ? @"YES" : @"NO",
+ self.delegate];
+
+ bool firstItem = YES;
+ for (FBGraphObject *item in self.selection) {
+ id objectId = [item objectForKey:@"id"];
+ if (!firstItem) {
+ [result appendFormat:@", "];
+ }
+ firstItem = NO;
+ [result appendFormat:@"%@", (objectId != nil) ? objectId : @""];
+ }
+ [result appendFormat:@"]>"];
+
+ return result;
+
+}
+
+
+#pragma mark -
+
+@end
diff --git a/src/ios/facebook/FBGraphPlace.h b/src/ios/facebook/FBGraphPlace.h
new file mode 100644
index 000000000..9e9bc3a4e
--- /dev/null
+++ b/src/ios/facebook/FBGraphPlace.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphLocation.h"
+#import "FBGraphObject.h"
+
+/*!
+ @protocol
+
+ @abstract
+ The `FBGraphPlace` protocol enables typed access to a place object
+ as represented in the Graph API.
+
+
+ @discussion
+ The `FBGraphPlace` protocol represents the most commonly used properties of a
+ Facebook place object. It may be used to access an `NSDictionary` object that has
+ been wrapped with an facade.
+ */
+@protocol FBGraphPlace
+
+/*!
+ @property
+ @abstract Typed access to the place ID.
+ */
+@property (retain, nonatomic) NSString *id;
+
+/*!
+ @property
+ @abstract Typed access to the place name.
+ */
+@property (retain, nonatomic) NSString *name;
+
+/*!
+ @property
+ @abstract Typed access to the place category.
+ */
+@property (retain, nonatomic) NSString *category;
+
+/*!
+ @property
+ @abstract Typed access to the place location.
+ */
+@property (retain, nonatomic) id location;
+
+@end
\ No newline at end of file
diff --git a/src/ios/facebook/FBGraphUser.h b/src/ios/facebook/FBGraphUser.h
new file mode 100644
index 000000000..5d7f6371b
--- /dev/null
+++ b/src/ios/facebook/FBGraphUser.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphPlace.h"
+#import "FBGraphObject.h"
+
+/*!
+ @protocol
+
+ @abstract
+ The `FBGraphUser` protocol enables typed access to a user object
+ as represented in the Graph API.
+
+
+ @discussion
+ The `FBGraphUser` protocol represents the most commonly used properties of a
+ Facebook user object. It may be used to access an `NSDictionary` object that has
+ been wrapped with an facade.
+ */
+@protocol FBGraphUser
+
+/*!
+ @property
+ @abstract Typed access to the user's ID.
+ */
+@property (retain, nonatomic) NSString *id;
+
+/*!
+ @property
+ @abstract Typed access to the user's name.
+ */
+@property (retain, nonatomic) NSString *name;
+
+/*!
+ @property
+ @abstract Typed access to the user's first name.
+ */
+@property (retain, nonatomic) NSString *first_name;
+
+/*!
+ @property
+ @abstract Typed access to the user's middle name.
+ */
+@property (retain, nonatomic) NSString *middle_name;
+
+/*!
+ @property
+ @abstract Typed access to the user's last name.
+ */
+@property (retain, nonatomic) NSString *last_name;
+
+/*!
+ @property
+ @abstract Typed access to the user's profile URL.
+ */
+@property (retain, nonatomic) NSString *link;
+
+/*!
+ @property
+ @abstract Typed access to the user's username.
+ */
+@property (retain, nonatomic) NSString *username;
+
+/*!
+ @property
+ @abstract Typed access to the user's birthday.
+ */
+@property (retain, nonatomic) NSString *birthday;
+
+/*!
+ @property
+ @abstract Typed access to the user's current city.
+ */
+@property (retain, nonatomic) id location;
+
+@end
\ No newline at end of file
diff --git a/src/ios/facebook/FBLogger.h b/src/ios/facebook/FBLogger.h
new file mode 100644
index 000000000..93a9c2e41
--- /dev/null
+++ b/src/ios/facebook/FBLogger.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+
+/*!
+ @class FBLogger
+
+ @abstract
+ Simple logging utility for conditionally logging strings and then emitting them
+ via NSLog().
+
+ @unsorted
+ */
+@interface FBLogger : NSObject
+
+// Access current accumulated contents of the logger.
+@property (copy, nonatomic) NSString *contents;
+
+// Each FBLogger gets a unique serial number to allow the client to log these numbers and, for instance, correlation of Request/Response
+@property (nonatomic, readonly) NSUInteger loggerSerialNumber;
+
+// The logging behavior of this logger. See the FB_LOG_BEHAVIOR* constants in FBSession.h
+@property (copy, nonatomic, readonly) NSString *loggingBehavior;
+
+// Is the current logger instance active, based on its loggingBehavior?
+@property (nonatomic, readonly) BOOL isActive;
+
+//
+// Instance methods
+//
+
+// Create with specified logging behavior
+- (id)initWithLoggingBehavior:(NSString *)loggingBehavior;
+
+// Append string, or key/value pair
+- (void)appendString:(NSString *)string;
+- (void)appendFormat:(NSString *)formatString, ...;
+- (void)appendKey:(NSString *)key value:(NSString *)value;
+
+// Emit log, clearing out the logger contents.
+- (void)emitToNSLog;
+
+//
+// Class methods
+//
+
+//
+// Return a globally unique serial number to be used for correlating multiple output from the same logger.
+//
++ (NSUInteger)newSerialNumber;
+
+// Simple helper to write a single log entry, based upon whether the behavior matches a specified on.
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ logEntry:(NSString *)logEntry;
+
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ formatString:(NSString *)formatString, ...;
+
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ timestampTag:(NSObject *)timestampTag
+ formatString:(NSString *)formatString, ...;
+
+// Register a timestamp label with the "current" time, to then be retrieved by singleShotLogEntry
+// to include a duration.
++ (void)registerCurrentTime:(NSString *)loggingBehavior
+ withTag:(NSObject *)timestampTag;
+
+// When logging strings, replace all instances of 'replace' with instances of 'replaceWith'.
++ (void)registerStringToReplace:(NSString *)replace
+ replaceWith:(NSString *)replaceWith;
+
+@end
diff --git a/src/ios/facebook/FBLogger.m b/src/ios/facebook/FBLogger.m
new file mode 100644
index 000000000..9c52bc869
--- /dev/null
+++ b/src/ios/facebook/FBLogger.m
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBLogger.h"
+#import "FBSession.h"
+#import "FBSettings.h"
+#import "FBUtility.h"
+
+static NSUInteger g_serialNumberCounter = 1111;
+static NSMutableDictionary *g_stringsToReplace = nil;
+static NSMutableDictionary *g_startTimesWithTags = nil;
+
+@interface FBLogger ()
+
+@property (nonatomic, retain, readonly) NSMutableString *internalContents;
+
+@end
+
+@implementation FBLogger
+
+@synthesize internalContents = _internalContents;
+@synthesize isActive = _isActive;
+@synthesize loggingBehavior = _loggingBehavior;
+@synthesize loggerSerialNumber = _loggerSerialNumber;
+
+// Lifetime
+
+- (id)initWithLoggingBehavior:(NSString *)loggingBehavior {
+ if (self = [super init]) {
+ _isActive = [[FBSettings loggingBehavior] containsObject:loggingBehavior];
+ _loggingBehavior = loggingBehavior;
+ if (_isActive) {
+ _internalContents = [[NSMutableString alloc] init];
+ _loggerSerialNumber = [FBLogger newSerialNumber];
+ }
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [_internalContents release];
+ [super dealloc];
+}
+
+// Public properties
+
+- (NSString *)contents {
+ return _internalContents;
+}
+
+- (void)setContents:(NSString *)contents {
+ if (_isActive) {
+ [_internalContents release];
+ _internalContents = [NSMutableString stringWithString:contents];
+ }
+}
+
+// Public instance methods
+
+- (void)appendString:(NSString *)string {
+ if (_isActive) {
+ [_internalContents appendString:string];
+ }
+}
+
+- (void)appendFormat:(NSString *)formatString, ... {
+ if (_isActive) {
+ va_list vaArguments;
+ va_start(vaArguments, formatString);
+ NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease];
+ va_end(vaArguments);
+
+ [self appendString:logString];
+ }
+}
+
+
+- (void)appendKey:(NSString *)key value:(NSString *)value {
+ if (_isActive && [value length]) {
+ [_internalContents appendFormat:@" %@:\t%@\n", key, value];
+ }
+}
+
+- (void)emitToNSLog {
+ if (_isActive) {
+
+ for (NSString *key in [g_stringsToReplace keyEnumerator]) {
+ [_internalContents replaceOccurrencesOfString:key
+ withString:[g_stringsToReplace objectForKey:key]
+ options:NSLiteralSearch
+ range:NSMakeRange(0, _internalContents.length)];
+ }
+
+ // Xcode 4.4 hangs on extremely long NSLog output (http://openradar.appspot.com/11972490). Truncate if needed.
+ const int MAX_LOG_STRING_LENGTH = 10000;
+ NSString *logString = _internalContents;
+ if (_internalContents.length > MAX_LOG_STRING_LENGTH) {
+ logString = [NSString stringWithFormat:@"TRUNCATED: %@", [_internalContents substringToIndex:MAX_LOG_STRING_LENGTH]];
+ }
+ NSLog(@"FBSDKLog: %@", logString);
+
+ [_internalContents setString:@""];
+ }
+}
+
+// Public static methods
+
++ (NSUInteger)newSerialNumber {
+ return g_serialNumberCounter++;
+}
+
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ logEntry:(NSString *)logEntry {
+ if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) {
+ FBLogger *logger = [[FBLogger alloc] initWithLoggingBehavior:loggingBehavior];
+ [logger appendString:logEntry];
+ [logger emitToNSLog];
+ [logger release];
+ }
+}
+
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ formatString:(NSString *)formatString, ... {
+
+ if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) {
+ va_list vaArguments;
+ va_start(vaArguments, formatString);
+ NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease];
+ va_end(vaArguments);
+
+ [self singleShotLogEntry:loggingBehavior logEntry:logString];
+ }
+}
+
+
++ (void)singleShotLogEntry:(NSString *)loggingBehavior
+ timestampTag:(NSObject *)timestampTag
+ formatString:(NSString *)formatString, ... {
+
+ if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) {
+ va_list vaArguments;
+ va_start(vaArguments, formatString);
+ NSString *logString = [[[NSString alloc] initWithFormat:formatString arguments:vaArguments] autorelease];
+ va_end(vaArguments);
+
+ // Start time of this "timestampTag" is stashed in the dictionary.
+ // Treat the incoming object tag simply as an address, since it's only used to identify during lifetime. If
+ // we send in as an object, the dictionary will try to copy it.
+ NSNumber *tagAsNumber = [NSNumber numberWithUnsignedLong:(unsigned long)(void *)timestampTag];
+ NSNumber *startTimeNumber = [g_startTimesWithTags objectForKey:tagAsNumber];
+
+ // Only log if there's been an associated start time.
+ if (startTimeNumber) {
+ unsigned long elapsed = [FBUtility currentTimeInMilliseconds] - startTimeNumber.unsignedLongValue;
+ [g_startTimesWithTags removeObjectForKey:tagAsNumber]; // served its purpose, remove
+
+ // Log string is appended with "%d msec", with nothing intervening. This gives the most control to the caller.
+ logString = [NSString stringWithFormat:@"%@%lu msec", logString, elapsed];
+
+ [self singleShotLogEntry:loggingBehavior logEntry:logString];
+ }
+ }
+}
+
++ (void)registerCurrentTime:(NSString *)loggingBehavior
+ withTag:(NSObject *)timestampTag {
+
+ if ([[FBSettings loggingBehavior] containsObject:loggingBehavior]) {
+
+ if (!g_startTimesWithTags) {
+ g_startTimesWithTags = [[NSMutableDictionary alloc] init];
+ }
+
+ FBConditionalLog(g_startTimesWithTags.count < 1000,
+ @"Unexpectedly large number of outstanding perf logging start times, something is likely wrong.");
+
+ unsigned long currTime = [FBUtility currentTimeInMilliseconds];
+
+ // Treat the incoming object tag simply as an address, since it's only used to identify during lifetime. If
+ // we send in as an object, the dictionary will try to copy it.
+ unsigned long tagAsNumber = (unsigned long)(void *)timestampTag;
+ [g_startTimesWithTags setObject:[NSNumber numberWithUnsignedLong:currTime]
+ forKey:[NSNumber numberWithUnsignedLong:tagAsNumber]];
+ }
+}
+
+
++ (void)registerStringToReplace:(NSString *)replace
+ replaceWith:(NSString *)replaceWith {
+
+ // Strings sent in here never get cleaned up, but that's OK, don't ever expect too many.
+
+ if ([[FBSettings loggingBehavior] count] > 0) { // otherwise there's no logging.
+
+ if (!g_stringsToReplace) {
+ g_stringsToReplace = [[NSMutableDictionary alloc] init];
+ }
+
+ [g_stringsToReplace setValue:replaceWith forKey:replace];
+ }
+}
+
+
+
+@end
diff --git a/src/ios/facebook/FBLoginDialog.h b/src/ios/facebook/FBLoginDialog.h
new file mode 100644
index 000000000..cd364a40e
--- /dev/null
+++ b/src/ios/facebook/FBLoginDialog.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#import "FBDialog.h"
+
+@protocol FBLoginDialogDelegate;
+
+/**
+ * Do not use this interface directly, instead, use authorize in Facebook.h
+ *
+ * Facebook Login Dialog interface for start the facebook webView login dialog.
+ * It start pop-ups prompting for credentials and permissions.
+ */
+
+@interface FBLoginDialog : FBDialog {
+ id _loginDelegate;
+}
+
+-(id) initWithURL:(NSString *) loginURL
+ loginParams:(NSMutableDictionary *) params
+ delegate:(id ) delegate;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+@protocol FBLoginDialogDelegate
+
+- (void)fbDialogLogin:(NSString*)token expirationDate:(NSDate*)expirationDate;
+
+- (void)fbDialogNotLogin:(BOOL)cancelled;
+
+@end
+
+
diff --git a/src/ios/facebook/FBLoginDialog.m b/src/ios/facebook/FBLoginDialog.m
new file mode 100644
index 000000000..5c70a3f40
--- /dev/null
+++ b/src/ios/facebook/FBLoginDialog.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBDialog.h"
+#import "FBLoginDialog.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation FBLoginDialog
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// public
+
+/*
+ * initialize the FBLoginDialog with url and parameters
+ */
+- (id)initWithURL:(NSString*) loginURL
+ loginParams:(NSMutableDictionary*) params
+ delegate:(id ) delegate{
+
+ self = [super init];
+ _serverURL = [loginURL retain];
+ _params = [params retain];
+ _loginDelegate = delegate;
+ return self;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// FBDialog
+
+/**
+ * Override FBDialog : to call when the webView Dialog did succeed
+ */
+- (void) dialogDidSucceed:(NSURL*)url {
+ NSString *q = [url absoluteString];
+ NSString *token = [self getStringFromUrl:q needle:@"access_token="];
+ NSString *expTime = [self getStringFromUrl:q needle:@"expires_in="];
+ NSDate *expirationDate =nil;
+
+ if (expTime != nil) {
+ int expVal = [expTime intValue];
+ if (expVal == 0) {
+ expirationDate = [NSDate distantFuture];
+ } else {
+ expirationDate = [NSDate dateWithTimeIntervalSinceNow:expVal];
+ }
+ }
+
+ if ((token == (NSString *) [NSNull null]) || (token.length == 0)) {
+ [self dialogDidCancel:url];
+ [self dismissWithSuccess:NO animated:YES];
+ } else {
+ if ([_loginDelegate respondsToSelector:@selector(fbDialogLogin:expirationDate:)]) {
+ [_loginDelegate fbDialogLogin:token expirationDate:expirationDate];
+ }
+ [self dismissWithSuccess:YES animated:YES];
+ }
+
+}
+
+/**
+ * Override FBDialog : to call with the login dialog get canceled
+ */
+- (void)dialogDidCancel:(NSURL *)url {
+ [self dismissWithSuccess:NO animated:YES];
+ if ([_loginDelegate respondsToSelector:@selector(fbDialogNotLogin:)]) {
+ [_loginDelegate fbDialogNotLogin:YES];
+ }
+}
+
+- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
+ if (!(([error.domain isEqualToString:@"NSURLErrorDomain"] && error.code == -999) ||
+ ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102))) {
+ [super webView:webView didFailLoadWithError:error];
+ if ([_loginDelegate respondsToSelector:@selector(fbDialogNotLogin:)]) {
+ [_loginDelegate fbDialogNotLogin:NO];
+ }
+ }
+}
+
+@end
diff --git a/src/ios/facebook/FBLoginView.h b/src/ios/facebook/FBLoginView.h
new file mode 100644
index 000000000..f3a1106ae
--- /dev/null
+++ b/src/ios/facebook/FBLoginView.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBSession.h"
+#import "FBGraphUser.h"
+
+@protocol FBLoginViewDelegate;
+
+/*!
+ @class
+ @abstract
+ */
+@interface FBLoginView : UIView
+
+/*!
+ @abstract
+ The permissions to login with. Defaults to nil, meaning basic permissions.
+
+ @discussion Methods and properties that specify permissions without a read or publish
+ qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred.
+ */
+@property (readwrite, copy) NSArray *permissions __attribute__((deprecated));
+
+/*!
+ @abstract
+ The read permissions to request if the user logs in via this view.
+
+ @discussion
+ Note, that if read permissions are specified, then publish permissions should not be specified.
+ */
+@property (nonatomic, copy) NSArray *readPermissions;
+
+/*!
+ @abstract
+ The publish permissions to request if the user logs in via this view.
+
+ @discussion
+ Note, that a defaultAudience value of FBSessionDefaultAudienceOnlyMe, FBSessionDefaultAudienceEveryone, or
+ FBSessionDefaultAudienceFriends should be set if publish permissions are specified. Additionally, when publish
+ permissions are specified, then read should not be specified.
+ */
+@property (nonatomic, copy) NSArray *publishPermissions;
+
+/*!
+ @abstract
+ The default audience to use, if publish permissions are requested at login time.
+ */
+@property (nonatomic, assign) FBSessionDefaultAudience defaultAudience;
+
+
+/*!
+ @abstract
+ Initializes and returns an `FBLoginView` object. The underlying session has basic permissions granted to it.
+ */
+- (id)init;
+
+/*!
+ @method
+
+ @abstract
+ Initializes and returns an `FBLoginView` object constructed with the specified permissions.
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil will indicates basic permissions.
+
+ @discussion Methods and properties that specify permissions without a read or publish
+ qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred.
+ */
+- (id)initWithPermissions:(NSArray *)permissions __attribute__((deprecated));
+
+/*!
+ @method
+
+ @abstract
+ Initializes and returns an `FBLoginView` object constructed with the specified permissions.
+
+ @param readPermissions An array of strings representing the read permissions to request during the
+ authentication flow. A value of nil will indicates basic permissions.
+
+ */
+- (id)initWithReadPermissions:(NSArray *)readPermissions;
+
+/*!
+ @method
+
+ @abstract
+ Initializes and returns an `FBLoginView` object constructed with the specified permissions.
+
+ @param publishPermissions An array of strings representing the publish permissions to request during the
+ authentication flow.
+
+ @param defaultAudience An audience for published posts; note that FBSessionDefaultAudienceNone is not valid
+ for permission requests that include publish or manage permissions.
+
+ */
+- (id)initWithPublishPermissions:(NSArray *)publishPermissions
+ defaultAudience:(FBSessionDefaultAudience)defaultAudience;
+
+/*!
+ @abstract
+ The delegate object that receives updates for selection and display control.
+ */
+@property (nonatomic, assign) IBOutlet id delegate;
+
+@end
+
+/*!
+ @protocol
+
+ @abstract
+ The `FBLoginViewDelegate` protocol defines the methods used to receive event
+ notifications from `FBLoginView` objects.
+ */
+@protocol FBLoginViewDelegate
+
+@optional
+
+/*!
+ @abstract
+ Tells the delegate that the view is now in logged in mode
+
+ @param loginView The login view that transitioned its view mode
+ */
+- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView;
+
+/*!
+ @abstract
+ Tells the delegate that the view is has now fetched user info
+
+ @param loginView The login view that transitioned its view mode
+
+ @param user The user info object describing the logged in user
+ */
+- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
+ user:(id)user;
+
+/*!
+ @abstract
+ Tells the delegate that the view is now in logged out mode
+
+ @param loginView The login view that transitioned its view mode
+ */
+- (void)loginViewShowingLoggedOutUser:(FBLoginView *)loginView;
+
+@end
+
diff --git a/src/ios/facebook/FBLoginView.m b/src/ios/facebook/FBLoginView.m
new file mode 100644
index 000000000..d2f858098
--- /dev/null
+++ b/src/ios/facebook/FBLoginView.m
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBLoginView.h"
+#import "FBProfilePictureView.h"
+#import "FBRequest.h"
+#import "FBRequestConnection+Internal.h"
+#import "FBSession.h"
+#import "FBGraphUser.h"
+#import "FBUtility.h"
+
+static NSString *const FBLoginViewCacheIdentity = @"FBLoginView";
+const int kButtonLabelX = 46;
+
+CGSize g_imageSize;
+
+@interface FBLoginView()
+
+- (void)initialize;
+- (void)buttonPressed:(id)sender;
+- (void)configureViewForStateLoggedIn:(BOOL)isLoggedIn;
+- (void)wireViewForSession:(FBSession *)session;
+- (void)wireViewForSessionWithoutOpening:(FBSession *)session;
+- (void)unwireViewForSession ;
+- (void)fetchMeInfo;
+- (void)informDelegate:(BOOL)userOnly;
+- (void)handleActiveSessionSetNotifications:(NSNotification *)notification;
+- (void)handleActiveSessionUnsetNotifications:(NSNotification *)notification;
+
+@property (retain, nonatomic) UILabel *label;
+@property (retain, nonatomic) UIButton *button;
+@property (retain, nonatomic) FBSession *session;
+@property (retain, nonatomic) FBRequestConnection *request;
+@property (retain, nonatomic) id user;
+
+@end
+
+@implementation FBLoginView
+
+@synthesize delegate = _delegate,
+ label = _label,
+ button = _button,
+ session = _session,
+ request = _request,
+ user = _user,
+ permissions = _permissions,
+ readPermissions = _readPermissions,
+ publishPermissions = _publishPermissions,
+ defaultAudience = _defaultAudience;
+
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ [self initialize];
+ }
+ return self;
+}
+
+- (id)initWithPermissions:(NSArray *)permissions {
+ self = [super init];
+ if (self) {
+ self.permissions = permissions;
+ [self initialize];
+ }
+ return self;
+}
+
+- (id)initWithReadPermissions:(NSArray *)readPermissions {
+ self = [super init];
+ if (self) {
+ self.readPermissions = readPermissions;
+ [self initialize];
+ }
+ return self;
+}
+
+- (id)initWithPublishPermissions:(NSArray *)publishPermissions
+ defaultAudience:(FBSessionDefaultAudience)defaultAudience {
+ self = [super init];
+ if (self) {
+ self.publishPermissions = publishPermissions;
+ self.defaultAudience = defaultAudience;
+ [self initialize];
+ }
+ return self;
+}
+
+- (id)initWithFrame:(CGRect)aRect {
+ self = [super initWithFrame:aRect];
+ if (self) {
+ [self initialize];
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super initWithCoder:aDecoder];
+ if (self) {
+ [self initialize];
+ }
+ return self;
+}
+
+- (void)dealloc {
+
+ // removes all observers for self
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ // if we have an outstanding request, cancel
+ [self.request cancel];
+
+ [_request release];
+ [_label release];
+ [_button release];
+ [_session release];
+ [_user release];
+ [_permissions release];
+
+ [super dealloc];
+}
+
+- (void)setDelegate:(id)newValue {
+ if (_delegate != newValue) {
+ _delegate = newValue;
+
+ // whenever the delegate value changes, we schedule one initial call to inform the delegate
+ // of our current state; we use a delay in order to avoid a callback in a setup or init method
+ [self performSelector:@selector(informDelegate:)
+ withObject:nil
+ afterDelay:.01];
+ }
+}
+
+- (void)initialize {
+ // the base class can cause virtual recursion, so
+ // to handle this we make initialize idempotent
+ if (self.button) {
+ return;
+ }
+
+ // setup view
+ self.autoresizesSubviews = YES;
+ self.clipsToBounds = YES;
+
+ // if our session has a cached token ready, we open it; note that it is important
+ // that we open the session before notification wiring is in place
+ [FBSession openActiveSessionWithAllowLoginUI:NO];
+
+ // wire-up the current session to the login view, before adding global session-change handlers
+ [self wireViewForSession:FBSession.activeSession];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleActiveSessionSetNotifications:)
+ name:FBSessionDidSetActiveSessionNotification
+ object:nil];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleActiveSessionUnsetNotifications:)
+ name:FBSessionDidUnsetActiveSessionNotification
+ object:nil];
+
+ // setup button
+ self.button = [UIButton buttonWithType:UIButtonTypeCustom];
+ [self.button addTarget:self
+ action:@selector(buttonPressed:)
+ forControlEvents:UIControlEventTouchUpInside];
+ self.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
+ self.button.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+
+ UIImage *image = [[UIImage imageNamed:@"FacebookSDKResources.bundle/FBLoginView/images/login-button-small.png"]
+ stretchableImageWithLeftCapWidth:kButtonLabelX topCapHeight:0];
+ g_imageSize = image.size;
+ [self.button setBackgroundImage:image forState:UIControlStateNormal];
+
+ image = [[UIImage imageNamed:@"FacebookSDKResources.bundle/FBLoginView/images/login-button-small-pressed.png"]
+ stretchableImageWithLeftCapWidth:kButtonLabelX topCapHeight:0];
+ [self.button setBackgroundImage:image forState:UIControlStateHighlighted];
+
+ [self addSubview:self.button];
+
+ // add a label that will appear over the button
+ self.label = [[[UILabel alloc] init] autorelease];
+ self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+ self.label.textAlignment = UITextAlignmentCenter;
+ self.label.backgroundColor = [UIColor clearColor];
+ self.label.font = [UIFont boldSystemFontOfSize:16.0];
+ self.label.textColor = [UIColor whiteColor];
+ self.label.shadowColor = [UIColor blackColor];
+ self.label.shadowOffset = CGSizeMake(0.0, -1.0);
+ [self addSubview:self.label];
+
+ // We force our height to be the same as the image, but we will let someone make us wider
+ // than the default image.
+ CGFloat width = MAX(self.frame.size.width, g_imageSize.width);
+ CGRect frame = CGRectMake(self.frame.origin.x, self.frame.origin.y,
+ width, image.size.height);
+ self.frame = frame;
+
+ CGRect buttonFrame = CGRectMake(0, 0, width, image.size.height);
+ self.button.frame = buttonFrame;
+
+ self.label.frame = CGRectMake(kButtonLabelX, 0, width - kButtonLabelX, image.size.height);
+
+ self.backgroundColor = [UIColor clearColor];
+
+ if (self.session.isOpen) {
+ [self fetchMeInfo];
+ [self configureViewForStateLoggedIn:YES];
+ } else {
+ [self configureViewForStateLoggedIn:NO];
+ }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+ CGSize logInSize = [[self logInText] sizeWithFont:self.label.font];
+ CGSize logOutSize = [[self logOutText] sizeWithFont:self.label.font];
+
+ // Leave at least a small margin around the label.
+ CGFloat desiredWidth = kButtonLabelX + 20 + MAX(logInSize.width, logOutSize.width);
+ // Never get smaller than the image
+ CGFloat width = MAX(desiredWidth, g_imageSize.width);
+
+ return CGSizeMake(width, g_imageSize.height);
+}
+
+- (NSString *)logInText {
+ return [FBUtility localizedStringForKey:@"FBLV:LogInButton" withDefault:@"Log In"];
+}
+
+- (NSString *)logOutText {
+ return [FBUtility localizedStringForKey:@"FBLV:LogOutButton" withDefault:@"Log Out"];
+}
+
+- (void)configureViewForStateLoggedIn:(BOOL)isLoggedIn {
+ if (isLoggedIn) {
+ self.label.text = [self logOutText];
+ } else {
+ self.label.text = [self logInText];
+ self.user = nil;
+ }
+}
+
+- (void)fetchMeInfo {
+ FBRequest *request = [FBRequest requestForMe];
+ [request setSession:self.session];
+ self.request = [[[FBRequestConnection alloc] init] autorelease];
+ [self.request addRequest:request
+ completionHandler:^(FBRequestConnection *connection, NSMutableDictionary *result, NSError *error) {
+ if (result) {
+ self.user = result;
+ [self informDelegate:YES];
+ } else {
+ self.user = nil;
+ }
+ self.request = nil;
+ }];
+ [self.request startWithCacheIdentity:FBLoginViewCacheIdentity
+ skipRoundtripIfCached:YES];
+
+}
+
+- (void)informDelegate:(BOOL)userOnly {
+ if (userOnly) {
+ if ([self.delegate respondsToSelector:@selector(loginViewFetchedUserInfo:user:)]) {
+ [self.delegate loginViewFetchedUserInfo:self
+ user:self.user];
+ }
+ } else if (FBSession.activeSession.isOpen) {
+ if ([self.delegate respondsToSelector:@selector(loginViewShowingLoggedInUser:)]) {
+ [self.delegate loginViewShowingLoggedInUser:self];
+ }
+ // any time we inform/reinform of isOpen event, we want to be sure
+ // to repass the user if we have it
+ if (self.user) {
+ [self informDelegate:YES];
+ }
+ } else {
+ if ([self.delegate respondsToSelector:@selector(loginViewShowingLoggedOutUser:)]) {
+ [self.delegate loginViewShowingLoggedOutUser:self];
+ }
+ }
+}
+
+- (void)wireViewForSessionWithoutOpening:(FBSession *)session {
+ // if there is an outstanding request for the previous session, cancel
+ [self.request cancel];
+ self.request = nil;
+
+ self.session = session;
+
+ // register a KVO observer
+ [self.session addObserver:self
+ forKeyPath:@"state"
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+}
+
+- (void)wireViewForSession:(FBSession *)session {
+ [self wireViewForSessionWithoutOpening:session];
+
+ // anytime we find that our session is created with an available token
+ // we open it on the spot
+ if (self.session.state == FBSessionStateCreatedTokenLoaded) {
+ [FBSession openActiveSessionWithAllowLoginUI:NO];
+ }
+}
+
+- (void)unwireViewForSession {
+ // this line of code is the main reason we need to hold on
+ // to the session object at all
+ [self.session removeObserver:self
+ forKeyPath:@"state"];
+ self.session = nil;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary *)change
+ context:(void *)context {
+ if (self.session.isOpen) {
+ [self fetchMeInfo];
+ [self configureViewForStateLoggedIn:YES];
+ } else {
+ [self configureViewForStateLoggedIn:NO];
+ }
+ [self informDelegate:NO];
+}
+
+- (void)handleActiveSessionSetNotifications:(NSNotification *)notification {
+ // NSNotificationCenter is a global channel, so we guard against
+ // unexpected uses of this notification the best we can
+ if ([notification.object isKindOfClass:[FBSession class]]) {
+ [self wireViewForSession:notification.object];
+ }
+}
+
+- (void)handleActiveSessionUnsetNotifications:(NSNotification *)notification {
+ [self unwireViewForSession];
+}
+
+- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
+{
+ if (buttonIndex == 0) { // logout
+ [FBSession.activeSession closeAndClearTokenInformation];
+ }
+}
+
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+- (void)buttonPressed:(id)sender {
+ if (self.session == FBSession.activeSession) {
+ if (!self.session.isOpen) { // login
+
+ // the policy here is:
+ // 1) if you provide unspecified permissions, then we fall back on legacy fast-app-switch
+ // 2) if you provide only read permissions, then we call a read-based open method that will use integrated auth
+ // 3) if you provide any publish permissions, then we combine the read-set and publish-set and call the publish-based
+ // method that will use integrated auth when availab le
+ // 4) if you provide any publish permissions, and don't specify a valid audience, the control will throw an exception
+ // when the user presses login
+ if (self.permissions) {
+ [FBSession openActiveSessionWithPermissions:self.permissions
+ allowLoginUI:YES
+ completionHandler:nil];
+ } else if (![self.publishPermissions count]) {
+ [FBSession openActiveSessionWithReadPermissions:self.publishPermissions
+ allowLoginUI:YES
+ completionHandler:nil];
+ } else {
+ // combined read and publish permissions will usually fail, but if the app wants us to
+ // try it here, then we will pass the aggregate set to the server
+ NSArray *permissions = self.publishPermissions;
+ if ([self.readPermissions count]) {
+ NSMutableSet *set = [NSMutableSet setWithArray:self.publishPermissions];
+ [set addObjectsFromArray:self.readPermissions];
+ permissions = [set allObjects];
+ }
+ [FBSession openActiveSessionWithPublishPermissions:permissions
+ defaultAudience:self.defaultAudience
+ allowLoginUI:YES
+ completionHandler:nil];
+ }
+ } else { // logout action sheet
+ NSString *name = self.user.name;
+ NSString *title = nil;
+ if (name) {
+ title = [NSString stringWithFormat:[FBUtility localizedStringForKey:@"FBLV:LoggedInAs"
+ withDefault:@"Logged in as %@"], name];
+ } else {
+ title = [FBUtility localizedStringForKey:@"FBLV:LoggedInUsingFacebook"
+ withDefault:@"Logged in using Facebook"];
+ }
+
+ NSString *cancelTitle = [FBUtility localizedStringForKey:@"FBLV:CancelAction"
+ withDefault:@"Cancel"];
+ NSString *logOutTitle = [FBUtility localizedStringForKey:@"FBLV:LogOutAction"
+ withDefault:@"Log Out"];
+ UIActionSheet *sheet = [[[UIActionSheet alloc] initWithTitle:title
+ delegate:self
+ cancelButtonTitle:cancelTitle
+ destructiveButtonTitle:logOutTitle
+ otherButtonTitles:nil]
+ autorelease];
+ // Show the sheet
+ [sheet showInView:self];
+ }
+ } else { // state of view out of sync with active session
+ // so resync
+ [self unwireViewForSession];
+ [self wireViewForSession:FBSession.activeSession];
+ [self configureViewForStateLoggedIn:self.session.isOpen];
+ [self informDelegate:NO];
+ }
+}
+#pragma GCC diagnostic warning "-Wdeprecated-declarations"
+@end
diff --git a/src/ios/facebook/FBNativeDialogs.h b/src/ios/facebook/FBNativeDialogs.h
new file mode 100644
index 000000000..c866649d8
--- /dev/null
+++ b/src/ios/facebook/FBNativeDialogs.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+
+@class FBSession;
+
+/*!
+ @typedef FBNativeDialogResult enum
+
+ @abstract
+ Passed to a handler to indicate the result of a dialog being displayed to the user.
+*/
+typedef enum {
+ /*! Indicates that the dialog action completed successfully. */
+ FBNativeDialogResultSucceeded,
+ /*! Indicates that the dialog action was cancelled (either by the user or the system). */
+ FBNativeDialogResultCancelled,
+ /*! Indicates that the dialog could not be shown (because not on ios6 or ios6 auth was not used). */
+ FBNativeDialogResultError
+} FBNativeDialogResult;
+
+/*!
+ @typedef
+
+ @abstract Defines a handler that will be called in response to the native share dialog
+ being displayed.
+ */
+typedef void (^FBShareDialogHandler)(FBNativeDialogResult result, NSError *error);
+
+/*!
+ @class FBNativeDialogs
+
+ @abstract
+ Provides methods to display native (i.e., non-Web-based) dialogs to the user.
+ Currently the iOS 6 sharing dialog is supported.
+*/
+@interface FBNativeDialogs : NSObject
+
+/*!
+ @abstract
+ Presents a dialog that allows the user to share a status update that may include
+ text, images, or URLs. This dialog is only available on iOS 6.0 and above. The
+ current active session returned by [FBSession activeSession] will be used to determine
+ whether the dialog will be displayed. If a session is active, it must be open and the
+ login method used to authenticate the user must be native iOS 6.0 authentication.
+ If no session active, then whether the call succeeds or not will depend on
+ whether Facebook integration has been configured.
+
+ @param viewController The view controller which will present the dialog.
+
+ @param initialText The text which will initially be populated in the dialog. The user
+ will have the opportunity to edit this text before posting it. May be nil.
+
+ @param image A UIImage that will be attached to the status update. May be nil.
+
+ @param url An NSURL that will be attached to the status update. May be nil.
+
+ @param handler A handler that will be called when the dialog is dismissed, or if an error
+ occurs. May be nil.
+
+ @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler
+ will still be called, with an error indicating the reason the dialog was not displayed)
+ */
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ initialText:(NSString*)initialText
+ image:(UIImage*)image
+ url:(NSURL*)url
+ handler:(FBShareDialogHandler)handler;
+
+/*!
+ @abstract
+ Presents a dialog that allows the user to share a status update that may include
+ text, images, or URLs. This dialog is only available on iOS 6.0 and above. The
+ current active session returned by [FBSession activeSession] will be used to determine
+ whether the dialog will be displayed. If a session is active, it must be open and the
+ login method used to authenticate the user must be native iOS 6.0 authentication.
+ If no session active, then whether the call succeeds or not will depend on
+ whether Facebook integration has been configured.
+
+ @param viewController The view controller which will present the dialog.
+
+ @param initialText The text which will initially be populated in the dialog. The user
+ will have the opportunity to edit this text before posting it. May be nil.
+
+ @param images An array of UIImages that will be attached to the status update. May
+ be nil.
+
+ @param urls An array of NSURLs that will be attached to the status update. May be nil.
+
+ @param handler A handler that will be called when the dialog is dismissed, or if an error
+ occurs. May be nil.
+
+ @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler
+ will still be called, with an error indicating the reason the dialog was not displayed)
+ */
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ initialText:(NSString*)initialText
+ images:(NSArray*)images
+ urls:(NSArray*)urls
+ handler:(FBShareDialogHandler)handler;
+
+/*!
+ @abstract
+ Presents a dialog that allows the user to share a status update that may include
+ text, images, or URLs. This dialog is only available on iOS 6.0 and above. An
+ may be specified, or nil may be passed to indicate that the current
+ active session should be used. If a session is specified (whether explicitly or by
+ virtue of being the active session), it must be open and the login method used to
+ authenticate the user must be native iOS 6.0 authentication. If no session is specified
+ (and there is no active session), then whether the call succeeds or not will depend on
+ whether Facebook integration has been configured.
+
+ @param viewController The view controller which will present the dialog.
+
+ @param session The to use to determine whether or not the user has been
+ authenticated with iOS native authentication. If nil, then [FBSession activeSession]
+ will be checked. See discussion above for the implications of nil or non-nil session.
+
+ @param initialText The text which will initially be populated in the dialog. The user
+ will have the opportunity to edit this text before posting it. May be nil.
+
+ @param images An array of UIImages that will be attached to the status update. May
+ be nil.
+
+ @param urls An array of NSURLs that will be attached to the status update. May be nil.
+
+ @param handler A handler that will be called when the dialog is dismissed, or if an error
+ occurs. May be nil.
+
+ @return YES if the dialog was presented, NO if not (in the case of a NO result, the handler
+ will still be called, with an error indicating the reason the dialog was not displayed)
+ */
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ session:(FBSession*)session
+ initialText:(NSString*)initialText
+ images:(NSArray*)images
+ urls:(NSArray*)urls
+ handler:(FBShareDialogHandler)handler;
+
+/*!
+ @abstract
+ Determines whether a call to presentShareDialogModallyFrom: will successfully present
+ a dialog. This is useful for applications that need to modify the available UI controls
+ depending on whether the dialog is available on the current platform and for the current
+ user.
+
+ @param session The to use to determine whether or not the user has been
+ authenticated with iOS native authentication. If nil, then [FBSession activeSession]
+ will be checked. See discussion above for the implications of nil or non-nil session.
+
+ @return YES if the dialog would be presented for the session, and NO if not
+ */
++ (BOOL)canPresentShareDialogWithSession:(FBSession*)session;
+
+@end
diff --git a/src/ios/facebook/FBNativeDialogs.m b/src/ios/facebook/FBNativeDialogs.m
new file mode 100644
index 000000000..509096cc8
--- /dev/null
+++ b/src/ios/facebook/FBNativeDialogs.m
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBNativeDialogs.h"
+#import "FBSession.h"
+#import "FBError.h"
+#import "FBUtility.h"
+#import "Social/Social.h"
+
+@interface FBNativeDialogs ()
+
++ (NSError*)createError:(NSString*)reason;
+
+@end
+
+@implementation FBNativeDialogs
+
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ initialText:(NSString*)initialText
+ image:(UIImage*)image
+ url:(NSURL*)url
+ handler:(FBShareDialogHandler)handler {
+ NSArray *images = image ? [NSArray arrayWithObject:image] : nil;
+ NSArray *urls = url ? [NSArray arrayWithObject:url] : nil;
+
+ return [self presentShareDialogModallyFrom:viewController
+ session:nil
+ initialText:initialText
+ images:images
+ urls:urls
+ handler:handler];
+}
+
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ initialText:(NSString*)initialText
+ images:(NSArray*)images
+ urls:(NSArray*)urls
+ handler:(FBShareDialogHandler)handler {
+
+ return [self presentShareDialogModallyFrom:viewController
+ session:nil
+ initialText:initialText
+ images:images
+ urls:urls
+ handler:handler];
+}
+
++ (BOOL)presentShareDialogModallyFrom:(UIViewController*)viewController
+ session:(FBSession*)session
+ initialText:(NSString*)initialText
+ images:(NSArray*)images
+ urls:(NSArray*)urls
+ handler:(FBShareDialogHandler)handler {
+
+ SLComposeViewController *composeViewController = [FBNativeDialogs composeViewControllerWithSession:session
+ handler:handler];
+ if (!composeViewController) {
+ return NO;
+ }
+
+ if (initialText) {
+ [composeViewController setInitialText:initialText];
+ }
+ if (images && images.count > 0) {
+ for (UIImage *image in images) {
+ [composeViewController addImage:image];
+ }
+ }
+ if (urls && urls.count > 0) {
+ for (NSURL *url in urls) {
+ [composeViewController addURL:url];
+ }
+ }
+
+ [composeViewController setCompletionHandler:^(SLComposeViewControllerResult result) {
+ BOOL cancelled = (result == SLComposeViewControllerResultCancelled);
+ if (handler) {
+ handler(cancelled ? FBNativeDialogResultCancelled : FBNativeDialogResultSucceeded, nil);
+ }
+ }];
+
+ [viewController presentModalViewController:composeViewController animated:YES];
+
+ return YES;
+}
+
++ (BOOL)canPresentShareDialogWithSession:(FBSession*)session {
+ return [FBNativeDialogs composeViewControllerWithSession:session
+ handler:nil] != nil;
+}
+
++ (SLComposeViewController*)composeViewControllerWithSession:(FBSession*)session
+ handler:(FBShareDialogHandler)handler {
+ // Can we even call the iOS API?
+ Class composeViewControllerClass = [SLComposeViewController class];
+ if (composeViewControllerClass == nil ||
+ [composeViewControllerClass isAvailableForServiceType:SLServiceTypeFacebook] == NO) {
+ if (handler) {
+ handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogNotSupported]);
+ }
+ return nil;
+ }
+
+ if (session == nil) {
+ // No session provided -- do we have an activeSession? We must either have a session that
+ // was authenticated with native auth, or no session at all (in which case the app is
+ // running unTOSed and we will rely on the OS to authenticate/TOS the user).
+ session = [FBSession activeSession];
+ }
+ if (session != nil) {
+ // If we have an open session and it's not native auth, fail. If the session is
+ // not open, attempting to put up the dialog will prompt the user to configure
+ // their account.
+ if (session.isOpen && session.loginType != FBSessionLoginTypeSystemAccount) {
+ if (handler) {
+ handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogInvalidForSession]);
+ }
+ return nil;
+ }
+ }
+
+ SLComposeViewController *composeViewController = [composeViewControllerClass composeViewControllerForServiceType:SLServiceTypeFacebook];
+ if (composeViewController == nil) {
+ if (handler) {
+ handler(FBNativeDialogResultError, [self createError:FBErrorNativeDialogCantBeDisplayed]);
+ }
+ return nil;
+ }
+ return composeViewController;
+}
+
++ (NSError*)createError:(NSString*)reason {
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:reason, FBErrorNativeDialogReasonKey, nil];
+ NSError *error = [NSError errorWithDomain:FacebookSDKDomain
+ code:FBErrorNativeDialog
+ userInfo:userInfo];
+ return error;
+}
+
+@end
diff --git a/src/ios/facebook/FBOpenGraphAction.h b/src/ios/facebook/FBOpenGraphAction.h
new file mode 100644
index 000000000..599e8eb4e
--- /dev/null
+++ b/src/ios/facebook/FBOpenGraphAction.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/xlicenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBGraphObject.h"
+
+@protocol FBGraphPlace;
+@protocol FBGraphUser;
+
+/*!
+ @protocol
+
+ @abstract
+ The `FBOpenGraphAction` protocol is the base protocol for use in posting and retrieving Open Graph actions.
+ It inherits from the `FBGraphObject` protocol; you may derive custome protocols from `FBOpenGraphAction` in order
+ implement typed access to your application's custom actions.
+
+ @discussion
+ Represents an Open Graph custom action, to be used directly, or from which to
+ derive custom action protocols with custom properties.
+ */
+@protocol FBOpenGraphAction
+
+/*!
+ @property
+ @abstract Typed access to action's id
+ */
+@property (retain, nonatomic) NSString *id;
+
+/*!
+ @property
+ @abstract Typed access to action's start time
+ */
+@property (retain, nonatomic) NSString *start_time;
+
+/*!
+ @property
+ @abstract Typed access to action's end time
+ */
+@property (retain, nonatomic) NSString *end_time;
+
+/*!
+ @property
+ @abstract Typed access to action's publication time
+ */
+@property (retain, nonatomic) NSString *publish_time;
+
+/*!
+ @property
+ @abstract Typed access to action's creation time
+ */
+@property (retain, nonatomic) NSString *created_time;
+
+/*!
+ @property
+ @abstract Typed access to action's expiration time
+ */
+@property (retain, nonatomic) NSString *expires_time;
+
+/*!
+ @property
+ @abstract Typed access to action's ref
+ */
+@property (retain, nonatomic) NSString *ref;
+
+/*!
+ @property
+ @abstract Typed access to action's user message
+ */
+@property (retain, nonatomic) NSString *message;
+
+/*!
+ @property
+ @abstract Typed access to action's place
+ */
+@property (retain, nonatomic) id place;
+
+/*!
+ @property
+ @abstract Typed access to action's tags
+ */
+@property (retain, nonatomic) NSArray *tags;
+
+/*!
+ @property
+ @abstract Typed access to action's images
+ */
+@property (retain, nonatomic) NSArray *image;
+
+/*!
+ @property
+ @abstract Typed access to action's from-user
+ */
+@property (retain, nonatomic) id from;
+
+/*!
+ @property
+ @abstract Typed access to action's likes
+ */
+@property (retain, nonatomic) NSArray *likes;
+
+/*!
+ @property
+ @abstract Typed access to action's application
+ */
+@property (retain, nonatomic) id application;
+
+/*!
+ @property
+ @abstract Typed access to action's comments
+ */
+@property (retain, nonatomic) NSArray *comments;
+
+@end
\ No newline at end of file
diff --git a/src/ios/facebook/FBPlacePickerCacheDescriptor.h b/src/ios/facebook/FBPlacePickerCacheDescriptor.h
new file mode 100644
index 000000000..6b9b2eda4
--- /dev/null
+++ b/src/ios/facebook/FBPlacePickerCacheDescriptor.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "FBCacheDescriptor.h"
+
+@interface FBPlacePickerCacheDescriptor : FBCacheDescriptor
+
+- (id)initWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate
+ radiusInMeters:(NSInteger)radiusInMeters
+ searchText:(NSString*)searchText
+ resultsLimit:(NSInteger)resultsLimit
+ fieldsForRequest:(NSSet*)fieldsForRequest;
+
+@property (nonatomic, readonly) CLLocationCoordinate2D locationCoordinate;
+@property (nonatomic, readonly) NSInteger radiusInMeters;
+@property (nonatomic, readonly) NSInteger resultsLimit;
+@property (nonatomic, readonly, copy) NSString *searchText;
+@property (nonatomic, readonly, copy) NSSet *fieldsForRequest;
+
+@end
+
diff --git a/src/ios/facebook/FBPlacePickerCacheDescriptor.m b/src/ios/facebook/FBPlacePickerCacheDescriptor.m
new file mode 100644
index 000000000..ecd512853
--- /dev/null
+++ b/src/ios/facebook/FBPlacePickerCacheDescriptor.m
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBPlacePickerCacheDescriptor.h"
+#import "FBGraphObjectTableDataSource.h"
+#import "FBGraphObjectPagingLoader.h"
+#import "FBPlacePickerViewController.h"
+#import "FBPlacePickerViewController+Internal.h"
+
+@interface FBPlacePickerCacheDescriptor ()
+
+@property (nonatomic, readwrite) CLLocationCoordinate2D locationCoordinate;
+@property (nonatomic, readwrite) NSInteger radiusInMeters;
+@property (nonatomic, readwrite) NSInteger resultsLimit;
+@property (nonatomic, readwrite, copy) NSString *searchText;
+@property (nonatomic, readwrite, copy) NSSet *fieldsForRequest;
+@property (nonatomic, readwrite, retain) FBGraphObjectPagingLoader *loader;
+
+// this property is only used by unit tests, and should not be removed or made public
+@property (nonatomic, readwrite, assign) BOOL hasCompletedFetch;
+
+@end
+
+@implementation FBPlacePickerCacheDescriptor
+
+@synthesize locationCoordinate = _locationCoordinate,
+ radiusInMeters = _radiusInMeters,
+ resultsLimit = _resultsLimit,
+ searchText = _searchText,
+ fieldsForRequest = _fieldsForRequest,
+ loader = _loader,
+ hasCompletedFetch = _hasCompletedFetch;
+
+- (id)initWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate
+ radiusInMeters:(NSInteger)radiusInMeters
+ searchText:(NSString*)searchText
+ resultsLimit:(NSInteger)resultsLimit
+ fieldsForRequest:(NSSet*)fieldsForRequest {
+ self = [super init];
+ if (self) {
+ self.locationCoordinate = locationCoordinate;
+ self.radiusInMeters = radiusInMeters <= 0 ? defaultRadius : radiusInMeters;
+ self.searchText = searchText;
+ self.resultsLimit = resultsLimit <= 0 ? defaultResultsLimit : resultsLimit;
+ self.fieldsForRequest = fieldsForRequest;
+ self.hasCompletedFetch = NO;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ self.fieldsForRequest = nil;
+ self.searchText = nil;
+ self.loader = nil;
+ [super dealloc];
+}
+
+- (void)prefetchAndCacheForSession:(FBSession*)session {
+ // Place queries require a session, so do nothing if we don't have one.
+ if (session == nil) {
+ return;
+ }
+
+ // datasource has some field ownership, so we need one here
+ FBGraphObjectTableDataSource *datasource = [[[FBGraphObjectTableDataSource alloc] init] autorelease];
+
+ // create the request object that we will start with
+ FBRequest *request = [FBPlacePickerViewController requestForPlacesSearchAtCoordinate:self.locationCoordinate
+ radiusInMeters:self.radiusInMeters
+ resultsLimit:self.resultsLimit
+ searchText:self.searchText
+ fields:self.fieldsForRequest
+ datasource:datasource
+ session:session];
+
+ self.loader.delegate = nil;
+ self.loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:datasource
+ pagingMode:FBGraphObjectPagingModeAsNeeded]
+ autorelease];
+ self.loader.session = session;
+ self.loader.delegate = self;
+
+ // make sure we are around to handle the delegate call
+ [self retain];
+
+ // seed the cache
+ [self.loader startLoadingWithRequest:request
+ cacheIdentity:FBPlacePickerCacheIdentity
+ skipRoundtripIfCached:NO];
+}
+
+- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader {
+ self.loader.delegate = nil;
+ self.loader = nil;
+ self.hasCompletedFetch = YES;
+
+ // achieving detachment
+ [self release];
+}
+
+@end
diff --git a/src/ios/facebook/FBPlacePickerViewController+Internal.h b/src/ios/facebook/FBPlacePickerViewController+Internal.h
new file mode 100644
index 000000000..f4c4e707a
--- /dev/null
+++ b/src/ios/facebook/FBPlacePickerViewController+Internal.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBPlacePickerViewController.h"
+#import "FBRequest.h"
+#import "FBGraphObjectTableDataSource.h"
+#import "FBSession.h"
+
+// This is the cache identity used by both the view controller and cache descriptor objects
+extern NSString *const FBPlacePickerCacheIdentity;
+
+extern const NSInteger defaultResultsLimit;
+extern const NSInteger defaultRadius;
+
+@interface FBPlacePickerViewController (Internal)
+
++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)resultsLimit
+ searchText:(NSString*)searchText
+ fields:(NSSet*)fieldsForRequest
+ datasource:(FBGraphObjectTableDataSource*)datasource
+ session:(FBSession*)session;
+@end
\ No newline at end of file
diff --git a/src/ios/facebook/FBPlacePickerViewController.h b/src/ios/facebook/FBPlacePickerViewController.h
new file mode 100644
index 000000000..6a27196e0
--- /dev/null
+++ b/src/ios/facebook/FBPlacePickerViewController.h
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "FBGraphPlace.h"
+#import "FBSession.h"
+#import "FBCacheDescriptor.h"
+#import "FBViewController.h"
+
+@protocol FBPlacePickerDelegate;
+
+/*!
+ @class FBPlacePickerViewController
+
+ @abstract
+ The `FBPlacePickerViewController` class creates a controller object that manages
+ the user interface for displaying and selecting nearby places.
+
+ @discussion
+ When the `FBPlacePickerViewController` view loads it creates a `UITableView` object
+ where the places near a given location will be displayed. You can access this view
+ through the `tableView` property.
+
+ The place data can be pre-fetched and cached prior to using the view controller. The
+ cache is setup using an object that can trigger the
+ data fetch. Any place data requests will first check the cache and use that data.
+ If the place picker is being displayed cached data will initially be shown before
+ a fresh copy is retrieved.
+
+ The `delegate` property may be set to an object that conforms to the
+ protocol. The `delegate` object will receive updates related to place selection and
+ data changes. The delegate can also be used to filter the places to display in the
+ picker.
+ */
+@interface FBPlacePickerViewController : FBViewController
+
+/*!
+ @abstract
+ Returns an outlet for the spinner used in the view controller.
+ */
+@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *spinner;
+
+/*!
+ @abstract
+ Returns an outlet for the table view managed by the view controller.
+ */
+@property (nonatomic, retain) IBOutlet UITableView *tableView;
+
+/*!
+ @abstract
+ Addtional fields to fetch when making the Graph API call to get place data.
+ */
+@property (nonatomic, copy) NSSet *fieldsForRequest;
+
+/*!
+ @abstract
+ A Boolean value that indicates whether place profile pictures are displayed.
+ */
+@property (nonatomic) BOOL itemPicturesEnabled;
+
+/*!
+ @abstract
+ The coordinates to use for place discovery.
+ */
+@property (nonatomic) CLLocationCoordinate2D locationCoordinate;
+
+/*!
+ @abstract
+ The radius to use for place discovery.
+ */
+@property (nonatomic) NSInteger radiusInMeters;
+
+/*!
+ @abstract
+ The maximum number of places to fetch.
+ */
+@property (nonatomic) NSInteger resultsLimit;
+
+/*!
+ @abstract
+ The search words used to narrow down the results returned.
+ */
+@property (nonatomic, copy) NSString *searchText;
+
+/*!
+ @abstract
+ The session that is used in the request for place data.
+ */
+@property (nonatomic, retain) FBSession *session;
+
+/*!
+ @abstract
+ The place that is currently selected in the view. This is nil
+ if nothing is selected.
+ */
+@property (nonatomic, retain, readonly) id selection;
+
+/*!
+ @abstract
+ Clears the current selection, so the picker is ready for a fresh use.
+ */
+- (void)clearSelection;
+
+/*!
+ @abstract
+ Initializes a place picker view controller.
+ */
+- (id)init;
+
+/*!
+ @abstract
+ Initializes a place picker view controller.
+
+ @param aDecoder An unarchiver object.
+ */
+- (id)initWithCoder:(NSCoder *)aDecoder;
+
+/*!
+ @abstract
+ Initializes a place picker view controller.
+
+ @param nibNameOrNil The name of the nib file to associate with the view controller. The nib file name should not contain any leading path information. If you specify nil, the nibName property is set to nil.
+ @param nibBundleOrNil The bundle in which to search for the nib file. This method looks for the nib file in the bundle's language-specific project directories first, followed by the Resources directory. If nil, this method looks for the nib file in the main bundle.
+ */
+- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
+
+/*!
+ @abstract
+ Configures the properties used in the caching data queries.
+
+ @discussion
+ Cache descriptors are used to fetch and cache the data used by the view controller.
+ If the view controller finds a cached copy of the data, it will
+ first display the cached content then fetch a fresh copy from the server.
+
+ @param cacheDescriptor The containing the cache query properties.
+ */
+- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor;
+
+/*!
+ @abstract
+ Initiates a query to get place data the first time or in response to changes in
+ the search criteria, filter, or location information.
+
+
+ @discussion
+ A cached copy will be returned if available. The cached view is temporary until a fresh copy is
+ retrieved from the server. It is legal to call this more than once.
+ */
+- (void)loadData;
+
+/*!
+ @method
+
+ @abstract
+ Creates a cache descriptor with additional fields and a profile ID for use with the
+ `FBPlacePickerViewController` object.
+
+ @discussion
+ An `FBCacheDescriptor` object may be used to pre-fetch data before it is used by
+ the view controller. It may also be used to configure the `FBPlacePickerViewController`
+ object.
+
+ @param locationCoordinate The coordinates to use for place discovery.
+ @param radiusInMeters The radius to use for place discovery.
+ @param searchText The search words used to narrow down the results returned.
+ @param resultsLimit The maximum number of places to fetch.
+ @param fieldsForRequest Addtional fields to fetch when making the Graph API call to get place data.
+ */
++ (FBCacheDescriptor*)cacheDescriptorWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate
+ radiusInMeters:(NSInteger)radiusInMeters
+ searchText:(NSString*)searchText
+ resultsLimit:(NSInteger)resultsLimit
+ fieldsForRequest:(NSSet*)fieldsForRequest;
+
+@end
+
+/*!
+ @protocol
+
+ @abstract
+ The `FBPlacePickerDelegate` protocol defines the methods used to receive event
+ notifications and allow for deeper control of the
+ view.
+ */
+@protocol FBPlacePickerDelegate
+@optional
+
+/*!
+ @abstract
+ Tells the delegate that data has been loaded.
+
+ @discussion
+ The object's `tableView` property is automatically
+ reloaded when this happens. However, if another table view, for example the
+ `UISearchBar` is showing data, then it may also need to be reloaded.
+
+ @param placePicker The place picker view controller whose data changed.
+ */
+- (void)placePickerViewControllerDataDidChange:(FBPlacePickerViewController *)placePicker;
+
+/*!
+ @abstract
+ Tells the delegate that the selection has changed.
+
+ @param placePicker The place picker view controller whose selection changed.
+ */
+- (void)placePickerViewControllerSelectionDidChange:(FBPlacePickerViewController *)placePicker;
+
+/*!
+ @abstract
+ Asks the delegate whether to include a place in the list.
+
+ @discussion
+ This can be used to implement a search bar that filters the places list.
+
+ @param placePicker The place picker view controller that is requesting this information.
+ @param place An object representing the place.
+ */
+- (BOOL)placePickerViewController:(FBPlacePickerViewController *)placePicker
+ shouldIncludePlace:(id )place;
+
+/*!
+ @abstract
+ Called if there is a communication error.
+
+ @param placePicker The place picker view controller that encountered the error.
+ @param error An error object containing details of the error.
+ */
+- (void)placePickerViewController:(FBPlacePickerViewController *)placePicker
+ handleError:(NSError *)error;
+
+@end
diff --git a/src/ios/facebook/FBPlacePickerViewController.m b/src/ios/facebook/FBPlacePickerViewController.m
new file mode 100644
index 000000000..9fc126ee9
--- /dev/null
+++ b/src/ios/facebook/FBPlacePickerViewController.m
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#import "FBError.h"
+#import "FBGraphObjectPagingLoader.h"
+#import "FBGraphObjectTableDataSource.h"
+#import "FBGraphObjectTableSelection.h"
+#import "FBLogger.h"
+#import "FBPlacePickerViewController.h"
+#import "FBRequest.h"
+#import "FBRequestConnection.h"
+#import "FBUtility.h"
+#import "FBPlacePickerCacheDescriptor.h"
+#import "FBSession+Internal.h"
+#import "FBSettings.h"
+
+NSString *const FBPlacePickerCacheIdentity = @"FBPlacePicker";
+
+static const NSInteger searchTextChangedTimerInterval = 2;
+const NSInteger defaultResultsLimit = 100;
+const NSInteger defaultRadius = 1000; // 1km
+static NSString *defaultImageName = @"FacebookSDKResources.bundle/FBPlacePickerView/images/fb_generic_place.png";
+
+@interface FBPlacePickerViewController ()
+
+@property (nonatomic, retain) FBGraphObjectTableDataSource *dataSource;
+@property (nonatomic, retain) FBGraphObjectTableSelection *selectionManager;
+@property (nonatomic, retain) FBGraphObjectPagingLoader *loader;
+@property (nonatomic, retain) NSTimer *searchTextChangedTimer;
+@property (nonatomic) BOOL trackActiveSession;
+
+- (void)initialize;
+- (void)loadDataPostThrottleSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached;
+- (NSTimer *)createSearchTextChangedTimer;
+- (void)updateView;
+- (void)centerAndStartSpinner;
+- (void)addSessionObserver:(FBSession*)session;
+- (void)removeSessionObserver:(FBSession*)session;
+- (void)clearData;
+
+@end
+
+@implementation FBPlacePickerViewController {
+ BOOL _hasSearchTextChangedSinceLastQuery;
+
+}
+
+@synthesize dataSource = _dataSource;
+@synthesize delegate = _delegate;
+@synthesize fieldsForRequest = _fieldsForRequest;
+@synthesize loader = _loader;
+@synthesize locationCoordinate = _locationCoordinate;
+@synthesize radiusInMeters = _radiusInMeters;
+@synthesize resultsLimit = _resultsLimit;
+@synthesize searchText = _searchText;
+@synthesize searchTextChangedTimer = _searchTextChangedTimer;
+@synthesize selectionManager = _selectionManager;
+@synthesize spinner = _spinner;
+@synthesize tableView = _tableView;
+@synthesize session = _session;
+@synthesize trackActiveSession = _trackActiveSession;
+
+- (id)init
+{
+ self = [super init];
+
+ if (self) {
+ [self initialize];
+ }
+
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder
+{
+ self = [super initWithCoder:aDecoder];
+
+ if (self) {
+ [self initialize];
+ }
+
+ return self;
+}
+
+- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
+{
+ self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
+
+
+ if (self) {
+ [self initialize];
+ }
+
+ return self;
+}
+
+- (void)initialize
+{
+ // Data Source
+ FBGraphObjectTableDataSource *dataSource = [[[FBGraphObjectTableDataSource alloc]
+ init]
+ autorelease];
+ dataSource.defaultPicture = [UIImage imageNamed:defaultImageName];
+ dataSource.controllerDelegate = self;
+ dataSource.itemSubtitleEnabled = YES;
+
+ // Selection Manager
+ FBGraphObjectTableSelection *selectionManager = [[[FBGraphObjectTableSelection alloc]
+ initWithDataSource:dataSource]
+ autorelease];
+ selectionManager.delegate = self;
+
+ // Paging loader
+ id loader = [[[FBGraphObjectPagingLoader alloc] initWithDataSource:dataSource
+ pagingMode:FBGraphObjectPagingModeAsNeeded]
+ autorelease];
+ self.loader = loader;
+ self.loader.delegate = self;
+
+ // Self
+ self.dataSource = dataSource;
+ self.delegate = nil;
+ self.selectionManager = selectionManager;
+ self.selectionManager.allowsMultipleSelection = NO;
+ self.resultsLimit = defaultResultsLimit;
+ self.radiusInMeters = defaultRadius;
+ self.itemPicturesEnabled = YES;
+ self.trackActiveSession = YES;
+}
+
+- (void)dealloc
+{
+ [_loader cancel];
+ _loader.delegate = nil;
+ [_loader release];
+
+ _dataSource.controllerDelegate = nil;
+
+ [_dataSource release];
+ [_fieldsForRequest release];
+ [_searchText release];
+ [_searchTextChangedTimer release];
+ [_selectionManager release];
+ [_spinner release];
+ [_tableView release];
+
+ [self removeSessionObserver:_session];
+ [_session release];
+
+ [super dealloc];
+}
+
+#pragma mark - Custom Properties
+
+- (BOOL)itemPicturesEnabled
+{
+ return self.dataSource.itemPicturesEnabled;
+}
+
+- (void)setItemPicturesEnabled:(BOOL)itemPicturesEnabled
+{
+ self.dataSource.itemPicturesEnabled = itemPicturesEnabled;
+}
+
+- (id)selection
+{
+ NSArray *selection = self.selectionManager.selection;
+ if ([selection count]) {
+ return [selection objectAtIndex:0];
+ } else {
+ return nil;
+ }
+}
+
+- (void)setSession:(FBSession *)session {
+ if (session != _session) {
+ [self removeSessionObserver:_session];
+
+ [_session release];
+ _session = [session retain];
+
+ [self addSessionObserver:session];
+
+ self.loader.session = session;
+
+ self.trackActiveSession = (session == nil);
+ }
+}
+
+#pragma mark - Public Methods
+
+- (void)loadData
+{
+ // when the app calls loadData,
+ // if we don't have a session and there is
+ // an open active session, use that
+ if (!self.session ||
+ (self.trackActiveSession && ![self.session isEqual:[FBSession activeSessionIfOpen]])) {
+ self.session = [FBSession activeSessionIfOpen];
+ self.trackActiveSession = YES;
+ }
+
+ // Sending a request on every keystroke is wasteful of bandwidth. Send a
+ // request the first time the user types something, then set up a 2-second timer
+ // and send whatever changes the user has made since then. (If nothing has changed
+ // in 2 seconds, we reset so the next change will cause an immediate re-query.)
+ if (!self.searchTextChangedTimer) {
+ self.searchTextChangedTimer = [self createSearchTextChangedTimer];
+ [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:YES]];
+ } else {
+ _hasSearchTextChangedSinceLastQuery = YES;
+ }
+}
+
+- (void)configureUsingCachedDescriptor:(FBCacheDescriptor*)cacheDescriptor {
+ if (![cacheDescriptor isKindOfClass:[FBPlacePickerCacheDescriptor class]]) {
+ [[NSException exceptionWithName:FBInvalidOperationException
+ reason:@"FBPlacePickerViewController: An attempt was made to configure "
+ @"an instance with a cache descriptor object that was not created "
+ @"by the FBPlacePickerViewController class"
+ userInfo:nil]
+ raise];
+ }
+ FBPlacePickerCacheDescriptor *cd = (FBPlacePickerCacheDescriptor*)cacheDescriptor;
+ self.locationCoordinate = cd.locationCoordinate;
+ self.radiusInMeters = cd.radiusInMeters;
+ self.resultsLimit = cd.resultsLimit;
+ self.searchText = cd.searchText;
+ self.fieldsForRequest = cd.fieldsForRequest;
+}
+
+- (void)clearSelection {
+ [self.selectionManager clearSelectionInTableView:self.tableView];
+}
+
+#pragma mark - Public Class Methods
+
++ (FBCacheDescriptor*)cacheDescriptorWithLocationCoordinate:(CLLocationCoordinate2D)locationCoordinate
+ radiusInMeters:(NSInteger)radiusInMeters
+ searchText:(NSString*)searchText
+ resultsLimit:(NSInteger)resultsLimit
+ fieldsForRequest:(NSSet*)fieldsForRequest {
+
+ return [[[FBPlacePickerCacheDescriptor alloc] initWithLocationCoordinate:locationCoordinate
+ radiusInMeters:radiusInMeters
+ searchText:searchText
+ resultsLimit:resultsLimit
+ fieldsForRequest:fieldsForRequest]
+ autorelease];
+}
+
+#pragma mark - private methods
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+ [FBLogger registerCurrentTime:FBLoggingBehaviorPerformanceCharacteristics
+ withTag:self];
+ CGRect bounds = self.canvasView.bounds;
+
+ if (!self.tableView) {
+ UITableView *tableView = [[[UITableView alloc] initWithFrame:bounds] autorelease];
+ tableView.autoresizingMask =
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+
+ self.tableView = tableView;
+ [self.canvasView addSubview:tableView];
+ }
+
+ if (!self.spinner) {
+ UIActivityIndicatorView *spinner = [[[UIActivityIndicatorView alloc]
+ initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]
+ autorelease];
+ spinner.hidesWhenStopped = YES;
+ // We want user to be able to scroll while we load.
+ spinner.userInteractionEnabled = NO;
+
+ self.spinner = spinner;
+ [self.canvasView addSubview:spinner];
+ }
+
+ self.tableView.delegate = self.selectionManager;
+ [self.dataSource bindTableView:self.tableView];
+ self.loader.tableView = self.tableView;
+}
+
+- (void)viewDidUnload
+{
+ [super viewDidUnload];
+
+ self.loader.tableView = nil;
+ self.spinner = nil;
+ self.tableView = nil;
+}
+
++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)resultsLimit
+ searchText:(NSString*)searchText
+ fields:(NSSet*)fieldsForRequest
+ datasource:(FBGraphObjectTableDataSource*)datasource
+ session:(FBSession*)session {
+
+ FBRequest *request = [FBRequest requestForPlacesSearchAtCoordinate:coordinate
+ radiusInMeters:radius
+ resultsLimit:resultsLimit
+ searchText:searchText];
+ [request setSession:session];
+
+ NSString *fields = [datasource fieldsForRequestIncluding:fieldsForRequest,
+ @"id",
+ @"name",
+ @"location",
+ @"category",
+ @"picture",
+ @"were_here_count",
+ nil];
+
+ [request.parameters setObject:fields forKey:@"fields"];
+
+ return request;
+}
+
+- (void)loadDataPostThrottleSkippingRoundTripIfCached:(NSNumber*)skipRoundTripIfCached {
+ // Place queries require a session, so do nothing if we don't have one.
+ if (self.session) {
+ FBRequest *request = [FBPlacePickerViewController requestForPlacesSearchAtCoordinate:self.locationCoordinate
+ radiusInMeters:self.radiusInMeters
+ resultsLimit:self.resultsLimit
+ searchText:self.searchText
+ fields:self.fieldsForRequest
+ datasource:self.dataSource
+ session:self.session];
+ _hasSearchTextChangedSinceLastQuery = NO;
+ [self.loader startLoadingWithRequest:request
+ cacheIdentity:FBPlacePickerCacheIdentity
+ skipRoundtripIfCached:skipRoundTripIfCached.boolValue];
+ }
+}
+
+- (void)updateView
+{
+ [self.dataSource update];
+ [self.tableView reloadData];
+}
+
+- (NSTimer *)createSearchTextChangedTimer {
+ NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:searchTextChangedTimerInterval
+ target:self
+ selector:@selector(searchTextChangedTimerFired:)
+ userInfo:nil
+ repeats:YES];
+ return timer;
+}
+
+- (void)searchTextChangedTimerFired:(NSTimer *)timer
+{
+ if (_hasSearchTextChangedSinceLastQuery) {
+ [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:YES]];
+ } else {
+ // Nothing has changed in 2 seconds. Invalidate and forget about this timer.
+ // Next time the user types, we will fire a query immediately again.
+ [self.searchTextChangedTimer invalidate];
+ self.searchTextChangedTimer = nil;
+ }
+}
+
+- (void)centerAndStartSpinner
+{
+ [FBUtility centerView:self.spinner tableView:self.tableView];
+ [self.spinner startAnimating];
+}
+
+- (void)addSessionObserver:(FBSession *)session {
+ [session addObserver:self
+ forKeyPath:@"state"
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+}
+
+- (void)removeSessionObserver:(FBSession *)session {
+ [session removeObserver:self
+ forKeyPath:@"state"];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary *)change
+ context:(void *)context {
+ if ([object isEqual:self.session] &&
+ self.session.isOpen == NO) {
+ [self clearData];
+ }
+}
+
+- (void)clearData {
+ [self.dataSource clearGraphObjects];
+ [self.selectionManager clearSelectionInTableView:self.tableView];
+ [self.tableView reloadData];
+ [self.loader reset];
+}
+
+#pragma mark - FBGraphObjectSelectionChangedDelegate
+
+- (void)graphObjectTableSelectionDidChange:
+(FBGraphObjectTableSelection *)selection
+{
+ if ([self.delegate respondsToSelector:
+ @selector(placePickerViewControllerSelectionDidChange:)]) {
+ [(id)self.delegate placePickerViewControllerSelectionDidChange:self];
+ }
+}
+
+#pragma mark - FBGraphObjectViewControllerDelegate
+
+- (BOOL)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ filterIncludesItem:(id)item
+{
+ id place = (id)item;
+
+ if ([self.delegate
+ respondsToSelector:@selector(placePickerViewController:shouldIncludePlace:)]) {
+ return [(id)self.delegate placePickerViewController:self
+ shouldIncludePlace:place];
+ } else {
+ return YES;
+ }
+}
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ titleOfItem:(id)graphObject
+{
+ return [graphObject objectForKey:@"name"];
+}
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ subtitleOfItem:(id)graphObject
+{
+ NSString *category = [graphObject objectForKey:@"category"];
+ NSNumber *wereHereCount = [graphObject objectForKey:@"were_here_count"];
+
+ if (wereHereCount) {
+ NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
+ [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
+ NSString *wereHere = [numberFormatter stringFromNumber:wereHereCount];
+ [numberFormatter release];
+
+ if (category) {
+ return [NSString stringWithFormat:@"%@ • %@ were here", [category capitalizedString], wereHere];
+ }
+ return [NSString stringWithFormat:@"%@ were here", wereHere];
+ }
+ if (category) {
+ return [category capitalizedString];
+ }
+ return nil;
+}
+
+- (NSString *)graphObjectTableDataSource:(FBGraphObjectTableDataSource *)dataSource
+ pictureUrlOfItem:(id)graphObject
+{
+ id picture = [graphObject objectForKey:@"picture"];
+ // Depending on what migration the app is in, we may get back either a string, or a
+ // dictionary with a "data" property that is a dictionary containing a "url" property.
+ if ([picture isKindOfClass:[NSString class]]) {
+ return picture;
+ }
+ id data = [picture objectForKey:@"data"];
+ return [data objectForKey:@"url"];
+}
+
+#pragma mark FBGraphObjectPagingLoaderDelegate members
+
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader willLoadURL:(NSString*)url {
+ // We only want to display our spinner on loading the first page. After that,
+ // a spinner will display in the last cell to indicate to the user that data is loading.
+ if ([self.dataSource numberOfSectionsInTableView:self.tableView] == 0) {
+ [self centerAndStartSpinner];
+ }
+}
+
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader didLoadData:(NSDictionary*)results {
+ [self.spinner stopAnimating];
+
+ // This logging currently goes here because we're effectively complete with our initial view when
+ // the first page of results come back. In the future, when we do caching, we will need to move
+ // this to a more appropriate place (e.g., after the cache has been brought in).
+ [FBLogger singleShotLogEntry:FBLoggingBehaviorPerformanceCharacteristics
+ timestampTag:self
+ formatString:@"Places Picker: first render "]; // logger will append "%d msec"
+
+ if ([self.delegate respondsToSelector:@selector(placePickerViewControllerDataDidChange:)]) {
+ [(id)self.delegate placePickerViewControllerDataDidChange:self];
+ }
+}
+
+- (void)pagingLoaderDidFinishLoading:(FBGraphObjectPagingLoader *)pagingLoader {
+ // No more results, stop spinner
+ [self.spinner stopAnimating];
+
+ // Call the delegate from here as well, since this might be the first response of a query
+ // that has no results.
+ if ([self.delegate respondsToSelector:@selector(placePickerViewControllerDataDidChange:)]) {
+ [(id)self.delegate placePickerViewControllerDataDidChange:self];
+ }
+
+ // if our current display is from cache, then kick-off a near-term refresh
+ if (pagingLoader.isResultFromCache) {
+ [self loadDataPostThrottleSkippingRoundTripIfCached:[NSNumber numberWithBool:NO]];
+ }
+}
+
+- (void)pagingLoader:(FBGraphObjectPagingLoader*)pagingLoader handleError:(NSError*)error {
+ if ([self.delegate respondsToSelector:@selector(placePickerViewController:handleError:)]) {
+ [(id)self.delegate placePickerViewController:self handleError:error];
+ }
+
+}
+
+- (void)pagingLoaderWasCancelled:(FBGraphObjectPagingLoader*)pagingLoader {
+ [self.spinner stopAnimating];
+}
+
+@end
+
diff --git a/src/ios/facebook/FBProfilePictureView.h b/src/ios/facebook/FBProfilePictureView.h
new file mode 100644
index 000000000..a0ec50a10
--- /dev/null
+++ b/src/ios/facebook/FBProfilePictureView.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+
+/*!
+ @typedef FBProfilePictureCropping enum
+
+ @abstract
+ Type used to specify the cropping treatment of the profile picture.
+
+ @discussion
+ */
+typedef enum {
+
+ /*! Square (default) - the square version that the Facebook user defined. */
+ FBProfilePictureCroppingSquare = 0,
+
+ /*! Original - the original profile picture, as uploaded. */
+ FBProfilePictureCroppingOriginal = 1
+
+} FBProfilePictureCropping;
+
+/*!
+ @class
+ @abstract
+ An instance of `FBProfilePictureView` is used to display a profile picture.
+
+ The default behavior of this control is to center the profile picture
+ in the view and shrinks it, if necessary, to the view's bounds, preserving the aspect ratio. The smallest
+ possible image is downloaded to ensure that scaling up never happens. Resizing the view may result in
+ a different size of the image being loaded. Canonical image sizes are documented in the "Pictures" section
+ of https://developers.facebook.com/docs/reference/api.
+ */
+@interface FBProfilePictureView : UIView
+
+/*!
+ @abstract
+ The Facebook ID of the user, place or object for which a picture should be fetched and displayed.
+ */
+@property (copy, nonatomic) NSString* profileID;
+
+/*!
+ @abstract
+ The cropping to use for the profile picture.
+ */
+@property (nonatomic) FBProfilePictureCropping pictureCropping;
+
+/*!
+ @abstract
+ Initializes and returns a profile view object.
+ */
+- (id)init;
+
+
+/*!
+ @abstract
+ Initializes and returns a profile view object for the given Facebook ID and cropping.
+
+ @param profileID The Facebook ID of the user, place or object for which a picture should be fetched and displayed.
+ @param pictureCropping The cropping to use for the profile picture.
+ */
+- (id)initWithProfileID:(NSString*)profileID
+ pictureCropping:(FBProfilePictureCropping)pictureCropping;
+
+
+@end
diff --git a/src/ios/facebook/FBProfilePictureView.m b/src/ios/facebook/FBProfilePictureView.m
new file mode 100644
index 000000000..d649b85f5
--- /dev/null
+++ b/src/ios/facebook/FBProfilePictureView.m
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBProfilePictureView.h"
+#import "FBURLConnection.h"
+#import "FBRequest.h"
+#import "FBUtility.h"
+#import "FBSDKVersion.h"
+
+@interface FBProfilePictureView()
+
+@property (readonly, nonatomic) NSString *imageQueryParamString;
+@property (retain, nonatomic) NSString *previousImageQueryParamString;
+
+@property (retain, nonatomic) FBURLConnection *connection;
+@property (retain, nonatomic) UIImageView *imageView;
+
+- (void)initialize;
+- (void)refreshImage:(BOOL)forceRefresh;
+- (void)ensureImageViewContentMode;
+
+@end
+
+@implementation FBProfilePictureView
+
+@synthesize profileID = _profileID;
+@synthesize pictureCropping = _pictureCropping;
+@synthesize connection = _connection;
+@synthesize imageView = _imageView;
+@synthesize previousImageQueryParamString = _previousImageQueryParamString;
+
+#pragma mark - Lifecycle
+
+- (void)dealloc {
+ [_profileID release];
+ [_imageView release];
+ [_connection release];
+ [_previousImageQueryParamString release];
+
+ [super dealloc];
+}
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ [self initialize];
+ }
+
+ return self;
+}
+
+- (id)initWithProfileID:(NSString *)profileID
+ pictureCropping:(FBProfilePictureCropping)pictureCropping {
+ self = [self init];
+ if (self) {
+ self.pictureCropping = pictureCropping;
+ self.profileID = profileID;
+ }
+
+ return self;
+}
+
+- (id)initWithFrame:(CGRect)frame {
+ self = [super initWithFrame:frame];
+ if (self) {
+ [self initialize];
+ }
+
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+ self = [super initWithCoder:aDecoder];
+ if (self) {
+ [self initialize];
+ }
+ return self;
+}
+
+#pragma mark -
+
+- (NSString *)imageQueryParamString {
+
+ static CGFloat screenScaleFactor = 0.0;
+ if (screenScaleFactor == 0.0) {
+ screenScaleFactor = [[UIScreen mainScreen] scale];
+ }
+
+ // Retina display doesn't increase the bounds that iOS returns. The larger size to fetch needs
+ // to be calculated using the scale factor accessed above.
+ int width = (int)(self.bounds.size.width * screenScaleFactor);
+
+ if (self.pictureCropping == FBProfilePictureCroppingSquare) {
+ return [NSString stringWithFormat:@"width=%d&height=%d&migration_bundle=%@",
+ width,
+ width,
+ FB_IOS_SDK_MIGRATION_BUNDLE];
+ }
+
+ // For non-square images, we choose between three variants knowing that the small profile picture is
+ // 50 pixels wide, normal is 100, and large is about 200.
+ if (width <= 50) {
+ return @"type=small";
+ } else if (width <= 100) {
+ return @"type=normal";
+ } else {
+ return @"type=large";
+ }
+}
+
+- (void)initialize {
+ // the base class can cause virtual recursion, so
+ // to handle this we make initialize idempotent
+ if (self.imageView) {
+ return;
+ }
+
+ UIImageView* imageView = [[[UIImageView alloc] initWithFrame:self.bounds] autorelease];
+ imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ self.imageView = imageView;
+
+ self.autoresizesSubviews = YES;
+ self.clipsToBounds = YES;
+
+ [self addSubview:self.imageView];
+}
+
+- (void)refreshImage:(BOOL)forceRefresh {
+ NSString *newImageQueryParamString = self.imageQueryParamString;
+
+ // If not forcing refresh, check to see if the previous size we used would be the same
+ // as what we'd request now, as this method could be called often on control bounds animation,
+ // and we only want to fetch when needed.
+ if (!forceRefresh && [self.previousImageQueryParamString isEqualToString:newImageQueryParamString]) {
+
+ // But we still may need to adjust the contentMode.
+ [self ensureImageViewContentMode];
+ return;
+ }
+
+ if (self.profileID) {
+
+ [self.connection cancel];
+
+ FBURLConnectionHandler handler =
+ ^(FBURLConnection *connection, NSError *error, NSURLResponse *response, NSData *data) {
+ FBConditionalLog(self.connection == connection, @"Inconsistent connection state");
+
+ self.connection = nil;
+ if (!error) {
+ self.imageView.image = [UIImage imageWithData:data];
+ [self ensureImageViewContentMode];
+ }
+ };
+
+ NSString *template = @"%@/%@/picture?%@";
+ NSString *urlString = [NSString stringWithFormat:template,
+ FBGraphBasePath,
+ self.profileID,
+ newImageQueryParamString];
+ NSURL *url = [NSURL URLWithString:urlString];
+
+ self.connection = [[[FBURLConnection alloc] initWithURL:url
+ completionHandler:handler]
+ autorelease];
+ } else {
+ BOOL isSquare = (self.pictureCropping == FBProfilePictureCroppingSquare);
+
+ NSString *blankImageName =
+ [NSString
+ stringWithFormat:@"FacebookSDKResources.bundle/FBProfilePictureView/images/fb_blank_profile_%@.png",
+ isSquare ? @"square" : @"portrait"];
+
+ self.imageView.image = [UIImage imageNamed:blankImageName];
+ [self ensureImageViewContentMode];
+ }
+
+ self.previousImageQueryParamString = newImageQueryParamString;
+}
+
+- (void)ensureImageViewContentMode {
+ // Set the image's contentMode such that if the image is larger than the control, we scale it down, preserving aspect
+ // ratio. Otherwise, we center it. This ensures that we never scale up, and pixellate, the image.
+ CGSize viewSize = self.bounds.size;
+ CGSize imageSize = self.imageView.image.size;
+ UIViewContentMode contentMode;
+
+ // If both of the view dimensions are larger than the image, we'll center the image to prevent scaling up.
+ // Note that unlike in choosing the image size, we *don't* use any Retina-display scaling factor to choose centering
+ // vs. filling. If we were to do so, we'd get profile pics shrinking to fill the the view on non-Retina, but getting
+ // centered and clipped on Retina.
+ if (viewSize.width > imageSize.width && viewSize.height > imageSize.height) {
+ contentMode = UIViewContentModeCenter;
+ } else {
+ contentMode = UIViewContentModeScaleAspectFit;
+ }
+
+ self.imageView.contentMode = contentMode;
+}
+
+- (void)setProfileID:(NSString*)profileID {
+ if (!_profileID || ![_profileID isEqualToString:profileID]) {
+ [_profileID release];
+ _profileID = [profileID copy];
+ [self refreshImage:YES];
+ }
+}
+
+- (void)setPictureCropping:(FBProfilePictureCropping)pictureCropping {
+ if (_pictureCropping != pictureCropping) {
+ _pictureCropping = pictureCropping;
+ [self refreshImage:YES];
+ }
+}
+
+// Lets us catch resizes of the control, or any outer layout, allowing us to potentially
+// choose a different image.
+- (void)layoutSubviews {
+ [self refreshImage:NO];
+ [super layoutSubviews];
+}
+
+
+@end
diff --git a/src/ios/facebook/FBRequest.h b/src/ios/facebook/FBRequest.h
new file mode 100644
index 000000000..6626c5720
--- /dev/null
+++ b/src/ios/facebook/FBRequest.h
@@ -0,0 +1,504 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "FBRequestConnection.h"
+#import "FBGraphObject.h"
+
+/*! The base URL used for graph requests */
+extern NSString* const FBGraphBasePath;
+
+// up-front decl's
+@protocol FBRequestDelegate;
+@class FBSession;
+@class UIImage;
+
+/*!
+ @typedef FBRequestState
+
+ @abstract
+ Deprecated - do not use in new code.
+
+ @discussion
+ FBRequestState is retained from earlier versions of the SDK to give existing
+ apps time to remove dependency on this.
+
+ @deprecated
+*/
+typedef NSUInteger FBRequestState __attribute__((deprecated));
+
+/*!
+ @class FBRequest
+
+ @abstract
+ The `FBRequest` object is used to setup and manage requests to Facebook Graph
+ and REST APIs. This class provides helper methods that simplify the connection
+ and response handling.
+
+ @discussion
+ An object is required for all authenticated uses of `FBRequest`.
+ Requests that do not require an unauthenticated user are also supported and
+ do not require an object to be passed in.
+
+ An instance of `FBRequest` represents the arguments and setup for a connection
+ to Facebook. After creating an `FBRequest` object it can be used to setup a
+ connection to Facebook through the object. The
+ object is created to manage a single connection. To
+ cancel a connection use the instance method in the class.
+
+ An `FBRequest` object may be reused to issue multiple connections to Facebook.
+ However each instance will manage one connection.
+
+ Class and instance methods prefixed with **start* ** can be used to perform the
+ request setup and initiate the connection in a single call.
+
+*/
+@interface FBRequest : NSObject {
+@private
+ id _delegate;
+ NSString* _url;
+ NSURLConnection* _connection;
+ NSMutableData* _responseText;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ FBRequestState _state;
+#pragma GCC diagnostic pop
+ NSError* _error;
+ BOOL _sessionDidExpire;
+ id _graphObject;
+}
+
+/*!
+ @methodgroup Creating a request
+
+ @method
+ Calls with the default parameters.
+*/
+- (id)init;
+
+/*!
+ @method
+ Calls with default parameters
+ except for the ones provided to this method.
+
+ @param session The session object representing the identity of the Facebook user making
+ the request. A nil value indicates a request that requires no token; to
+ use the active session pass `[FBSession activeSession]`.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+*/
+- (id)initWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath;
+
+/*!
+ @method
+
+ @abstract
+ Initializes an `FBRequest` object for a Graph API request call.
+
+ @discussion
+ Note that this only sets properties on the `FBRequest` object.
+
+ To send the request, initialize an object, add this request,
+ and send <[FBRequestConnection start]>. See other methods on this
+ class for shortcuts to simplify this process.
+
+ @param session The session object representing the identity of the Facebook user making
+ the request. A nil value indicates a request that requires no token; to
+ use the active session pass `[FBSession activeSession]`.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param parameters The parameters for the request. A value of nil sends only the automatically handled
+ parameters, for example, the access token. The default is nil.
+
+ @param HTTPMethod The HTTP method to use for the request. The default is value of nil implies a GET.
+*/
+- (id)initWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath
+ parameters:(NSDictionary *)parameters
+ HTTPMethod:(NSString *)HTTPMethod;
+
+/*!
+ @method
+ @abstract
+ Initialize a `FBRequest` object that will do a graph request.
+
+ @discussion
+ Note that this only sets properties on the `FBRequest`.
+
+ To send the request, initialize a , add this request,
+ and send <[FBRequestConnection start]>. See other methods on this
+ class for shortcuts to simplify this process.
+
+ @param session The session object representing the identity of the Facebook user making
+ the request. A nil value indicates a request that requires no token; to
+ use the active session pass `[FBSession activeSession]`.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param graphObject An object or open graph action to post.
+*/
+- (id)initForPostWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath
+ graphObject:(id)graphObject;
+
+/*!
+ @method
+ @abstract
+ Initialize a `FBRequest` object that will do a rest API request.
+
+ @discussion
+ Prefer to use graph requests instead of this where possible.
+
+ Note that this only sets properties on the `FBRequest`.
+
+ To send the request, initialize a , add this request,
+ and send <[FBRequestConnection start]>. See other methods on this
+ class for shortcuts to simplify this process.
+
+ @param session The session object representing the identity of the Facebook user making
+ the request. A nil value indicates a request that requires no token; to
+ use the active session pass `[FBSession activeSession]`.
+
+ @param restMethod A valid REST API method.
+
+ @param parameters The parameters for the request. A value of nil sends only the automatically handled
+ parameters, for example, the access token. The default is nil.
+
+ @param HTTPMethod The HTTP method to use for the request. The default is value of nil implies a GET.
+
+*/
+- (id)initWithSession:(FBSession*)session
+ restMethod:(NSString *)restMethod
+ parameters:(NSDictionary *)parameters
+ HTTPMethod:(NSString *)HTTPMethod;
+
+/*!
+ @abstract
+ The parameters for the request.
+
+ @discussion
+ May be used to read the parameters that were automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+
+ `NSString` parameters are used to generate URL parameter values or JSON
+ parameters. `NSData` and `UIImage` parameters are added as attachments
+ to the HTTP body and referenced by name in the URL and/or JSON.
+*/
+@property(nonatomic, retain, readonly) NSMutableDictionary *parameters;
+
+/*!
+ @abstract
+ The session object to use for the request.
+
+ @discussion
+ May be used to read the session that was automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+*/
+@property(nonatomic, retain) FBSession *session;
+
+/*!
+ @abstract
+ The Graph API endpoint to use for the request, for example "me".
+
+ @discussion
+ May be used to read the Graph API endpoint that was automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+*/
+@property(nonatomic, copy) NSString *graphPath;
+
+/*!
+ @abstract
+ A valid REST API method.
+
+ @discussion
+ May be used to read the REST method that was automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+
+ Use the Graph API equivalent of the API if it exists as the REST API
+ method is deprecated if there is a Graph API equivalent.
+*/
+@property(nonatomic, copy) NSString *restMethod;
+
+/*!
+ @abstract
+ The HTTPMethod to use for the request, for example "GET" or "POST".
+
+ @discussion
+ May be used to read the HTTP method that was automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+*/
+@property(nonatomic, copy) NSString *HTTPMethod;
+
+/*!
+ @abstract
+ The graph object to post with the request.
+
+ @discussion
+ May be used to read the graph object that was automatically set during
+ the object initiliazation. Make any required modifications prior to
+ sending the request.
+*/
+@property(nonatomic, retain) id graphObject;
+
+/*!
+ @methodgroup Instance methods
+*/
+
+/*!
+ @method
+
+ @abstract
+ Starts a connection to the Facebook API.
+
+ @discussion
+ This is used to start an API call to Facebook and call the block when the
+ request completes with a success, error, or cancel.
+
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+*/
+- (FBRequestConnection*)startWithCompletionHandler:(FBRequestHandler)handler;
+
+/*!
+ @methodgroup FBRequestConnection start methods
+
+ @abstract
+ These methods start an .
+
+ @discussion
+ These methods simplify the process of preparing a request and starting
+ the connection. The methods handle initializing an `FBRequest` object,
+ initializing a object, adding the `FBRequest`
+ object to the to the , and finally starting the
+ connection.
+*/
+
+/*!
+ @methodgroup FBRequest factory methods
+
+ @abstract
+ These methods initialize a `FBRequest` for common scenarios.
+
+ @discussion
+ These simplify the process of preparing a request to send. These
+ initialize a `FBRequest` based on strongly typed parameters that are
+ specific to the scenario.
+
+ These method do not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+*/
+
+// request*
+//
+// Summary:
+// Helper methods used to create common request objects which can be used to create single or batch connections
+//
+// session: - the session object representing the identity of the
+// Facebook user making the request; nil implies an
+// unauthenticated request; default=nil
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a Graph API call to the "me" endpoint, using the active session.
+
+ @discussion
+ Simplifies preparing a request to retrieve the user's identity.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ A successful Graph API call will return an object representing the
+ user's identity.
+
+ Note you may change the session property after construction if a session other than
+ the active session is preferred.
+*/
++ (FBRequest*)requestForMe;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a Graph API call to the "me/friends" endpoint using the active session.
+
+ @discussion
+ Simplifies preparing a request to retrieve the user's friends.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ A successful Graph API call will return an array of objects representing the
+ user's friends.
+*/
++ (FBRequest*)requestForMyFriends;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a Graph API call to upload a photo to the app's album using the active session.
+
+ @discussion
+ Simplifies preparing a request to post a photo.
+
+ To post a photo to a specific album, get the `FBRequest` returned from this method
+ call, then modify the request parameters by adding the album ID to an "album" key.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ @param photo A `UIImage` for the photo to upload.
+*/
++ (FBRequest*)requestForUploadPhoto:(UIImage *)photo;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a status update.
+
+ @discussion
+ Simplifies preparing a request to post a status update.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ @param message The message to post.
+ */
++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a status update.
+
+ @discussion
+ Simplifies preparing a request to post a status update.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ @param message The message to post.
+ @param place The place to checkin with, or nil. Place may be an fbid or a
+ graph object representing a place.
+ @param tags Array of friends to tag in the status update, each element
+ may be an fbid or a graph object representing a user.
+ */
++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message
+ place:(id)place
+ tags:(id)tags;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a Graph API call to the "search" endpoint
+ for a given location using the active session.
+
+ @discussion
+ Simplifies preparing a request to search for places near a coordinate.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ A successful Graph API call will return an array of objects representing
+ the nearby locations.
+
+ @param coordinate The search coordinates.
+
+ @param radius The search radius in meters.
+
+ @param limit The maxiumum number of results to return. It is
+ possible to receive fewer than this because of the radius and because of server limits.
+
+ @param searchText The text to use in the query to narrow the set of places
+ returned.
+*/
++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)limit
+ searchText:(NSString*)searchText;
+
+/*!
+ @method
+
+ @abstract
+ Returns a newly initialized request object that can be used to make a Graph API call for the active session.
+
+ @discussion
+ This method simplifies the preparation of a Graph API call.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+ */
++ (FBRequest*)requestForGraphPath:(NSString*)graphPath;
+
+/*!
+ @method
+
+ @abstract
+ Creates a request representing a POST for a graph object.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param graphObject An object or open graph action to post.
+ */
++ (FBRequest*)requestForPostWithGraphPath:(NSString*)graphPath
+ graphObject:(id)graphObject;
+
+/*!
+ @method
+
+ @abstract
+ Returns a newly initialized request object that can be used to make a Graph API call for the active session.
+
+ @discussion
+ This method simplifies the preparation of a Graph API call.
+
+ This method does not initialize an object. To initiate the API
+ call first instantiate an object, add the request to this object,
+ then call the `start` method on the connection instance.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param parameters The parameters for the request. A value of nil sends only the automatically handled parameters, for example, the access token. The default is nil.
+
+ @param HTTPMethod The HTTP method to use for the request. A nil value implies a GET.
+ */
++ (FBRequest*)requestWithGraphPath:(NSString*)graphPath
+ parameters:(NSDictionary*)parameters
+ HTTPMethod:(NSString*)HTTPMethod;
+@end
diff --git a/src/ios/facebook/FBRequest.m b/src/ios/facebook/FBRequest.m
new file mode 100644
index 000000000..a7f2700e2
--- /dev/null
+++ b/src/ios/facebook/FBRequest.m
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2010 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Facebook.h"
+#import "FBUtility.h"
+#import "FBSession+Internal.h"
+#import "FBSDKVersion.h"
+
+// constants
+NSString *const FBGraphBasePath = @"https://graph." FB_BASE_URL;
+
+static NSString *const kGetHTTPMethod = @"GET";
+static NSString *const kPostHTTPMethod = @"POST";
+
+// ----------------------------------------------------------------------------
+// FBRequest
+
+@implementation FBRequest
+
+@synthesize parameters = _parameters;
+@synthesize session = _session;
+@synthesize graphPath = _graphPath;
+@synthesize restMethod = _restMethod;
+@synthesize HTTPMethod = _HTTPMethod;
+
+- (id)init
+{
+ return [self initWithSession:nil
+ graphPath:nil
+ parameters:nil
+ HTTPMethod:nil];
+}
+
+- (id)initWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath
+{
+ return [self initWithSession:session
+ graphPath:graphPath
+ parameters:nil
+ HTTPMethod:nil];
+}
+
+- (id)initForPostWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath
+ graphObject:(id)graphObject {
+ self = [self initWithSession:session
+ graphPath:graphPath
+ parameters:nil
+ HTTPMethod:kPostHTTPMethod];
+ if (self) {
+ self.graphObject = graphObject;
+ }
+ return self;
+}
+
+- (id)initWithSession:(FBSession*)session
+ restMethod:(NSString *)restMethod
+ parameters:(NSDictionary *)parameters
+ HTTPMethod:(NSString *)HTTPMethod
+{
+ // reusing the more common initializer...
+ self = [self initWithSession:session
+ graphPath:nil // but assuring a nil graphPath for the rest case
+ parameters:parameters
+ HTTPMethod:HTTPMethod];
+ if (self) {
+ self.restMethod = restMethod;
+ }
+ return self;
+}
+
+- (id)initWithSession:(FBSession*)session
+ graphPath:(NSString *)graphPath
+ parameters:(NSDictionary *)parameters
+ HTTPMethod:(NSString *)HTTPMethod
+{
+ if (self = [super init]) {
+ // set default for nil
+ if (!HTTPMethod) {
+ HTTPMethod = kGetHTTPMethod;
+ }
+
+ self.session = session;
+ self.graphPath = graphPath;
+ self.HTTPMethod = HTTPMethod;
+
+ // all request objects start life with a migration bundle set for the SDK
+ _parameters = [[NSMutableDictionary alloc]
+ initWithObjectsAndKeys:FB_IOS_SDK_MIGRATION_BUNDLE, @"migration_bundle", nil];
+ if (parameters) {
+ // but the incoming dictionary's migration bundle trumps the default one, if present
+ [self.parameters addEntriesFromDictionary:parameters];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [_graphObject release];
+ [_session release];
+ [_graphPath release];
+ [_restMethod release];
+ [_HTTPMethod release];
+ [_parameters release];
+ [super dealloc];
+}
+
+//@property(nonatomic,retain) id graphObject;
+- (id)graphObject {
+ return _graphObject;
+}
+
+- (void)setGraphObject:(id)newValue {
+ if (_graphObject != newValue) {
+ [_graphObject release];
+ _graphObject = [newValue retain];
+ }
+
+ // setting this property implies you want a post, if you really
+ // want a get, reset the method to get after setting this property
+ self.HTTPMethod = kPostHTTPMethod;
+}
+
+- (FBRequestConnection*)startWithCompletionHandler:(FBRequestHandler)handler
+{
+ FBRequestConnection *connection = [[[FBRequestConnection alloc] init] autorelease];
+ [connection addRequest:self completionHandler:handler];
+ [connection start];
+ return connection;
+}
+
++ (FBRequest*)requestForMe {
+ return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:@"me"]
+ autorelease];
+}
+
++ (FBRequest*)requestForMyFriends {
+ return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:@"me/friends"
+ parameters:[NSDictionary dictionaryWithObjectsAndKeys:
+ @"id,name,username,first_name,last_name", @"fields",
+ nil]
+ HTTPMethod:nil]
+ autorelease];
+}
+
++ (FBRequest *)requestForUploadPhoto:(UIImage *)photo
+{
+ NSString *graphPath = @"me/photos";
+ NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
+ [parameters setObject:photo forKey:@"picture"];
+
+ FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:graphPath
+ parameters:parameters
+ HTTPMethod:@"POST"]
+ autorelease];
+
+ [parameters release];
+
+ return request;
+}
+
++ (FBRequest*)requestForGraphPath:(NSString*)graphPath
+{
+ FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:graphPath
+ parameters:nil
+ HTTPMethod:nil]
+ autorelease];
+ return request;
+}
+
++ (FBRequest*)requestForPostWithGraphPath:(NSString*)graphPath
+ graphObject:(id)graphObject {
+ return [[[FBRequest alloc] initForPostWithSession:[FBSession activeSessionIfOpen]
+ graphPath:graphPath
+ graphObject:graphObject]
+ autorelease];
+}
+
++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message {
+ return [FBRequest requestForPostStatusUpdate:message
+ place:nil
+ tags:nil];
+}
+
++ (FBRequest *)requestForPostStatusUpdate:(NSString *)message
+ place:(id)place
+ tags:(id)tags {
+
+ NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObject:message forKey:@"message"];
+ // if we have a place object, use it
+ if (place) {
+ [params setObject:[FBUtility stringFBIDFromObject:place]
+ forKey:@"place"];
+ }
+ // ditto tags
+ if (tags) {
+ NSMutableString *tagsValue = [NSMutableString string];
+ NSString *format = @"%@";
+ for (id tag in tags) {
+ [tagsValue appendFormat:format, [FBUtility stringFBIDFromObject:tag]];
+ format = @",%@";
+ }
+ if ([tagsValue length]) {
+ [params setObject:tagsValue
+ forKey:@"tags"];
+ }
+ }
+
+ return [FBRequest requestWithGraphPath:@"me/feed"
+ parameters:params
+ HTTPMethod:@"POST"];
+}
+
++ (FBRequest*)requestWithGraphPath:(NSString*)graphPath
+ parameters:(NSDictionary*)parameters
+ HTTPMethod:(NSString*)HTTPMethod {
+ return [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:graphPath
+ parameters:parameters
+ HTTPMethod:HTTPMethod]
+ autorelease];
+}
+
++ (FBRequest*)requestForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)limit
+ searchText:(NSString*)searchText
+{
+ NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
+ [parameters setObject:@"place" forKey:@"type"];
+ [parameters setObject:[NSString stringWithFormat:@"%d", limit] forKey:@"limit"];
+ [parameters setObject:[NSString stringWithFormat:@"%lf,%lf", coordinate.latitude, coordinate.longitude]
+ forKey:@"center"];
+ [parameters setObject:[NSString stringWithFormat:@"%d", radius] forKey:@"distance"];
+ if ([searchText length]) {
+ [parameters setObject:searchText forKey:@"q"];
+ }
+
+ FBRequest *request = [[[FBRequest alloc] initWithSession:[FBSession activeSessionIfOpen]
+ graphPath:@"search"
+ parameters:parameters
+ HTTPMethod:nil]
+ autorelease];
+ [parameters release];
+
+ return request;
+}
+
+@end
+
+// ----------------------------------------------------------------------------
+// Deprecated FBRequest implementation
+
+@implementation FBRequest (Deprecated)
+
+// ----------------------------------------------------------------------------
+// deprecated public properties
+
+//@property(nonatomic,assign) id delegate;
+- (id)delegate {
+ return _delegate;
+}
+
+- (void)setDelegate:(id)newValue {
+ _delegate = newValue;
+}
+
+//@property(nonatomic,copy) NSString* url;
+- (NSString*)url {
+ return _url;
+}
+
+- (void)setUrl:(NSString*)newValue {
+ if (_url != newValue) {
+ [_url release];
+ _url = [newValue copy];
+ }
+}
+
+//@property(nonatomic,copy) NSString* httpMethod;
+- (NSString*)httpMethod {
+ return self.HTTPMethod;
+}
+
+- (void)setHttpMethod:(NSString*)newValue {
+ self.HTTPMethod = newValue;
+}
+
+//@property(nonatomic,retain) NSMutableDictionary* params;
+- (NSMutableDictionary*)params {
+ return _parameters;
+}
+
+- (void)setParams:(NSMutableDictionary*)newValue {
+ if (_parameters != newValue) {
+ [_parameters release];
+ _parameters = [newValue retain];
+ }
+}
+
+//@property(nonatomic,retain) NSURLConnection* connection;
+- (NSURLConnection*)connection {
+ return _connection;
+}
+
+- (void)setConnection:(NSURLConnection*)newValue {
+ if (_connection != newValue) {
+ [_connection release];
+ _connection = [newValue retain];
+ }
+}
+
+//@property(nonatomic,retain) NSMutableData* responseText;
+- (NSMutableData*)responseText {
+ return _responseText;
+}
+
+- (void)setResponseText:(NSMutableData*)newValue {
+ if (_responseText != newValue) {
+ [_responseText release];
+ _responseText = [newValue retain];
+ }
+}
+
+//@property(nonatomic,retain) NSError* error;
+- (NSError*)error {
+ return _error;
+}
+
+- (void)setError:(NSError*)newValue {
+ if (_error != newValue) {
+ [_error release];
+ _error = [newValue retain];
+ }
+}
+
+//@property(nonatomic,readonly+readwrite) FBRequestState state;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+- (FBRequestState)state {
+ return _state;
+}
+
+- (void)setState:(FBRequestState)newValue {
+ _state = newValue;
+}
+#pragma GCC diagnostic pop
+
+//@property(nonatomic,readonly+readwrite) BOOL sessionDidExpire;
+- (BOOL)sessionDidExpire {
+ return _sessionDidExpire;
+}
+
+- (void)setSessionDidExpire:(BOOL)newValue {
+ _sessionDidExpire = newValue;
+}
+
+// ----------------------------------------------------------------------------
+// deprecated public methods
+
+- (BOOL)loading
+{
+ return (_state == kFBRequestStateLoading);
+}
+
++ (NSString *)serializeURL:(NSString *)baseUrl
+ params:(NSDictionary *)params {
+ return [self serializeURL:baseUrl params:params httpMethod:kGetHTTPMethod];
+}
+
++ (NSString*)serializeURL:(NSString *)baseUrl
+ params:(NSDictionary *)params
+ httpMethod:(NSString *)httpMethod {
+
+ NSURL* parsedURL = [NSURL URLWithString:baseUrl];
+ NSString* queryPrefix = parsedURL.query ? @"&" : @"?";
+
+ NSMutableArray* pairs = [NSMutableArray array];
+ for (NSString* key in [params keyEnumerator]) {
+ id value = [params objectForKey:key];
+ if ([value isKindOfClass:[UIImage class]]
+ || [value isKindOfClass:[NSData class]]) {
+ if ([httpMethod isEqualToString:kGetHTTPMethod]) {
+ NSLog(@"can not use GET to upload a file");
+ }
+ continue;
+ }
+
+ NSString* escaped_value = [FBUtility stringByURLEncodingString:value];
+ [pairs addObject:[NSString stringWithFormat:@"%@=%@", key, escaped_value]];
+ }
+ NSString* query = [pairs componentsJoinedByString:@"&"];
+
+ return [NSString stringWithFormat:@"%@%@%@", baseUrl, queryPrefix, query];
+}
+
+#pragma mark Debugging helpers
+
+- (NSString*)description {
+ NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, session: %p",
+ NSStringFromClass([self class]),
+ self,
+ self.session];
+ if (self.graphPath) {
+ [result appendFormat:@", graphPath: %@", self.graphPath];
+ }
+ if (self.graphObject) {
+ [result appendFormat:@", graphObject: %@", self.graphObject];
+ NSString *graphObjectID = [self.graphObject objectForKey:@"id"];
+ if (graphObjectID) {
+ [result appendFormat:@" (id=%@)", graphObjectID];
+ }
+ }
+ if (self.restMethod) {
+ [result appendFormat:@", restMethod: %@", self.restMethod];
+ }
+ if (self.HTTPMethod) {
+ [result appendFormat:@", HTTPMethod: %@", self.HTTPMethod];
+ }
+ [result appendFormat:@", parameters: %@>", [self.parameters description]];
+ return result;
+
+}
+
+#pragma mark -
+
+@end
diff --git a/src/ios/facebook/FBRequestBody.h b/src/ios/facebook/FBRequestBody.h
new file mode 100644
index 000000000..ea76e5082
--- /dev/null
+++ b/src/ios/facebook/FBRequestBody.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "FBLogger.h"
+
+@interface FBRequestBody : NSObject
+
+@property (nonatomic, retain, readonly) NSData *data;
+
+- (id)init;
+
+- (void)appendWithKey:(NSString *)key
+ formValue:(NSString *)value
+ logger:(FBLogger *)logger;
+
+- (void)appendWithKey:(NSString *)key
+ imageValue:(UIImage *)image
+ logger:(FBLogger *)logger;
+
+- (void)appendWithKey:(NSString *)key
+ dataValue:(NSData *)data
+ logger:(FBLogger *)logger;
+
++ (NSString *)mimeContentType;
+
+@end
diff --git a/src/ios/facebook/FBRequestBody.m b/src/ios/facebook/FBRequestBody.m
new file mode 100644
index 000000000..44bc52e9d
--- /dev/null
+++ b/src/ios/facebook/FBRequestBody.m
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBRequestBody.h"
+
+static NSString *kStringBoundary = @"3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";
+
+@interface FBRequestBody ()
+@property (nonatomic, retain, readonly) NSMutableData *mutableData;
+- (void)appendUTF8:(NSString *)utf8;
+@end
+
+@implementation FBRequestBody
+
+@synthesize mutableData = _mutableData;
+
+- (id)init
+{
+ if (self = [super init]) {
+ _mutableData = [[NSMutableData alloc] init];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [_mutableData release];
+ [super dealloc];
+}
+
++ (NSString *)mimeContentType
+{
+ return [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kStringBoundary];
+}
+
+- (void)appendUTF8:(NSString *)utf8
+{
+ if (![self.mutableData length]) {
+ NSString *headerUTF8 = [NSString stringWithFormat:@"--%@\r\n", kStringBoundary];
+ NSData *headerData = [headerUTF8 dataUsingEncoding:NSUTF8StringEncoding];
+ [self.mutableData appendData:headerData];
+ }
+ NSData *data = [utf8 dataUsingEncoding:NSUTF8StringEncoding];
+ [self.mutableData appendData:data];
+}
+
+- (void)appendRecordBoundary
+{
+ NSString *boundary = [NSString stringWithFormat:@"\r\n--%@\r\n", kStringBoundary];
+ [self appendUTF8:boundary];
+}
+
+- (void)appendWithKey:(NSString *)key
+ formValue:(NSString *)value
+ logger:(FBLogger *)logger
+{
+ NSString *disposition =
+ [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key];
+ [self appendUTF8:disposition];
+ [self appendUTF8:value];
+ [self appendRecordBoundary];
+ [logger appendFormat:@"\n %@:\t%@", key, (NSString *)value];
+}
+
+- (void)appendWithKey:(NSString *)key
+ imageValue:(UIImage *)image
+ logger:(FBLogger *)logger
+{
+ NSString *disposition =
+ [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, key];
+ [self appendUTF8:disposition];
+ [self appendUTF8:@"Content-Type: image/png\r\n\r\n"];
+ NSData *data = UIImagePNGRepresentation(image);
+ [self.mutableData appendData:data];
+ [self appendRecordBoundary];
+ [logger appendFormat:@"\n %@:\t", key, [data length] / 1024];
+}
+
+- (void)appendWithKey:(NSString *)key
+ dataValue:(NSData *)data
+ logger:(FBLogger *)logger
+{
+ NSString *disposition =
+ [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", key, key];
+ [self appendUTF8:disposition];
+ [self appendUTF8:@"Content-Type: content/unknown\r\n\r\n"];
+ [self.mutableData appendData:data];
+ [self appendRecordBoundary];
+ [logger appendFormat:@"\n %@:\t", key, [data length] / 1024];
+}
+
+- (NSData *)data
+{
+ // No need to enforce immutability since this is internal-only and sdk will
+ // never cast/modify.
+ return self.mutableData;
+}
+
+@end
diff --git a/src/ios/facebook/FBRequestConnection+Internal.h b/src/ios/facebook/FBRequestConnection+Internal.h
new file mode 100644
index 000000000..c44442d41
--- /dev/null
+++ b/src/ios/facebook/FBRequestConnection+Internal.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBRequestConnection.h"
+
+@interface FBRequestConnection (Internal)
+
+@property (nonatomic, readonly) BOOL isResultFromCache;
+
+- (void)startWithCacheIdentity:(NSString*)cacheIdentity
+ skipRoundtripIfCached:(BOOL)consultCache;
+
+@end
diff --git a/src/ios/facebook/FBRequestConnection.h b/src/ios/facebook/FBRequestConnection.h
new file mode 100644
index 000000000..21f7a2d4a
--- /dev/null
+++ b/src/ios/facebook/FBRequestConnection.h
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import
+#import "FBGraphObject.h"
+
+// up-front decl's
+@class FBRequest;
+@class FBRequestConnection;
+@class UIImage;
+
+/*!
+ Normally requests return JSON data that is parsed into a set of `NSDictionary`
+ and `NSArray` objects.
+
+ When a request returns a non-JSON response, that response is packaged in
+ a `NSDictionary` using FBNonJSONResponseProperty as the key and the literal
+ response as the value.
+*/
+extern NSString *const FBNonJSONResponseProperty;
+
+/*!
+ @typedef FBRequestHandler
+
+ @abstract
+ A block that is passed to addRequest to register for a callback with the results of that
+ request once the connection completes.
+
+ @discussion
+ Pass a block of this type when calling addRequest. This will be called once
+ the request completes. The call occurs on the UI thread.
+
+ @param connection The `FBRequestConnection` that sent the request.
+
+ @param result The result of the request. This is a translation of
+ JSON data to `NSDictionary` and `NSArray` objects. This
+ is nil if there was an error.
+
+ @param error The `NSError` representing any error that occurred.
+
+*/
+typedef void (^FBRequestHandler)(FBRequestConnection *connection,
+ id result,
+ NSError *error);
+
+/*!
+ @class FBRequestConnection
+
+ @abstract
+ The `FBRequestConnection` represents a single connection to Facebook to service a request.
+
+ @discussion
+ The request settings are encapsulated in a reusable object. The
+ `FBRequestConnection` object encapsulates the concerns of a single communication
+ e.g. starting a connection, canceling a connection, or batching requests.
+
+*/
+@interface FBRequestConnection : NSObject
+
+/*!
+ @methodgroup Creating a request
+*/
+
+/*!
+ @method
+
+ Calls with a default timeout of 180 seconds.
+*/
+- (id)init;
+
+/*!
+ @method
+
+ @abstract
+ `FBRequestConnection` objects are used to issue one or more requests as a single
+ request/response connection with Facebook.
+
+ @discussion
+ For a single request, the usual method for creating an `FBRequestConnection`
+ object is to call one of the **start* ** methods on . However, it is
+ allowable to init an `FBRequestConnection` object directly, and call
+ to add one or more request objects to the
+ connection, before calling start.
+
+ Note that if requests are part of a batch, they must have an open
+ FBSession that has an access token associated with it. Alternatively a default App ID
+ must be set either in the plist or through an explicit call to <[FBSession defaultAppID]>.
+
+ @param timeout The `NSTimeInterval` (seconds) to wait for a response before giving up.
+*/
+
+- (id)initWithTimeout:(NSTimeInterval)timeout;
+
+// properties
+
+/*!
+ @abstract
+ The request that will be sent to the server.
+
+ @discussion
+ This property can be used to create a `NSURLRequest` without using
+ `FBRequestConnection` to send that request. It is legal to set this property
+ in which case the provided `NSMutableURLRequest` will be used instead. However,
+ the `NSMutableURLRequest` must result in an appropriate response. Furthermore, once
+ this property has been set, no more objects can be added to this
+ `FBRequestConnection`.
+*/
+@property(nonatomic, retain, readwrite) NSMutableURLRequest *urlRequest;
+
+/*!
+ @abstract
+ The raw response that was returned from the server. (readonly)
+
+ @discussion
+ This property can be used to inspect HTTP headers that were returned from
+ the server.
+
+ The property is nil until the request completes. If there was a response
+ then this property will be non-nil during the FBRequestHandler callback.
+*/
+@property(nonatomic, retain, readonly) NSHTTPURLResponse *urlResponse;
+
+/*!
+ @methodgroup Adding requests
+*/
+
+/*!
+ @method
+
+ @abstract
+ This method adds an object to this connection and then calls
+ on the connection.
+
+ @discussion
+ The completion handler is retained until the block is called upon the
+ completion or cancellation of the connection.
+
+ @param request A request to be included in the round-trip when start is called.
+ @param handler A handler to call back when the round-trip completes or times out.
+*/
+- (void)addRequest:(FBRequest*)request
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ This method adds an object to this connection and then calls
+ on the connection.
+
+ @discussion
+ The completion handler is retained until the block is called upon the
+ completion or cancellation of the connection. This request can be named
+ to allow for using the request's response in a subsequent request.
+
+ @param request A request to be included in the round-trip when start is called.
+
+ @param handler A handler to call back when the round-trip completes or times out.
+
+ @param name An optional name for this request. This can be used to feed
+ the results of one request to the input of another in the same
+ `FBRequestConnection` as described in
+ [Graph API Batch Requests]( https://developers.facebook.com/docs/reference/api/batch/ ).
+*/
+- (void)addRequest:(FBRequest*)request
+ completionHandler:(FBRequestHandler)handler
+ batchEntryName:(NSString*)name;
+
+/*!
+ @methodgroup Instance methods
+*/
+
+/*!
+ @method
+
+ @abstract
+ This method starts a connection with the server and is capable of handling all of the
+ requests that were added to the connection.
+
+ @discussion
+ Errors are reported via the handler callback, even in cases where no
+ communication is attempted by the implementation of `FBRequestConnection`. In
+ such cases multiple error conditions may apply, and if so the following
+ priority (highest to lowest) is used:
+
+ - `FBRequestConnectionInvalidRequestKey` -- this error is reported when an
+ cannot be encoded for transmission.
+
+ - `FBRequestConnectionInvalidBatchKey` -- this error is reported when any
+ request in the connection cannot be encoded for transmission with the batch.
+ In this scenario all requests fail.
+
+ This method cannot be called twice for an `FBRequestConnection` instance.
+*/
+- (void)start;
+
+/*!
+ @method
+
+ @abstract
+ Signals that a connection should be logically terminated as the
+ application is no longer interested in a response.
+
+ @discussion
+ Synchronously calls any handlers indicating the request was cancelled. Cancel
+ does not guarantee that the request-related processing will cease. It
+ does promise that all handlers will complete before the cancel returns. A call to
+ cancel prior to a start implies a cancellation of all requests associated
+ with the connection.
+*/
+- (void)cancel;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API request for user info (/me), creates an
+ then uses an object to start the connection with Facebook. The
+ request uses the active session represented by `[FBSession activeSession]`.
+
+ See
+
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startForMeWithCompletionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API request for user friends (/me/friends), creates an
+ then uses an object to start the connection with Facebook. The
+ request uses the active session represented by `[FBSession activeSession]`.
+
+ See
+
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startForMyFriendsWithCompletionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API post of a photo. The request
+ uses the active session represented by `[FBSession activeSession]`.
+
+ @param photo A `UIImage` for the photo to upload.
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startForUploadPhoto:(UIImage *)photo
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API post of a status update. The request
+ uses the active session represented by `[FBSession activeSession]`.
+
+ @param message The message to post.
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API post of a status update. The request
+ uses the active session represented by `[FBSession activeSession]`.
+
+ @param message The message to post.
+ @param place The place to checkin with, or nil. Place may be an fbid or a
+ graph object representing a place.
+ @param tags Array of friends to tag in the status update, each element
+ may be an fbid or a graph object representing a user.
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message
+ place:(id)place
+ tags:(id)tags
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Starts a request representing a Graph API call to the "search" endpoint
+ for a given location using the active session.
+
+ @discussion
+ Simplifies starting a request to search for places near a coordinate.
+
+ This method creates the necessary object and initializes and
+ starts an object. A successful Graph API call will
+ return an array of objects representing the nearby locations.
+
+ @param coordinate The search coordinates.
+
+ @param radius The search radius in meters.
+
+ @param limit The maxiumum number of results to return. It is
+ possible to receive fewer than this because of the
+ radius and because of server limits.
+
+ @param searchText The text to use in the query to narrow the set of places
+ returned.
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)limit
+ searchText:(NSString*)searchText
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make a graph API request, creates an object for then
+ uses an object to start the connection with Facebook. The
+ request uses the active session represented by `[FBSession activeSession]`.
+
+ See
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Simple method to make post an object using the graph API, creates an object for
+ HTTP POST, then uses to start a connection with Facebook. The request uses
+ the active session represented by `[FBSession activeSession]`.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param graphObject An object or open graph action to post.
+
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startForPostWithGraphPath:(NSString*)graphPath
+ graphObject:(id)graphObject
+ completionHandler:(FBRequestHandler)handler;
+
+/*!
+ @method
+
+ @abstract
+ Creates an `FBRequest` object for a Graph API call, instantiate an
+ object, add the request to the newly created
+ connection and finally start the connection. Use this method for
+ specifying the request parameters and HTTP Method. The request uses
+ the active session represented by `[FBSession activeSession]`.
+
+ @param graphPath The Graph API endpoint to use for the request, for example "me".
+
+ @param parameters The parameters for the request. A value of nil sends only the automatically handled parameters, for example, the access token. The default is nil.
+
+ @param HTTPMethod The HTTP method to use for the request. A nil value implies a GET.
+
+ @param handler The handler block to call when the request completes with a success, error, or cancel action.
+ */
++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath
+ parameters:(NSDictionary*)parameters
+ HTTPMethod:(NSString*)HTTPMethod
+ completionHandler:(FBRequestHandler)handler;
+
+@end
diff --git a/src/ios/facebook/FBRequestConnection.m b/src/ios/facebook/FBRequestConnection.m
new file mode 100644
index 000000000..cdb563208
--- /dev/null
+++ b/src/ios/facebook/FBRequestConnection.m
@@ -0,0 +1,1438 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+#import "FBSBJSON.h"
+#import "FBError.h"
+#import "FBURLConnection.h"
+#import "FBRequestBody.h"
+#import "FBSession.h"
+#import "FBSession+Internal.h"
+#import "FBSettings.h"
+#import "FBRequestConnection.h"
+#import "FBRequestConnection+Internal.h"
+#import "FBRequest.h"
+#import "Facebook.h"
+#import "FBGraphObject.h"
+#import "FBLogger.h"
+#import "FBUtility.h"
+#import "FBDataDiskCache.h"
+#import "FBSDKVersion.h"
+
+// URL construction constants
+NSString *const kGraphURL = @"https://graph." FB_BASE_URL;
+NSString *const kGraphBaseURL = @"https://graph." FB_BASE_URL @"/";
+NSString *const kRestBaseURL = @"https://api." FB_BASE_URL @"/method/";
+NSString *const kBatchKey = @"batch";
+NSString *const kBatchMethodKey = @"method";
+NSString *const kBatchRelativeURLKey = @"relative_url";
+NSString *const kBatchAttachmentKey = @"attached_files";
+NSString *const kBatchFileNamePrefix = @"file";
+
+NSString *const kAccessTokenKey = @"access_token";
+NSString *const kSDK = @"ios";
+NSString *const kUserAgentBase = @"FBiOSSDK";
+
+NSString *const kExtendTokenRestMethod = @"auth.extendSSOAccessToken";
+NSString *const kBatchRestMethodBaseURL = @"method/";
+
+// response object property/key
+NSString *const FBNonJSONResponseProperty = @"FACEBOOK_NON_JSON_RESULT";
+
+static const int kRESTAPIAccessTokenErrorCode = 190;
+static const int kRESTAPIPermissionErrorCode = 200;
+static const int kAPISessionNoLongerActiveErrorCode = 2500;
+static const NSTimeInterval kDefaultTimeout = 180.0;
+static const int kMaximumBatchSize = 50;
+
+typedef void (^KeyValueActionHandler)(NSString *key, id value);
+
+// ----------------------------------------------------------------------------
+// Private class to store requests and their metadata.
+//
+@interface FBRequestMetadata : NSObject
+
+@property (nonatomic, retain) FBRequest *request;
+@property (nonatomic, copy) FBRequestHandler completionHandler;
+@property (nonatomic, copy) NSString *batchEntryName;
+
+- (id) initWithRequest:(FBRequest *)request
+ completionHandler:(FBRequestHandler)handler
+ batchEntryName:(NSString *)name;
+
+@end
+
+@implementation FBRequestMetadata
+
+@synthesize batchEntryName = _batchEntryName;
+@synthesize completionHandler = _completionHandler;
+@synthesize request = _request;
+
+- (id) initWithRequest:(FBRequest *)request
+ completionHandler:(FBRequestHandler)handler
+ batchEntryName:(NSString *)name {
+
+ if (self = [super init]) {
+ self.request = request;
+ self.completionHandler = handler;
+ self.batchEntryName = name;
+ }
+ return self;
+}
+
+- (void) dealloc {
+ [_request release];
+ [_completionHandler release];
+ [_batchEntryName release];
+ [super dealloc];
+}
+
+- (NSString*)description {
+ return [NSString stringWithFormat:@"<%@: %p, batchEntryName: %@, completionHandler: %p, request: %@>",
+ NSStringFromClass([self class]),
+ self,
+ self.batchEntryName,
+ self.completionHandler,
+ self.request.description];
+}
+
+@end
+
+// ----------------------------------------------------------------------------
+// FBRequestConnectionState
+
+typedef enum FBRequestConnectionState {
+ kStateCreated,
+ kStateSerialized,
+ kStateStarted,
+ kStateCompleted,
+ kStateCancelled,
+} FBRequestConnectionState;
+
+// ----------------------------------------------------------------------------
+// Private properties and methods
+
+@interface FBRequestConnection ()
+
+@property (nonatomic, retain) FBURLConnection *connection;
+@property (nonatomic, retain) NSMutableArray *requests;
+@property (nonatomic) FBRequestConnectionState state;
+@property (nonatomic) NSTimeInterval timeout;
+@property (nonatomic, retain) NSMutableURLRequest *internalUrlRequest;
+@property (nonatomic, retain, readwrite) NSHTTPURLResponse *urlResponse;
+@property (nonatomic, retain) FBRequest *deprecatedRequest;
+@property (nonatomic, retain) FBLogger *logger;
+@property (nonatomic) unsigned long requestStartTime;
+@property (nonatomic, readonly) BOOL isResultFromCache;
+
+- (NSMutableURLRequest *)requestWithBatch:(NSArray *)requests
+ timeout:(NSTimeInterval)timeout;
+
+- (NSString *)urlStringForSingleRequest:(FBRequest *)request forBatch:(BOOL)forBatch;
+
+- (void)appendJSONRequests:(NSArray *)requests
+ toBody:(FBRequestBody *)body
+ andNameAttachments:(NSMutableDictionary *)attachments
+ logger:(FBLogger *)logger;
+
+- (void)addRequest:(FBRequestMetadata *)metadata
+ toBatch:(NSMutableArray *)batch
+ attachments:(NSDictionary *)attachments;
+
+- (BOOL)isAttachment:(id)item;
+
+- (void)appendAttachments:(NSDictionary *)attachments
+ toBody:(FBRequestBody *)body
+ addFormData:(BOOL)addFormData
+ logger:(FBLogger *)logger;
+
++ (void)processGraphObject:(id)object
+ forPath:(NSString*)path
+ withAction:(KeyValueActionHandler)action;
+
+- (void)completeWithResponse:(NSURLResponse *)response
+ data:(NSData *)data
+ orError:(NSError *)error;
+
+- (NSArray *)parseJSONResponse:(NSData *)data
+ error:(NSError **)error
+ statusCode:(int)statusCode;
+
+- (id)parseJSONOrOtherwise:(NSString *)utf8
+ error:(NSError **)error;
+
+- (void)completeDeprecatedWithData:(NSData *)data
+ results:(NSArray *)results
+ orError:(NSError *)error;
+
+- (void)completeWithResults:(NSArray *)results
+ orError:(NSError *)error;
+
+- (NSError *)errorFromResult:(id)idResult;
+
+- (NSError *)errorWithCode:(FBErrorCode)code
+ statusCode:(int)statusCode
+ parsedJSONResponse:(id)response
+ innerError:(NSError*)innerError
+ message:(NSString*)message;
+
+- (NSError *)checkConnectionError:(NSError *)innerError
+ statusCode:(int)statusCode
+ parsedJSONResponse:(id)response;
+
+- (BOOL)isInvalidSessionError:(NSError *)error
+ resultIndex:(int)index;
+
+- (void)registerTokenToOmitFromLog:(NSString *)token;
+
+- (void)addPiggybackRequests;
+
+- (void)logRequest:(NSMutableURLRequest *)request
+ bodyLength:(int)bodyLength
+ bodyLogger:(FBLogger *)bodyLogger
+ attachmentLogger:(FBLogger *)attachmentLogger;
+
+- (NSString *)getBatchAppID:(NSArray*)requests;
+
++ (NSString *)userAgent;
+
++ (void)addRequestToExtendTokenForSession:(FBSession*)session connection:(FBRequestConnection*)connection;
+
+@end
+
+// ----------------------------------------------------------------------------
+// FBRequestConnection
+
+@implementation FBRequestConnection
+
+// ----------------------------------------------------------------------------
+// Property implementations
+
+@synthesize connection = _connection;
+@synthesize requests = _requests;
+@synthesize state = _state;
+@synthesize timeout = _timeout;
+@synthesize internalUrlRequest = _internalUrlRequest;
+@synthesize urlResponse = _urlResponse;
+@synthesize deprecatedRequest = _deprecatedRequest;
+@synthesize logger = _logger;
+@synthesize requestStartTime = _requestStartTime;
+@synthesize isResultFromCache = _isResultFromCache;
+
+- (NSMutableURLRequest *)urlRequest
+{
+ if (self.internalUrlRequest) {
+ NSMutableURLRequest *request = self.internalUrlRequest;
+
+ [request setValue:[FBRequestConnection userAgent] forHTTPHeaderField:@"User-Agent"];
+ [self logRequest:request bodyLength:0 bodyLogger:nil attachmentLogger:nil];
+
+ return request;
+
+ } else {
+ // CONSIDER: Could move to kStateSerialized here by caching result, but
+ // it seems bad for a get accessor to modify state in observable manner.
+ return [self requestWithBatch:self.requests timeout:_timeout];
+ }
+}
+
+- (void)setUrlRequest:(NSMutableURLRequest *)request
+{
+ NSAssert((self.state == kStateCreated) || (self.state == kStateSerialized),
+ @"Cannot set urlRequest after starting or cancelling.");
+ self.state = kStateSerialized;
+
+ self.internalUrlRequest = request;
+}
+
+// ----------------------------------------------------------------------------
+// Lifetime
+
+- (id)init
+{
+ return [self initWithTimeout:kDefaultTimeout];
+}
+
+- (id)initWithTimeout:(NSTimeInterval)timeout
+{
+ if (self = [super init]) {
+ _requests = [[NSMutableArray alloc] init];
+ _timeout = timeout;
+ _state = kStateCreated;
+ _logger = [[FBLogger alloc] initWithLoggingBehavior:FBLoggingBehaviorFBRequests];
+ _isResultFromCache = NO;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [_connection cancel];
+ [_connection release];
+ [_requests release];
+ [_internalUrlRequest release];
+ [_urlResponse release];
+ [_deprecatedRequest release];
+ [_logger release];
+ [super dealloc];
+}
+
+// ----------------------------------------------------------------------------
+// Public methods
+
+- (void)addRequest:(FBRequest *)request
+ completionHandler:(FBRequestHandler)handler
+{
+ [self addRequest:request completionHandler:handler batchEntryName:nil];
+}
+
+- (void)addRequest:(FBRequest *)request
+ completionHandler:(FBRequestHandler)handler
+ batchEntryName:(NSString *)name
+{
+ NSAssert(self.state == kStateCreated,
+ @"Requests must be added before starting or cancelling.");
+
+ FBRequestMetadata *metadata = [[FBRequestMetadata alloc] initWithRequest:request
+ completionHandler:handler
+ batchEntryName:name];
+ [self.requests addObject:metadata];
+ [metadata release];
+}
+
+- (void)start
+{
+ [self startWithCacheIdentity:nil
+ skipRoundtripIfCached:NO];
+}
+
+- (void)cancel {
+ // Cancelling self.connection might trigger error handlers that cause us to
+ // get freed. Make sure we stick around long enough to finish this method call.
+ [[self retain] autorelease];
+
+ [self.connection cancel];
+ self.connection = nil;
+ self.state = kStateCancelled;
+}
+
+// ----------------------------------------------------------------------------
+// Public class methods
+
++ (FBRequestConnection*)startForMeWithCompletionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForMe];
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection*)startForMyFriendsWithCompletionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForMyFriends];
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection*)startForUploadPhoto:(UIImage *)photo
+ completionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForUploadPhoto:photo];
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message
+ completionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForPostStatusUpdate:message];
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection *)startForPostStatusUpdate:(NSString *)message
+ place:(id)place
+ tags:(id)tags
+ completionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForPostStatusUpdate:message
+ place:place
+ tags:tags];
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection*)startForPlacesSearchAtCoordinate:(CLLocationCoordinate2D)coordinate
+ radiusInMeters:(NSInteger)radius
+ resultsLimit:(NSInteger)limit
+ searchText:(NSString*)searchText
+ completionHandler:(FBRequestHandler)handler {
+ FBRequest *request = [FBRequest requestForPlacesSearchAtCoordinate:coordinate
+ radiusInMeters:radius
+ resultsLimit:limit
+ searchText:searchText];
+
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath
+ completionHandler:(FBRequestHandler)handler
+{
+ return [FBRequestConnection startWithGraphPath:graphPath
+ parameters:nil
+ HTTPMethod:nil
+ completionHandler:handler];
+}
+
++ (FBRequestConnection*)startForPostWithGraphPath:(NSString*)graphPath
+ graphObject:(id)graphObject
+ completionHandler:(FBRequestHandler)handler
+{
+ FBRequest *request = [FBRequest requestForPostWithGraphPath:graphPath
+ graphObject:graphObject];
+
+ return [request startWithCompletionHandler:handler];
+}
+
++ (FBRequestConnection*)startWithGraphPath:(NSString*)graphPath
+ parameters:(NSDictionary*)parameters
+ HTTPMethod:(NSString*)HTTPMethod
+ completionHandler:(FBRequestHandler)handler
+{
+ FBRequest *request = [FBRequest requestWithGraphPath:graphPath
+ parameters:parameters
+ HTTPMethod:HTTPMethod];
+
+ return [request startWithCompletionHandler:handler];
+}
+
+// ----------------------------------------------------------------------------
+// Private methods
+
+- (void)startWithCacheIdentity:(NSString*)cacheIdentity
+ skipRoundtripIfCached:(BOOL)skipRoundtripIfCached
+{
+ if ([self.requests count] == 1) {
+ FBRequestMetadata *firstMetadata = [self.requests objectAtIndex:0];
+ if ([firstMetadata.request delegate]) {
+ self.deprecatedRequest = firstMetadata.request;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ [self.deprecatedRequest setState:kFBRequestStateLoading];
+#pragma GCC diagnostic pop
+ }
+ }
+
+ NSMutableURLRequest *request = nil;
+ NSData *cachedData = nil;
+ NSURL *cacheIdentityURL = nil;
+ if (cacheIdentity) {
+ // warning! this property has significant side-effects, and should be executed at the right moment
+ // depending on whether there may be batching or whether we are certain there is no batching
+ request = self.urlRequest;
+
+ // when we generalize this for consumers of FBRequest, then we will use a more
+ // normalized form for our identification scheme than this URL construction; given the only
+ // clients are the two pickers -- this scheme achieves stability via being a closed system,
+ // and provides a simple first step to the more general solution
+ cacheIdentityURL = [[[NSURL alloc] initWithScheme:@"FBRequestCache"
+ host:cacheIdentity
+ path:[NSString stringWithFormat:@"/%@", request.URL]]
+ autorelease];
+
+ if (skipRoundtripIfCached) {
+ cachedData = [[FBDataDiskCache sharedCache] dataForURL:cacheIdentityURL];
+ }
+ }
+
+ if (self.internalUrlRequest == nil && !cacheIdentity) {
+ // If we have all Graph API calls, see if we want to piggyback any internal calls onto
+ // the request to reduce round-trips. (The piggybacked calls may themselves be non-Graph
+ // API calls, but must be limited to API calls which are batchable. Not all are, which is
+ // why we won't piggyback on top of a REST API call.) Don't try this if the caller gave us
+ // an already-formed request object, since we don't know its structure.
+ BOOL safeForPiggyback = YES;
+ for (FBRequestMetadata *requestMetadata in self.requests) {
+ if (requestMetadata.request.restMethod) {
+ safeForPiggyback = NO;
+ break;
+ }
+ }
+ // If we wouldn't be able to compute a batch_app_id, don't piggyback on this
+ // request.
+ NSString *batchAppID = [self getBatchAppID:self.requests];
+ safeForPiggyback &= (batchAppID != nil) && (batchAppID.length > 0);
+
+ if (safeForPiggyback) {
+ [self addPiggybackRequests];
+ }
+ }
+
+ // warning! this property is side-effecting (and should probably be refactored at some point...)
+ // still, if we have made it this far and still don't have a request object, we need one now
+ if (!request) {
+ request = self.urlRequest;
+ }
+
+ NSAssert((self.state == kStateCreated) || (self.state == kStateSerialized),
+ @"Cannot call start again after calling start or cancel.");
+ self.state = kStateStarted;
+
+ _requestStartTime = [FBUtility currentTimeInMilliseconds];
+
+ if (!cachedData) {
+ FBURLConnectionHandler handler =
+ ^(FBURLConnection *connection,
+ NSError *error,
+ NSURLResponse *response,
+ NSData *responseData) {
+ // cache this data if we have successful response and a cache identity to work with
+ if (cacheIdentityURL &&
+ [response isKindOfClass:[NSHTTPURLResponse class]] &&
+ ((NSHTTPURLResponse*)response).statusCode == 200) {
+ [[FBDataDiskCache sharedCache] setData:responseData
+ forURL:cacheIdentityURL];
+ }
+ // complete on result from round-trip to server
+ [self completeWithResponse:response
+ data:responseData
+ orError:error];
+ };
+
+ id deprecatedDelegate = [self.deprecatedRequest delegate];
+ if ([deprecatedDelegate respondsToSelector:@selector(requestLoading:)]) {
+ [deprecatedDelegate requestLoading:self.deprecatedRequest];
+ }
+
+ FBURLConnection *connection = [[FBURLConnection alloc] initWithRequest:request
+ skipRoundTripIfCached:NO
+ completionHandler:handler];
+ self.connection = connection;
+ [connection release];
+ } else {
+ _isResultFromCache = YES;
+
+ // complete on result from cache
+ [self completeWithResponse:nil
+ data:cachedData
+ orError:nil];
+
+ }
+}
+
+//
+// Generates a NSURLRequest based on the contents of self.requests, and sets
+// options on the request. Chooses between URL-based request for a single
+// request and JSON-based request for batches.
+//
+- (NSMutableURLRequest *)requestWithBatch:(NSArray *)requests
+ timeout:(NSTimeInterval)timeout
+{
+ FBRequestBody *body = [[FBRequestBody alloc] init];
+ FBLogger *bodyLogger = [[FBLogger alloc] initWithLoggingBehavior:_logger.loggingBehavior];
+ FBLogger *attachmentLogger = [[FBLogger alloc] initWithLoggingBehavior:_logger.loggingBehavior];
+
+ NSMutableURLRequest *request;
+
+ if (requests.count == 0) {
+ [[NSException exceptionWithName:FBInvalidOperationException
+ reason:@"FBRequestConnection: Must have at least one request or urlRequest not specified."
+ userInfo:nil]
+ raise];
+
+ }
+
+ if ([requests count] == 1) {
+ FBRequestMetadata *metadata = [requests objectAtIndex:0];
+ NSURL *url = [NSURL URLWithString:[self urlStringForSingleRequest:metadata.request forBatch:NO]];
+ request = [NSMutableURLRequest requestWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeout];
+
+ // HTTP methods are case-sensitive; be helpful in case someone provided a mixed case one.
+ NSString *httpMethod = [metadata.request.HTTPMethod uppercaseString];
+ [request setHTTPMethod:httpMethod];
+ [self appendAttachments:metadata.request.parameters
+ toBody:body
+ addFormData:[httpMethod isEqualToString:@"POST"]
+ logger:attachmentLogger];
+
+ // if we have a post object, also roll that into the body
+ if (metadata.request.graphObject) {
+ [FBRequestConnection processGraphObject:metadata.request.graphObject
+ forPath:[url path]
+ withAction:^(NSString *key, id value) {
+ [body appendWithKey:key formValue:value logger:bodyLogger];
+ }];
+ }
+ } else {
+ // Find the session with an app ID and use that as the batch_app_id. If we can't
+ // find one, try to load it from the plist. As a last resort, pass 0.
+ NSString *batchAppID = [self getBatchAppID:requests];
+ if (!batchAppID || batchAppID.length == 0) {
+ // The Graph API batch method requires either an access token or batch_app_id.
+ // If we can't determine an App ID to use for the batch, we can't issue it.
+ [[NSException exceptionWithName:FBInvalidOperationException
+ reason:@"FBRequestConnection: At least one request in a"
+ " batch must have an open FBSession, or a default"
+ " app ID must be specified."
+ userInfo:nil]
+ raise];
+ }
+
+ [body appendWithKey:@"batch_app_id" formValue:batchAppID logger:bodyLogger];
+
+ NSMutableDictionary *attachments = [[NSMutableDictionary alloc] init];
+
+ [self appendJSONRequests:requests
+ toBody:body
+ andNameAttachments:attachments
+ logger:bodyLogger];
+
+ [self appendAttachments:attachments
+ toBody:body
+ addFormData:NO
+ logger:attachmentLogger];
+
+ [attachments release];
+
+ request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kGraphURL]
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
+ timeoutInterval:timeout];
+ [request setHTTPMethod:@"POST"];
+ }
+
+ [request setHTTPBody:[body data]];
+ NSUInteger bodyLength = [[body data] length] / 1024;
+ [body release];
+
+ [request setValue:[FBRequestConnection userAgent] forHTTPHeaderField:@"User-Agent"];
+ [request setValue:[FBRequestBody mimeContentType] forHTTPHeaderField:@"Content-Type"];
+
+ [self logRequest:request bodyLength:bodyLength bodyLogger:bodyLogger attachmentLogger:attachmentLogger];
+
+ // Safely release now that everything's serialized into the logger.
+ [bodyLogger release];
+ [attachmentLogger release];
+
+ return request;
+}
+
+- (void)logRequest:(NSMutableURLRequest *)request
+ bodyLength:(int)bodyLength
+ bodyLogger:(FBLogger *)bodyLogger
+ attachmentLogger:(FBLogger *)attachmentLogger
+{
+ if (_logger.isActive) {
+ [_logger appendFormat:@"Request <#%d>:\n", _logger.loggerSerialNumber];
+ [_logger appendKey:@"URL" value:[[request URL] absoluteString]];
+ [_logger appendKey:@"Method" value:[request HTTPMethod]];
+ [_logger appendKey:@"UserAgent" value:[request valueForHTTPHeaderField:@"User-Agent"]];
+ [_logger appendKey:@"MIME" value:[request valueForHTTPHeaderField:@"Content-Type"]];
+
+ if (bodyLength != 0) {
+ [_logger appendKey:@"Body Size" value:[NSString stringWithFormat:@"%d kB", bodyLength / 1024]];
+ }
+
+ if (bodyLogger != nil) {
+ [_logger appendKey:@"Body (w/o attachments)" value:bodyLogger.contents];
+ }
+
+ if (attachmentLogger != nil) {
+ [_logger appendKey:@"Attachments" value:attachmentLogger.contents];
+ }
+
+ [_logger appendString:@"\n"];
+
+ [_logger emitToNSLog];
+ }
+}
+
+//
+// Generates a URL for a batch containing only a single request,
+// and names all attachments that need to go in the body of the
+// request.
+//
+// The URL contains all parameters that are not body attachments,
+// including the session key if present.
+//
+// Attachments are named and referenced by name in the URL.
+//
+- (NSString *)urlStringForSingleRequest:(FBRequest *)request forBatch:(BOOL)forBatch
+{
+ [request.parameters setValue:@"json" forKey:@"format"];
+ [request.parameters setValue:kSDK forKey:@"sdk"];
+ NSString *token = request.session.accessToken;
+ if (token) {
+ [request.parameters setValue:token forKey:kAccessTokenKey];
+ [self registerTokenToOmitFromLog:token];
+ }
+
+ NSString *baseURL;
+ if (request.restMethod) {
+ if (forBatch) {
+ baseURL = [kBatchRestMethodBaseURL stringByAppendingString:request.restMethod];
+ } else {
+ baseURL = [kRestBaseURL stringByAppendingString:request.restMethod];
+ }
+ } else {
+ if (forBatch) {
+ baseURL = request.graphPath;
+ } else {
+ baseURL = [kGraphBaseURL stringByAppendingString:request.graphPath];
+ }
+ }
+
+ NSString *url = [FBRequest serializeURL:baseURL
+ params:request.parameters
+ httpMethod:request.HTTPMethod];
+ return url;
+}
+
+// Find the first session with an app ID and use that as the batch_app_id. If we can't
+// find one, return the default app ID (which may still be nil if not specified
+// programmatically or via the plist).
+- (NSString *)getBatchAppID:(NSArray*)requests
+{
+ for (FBRequestMetadata *metadata in requests) {
+ if (metadata.request.session.appID.length > 0) {
+ return metadata.request.session.appID;
+ }
+ }
+ return [FBSession defaultAppID];
+}
+
+//
+// Serializes all requests in the batch to JSON and appends the result to
+// body. Also names all attachments that need to go as separate blocks in
+// the body of the request.
+//
+// All the requests are serialized into JSON, with any binary attachments
+// named and referenced by name in the JSON.
+//
+- (void)appendJSONRequests:(NSArray *)requests
+ toBody:(FBRequestBody *)body
+ andNameAttachments:(NSMutableDictionary *)attachments
+ logger:(FBLogger *)logger
+{
+ NSMutableArray *batch = [[NSMutableArray alloc] init];
+ for (FBRequestMetadata *metadata in requests) {
+ [self addRequest:metadata
+ toBatch:batch
+ attachments:attachments];
+ }
+
+ FBSBJSON *writer = [[FBSBJSON alloc] init];
+ NSString *jsonBatch = [writer stringWithObject:batch];
+ [writer release];
+ [batch release];
+
+ [body appendWithKey:kBatchKey formValue:jsonBatch logger:logger];
+}
+
+//
+// Adds request data to a batch in a format expected by the JsonWriter.
+// Binary attachments are referenced by name in JSON and added to the
+// attachments dictionary.
+//
+- (void)addRequest:(FBRequestMetadata *)metadata
+ toBatch:(NSMutableArray *)batch
+ attachments:(NSDictionary *)attachments
+{
+ NSMutableDictionary *requestElement = [[[NSMutableDictionary alloc] init] autorelease];
+
+ if (metadata.batchEntryName) {
+ [requestElement setObject:metadata.batchEntryName forKey:@"name"];
+ }
+
+ NSString *token = metadata.request.session.accessToken;
+ if (token) {
+ [metadata.request.parameters setObject:token forKey:kAccessTokenKey];
+ [self registerTokenToOmitFromLog:token];
+ }
+
+ NSString *urlString = [self urlStringForSingleRequest:metadata.request forBatch:YES];
+ [requestElement setObject:urlString forKey:kBatchRelativeURLKey];
+ [requestElement setObject:metadata.request.HTTPMethod forKey:kBatchMethodKey];
+
+ NSMutableString *attachmentNames = [NSMutableString string];
+
+ for (id key in [metadata.request.parameters keyEnumerator]) {
+ NSObject *value = [metadata.request.parameters objectForKey:key];
+ if ([self isAttachment:value]) {
+ NSString *name = [NSString stringWithFormat:@"%@%d",
+ kBatchFileNamePrefix,
+ [attachments count]];
+ if ([attachmentNames length]) {
+ [attachmentNames appendString:@","];
+ }
+ [attachmentNames appendString:name];
+ [attachments setValue:value forKey:name];
+ }
+ }
+
+ // if we have a post object, also roll that into the body
+ if (metadata.request.graphObject) {
+ NSMutableString *bodyValue = [[[NSMutableString alloc] init] autorelease];
+ __block NSString *delimiter = @"";
+ [FBRequestConnection
+ processGraphObject:metadata.request.graphObject
+ forPath:urlString
+ withAction:^(NSString *key, id value) {
+ // escape the value
+ value = [FBUtility stringByURLEncodingString:[value description]];
+ [bodyValue appendFormat:@"%@%@=%@",
+ delimiter,
+ key,
+ value];
+ delimiter = @"&";
+ }];
+ [requestElement setObject:bodyValue forKey:@"body"];
+ }
+
+ if ([attachmentNames length]) {
+ [requestElement setObject:attachmentNames forKey:kBatchAttachmentKey];
+ }
+
+ [batch addObject:requestElement];
+}
+
+- (BOOL)isAttachment:(id)item
+{
+ return
+ [item isKindOfClass:[UIImage class]] ||
+ [item isKindOfClass:[NSData class]];
+}
+
+- (void)appendAttachments:(NSDictionary *)attachments
+ toBody:(FBRequestBody *)body
+ addFormData:(BOOL)addFormData
+ logger:(FBLogger *)logger
+{
+ // key is name for both, first case is string which we can print, second pass grabs object
+ if (addFormData) {
+ for (NSString *key in [attachments keyEnumerator]) {
+ NSObject *value = [attachments objectForKey:key];
+ if ([value isKindOfClass:[NSString class]]) {
+ [body appendWithKey:key formValue:(NSString *)value logger:logger];
+ }
+ }
+ }
+
+ for (NSString *key in [attachments keyEnumerator]) {
+ NSObject *value = [attachments objectForKey:key];
+ if ([value isKindOfClass:[UIImage class]]) {
+ [body appendWithKey:key imageValue:(UIImage *)value logger:logger];
+ } else if ([value isKindOfClass:[NSData class]]) {
+ [body appendWithKey:key dataValue:(NSData *)value logger:logger];
+ }
+ }
+}
+
+#pragma mark Graph Object serialization
+
++ (void)processGraphObjectPropertyKey:(NSString*)key
+ value:(id)value
+ action:(KeyValueActionHandler)action
+ passByValue:(BOOL)passByValue {
+ if ([value conformsToProtocol:@protocol(FBGraphObject)]) {
+ NSDictionary *refObject = (NSDictionary*)value;
+
+ if (passByValue) {
+ // We need to pass all properties of this object in key[propertyName] format.
+ for (NSString *propertyName in refObject) {
+ NSString *subKey = [NSString stringWithFormat:@"%@[%@]", key, propertyName];
+ id subValue = [refObject objectForKey:propertyName];
+ // Note that passByValue is not inherited by subkeys.
+ [self processGraphObjectPropertyKey:subKey value:subValue action:action passByValue:NO];
+ }
+ } else {
+ // Normal case is passing objects by reference, so just pass the ID or URL, if any.
+ NSString *subValue;
+ if ((subValue = [refObject objectForKey:@"id"])) { // fbid
+ if ([subValue isKindOfClass:[NSDecimalNumber class]]) {
+ subValue = [(NSDecimalNumber*)subValue stringValue];
+ }
+ action(key, subValue);
+ } else if ((subValue = [refObject objectForKey:@"url"])) { // canonical url (external)
+ action(key, subValue);
+ }
+ }
+ } else if ([value isKindOfClass:[NSString class]] ||
+ [value isKindOfClass:[NSNumber class]]) {
+ // Just serialize these.
+ action(key, value);
+ } else if ([value isKindOfClass:[NSArray class]]) {
+ // Arrays are serialized as multiple elements with keys of the
+ // form key[0], key[1], etc.
+ NSArray *array = (NSArray*)value;
+ int count = array.count;
+ for (int i = 0; i < count; ++i) {
+ NSString *subKey = [NSString stringWithFormat:@"%@[%d]", key, i];
+ id subValue = [array objectAtIndex:i];
+ [self processGraphObjectPropertyKey:subKey value:subValue action:action passByValue:passByValue];
+ }
+ }
+}
+
++ (void)processGraphObject:(id)object forPath:(NSString*)path withAction:(KeyValueActionHandler)action {
+ BOOL isOGAction = NO;
+ if ([path hasPrefix:@"me/"] ||
+ [path hasPrefix:@"/me/"]) {
+ // In general, graph objects are passed by reference (ID/URL). But if this is an OG Action,
+ // we need to pass the entire values of the contents of the 'image' property, as they
+ // contain important metadata beyond just a URL. We don't have a 100% foolproof way of knowing
+ // if we are posting an OG Action, given that batched requests can have parameter substitution,
+ // but passing the OG Action type as a substituted parameter is unlikely.
+ // It looks like an OG Action if it's posted to me/namespace:action[?other=stuff].
+ NSUInteger colonLocation = [path rangeOfString:@":"].location;
+ NSUInteger questionMarkLocation = [path rangeOfString:@"?"].location;
+ isOGAction = (colonLocation != NSNotFound && colonLocation > 3) &&
+ (questionMarkLocation == NSNotFound || colonLocation < questionMarkLocation);
+ }
+
+ for (NSString *key in [object keyEnumerator]) {
+ NSObject *value = [object objectForKey:key];
+ BOOL passByValue = isOGAction && [key isEqualToString:@"image"];
+ [self processGraphObjectPropertyKey:key value:value action:action passByValue:passByValue];
+ }
+}
+
+#pragma mark -
+
+- (void)completeWithResponse:(NSURLResponse *)response
+ data:(NSData *)data
+ orError:(NSError *)error
+{
+ NSAssert(self.state == kStateStarted,
+ @"Unexpected state %d in completeWithResponse",
+ self.state);
+ self.state = kStateCompleted;
+
+ int statusCode;
+ if (response) {
+ NSAssert([response isKindOfClass:[NSHTTPURLResponse class]],
+ @"Expected NSHTTPURLResponse, got %@",
+ response);
+ self.urlResponse = (NSHTTPURLResponse *)response;
+ statusCode = self.urlResponse.statusCode;
+
+ if (!error && [response.MIMEType hasPrefix:@"image"]) {
+ error = [self errorWithCode:FBErrorNonTextMimeTypeReturned
+ statusCode:0
+ parsedJSONResponse:nil
+ innerError:nil
+ message:@"Response is a non-text MIME type; endpoints that return images and other "
+ @"binary data should be fetched using NSURLRequest and NSURLConnection"];
+ }
+ } else {
+ // the cached case is always successful, from an http perspective
+ statusCode = 200;
+ }
+
+
+
+ NSArray *results = nil;
+ if (!error) {
+ results = [self parseJSONResponse:data
+ error:&error
+ statusCode:statusCode];
+ }
+
+ // the cached case has data but no response,
+ // in which case we skip connection-related errors
+ if (response || !data) {
+ error = [self checkConnectionError:error
+ statusCode:statusCode
+ parsedJSONResponse:results];
+ }
+
+ if (!error) {
+ if ([self.requests count] != [results count]) {
+ NSLog(@"Expected %d results, got %d", [self.requests count], [results count]);
+ error = [self errorWithCode:FBErrorProtocolMismatch
+ statusCode:statusCode
+ parsedJSONResponse:results
+ innerError:nil
+ message:nil];
+ }
+ }
+
+ if (!error) {
+
+ [_logger appendFormat:@"Response <#%d>\nDuration: %lu msec\nSize: %d kB\nResponse Body:\n%@\n\n",
+ [_logger loggerSerialNumber],
+ [FBUtility currentTimeInMilliseconds] - _requestStartTime,
+ [data length],
+ results];
+
+ } else {
+
+ [_logger appendFormat:@"Response <#%d> :\n%@\n\n",
+ [_logger loggerSerialNumber],
+ [error localizedDescription]];
+
+ }
+ [_logger emitToNSLog];
+
+ if (self.deprecatedRequest) {
+ [self completeDeprecatedWithData:data results:results orError:error];
+ } else {
+ [self completeWithResults:results orError:error];
+ }
+
+ self.connection = nil;
+ self.urlResponse = (NSHTTPURLResponse *)response;
+}
+
+//
+// If there is one request, the JSON is the response.
+// If there are multiple requests, the JSON has an array of dictionaries whose
+// body property is the response.
+// [{ "code":200,
+// "body":"JSON-response-as-a-string" },
+// { "code":200,
+// "body":"JSON-response-as-a-string" }]
+//
+// In both cases, this function returns an NSArray containing the results.
+// The NSArray looks just like the multiple request case except the body
+// value is converted from a string to parsed JSON.
+//
+- (NSArray *)parseJSONResponse:(NSData *)data
+ error:(NSError **)error
+ statusCode:(int)statusCode;
+{
+ // Graph API can return "true" or "false", which is not valid JSON.
+ // Translate that before asking JSON parser to look at it.
+ NSString *responseUTF8 = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ NSArray *results = nil;
+ id response = [self parseJSONOrOtherwise:responseUTF8 error:error];
+
+ if (*error) {
+ // no-op
+ } else if ([self.requests count] == 1) {
+ // response is the entry, so put it in a dictionary under "body" and add
+ // that to array of responses.
+ NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease];
+ [result setObject:[NSNumber numberWithInt:statusCode] forKey:@"code"];
+ [result setObject:response forKey:@"body"];
+
+ NSMutableArray *mutableResults = [[[NSMutableArray alloc] init] autorelease];
+ [mutableResults addObject:result];
+ results = mutableResults;
+ } else if ([response isKindOfClass:[NSArray class]]) {
+ // response is the array of responses, but the body element of each needs
+ // to be decoded from JSON.
+ NSMutableArray *mutableResults = [[[NSMutableArray alloc] init] autorelease];
+ for (id item in response) {
+ // Don't let errors parsing one response stop us from parsing another.
+ NSError *batchResultError = nil;
+ if (![item isKindOfClass:[NSDictionary class]]) {
+ [mutableResults addObject:item];
+ } else {
+ NSDictionary *itemDictionary = (NSDictionary *)item;
+ NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease];
+ for (NSString *key in [itemDictionary keyEnumerator]) {
+ id value = [itemDictionary objectForKey:key];
+ if ([key isEqualToString:@"body"]) {
+ id body = [self parseJSONOrOtherwise:value error:&batchResultError];
+ [result setObject:body forKey:key];
+ } else {
+ [result setObject:value forKey:key];
+ }
+ }
+ [mutableResults addObject:result];
+ }
+ if (batchResultError) {
+ // We'll report back the last error we saw.
+ *error = batchResultError;
+ }
+ }
+ results = mutableResults;
+ } else {
+ *error = [self errorWithCode:FBErrorProtocolMismatch
+ statusCode:statusCode
+ parsedJSONResponse:results
+ innerError:nil
+ message:nil];
+ }
+
+ [responseUTF8 release];
+ return results;
+}
+
+- (id)parseJSONOrOtherwise:(NSString *)utf8
+ error:(NSError **)error
+{
+ id parsed = nil;
+ if (!(*error)) {
+ FBSBJSON *parser = [[FBSBJSON alloc] init];
+ parsed = [parser objectWithString:utf8 error:error];
+ // if we fail parse we attemp a reparse of a modified input to support results in the form "foo=bar", "true", etc.
+ if (*error) {
+ // we round-trip our hand-wired response through the parser in order to remain
+ // consistent with the rest of the output of this function (note, if perf turns out
+ // to be a problem -- unlikely -- we can return the following dictionary outright)
+ NSDictionary *original = [NSDictionary dictionaryWithObjectsAndKeys:
+ utf8, FBNonJSONResponseProperty,
+ nil];
+ NSString *jsonrep = [parser stringWithObject:original];
+ NSError *reparseError = nil;
+ parsed = [parser objectWithString:jsonrep error:&reparseError];
+ if (!reparseError) {
+ *error = nil;
+ }
+ }
+ [parser release];
+ }
+ return parsed;
+}
+
+- (void)completeDeprecatedWithData:(NSData *)data
+ results:(NSArray *)results
+ orError:(NSError *)error
+{
+ id result = [results objectAtIndex:0];
+ if ([result isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *resultDictionary = (NSDictionary *)result;
+ result = [resultDictionary objectForKey:@"body"];
+ }
+
+ id delegate = [self.deprecatedRequest delegate];
+
+ if (!error) {
+ if ([delegate respondsToSelector:@selector(request:didReceiveResponse:)]) {
+ [delegate request:self.deprecatedRequest
+ didReceiveResponse:self.urlResponse];
+ }
+ if ([delegate respondsToSelector:@selector(request:didLoadRawResponse:)]) {
+ [delegate request:self.deprecatedRequest didLoadRawResponse:data];
+ }
+
+ error = [self errorFromResult:result];
+ }
+
+ if (!error) {
+ if ([delegate respondsToSelector:@selector(request:didLoad:)]) {
+ [delegate request:self.deprecatedRequest didLoad:result];
+ }
+ } else {
+ if ([self isInvalidSessionError:error resultIndex:0]) {
+ [self.deprecatedRequest setSessionDidExpire:YES];
+ [self.deprecatedRequest.session close];
+ }
+
+ [self.deprecatedRequest setError:error];
+ if ([delegate respondsToSelector:@selector(request:didFailWithError:)]) {
+ [delegate request:self.deprecatedRequest didFailWithError:error];
+ }
+ }
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ [self.deprecatedRequest setState:kFBRequestStateComplete];
+#pragma GCC diagnostic pop
+}
+
+- (void)completeWithResults:(NSArray *)results
+ orError:(NSError *)error
+{
+ int count = [self.requests count];
+ for (int i = 0; i < count; i++) {
+ FBRequestMetadata *metadata = [self.requests objectAtIndex:i];
+ id result = error ? nil : [results objectAtIndex:i];
+ NSError *itemError = error ? error : [self errorFromResult:result];
+
+ id body = nil;
+ if (!itemError && [result isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *resultDictionary = (NSDictionary *)result;
+ body = [FBGraphObject graphObjectWrappingDictionary:[resultDictionary objectForKey:@"body"]];
+ }
+
+ // if we lack permissions, use this as a cue to refresh the
+ // OS's understanding of current permissions
+ if ((metadata.request.session.loginType == FBSessionLoginTypeSystemAccount) &&
+ [self isInsufficientPermissionError:error
+ resultIndex:error == itemError ? i : 0]) {
+ [FBSession renewSystemAuthorization];
+ }
+
+ if ([self isInvalidSessionError:itemError
+ resultIndex:error == itemError ? i : 0]) {
+ [metadata.request.session closeAndClearTokenInformation:itemError];
+ if (metadata.request.session.loginType == FBSessionLoginTypeSystemAccount){
+ [FBSession renewSystemAuthorization];
+ }
+ } else if ([metadata.request.session shouldExtendAccessToken]) {
+ // If we have not had the opportunity to piggyback a token-extension request,
+ // but we need to, do so now as a separate request.
+ FBRequestConnection *connection = [[FBRequestConnection alloc] init];
+ [FBRequestConnection addRequestToExtendTokenForSession:metadata.request.session
+ connection:connection];
+ [connection start];
+ [connection release];
+ }
+
+ if (metadata.completionHandler) {
+ // task #1256476: in the current implementation, FBErrorParsedJSONResponseKey has two
+ // semantics; both of which are used by the implementation; the right fix is to break the meaning into
+ // two throughout, and surface both in the public API; the following fix is a lower risk and also
+ // less correct solution that improves the public API surface for this release
+ // Unpack FBErrorParsedJSONResponseKey array if present
+ id parsedResponse;
+ if ((parsedResponse = itemError.userInfo) && // do we have an error with userInfo
+ (parsedResponse = [parsedResponse objectForKey:FBErrorParsedJSONResponseKey]) && // response present?
+ ([parsedResponse isKindOfClass:[NSArray class]])) { // array?
+ id newValue = nil;
+ // if we successfully spelunk this far, then we don't want to return FBErrorParsedJSONResponseKey as is
+ // but if there is an empty array here, then we are better off nil-ing the key
+ if ([parsedResponse count]) {
+ newValue = [parsedResponse objectAtIndex:0];
+ }
+ itemError = [self errorWithCode:itemError.code
+ statusCode:[[itemError.userInfo objectForKey:FBErrorHTTPStatusCodeKey] intValue]
+ parsedJSONResponse:newValue
+ innerError:[itemError.userInfo objectForKey:FBErrorInnerErrorKey]
+ message:[itemError.userInfo objectForKey:NSLocalizedDescriptionKey]];
+ }
+
+ metadata.completionHandler(self, body, itemError);
+ }
+ }
+}
+
+- (NSError *)errorFromResult:(id)idResult
+{
+ if ([idResult isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dictionary = (NSDictionary *)idResult;
+
+ if ([dictionary objectForKey:@"error"] ||
+ [dictionary objectForKey:@"error_code"] ||
+ [dictionary objectForKey:@"error_msg"] ||
+ [dictionary objectForKey:@"error_reason"]) {
+
+ NSMutableDictionary *userInfo = [[[NSMutableDictionary alloc] init] autorelease];
+ [userInfo addEntriesFromDictionary:dictionary];
+ return [self errorWithCode:FBErrorRequestConnectionApi
+ statusCode:200
+ parsedJSONResponse:idResult
+ innerError:nil
+ message:nil];
+ }
+
+ NSNumber *code = [dictionary valueForKey:@"code"];
+ if (code) {
+ return [self checkConnectionError:nil
+ statusCode:[code intValue]
+ parsedJSONResponse:idResult];
+ }
+ }
+
+ return nil;
+}
+
+- (NSError *)errorWithCode:(FBErrorCode)code
+ statusCode:(int)statusCode
+ parsedJSONResponse:(id)response
+ innerError:(NSError*)innerError
+ message:(NSString*)message {
+ NSMutableDictionary *userInfo = [[[NSMutableDictionary alloc] init] autorelease];
+ [userInfo setObject:[NSNumber numberWithInt:statusCode] forKey:FBErrorHTTPStatusCodeKey];
+
+ if (response) {
+ [userInfo setObject:response forKey:FBErrorParsedJSONResponseKey];
+ }
+
+ if (innerError) {
+ [userInfo setObject:innerError forKey:FBErrorInnerErrorKey];
+ }
+
+ if (message) {
+ [userInfo setObject:message
+ forKey:NSLocalizedDescriptionKey];
+ }
+
+ NSError *error = [[[NSError alloc]
+ initWithDomain:FacebookSDKDomain
+ code:code
+ userInfo:userInfo]
+ autorelease];
+
+ return error;
+}
+
+- (NSError *)checkConnectionError:(NSError *)innerError
+ statusCode:(int)statusCode
+ parsedJSONResponse:response
+{
+ // We don't want to re-wrap our own errors.
+ if (innerError &&
+ [innerError.domain isEqualToString:FacebookSDKDomain]) {
+ return innerError;
+ }
+ NSError *result = nil;
+ if (innerError || ((statusCode < 200) || (statusCode >= 300))) {
+ NSLog(@"Error: HTTP status code: %d", statusCode);
+ result = [self errorWithCode:FBErrorHTTPError
+ statusCode:statusCode
+ parsedJSONResponse:response
+ innerError:innerError
+ message:nil];
+ }
+ return result;
+}
+
+- (BOOL)getCodeValueForError:(NSError *)error
+ resultIndex:(int)index
+ value:(int *)pvalue {
+
+ // does this error have a response? that is an array?
+ id response = [error.userInfo objectForKey:FBErrorParsedJSONResponseKey];
+ if (response && [response isKindOfClass:[NSArray class]]) {
+
+ // spelunking a JSON array & nested objects (eg. response[index].body.error.code)
+ id item, body, error, code;
+ if ((item = [response objectAtIndex:index]) && // response[index]
+ [item isKindOfClass:[NSDictionary class]] &&
+ (body = [item objectForKey:@"body"]) && // response[index].body
+ [body isKindOfClass:[NSDictionary class]] &&
+ (error = [body objectForKey:@"error"]) && // response[index].body.error
+ [error isKindOfClass:[NSDictionary class]] &&
+ (code = [error objectForKey:@"code"]) && // response[index].body.error.code
+ [code isKindOfClass:[NSNumber class]]) {
+ // is it a 190 packaged in the original response, then YES
+ if (pvalue) {
+ *pvalue = [code intValue];
+ }
+ return YES;
+ }
+ }
+ // else NO
+ return NO;
+}
+
+- (BOOL)isInsufficientPermissionError:(NSError *)error
+ resultIndex:(int)index {
+
+ int value;
+ if ([self getCodeValueForError:error
+ resultIndex:index
+ value:&value]) {
+ return value == kRESTAPIPermissionErrorCode;
+ }
+ return NO;
+}
+
+- (BOOL)isInvalidSessionError:(NSError *)error
+ resultIndex:(int)index {
+
+ int value;
+ if ([self getCodeValueForError:error
+ resultIndex:index
+ value:&value]) {
+ return value == kRESTAPIAccessTokenErrorCode || value == kAPISessionNoLongerActiveErrorCode;
+ }
+ return NO;
+}
+
+- (void)registerTokenToOmitFromLog:(NSString *)token
+{
+ if (![[FBSettings loggingBehavior] containsObject:FBLoggingBehaviorAccessTokens]) {
+ [FBLogger registerStringToReplace:token replaceWith:@"ACCESS_TOKEN_REMOVED"];
+ }
+}
+
++ (NSString *)userAgent
+{
+ static NSString *agent = nil;
+
+ if (!agent) {
+ agent = [[NSString stringWithFormat:@"%@.%@", kUserAgentBase, FB_IOS_SDK_VERSION_STRING] retain];
+ }
+
+ return agent;
+}
+
+- (void)addPiggybackRequests
+{
+ // Get the set of sessions used by our requests
+ NSMutableSet *sessions = [[NSMutableSet alloc] init];
+ for (FBRequestMetadata *requestMetadata in self.requests) {
+ // Have we seen this session yet? If not, assume we'll extend its token if it wants us to.
+ if (requestMetadata.request.session) {
+ [sessions addObject:requestMetadata.request.session];
+ }
+ }
+
+ for (FBSession *session in sessions) {
+ if (self.requests.count >= kMaximumBatchSize) {
+ break;
+ }
+ if ([session shouldExtendAccessToken]) {
+ [FBRequestConnection addRequestToExtendTokenForSession:session connection:self];
+ }
+ }
+
+ [sessions release];
+}
+
++ (void)addRequestToExtendTokenForSession:(FBSession*)session connection:(FBRequestConnection*)connection
+{
+ FBRequest *request = [[FBRequest alloc] initWithSession:session
+ restMethod:kExtendTokenRestMethod
+ parameters:nil
+ HTTPMethod:nil];
+ [connection addRequest:request
+ completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
+ // extract what we care about
+ id token = [result objectForKey:@"access_token"];
+ id expireTime = [result objectForKey:@"expires_at"];
+
+ // if we have a token and it is not a string (?) punt
+ if (token && ![token isKindOfClass:[NSString class]]) {
+ expireTime = nil;
+ }
+
+ // get a date if possible
+ NSDate *expirationDate = nil;
+ if (expireTime) {
+ NSTimeInterval timeInterval = [expireTime doubleValue];
+ if (timeInterval != 0) {
+ expirationDate = [NSDate dateWithTimeIntervalSince1970:timeInterval];
+ }
+ }
+
+ // if we ended up with at least a date (and maybe a token) refresh the session token
+ if (expirationDate) {
+ [session refreshAccessToken:token
+ expirationDate:expirationDate];
+ }
+ }];
+ [request release];
+}
+
+#pragma mark Debugging helpers
+
+- (NSString*)description {
+ NSMutableString *result = [NSMutableString stringWithFormat:@"<%@: %p, %d request(s): (\n",
+ NSStringFromClass([self class]),
+ self,
+ self.requests.count];
+ BOOL comma = NO;
+ for (FBRequestMetadata *metadata in self.requests) {
+ FBRequest *request = metadata.request;
+ if (comma) {
+ [result appendString:@",\n"];
+ }
+ [result appendString:[request description]];
+ comma = YES;
+ }
+ [result appendString:@"\n)>"];
+ return result;
+
+}
+
+#pragma mark -
+
+@end
diff --git a/src/ios/facebook/FBSDKVersion.h b/src/ios/facebook/FBSDKVersion.h
new file mode 100644
index 000000000..c46faaf77
--- /dev/null
+++ b/src/ios/facebook/FBSDKVersion.h
@@ -0,0 +1,2 @@
+#define FB_IOS_SDK_VERSION_STRING @"3.1.1"
+#define FB_IOS_SDK_MIGRATION_BUNDLE @"fbsdk:20121003"
diff --git a/src/ios/facebook/FBSession+Internal.h b/src/ios/facebook/FBSession+Internal.h
new file mode 100644
index 000000000..187fc1f5a
--- /dev/null
+++ b/src/ios/facebook/FBSession+Internal.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBSession.h"
+
+@interface FBSession (Internal)
+
+- (void)refreshAccessToken:(NSString*)token expirationDate:(NSDate*)expireDate;
+- (BOOL)shouldExtendAccessToken;
+- (void)closeAndClearTokenInformation:(NSError*) error;
+
++ (FBSession*)activeSessionIfOpen;
+
++ (void)deleteFacebookCookies;
++ (NSDate*)expirationDateFromExpirationTimeString:(NSString*)expirationTime;
++ (void)renewSystemAuthorization;
+
+@end
diff --git a/src/ios/facebook/FBSession+Protected.h b/src/ios/facebook/FBSession+Protected.h
new file mode 100644
index 000000000..eabf38c78
--- /dev/null
+++ b/src/ios/facebook/FBSession+Protected.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FBSession.h"
+
+// Methods here are meant to be used only by internal subclasses of FBSession
+// and not by any other classes, external or internal.
+@interface FBSession (Protected)
+
+- (BOOL)transitionToState:(FBSessionState)state
+ andUpdateToken:(NSString*)token
+ andExpirationDate:(NSDate*)date
+ shouldCache:(BOOL)shouldCache
+ loginType:(FBSessionLoginType)loginType;
+- (void)transitionAndCallHandlerWithState:(FBSessionState)status
+ error:(NSError*)error
+ token:(NSString*)token
+ expirationDate:(NSDate*)date
+ shouldCache:(BOOL)shouldCache
+ loginType:(FBSessionLoginType)loginType;
+- (void)authorizeWithPermissions:(NSArray*)permissions
+ behavior:(FBSessionLoginBehavior)behavior
+ defaultAudience:(FBSessionDefaultAudience)audience
+ isReauthorize:(BOOL)isReauthorize;
+
++ (NSError*)errorLoginFailedWithReason:(NSString*)errorReason
+ errorCode:(NSString*)errorCode
+ innerError:(NSError*)innerError;
+
+@end
diff --git a/src/ios/facebook/FBSession.h b/src/ios/facebook/FBSession.h
new file mode 100644
index 000000000..209fa2893
--- /dev/null
+++ b/src/ios/facebook/FBSession.h
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import
+
+// up-front decl's
+@class FBSession;
+@class FBSessionTokenCachingStrategy;
+
+#define FB_SESSIONSTATETERMINALBIT (1 << 8)
+
+#define FB_SESSIONSTATEOPENBIT (1 << 9)
+
+/*
+ * Constants used by NSNotificationCenter for active session notification
+ */
+
+/*! NSNotificationCenter name indicating that a new active session was set */
+extern NSString *const FBSessionDidSetActiveSessionNotification;
+
+/*! NSNotificationCenter name indicating that an active session was unset */
+extern NSString *const FBSessionDidUnsetActiveSessionNotification;
+
+/*! NSNotificationCenter name indicating that the active session is open */
+extern NSString *const FBSessionDidBecomeOpenActiveSessionNotification;
+
+/*! NSNotificationCenter name indicating that there is no longer an open active session */
+extern NSString *const FBSessionDidBecomeClosedActiveSessionNotification;
+
+/*!
+ @typedef FBSessionState enum
+
+ @abstract Passed to handler block each time a session state changes
+
+ @discussion
+ */
+typedef enum {
+ /*! One of two initial states indicating that no valid cached token was found */
+ FBSessionStateCreated = 0,
+ /*! One of two initial session states indicating that a cached token was loaded;
+ when a session is in this state, a call to open* will result in an open session,
+ without UX or app-switching*/
+ FBSessionStateCreatedTokenLoaded = 1,
+ /*! One of three pre-open session states indicating that an attempt to open the session
+ is underway*/
+ FBSessionStateCreatedOpening = 2,
+
+ /*! Open session state indicating user has logged in or a cached token is available */
+ FBSessionStateOpen = 1 | FB_SESSIONSTATEOPENBIT,
+ /*! Open session state indicating token has been extended */
+ FBSessionStateOpenTokenExtended = 2 | FB_SESSIONSTATEOPENBIT,
+
+ /*! Closed session state indicating that a login attempt failed */
+ FBSessionStateClosedLoginFailed = 1 | FB_SESSIONSTATETERMINALBIT, // NSError obj w/more info
+ /*! Closed session state indicating that the session was closed, but the users token
+ remains cached on the device for later use */
+ FBSessionStateClosed = 2 | FB_SESSIONSTATETERMINALBIT, // "
+} FBSessionState;
+
+/*! helper macro to test for states that imply an open session */
+#define FB_ISSESSIONOPENWITHSTATE(state) (0 != (state & FB_SESSIONSTATEOPENBIT))
+
+/*! helper macro to test for states that are terminal */
+#define FB_ISSESSIONSTATETERMINAL(state) (0 != (state & FB_SESSIONSTATETERMINALBIT))
+
+/*!
+ @typedef FBSessionLoginBehavior enum
+
+ @abstract
+ Passed to open to indicate whether Facebook Login should allow for fallback to be attempted.
+
+ @discussion
+ Facebook Login authorizes the application to act on behalf of the user, using the user's
+ Facebook account. Usually a Facebook Login will rely on an account maintained outside of
+ the application, by the native Facebook application, the browser, or perhaps the device
+ itself. This avoids the need for a user to enter their username and password directly, and
+ provides the most secure and lowest friction way for a user to authorize the application to
+ interact with Facebook. If a Facebook Login is not possible, a fallback Facebook Login may be
+ attempted, where the user is prompted to enter their credentials in a web-view hosted directly
+ by the application.
+
+ The `FBSessionLoginBehavior` enum specifies whether to allow fallback, disallow fallback, or
+ force fallback login behavior. Most applications will use the default, which attempts a normal
+ Facebook Login, and only falls back if needed. In rare cases, it may be preferable to disallow
+ fallback Facebook Login completely, or to force a fallback login.
+ */
+typedef enum {
+ /*! Attempt Facebook Login, ask user for credentials if necessary */
+ FBSessionLoginBehaviorWithFallbackToWebView = 0,
+ /*! Attempt Facebook Login, no direct request for credentials will be made */
+ FBSessionLoginBehaviorWithNoFallbackToWebView = 1,
+ /*! Only attempt WebView Login; ask user for credentials */
+ FBSessionLoginBehaviorForcingWebView = 2,
+ /*! Attempt Facebook Login, prefering system account and falling back to fast app switch if necessary */
+ FBSessionLoginBehaviorUseSystemAccountIfPresent = 3,
+} FBSessionLoginBehavior;
+
+/*!
+ @typedef FBSessionDefaultAudience enum
+
+ @abstract
+ Passed to open to indicate which default audience to use for sessions that post data to Facebook.
+
+ @discussion
+ Certain operations such as publishing a status or publishing a photo require an audience. When the user
+ grants an application permission to perform a publish operation, a default audience is selected as the
+ publication ceiling for the application. This enumerated value allows the application to select which
+ audience to ask the user to grant publish permission for.
+ */
+typedef enum {
+ /*! No audience needed; this value is useful for cases where data will only be read from Facebook */
+ FBSessionDefaultAudienceNone = 0,
+ /*! Indicates that only the user is able to see posts made by the application */
+ FBSessionDefaultAudienceOnlyMe = 10,
+ /*! Indicates that the user's friends are able to see posts made by the application */
+ FBSessionDefaultAudienceFriends = 20,
+ /*! Indicates that all Facebook users are able to see posts made by the application */
+ FBSessionDefaultAudienceEveryone = 30,
+} FBSessionDefaultAudience;
+
+/*!
+ @typedef FBSessionLoginType enum
+
+ @abstract
+ Used as the type of the loginType property in order to specify what underlying technology was used to
+ login the user.
+
+ @discussion
+ The FBSession object is an abstraction over five distinct mechanisms. This enum allows an application
+ to test for the mechanism used by a particular instance of FBSession. Usually the mechanism used for a
+ given login does not matter, however for certain capabilities, the type of login can impact the behavior
+ of other Facebook functionality.
+ */
+typedef enum {
+ /*! A login type has not yet been established */
+ FBSessionLoginTypeNone = 0,
+ /*! A system integrated account was used to log the user into the application */
+ FBSessionLoginTypeSystemAccount = 1,
+ /*! The Facebook native application was used to log the user into the application */
+ FBSessionLoginTypeFacebookApplication = 2,
+ /*! Safari was used to log the user into the application */
+ FBSessionLoginTypeFacebookViaSafari = 3,
+ /*! A web view was used to log the user into the application */
+ FBSessionLoginTypeWebView = 4,
+ /*! A test user was used to create an open session */
+ FBSessionLoginTypeTestUser = 5,
+} FBSessionLoginType;
+
+/*!
+ @typedef
+
+ @abstract Block type used to define blocks called by for state updates
+ @discussion
+ */
+typedef void (^FBSessionStateHandler)(FBSession *session,
+ FBSessionState status,
+ NSError *error);
+
+/*!
+ @typedef
+
+ @abstract Block type used to define blocks called by <[FBSession reauthorizeWithPermissions]>/.
+
+ @discussion
+ */
+typedef void (^FBSessionReauthorizeResultHandler)(FBSession *session,
+ NSError *error);
+
+/*!
+ @class FBSession
+
+ @abstract
+ The `FBSession` object is used to authenticate a user and manage the user's session. After
+ initializing a `FBSession` object the Facebook App ID and desired permissions are stored.
+ Opening the session will initiate the authentication flow after which a valid user session
+ should be available and subsequently cached. Closing the session can optionally clear the
+ cache.
+
+ If an request requires user authorization then an `FBSession` object should be used.
+
+
+ @discussion
+ Instances of the `FBSession` class provide notification of state changes in the following ways:
+
+ 1. Callers of certain `FBSession` methods may provide a block that will be called
+ back in the course of state transitions for the session (e.g. login or session closed).
+
+ 2. The object supports Key-Value Observing (KVO) for property changes.
+ */
+@interface FBSession : NSObject
+
+/*!
+ @methodgroup Creating a session
+ */
+
+/*!
+ @method
+
+ @abstract
+ Returns a newly initialized Facebook session with default values for the parameters
+ to .
+ */
+- (id)init;
+
+/*!
+ @method
+
+ @abstract
+ Returns a newly initialized Facebook session with the specified permissions and other
+ default values for parameters to .
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. The default is nil.
+
+ @discussion
+ It is required that any single permission request request (including initial log in) represent read-only permissions
+ or publish permissions only; not both. The permissions passed here should reflect this requirement.
+
+ */
+- (id)initWithPermissions:(NSArray*)permissions;
+
+/*!
+ @method
+
+ @abstract
+ Following are the descriptions of the arguments along with their
+ defaults when ommitted.
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. The default is nil.
+ @param appID The Facebook App ID for the session. If nil is passed in the default App ID will be obtained from a call to <[FBSession defaultAppID]>. The default is nil.
+ @param urlSchemeSuffix The URL Scheme Suffix to be used in scenarious where multiple iOS apps use one Facebook App ID. A value of nil indicates that this information should be pulled from the plist. The default is nil.
+ @param tokenCachingStrategy Specifies a key name to use for cached token information in NSUserDefaults, nil
+ indicates a default value of @"FBAccessTokenInformationKey".
+
+ @discussion
+ It is required that any single permission request request (including initial log in) represent read-only permissions
+ or publish permissions only; not both. The permissions passed here should reflect this requirement.
+ */
+- (id)initWithAppID:(NSString*)appID
+ permissions:(NSArray*)permissions
+ urlSchemeSuffix:(NSString*)urlSchemeSuffix
+ tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy;
+
+/*!
+ @method
+
+ @abstract
+ Following are the descriptions of the arguments along with their
+ defaults when ommitted.
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. The default is nil.
+ @param defaultAudience Most applications use FBSessionDefaultAudienceNone here, only specifying an audience when using reauthorize to request publish permissions.
+ @param appID The Facebook App ID for the session. If nil is passed in the default App ID will be obtained from a call to <[FBSession defaultAppID]>. The default is nil.
+ @param urlSchemeSuffix The URL Scheme Suffix to be used in scenarious where multiple iOS apps use one Facebook App ID. A value of nil indicates that this information should be pulled from the plist. The default is nil.
+ @param tokenCachingStrategy Specifies a key name to use for cached token information in NSUserDefaults, nil
+ indicates a default value of @"FBAccessTokenInformationKey".
+
+ @discussion
+ It is required that any single permission request request (including initial log in) represent read-only permissions
+ or publish permissions only; not both. The permissions passed here should reflect this requirement. If publish permissions
+ are used, then the audience must also be specified.
+ */
+- (id)initWithAppID:(NSString*)appID
+ permissions:(NSArray*)permissions
+ defaultAudience:(FBSessionDefaultAudience)defaultAudience
+ urlSchemeSuffix:(NSString*)urlSchemeSuffix
+ tokenCacheStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy;
+
+// instance readonly properties
+
+/*! @abstract Indicates whether the session is open and ready for use. */
+@property(readonly) BOOL isOpen;
+
+/*! @abstract Detailed session state */
+@property(readonly) FBSessionState state;
+
+/*! @abstract Identifies the Facebook app which the session object represents. */
+@property(readonly, copy) NSString *appID;
+
+/*! @abstract Identifies the URL Scheme Suffix used by the session. This is used when multiple iOS apps share a single Facebook app ID. */
+@property(readonly, copy) NSString *urlSchemeSuffix;
+
+/*! @abstract The access token for the session object. */
+@property(readonly, copy) NSString *accessToken;
+
+/*! @abstract The expiration date of the access token for the session object. */
+@property(readonly, copy) NSDate *expirationDate;
+
+/*! @abstract The permissions granted to the access token during the authentication flow. */
+@property(readonly, copy) NSArray *permissions;
+
+/*! @abstract Specifies the login type used to authenticate the user. */
+@property(readonly) FBSessionLoginType loginType;
+
+/*!
+ @methodgroup Instance methods
+ */
+
+/*!
+ @method
+
+ @abstract Opens a session for the Facebook.
+
+ @discussion
+ A session may not be used with and other classes in the SDK until it is open. If, prior
+ to calling open, the session is in the state, then no UX occurs, and
+ the session becomes available for use. If the session is in the state, prior
+ to calling open, then a call to open causes login UX to occur, either via the Facebook application
+ or via mobile Safari.
+
+ Open may be called at most once and must be called after the `FBSession` is initialized. Open must
+ be called before the session is closed. Calling an open method at an invalid time will result in
+ an exception. The open session methods may be passed a block that will be called back when the session
+ state changes. The block will be released when the session is closed.
+
+ @param handler A block to call with the state changes. The default is nil.
+*/
+- (void)openWithCompletionHandler:(FBSessionStateHandler)handler;
+
+/*!
+ @method
+
+ @abstract Logs a user on to Facebook.
+
+ @discussion
+ A session may not be used with and other classes in the SDK until it is open. If, prior
+ to calling open, the session is in the state, then no UX occurs, and
+ the session becomes available for use. If the session is in the state, prior
+ to calling open, then a call to open causes login UX to occur, either via the Facebook application
+ or via mobile Safari.
+
+ The method may be called at most once and must be called after the `FBSession` is initialized. It must
+ be called before the session is closed. Calling the method at an invalid time will result in
+ an exception. The open session methods may be passed a block that will be called back when the session
+ state changes. The block will be released when the session is closed.
+
+ @param behavior Controls whether to allow, force, or prohibit Facebook Login or Inline Facebook Login. The default
+ is to allow Facebook Login, with fallback to Inline Facebook Login.
+ @param handler A block to call with session state changes. The default is nil.
+ */
+- (void)openWithBehavior:(FBSessionLoginBehavior)behavior
+ completionHandler:(FBSessionStateHandler)handler;
+
+/*!
+ @abstract
+ Closes the local in-memory session object, but does not clear the persisted token cache.
+ */
+- (void)close;
+
+/*!
+ @abstract
+ Closes the in-memory session, and clears any persisted cache related to the session.
+*/
+- (void)closeAndClearTokenInformation;
+
+/*!
+ @abstract
+ Reauthorizes the session, with additional permissions.
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. The default is nil.
+ @param behavior Controls whether to allow, force, or prohibit Facebook Login. The default
+ is to allow Facebook Login and fall back to Inline Facebook Login if needed.
+ @param handler A block to call with session state changes. The default is nil.
+
+ @discussion Methods and properties that specify permissions without a read or publish
+ qualification are deprecated; use of a read-qualified or publish-qualified alternative is preferred
+ (e.g. reauthorizeWithReadPermissions or reauthorizeWithPublishPermissions)
+ */
+- (void)reauthorizeWithPermissions:(NSArray*)permissions
+ behavior:(FBSessionLoginBehavior)behavior
+ completionHandler:(FBSessionReauthorizeResultHandler)handler
+ __attribute__((deprecated));
+
+/*!
+ @abstract
+ Reauthorizes the session, with additional permissions.
+
+ @param readPermissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions.
+
+ @param handler A block to call with session state changes. The default is nil.
+ */
+- (void)reauthorizeWithReadPermissions:(NSArray*)readPermissions
+ completionHandler:(FBSessionReauthorizeResultHandler)handler;
+
+/*!
+ @abstract
+ Reauthorizes the session, with additional permissions.
+
+ @param writePermissions An array of strings representing the permissions to request during the
+ authentication flow.
+
+ @param defaultAudience Specifies the audience for posts.
+
+ @param handler A block to call with session state changes. The default is nil.
+ */
+- (void)reauthorizeWithPublishPermissions:(NSArray*)writePermissions
+ defaultAudience:(FBSessionDefaultAudience)defaultAudience
+ completionHandler:(FBSessionReauthorizeResultHandler)handler;
+
+/*!
+ @abstract
+ A helper method that is used to provide an implementation for
+ [UIApplicationDelegate application:openURL:sourceApplication:annotation:]. It should be invoked during
+ the Facebook Login flow and will update the session information based on the incoming URL.
+
+ @param url The URL as passed to [UIApplicationDelegate application:openURL:sourceApplication:annotation:].
+*/
+- (BOOL)handleOpenURL:(NSURL*)url;
+
+/*!
+ @abstract
+ A helper method that is used to provide an implementation for
+ [UIApplicationDelegate applicationDidBecomeActive:] to properly resolve session state for
+ the Facebook Login flow, specifically to support app-switch login.
+*/
+- (void)handleDidBecomeActive;
+
+/*!
+ @methodgroup Class methods
+ */
+
+/*!
+ @abstract
+ This is the simplest method for opening a session with Facebook. Using sessionOpen logs on a user,
+ and sets the static activeSession which becomes the default session object for any Facebook UI widgets
+ used by the application. This session becomes the active session, whether open succeeds or fails.
+
+ Note, if there is not a cached token available, this method will present UI to the user in order to
+ open the session via explicit login by the user.
+
+ @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if
+ no login UI will be required to accomplish the operation. For example, at application startup it may not
+ be disirable to transition to login UI for the user, and yet an open session is desired so long as a cached
+ token can be used to open the session. Passing NO to this argument, assures the method will not present UI
+ to the user in order to open the session.
+
+ @discussion
+ Returns YES if the session was opened synchronously without presenting UI to the user. This occurs
+ when there is a cached token available from a previous run of the application. If NO is returned, this indicates
+ that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is
+ possible that the user will login, and the session will become open asynchronously. The primary use for
+ this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session
+ is opened via cache.
+ */
++ (BOOL)openActiveSessionWithAllowLoginUI:(BOOL)allowLoginUI;
+
+/*!
+ @abstract
+ This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user,
+ and sets the static activeSession which becomes the default session object for any Facebook UI widgets
+ used by the application. This session becomes the active session, whether open succeeds or fails.
+
+ @param permissions An array of strings representing the permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. A nil value specifies
+ default permissions.
+
+ @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if
+ no login UI will be required to accomplish the operation. For example, at application startup it may not
+ be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached
+ token can be used to open the session. Passing NO to this argument, assures the method will not present UI
+ to the user in order to open the session.
+
+ @param handler Many applications will benefit from notification when a session becomes invalid
+ or undergoes other state transitions. If a block is provided, the FBSession
+ object will call the block each time the session changes state.
+
+ @discussion
+ Returns true if the session was opened synchronously without presenting UI to the user. This occurs
+ when there is a cached token available from a previous run of the application. If NO is returned, this indicates
+ that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is
+ possible that the user will login, and the session will become open asynchronously. The primary use for
+ this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session
+ is opened via cache.
+
+ It is required that initial permissions requests represent read-only permissions only. If publish
+ permissions are needed, you may use reauthorizeWithPermissions to specify additional permissions as
+ well as an audience. Use of this method will result in a legacy fast-app-switch Facebook Login due to
+ the requirement to seperate read and publish permissions for newer applications. Methods and properties
+ that specify permissions without a read or publish qualification are deprecated; use of a read-qualified
+ or publish-qualified alternative is preferred.
+ */
++ (BOOL)openActiveSessionWithPermissions:(NSArray*)permissions
+ allowLoginUI:(BOOL)allowLoginUI
+ completionHandler:(FBSessionStateHandler)handler
+ __attribute__((deprecated));
+
+/*!
+ @abstract
+ This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user,
+ and sets the static activeSession which becomes the default session object for any Facebook UI widgets
+ used by the application. This session becomes the active session, whether open succeeds or fails.
+
+ @param readPermissions An array of strings representing the read permissions to request during the
+ authentication flow. A value of nil indicates basic permissions. It is not allowed to pass publish
+ permissions to this method.
+
+ @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if
+ no login UI will be required to accomplish the operation. For example, at application startup it may not
+ be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached
+ token can be used to open the session. Passing NO to this argument, assures the method will not present UI
+ to the user in order to open the session.
+
+ @param handler Many applications will benefit from notification when a session becomes invalid
+ or undergoes other state transitions. If a block is provided, the FBSession
+ object will call the block each time the session changes state.
+
+ @discussion
+ Returns true if the session was opened synchronously without presenting UI to the user. This occurs
+ when there is a cached token available from a previous run of the application. If NO is returned, this indicates
+ that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is
+ possible that the user will login, and the session will become open asynchronously. The primary use for
+ this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session
+ is opened via cache.
+
+ */
++ (BOOL)openActiveSessionWithReadPermissions:(NSArray*)readPermissions
+ allowLoginUI:(BOOL)allowLoginUI
+ completionHandler:(FBSessionStateHandler)handler;
+
+/*!
+ @abstract
+ This is a simple method for opening a session with Facebook. Using sessionOpen logs on a user,
+ and sets the static activeSession which becomes the default session object for any Facebook UI widgets
+ used by the application. This session becomes the active session, whether open succeeds or fails.
+
+ @param publishPermissions An array of strings representing the publish permissions to request during the
+ authentication flow.
+
+ @param defaultAudience Anytime an app publishes on behalf of a user, the post must have an audience (e.g. me, my friends, etc.)
+ The default audience is used to notify the user of the cieling that the user agrees to grant to the app for the provided permissions.
+
+ @param allowLoginUI Sometimes it is useful to attempt to open a session, but only if
+ no login UI will be required to accomplish the operation. For example, at application startup it may not
+ be desirable to transition to login UI for the user, and yet an open session is desired so long as a cached
+ token can be used to open the session. Passing NO to this argument, assures the method will not present UI
+ to the user in order to open the session.
+
+ @param handler Many applications will benefit from notification when a session becomes invalid
+ or undergoes other state transitions. If a block is provided, the FBSession
+ object will call the block each time the session changes state.
+
+ @discussion
+ Returns true if the session was opened synchronously without presenting UI to the user. This occurs
+ when there is a cached token available from a previous run of the application. If NO is returned, this indicates
+ that the session was not immediately opened, via cache. However, if YES was passed as allowLoginUI, then it is
+ possible that the user will login, and the session will become open asynchronously. The primary use for
+ this return value is to switch-on facebook capabilities in your UX upon startup, in the case were the session
+ is opened via cache.
+
+ */
++ (BOOL)openActiveSessionWithPublishPermissions:(NSArray*)publishPermissions
+ defaultAudience:(FBSessionDefaultAudience)defaultAudience
+ allowLoginUI:(BOOL)allowLoginUI
+ completionHandler:(FBSessionStateHandler)handler;
+
+/*!
+ @abstract
+ An appication may get or set the current active session. Certain high-level components in the SDK
+ will use the activeSession to set default session (e.g. `FBLoginView`, `FBFriendPickerViewController`)
+
+ @discussion
+ If sessionOpen* is called, the resulting `FBSession` object also becomes the activeSession. If another
+ session was active at the time, it is closed automatically. If activeSession is called when no session
+ is active, a session object is instatiated and returned; in this case open must be called on the session
+ in order for it to be useable for communication with Facebook.
+ */
++ (FBSession*)activeSession;
+
+/*!
+ @abstract
+ An appication may get or set the current active session. Certain high-level components in the SDK
+ will use the activeSession to set default session (e.g. `FBLoginView`, `FBFriendPickerViewController`)
+
+ @param session The FBSession object to become the active session
+
+ @discussion
+ If an application prefers the flexibilility of directly instantiating a session object, an active
+ session can be set directly.
+ */
++ (FBSession*)setActiveSession:(FBSession*)session;
+
+/*!
+ @method
+
+ @abstract Set the default Facebook App ID to use for sessions. The app ID may be
+ overridden on a per session basis.
+
+ @param appID The default Facebook App ID to use for methods.
+ */
++ (void)setDefaultAppID:(NSString*)appID;
+
+/*!
+ @method
+
+ @abstract Get the default Facebook App ID to use for sessions. If not explicitly
+ set, the default will be read from the application's plist. The app ID may be
+ overridden on a per session basis.
+ */
++ (NSString*)defaultAppID;
+
+@end
diff --git a/src/ios/facebook/FBSession.m b/src/ios/facebook/FBSession.m
new file mode 100644
index 000000000..eb9b54f8c
--- /dev/null
+++ b/src/ios/facebook/FBSession.m
@@ -0,0 +1,1761 @@
+/*
+ * Copyright 2012 Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import