0

Example project showing issue: http://d.pr/f/g8x5

I'm building an app that interacts with the Twitter API on iOS (using the native OS access, not through the web API). I send the items to Core Data which then should show up in my UITableViewController that uses NSFetchedResultsController.

Here's the code in my cellForRowAtIndexPath: method:

Tweet *tweet = [self.fetchedResultsController objectAtIndexPath:indexPath];

cell.nameLabel.text = tweet.name;
cell.screenNameLabel.text = [NSString stringWithFormat:@"@%@", tweet.screenname];

NSString *tweetText = tweet.text;
tweetText = [tweetText stringByReplacingOccurrencesOfString:@"&" withString:@"&"];

NSMutableAttributedString *tweetAttributedText = [[NSMutableAttributedString alloc] initWithString:tweetText];

// Color @usernames in tweet text for label
NSError *error;
NSRegularExpression *screenNameRegEx = [NSRegularExpression regularExpressionWithPattern:@"\\B@[a-zA-Z0-9_]+" options:0 error:&error];
NSArray *screenNameMatches = [screenNameRegEx matchesInString:tweetText options:0 range:NSMakeRange(0, tweetText.length)];

for (NSTextCheckingResult *match in screenNameMatches) {
    [tweetAttributedText addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:31/255.0 green:121/255.0 blue:189/255.0 alpha:1.0] range:match.range];
}

// Color #hashtags
NSRegularExpression *hashtagRegEx = [NSRegularExpression regularExpressionWithPattern:@"\\B#[a-zA-Z0-9_]+" options:0 error:&error];
NSArray *hashtagMatches = [hashtagRegEx matchesInString:tweetText options:0 range:NSMakeRange(0, tweetText.length)];

for (NSTextCheckingResult *match in hashtagMatches) {
    [tweetAttributedText addAttribute:NSForegroundColorAttributeName value:[UIColor lightGrayColor] range:match.range];
}

// Color links
NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:&error];
NSArray *linkMatches = [linkDetector matchesInString:tweetText options:0 range:NSMakeRange(0, tweetText.length)];

for (NSTextCheckingResult *match in linkMatches) {
    // Make sure it's not a telephone number link
    if (![match.URL.scheme isEqualToString:@"tel"]) {
        [tweetAttributedText addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithRed:21/255.0 green:116/255.0 blue:255/255.0 alpha:1.0] range:match.range];
    }
}

cell.tweetTextLabel.attributedText = tweetAttributedText;

return cell;

And then in my viewDidAppear I call the Twitter API and pull down new articles:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    self.accountStore = [[ACAccountStore alloc] init];

    // If user has logged into Twitter system-wide
    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
        ACAccountType *twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

        [self.accountStore requestAccessToAccountsWithType:twitterAccountType options:nil completion:^(BOOL granted, NSError *error) {
            if (granted) {
                NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:twitterAccountType];
                NSURL *requestURL = [NSURL URLWithString:@"https://api.twitter.com"
                                     @"/1.1/statuses/home_timeline.json"];
                NSDictionary *requestParameters = @{@"include_rts": @"0",
                                                    @"count": @"20"};

                SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:requestURL parameters:requestParameters];
                request.account = twitterAccounts[0];

                [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                    if (responseData) {
                        if (urlResponse.statusCode >= 200 && urlResponse.statusCode < 300) {
                            NSError *jsonError;
                            NSArray *homeTimelineData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:&jsonError];

                            for (NSDictionary *tweetDictionary in homeTimelineData) {
                                NSManagedObjectContext *context = self.managedObjectContext;

                                Tweet *tweet = [NSEntityDescription insertNewObjectForEntityForName:@"Tweet" inManagedObjectContext:context];
                                tweet.text = [tweetDictionary objectForKey:@"text"];
                                tweet.name = [[tweetDictionary objectForKey:@"user"] objectForKey:@"name"];
                                tweet.screenname = [[tweetDictionary objectForKey:@"user"] objectForKey:@"screen_name"];

                                // Save the date of when the tweet was posted
                                NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
                                dateFormatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
                                tweet.date = [dateFormatter dateFromString:[tweetDictionary objectForKey:@"created_at"]];

                                NSError *error;
                                if (![context save:&error]) {
                                    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
                                }
                            }
                        }
                    }
                }];
            }
            else {

            }
        }];
    }
    // If not logged in system-wide, alert user to log in via Settings
    else {
        UIAlertView *notLoggedInAlert = [[UIAlertView alloc] initWithTitle:@"Not Logged in to Twitter" message:@"You must be logged into Twitter. Go to the Settings app and log in from there." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];

        [notLoggedInAlert show];
    }
}

