Friday, April 19, 2024
HomeRuby On RailsClasses From Constructing iOS Widgets — Cellular (2022)

Classes From Constructing iOS Widgets — Cellular (2022)


By Carlos Pereira, James Lockhart, and Cecilia Hunka

When the iOS 14 beta was initially introduced, we knew we would have liked to make the most of the brand new widget options and get one thing to our retailers. The brand new widgets appeared superior and will actually give retailers a strategy to see their store’s knowledge at a look with no need to open our app.

Quick ahead a few years, and we now have a lot of suggestions from the brand new design. We knew retailers have been utilizing them, however they wanted extra. The present design was missing and solely supplied two metrics—additionally, they took up plenty of house. This expertise prompted us to start out a brand new venture. To improve our authentic design to higher match our service provider’s wants.

Our widgets primarily deal with analytics. Analytics may help retailers perceive how they’re doing and acquire insights to make higher choices shortly about their enterprise. Monitoring metrics is a day by day exercise for lots of our retailers, and on cellular, we now have the chance to offer retailers a quicker strategy to entry this knowledge by means of widgets. As widgets present entry to “at a look” details about your store and permit retailers a singular avenue to shortly get a pulse on their outlets that they wouldn’t discover on desktop.

A screenshot showing the add widget screen for Insights on iOS
Add Insights widget

After gathering suggestions and constantly in search of alternatives to boost our widget capabilities, we’re at our third iteration, and we’ll share with you ways we approached constructing widgets and a number of the challenges we confronted.

Why We Didn’t Use React Native

A pair years in the past Shopify determined to go all in on React Native. New improvement was performed in React Native and we started migrating some apps to the brand new stack. Together with our flagship admin app, the place we have been constructing our widgets. Which posed the query, ought to we write the widgets in React Native?

After performing some investigation we shortly hit some roadblocks: app extensions are restricted when it comes to reminiscence, WidgetKit’s structure is extremely optimized to work with SwiftUI because the view hierarchy is serialized to disk, there’s additionally, presently, no official help within the React Native group for widgets.

Shopify believes in utilizing the correct device for the job, we consider that native improvement with SwiftUI was the only option on this case.

When constructing out our structure for widgets, we wished to create a constant expertise on each iOS and Android whereas preserving platform idioms the place it made sense. Under we’ll go over our expertise and methods constructing the widgets, stating a number of the harder challenges we confronted. Our intention is to shed some gentle round these much less talked about surfaces, give some inspiration to your initiatives, and hopefully, save time in the case of implementing your widgets.

Fetching Knowledge

Some forms of widgets have knowledge that change much less regularly (for instance, reminders) and a few that may be forecasted for the whole day (for instance, Calendar and climate). In our case, the retailers want up-to-date metrics about their enterprise, so we have to present knowledge as contemporary as attainable. Time for our widget is essential. Delays in knowledge may cause confusion, and even worse, delay info that would inform a enterprise determination. For instance, let’s say you watch the shares app. You’d anticipate the inventory app and its corresponding widget knowledge to be as updated as attainable. If the information is a number of hours stale, you possibly can miss worthwhile info for making choices or you possibly can miss an necessary drop or rise in value. With our product, our retailers want as updated info as we are able to present them to run their enterprise.

Fetching Knowledge within the App

Widgets might be stored updated with related and well timed info by utilizing knowledge accessible regionally or fetching it from a server. The server fetching might be initiated by the widget itself or by the host app. In our case, because the app doesn’t share the identical info because the widget, we determined it made extra sense to fetch it from the widget.

We nonetheless take into account shifting knowledge fetching to the app as soon as we begin sharing related knowledge between widgets and the app. This structure may simplify the dealing with of authentication, state administration, updating knowledge, and caching in our widget since just one course of may have this job fairly than two separate processes. It’s price noting that the widget can entry code from the principle app, however they’ll solely talk knowledge by means of keychain and shared person defaults as widgets run on separate processes. Sharing the information fetching, nevertheless, comes with an added complexity of getting a background course of pushing or making knowledge accessible to the widgets, since widgets should stay practical even when the app isn’t within the foreground or background. For now, we’re pleased with the present answer: the widgets fetch knowledge independently from the app whereas sharing the session administration code and tokens.

