How Carousell launched its first widget for iOS14 (Part 2)

Zhe Yu
Zhe Yu
Nov 20 · 7 min read

by Jay Ang and Beemo Lee

Image for post
Image for post

In the year 2012, Carousell pioneered the mobile classified space, making it easier for millions to buy and sell. Fast forward to the year 2020, when Apple announced iOS 14, our innovative DNA jumped on the opportunity to develop the very exciting Widget on homescreen feature. In this article, the iOS team will cover how we built Carousell’s Widget and some pitfalls that we discovered during our development process.

The iOS team collaborated with the Product and Design team to come up with a design guideline and more importantly decided what gets displayed on the widget. For more in-depth explanation, feel free to check out part 1 of our post.

As seen in our previous article, the brainstorm session generated lots of ideas. The next step was narrowing down the ideas by accessing the technical implementations. Some questions that surfaced throughout the process were:

1. Can we have multiple widgets for our app?

2. How do we define the update process of the widget? Were we able to control the update frequency?

3. What sort of widget interactions are customisable?

4. Can we have multiple touch points in a small widget?

With no publicly available sample widgets for us to play around with, we relied heavily on WWDC videos and Apple widget documentation to guide us through the unknowns. Once we were confident of what is technically possible, we finalised the features that we wanted to showcase in our very first Carousell widget. The main goal was to present Carousell’s Activity tab in a glanceable manner.

Throughout the development process, we worked closely with the design and product team by having dogfooding. Dogfooding is a practice adopted in Carousell where our internal staff gets to test out our latest app build that is not publicly released yet. It helps in quality control and to grow our confidence in shipping out features to our users.

Technical Deep Dive

Image for post
Image for post

As shown in the widget preview above, we display key information such as number of chats and recent messages in a glanceable manner. To retrieve this information, we reused the same APIs that were already implemented in our app’s Activity and Chat screen.

As with most iOS apps, our networking request is also built on top of Alamofire. We organised our code base in such a way that our models are located in a separated module, known as Liquore. Hence, we could easily reuse the response model in widget target by simply importing Liquore module.

We have a Provider struct that conforms to the TimelineProvider protocol. By implementing the TimelineProvider protocol, the Provider struct acts as a manager on advising when the widget display should be updated.

TimelineProvider protocol contains a method called getTimeline(in:completion). This is where we placed both Activity and Offer API calls in a DispatchGroup to update the response displayed on the widget. The main reason as to why we went with the DispatchGroup approach is due to the dependency of ActivityAPI on the unreadCount value that is coming from the OfferAPI response. By having DispatchGroup, we can ensure the unreadCount value is accurate to be consumed by ActivityAPI.

let dispatchGroup = DispatchGroup()dispatchGroup.enter()ActivityAPI.loadActivities { activityMessages in
activityEntry = activityMessages
}
dispatchGroup.enter()OfferAPI.loadOffers { offers in
offerEntry = offers
}
dispatchGroup.notify(queue: .main) {
let timeline = Timeline(entries: [activityEntry(activity: activityEntry, offer: offerEntry)], policy: .atEnd)
completion(timeline)
}

Edge Cases

Tada! Now that is all up and working, we still needed to handle some edge cases. As we do use some authentication value to make the API request to show relevant information on the widget, we have a singleton called APIManager, that manages API request. As the authentication type may change throughout the session (e.g. User logging out, user switch to different accounts and etc…), we need a way to reflect those changes in our Widget extension.

Our APIManager is set up as a singleton. Now, we won’t dive into whether singleton is bad or good, that’s a whole separate discussion. With APIManager being a singleton, we were able to ensure that we have only one instance to be used throughout the entire project. This works great for most of our cases, but not in our widget implementation. The major reason is because the widget is set up as a separated target. Although we were able to reuse the methods, the APIManager in the widget target is considered a separated singleton.

Essentially, we ended up with two singletons for two different targets. This is an issue for us as any updates on APIManager from the main app target would not be reflected in the widget target. For instance, if a user logs out of the app, the authentication status, which is changed should also be reflected in the widget target, is currently not!

Let’s tackle this issue by making changes to our API calls in getTimeline(in:completion:) method.

let client = APIManager.initWidgetStoredAPIServer()ActivityAPI.loadActivities(client: client) { activityMessages in
activityEntry = activityMessages
}
OfferAPI.loadOffers(client: client) { offers in
offerEntry = offers
}

The key difference here is we instantiate APIManager when getTimeline(in:completion:) is called. This would ensure that we always get the latest authentication value that took place in the main target app. With the newly instantiated APIManager, we injected the constant into our respective API calls, and viola, problem solved!

For added security, we store authentication values in Keychain. We leveraged on a third-party dependency, SAMKeyChain to handle the interaction with system Keychain. We encountered an issue where our widget was not updating whenever the device is locked. The documentation as stated below gave us a hint that we needed to update our accessibilityType.

@param accessibilityType One of the "Keychain Item Accessibility Constants"used for determining when a keychain item should be readable.If the value is `NULL` (the default), the Keychain default will be used whichis highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`for background applications or `kSecAttrAccessibleWhenUnlocked` for allother applications.

As we wanted to have our widget to be updated smoothly on the background too, we had tweaked SAMKeyChain Accessibility value, and decided that

SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlock) 

was exactly how we wanted it to be.

Image for post
Image for post

Another edge case to be handled was network failure. If only we live in an ideal world where networking failure is non-existent… But this is a problem that we need to address as it would impact what should be shown on the widget. Instead of displaying an empty state, we went ahead and displayed the latest cache value whenever we received invalid data. We resorted to the last option of displaying an empty state when we encountered the permission denied exception (which could happen when the user logs out).

Now here are some bonus pitfalls that you may encounter:

1. Product Module Name needs to be unique

When we added the widget target, Product Module Name of the widget is set exactly as the main app’s name. This resulted in compilation error for us. An easy fix would be changing that value to something unique:

Image for post
Image for post

2. Testflight distribution error

At Carousell we leverage Testflight to distribute nightly builds for internal testing to all staff. After incorporating widget target into our app, we encountered this error from Testflight:

ERROR ITMS-90685: CFBundleIdentifier Collision. There is more than one bundle with the CFBundleIdentifer value under the iOS applicationERROR ITMS-90205: Invalid Bundle. The bundle contains disallowed nested bundlesERROR ITMS-90206: Invalid Bundle, The bundle contains disallowed file 'Frameworks'

Yikes! Looks like we have a nested bundle issue. This is because we have a similar internal framework that is needed by our widget extension. To fix this, we added a Run Script to the widget target:

cd "${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/"if [[ -d "Frameworks" ]]; thenrm -fr Frameworksfi

Overall, it was a great learning experience building the first-ever Carousell iOS14 widget. Hope this article has given you an insight to how Carousell widget came to life, and how you can avoid the similar roadblocks that we have encountered. What excites us even more is the possibility of this widget to empower our users to continue buying and selling on Carousell in a seamless manner.

References:
Here are many resources online about how to develop and create a widget, that were really helpful for us, so we want to share those with you as well:

P.S. We are hiring iOS Engineers and other engineering roles to join us in Singapore, India and Taiwan! Check our https://careers.carousell.com for more information.

Carousell Insider

What's going on under the hood at Carousell

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store