Hacking the default Facebook UIActivity

Introduction

UIActivity has been introduced in iOS 6.0 and one of its purposes was making social sharing easier.

In most cases, you wouldn’t need the TWTweetComposeViewController or SLComposeViewController anymore to send a tweet or post a status on Facebook (or integrating the Facebook SDK).

Instead, you would just create a UIActivityViewController, passing it some content to share (image, url and/or text) and you are presented with the standard system share picker, with every available option.

4 lines of code, no external SDK.

1
2
3
4
5
NSArray *activityItems = @[@"Awesome blog!",
                           [NSURL URLWithString:@"http://blog.jldagon.me"]];
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItems
                                                                                         applicationActivities:nil];
[self presentViewController:activityVC animated:YES completion:nil];

Now, if you choose to share this content on Facebook, the standard share sheet is presented. You don’t need the Facebook SDK with a Facebook app id, you don’t need to have FB login.

And voilà. The text and URL have been shared on your Facebook profile!

But—there’s always a but—…

What if you would like the publication to be attributed to your Facebook app?

All contents shared with a UIActivityViewController are attributed to iOS. You can see it highlighted in the above screenshot.
That means no metrics, no measure of engagement through Facebook insights…

Facebook itself lists the disadvantages of using this standard component:

  • Share sheet does not support the complete platform attachment model
  • Can only be used if people using your app log in to Facebook via iOS 6
  • Does not support tagging friends
  • Posts from the share sheet are attributed to iOS

Point #2 can be problematic too.

What if the users have the Facebook app installed, but they haven’t linked their FB account in the iOS settings?

Facebook does not even appear as an option to share the content. Bummer.

Solving the problem

To fix these issues, you could roll your own social sharing component, handling Twitter, iMessage, Mail, Facebook and whatever other destination you wish. And it’s a great idea, especially if you want to customize the UI and have a complete control over the entire sharing process.

Or, if you’re willing to resort to some hackery, you could modify the behavior of the default Facebook UIActivity.

Disclaimer: That’s probably a bad idea, and could lead to rejections from the review team. I wouldn’t risk it for a contract work for example.

Is it be possible to use the Share Dialog of the Facebook app when we press the Facebook button?

Well, yes!

All we need is to find a way to intercept the tap on the Facebook button and replace the standard behavior by our custom code.

Each item presented by the UIActivityViewController is tied to a UIActivity object, describing what types of items it can share and how it can share them:

(from Apple documentation)

1
- (NSString *)activityType

A string that identifies the provided service to your app.

1
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems;

Returns a Boolean indicating whether the service can act on the specified data items.

1
- (void)prepareWithActivityItems:(NSArray *)activityItems

Prepares your service to act on the specified data.

1
- (void)performActivity;

Performs the service when no custom view controller is provided.

1
- (void)activityDidFinish:(BOOL)completed

Notifies the system that your activity object has completed its work.

We need to:

  1. identify the Facebook activity’s activityType
  2. somehow override canPerformWithActivityItems: to make it return YES when there’s no Facebook account linked in the iOS settings, but the Facebook app is installed
  3. plug ourselves into -prepareWithActivityItems: and prepare the data we’re going to feed the Facebook app with
  4. launch the share dialog in -performActivity
  5. call -activityDidFinish: when the content has been shared

I initially tried to swizzle -prepareWithActivityItems: on UIActivity to see what was going on and get the activity type.

Except that it wasn’t being called at all. I realized that it was because everything happens in a subclass of UIActivity. A private subclass.

So in order to do all the swizzling, I had to find what subclass was used. Digging through the UIKit runtime headers, I finally hit UISocialActivity (header here).

  1. This time it worked, methods were being called. Facebook’s activityType is com.apple.UIKit.activity.PostToFacebook.

  2. Enabling Facebook activity with no FB account set up

1
2
3
4
5
6
7
8
- (BOOL)cca_canPerformWithActivityItems:(NSArray *)activityItems
{
    if (/* we're in the Facebook activity and the Facebook app is here */) {
        return YES;
    }
    // Other activities or no Facebook app
    return [self cca_canPerformWithActivityItems:activityItems];
}
  1. In the swizzled -prepareWithActivityItems:, we’re retrieving the URL (if any), image(s) and text passed as activity items. Then we store a FBLinkShareParams or FBPhotosParam depending on what data is shared.

  2. Displaying the app through Facebook’s FBDialogs with the *Params object we created in step 3.

  3. When the Facebook app returns to our app, it calls the share completion handler in which we’re calling activityDidFinish:.

