Synchroneity
Before getting into the issue that you're asking about, it's well worth discussing what @MartinR mentioned at greater length: The method -[ACAccount performRequestWithHandler:]
is asynchronous.
Calling the method from the thread that code is being executed on results in some activity starting up on a second background queue. Meanwhile, execution continues on the first thread (the thread that is executing performRequestWithHandler:
and executeTweetRequest
). Some indeterminate amount of time after executeTweetRequest
is called, the block passed as the only argument to -[ACAccount performRequestWithHandler:]
is called.
Consequently, you will not be able to synchronously return the array of tweets from executeTweetRequest
.
The most stylish approach would be to change your method to be block based:
+ (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block
{
NSURL *getTweetUrl = [NSURL URLWithString:@"http://api.twitter.com/1.1/statuses/home_timeline.json"];
SLRequest *tweetRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:getTweetUrl
parameters:nil];
tweetRequest.account = //...this will be addressed in a moment
[tweetRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if ([urlResponse statusCode] == 200) {
NSError *jsonParsingError = nil;
NSArray *fetchedTweets = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonParsingError];
block(fetchedTweets, jsonParsingError);
}
}];
}
When calling fetchTweetsWithCompletion:
from your second UITableViewController
subclass, you will pass in a block probably much like the following:
- (void)viewDidLoad
{
//...
[TwitterWrapper fetchTweetsWithCompletion:^(NSArray *tweets, NSError *error) {
if (tweets) {
self.tweets = tweets;
[self.tableView reloadData];
} else if (error) {
//present a UIAlertView, perhaps...
}
}];
//...
}
The result of this code is that once the fetch of the tweets on a background queue has completed, your table view controller's property tweets
will be set to the fetched result and its table view will reload its data.
Scope (Addressing The Question)
The issue that you're describing
since it is a + (NSArray *) it does not recognize my (ACAccount) or any object outside
is that you're unable to access the instance variables of any of the instances of your class (I'm going to call it TwitterWrapper
for convenience) from within your class method +[TwitterWrapper executeTweetFetch]
. The problem here is one of scope. (As a result, thanks to ARC, the problem is also one of memory management.)
Our goal is to stash an instance of ACAccount
somewhere from your first table view controller and access that instance from your second table view controller.
There are several ways of doing this:
Use A Global Variable
The worst approach is to a use a dreaded global variable:
In TwitterWrapper.h
, you would declare extern
the ACAccount
for twitter:
//TwitterWrapper.h
extern ACAccount *twitterAccount;
@interface TwitterWrapper : executeTweetFetch
+ (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block;
@end
In TwitterWrapper.m
, you would define twitterAccount
so that it has global scope (for example, prior to the @implementation
block:
ACAccount *twitterAccount;
@implementation TwitterWrapper
//...
@end
And in the definition of your class method executeTweetFetch
you would have
+ (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block
{
//...
tweetRequest.account = twitterAccount;
//...
}
NOTE: This approach is very unstylish, not to mention outright dangerous. Global variables should be avoided whenever possible. Avoiding one is certainly possible in this case.
The next two approaches both involve changing fetchTweetsWithCompletion:
into an instance method and adding an ACAccount
property to TwitterWrapper
. The class' @interface
will look like
@interface TwitterWrapper : NSObject
@property (nonatomic) ACAccount *account;
- (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block;
@end
The @implementation
will look like
@implementation TwitterWrapper
- (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block
{
//...
twitterRequest.account = self.account;
//...
}
@end
Use A Shared Instance
The second worst approach in this case is to use a shared instance of TwitterWrapper
. This involves adding a class method (called something clever like +sharedWrapper
) to the class:
@interface TwitterWrapper : NSObject
+ (TwitterWrapper *)sharedWrapper;
@property (nonatomic) ACAccount *account;
- (void)fetchTweetsWithCompletion:(void (^)(NSArray *, NSError *))block;
@end
and using Grand Central Dispatch's dispatch_once
to initialize a static
local variable:
@implementation TwitterWrapper
//...
+ (TwitterWrapper *)sharedWrapper
{
static TwitterWrapper *sharedWrapper = nil;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
sharedWrapper = [[self alloc] init];
});
return sharedWrapper;
}
//...
@end
At this point, you have an instance of TwitterWrapper
into which you can store the instance of ACAccount
from your first table view controller and make use of (via self.account
within the implementation of -fetchTweetsWithCompletion:
) from your second table view controller:
Somewhere within your first UITableViewController
subclass you would have
//...
ACAccount *theAccount = //this is initialized somehow within this class (the first UITableViewController subclass), probably by the selection of a row
[[TwitterWrapper sharedWrapper] setAccount:theAccount];
//...
Then, within your second UITableViewController
subclass (probably within -viewDidLoad
), you would have
//...
[[TwitterWrapper sharedWrapper] fetchTweetsWithCompletion:^(NSArray *tweets, NSError *error) {
if (tweets) {
self.tweets = tweets;
[self.tableView reloadData];
} else if (error) {
//present a UIAlertView, perhaps...
}
}];
//...
This solution, however, is very similar to using a global variable. (The difference is that the creation of the global variable will be thread safe.)
Pass An Instance Of TwitterWrapper
A much better solution is to pass an instance of TwitterWrapper
from your first table view controller to your second.
NOTE: For brevity, I will assume that the second table view controller becomes active immediately after the user selects an account row in the first table view controller.
To do this, you will need to add a property
@property (nonatomic) TwitterWrapper *twitterWrapper;
to your second UITableViewController
subclass.
Somewhere within your first table view controller you would have
//...
ACAccount *theAccount = //this is initialized somehow within this class (the first UITableViewController subclass), probably by the selection of a row
TwitterWrapper *theTwitterWrapper = [[TwitterWrapper alloc] init];
twitterWrapper.account = theAccount;
SecondTableViewController *tweetsTableViewController = //initialize the table view controller
tweetsTableViewController.twitterWrapper = theTwitterWrapper;
//...
Then, within your second UITableViewController
subclass (probably within -viewDidLoad
), you would have
//...
[self.twitterWrapper fetchTweetsWithCompletion:^(NSArray *tweets, NSError *error) {
if (tweets) {
self.tweets = tweets;
[self.tableView reloadData];
} else if (error) {
//present a UIAlertView, perhaps...
}
}];
//...
Pass An Instance Of ACAccount
Rather than storing an instance of TwitterWrapper
on your second UITableViewController
subclass instance, the best solution is to store an instance of ACAccount
and alter TwitterWrapper
's interface for fetching tweets once again.
In this case, we'll want the fetch method to again be a class method. As @MartinR suggested, add an account parameter to your fetch method:
+ (void)fetchTweetsWithAccount:(ACAccount *)theAccount completion:(void (^)(NSArray *, NSError *))block
{
NSURL *getTweetUrl = [NSURL URLWithString:@"http://api.twitter.com/1.1/statuses/home_timeline.json"];
SLRequest *tweetRequest = [SLRequest requestForServiceType:SLServiceTypeTwitter
requestMethod:SLRequestMethodGET
URL:getTweetUrl
parameters:nil];
tweetRequest.account = theAccount;
[tweetRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if ([urlResponse statusCode] == 200) {
NSError *jsonParsingError = nil;
NSArray *fetchedTweets = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonParsingError];
block(fetchedTweets, jsonParsingError);
}
}];
}
Next, add a property of type ACAccount
to your second UITableViewController
subclass:
@property (nonatomic) ACAccount *account;
Then, somewhere within your first table view controller you would have
//...
ACAccount *theAccount = ////this is initialized somehow within this class (the first UITableViewController subclass), probably by the selection of a row
SecondTableViewController *tweetsTableViewController = //initialize the table view controller
tweetsTableViewController.account = theAccount;
//...
Then, within your second UITableViewController
subclass (probably within -viewDidLoad
), you would have
//...
[TwitterWrapper fetchTweetsWithAccount:self.account completion:^(NSArray *tweets, NSError *error) {
if (tweets) {
self.tweets = tweets;
[self.tableView reloadData];
} else if (error) {
//present a UIAlertView, perhaps...
}
}];
//...