-1

I am creating a NSObject to load the TimeLine twitter Code:

+ (NSArray *)executeTweetFetch

{

    __block NSArray *fetchedTweets = [NSArray array];

    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 = ACAccount HERE!!!;

    [tweetRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {

        if ([urlResponse statusCode] == 200) {
            // Parse the responseData, which we asked to be in JSON format for this request, into an NSDictionary using NSJSONSerialization.

            NSError *jsonParsingError = nil;
            fetchedTweets = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonParsingError];
            //At this point, fetchedTweet seems working fine, it gets the array send back.
        }
    }];

    return fetchedTweets;

}

...is Ok...but I have to add a ACAccount (another view). and since it is a + (NSArray *) it does not recognize my (ACAccount) or any object outside

Piyush Dubey
  • 2,416
  • 1
  • 24
  • 39
Lcstest Test
  • 113
  • 1
  • 2
  • 9
  • What does "I have to add a ACAccount (another view)" mean? – Martin R Jan 20 '13 at 15:14
  • edited....the user select a account in other view(TableView) – Lcstest Test Jan 20 '13 at 15:17
  • 2
    Why don't you add another parameter: `+ (NSArray *)executeTweetFetchForAccount:(ACAccount *)account`? - But your code has another big problem: `performRequestWithHandler` is an *asynchronous* function, which means that the block is not executed immediately, but later, when the request is done. Therefore `fetchedTweets` will always be an empty array when the function returns. – Martin R Jan 20 '13 at 15:20
  • works like this: UITableviewController1(select account) -> NSObject(this,add account in "ACAccount HERE!!!") -> UITableviewController2(call NSObject and get fetchedTweets) – Lcstest Test Jan 20 '13 at 15:24

1 Answers1

3

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...
        }
    }];
    //...
Community
  • 1
  • 1
Nate Chandler
  • 4,533
  • 1
  • 23
  • 32
  • Wow..first of all want to thank you for the reply and attention.little doubt: the "Tweets" is equal to "fetchedTweets" .. and gave an error n (NSError *, NSArray *) block – Lcstest Test Jan 20 '13 at 20:09
  • Yeah, use one of the approaches. The last one is the best choice in this case, from what you've said about what you're doing. – Nate Chandler Jan 20 '13 at 20:26
  • If I understand what you're asking (little doubt: the "Tweets" is equal to "fetchedTweets"), yes. The variable `fetchedTweets` in the handler block passed to `performRequestWithHandler:` refers to the same `NSArray` as the variable `tweets` in the completion block passed to `fetchTweetsWithCompletion:` (and `fetchTweetsWithAccount:completion:`). – Nate Chandler Jan 20 '13 at 20:33
  • I tested the method (using an account without value, only initialized). and the array is null – Lcstest Test Jan 20 '13 at 20:35
  • I mentioned four different approaches. Which did you test? – Nate Chandler Jan 20 '13 at 20:37
  • You used a global variable? – Nate Chandler Jan 20 '13 at 20:38
  • I'm not sure what you mean by "using an account without value, only initialized". – Nate Chandler Jan 20 '13 at 20:40
  • I used, but it gave an error: Undefined symbols for architecture i386: "_twitterAccount", referenced from: +[Fetcher fetchTweetsWithCompletion:] in Fetcher.o ld: symbol(s) not found for architecture i386 clang: error: linker command failed with exit code 1 (use -v to see invocation) – Lcstest Test Jan 20 '13 at 20:40
  • I fixed the code that was causing the error. The variable `twitterAccount` needs to be defined, not just declared `extern`. Add the line `ACAccount *twitterAccount;` to `TwitterWrapper.m` before the `@implementation` block. – Nate Chandler Jan 20 '13 at 20:44
  • Did you set `twitterAccount` from within your first table view controller? You need to set it to the account that is selected before displaying the second table view controller. – Nate Chandler Jan 20 '13 at 20:48
  • Sorry but which method (UITableView) have to match the account (accounts [indexPath]) the variable of NSObject – Lcstest Test Jan 20 '13 at 20:50
  • 1view not recognize the TwitterAccount of NSObject – Lcstest Test Jan 20 '13 at 20:53
  • That's hard for me to know without seeing more code. You need to set the global variable to be the account that is selected in the first table view before showing the second table view controller. Otherwise, when `fetchTweetsWithCompletion:` is called, it will set `tweetRequest.account` to `nil` rather than to the account that is selected in the first table view. – Nate Chandler Jan 20 '13 at 20:53
  • is possible to have another kind of contact with you (to show the code in detail) – Lcstest Test Jan 20 '13 at 20:55
  • Sure. Do you use IRC? freenode.net #iphonedev would be a good place to continue this conversation. – Nate Chandler Jan 20 '13 at 20:56
  • Did you `#import` the appropriate file (`TwitterWrapper.h`) into your first table view controller's implementation file? – Nate Chandler Jan 20 '13 at 20:59
  • Sure, who should I add on iMessage? – Nate Chandler Jan 20 '13 at 21:00
  • I add you? is that I have little problems with the account (not recognized) – Lcstest Test Jan 20 '13 at 21:02
  • How about we use gmail chat instead? – Nate Chandler Jan 20 '13 at 21:05
  • nate.chandler.stackoverflow@gmail.com – Nate Chandler Jan 20 '13 at 21:06
  • send me an email, and we'll get this figured out. – Nate Chandler Jan 20 '13 at 21:08