Spotlight

Stan from your home screen: an iOS widget for K-pop stars

By ‎@i_am_daniele‎
Monday, 16 November 2020

#KpopTwitter is always one of the hottest topics on Twitter. No, it’s not just the Army. In 2019 alone, 6.1 billion Tweets were posted with the hashtag #KpopTwitter, from all over the world. I knew I had to join the movement in the most techy way imaginable: by building a K-pop iOS widget!

This Tweet is unavailable
This Tweet is unavailable.

One of the new, and in my opinion, most exciting features of iOS 14, is the addition of widgets. Widgets can provide a glimpse into your favorite apps by previewing and updating interesting content throughout the day. They are gentle on battery consumption and make your home screen immediately more useful. If you want some inspiration, ClattersAviary, and Nighthawk are great examples of apps that have widgets that integrate with Twitter.

We’re going to create our own widget, and you can see the final result on our TwitterDev Github.

Widgets can display content that changes throughout the day. Throughout the day, our widget will display different images that Twitter recognizes to be in the context of K-pop. You don’t need to learn #KpopTwitter’s stan language; our app is going to rely on Tweet annotations to pick relevant images for you. Using Tweet annotations, the Twitter API v2 makes available Twitter’s interpretation of the context of a Tweet, as detected by our machine learning models. That’s right - you get machine learning right out of the box, with the Twitter API v2. What’s great about Tweet annotations is that you can use them with endpoints like recent search or filtered stream to get the Tweets you care about.

There are many annotations you can choose from, but for this app we’re going to use 55.888105153038958593 (Music Genre - K-pop) for our search query.

The widget extension specifies how often the content should change and refresh. Based on that, the OS decides what to display based on how much screen time a widget gets, the status of its parent app, and other circumstances. With that in mind, we’re going to display one image per hour. Just like the popular hit song "Just one day", using the code below, the widget will only request one day worth of Tweets through recent search:

This Tweet is unavailable
This Tweet is unavailable.
 // Compose the URL for Recent search
    var components = URLComponents()
    components.scheme = "https"
    components.host = "api.twitter.com"
    components.path = "/2/tweets/search/recent"
    components.queryItems = [
      // Request images
      URLQueryItem(name: "tweet.fields", value: "attachments"),
      
      // Request the profile picture of the user who Tweeted this image
      URLQueryItem(name: "user.fields", value: "profile_image_url"),
      
      // Request the url for each image in the Tweet
      URLQueryItem(name: "media.fields", value: "url"),
      
      // Include media and user object in the response
      URLQueryItem(name: "expansions", value: "attachments.media_keys,author_id"),
      
      // We're looking for K-pop Tweets with image, and no Retweets
      URLQueryItem(name: "query", value: "context:55.888105153038958593 has:images -is:retweet"),
      
      // 24 Tweets (so we can display one per hour)
      URLQueryItem(name: "max_results", value: "24")
    ]

With the results, we’re going to create a Timeline object containing the 24 Tweets separated by hour:

This Tweet is unavailable
This Tweet is unavailable.
let entries: [TweetEntry] = tweets.enumerated().compactMap {
       (index, tweet) in
       guard let media = response.media(tweet: tweet)?.first,
          let user = response.user(of: tweet) else {
         return nil
       }
       
       // Assign each Tweet to a specific hour of the day, based on the position in the payload
       let date = Calendar.current.date(byAdding: .hour, value: index, to: date)!
       
       return TweetEntry(date: date, media: media, tweet: tweet, user: user)
     }
     
     let timeline = Timeline(entries: entries, policy: .atEnd)
     completion(timeline)
   }

Per the code above, the widget provides a Timeline object with 24 entries or fewer. WidgetKit will then present an entry based on the image’s date and time. It’s unlikely that we’ll ever run out of Tweets throughout the day, but policy: .atEnd will ensure that the widget creates a new Timeline (and consequently request new Tweets) once the OS goes through all the entries.

With the refresh logic ready, it’s time to build the UI for the home screen widget. SwiftUI makes it extremely easy to create the UI. In this case, we’re going to need only two components:

  • The image we want to display
  • The name and profile picture of the author of the Tweet to overlay to the image

Lastly, here’s how we are creating the image to display:

This Tweet is unavailable
This Tweet is unavailable.
struct TweetImage: View {
  let media: Media?
  
  @ViewBuilder
  var body: some View {
    if let mediaURLString = media?.url,
       let mediaURL = URL(string: mediaURLString),
       let imageData = try? Data(contentsOf: mediaURL),
       let image = UIImage(data: imageData) {
      Image(uiImage: image)
        .resizable()
        .scaledToFill()
    } else {
      VStack {
        Text("🤳").font(.largeTitle)
        Text("Loading pictures…").font(.caption)
      }
    }
  }
}

Note how we’re displaying a placeholder message, if for any reason an image fails to upload (this can happen, for example, if a Tweet we requested earlier in the day is no longer available).

This is really all you need to create a widget! Check out the finished project on the TwitterDev Github to see the completed widget. Tweet at TwitterDev or my @ if you end up creating your own widget!

This Tweet is unavailable
This Tweet is unavailable.