A flow diagram highlighting widgets fetch data independently from the app while sharing the session management code and tokens
Present answer the place widgets fetch knowledge independently

Querying Enterprise Analytics Knowledge with Reportify and ShopifyQL

The enterprise knowledge and visualizations displayed within the widgets are powered by Reportify, an in-house service that exposes knowledge by means of a set of schemas queried through ShopifyQL, Shopify’s Commerce knowledge querying language. It seems to be similar to SQL however is designed round knowledge for commerce. For instance, to fetch a outlets whole gross sales for the day:

Making Our Widgets Antifragile

iOS Widgets’ structure is in-built a means that updates are aware of battery utilization and are budgeted by the system. In the identical means, our widgets should even be aware of saving bandwidth when fetching knowledge over a community. Whereas growing our second iteration we got here throughout a peculiar downside that was exacerbated by our particular use case.

Since we want knowledge to be contemporary, we all the time pull new knowledge from our backend on each replace. Every replace is roughly quarter-hour aside to keep away from having our widgets cease updating (which you’ll examine why on Apple’s Developer web site). We discovered that iOS calls the replace strategies, getTimeline() and getSnapshot(), greater than as soon as in an replace cycle. In widgets like calendar, these additional calls come with out a lot additional value as the information is saved regionally. Nonetheless, in our app, this was triggering two to 5 additional community calls for a similar widget with the identical knowledge in fast succession.

We additionally observed these calls have been inflicting a seemingly unrelated kick out subject affecting the app. Every widget runs on a distinct course of than the principle utility, and all widgets share the keychain. As soon as the app requests knowledge from the API, it checks to see if it has an authenticated token within the keychain. If that token is stale, our system pauses updates, refreshes the token, and continues community requests. Within the case of our widgets, every widget name to replace was creating one other workflow that would want a token refresh. After we solely had a single widget or replace circulate, it labored nice! Even 4 to 5 updates would often work fairly nicely. Nonetheless, finally one among these community calls would come out of order and an invalid token would get saved. On our subsequent replace, we now have no strategy to retrieve knowledge or request a brand new token leading to a session kick out. This was an excellent discover because it was inflicting plenty of frustration for our affected retailers and ourselves, who may by no means actually put our finger on why this stuff would, each on occasion, simply log us out.

As a way to appropriate the pointless roundtrips, we constructed a easy short-lived cache:

  1. The system asks our widget to supply new knowledge
  2. We first look into the native cache utilizing a key particular to that widget. On iOS, our key’s produced from a configuration for that widget as there’s no distinctive identifiers supplied. We additionally bear in mind configuration comparable to locale in order to not keep away from forcing updates after a language change.
  3. If there’s knowledge, and that knowledge was set lower than one minute in the past, we return it and keep away from making a community request. 
  4. In any other case, we fetch the information as regular and retailer it within the cache with the timestamp.
A flow diagram highlighting the steps of the cache
The straightforward short-lived cache circulate

With this answer, we diminished unused community calls and system load, prevented amassing incorrect analytics, and glued a protracted operating bug with our app kick outs!

Implementing Decoder Technique with Dynamic Alternatives

When fetching the information from the Analytics REST service, every widget might be configured with two to seven metrics from a complete of 12. This set ought to develop sooner or later as new metrics can be found too! Our present set of metrics are all time-based and have an identical construction.

However that doesn’t imply the construction of future metrics won’t change. For instance, what a couple of metric that incorporates knowledge that isn’t mapped over a time vary? (like orders to satisfy, which doesn’t include any historic info).

The service provider can also be capable of configure the order the metrics seem, which store (if they’ve multiple store), and which date vary represents the information: right this moment, final 7 days, and final 30 days.

We needed to implement a knowledge fetching and decoding mechanism that:

  • solely fetches the information the service provider requested as a way to keep away from asking for unneeded info
  • helps a set of metrics in addition to being versatile so as to add future metrics with totally different shapes
  • helps totally different date ranges for the information.

A simplified model of the answer is proven beneath. First, we create a struct to symbolize the question to the analytics service (Reportify).

Then, we create a category to symbolize the decodable response. Proper now it has a set construction (worth, comparability, and chart values), however sooner or later we are able to use an enum or totally different subclasses to decode totally different shapes.

