스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
훌륭한 위젯의 원리
우수한 위젯의 기본 요건인 관련성과 맞춤화에 대해 알아봅니다. 타임라인 항목과 TimelineReloadPolicies를 사용하여 위젯을 최신 상태로 유지하는 방법 및 다양한 프레젠테이션 환경과 물리적 위치에 맞게 위젯을 조정하는 방법을 확인하세요. 마지막으로 사용자가 원하는 대로 맞춤화할 수 있는 맞춤화 지원 위젯을 제작하는 방법을 다룹니다.
리소스
관련 비디오
WWDC22
WWDC21
WWDC20
-
다운로드
♪ ♪ Hello, my name is Brett Cato, and I’m an engineer on the System Experience team. Today we’re going to be talking about principles of great widgets, diving into some hot topics to help you build the best widgets possible. In iOS 14, we introduced WidgetKit. WidgetKit allows you to create beautiful, dynamic, multiplatform widgets right on the user’s Home Screen on iOS, iPadOS, and macOS.
Now, before we get started, if you missed any of the talks from WWDC 2020, there are some really great ones, like “Meet WidgetKit,” which provides an introduction to the WidgetKit framework and its core concepts. “Widgets code-along” is a three-part series of building a widget with increasing complexity from basic to advanced. “Building SwiftUI views for widgets” details how to get the most out of SwiftUI to build adaptable views for your widgets. Lastly, “Design great widgets” explores design considerations from our human interface group with many, many examples.
Today we’re gonna touch on two topics for great widgets: keeping them relevant and enabling widget customization. So let’s get started with relevance. There’s three types of relevance that we’re gonna discuss in depth to make sure your widget is up to date and can adapt to its environment. We’re gonna talk about time, presentation, and location relevance. At the heart of WidgetKit is the timeline. This is the core mechanism of Widgets to stay relevant throughout the day. A timeline is composed of one or more timeline entries. In this example, you can see three such entries, one at 9:00 AM, 9:30 AM, and 10:00 AM. Now, when the system asks your widget for a timeline, the resulting timeline will be archived and its entries rendered at specific times you specify into the future. This lets the system have your user interface ready at a moment’s notice to be displayed right when the user needs it. Let’s look at some examples of timelines, starting from the most simplistic, and look at the type of content that might be in them.
Here’s an example of the simplest timeline with just a single entry: Screen Time. Now, because Screen Time can’t predict or forecast data into the future and it’s strictly based on the statistical history of how the user’s device has been used, it really can’t leverage multiple entries in its timeline very well, so it just uses a single entry. This is the most simplistic timeline that you can have. That said, though, not all widgets need to be single entry, and in fact, in most cases, I’d encourage you to consider and question why you aren’t offering multiple entries in your timeline. If you have forward-looking content, have important dates or deadlines, or can forecast your content into the future, your widget should really be taking advantage of multiple entries in your timeline. Here’s another example from Weather. As you can see, Weather’s timeline provides multiple entries to last hours throughout the day. Now, the first entry here is the most accurate because it represents the current weather, and all the other subsequent entries represent forecast data at times later in the day. This extra forecast data is super useful as widget reloads aren’t guaranteed to reload at the exact time that you specify. And if a reload doesn’t hit that target date exactly when it’s desired, then now the system has additional forecasted content to tide over the user experience.
Here’s an example from Photos. You can see that the Photos timeline provides a number of personal and relevant photos to appear at specific times of the day. For me, these are some really, really great memories. And now, while the widget reloads only a couple times a day, it really gives the feeling of being lively throughout by presenting new and fresh content from these multiple timeline entries. As you can see here, even if you don’t have data that’s forecastable like weather, as in this example with Photos, you can still incorporate content that’s relevant to the user in a way that can surprise and delight by leveraging these multiple entries in your timeline.
Now, because some widgets are viewed more than others, we decided to give widgets a fairness factor for updates. We call this an update budget. Budgets are allocated and accumulated through the day, and they’re heavily influenced by user viewing habits. A frequently viewed widget can be expected to receive somewhere in the ballpark of around 40 to 70 background updates per day, which translates to an update roughly every 15 to 30 minutes if spaced evenly during the normal hours that a user is awake. However, of course not all reloads have to be evenly spaced like this, and our goal was to enable and support a varied update cadence for different requirements. For example, maybe you have a sports widget that’s largely idle until an upcoming game for a favorite team, and then it can receive a burst of score updates before, during, and even after the game.
WidgetKit’s smart, and it may withhold updates when the user isn’t using the device for extended periods of time like when the user’s sleeping. Otherwise, widget updates may be withheld until budgets become available for a particular widget. All that said, though, reloads are not an every second operation. Widgets are not about creating a live running experience on the Home Screen. Now, there’s numerous ways that your widgets will refresh throughout the day. And we’re gonna walk through each of these briefly to understand what they are, how they work, and how they function relative to the budget that we just introduced. The first is the TimelineReloadPolicy, which is API in WidgetKit. This is the core mechanism for your reloads to occur automatically. When you provide a timeline, you also provide a reload policy alongside it. This reload policy informs the system when you’d like to automatically refresh your widget in the background. These automatic updates are budgeted and debited from your widget’s current available budget.
Next is the WidgetCenter reload API. This is an API to refresh your widget when events occur that would invalidate your widget’s existing data. Now, normally, requests using this API would consume available budget, meaning that updates won’t occur until budget is available. However, there are a few situational exceptions that will make these reloads occur both immediately and budget-free. These are when your container app is foreground to the user or when your app is participating in a user session, like Navigation or Now Playing audio.
This API is really supplemental to the automatic background updates from the TimelineReloadPolicy API. Your widgets can also update when a significant location change has occurred. When the system detects a significant location change and your widget uses location, the system will grant you a budget-free update. This refresh occurs when the user next views your widget so that location can be resolved appropriately. Now, note that this is not guaranteed to happen right when the location change occurs but, rather, the next time the user views the widget. We’ll talk a little bit more on location later. The system can refresh your widget, too, if its presentation environment changes. For example, maybe the user changes an Accessibility preference like dynamic text or bold text, there’s a language or region change, an iCloud or App Store account change, significant time change, and more. Lastly, due to budgeting, if a user has a widget that’s rarely seen, it may not receive as many updates as you, the developer, may prefer. The system knows what dates are in your timeline, what your preferred refresh time is, and general view history from the user. The system may grant a budget-free reload if it thinks the data may be stale when it’s viewed by the user. Of course, all these system-initiated updates are always budget-free. As we learned just a moment ago, each timeline comes paired with a reload policy that describes when it should automatically update in the background. WidgetKit offers three choices here: atEnd, afterDate, and never. Which of these three reload policies are appropriate for your widget? Let’s dive in to talk about each one, with some examples for context, and we’ll talk about gotchas on what to be mindful about with each one.
The first policy we’ll discuss is the atEnd reload policy. This policy will mark your widget eligible to be refreshed when the timeline comes to an end, that is, of course, when the last entry becomes relevant. In this case, it’s at 10:30 AM. Note, too, that this time is simply the time that the widget becomes eligible for refreshing, and it doesn’t guarantee that it’ll refresh exactly at this time. Further, if using atEnd with widgets that have single-entry timelines, like the Screen Time example we talked about before, the system will choose an appropriate time for your widget. atEnd is recommended if your widget already has content that extends beyond the life of its current timeline. I kind of like to think about it like viewing windows into your content. Let’s look at an example. Outlined here is the current timeline for the Calendar widget on June 7th. It has four entries. I’ve included its past and future content to help showcase the windows. And as we change the timeline for June 8th, you can see what I mean by viewing a window into its content.
And again for the timeline for June 9th. We’re just changing the perspective on the data that’s already available and provided by the user. This type of content is really where using the atEnd policy shines.
Now, some examples of widgets that use the atEnd policy include Reminders, Calendar, Photos, Tips, and more. Again, these are all widgets with endless content well known into the future where you can simply view a window into it. It’s not really a great fit for single-entry timelines because the system’s gonna choose a reload time for you, and it’s probably not what you want. AtEnd is also not recommended if your timeline has projected content that loses relevance or accuracy over time. We simply want your content to be the most relevant that it can be, and if we wait until the end of the timeline when things are the least relevant to become eligible for updating, then it just doesn’t lead to the best user experience. So that’s atEnd. Now, let’s talk about afterDate. The afterDate reload policy makes your widget eligible for reloading after the date that’s specified. With afterDate, you’re really in full control of the eligibility time. In this example, let’s pretend the timeline is valid from 9:00 AM to roughly 11:00 AM. A widget reload policy date of 9:30 AM was chosen because the widget’s forecast data starts to lose accuracy around 9:45 or so. This gives the widget plenty of runway after 9:30 to still show its forecast data, even if it’s not reloaded immediately at 9:30.
afterDate is most appropriate for content that can change unpredictably or unexpectedly throughout the day as well as for data whose accuracy or relevance changes periodically.
Some examples of widgets that use afterDate include Stocks, Weather, News, Mail, and more. These are all widgets with content that can change unpredictably or unexpectedly through the day. Now, with afterDate, you have to really be careful of a few potential issues. Be cautious of near-immediate reloads. It can be valid to specify a date a minute or so away but usually only for very narrow windows of time. Asking for too many refreshes at this level of resolution can starve yourself of reloads later.
Also be careful if you align widgets refresh dates across devices. For example, the U.S. Stock Market opens at 9:30 AM on the East Coast and you schedule a refresh at exactly 9:30 AM to fetch that initial data for the day from servers. Remember, your widget could be on thousands or even many millions of devices, potentially even multiple times on each of those devices as well. And if you have to have time-aligned data pulls like this, you really should consider adding some level of random jitter to these dates in addition to hitting caching servers. These are especially important to consider to avoid any potentially unplanned or unnecessary costs. Lastly, let’s talk about my favorite reload policy, which is never. The never reload policy is the simplest policy because it never automatically reloads. If your widget’s content can only change through its container application being foreground or through discrete events like push notifications, then never is a great choice for your widget. When using the never policy, you can keep your widget up to date solely with the WidgetCenter reload API from your container or other accessory extensions. This really minimizes reloads to happen only when necessary and it keeps minimal impact to your budget and to the user’s battery life. Never is also appropriate if your widget requires an explicit user condition in your app that hasn’t yet been satisfied. For example, maybe logging in to a service or purchasing some specific content. Consider never if your widget can’t produce meaningful content before these types of conditions are met in your app. Good examples of widgets that use the never policy include TV, Notes, Music, Podcasts, Contacts, and more. These are all widgets and apps that require user interaction in the app to drive content changes or they receive pushes for those content updates. In summary, leverage timeline entries to your advantage. Choose the correct reload policy for your widget, and use WidgetCenter’s reload API to reload timeline events on discrete events. All right, let’s move on to presentation relevance. Your widget may be presented in specific contexts on iOS or macOS that could result in your widget changing appearance. The widget may sometimes be rerendered without any timeline updates. And great widgets will always adapt to these presentation environments appropriately. So we’re gonna talk about color schemes, Dark and Light Mode, partial privacy redactions, which are new in iOS 15, and full-on privacy redactions.
WidgetKit will automatically handle shifting your content between Light and Dark Mode as the system settings change. This is because we use the power of SwiftUI. Now think about how you want your widget to look in these respective contexts. Here you can see examples of Notes and Calendar in both the light and dark schemes. Remember, though, that not all widgets necessarily have to conform to Light and Dark Mode by changing their background and text colors. There are a few widgets, like Music and Stocks, for example, that don’t change their colors. If your application design is agnostic of this high contrast content of dark and light styles, then feel free to continue whatever color scheme makes sense for your widget.
You can also preview color scheme changes to your widget in Xcode through Xcode Previews. Here’s a quick example of previewing a systemSmall widget in Light Mode.
And now in Dark Mode. All we had to do was add the colorScheme environment override to preview how our widget would look in this environment. And if you want the system standard background color for Light and Dark Mode as shown here, then use BackgroundStyle for your fill.
Widgets are also subjected to presentation environments that can be privacy sensitive, like the Lock Screen on iOS. New in iOS 15, widgets can now redact partial content in these situations. Now, that may sound really confusing, so let’s give an example to see what the heck I’m talking about here.
Let’s pretend we had a banking widget where I’m showing the balance available in a specific account. In this case, I have $128.45 available in my account.
Now, when we go to the Lock Screen and lock the device, this account balance still appears in iOS 14 because WidgetKit really had no way to dynamically redact the balance information when the device became locked. However, that’s now changed in iOS 15, and you may now contextually mark specific views to be redacted in these situations. Now, to show you how to do that, all we do is, we simply insert the .privacySensitive view modifier on the text representing the balance amount. So when not passcode locked, as you can see here, the view still renders the balance amount as you’d expect. But when we now lock the device, the balance amount becomes masked, or redacted.
Be aware, too, that this modifier can be applied to any view, including container views like hstacks and vstacks. And if applied to a container, the entire container will be redacted.
If your app leverages complete data protection that can’t be touched while an iOS device is passcode locked, for example, maybe your app is using Health data and you skipped building a widget or were scared to include it because you thought it had to be shown on the Lock Screen, then this feature is really for you.
WidgetKit can automatically replace your active timeline content with your placeholder content when the device is passcode locked for a full redaction of content. And it can even withhold updates for the duration while the device is passcode locked. This is all possible when you adopt the default-data-protection entitlement listed here. Your timeline data will be stored per your wishes in that data class on the device. We really take your data privacy seriously here. Lastly, I want to talk about location relevancy. Widgets, like your app, can also provide contextual information for our current physical location or just simply locations that are relevant to the user in one way or another.
If your application uses location normally, then it’s likely that your widget should too. Because widgets can have multiple instances on the Home screen and in Notification Center on the Mac, consider offering pre-selected and even searchable locations for your widgets in addition to using the current location just like we do with the Weather widget through the power of Intents. Now, to fetch current location in your widget, there’s only a few steps required. First, you need to specify the NSWidgetUsesLocation key in your Info.plist. This lets the system know that you’ll be using location from your widget extension. Secondly, use the CLLocationManager as you would normally, except this time from your TimelineProvider in your widget extension. Consider the resolution your widget requires, as it’s faster to resolve coarse location, and it can result in a better user experience if you don’t need super precise location. In general, the more precise the location requested, the longer it’s gonna take to resolve.
Lastly, you can check whether the widget is authorized for location updates by using the isAuthorizedForWidgetUpdates API on the CLLocationManager. This’ll tell you whether the user has granted your widget permission to use location or not. Speaking of permissions, let’s look at location permissions for widgets in more detail to see how they work. Permissions for the widget are generally shared with its app container. Here, you can see a list of the available location permissions available from the Settings app.
If the user selects “While using app,” the user’s location is available to the widget only when the widget’s container app is foreground to the user or other situations that would consider the app to be in-use, like being in a nav session.
If the user selects “While using app or widgets,” then location is available just the same as before with “While using app,” but with an additional allowance specific to the widget. This allows the widget permissions to receive location up to 15 minutes after a widget was last viewed. And when location can’t be resolved with either of these two permissions set, consider alternate content, potentially even prior content if it’s available, or simply indicating that the location couldn’t be resolved.
If the user selects “Always” for the permission, the widget will always have available permission to access location. Finally, let’s talk about building customizable widgets. We’re gonna talk about size, kind, and configuration as a means for users to personalize their widget experience. Now, widgets come in all different sizes. And it’s recommended to support as many sizes as you can so that users have choice when placing their widgets. Remember that small variances do exist in sizing between devices, and it’s best to use system standard padding, margins, text styles, and text sizes when possible.
Now, if you look at this iPad, it’s really transformed into a content showcase. If it’s not obvious, a new size has been added in iOS 15 specifically for the iPad, and we call it Extra Large.
Here it is so you can see it better. It’s the same height as the large widget but wider to showcase even more content that’s available on the iPad. And here’s how you add it to your widget. Here I’ve added it to an existing widget config that already supports the systemLarge family.
By default, if you don’t specify supported families on your widget configuration, then the new size will be supported automatically if you build with the iOS 15 SDK, or later.
The next axis for personalization is the widget kind. Kinds of widgets offer different perspectives into your data or content. So think about what kinds of widgets might make the most sense in your app. The best way to describe these is really by example. Here’s an example from Clock. On the left, we have a City widget that tracks the time of a single city, and on the right, a World Clock widget that can accommodate many different cities in the same view. Here’s another example from Stocks. On the left, there’s a stock Symbol widget that shows the stock price for a given stock-- in this case, Apple-- and on the right, an Overview widget. The Overview widget will show a series of stocks reflected in the user’s Stocks app. So how do you publish these different kinds of widgets? Well, to publish multiple kinds of widgets is super trivial.
Using that Stocks example from before, here I have a Symbol widget and an Overview widget in the top left. To publish these, you just return the different widget configurations in a WidgetBundle object and include the @main attribute to the WidgetBundle. It’s that easy. Note that the order that widgets are defined in the bundle is also very important. This order reflects how your widgets will appear in the widget gallery. So make sure that the first widget listed is your hero use case. Lastly, it’s not possible to dynamically publish or dynamically retract the availability of a widget once your app is installed. So some care and thought should be applied around the support of your widgets over time.
The last axis for customizability is through configuration. And widgets support two types: static and Intent-based configurations. Static widget configurations deliver the same content for each widget instance. Remember that Stocks Overview example from before? Well, each one’s gonna show the exact same content even if they’re in different locations on the user’s device. Static widgets are super simple, and they’re elegant widget definitions because they take no configuration or setup from the user.
On the other hand, Intent widget configurations deliver user-configured content for each instance. Check out these Stock Symbol widgets on the right. This user has configured many different instances, each showcasing a different stock. Now, this is some pretty sweet stuff. And a user could even combine these all into a stack of widgets on their Home Screen to flip through to save space.
So how are these configured by the user, you might ask. Well, from a user’s perspective, the system will provide all the surrounding UI pieces. And after tapping on a widget in Edit mode that supports Intents, the configuration platter is shown. It presents a list of parameters from the Intent that the user can configure. And in this example, there’s a single parameter for the Symbol to track and its value is currently the string AAPL for the Apple stock. Now, once a user taps on a parameter, an input controller appears to collect the input from the user. And once completed, the widget will update automatically. Now, there’s a ton of customization that you can do with an Intent, and there’s a great talk from WWDC 2020 that I’ll share in a moment to help you get started if you want an Intent configuration. In the meantime, let’s just quickly highlight what the differences are between coding a widget based on Intents versus a static configuration.
The static configuration’s really simple. You just instantiate a StaticConfiguration object with the standard TimelineProvider. Let’s contrast that with an Intent configuration. All that’s different here is that the StaticConfiguration is replaced by an IntentConfiguration, and the TimelineProvider is replaced by an IntentTimelineProvider. Each of these has a slightly modified interface to support Intents, and this is so you can receive the user-configured Intent when populating your timeline. For a more detailed look at creating and configuring Intents for widgets, including an overview on the various data types supported and what they look like from the config UI, check out the “Add configuration and intelligence to your widgets” talk from WWDC 2020.
To wrap up, we’ve reviewed how great widgets make use of their timeline entries, master their reload policy, and adapt to their presentation and potentially physical environments. Plan to offer different sizes, kinds, and configurations to give a dynamic and personalizable experience to your users. Thank you.
-
-
15:46 - Xcode Previews for Widget Views with Color Scheme Overrides
struct MyWidgetEntryView : View { var date: Date var body: some View { ZStack { Rectangle().fill(BackgroundStyle()) VStack { Text("Hello") } } } } struct MyWidget_Previews: PreviewProvider { static var previews: some View { MyWidgetEntryView(date: Date()) .previewContext(WidgetPreviewContext(family: .systemSmall)) .environment(\.colorScheme, .dark) } }
-
16:34 - Widget Partial Privacy Redactions - Banking Example
struct MyWidgetEntryView : View { var body: some View { ZStack { Rectangle().fill(BackgroundStyle()) VStack(alignment: .leading) { Text("Balance") .font(.largeTitle) .fontWeight(.bold) .foregroundColor(Color.blue) Text("$128.45") .privacySensitive() .font(.title2) .foregroundColor(Color.gray) } } } }
-
23:08 - WidgetBundle Example
struct IndividualSymbolWidget : Widget { var body: some WidgetConfiguration { … } } struct StocksOverviewWidget : Widget { var body: some WidgetConfiguration { … } } @main struct MyWidgetBundle: WidgetBundle { var body: some Widget { // Order of these widgets defines the order in the Widget Gallery IndividualSymbolWidget() StocksOverviewWidget() } }
-
25:43 - Static Widget Configuration Example
@main public struct SampleWidget: Widget { public var body: some WidgetConfiguration { StaticConfiguration(kind: "com.sample.myStaticSampleWidgetKind", provider: Provider()) { entry in SampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } public struct Provider: TimelineProvider { public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let entry = SimpleEntry(date: Date()) // TODO: Generate a timeline entry completion(timeline) } }
-
25:55 - Intent Widget Configuration Example
@main public struct SampleWidget: Widget { public var body: some WidgetConfiguration { IntentConfiguration(kind: "com.sample.myIntentSampleWidgetKind", intent: SampleConfigurationIntent.self provider: Provider()) { entry in SampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } public struct Provider: IntentTimelineProvider { public func timeline(for configuration: SampleConfigurationIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) // generate a timeline completion(timeline) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.