IOS14 Widget development pit (1) corrected – first look and refresh

preface

Reprinted: write a program of lemon extract original text

After a month of development on the Widget, several issues have been resolved and this article has been reedited to correct previous errors and adapt to the latest version.

Updated 15 October 2020 with a new understanding of refreshes and views, updated the refreshes section.

Here is a record of some of the pits I encountered in the process of development, I hope to be useful to the development. The code mentioned in this article is only sample code, which provides ideas and cannot be copied directly. Some knowledge of developing Today Widgets is required for comparison. Some content of this article is quoted from the network, if there is infringement, please contact to delete.

Development of guidelines

  1. WidgetExtension uses a new WidgetKit that is different from the Today Widget,It can only be developed using SwiftUI, so need SwiftUI and Swift foundation.
  2. Widgets support only 3 sizes systemSmall (2×2), systemMedium (4X2), and systemLarge (4×4)
  3. By default, tap the Widget to open the main application
  4. A Widget, like a TodayWidget, is a standalone application that needs to be set to App Groups in the project to communicate with the main application, which will be covered later.
  5. Apple has officially deprecated Today Extension, Xcode12 no longer provides the addition of Today Extension, and apps that already have Today widgets will be displayed in a specific area.

The preparatory work

The deployment environment

Widget development requires installation of Xcode 12 and iOS 14. Apple official download link

Create a project

Normal project creation process, I use Swift language, interface Storyboard, can be set to their own accustomed configuration, Create a new Xcode project -> fill in Product Name-> Next-> Create

As an ios developer, it is particularly important to have a learning atmosphere and a communication circle when encountering problems, which is of great help to me. Here is one of my ios communication groups: 711315161, into the group password iOS share BAT, Ali interview questions, interview experience, discuss technology, we exchange learning growth together! Hope to help developers avoid detours.

Introducing the 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 name of the project, nor can it be named as “Widget” (because the Widget is an existing class name). When deleting the Widget, you cannot delete only the file but also delete it in the Targets of the project. Using the name that has already been deleted will cause an error that the file cannot be found.
  3. If the Widget supports user Configuration attributes (for example, weather components, where the user can select a city), the Include Configuration Intent option needs to be enabled. I suggest you check it. Who knows if you’ll ask for support later.
  4. Once created, it automatically generates five structs and its own methods

Begin to write

To know the code

Preview view -Previews

The preview view of code running is a new SwiftUI feature that displays the results on the right side of the Widget and supports hot updates, but it is cumbersome. It is not a required part of the Widget and can be deleted or commented out.

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

Copy the code

Data Provider -Provider

The Provider is the most important part of the Widget, it determines the team a placeholder/getSnapshot/getTimeline these three data show. If the Include Configuration Intent is selected during project creation, the Provider inherits from IntentTimelineProvider and supports user editing. If the Include Configuration Intent is not selected, the Provider inherits from TimelineProvider and does not support user editing. We’ll talk about that 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) } }Copy the code

The getSnapshot method provides the user with a preview of what the component looks like and what data is displayed. It can be written as “fake information”, which is what this screen looks like.

The getTimeline method is the refresh event of the Widget when it is displayed on the desktop, and returns a Timeline instance that contains all the items to be displayed: the expected time (the date of the item) and the time when the Timeline is “expired.” Because a Widget can’t “predict” its future state the way a weather app can, it can only tell it what data to display at what time in the form of a timeline.

Data model -SimpleEntry

Model of Widget, where Date is the property of TimelineEntry and is the time when the data is displayed. It cannot be deleted, but you need to add custom attributes to it:

struct SimpleEntry: TimelineEntry {
    public let date: Date
    xxxxx
}

Copy the code

Interface – MainWidgetEntryView

The View displayed by the Widget on which you can edit the interface and display data, or you can customize the View and call it from there.

struct MainWidgetEntryView : View {
    var entry: Provider.Entry
    var body: some View {
		xxxxxx
	}
}

Copy the code

Entry – MainWidget

The main entry function of a Widget that sets the title and description of the Widget, and specifies the View, Provider, and supported size to be displayed.

@main struct MainWidget: Widget {let kind: String = "MainWidget"// identifier, cannot be repeated with other Widgets, preferably with the name of the current Widgets. var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in MainWidgetEntryView(entry: Entry)}. ConfigurationDisplayName (" My widgets "). / / the name of the Widget displays the description (" This is an example Widget. "), / / the description of the Widget}}Copy the code

In the pit of

GetTimeline is the first pit, iOS14 Widgets can’t actively update data!! The Today widget is able to retrieve the latest data, directly controlled by the application, but iOS 14 widget is not. The system will only ask the widget for a series of data and display the obtained data according to the current time. Since the code does not run actively, it tends to be more static displays of information, and even animations and videos are banned. This means that we can only write in advance what data should be displayed at the next time for the team, and make a timeline for the system to read the display. However, we can automatically request and refresh data by doing normal data requests and populations in the form of closures. This refreshing approach was so different from my normal development thinking that I made a lot of mistakes.

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) }Copy the code

The official example code says: display the time for each hour of the five hours from now, and then re-run getTimeline. Only after you understand the meaning of this method can you write the effect you want. To control the refresh time, you only need to control the number of Calendar.Component Value and entries and set the TimeLine policy. However, according to my test, the maximum refresh frequency of getTimeline is once every 5 minutes, and it will not work if the refresh frequency is higher than this. When we populate entries, we should populate them with data that needs to be displayed within 5 minutes.

Example: to implement a refresh clock per second, in order to refresh the clock as accurately as possible, you should provide entries with 300 time data of 300 seconds from 0 to 299, which can be converted into a string display specific to seconds during View display, and obtain 5 minutes of time data again after running a cycle. As a result, the second display will have a certain deviation of 1 ~ 3s, and the high frequency refresh will also lead to an increase in power consumption.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {var currentDate = Date() // Refresh every 5 minutes let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)! var arr:[SimpleEntry] = [] var tempDate = Date() for idx in 0... 300 { tempDate = Calendar.current.date(byAdding: .second, value: idx, to: currentDate)! let tempEntry = SimpleEntry(date: tempDate) arr.append(tempEntry) } let timeline = Timeline(entries: arr, policy: .after(refreshDate)) completion(timeline) }Copy the code

Main program refresh and second pit

In the main application, we can use WidgetCenter provided by WidgetKit to manage the widgets, reloadTimelines to force a refresh of the widgets we specify, or reloadAllTimelines to refresh all the widgets.

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

Copy the code

If your main program is written by Oojective -c, you’ll need to use OC to call Swift, as described in the reference documentation, because WidgetKit didn’t write the 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
    }
}

Copy the code

In the code above

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

Copy the code

and

@available(iOS 14, *)

Copy the code

That’s the second hole. First, this judgment is added because the Widget will only work if one of the three conditions is met, and it will fail to be packaged without this judgment. This solution was found in the problem feedback from Apple Developer.

Second, WidgetKit was new to iOS 14, and since our project had to be rolled down to iOS 10, we had to build and package it.

reference

I am a novice, if there is a wrong place welcome correction, looking forward to exchange and development with you, I suggest reading the official description of the document before looking for the relevant network information.

“Creating a Widget Extension” “Keeping a Widget Up To Date” “iOS14 widgets from a developer’s perspective” “iOS14WidgetKit development Practice 1-4” “iOS14 Widgets “How to create Widgets in iOS 14 in Swift” “Swiftui-Text” “Mixed OC calling Swift”