Subsequent, we create a response wrapper that makes an attempt to decode the metrics based mostly on an inventory of metric sorts handed to it. Every metric has its configuration, so we all know which class is used to learn the values.

Lastly, when the widget Timeline Supplier asks for brand new knowledge, we fetch the information from the present metrics and decode the response. 

Constructing the UI

We wished to help the three widget sizes: small, medium, and huge. From the beginning we wished to have a single View to help all sizes as an try to attenuate UI discrepancies and make the code straightforward to keep up.

We began by figuring out the frequent construction and creating parts. We ended up with a Metric Cell element that has three variations:

A metric cell from a widget
A metric cell
A metric cell from a widget
A metric cell with a sparkline
A metric cell from a widget
A metric cell with barchart

All three variations include a metric identify and worth, chart, and a comparability. Because the widget containers turn out to be larger, we present the service provider extra knowledge. Every view dimension incorporates extra metrics, and the biggest widget incorporates a full width chart on the primary chosen metric. The comparability indicator additionally will get shifted from backside to proper on this variation.

The primary chosen metric, on the massive widget, is proven as a full width cell with a bar chart displaying the information extra clearly; we name it the Main cell. We added a construction to point if a cell goes for use as major or not. Moreover the first flag, our element doesn’t have any context concerning the widget dimension, so we use chart knowledge as an indicator to render a cell major or not. This paradigm suits very nicely with SwiftUI.

A simplified model of the particular Cell View:

After constructing our cells, we have to create a construction to render them in a grid in keeping with the scale and metrics chosen by the service provider. This element additionally has no context of the widget dimension, so our structure choices are primarily based mostly on what number of metrics we’re receiving. On this instance, we’ll consult with the View as a WidgetView.

The WidgetView is initialized with a WidgetState, a struct that holds many of the widget knowledge comparable to store info, the chosen metrics and their knowledge, and a final up to date string (which represents the final time the widget was up to date).

To have the ability to make choices on structure based mostly on the widget traits, we created an OptionSet referred to as LayoutOption. That is handed as an array to the WidgetView.

Structure choices:

That helped us to not tie this element to Widget households, fairly to structure traits that makes this element very reusable in different contexts.

The WidgetView structure is constructed utilizing primarily a LazyVGrid element:

A simplified model of the particular View:

Including Dynamic Dates

One necessary piece of data on our widget is the final up to date timestamp. It helps take away confusion by permitting retailers too shortly know the way contemporary the information is that they’re . Since iOS has an approximate replace time with many variables, coupled with knowledge connectivity, it’s very attainable the information might be over quarter-hour previous. If the information is sort of stale (say you went to the cottage for the weekend and missed just a few updates) and there was no replace string, you’d assume the information you’re is as much as the second. This could trigger pointless confusion for our retailers. The answer right here was to make sure there’s some communication to our service provider when the final replace was.

In our earlier design, we solely had small widgets, and so they have been capable of show just one metric. This info resulted in a protracted string, that on smaller gadgets, would typically wrap and present over two strains. This was nice when house was considerable in our older design however not in our new knowledge wealthy designs. We explored how we may greatest work with timestamps on widgets, and probably the most promising answer was to make use of relative time. As a substitute of getting a static worth comparable to “as of three:30pm” like our earlier iteration, we’d have a dynamic date that will appear like: “1 min, 3 sec in the past.”

One factor to recollect is that though the widget is seen, we now have a restricted variety of updates we are able to set off. In any other case, it could be consuming plenty of pointless sources on the service provider’s machine. We knew we couldn’t maintain triggering updates on the widget as typically as we wished (nor would it not be allowed), however iOS has methods to cope with this. Apple did launch help for dynamic textual content on widgets throughout our improvement that allowed utilizing timers in your widgets with out requiring updates. We merely must move a mode to a Textual content element and it robotically retains the whole lot updated:

Textual content("(now, model: .relative) in the past")

It was good, however we now have no choices to customise the relative model. Having the ability to customise the relative model was an necessary level for us, as the present supported model doesn’t match nicely with our widget structure. One among our greatest constraints with widgets is house as we all the time want to consider the smallest widget attainable. In the long run we determined to not transfer ahead with the relative time method, and stored a diminished model of our earlier timestamp.

