iOS14 Widget Development Stepping Pit (1) Revised Version - First Acquaintance and Refresh

foreword

Revised version on December 23, 2020, revised some descriptions and errors

Here are some pitfalls I encountered during the development process, hoping to be useful for development. The codes involved in this article are just sample codes, which only provide ideas and cannot be copied and used directly. Some knowledge of developing Today Widget is required for comparison. Part of the content of this article is quoted from the Internet. If there is any infringement, please contact to delete it.

Development Notes

  1. WidgetExtension uses the new WidgetKit which is different from Today Widget,It can only be developed using SwiftUI, so SwiftUI and Swift foundation are required.
  2. Widget only supports 3 sizes systemSmall (2x2), systemMedium (4x2), systemLarge (4x4)
  3. Click on the Widget to open the main application by default
  4. Widget is similar to TodayWidget, which is an independently running program. It needs to set App Groups in the project to make it communicate with the main program. This will be discussed later.
  5. Apple has officially abandoned the Today Extension, and Xcode12 no longer provides the addition of the Today Extension. Applications that already have a Today Widget will be displayed in a specific area for display.

Preparation

deployment environment

Widget development requires Xcode 12 and iOS 14 to be installed. Apple official download link

create project

The normal process of creating a project, I use the Swift language, the interface Storyboard, can be set to the configuration you are used to,
Create a new Xcode project -> Fill in Product Name-> Next-> Create

Introduce Widget Extension

  1. File -> New -> target-> Widget Extension ->Next
  2. Since a new Target is added, the name of the Widget cannot be the same as the project name, nor can it be called "Widget" (because Widget is an existing class name). When deleting, you can't just delete the file but also in the Targets of the project. Delete, the name that has been deleted once will report an error that the file cannot be found.
  3. If the Widget supports user configuration attributes (for example, the weather component, the user can select a city), you need to check the Include Configuration Intent option, and do not check it if it does not support it. It is recommended to check it, who knows if it will ask for support in the future.
  4. After creation, 5 structs and their own methods will be automatically generated

start writing

know the code

Preview view-Previews

The preview view of code running is a new feature of SwiftUI. It will display the running results on the right view and support hot update, which is convenient for developers to debug and use SwiftUI views. It is not a necessary part of Widget and can be deleted or commented directly.