And there we are with a correctly—attributed publication:

This solution requires the Facebook SDK, so you must have a well-configured Facebook app, indicate the FacebookAppID and FacebookDisplayName in your Info.plist as well as the fb{app id} url scheme.

The code is available on GitHub: CCAFacebookAppActivity

– I’m @Jilouc on Twitter if you have any comment.

Auto-Layout and UITableView cells

Important note

This post was written before the release of iOS 8. It’s now possible to avoid calculating the height, and let the system figure it out by itself. I encourage you to watch session 226 from WWDC 2014: What’s new in Table and Collection Views (transcript)

Introduction

Auto-Layout. I’ve been very reluctant to use it when it was introduced with iOS 6. I considered it was not worth the learning curve and sticked with classical frame/center positioning. But Apple is really pushing us iOS developers to use Auto-Layout. It has been clear during WWDC 2013.
I must admit that the improvements they made in Xcode 5 are great, so I decided to give it a try before the release of iOS 7.

One of the biggest issues I had is making it work with table views. For “Manual Layout”, there were techniques widely adopted and best practices we were accustomed to. How can they be adapted, using Auto-Layout?

The most popular answer regarding the subject on Stack Overflow gives useful hints but looks incomplete.

Note: the sample project for this post is available on GitHub.

Setting up the cell

The first step is to create the cell. For this post, we will use a nib for our custom cell class CCACustomCell (but everything can be done programmatically).

It will hold a single label of variable height. It’s achieved by:

  • setting its numberOfLines property to 0
  • adding 4 constraints, pinning the label to the 4 edges of the cell’s contentView.

This way, the content view height will fit the label height:

1
Content view height = Top constraint + label height + Bottom constraint

It would of course be possible to achieve a much more complex layout, as long as you set up the constraints properly. The new visual constraints editing system in Xcode 5 makes it really simpler.

Getting the cell height

Our view controller registers the custom cell we just created.

1
2
UINib *cellNib = [UINib nibWithNibName:@"CCACustomCell" bundle:nil];
[self.tableView registerNib:cellNib forCellReuseIdentifier:@"Cell"];

We will use an offscreen extra-cell to make all our height-related stuff. Add CCACustomCell *_stubCell as an ivar.

1
_stubCell = [cellNib instantiateWithOwner:nil options:nil][0];

Then we need to compute the cell height, and making it use the constraints we set up.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)configureCell:(CCACustomCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    cell.customLabel.text = _tableData[indexPath.row % _tableData.count];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self configureCell:_stubCell atIndexPath:indexPath];
    [_stubCell layoutSubviews];

    CGFloat height = [_stubCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    return height + 1;
}

What’s the magic here?

  1. Configuring the content of the cell
  2. Forcing a layout of the cell to apply constraints
  3. Getting the height of the contentView, computed using Auto-Layout. We can’t directly call systemLayoutSizeFittingSize:UILayoutFittingCompressedSize: on the cell because the constraints we’ve set up are relative to the content view. Finally, we use UILayoutFittingCompressedSize to get the smallest size fitting the content.
  4. Adding a bonus 1. I’ve seen a lot of posts on Stack Overflow telling we mysteriously need to add up “some pixels sometimes”. But that’s not mysterious at all. We’ve computed the content view height but we actually need to return… the cell height here. And it’s 1 pixel higher, because of the separator, which height is 1 pt (0.5 for Retina screens on iOS 7, to be exact).

Notice how the label is truncated in the 3rd cell on the left. That’s what can happen without adding the extra 1 pixel.

Performance

While the current implementation is perfectly valid for a low number of rows, it’s a real performance killer when you have dozens of rows.

For this simple cell, it took up to 30s to display the table view for 100,000 rows. And that was on the iOS Simulator, not some old crappy iPhone 3G.

This is because the table view calls tableView:heightForRowAtIndexPath: once per row, to get the total height. And for each row, we’re asking to layout the cell (with Auto-Layout, this means solving a linear equations system).

Fortunately, Apple added tableView:estimatedHeightForRowAtIndexPath: in iOS 7. This allows us to only return a vague estimate for the row height. And we don’t really need to be precise. In our case, something like that is enough:

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 40.f;
}

