Processing Tweets Using Natural Language and Create ML on iOS

Leveraging native iOS libraries to perform tasks like tokenization, named entity recognition and sentiment analysis

It’s seems that, in the last decade, people increasingly have turned to social networks to express their opinions on everything from everyday life to politics and even businesses. This has been an important source — for businesses — to probe the market and get a clear picture of the needs and directions that influence it.

For me, the social media that seems the most active in the area of expressing opinions is probably Twitter.

Twitter is an online news and social networking service that allows users to send and read short messages of 280 characters called “Tweets”. Registered users can read and post Tweets, but those who are not registered can only read them.

So Twitter is a public platform with a wealth of public opinion from people around the world and from all age groups. In October 2019, Twitter had more than 330 million active users per month, with 40% of those active daily.

The most widely-used technique to assess opinions in a given text is sentiment analysis. It’s extremely useful in monitoring social media because it allows us to have an overview of public opinion behind certain topics. However, it’s also handy for business analysis and various other situations in which the text needs to be analyzed.

Sentiment analysis is in demand due to its effectiveness. Thousands of text documents can be processed in seconds, compared to the hours (or days) a team of human reviewers would need to complete the same analysis manually. That’s why so many companies are adopting text and sentiment analysis and incorporating it into their processes and decision pipelines.

The last presidential election in the USA was probably the most interesting source of data analysis, and Seth Stephens-Davidowitz’s book “Everybody Lies” is in my opinion the one that covered all the underlying aspects of opinion making. In this book, he explores how hard it is to capture what people really think. I highly recommend it—informative but also really funny.

Since almost everyone has a smartphone in their pocket and uses it more than any other electronic device, it seems obvious to create a mobile application that employs sentiment analysis. Empowering users to look into trends and analyze Tweets by using machine learning techniques seems like a good way to democratize public opinion.

In this article, I’ll create an iOS application that will consume the Twitter API and process Tweets using natural language processing techniques.


  1. Twitter API
  2. Flask API
  3. Natural Language API
  4. Custom sentiment analysis classifier
  5. Building the iOS Application
  6. Testing on real data
  7. Conclusion

All the material used in this project can be downloaded on my GitHub account:

This is a look at what the final result will look like:

Twitter API

Before getting started, you must have or create a twitter account. Once your account is ready to use, go directly to Twitter’s developer site. Log in and click on “create a new application” (then fill in the information requested on the next page).

Given all the controversies around applications accessing public data on various social media, Twitter tied up the process, so it will take a few days in order to have full access to the API.

After the developer account has been created and approved by Twitter, you will normally access this page, which gives you two essential pieces of information: your CONSUMER KEY and your CONSUMER SECRET, which we will use for the rest of this article.

Flask API

Now we need to consume the Twitter API and create our own API that will be accessed by the iOS Application.

I chose python-twitter, which is a Python package that provides a pure Python interface to Twitter’s API.

I chose a Flask library called Flask-RESTful made by Twilio that encourages best practices when it comes to APIs.

The Python code is pretty simple and straightforward:

from flask import Flask
from flask_restful import Resource, Api
import twitter

app = Flask(__name__)
api = Api(app)