Including Configuration

Our new widgets have a large amount of configuration, permitting for retailers to decide on precisely what they care about. For every widget dimension, the service provider can choose the shop, a sure variety of metrics, and a date vary. On iOS, widgets are configured by means of the SiriKit Intents API. We confronted some challenges with the WidgetConfiguration, however thankfully, all had workarounds that match our use instances.

Insights widget configuration
Insights widget configuration

It’s Not Potential to Deselect a Metric

When defining a discipline that has a number of values supplied dynamically, we are able to restrict the variety of choices per widget household. This was necessary for us, since every widget dimension has a distinct variety of metrics it could help. Nonetheless, the present UI on iOS for widget configuration solely permits choosing a worth however not deselecting it. So, as soon as we chosen a metric we couldn’t take away it, solely replace the choice. However what if the service provider have been solely enthusiastic about one metric on the small widget? We solved this with a small design change, by offering “None” as an possibility. If the service provider have been to decide on this selection, it could be ignored and proven as an empty state. 

It is not attainable to validate the person picks

With the addition of “None” and the best way intents are designed, it was attainable to pick out all “None” and have a widget with no metrics. As well as, it was attainable to pick out the identical metric twice.. We wish to have the ability to validate the person choice, however the Intents API did not help it. The answer was to embrace the truth that a widget might be empty and present as an empty state. Duplicates have been filtered out so any greater than a single metric alternative was modified to “None” earlier than we despatched any community requests.

The First Calls to getTimeline and getSnapshot Don’t Respect the Most Metric Rely

For intent configurations supplied dynamically, we should present default values within the IntentHandler. In our case, the metrics checklist varies per widget household. Within the IntentHandler, it’s not attainable to question which widget household is getting used. So we needed to return no less than as many metrics as the biggest widget (seven). 

Nonetheless, even when we restrict the variety of metrics per household, the primary getTimeline and getSnapshot calls within the Timeline Supplier have been filling the configuration object with all default metrics, so a small widget would have seven metrics as a substitute of two!

We ended up including some cleanup code at first of the Timeline Supplier strategies that trims the checklist relying on the anticipated variety of metrics.

Optimizing Testing

Automated exams are a elementary a part of Shopify’s improvement course of. Within the Shopify app, we now have an excellent quantity of unit and snapshot exams. The previous widgets on Android had good take a look at protection already, and we constructed on the prevailing infrastructure. On iOS, nevertheless, there have been no exams because it’s presently not attainable so as to add take a look at targets towards a widget extension on Xcode.

Given this is able to be a posh venture and we didn’t need to compromise on high quality, we investigated attainable options for it.

The only answer can be so as to add every file on each the app and within the widget extension targets, then we may unit take a look at it within the app facet in our customary take a look at goal. We determined not to do that since we’d all the time want so as to add a file to each targets, and it could bloat the Shopify app unnecessarily.

We selected to create a separate module (a framework in our case) and transfer all testable code there. Then we may create unit and snapshot exams for this module.

We ended up shifting many of the code, like views and enterprise logic, to this new module (WidgetCore), whereas the extension solely had WidgetKit particular code and configuration like Timeline supplier, widget bundle, and intent definition generated information.

Given our code within the Shopify app relies on UIKit, we did must replace our in-house snapshot testing framework to help SwiftUI views. We have been very pleased with the outcomes. We ended up reaching a excessive take a look at protection, and the exams flagged many regressions throughout improvement.

Quick SwiftUI Previews 

The Shopify app is an enormous utility, and it takes some time to construct. Given the widget extension relies on our important app goal, it took a very long time to arrange the SwiftUI previews. This triggered frustration throughout improvement. It additionally eliminated one of many greatest advantages of SwiftUI—our potential to iterate shortly with Previews and the quick suggestions cycle throughout UI improvement.

One concept we had was to create a module that didn’t depend on our important app goal. We created one referred to as WidgetCore the place we put plenty of our reusable Views and enterprise logic. It was quick to construct and will additionally render SwiftUI previews. The one caveat is, because it wasn’t a widget extension goal, we couldn’t leverage the WidgetPreviewContext API to render views on a tool. It meant we would have liked to load up the extension to make sure the designs and modifications have been all the time working as anticipated on all sizes and modes (gentle and darkish).

To unravel this downside, we created a PreviewLayout extension. This had all of the widget sizes based mostly on the Apple documentation, and we have been in a position to make use of it in an identical means:

Our PreviewLayout extension can be used on all of our widget associated views in our WidgetCore module to emulate the sizes in previews:

Buying Analytics

Shopify Insights widget largest size in light mode
Largest widget in gentle mode

Since our first widgets have been developed, we wished to grasp how retailers are utilizing the performance, so we are able to all the time enhance it. The information workforce constructed some dashboards displaying issues like an in depth view of what number of widgets put in, the preferred metrics, and sizes.

A lot of the knowledge used to construct the dashboards come from analytics occasions fired by means of the widgets and the Shopify app.

For the brand new widgets, we wished to higher perceive adoption and retention of widgets, so we would have liked to seize how customers are configuring their widgets over time and which of them are being added or eliminated.

Managing Distinctive Ids

WidgetKit has the WidgetCenter struct that enables requesting details about the widgets presently configured within the machine by means of the getCurrentConfigurations technique. Nonetheless, the checklist of metadata returned (WidgetInfo) doesn’t have a secure distinctive identifier. Its identifier is the item itself, because it’s hashable. Given this constraint, if two similar widgets are added, they’ll each have the identical identifier. Additionally, given the intent configuration is a part of the id, if one thing modifications (for instance, date vary) it’ll appear like it’s a very totally different widget.

Given this limitation, we needed to regulate the best way we calculate the variety of distinctive widgets. It additionally made it more durable to tell apart between totally different life-cycle occasions (including, eradicating, and configuring). Hopefully there might be a strategy to get distinctive ids for widgets in future variations of iOS. For now we created a single worth derived from a very powerful elements of the widget configuration.

Detecting, Including, and Eradicating Widgets 

At the moment there’s no WidgetKit life cycle technique that tells us when a widget was added, configured, or eliminated. We would have liked it so we are able to higher perceive how widgets are getting used.

After some exploration, we observed that the one strategies we may rely on have been getTimeline and getSnapshot. We then determined to construct one thing that would simulate these lacking life cycle strategies by utilizing those we had accessible. getSnapshot is often referred to as on state transitions and likewise on the widget Gallery, so we discarded it as an possibility.

We constructed an answer that did the next

  1. Each time the Timeline suppliers’ getTimeline is known as, we name WidgetKit’s getCurrentConfigurations to see what are the present widgets put in.
  2. We then evaluate this checklist with a earlier snapshot we persist on disk.
  3. Based mostly on this comparability we attempt to guess which widgets have been added and eliminated.
  4. Then we triggered the correct life cycle strategies: didAddWidgets(), didRemoveWidgets().

Because of identifiers not being secure, we couldn’t discover a dependable method to detect configuration modifications, so we ended up not supporting it.

We additionally observed that WidgetKit.getCurrentConfigurations’s outcomes can have some delay. If we take away a widget, it could take a pair getTimeline requires it to be mirrored. We adjusted our analytics scheme to take that into consideration.

Detecting, adding, and removing widgets solution
Present answer

Our method to widgets made supporting iOS 16 out of the gate actually easy with just a few modifications. Since our lock display problems will floor the identical info as our dwelling display widgets, we are able to truly reuse the Intent configuration, Timeline Supplier, and many of the views! The one change we have to make is to regulate the supported households to incorporate .accessoryInline, .accessoryCircular, and .accessoryRectangular, and, after all, draw these views.

Our Foremost View would additionally simply want a slight adjustment to work with our present dwelling display widgets.

Migrating Gracefully

WidgetKit was launched for watchOS problems in iOS 16. This replace comes with a foreboding message from Apple:

Essential
As quickly as you provide a widget-based complication, the system stops calling ClockKit APIs. For instance, it not calls your CLKComplicationDataSource object’s strategies to request timeline entries. The system should wake your knowledge supply for migration requests.

We actually care about our apps at Shopify, so we actually wanted to unpack what this meant, and the way does this have an effect on our retailers operating older gadgets? With some testing on gadgets, we have been capable of finding out, the whole lot is ok.

For those who’re presently operating WidgetKit problems and add help for lock display problems, your ClockKit app and problems will proceed to operate as you’d anticipate.