But none of the items actually show up until I tap on each individual cell, then only that one will show up. It feels to me like it's an issue where the main thread isn't updating, but I have no idea what's going on.

What exactly am I doing wrong?

Doug Smith
  • 29,668
  • 57
  • 204
  • 388
  • What are the frames of the labels? Is tapping the cell causing the cell to layout its contents? What debugging have you done? – Wain Mar 12 '14 at 20:36
  • Have you implemented the `NSFetchedResultsControllerDelegate` properly with the `UITableView`, https://developer.apple.com/library/ios/DOCUMENTATION/CoreData/Reference/NSFetchedResultsController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40008227-CH1-SW26 ? – Rich Mar 12 '14 at 20:37
  • I believe so, I copied the implementation from the master-detail template with Core Data Apple offers in Xcode. – Doug Smith Mar 12 '14 at 20:46
  • @Wain I don't believe so, no. It seems the main thread hasn't been made aware of UI changes until I manually interact with the UI. – Doug Smith Mar 12 '14 at 20:47
  • Are you using the main thread context on a background thread? – Wain Mar 12 '14 at 21:24

2 Answers2

1

In looking at it again, the SLRequest callback is being made on a background thread which prevents CoreData and UIKit from functioning properly. Wrap the entire block in:

dispatch_async(dispatch_get_main_queue(), ^{
    if (urlResponse.statusCode >= 200 && urlResponse.statusCode < 300) {
    ...
    }
});
David Berry
  • 40,941
  • 12
  • 84
  • 95
  • But then I block the main thread. How do I combine this with being able to not block the main thread but update the main thread when it's finished? – Doug Smith Mar 12 '14 at 21:42
  • You can create a separate ModelObjectManager for usage on the background thread and deal with merging the results, if the data parsing really is that significant. You'll need to check the Apple documents for the details on using CoreData on a background thread. To clarify, you're not doing the network operation (the long part) on the main thread, you're just parsing out the response. Another option might be to parse out the response in the background thread and send some kind of intermediate results (NSArray of NSDictionary) to the foreground. – David Berry Mar 12 '14 at 21:58
0

You need to implement the NSFetchedArrayController delegate methods and set yourself as the delegate of the fetch controller. That way you'll be notified of changes to the fetched contents as they're made.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • @Doug Smith Here's the [documentation](https://developer.apple.com/library/ios/DOCUMENTATION/CoreData/Reference/NSFetchedResultsController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40008227-CH1-SW26) which is helpful guide on how to set it up. – Rich Mar 12 '14 at 20:39
  • I do indeed set myself as the delegate (see attached project). I assume you mean `NSFetchedResultsController`, not `NSFetchedArrayController` though. – Doug Smith Mar 12 '14 at 20:45
  • Yes, I do indeed mean NSFetchedResultsController, do you implement the delegate methods? Specifically controllerWillChangeContent: controllerDidChangeContent: and controller:didChangeSection:atIndex:forChangeType:? You can brute force it by only providing the first two, but you'll get better visual results by providing all three. See here for the Apple docs https://developer.apple.com/library/ios/DOCUMENTATION/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intf/NSFetchedResultsControllerDelegate – David Berry Mar 12 '14 at 20:48
  • I implement all three. – Doug Smith Mar 12 '14 at 20:49