twitter_api = twitter.Api(CONSUMER_KEY,

def get_tweets(search_query: str) -> list:
    query = 'q=%22' + search_query + '%22%20-RT%20lang%3Aen%20until%3A2020-01-21%20since%3A2019-01-29%20-filter%3Alinks%20' 
    results = twitter_api.GetSearch(raw_query=query,
    return results

class Tweets(Resource):
    def get(self, search_query):
        results = get_tweets(search_query)
        only_tweets = results['statuses']
        tweets = []
        i = 0
        for tweet in only_tweets:
            tweets.append({'text': tweet['text']})
            i = i + 1
        final_tweets = {'tweets': tweets}
        return final_tweets

api.add_resource(Tweets, '/<string:search_query>')

if __name__ == '__main__':, port=5000)

Here are the steps:

  1. Create an instance of Flask.
  2. Feed the Flask app instance to the Api instance from Flask-RESTful.
  3. Create four variables that will hold the ACCESS_TOKEN , ACCESS_TOKEN_SECRET , CONSUMER_KEY, and CONSUMER_SECRET.
  4. Create an instance of the Twitter API and feed it all the KEYS.
  5. Create a function called get_tweets() that has one parameter called search_query, which will make a query to the Twitter API and return a dictionary of tweets.
  6. Create a class Tweets that will be used as an entry point for our API.
  7. Add a GET method to the class that will call get_tweets and will only return a dictionary with a Tweet’s text.
  8. Add the class as a resource to the API and define the routing.
  9. That’s all 🤩

Run the API and use this URL to check:

The API call structure is pretty simple:

  • Server’s address:
  • Search query: in this example I used “boeing”
  • Structure: {Server’s address}/{Search query}

Natural Language API

Natural Language is Apple’s API for text processing, and has been available since iOS 12. The API is pretty powerful and fully native—you can perform a variety of NLP-related tasks:

  1. Tokenization: tokenization seeks to transform a text into a series of individual tokens. Each token represents a word, and identifying words seems like a relatively simple task. But it can be tricky. Consider managing English phrases such as: “I’m cold”— here, the tokenization model must separate “I” as being a first word, with “am” as the second.
  2. Named Entity Recognition (NER): In automatic language processing, the recognition of named entities seeks to detect entities such as people, companies, or places in a text. Example:Does Elizabeth Warren have a plan for my left AirPod” -> [“PersonalName” : “Elizabeth Warren”]
  3. Sentiment Analysis (new in iOS 13): This is a new feature in iOS 13 that gives you the power to access the general sentiment in a given word, sentence, paragraph, or document. The sentiment score is structured as a float number that can range from -1.0 to 1.0. Ascore of 1.0 is the most positive, a score of -1.0 is the most negative, and a score of 0.0 is neutral.

The Natural Language SDK has many more features that won’t be covered in this article, but they all follow a similar scheme.

Custom sentiment analysis classifier

The sentiment analysis model will be made with CreateML, which is Apple’s high-level API for making simple machine learning models ranging from image classification to text classification.

I used the IMDB dataset to train the model—nothing fancy here, but I wrote a full article on how you can make a simple yet powerful model.

Full article:

Building the iOS application

All the code is Dark Mode-ready 😉.

Create a new project

To begin, we need to create an iOS project with a single view app. Make sure to choose Storyboard in the “User interface” dropdown menu (Xcode 11 only):

Create View Controllers

We only need two ViewControllers:

  • ViewController():

This is where we’ll set our UICollectionView with all the Tweets retrieved from the Flask API

  • DetailsViewController():

This is where we will be able to make various processing to a given tweet.

Create a navigation

  • MainTabBarController():

This is our main navigation where we’ll create the full navigation structure for our application.

Navigation in iOS is pretty straightforward and easy to implement. Here, I’ve changed some things like the font for the navigation bar (and others) so it looks nice.

class MainTabBarController: UITabBarController {
    override func viewDidLoad() {
        tabBar.backgroundColor = UIColor(red: 239/255, green: 239/255, blue: 244/255, alpha: 0.1)
        tabBar.unselectedItemTintColor = UIColor.lightGray
        tabBar.isTranslucent = true
    func setupTabBar() {
        let vc = ViewController()
        let viewController = UINavigationController(rootViewController: vc)
        viewController.tabBarItem.image = UIImage(systemName: "text.bubble")
        viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 1, left: 0, bottom: -1, right: 0)
        viewController.tabBarItem.title = "Tweets"
        viewControllers = [viewController]
        UITabBar.appearance().tintColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
        let navigation = UINavigationBar.appearance()
        let navigationFont = UIFont(name: "Avenir", size: 20)
        let navigationLargeFont = UIFont(name: "Avenir-Heavy", size: 34)
        navigation.titleTextAttributes = [NSAttributedString.Key.foregroundColor:, NSAttributedString.Key.font: navigationFont!]
        if #available(iOS 11, *) {
            navigation.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor:, NSAttributedString.Key.font: navigationLargeFont!]

Now we have our project ready to go. I don’t like using storyboards myself, so the app in this tutorial is built programmatically, which means no buttons or switches to toggle — just pure code 🤗.

To follow this method, you’ll have to delete the main.storyboard and set your SceneDelegate.swift file (Xcode 11 only) like so:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(frame: windowScene.coordinateSpace.bounds)
        window?.windowScene = windowScene
        window?.rootViewController = MainTabBarController()

With Xcode 11, you’ll have to change the Info.plist file like so:

You need to delete the “Storyboard Name” in the file, and that’s about it.

Setup ViewController():

  • Set up UICollectionView :
let cellId = "cellId"

var newCollection: UICollectionView = {
    let collection = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
    collection.backgroundColor = UIColor.clear
    collection.translatesAutoresizingMaskIntoConstraints = false
    collection.isScrollEnabled = true
    collection.showsVerticalScrollIndicator = false
    collection.allowsMultipleSelection = true
    return collection

func setupCollection() {
    newCollection.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    newCollection.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
    newCollection.heightAnchor.constraint(equalToConstant: view.frame.height).isActive = true
    newCollection.widthAnchor.constraint(equalToConstant: view.frame.width).isActive = true
    newCollection.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
    newCollection.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
func setupCollectionView() {
    if #available(iOS 13.0, *) {
        newCollection.backgroundColor = UIColor.systemBackground
    } else {
        // Fallback on earlier versions
       newCollection.backgroundColor = .white
    newCollection.register(ViewControllerCell.self, forCellWithReuseIdentifier: cellId)
    newCollection.alwaysBounceVertical = true
    newCollection.delegate = self
    newCollection.dataSource = self

We also need a CollectionViewCell. I’ve created a custom one that you can find in this GitHub repository.

  • Populate the UICollectionView :

We need a model to host our Tweet, so for the sake of simplicity, I’ve created a struct called “Model”:

import Foundation
import UIKit

struct Model {
    var text: String
    var color: UIColor
    var query: String
    init(text: String, color: UIColor, query: String) {
        self.text = text
        self.color = color
        self.query = query

We also need to create an array of Models that will be used to populate the UICollectionView:

Now we have an array of models we can use to populate the collection. I prefer to use extensions, so I’ve created a separate file:

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.cells.count
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ViewControllerCell
        cell.query.text = "(self.cells[indexPath.row].query)"
        cell.text.text = self.cells[indexPath.row].text
        cell.contentView.layer.borderColor = self.cells[indexPath.row].color.cgColor
        cell.cornerColor = self.cells[indexPath.row].color
        return cell
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = round((view.frame.width - 40) / 2.0)*2
        let height = self.cells[indexPath.row].text.heightWithConstrainedWidth(width: width, font: UIFont.boldSystemFont(ofSize: 12)) + 90
        return CGSize(width: width, height: height)
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 25, left: 0, bottom: 25, right: 0)
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        switch mMode {
        case .view:
            newCollection.deselectItem(at: indexPath, animated: true)
            let controller = DetailsViewController()
            controller.tweet.text = self.cells[indexPath.row].text
            controller.query = self.cells[indexPath.row].query
            let navController = UINavigationController(rootViewController: controller)
            self.present(navController, animated: true, completion: nil)
        case .select:
            dictionarySelectedIndexPath[indexPath] = true
    func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
        if mMode == .select {
            dictionarySelectedIndexPath[indexPath] = false
extension String {
    func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
        return boundingBox.height

You will also find at the end of the file an extension for the String class so that you can calculate the height of the text and resize the collection view cells dynamically.

  • New search query

We need to set up the tabBar and add a rightBarButtonItem with a ‘plus’ sign button that will allow users to add a new search query and call the Flask API.

This button should also trigger a UIAlertController with a TextField:

@objc func addCell(_ sender: UIBarButtonItem) {
    let alertController = UIAlertController(title: "", message: "", preferredStyle: .alert)
    alertController.addTextField { textField in
        textField.placeholder = "Keyword"
    let confirmAction = UIAlertAction(title: "Search", style: .default) { [weak alertController] _ in
        guard let alertController = alertController, let textField = alertController.textFields?.first else { return }
        if let text = textField.text {
            self.getTweets(keyword: text) { (values) in
                if let tweets = values {
                    for tweet in tweets {
                        let cell = Model(text: tweet, color:, query: text)
    let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)
    present(alertController, animated: true, completion: nil)

Set up DetailsViewController():

When the user clicks on a CollectionView item, DetailsViewController() will be presented with a series of buttons and the original tweet:

  • Original search query
  • Original Tweet
  • Button to get the sentiment from the custom model made by using Create ML
  • Button to get the sentiment from the built-in Natural Language API (iOS 13 only)
  • Button to get “Word Tagging”, which is basically a tokenizer that will transform the Tweet into a series of individual tokens
  • Button to get “Entity Tagging”, which will try to identify entities like “personalNames”, “placeNames” and “organizationName”
  • Button to get “Lexical Tagging”, which will identify the lexical form of every single word (noun, adverb, verb, preposition, etc.)

To better structure the codebase, I’ve created a class that I called TextAnalysis with a couple of static functions:

  • WordTagging:
static func WordTagging(text: String, completion: @escaping ([String]?) -> ()) {
    var words = [String]()

    let tagger = NSLinguisticTagger(tagSchemes: [.tokenType], options: 0)
    tagger.string = text

    let range = NSRange(location: 0, length: text.utf16.count)
    let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace]
    tagger.enumerateTags(in: range, unit: .word, scheme: .tokenType, options: options) { _, tokenRange, _ in
        let word = (text as NSString).substring(with: tokenRange)
  • EntityTagging:
static func EntityTagging(text: String, completion: @escaping ([Dictionary<String, String>]?) -> ()) {
    var words = [Dictionary<String, String>]()
    let tagger = NSLinguisticTagger(tagSchemes: [.nameType], options: 0)
    tagger.string = text
    let range = NSRange(location:0, length: text.utf16.count)
    let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace, .joinNames]
    let tags: [NSLinguisticTag] = [.personalName, .placeName, .organizationName]
    tagger.enumerateTags(in: range, unit: .word, scheme: .nameType, options: options) { tag, tokenRange, stop in
        if let tag = tag, tags.contains(tag) {
            let name = (text as NSString).substring(with: tokenRange)
            var entity = Dictionary<String, String>()
            entity.updateValue(name, forKey: tag.rawValue)
  • LexicalTagging:
static func LexicalTagging(text: String, completion: @escaping ([Dictionary<String, String>]?) -> ()) {
    var words = [Dictionary<String, String>]()
    let tagger = NSLinguisticTagger(tagSchemes: [.lexicalClass], options: 0)
    tagger.string = text
    let range = NSRange(location: 0, length: text.utf16.count)
    let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace]
    tagger.enumerateTags(in: range, unit: .word, scheme: .lexicalClass, options: options) { tag, tokenRange, _ in
        if let tag = tag {
            let word = (text as NSString).substring(with: tokenRange)
            var entity = Dictionary<String, String>()
            entity.updateValue(word, forKey: tag.rawValue)
  • getSentimentFromBuildInAPI:
static func getSentimentFromBuildInAPI(text: String) -> String {
    let tagger = NLTagger(tagSchemes: [.sentimentScore])
    tagger.string = text
    let (sentiment, _) = tagger.tag(at: text.startIndex, unit: .paragraph, scheme: .sentimentScore)
    if Double(sentiment!.rawValue)! < 0 {
        return "Negative"
    } else if Double(sentiment!.rawValue)! > 0 {
        return "Positve"
    } else {
        return "Neutral"
  • getSentiment:
static func getSentiment(text: String) -> String {
    let model = SentimentClassifier()
    do {
        let prediction = try model.prediction(text: text)
        return prediction.label
    } catch {
        return error as! String

You’ll notice that a lot of the functions have closures—that’s because almost all the built-in functions provided by the Natural Language API have asynchronous processes, so in order to not stop the main thread, I chose to use closures to return the result once everything is completed.

Testing on real data

I will make a series of search queries to Twitter’s API,



  • [“Trump”, “boasted”, “Wed.”, “his”, “side”, “of”, “the”, “trial”, “has”, “information”, “that”, “was”, “n’t”, “shared”, “with”, “the”, “House”, “Dems”, “Is”, “n’t”, “that”, “obstruc”, “https”, “”, “5wHKpEFAFv”]


  • [[“PersonalName”: “Trump”], [“OrganizationName”: “House Dems”]]


  • [[“Noun”: “Trump”], [“Verb”: “boasted”], [“Noun”: “Wed.”], [“Determiner”: “his”], [“Noun”: “side”], [“Preposition”: “of”], [“Determiner”: “the”], [“Noun”: “trial”], [“Verb”: “has”], [“Noun”: “information”], [“Pronoun”: “that”], [“Verb”: “was”], [“Adverb”: “n’t”], [“Verb”: “shared”], [“Preposition”: “with”], [“Determiner”: “the”], [“Noun”: “House”], [“Noun”: “Dems”], [“Verb”: “Is”], [“Adverb”: “n’t”], [“Determiner”: “that”], [“Adverb”: “obstruc”], [“OtherWord”: “https”], [“Noun”: “”], [“Noun”: “5wHKpEFAFv”]]

Sentiment from built-in API:

  • Negative

Sentiment from custom classifier

  • Positive



  • [“hoes”, “will”, “preach”, “about”, “voting”, “the”, “old”, “white”, “men”, “out”, “but”, “then”, “campaign”, “for”, “bernie”, “sanders”]


  • []


  • [[“Noun”: “hoes”], [“Verb”: “will”], [“Verb”: “preach”], [“Preposition”: “about”], [“Noun”: “voting”], [“Determiner”: “the”], [“Adjective”: “old”], [“Adjective”: “white”], [“Noun”: “men”], [“Particle”: “out”], [“Conjunction”: “but”], [“Adverb”: “then”], [“Noun”: “campaign”], [“Preposition”: “for”], [“Noun”: “bernie”], [“Noun”: “sanders”]]

Sentiment from Build-in API:

  • Negative

Sentiment from custom classifier

  • Positive



  • [“Boeing”, “gone”, “any”, “other”, “new”, “resignations”]


  • []


  • [[“Verb”: “Boeing”], [“Verb”: “gone”], [“Determiner”: “any”], [“Adjective”: “other”], [“Adjective”: “new”], [“Noun”: “resignations”]]

Sentiment from built-in API:

  • Negative

Sentiment from custom classifier

  • Negative



  • [“just”, “got”, “hit”, “by”, “a”, “tesla”]


  • []


  • [[“Adverb”: “just”], [“Verb”: “got”], [“Verb”: “hit”], [“Preposition”: “by”], [“Determiner”: “a”], [“Noun”: “tesla”]]

Sentiment from Build-in API:

  • Positive

Sentiment from custom classifier

  • Positive



  • [“Google”, “Calendar”, “is”, “my”, “love”, “language”]


  • [[“OrganizationName”: “Google Calendar”]]


  • [[“Noun”: “Google”], [“Noun”: “Calendar”], [“Verb”: “is”], [“Determiner”: “my”], [“Noun”: “love”], [“Noun”: “language”]]

Sentiment from Build-in API:

  • Positive

Sentiment from custom classifier

  • Positive



  • [“I”, “’m”, “looking”, “into”, “going”, “back”, “to”, “school”, “but”, “literally”, “I”, “ca”, “n’t”, “even”, “afford”, “to”, “buy”, “Microsoft”, “word”]


  • [[“OrganizationName”: “Microsoft”]]


  • [[“Pronoun”: “I”], [“Verb”: “’m”], [“Verb”: “looking”], [“Preposition”: “into”], [“Verb”: “going”], [“Adverb”: “back”], [“Preposition”: “to”], [“Noun”: “school”], [“Conjunction”: “but”], [“Adverb”: “literally”], [“Pronoun”: “I”], [“Verb”: “ca”], [“Adverb”: “n’t”], [“Adverb”: “even”], [“Verb”: “afford”], [“Particle”: “to”], [“Verb”: “buy”], [“Noun”: “Microsoft”], [“Noun”: “word”]]

Sentiment from Build-in API:

  • Negative

Sentiment from custom classifier

  • Negative



  • [“Why”, “tf”, “is”, “a”, “peloton”, “over”, “$”, “2000”]


  • []


  • [[“Pronoun”: “Why”], [“Noun”: “tf”], [“Verb”: “is”], [“Determiner”: “a”], [“Noun”: “peloton”], [“Preposition”: “over”], [“OtherWord”: “$”], [“Number”: “2000”]]

Sentiment from Build-in API:

  • Negative

Sentiment from custom classifier

  • Negative


The idea of this project is to emphasize that with very few lines of code, we can get something that’s decent and perfectly usable using only native Swift code.

There’s something that I never mentioned in the article, which is Big Data. To make the application more interesting in terms of real-time data processing, we’d need to process millions of Tweets produced in a day (Around 500 million are actually produced in a given day).

To do that effectively, we’ll need to use all the techniques provided by big data providers like Hadoop or Spark, but that seems to be a topic for another article.

If you liked this tutorial, please share it with your friends. If you have any questions, don’t hesitate to send me an email at [email protected].

Avatar photo


Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *