NetEase Cloud Music iOS14 Widget Practice Manual

Original address: NetEase Cloud Music iOS14 Widget Practical Manual

foreword

Apple released a widget (WidgetKit) at this year's WWDC20, which supports the display of dynamic information and personalized content on the home screen of iOS and iPadOS. Coupled with the addition of the iOS system application drawer, Apple has made a big move against the conservative home screen, causing users to look forward to widgets. However, there are many restrictions on the operation of widgets, and how to make a good user experience in a limited mechanism becomes a challenge that needs to be completed.

Brief description of the widget

Widgets can realize content display and function jump on the home screen.
The system will obtain the timeline from the widget, and display the data on the timeline according to the current time. Click the visual element being displayed to jump to the APP and realize the corresponding function.
The widget effect of cloud music is as follows:
Preview

Talking about Development Ideas

Widget technology stack
First of all, it needs to be clear that the widget is independent of the App environment (ie App Extension), and the life cycle, storage space, and running process of the widget are different from the App. So we need to introduce some infrastructure in this environment, such as network communication framework, image caching framework, data persistence framework, etc.
The life cycle of the widget itself is an interesting point. To put it bluntly, the life cycle of a widget is consistent with that of the desktop process, but this does not mean that the widget can execute code at any time to complete the business. Widgets use the data defined by the Timeline to render views, and our code can only be executed when refreshing the Timeline ( getTimeline) and creating a snapshot ( getSnapshot). In general, network data is fetched when the Timeline is refreshed, and appropriate views are rendered when snapshots are created.
In most cases, you need to use data to drive the view display. This data can be obtained through a network request, or it can be obtained from the App using the sharing mechanism of App Groups.
After the data is obtained when refreshing the Time Line, the Timeline can be synthesized according to business requirements. Timeline is an array TimelineEntrywith elements. ATimelineEntry time object containing a that tells the system when to use this object to create a snapshot of the widget. dateIt can also be inherited TimelineEntryand added to the data model or other information required by the business.
In order for widgets to display views, SwiftUI needs to be used to complete the layout and style building of widgets. How to implement layout and style will be described below.
After the user clicks on the widget, the App will be opened AppDelegateand openURL:the method of will be called. We need to handle this event openURL:in so that the user jumps directly to the desired page or calls a function.
Finally, if you need the customization options that are exposed to the user widget, use theIntentsThe framework defines the data structure in advance, and provides the data when the user edits the widget, and the system will draw the interface according to the data. The custom data selected by the user will be provided in the form of parameters when refreshing the Time Line ( getTimeline) and creating a snapshot ( ), and then execute different business logics according to different custom data.getSnapshot

App Extension

If you already have App Extension development experience, you can skip this chapter.
According to Apple: App Extension can extend custom functions and content outside the application and provide it to users when they interact with other applications or systems. For example, your app can appear as a widget on your home screen. That is to say, the widget is a kind of App Extension, and the development work of the widget is basically in the environment of the App Extension.
What is the relationship between App and App Extension?
In essence, they are two independent programs. Your main program can neither access the code of the App Extension nor its storage space. This is completely two processes and two programs. App Extension relies on your App body as a carrier. If the App is uninstalled, the App Extension will not exist in the system. Moreover, most of App Extension's life cycle acts on a specific field, and is managed by system control according to events triggered by users.

Create App Extension and configuration file

The following briefly describes how to create the App Extension of the widget and configure the certificate environment.
Add a Widget Extension in Xcode (the path is as follows: File-New-Target-iOS tab-Widget Extension). Don't forget to check if you need the custom function of the widget Include Configuration Intent.

The first step in creating an App Extension
Add App Groups in the Target of Widget Extension, and keep the same App Group ID as the main program. If there are no App Groups in the main program, you need to add the App Groups of the main App at this time and define the Group ID.
Create App Extension Step 2
If your developer account is logged in Xcode, then the configuration file and App ID of the application will be correct at this time. If you are not logged into Xcode, you need to go to the Apple Developer Center and manually create the App ID and configuration file for the App Extension. Don't forget to configure App Groups in App ID at this point.

App Groups Data Communication

Because App and App Extension can't communicate directly, when you need to share information, you need to use App Groups to communicate. App Groups have two ways of sharing data, NSUserDefaultsand NSFileManager.

NSUserDefaults shared data

initWithSuiteName:Initialize the instance with NSUserDefaults . suitenamePass in the previously defined App GroupID.

