Tips

Displaying Tweets in iOS Apps

By Evan Sobkowicz
Thursday, 5 December 2019

Tweets can quickly bring your app to life with original content, including photos, videos, and gifs. You can quickly display breaking news, memes, or conversations happening on Twitter.

The current recommended way to integrate Tweets into your app is by using our Javascript-based Embedded Tweets. Since Twitter Kit was deprecated, integrating Tweets into mobile apps can be more complicated, but at the same time does not require any 3rd-party libraries.

Many apps are already using our Javascript embeds, and we’re seeing rapid growth in this area. We’ve also heard from many developers on our community forum that they’d like more guidance on best practices around rendering Tweets in mobile apps.

Integrating WebViews into your app can be a complicated task, especially since Embedded Tweets have a dynamic height due to the variable content of Tweets. The dynamic height also causes some non ideal behavior with scrolling performance as the embeds load onto the page. This post dives into some tips on how to integrate Twitter’s Javascript embeds into your native iOS app using Swift and WKWebView and ensure they feel like a native part of the UI to your users.

This Tweet is unavailable
This Tweet is unavailable.

Loading an Embedded Tweet

First, we need to load an Embedded Tweet into a web view. Let’s set up the WKWebView, with a full width frame, and an HTML skeleton template with an empty div to insert the Tweet into later. The INDEX value will be important later, and used to identify the WebView if you are creating more than one.

This Tweet is unavailable
This Tweet is unavailable.
let HTML_TEMPLATE = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no'></head><body><div id='wrapper'></div></body></html>"
let webView = WKWebView()
webView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: CGFloat(DEFAULT_HEIGHT))
webView.tag = INDEX
webView.loadHTMLString(HTML_TEMPLATE, baseURL: “https://your-app-here.com”)

This will load an empty HTML wrapper into a WKWebview, with the full width of the view, and a default height. We’ll get into the dynamic height resizing in a minute.

You’ll need our widgets.js script to render Embedded Tweets within the web view, just like you would if you were integrating Embedded Tweets into your website. You can load the script once, and inject it into each webview to reduce network requests and speed up load time:

This Tweet is unavailable
This Tweet is unavailable.
// Load widgets.js once
class WidgetsJsManager {
    static let shared = WidgetsJsManager()
    
    var content: String?
    
    func load() {
       do {
           content = try String(contentsOf: URL(string: "https://platform.twitter.com/widgets.js")!)
       } catch {
           print("Could not load widgets.js script")
       }
    }
    
    func getScriptContent() -> String? {
        return content
    }
}

let WidgetsJs = WidgetsJsManager.shared

func viewDidLoad() {
    WidgetsJs.load()
}

// Inject it when a webview loads
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    if let widgetsJsScript = WidgetsJs.getScriptContent() {
        webView.evaluateJavaScript(widgetsJsScript)
        webView.evaluateJavaScript("twttr.widgets.load();")
    }
}

Next, you need to load the Tweet Embed into the HTML wrapper. You’ll need to know the ID of the Tweet you want to embed. You could get Tweet IDs from your backend, or by making a request to the Twitter API.

This Tweet is unavailable
This Tweet is unavailable.
let loadScript = """
  twttr.widgets.createTweet(
TWEET_ID_STRING, 
	document.getElementById('wrapper'),
{ align: 'center' }
  );
"""
webView.evaluateJavaScript(loadScript)

Dynamic Height Resizing

Because of the nature of Tweets, they can come in a variety of different heights. Images, quoted Tweets, cards, and the length of the Tweet text can all affect the height of the Tweet.

The height of views in iOS, UITableViewCell in this case, needs to be set explicitly, and we won’t know how tall the Tweet will be until it renders.

WKWebView supports the WKScriptMessageHandler protocol, which allows you to call back into your app code from the Javascript within the web view. We’ll use this to tell the app how tall the Tweet is after it’s done loading.

Update the script where you load the Tweet to use a new “heightCallback” and pass the height of the Tweet element:

This Tweet is unavailable
This Tweet is unavailable.
let loadScript = """
  twttr.widgets.createTweet(
TWEET_ID_STRING, 
	document.getElementById('wrapper'),
{ align: 'center' }
  ).then(el => {
     window.webkit.messageHandlers.heightCallback.postMessage(
        el.offsetHeight.toString()
     )
  });
"""
webView.evaluateJavaScript(loadScript)

Next, extend your controller to listen to Javascript callbacks:

This Tweet is unavailable
This Tweet is unavailable.
extension ViewController: WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
        case “heightCallback”:
            updateHeight(row: message.webView!.tag, height: message.body as! String)
        default:
            print("Unhandled callback")
        }
    }
    
}

You’ll also need to register the callback on the WKWebView:

This Tweet is unavailable
This Tweet is unavailable.
webView.configuration.userContentController.add(self, name: “heightCallback”)

You can implement the updateHeight function yourself to store the height, keyed by the WKWebView’s tag field (and index as we implemented before). 

Then you can adjust the height of the view, a UITableViewCell in this example, to be the height of the Tweet.

This Tweet is unavailable
This Tweet is unavailable.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if let tweet = Tweets.getByIdx(indexPath.row) {
            return tweet.height
        }
        return DefaultCellHeight
    }

Great! We’ve now set up our views to dynamically resize depending on the height of the Tweet.

This Tweet is unavailable
This Tweet is unavailable.

Scroll Performance & Preloading WebViews

While building a UITableView with many WebViews that are dynamically resizing, scrolling performance can be a little slow and jumpy.

One solution to this problem is to preload the WebViews, so that each Tweet’s height is communicated to the UITableView earlier, and hopefully before the user starts scrolling. You can preload the webviews as soon as widgets.js has loaded.

This Tweet is unavailable
This Tweet is unavailable.
func preloadWebviews() {
        Tweets.all().forEach { tweet in
            tweet.setWebView(createWebView(idx: tweet.idx))
        }
    }

Preloading each WebView will improve the performance once the user starts scrolling down the view, since each Tweet will have already rendered and communicated its height to the controller. 

Note: If you are trying to preload a large number of Tweets, there might be some performance implications, and you might want to look into paginating the WebView preloading.

Additionally, in your updateHeight function, you can include this line to reload the view without animation. This reduces tableview jumpiness as each UITableViewCell is resizing.

This Tweet is unavailable
This Tweet is unavailable.
tableView.reloadRowWithoutAnimation(IndexPath(row: idx, section: 0))

These tips should help keep your app feeling slick and performant.

This Tweet is unavailable
This Tweet is unavailable.

Navigation

In order to make the background of the Embedded Tweet clickable within an iOS WKWebView, you’ll need to set the controller to be a WKUIDelegate and add the following code:

This Tweet is unavailable
This Tweet is unavailable.
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        if let url = navigationAction.request.url, navigationAction.targetFrame == nil{
            openInSafarViewController(url)
        }
        return nil
    }

Opening Twitter.com links in SafariViewController provides the best browsing experience for your users, and also keeps them within your app.

This Tweet is unavailable
This Tweet is unavailable.

 

There are many ways you can integrate the Javascript-based Embedded Tweets into your app, and this post is aimed at providing a little guidance on navigating some of the challenges you might face.

We used several libraries and services beyond the Twitter API to make this tutorial, but you may have different needs and requirements and should evaluate whether those tools are right for you.

We hope these tips make it easier to integrate Embedded Tweets into your app. You can see the complete controller code used to write a simple app with these features on GitHub.

We’d love to hear about how you're using Embedded Tweets in native apps, additional challenges you face, and any questions you might have in the Twitter for Websites developer forum.

This Tweet is unavailable
This Tweet is unavailable.
@evansobkowicz

Evan Sobkowicz

‎@evansobkowicz‎

Software Engineer, Twitter

Only on Twitter