struct MainWidget_Previews: PreviewProvider {
    
    
    static var previews: some View {
    
    
        MainWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Data Provider-Provider

Provider is the most important part of Widget, which determines the display of the three data of placeholder/getSnapshot/getTimeline of the widget. If Include Configuration Intent is checked when the project is created, the Provider inherits from IntentTimelineProvider to support user configuration data, and if it is not checked, it inherits from TimelineProvider and does not support user configuration data. This will be discussed later.


struct Provider: TimelineProvider {
    
    
    func placeholder(in context: Context) -> SimpleEntry {
    
    
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    
    
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
    
    
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

getSnapshot 方法是提供一个预览数据,可以让用户看到该组件的一个大致情况,是长什么样、显示什么数据的,可以写成固定数据,国外的文章里叫它
“fake information” ,就是这个界面显示的样子:(以iWidget为例子)

Where the snapshot is displayed

getTimeline 方法就是Widget在桌面显示时的刷新事件,返回的是一个Timeline实例,其中包含要显示的所有条目:预期显示的时间(条目的日期)以及时间轴“过期”的时间。
因为Widget程序无法像天气应用程序那样“预测”它的未来状态,因此只能用时间轴的形式告诉它什么时间显示什么数据。

数据模型-SimpleEntry

Widget的Model,其中的Date是TimelineEntry的属性,是保存的是显示数据的时间,不可删除,需要自定义属性在它下面添加即可:

struct SimpleEntry: TimelineEntry {
    
    
    let date: Date
    xxxxx
}

界面-MainWidgetEntryView

Widget显示的View,在这个View上编辑界面,显示数据,也可以自定义View之后在这里调用。而且,一个Widget是可以直接支持3个尺寸的界面的。

struct MainWidgetEntryView : View {
    
    
	@Environment(\.widgetFamily) var family
    var entry: Provider.Entry
    var body: some View {
    
    
		switch family {
    
    
        case .systemSmall: Text("小尺寸界面")
        case .systemMedium: Text("中尺寸界面")
        default: Text("大尺寸界面")
        }
	}
}

入口-MainWidget

Widget 的主入口函数,可以设置Widget的标题和说明,规定其显示的View、Provider、支持的尺寸等信息。

@main
struct MainWidget: Widget {
    
    
    let kind: String = "MainWidget"// 标识符,不能和其他Widget重复,最好就是使用当前Widgets的名字。

    var body: some WidgetConfiguration {
    
    
        StaticConfiguration(kind: kind, provider: Provider()) {
    
     entry in
            MainWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")//Widget显示的名字
        .description("This is an example widget.")//Widget的描述
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

遇到的坑

getTimeline 就是第一个坑,iOS14 Widget是无法主动更新数据的!!!
Today小组件是可以主动获取最新的数据,由程序直接控制,但 iOS 14 的小组件却不是,系统只会向小组件询问一系列的数据,并根据当前的时间将获取到的数据展示出来。由于代码不是主动运行的,这使它更偏向于静态的Displays of information, even animations and videos are prohibited.
This means that we can only write in advance what data should be displayed for the widget at the next time, and make it into a timeline for the system to read and display. But we can automatically request and refresh data by performing normal data request and filling in the form of closure. This refresh method is quite different from my usual development thinking, which caused me to make many mistakes.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    
        var entries: [SimpleEntry] = []
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
    
    
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

The official sample code means: display the time of each hour of 5 hours from now, and then run getTimeline again after the display. After understanding the meaning of this method, you can write the effect you want. Therefore, we only need to control the value of the Calendar.Component
of the refresh time and the number of elements in the entries, and set the policy of TimeLine to control the refresh time, times and methods of the Widget. But after my test, the highest refresh frequency of getTimeline is once every 5 minutes , and it will not work if it is higher than this frequency. When filling entries , we should fill them with the data that needs to be displayed within 5 minutes.

Example: To implement a clock that is refreshed every second, in order to refresh as accurately as possible every second, 300 time data for 300 seconds should be provided to the entries . When the View is displayed, it can be converted into a string that is specific to the second. Run a Time data is acquired again for 5 minutes after the period.
This leads to a certain deviation of 1-3 seconds in the second display, and high-frequency refresh will also lead to an increase in power consumption, and it will occasionally stop. At present (December 23, 2020), no suitable solution has been found. If so, please chat with me privately! ! ! !

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    
	var currentDate = Date()
	var arr:[SimpleEntry] = []
		for idx in 0...300 {
    
    
			let tempDate = Calendar.current.date(byAdding: .second, value: idx, to: currentDate)!
			let tempEntry = SimpleEntry(date: tempDate)
			arr.append(tempEntry)
		}
		let timeline = Timeline(entries: arr, policy: .atEnd)
		completion(timeline)
    }

Main program refresh and second pit

In the main program, we can use WidgetCenter provided by WidgetKit to manage widgets, among which reloadTimelines is used to force a refresh of the widget we specify, or reloadAllTimelines to refresh all widgets.

WidgetCenter.shared.reloadTimelines(ofKind: "xxx")
WidgetCenter.shared.reloadAllTimelines()

If your main program is written in Oojective-C , then you need to use OC to call Swift to write it. For details on the mixed configuration method, please refer to the reference document "Mixed oc calls swift" , because WidgetKit does not write an OC version.

import WidgetKit
@objcMembers class WidgetTool: NSObject {
    
    
    @available(iOS 14, *)
    @objc func refreshWidget(sizeType: NSInteger) {
    
    
        #if arch(arm64) || arch(i386) || arch(x86_64)
        WidgetCenter.shared.reloadTimelines(ofKind: "xxx")
        #endif
    }
}

in the code above

 #if arch(arm64) || arch(i386) || arch(x86_64)
        xxxx
 #endif

and

@available(iOS 14, *)

It is the second pit.
First, this judgment is added because the Widget can only run when one of these three conditions is satisfied. If this sentence is not added, an error will be reported when packaging. This solution is found from the problem feedback of Apple Developer .

Second, WidgetKit is newly released in iOS 14 , because our project needs to support down to iOS 10 , so we need to add version judgment to compile and package.

references

I am a novice, please correct me if I make any mistakes. I look forward to communicating and developing with you. It is recommended to read the official documentation before looking for relevant network information.

"Creating a Widget Extension"
"Keeping a Widget Up To Date"
"A developer's perspective on iOS 14 widgets"
"iOS14WidgetKit development practice 1-4"
"iOS14 Widget development and handling of error-prone areas"
"How to create Widgets in iOS 14 in Swift"
"SwiftUI-Text"
"Mixed oc calling swift"

Guess you like

Origin blog.csdn.net/qq_38718912/article/details/107658804