- (instancetype)initWithSuiteName:(NSString *)suitename;

Then you can use NSUserDefaultsthe access method of the instance to store and get the shared data. For example, if we need to share the current user information with the widget, we can do the following.

//使用 Groups ID 初始化一个供 App Groups 使用的 NSUserDefaults 对象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.company.appGroupName"];
//写入数据
[userDefaults setValue:@"123456789" forKey:@"userID"];
//读取数据
NSString *userIDStr = [userDefaults valueForKey:@"userID"];

NSFileManager shared data

Use NSFileManager containerURLForSecurityApplicationGroupIdentifier:to obtain the address of the storage space shared by the App Group, and then perform file access operations.

- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier;

SwiftUI building components

It should be based on considerations such as power consumption. Apple requires that widgets can only use SwiftUI and cannot UIViewRepresentablebe UIKitused through bridging.
The interaction method of the widget is simple, only click, and the view is small. The SwiftUI knowledge required for development is relatively simple, just build a widget view reasonably, and generally speaking, operations such as data binding are not involved.
This chapter mainly introduces how to use SwiftUI to build widgets, and I will assume that readers already have basic knowledge of SwiftUI. If you are still relatively new to SwiftUI, you can improve your understanding through the two video tutorials in the reference materials ( [Fifteen Minutes to Understand SwiftUI] Layout / [Fifteen Minutes to Understand SwiftUI] Style ). You can also refer to the development documentation or related topics of WWDC19/20 to get more knowledge about SwiftUI.

Complete Widget Views with SwiftUI

Let's use a simple development example to help you develop widget views using SwiftUI.
First look at the visual draft of the widget:
Widget Mockups
briefly analyze the view elements in the visual draft:

  1. Cover all the background images ( Image)
  2. Black gradient from bottom to top ( LinearGradient)
  3. Cloud Music Logo ( Image) in the upper right corner
  4. ImageThe calendar icon ( ) in the middle of the widget
  5. Two lines of text below the calendar icon ( Text)

Through the analysis, it is not difficult to find that to achieve the effect of the visual draft, it needs to use Text, Image, and LinearGradientthree components to complete it.
Classify visual elements 1/2/3 as background views for easy reuse by other components. The 4/5 related to the component's content type are then assigned to the foreground view.
Widget View Analysis
First implement the background view:

struct WidgetSmallBackgroundView: View {
    
    
    
    // 底部遮罩的占比为整体高度的 40%
    var contianerRatio : CGFloat = 0.4
    
    // 背景图片
    var backgroundImage : Image = Image("backgroundImageName")
    
    // 从上到下的渐变颜色
    let gradientTopColor = Color(hex:0x000000, alpha: 0)
    let gradientBottomColor = Color(hex:0x000000, alpha: 0.35)
    
    // 遮罩视图 简单封装 使代码更为直观
    func gradientView() -> LinearGradient {
    
    
        return LinearGradient(gradient: Gradient(colors: [gradientTopColor, gradientBottomColor]), startPoint: .top, endPoint: .bottom)
    }
    
    var body: some View {
    
    
        // 使用 GeometryReader 获取小组件的大小
        GeometryReader{
    
     geo in
            // 使用 ZStack 叠放 logo 图标 和 底部遮罩
            ZStack{
    
    
                // 构建 logo 图标, 使用 frame 确定图标大小, 使用 position 定位图标位置
                Image("icon_logo")
                    .resizable()
                    .scaledToFill()
                    .frame(width: 20, height: 20)
                    .position(x: geo.size.width - (20/2) - 10 , y : (20/2) + 10)
                    .ignoresSafeArea(.all)
                // 构建 遮罩视图, 使用 frame 确定遮罩大小, 使用 position 定位遮罩位置
                gradientView()
                    .frame(width: geo.size.width, height: geo.size.height * CGFloat(contianerRatio))
                    .position(x: geo.size.width / 2.0, y: geo.size.height * (1 - CGFloat(contianerRatio / 2.0)))
            }
            .frame(width: geo.size.width, height: geo.size.height)
            // 添加上覆盖底部的背景图片
            .background(backgroundImage
                            .resizable()
                            .scaledToFill()
            )
        }
    }
}

The effect of the background view is as follows:
Widget background view
Next, place the background view in the view of the widget, and realize the icon and text view in the middle, thus completing the visual construction process of the entire component:


struct WidgetSmallView : View {
    
    
    
    // 设置大图标的宽高为小组件高度的 40%
    func bigIconWidgetHeight(viewHeight:CGFloat) -> CGFloat {
    
    
        return viewHeight * 0.4
    }
    
    var body: some View {
    
    
        
        GeometryReader{
    
     geo in
            VStack(alignment: .center, spacing : 2){
    
    
                Image("iconImageName")
                    .resizable()
                    .scaledToFill()
                    .frame(width: bigIconWidgetHeight(viewHeight: geo.size.height), height: bigIconWidgetHeight(viewHeight: geo.size.height))
                
                Text("每日推荐")
                    .foregroundColor(.white)
                    .font(.system(size: 15))
                    .fontWeight(.medium)
                    .lineLimit(1)
                    .frame(height: 21)
                
                Text("为你带来每日惊喜")
                    .foregroundColor(.white)
                    .font(.system(size: 13))
                    .fontWeight(.regular)
                    .opacity(0.8)
                    .lineLimit(1)
                    .frame(height: 18)
            }
            // 增加 padding 使 Text 过长时不会触及小组件边框
            .padding(EdgeInsets(top: 0, leading: 14, bottom: 0, trailing: 14))
            .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
            // 设置背景视图
            .background(WidgetSmallBackgroundView())
        }
    }
}

Through the above simple example, we can find that in the conventional flow layout VStack, HStackthe layout effect can be achieved by using and . And if you want to achieve the effect of the logo icon in the example, you need to use position/offsetto change the positioning coordinates to achieve the goal.

A little supplement about the Link view

Link is a clickable view that will open in the associated application if possible, otherwise in the user's default web browser. Medium/large size widgets can use it to set different jump parameters for the click area. Because the above example is a small-sized component, Link cannot be used to distinguish jumps, so I will add it here.

Link("View Our Terms of Service", destination: URL(string: "https://www.example.com/TOS.html")!)

retrieve data

network request

It can be used in the widget URLSession, so the network request is basically the same as that in the App, so I won't go into details here.
Points to note:

  1. To use a third-party framework, you need to introduce the Target where the widget resides.
  2. A network request is invoked when the Timeline is refreshed.
  3. If you need to share information with App, you need to access it through App Group.

image loading cache

The image cache is different from that in the App. ImageViews in SwiftUI currently do not support passing URLs to load images from the web. It is also impossible to use the method Dataof to complete the loading of network pictures.
You can only get all the network pictures on the Timeline by refreshing the Timeline and calling the network request data.

 func getTimeline(for configuration: Intent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    
    // 发起网络请求
    widgetManager.requestAPI(family : context.family, configuration: configuration) {
    
     widgetResponse, date in
        // 在接口回调中生成 Timeline entry
        let entry = WidgetEntry(date: Date(), configuration: configuration, response: widgetResponse, family : context.family)
        // 解析出 Timeline entry 所需要的网络图片
        let urls = entry.urlsNeedDownload()
        // 查询本地缓存以及下载网络图片
        WidgetImageManager().getImages(urls: urls) {
    
    
            let entries = [entry]
            let timeline = Timeline(entries: entries, policy: .after(date))
            completion(timeline)
        }
    }
}

getImagesIn the method, we need to maintain a queue to sequentially query the local cache and download network pictures when the cache misses.

 public func getImages(urls : [String] , complition : @escaping () -> ()){
    
    
    
    // 创建目录
    WidgetImageManager.createImageSaveDirIfNeeded()
    
    // 去重
    let urlSet = Set(urls)
    let urlArr = Array(urlSet)
    
    self.complition = complition
    
    self.queue = OperationQueue.main
    self.queue?.maxConcurrentOperationCount = 2
    let finishBlock = BlockOperation {
    
    
        self.complition?()
    }
    
    for url in urlArr {
    
    
        let op = SwiftOperation {
    
     finish in
            self.getImage(url: url) {
    
    
                finish(true)
            }
        }
        
        finishBlock.addDependency(op)
        self.queue?.addOperation(op)
    }
    
    self.queue?.addOperation(finishBlock)
}

public func getImage(url : String , complition : @escaping () -> ()) -> Void {
    
    
    let path = WidgetImageManager.pathFromUrl(url: url)
    if FileManager.default.fileExists(atPath: path) {
    
    
        complition()
        return
    }
    
    let safeUrl = WidgetImageManager.filterUrl(url: url)
    WidgetHttpClient.shareInstance.download(url: safeUrl, dstPath: path) {
    
     (result) in
        complition()
    }
}

Data Acquisition for Preview Status

When the user adds a widget, he will see the view of the widget in the preview interface. At this point, the system will trigger placeholderthe method , and we need to return a Timeline in this method to render the preview view.
In order to ensure the user experience, it is necessary to prepare a piece of local data for the interface call to ensure that the user can see the real view on the preview interface, and try not to display the skeleton screen without data.
PreviewStatus

TimeLine

The content changes of widgets all depend on Timeline. Widgets are essentially a series of static views driven by Timeline.

Understanding TimeLine

As mentioned earlier, Timeline is an array TimelineEntrywith elements. ATimelineEntry time object containing a that tells the system when to use this object to create a snapshot of the widget. dateIt can also be inherited TimelineEntryand added to the data model or other information required by the business.
TimeLine
Before generating a new Timeline, the system will always use the last generated Timeline to display data.
If there is only one entry in the Timeline array, the view is immutable. If you need widgets to change over time, you can generate multiple entries in Timeline and give them an appropriate time, and the system will use the entry to drive the view at the specified time.

Reload

The so-called widget refresh actually refreshes the Timeline, which causes the widget view driven by Timeline data to change.
There are two refresh methods:

  1. System reloads
  2. App-driven reloads

System reloads

Timeline refresh initiated by the system. The system determines the frequency of System Reloads for each different Timeline. Refresh requests exceeding the frequency will not take effect. Frequently used widgets can be refreshed more frequently.
ReloadPolicy:
When generating Timeline, we can define a ReloadPolicy to tell the system when to update Timeline. ReloadPolicy has three flavors:

  • atEnd
    • Refresh after all the entries provided by Timeline are displayed, that is to say, the current timeline will not be refreshed as long as there are entries that are not displayed

ReloadPolicyAtEnd

  • after(date)
    • date is the specified next refresh time, and the system will refresh Timeline at this time.

ReloadPolicyAfter

  • never
    • ReloadPolicy will never refresh the Timeline, and the widget will keep the display content of that entry after the last entry is displayed

ReloadPolicyNever

The timing of Timeline Reload is uniformly controlled by the system, and in order to ensure performance, the system will decide whether to refresh Timeline at a certain moment according to the refresh timing required by the APP according to the importance level of each Reload request. Therefore, if you request to refresh the Timeline too frequently, it is likely to be restricted by the system and cannot achieve the ideal refresh effect. In other words, the time to refresh Timeline defined in atEnd, after(date) mentioned above can be regarded as the earliest time to refresh Timeline, and according to the arrangement of the system, these times may be delayed.

App-driven reloads

The refresh of the widget Timeline is triggered by the App. When the App is in the background, background push can trigger reload; when the App is in the foreground, WidgetCenter can actively trigger reload.
App-driven Reloads
Calling WidgetCenter can refresh some widgets or all widgets according to kindthe identifier .

/// Reloads the timelines for all widgets of a particular kind.
/// - Parameter kind: A string that identifies the widget and matches the
///   value you used when you created the widget's configuration.
public func reloadTimelines(ofKind kind: String)
/// Reloads the timelines for all configured widgets belonging to the
/// containing app.
public func reloadAllTimelines()

Click to land

When the user clicks on the content or function entry on the widget, it is necessary to correctly respond to the user's needs after opening the App, and present the corresponding content or function to the user.
This needs to be done in two parts. First, define different parameters for different click areas in the widget, and then present different interfaces openURL:in according to different parameters.

Distinguish between different click areas

If you want to define different parameters for different regions, you need to combine widgetURL and Link.

widgetURL

The scope of widgetURL is the entire widget, and there can only be one widgetURL on a widget. Additional widgetURL parameters will not take effect.
widgetURL
code show as below:

struct WidgetLargeView : View {
    
    
    var body: some View {
    
    
        GeometryReader{
    
     geo in
            WidgetLargeTopView()
            ...
        }
        .widgetURL(URL(string: "jump://Large")!)
    }
}

Link

The Link scope is the actual size of the Link component. Multiple Links can be added, and there is no limit to the number. It should be noted that the Link API cannot be used under the systemSmall type of the widget.
Link
code show as below:

struct WidgetLargeView : View {
    
    
    var body: some View {
    
    
        GeometryReader{
    
     geo in
            WidgetLargeTopView()
            Link(destination: URL(string: "自定义的Scheme://Unit")!) {
    
    
                WidgetLargeUnitView()
            }
            ...
        }
        .widgetURL(URL(string: "自定义的Scheme://Large")!)
    }
}

URL Schemes

URL Schemes are the bridge for widgets to jump to App, and also the channel for jumping between Apps. The average developer should be familiar with it.
Registering a custom URL Scheme is very simple, set it through info.plist--> URL Types--> item0--> URL Schemes--> 自定义的Scheme.
Afterwards, in the widget, you can open your own App through 自定义的Scheme://the spliced ​​URL object, and ://you can add parameters after to indicate the desired function or content.
Note: When adding parameters, the Chinese characters that appear must be escaped. Here you can use NSURLComponentsand NSURLQueryItemto concatenate the jump URL string. It has its own escape effect and the operation URL is more standardized.

NSURLComponents *components = [NSURLComponents componentsWithString:@"自定义的Scheme://"];
NSMutableArray<NSURLQueryItem *> *queryItems = @[].mutableCopy;
NSURLQueryItem *aItem = [NSURLQueryItem queryItemWithName:@"a" value:@"参数a"];
[queryItems addObject:aItem];
NSURLQueryItem *bItem = [NSURLQueryItem queryItemWithName:@"b" value:@"参数b"];
[queryItems addObject:bItem];
components.queryItems = queryItems;
NSURL *url = components.URL;

Processing after launching the app

After clicking the widget to jump to the App, the openURL method of AppDelegate will be triggered.

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options

In the openURL method, by parsing the url parameters, the function jump or content display required by the user is clarified, and then the corresponding implementation is carried out. This puts forward certain requirements for the routing capability of the project, and because it has little connection with the development of small components, it will not be described in detail.

Dynamically configure widgets

Widgets allow users to configure custom data without opening the application. Using Intentsthe framework , you can define the configuration page that users see when editing widgets.
The definition of words used here instead of drawing is because the configuration data can only be generated Intentsthrough , and the system will build the configuration page based on the generated data.
ConfigurationWidget

Build a simple custom function

Building a simple custom function requires two steps:

  1. Create and configure an IntentDefinition file
  2. Modify the related parameters of Widget to support ConfigurationIntent.

1. Create and configure IntentDefinition file

If you checked it when creating the widget Target Include Configuration Intent, Xcode will automatically generate IntentDefinitionthe file .
If Include Configuration Intentthe option , then you need to add IntentDefinitionthe file manually.
Menu File-> New-> ThenFile find and add it to the widget Target. After the file is created, open the file to configure it. First, you need to remember the class name in the Custom Class on the left. Xcode will automatically generate a ConfigurationIntent class after compilation according to this name, which stores user configuration information. Of course, you can also fill in a class name you specify here. It should be noted that this class will not be generated until the project is compiled. Then we need to create a custom parameter template, click the sign below to create a parameter. After that, you can define the Type of the created Parameter. In addition to the relatively intuitive system type, there are two more difficult to understand Enums and Types columns. System types Specific types have further customization options to customize the input UI. For example, the Decimal type can choose to use the input box (Number Field) input or the slider (Slider) input, and can customize the upper and lower limits of the input; the Duration type can customize the input value unit as seconds, minutes or hours; Date Components can specify Enter a date or time, specify the format of the date, and more. The simple understanding of Enum is that Enums are written to deathSiri Intent Definition File
IntentDefinition1
.intentdefinition
IntentDefinition2

Parameter+

IntentDefinition3


IntentDefinitionSystemType

.intentdefinitionThe static configuration in the file can only be updated after the release.
Type
Types are much more flexible and can be dynamically generated at runtime. Generally speaking, we use Types to make custom options.
IntentDefinitionType
Support for inputting multiple values
​​Most types of parameters support inputting multiple values, that is, inputting an array. At the same time, it supports to limit the fixed length of the array according to different Widget sizes.
IntentDefinitionFixedSize
Controlling the Display Conditions of Configuration Items
You can control a configuration item to display only when another configuration item contains any/specific value. As shown in the figure below, the Up Next Widget of the Calendar App will only display the Calendars configuration item when the Mirror Calendar App option is not selected.
IntentDefinitionParametersControl1
In the Intent definition file, set a certain parameter A to another parameter B Parent Parameter, so that the display of parameter B depends on the value of parameter A.
For example, in the image below, calendarthe parameter is mirrorCalendarApponly falsedisplayed when the value of the parameter is :
IntentDefinitionParametersControl2

2. Modify the relevant parameters of the Widget to support ConfigurationIntent

Replace in the Widget class StaticConfigurationwithIntentConfiguration
old:

@main
struct MyWidget: Widget {
    
    
    let kind: String = "MyWidget"
    var body: some WidgetConfiguration {
    
    
        StaticConfiguration(kind: kind, provider: Provider()) {
    
     entry in
            MyWidgetEntryView(entry: entry)
        }
    }
}

new:

@main
struct MyWidget: Widget {
    
    
    let kind: String = "MyWidget"
    var body: some WidgetConfiguration {
    
    
        IntentConfiguration(kind: kind, intent: WidgetConfiguratIntent.self, provider: Provider()) {
    
     entry in
            MyWidgetEntryView(entry: entry)
        }
    }
}

Add the ConfigurationIntent parameter in the Timeline Entry class.
The code is as follows:

struct SimpleEntry: TimelineEntry {
    
    
 		let date: Date
		let configuration: WidgetConfiguratIntent
}

Change the inheritance of IntentTimelineProvider
Provider to IntentTimelineProvider, and Intentadd the type alias of .
old:

struct Provider: TimelineProvider {
    
    
 		...
}

new:

struct Provider: IntentTimelineProvider {
    
    
 		typealias Intent = WidgetConfiguratIntent
 		...
}

Modify the input parameters of getSnapshot / getTimeline in turn to add support for customization. And when creating Timeline Entry, pass in the configuration.

Build custom portals using interface data

In IntentTarget, find IntentHandlerthe file and follow ConfiguratIntentHandlingthe protocol in the ConfigurationIntent generated class. A method
that implements the protocol requirements . In this method, we can call the interface to obtain custom data and generate the data source input parameters required by the block.provideModeArrOptionsCollectionForConfiguration:withCompletion:
completion

- (void)provideModeArrOptionsCollectionForConfiguration:(WidgetConfiguratIntent *)intent withCompletion:(void (^)(INObjectCollection<NMWidgetModel *> * _Nullable modeArrOptionsCollection, NSError * _Nullable error))completion {
    
    
 
 [self apiRequest:(NSDictionary *result){
    
    
 // 处理获取到的数据
 ....
 NSMutableArray *allModelArr = ....;
 // 生成配置所需要的数据
 INObjectCollection *collection = [[INObjectCollection alloc] initWithItems:allModeArr];
 completion(collection,nil);
 }];
}

Widget gets custom parameters

When the widget generates a view based on the Timeline Entry, read the configuration attribute of the Entry to obtain whether the user defines the attribute and the detailed value of the attribute.

Summarize

Both advantages and disadvantages

WidgetKitWorks
Widgets are a thing with obvious advantages and disadvantages. It is really convenient to click and use on the desktop, but the lack of interactive methods and the inability to update data in real time are very big flaws. As Apple said: "Widgets are not mini-apps", don't use the thinking of developing App to make widgets. Widgets are just static views driven by a series of data.

Advantage:

  1. Standing on the desktop greatly increases the exposure of the product.
  2. Utilizing a web interface and data sharing, it is possible to present personalized content relevant to the user.
  3. Shortened access paths to functions. One click can let the user reach the desired function.
  4. It can be added repeatedly, with custom and recommendation algorithms, adding multiple widgets with different styles and data.
  5. Custom configuration is simple.
  6. Multiple sizes, large size can carry complex content display.

shortcoming:

  1. Data cannot be updated in real time.
  2. Only click to interact.
  3. The background of the widget cannot be set transparent.
  4. Moving images (video/motion graphics) cannot be displayed.

Tail

The development practice of small components has come to an end. It can be seen that although the components are small, they still need a lot of knowledge. Including Timeline, Intents, SwiftUI and other frameworks and concepts that are difficult to come into contact with in normal development, you need to understand and learn.
The weak interactive ability and data refresh mechanism of widgets are its flaws. Apple is very restrained about the ability of small components. During development, many ideas and requirements are limited by framework capabilities and cannot be realized. I hope that Apple can open up new capabilities in subsequent iterations. For example, the support part does not need to start the interactive form of the App.
But the flaws do not hide the advantages, showing users the content they like or providing the function entrance that users want, and amplifying the advantages of widgets is the correct development method of widgets at present.

References

Guess you like

Origin blog.csdn.net/qq_14920635/article/details/122843064