iOS Activity Feed Components

In this tutorial, we’re going to build an app using Stream’s iOS Activity Feed Components with Swift. At the end of this tutorial you’ll have:

  • A fully functional timeline feed with likes and comments
  • A status update form with support for emoji’s, images, links, hashtags, mentions, and other rich content
  • Realtime updates

More information and resources on how you can use Stream APIs are available at the end of the tutorial.

Setup

To get started, create a new Swift project in Xcode with a “Single View App” using the project name GetStreamActivityFeedDemo. Stream’s iOS Activity Feed Components are published on all major package repositories; so we’ll use CocoaPods as our dependency manager for this tutorial.

To continue, navigate to your project path:

cd ~/path/to/my/projects/GetStreamActivityFeedDemo/

Ensure CocoaPods is installed

Note: If you do not currently have CocoaPods installed, you can do so by running the following command:

sudo gem install cocoapods

Moving On

Now that you are in your project directory and have ensured that CocoaPods is installed, let’s go ahead and initialize CocoaPods and install the Stream Activity Feed Components within our project.

Initialize CocoaPods and create a Podfile:

pod init

Open the generated Podfile and modify the contents of the file with the following:

platform :ios, '11.0'
inhibit_all_warnings!

target 'GetStreamActivityFeedDemo' do
  use_frameworks!
  pod 'GetStreamActivityFeed'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = '4.2'
    end
  end
end

Now that we’ve modified our Podfile, let’s go ahead and install the project dependencies via the terminal with one simple command:

pod install

The above command will generate the GetStreamActivityFeedDemo.xcworkspace file automatically. With our workspace now containing our Pods project with dependencies, as well as our original project, let’s go ahead and move over to Xcode to complete the process.

Debugging

If you encounter any issues while installing the package, please make sure that you are using the latest version of CocoaPods and that your repository is up to date. You can use the following command to update:

sudo gem update cocoapods

Note: Occasionally, it helps to clean the CocoaPods cache using the following command:

rm -rf ~/Library/Caches/CocoaPods/

Once you’ve cleared the CocoaPods cache, it’s important to run the command pod install from within your directory to regenerate the cache.

Add Stream to your application

To setup Stream we need to add a couple things in your AppDelegate.swift. Here is what your AppDelegate should look like:

import UIKit
import GetStream
import GetStreamActivityFeed

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        Client.config = .init(apiKey: "du8he7epvp94",
                              appId: "45206",
                              token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWFkNTllNjUtZGZlNy00NWE2LTgyOTAtOTMwN2IzZTJkNzY5In0.U249pFFGBbqJZHce3g39PDFJfXz2vxjmIGqjzGGwSvM")

        Client.shared.getCurrentUser(typeOf: GetStreamActivityFeed.User.self) { result in
            if result.error == nil, let viewController = self.window?.rootViewController as? ViewController {
                 viewController.reloadData()
            }
        }
        return true
    }
}

Your application is now configured with the API credentials for the demo application.
Client.shared is a singleton we define in app delegate and re-use throughout the entire application. The shared client manages current user's session (token) and your Stream application API credentials (apiKey and appId).

Note for the impatient: we still need to add a tiny bit of code before we can launch our application. Bear with us for a minute :-)

Because this is a tutorial, we do not have a real signup flow and we will use a pre-generated user session token, on a real-world application, your authentication backend would generate such token at log-in / signup and hand it over to the mobile app.

This is how you would generate the token server side (no need to do this for this tutorial):

// npm install getstream --save
let stream = require('getstream');

let client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET');
let userToken = client.createUserSessionToken(userId);
# pip install stream-python

import stream

client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
user_token = client.create_user_token(user_id)
# gem install "stream-ruby"

# or add this to your Gemfile and then run bundler
# gem "stream-ruby"

require 'stream'

client = Stream::Client.new('YOUR_API_KEY', 'API_KEY_SECRET', :location => 'us-east')
user_token = client.create_user_session_token(userId)
# composer require get-stream/stream

$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');
$userToken = client->createUserSessionToken($userId);

Timeline Feed

Adding a timeline feed is very simple, the library comes with a built-in FlatFeedViewController class which loads the current user's timeline using the APIs and renders its content.

Because we started this project as a “Single View App” we already have a ViewController in our project.

The example newsfeed is loaded with some demo data, this way you can see how different kind of activities are rendered out of the box.

Update ViewController

First, we need to update our ViewController

  • Open ViewController.swift and import GetStreamActivityFeed and GeStream
  • Change ViewController's parent class from the UIViewController to FlatFeedViewController<Activity>
import UIKit
import GetStream
import GetStreamActivityFeed

class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {

    override func viewDidLoad() {
        if let feedId = FeedId(feedSlug: "timeline") {
            let timelineFlatFeed = Client.shared.flatFeed(feedId)
            presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed)
        }
        super.viewDidLoad()
    }
}

Our application is now showing the content of our user's timeline. To make this more interesting we have added some random example data.

The timeline feed supports out of the box:

  1. Custom Activity type
  2. Attachments with images and Open Graph data
  3. Open web links
  4. Open an image gallery
  5. Emojis 🙌
  6. Pagination
  7. Refresh by pull from the top

Note: real applications have more than just one view controller and often need more complex customizations. Under the hood the activity feed library allows you fine tune how feeds are used in your application. At the end of the tutorial you can find the link to our example application which shows how to use this library in a real world application.

Likes

The Activity Feed Components provide functionality to add/remove reactions to the activities such as likes and comments.

Adding likes to activities is very simple and supported out-of-the-box by the library. The only thing we need to do is change how we initialize the feed presenter class to show likes.

import UIKit
import GetStream
import GetStreamActivityFeed

class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {

    override func viewDidLoad() {
        if let feedId = FeedId(feedSlug: "timeline") {
            let timelineFlatFeed = Client.shared.flatFeed(feedId)
            presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: .likes)
        }
        super.viewDidLoad()
    }
}

Behind the scenes this adds an additional table view cell o display the Like button and the likes count.

Additional setup for like buttons possible with overriding the next method:

override func updateActions(in cell: PostActionsTableViewCell,
          activityPresenter: ActivityPresenter<Activity>) {
// cell.updateLike(...) { ... }
}

Comments

Comments are a very common functionality used on newsfeeds. The library comes with built-in support for comments, let’s see how we can add comments along with each activity.

For this we are going to add a detail screen for each activity. The detail screen will show the full activity information as well as the list of comments and a comment box to post new ones.

First we are going to add this method to our View Controller class:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let detailViewController = DetailViewController<GetStreamActivityFeed.Activity>()
  detailViewController.activityPresenter = activityPresenter(in: indexPath.section)
  detailViewController.sections = [.activity, .comments]
  present(UINavigationController(rootViewController: detailViewController), animated: true)
}

The code that we just added does two things:

  • It handles the tap on activities by overriding tableView’s didSelectRowAt method
  • Initializes and present DetailViewController with current activity’s data

As a final touch, we are going to add the comment count next to the like button. For this we only need to add .comments to the list of reactionTypes

This is how the ViewController should look after these two changes:

import UIKit
import GetStream
import GetStreamActivityFeed

class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {

   override func viewDidLoad() {
      if let feedId = FeedId(feedSlug: "timeline") {
          let timelineFlatFeed = Client.shared.flatFeed(feedId)
          presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: [.likes, .comments])
      }
      super.viewDidLoad()
   }

   override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      let detailViewController = DetailViewController<GetStreamActivityFeed.Activity>()
      detailViewController.activityPresenter = activityPresenter(in: indexPath.section)
      detailViewController.sections = [.activity, .comments]
      present(UINavigationController(rootViewController: detailViewController), animated: true)
   }
}

Bonus points: our library also supports nested reactions such as like on comments or threaded comments. You can see this in action when you open the detail screen ;)

If you are using the iOS simulator to run the examples, make sure to use the Software Keyboard (⌘K)

Status Updates

Now that we have a working timeline with likes and comments, let’s add a status update screen to post content ourselves.

We will use TextToolBar for the input of the text. Add an instance variable textToolBar in our ViewController:

let textToolBar = TextToolBar.make()

Let’s setup textToolBar in a separate method setupTextToolBar and call at the end of viewDidLoad (Make sure to add that after super.viewDidLoad())

func setupTextToolBar() {
  textToolBar.addToSuperview(view, placeholderText: "Share something...")
  // Enable image picker
  textToolBar.enableImagePicking(with: self)
  // Enable URL unfurling
  textToolBar.linksDetectorEnabled = true
  textToolBar.sendButton.addTarget(self,
                                   action: #selector(save(_:)),
                                   for: .touchUpInside)
}

Now we need to add a save() method to create a new activity:

@objc func save(_ sender: UIButton) {
  // Hide the keyboard.
  view.endEditing(true)

  if textToolBar.isValidContent, let presenter = presenter {
      textToolBar.addActivity(to: presenter.flatFeed) { result in
          print(result)
      }
  }
}

Add this at the bottom of viewDidLoad() to show the status update form:

setupTextToolBar()

Our timeline view comes with refresh control included; if you scroll down the timeline you will get the new activity loaded.

import UIKit
import GetStream
import GetStreamActivityFeed

class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {

    let textToolBar = TextToolBar.make()

    override func viewDidLoad() {
        if let feedId = FeedId(feedSlug: "timeline") {
            let timelineFlatFeed = Client.shared.flatFeed(feedId)
            presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed,
                                                                          reactionTypes: [.likes, .comments])
        }
        super.viewDidLoad()
        setupTextToolBar()
    }

    func setupTextToolBar() {
        textToolBar.addToSuperview(view, placeholderText: "Share something...")
        // Enable URL unfurling.
        textToolBar.linksDetectorEnabled = true
        // Enable image picker.
        textToolBar.enableImagePicking(with: self)
        textToolBar.sendButton.addTarget(self, action: #selector(save(_:)), for: .touchUpInside)
    }

    @objc func save(_ sender: UIButton) {
        // Hide the keyboard.
        view.endEditing(true)

        if textToolBar.isValidContent, let presenter = presenter {
            textToolBar.addActivity(to: presenter.flatFeed) { result in
                print(result) // It will print the added activity or error.
            }
        }
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if indexPath.row == 0 {
            let detailViewController = DetailViewController<GetStreamActivityFeed.Activity>()
            detailViewController.activityPresenter = activityPresenter(in: indexPath.section)
            detailViewController.sections = [.activity, .comments]
            present(UINavigationController(rootViewController: detailViewController), animated: true)
        } else {
            super.tableView(tableView, didSelectRowAt: indexPath)
        }
    }
}

Our timeline now has support for sharing status updates, the status update box comes with built-in support for:

  1. URL Previews
  2. Image Uploads
  3. Emoji and user mentions

Realtime Updates

As a final touch, we are connecting the timeline to Stream's real time APIs so that we can show a message each time a new activity is added to the feed.

To get FlatFeedViewController to listen for updates, we only need to call subscribeForUpdates(), that will create a Websocket connection to Stream APIs and subscribe for changes on the current feed.

override func viewDidLoad() {
  if let feedId = FeedId(feedSlug: "timeline") {
      let timelineFlatFeed = Client.shared.flatFeed(feedId)
      presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: [.likes, .comments])
  }

  super.viewDidLoad()
  setupTextToolBar()
  subscribeForUpdates()
}

Our feed now will show a notification every time a new activity is added. You can see this in action if you add a new activity.

Server-Side Integration

So far we looked at how you can read and post activities using Swift. In most cases you will also need to perform server-side interactions such as creating follow relationships, adding activities or change user-data. All the functionality we looked at in this tutorial is exposed via Stream's REST API and can be used server-side.

This is how we can add an activity from server-side:

// npm install getstream --save
// let stream = require('getstream');

let client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET');

let feed = client.feed('timeline', 'user-one');
feed.addActivity({
    'actor': client.user('user-one').ref(),
    'verb': 'post',
    'object': 'I love this picture',
    'attachments': {
        'og': {
            'title': 'Crozzon di Brenta photo by Lorenzo Spoleti',
            'description': 'Download this photo in Italy by Lorenzo Spoleti',
            'url': 'https://unsplash.com/photos/yxKHOTkAins',
            'images': [
                {
                    'image': 'https://goo.gl/7dePYs'
                }
            ]
        }
    }
})
# pip install stream-python

import stream

client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET')

feed = client.feed('timeline', 'user-one')

feed.add_activity({
  "actor": client.users.create_reference('user-one'),
  "verb": "post",
  "object": "I love this picture",
  "attachments": {
      "og": {
          "title": "Crozzon di Brenta photo by Lorenzo Spoleti",
          "description": "Download this photo in Italy by Lorenzo Spoleti",
          "url": "https://unsplash.com/photos/yxKHOTkAins",
          "images": [
            {
              "image": "https://goo.gl/7dePYs"
            }
          ]
      }
  }
})
# gem install "stream-ruby"

require 'stream'

client = Stream::Client.new('YOUR_API_KEY', 'API_KEY_SECRET')

feed = client.feed('user', 'user-one')

feed.add_activity({
  actor: client.collections.create_user_reference('user-one'),
  verb: 'post',
  object: 'I love this picture',
  attachments: {
    og: {
      title: 'Crozzon di Brenta photo by Lorenzo Spoleti',
      description: 'Download this photo in Italy by Lorenzo Spoleti',
      url: 'https://unsplash.com/photos/yxKHOTkAins',
      images: [
        {
          image: 'https://goo.gl/7dePYs'
        }
      ]
    }
  }
})
// composer require get-stream/stream

$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');

$feed = $client->feed('user', 'user-one');
$feed->addActivity([
	'actor' => $client->collections()->createUserReference('user-one'),
	'verb' => 'post',
	'object' => 'I love this picture',
	'attachments' => [
		'og' => [
			'title' => 'Crozzon di Brenta photo by Lorenzo Spoleti',
			'description' => 'Download this photo in Italy by Lorenzo Spoleti',
			'url' => 'https://unsplash.com/photos/yxKHOTkAins',
			'images' => (
				[
					'image' => 'https://goo.gl/7dePYs'
				]
			)
		]
	]
]);
// See installation details at https://github.com/GetStream/stream-java

Client client = Client.builder("YOUR_API_KEY", "API_KEY_SECRET").build();

FlatFeed feed = client.flatFeed("timeline", "user-one");
feed.addActivity(Activity.builder()
	.actor(Enrichment.createUserReference("user-one"))
	.verb("post")
	.object("I love this picture")
       	.extraField("attachments", ImmutableMap.of("og", new ImmutableMap.Builder<String, Object>()
		.put("title", "Crozzon di Brenta photo by Lorenzo Spoleti")
		.put("description", "Download this photo in Italy by Lorenzo Spoleti")
		.put("url", "https://unsplash.com/photos/yxKHOTkAins")
		.put("images", Lists.newArrayList(ImmutableMap.of("image","https://goo.gl/7dePYs")))
		.build()))
	.build()).join();
import (
    stream "gopkg.in/GetStream/stream-go2.v1"
)

client, err := stream.NewClient("YOUR_API_KEY", "API_KEY_SECRET")
if err != nil {
	// ...
}

feed := client.FlatFeed("user", "user-one")

_, err = feed.AddActivity(stream.Activity{
	Actor:  client.Collections().CreateUserReference("user-one"),
	Verb:   "post",
	Object: "I love this picture",
	Extra: map[string]interface{}{
		"attachments": map[string]interface{}{
			"og": map[string]interface{}{
				"title":       "Crozzon di Brenta photo by Lorenzo Spoleti",
				"description": "Download this photo in Italy by Lorenzo Spoleti",
				"url":         "https://unsplash.com/photos/yxKHOTkAins",
				"images": []interface{}{
					map[string]string{}{
						"image": "https://goo.gl/7dePYs",
					},
				},
			},
		},
	},
})
if err != nil {
	// ...
}
//dotnet add package stream-net
var client = new Stream.StreamClient("YOUR_API_KEY", "API_KEY_SECRET");

var feed = client.Feed("timeline", "user-one");
var user = Stream.Users.Ref("user-one");
var activity = new Stream.Activity(Stream.Users.Ref("user-one"), "post", "i love this picture");
var ogData = new Dictionary<string, object>()
{
    {"title", "Crozzon di Brenta photo by Lorenzo Spoleti"},
    {"description", "Download this photo in Italy by Lorenzo Spoleti"},
    {"url", "https://unsplash.com/photos/yxKHOTkAins"},
    {"images", new Dictionary<string,string>[]
        {
                new Dictionary<string, string>()
                {
                    {"image", "https://goo.gl/7dePYs"}
                }
        }
    }
};
var attachment = new Dictionary<string, object>()
{
    {"og", ogData}
};
activity.SetData("attachments", attachment);
await feed.AddActivity(activity);

Conclusion and Next Steps

In this tutorial we saw how easy it is to use Stream API and the iOS Activity Feed library to add a fully featured timeline to a mobile application.

Adding feeds to a mobile app can take weeks or months. Stream makes it easy and gives you the tools and the resources to improve user engagement within your app. Time to add a feed!

Stream’s API is used by over 300 million end users. The API tutorial and the docs help you take a deep dive and learn how to fully customize your activity feed’s behaviour.