What we had assumed was that WidgetKit itself was taking the place of WatchOS problems; nevertheless, to make use of Widgetkit on WatchOS, you have to create a new goal for the Watch. This is smart, though the APIs are so related we had assumed it was a one and performed method. One WidgetKit extension for each platforms.

One factor to be careful for,  should you do implement the brand new WidgetKit on WatchOS, in case your customers are on WatchOS 9 and above will lose all of their problems from ClockKit. Apple did present a migration API to help the change that’s referred to as as a substitute of your previous problems.

For those who don’t have the luxurious of simply setting your goal to iOS 16, your problems will proceed to load up for these on WatchOS 8 and beneath from our testing.

We already had a set of widgets operating on each platforms, now we needed to resolve learn how to transition to the brand new replace as they’d be changing the prevailing implementation. On iOS we had two totally different widget varieties every with their very own small widget (you possibly can consider varieties as a widget group). With the brand new implementation, we wished to supply a single widget form that provided all three sizes. We didn’t discover a lot documentation across the migration, so we simulated what occurs to the widgets below totally different eventualities.

If the service provider has a widget on their dwelling display and the app updates, one among two issues would occur:

  1. The widget would turn out to be a white clean sq. (the type IDs matched).
  2. The widget simply disappeared altogether (the type ID was modified).

The preliminary plan (we had hoped for) was to make one among our widgets remodel into the brand new widget whereas eradicating the opposite one. Given the above, this technique wouldn’t work. This additionally consists of some annoying tech debt since all of our Intent information would proceed to say the identify of the previous widget.

The compromise we got here to, in order to not fully take away all of a service provider’s widgets in a single day, was to deprecate the previous widgets on the identical time we launch the brand new ones. To deprecate, we up to date our previous widget’s UI to show a message informing that the widget is not supported, and the service provider should add the brand new ones. The lesson right here is it’s important to watch out once you make choices round widget grouping because it’s not straightforward to vary.

There’s no means so as to add a brand new widget programmatically or to convey the service provider to the widget gallery by tapping on the previous widget. We additionally added some communication to assist ease the transition by:

      • updating our assist heart docs, together with info round learn how to use widgets 
      • pointing our previous widgets to open the assistance heart docs 
      • giving a lot of time earlier than eradicating the deprecation message.

      In the long run, it wasn’t probably the most very best state of affairs, and we got here away studying concerning the pitfalls throughout the two ecosystems. One piece of recommendation is to actually mirror on present and future wants when defining which widgets to supply and learn how to cut up them, since a future modification will not be easy.

      Screen displaying the notice: This widget is no longer active. Add the new Shopify Insights widget for an improved view of your data. Learn more.
      Widget deprecation message

      As we proceed to study how retailers use our new technology of widgets, we’ll proceed to hone in on one of the best expertise throughout each our platforms. Our widgets have been made to be versatile, and we’ll have the ability to frequently develop the checklist of metrics we provide by means of our customization. This work opens the best way for different groups at Shopify to construct on what we’ve began and create extra widgets to assist Retailers too.

      2022 is a busy 12 months with iOS 16 popping out. We’ve acquired a brand new WidgetKit expertise to combine to our watch problems, lock display problems, and dwell actions hopefully later this 12 months!

      Carlos Pereira is a Senior Developer based mostly in Montreal, Canada, with greater than a decade of expertise constructing native iOS purposes in Goal-C, Swift and now React Native. At the moment contributing to the Shopify admin app.

      James Lockhart is a Employees Developer based mostly in Ottawa, Canada. Experiencing cellular improvement over the previous 10+ years: from Phonegap to native iOS/Android and now React native. He’s an avid baker and prepare dinner when not contributing to the Shopify admin app.

      Cecilia Hunka is a developer on the Core Admin Expertise workforce at Shopify. Based mostly in Toronto, Canada, she loves dwell music and jigsaw puzzles.


      Wherever you’re, your subsequent journey begins right here! If constructing programs from the bottom as much as remedy real-world issues pursuits you, our Engineering weblog has tales about different challenges we now have encountered. Intrigued? Go to our Engineering profession web page to seek out out about our open positions and study Digital by Design.

    RELATED ARTICLES

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here

    Most Popular

    Recent Comments