This way, the table view only calls tableView:heightForRowAtIndexPath: as much as it needs to fill the screen. For the other cells, tableView:estimatedHeightForRowAtIndexPath: is used. And it will compute the real height when needed.

Known issues

I have not been able to get it to work when the cells have an accessory view. The layout is done as if the content view width is equal to the cell width, but it’s not true anymore.

One possible work-around for that is to take the accessory view width into account when you set up the constraints.

While writing this post, I found SmartTables by Jonathan Wight (@schwa), who ended up with a very similar solution. I also encourage you to read Apple’s Auto-Layout Guide.

Reminder: the sample project is available on GitHub.

@Jilouc on Twitter.

Il y a un an, je démissionnais.

Le déclic

Il y a un an, jour pour jour, je postais ma lettre de démission. J’ai choisi de quitter mon CDI pépère dans un grand groupe pour tenter l’aventure freelance. L’idée me trottait dans la tête depuis assez longtemps, mais le déclencheur final aura été mon passage à San Francisco pour la WWDC 2012. Ironiquement, c’est mon employeur qui m’y avait envoyé (et je l’en remercie encore !). Difficile en effet de passer 2 semaines plongé dans le monde de la Silicon Valley sans être sensible à l’esprit qui y règne. J’ai rencontré des indépendants, des fondateurs de startup, tous passionnés. Même les employés d’Apple, Google avec qui j’ai pu parler m’ont donné cette envie de passer à autre chose…

Prendre les choses en main

Je n’avais pas de produit, pas d’idée révolutionnaire, juste l’envie de pouvoir choisir ce que je voulais faire. Mon expertise : le développement d’apps pour iOS. Depuis 2008, j’ai cumulé quasiment autant d’expérience qu’il est possible d’en avoir sur le sujet. Le produit que j’allais vendre, ce serait donc moi.
Et mes propres idées dans tout ça ? J’espèrais bien pouvoir régulièrement dégager du temps pour m’en occuper.

De la mise à jour de Tweetbot pour iOS 7

Tweetbot 3 est sorti aujourd’hui. Tapbots a entièrement revu son célèbre client Twitter pour embrasser à pleine bouche les nouveautés d’iOS 7. Paul Haddad et Mark Jardine se sont mis au travail dès l’annonce faite durant la WWDC en juin.

Je ne vais pas faire une review de Tweetbot 3, que je trouve particulièrement réussie. Ils sont parvenus à garder ce qui faisait la spécificité de l’app tout en s’adaptant à iOS 7 à merveille. Elle est immédiatement venue remplacer son prédécesseur sur mon iPhone, et il en sera de même pour la future app iPad à sa sortie, je n’en doute pas.

Je préfère revenir sur les réactions que cette sortie a provoquées.

Tapbots a choisi d’en faire une mise à jour payante, pour 2,99€ (le prix augmentera plus tard). De manière prévisible, cela a entraîné un flot de réactions dénonçant l’horrible choix des développeurs gloutons. Comme lors de la sortie de Tweetbot pour iPad et comme lors de la sortie de Tweetbot pour Mac.

Revenons il y a quelques jours. Tapbots annonçait sur son blog que l’app avait été soumise pour validation à Apple. Ils confirmaient au passage qu’ils avaient initialement visé une sortie conjointe à celle d’iOS 7. Seulement voilà, développer une telle app prend du temps, énormément de temps.

UIScrollCeption: embedding multiple UIWebViews in a UITableView

You don’t have that much choice to display HTML content with basic UIKit components. You’re basically limited to UIWebView and… nothing else.

It’s a mini Safari, so it does the job very well. But there’s WebKit under the hood so it’s quite heavy: it has a significant memory trace and takes a while to load.

At this point, you need to think about what the HTML looks like. There are some great open-source components out there to display rich-text:

  • DTCoreText can create attributed strings from HTML and takes care of the rendering. It supports rich text, images, videos, transforms and much more. Really great.
  • OHAttributedLabel or TTTAttributedLabel both support basic formatting stuff.

If you generate the HTML content yourself and have a complete control over it and if it matches the features of one of these components, go ahead and use it.
However, if you have arbitrary HTML, maybe including some pieces of Javascript, advanced styling: you’re stuck with UIWebView.