What is a Widget

According to the official documentation, widgets are App extensions that are located in the “Today” view. Widgets allow users to quickly access current and important information. Users tend to open the Today view often and expect the information they are interested in to be immediately available. The user can also set whether to allow the “Today” view to appear on the lock screen.

However, it is worth noting that widgets are supposed to provide quick updates and easy tasks, and that using widgets is not a good choice for multi-step and heavy tasks.

First look at the results:



As shown in the figure, Wiget has two display states — expanded and collapsed. It is worth noting that in the collapsed state, the height of the widgets is controlled by the system. All the widgets are fixed in height, which does not take effect. The AppIcon button on the left and the expand and fold button on the right at the top of each widget are not customizable (the right button is a right arrow or a down arrow, but the words “expand” and “fold” on the lower version). The only thing that can be changed is the display name, which is the same as the main App. You can modify the “Bundle display name” in the corresponding TARGETS info.plist to complete the setting, but it is generally consistent with the main App.

Widget implementation

Widget creation is simple, Xcode->File->New->Target->Today Extension as shown below:

Create a new folder:

The Deployment version of the widget created by default is high and needs to be changed under the target of the corresponding widget:

Okay, so this TodayViewController is our main battleground, and like any ViewController, it has viewDidLoad(), viewWillAppear(_ Animated: Bool) and so on. The default widget is put the style you need to set up widgetLargestAvailableDisplayMode pack up and their agent method will have a state, but can’t code control or fold, only is the user click on the button to expand and collapse

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.extensionContext? . WidgetLargestAvailableDisplayMode =. Expanded} @ the available (iOSApplicationExtension 10.0, *) func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { if activeDisplayMode == .compact { self.preferredContentSize = CGSize.init(width: UIScreen. Main. Bounds. Size. Width, height: 140) / / here by the system control, set the effect not} else {self. PreferredContentSize = CGSize. Init (width: UIScreen.main.bounds.size.width, height: cellHeight * CGFloat(viewModel.entityCount) + 35) } }Copy the code

We can prepare some initial necessary data in viewDidLoad, like synchronizing necessary data from the main App, and usually when we go to the “Today” view, we want to update the data, so we load the network data in viewWillAppear, and because of the nature of our application, we have a high requirement for live rows of data, So I will open a timer polling interface in viewWillAppear to refresh the data (polling interval is set by the user in the main App), so that the user can update the data even if it stays on this page all the time. The viewWillDisappear timer is cancelled.

override func viewWillAppear(_ animated: Bool) {super.viewwillAppear (animated) if #available(iOSApplicationExtension 10.0, *) {self.extensionContext? .widgetLargestAvailableDisplayMode = .expanded timer = Timer.init(timeInterval: TimeInterval(reloadTime), repeats: true, block: { [weak self] (_) in self? .lodaData() }) } else { lodaData() } if let timer = timer { RunLoop.current.add(timer, forMode: .common) } timer?.fire() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) timer?.invalidate() timer = nil }Copy the code

Third party library use

It is possible to use some third-party libraries (or your own private libraries) to implement widgets. How do you import third-party libraries into widgets? Almost like a normal project, our widget is a target that we create, so all we need to do is declare it in our podFile

It is important to note that the version number (or commitID) of the libraries used by both the main project and Widge must be the same or an error will be reported

4. Data synchronization

A Widget is an extension to an App, but it is also a standalone Widget, so it has the following features:

  • Widgets are stored separately from the main App and therefore require a set of certificates. The Widget’s BundleID is appended to the main App’s BundleID

  • You can’t share data with your main App directly, but only through App Groups, so check “App Groups” in Capabilities

There are two ways to synchronize data: NSUserDefaults and NSFileManager. Let’s take the first one as an example. Since I’m synchronizing a little bit more data, I created a Manager to synchronize data, and I tried to minimize the use of hard-coded keys. Create each OpenDataManger class:

Enum Key enum UesrDefaultKey: String { case todayWidgetData = "kTodayWidgetData" case currentLagalKey = "kCurrentLagalKey" case baseUrl = "kBaseUrl" Case widgetReloadTime = "kWidgetReloadTime"} /// Open data class OpenDataManager {static let appGroupKey = "group.xxxx.xxxx" static func setUserDafaultValue(jsonString: String, key: UesrDefaultKey) { let shareDefault = UserDefaults.init(suiteName: appGroupKey) shareDefault? .set(jsonString, forKey: key.rawValue) shareDefault? .synchronize() } static func getUserDafaultValue(key: UesrDefaultKey) -> String { guard let shareDefault = UserDefaults.init(suiteName: appGroupKey) else { return "" } guard let resString = shareDefault.object(forKey: key.rawValue) as? String else { return "" } return resString } }Copy the code

It is worth mentioning here:

  1. Because the OpenDataManger needs to be used in both the main project and the Widget, you need to check both targets for Target Membership.
  2. The UserDefaults here need to use the App Groups that specify the group key, not the usual onesUserDefaults.standardThat is:let shareDefault = UserDefaults.init(suiteName: appGroupKey)

Use the following (to get the requested underlying domain name as an example) :

Main project storage:

	let baseUrl = NetGuardMannager.shared.hostAPI
	OpenDataManager.setUserDafaultValue(jsonString: baseUrl, key: .baseUrl)
Copy the code

Used in widgets:

	let baseUrl = OpenDataManager.getUserDafaultValue(key: .baseUrl)
Copy the code

5. Invoke the main App

Since widgets and the main App are completely independent, they cannot communicate directly with each other. OpenURL is needed to evoke the main App. Since our App used to interact with H5 a lot, we have a special routing protocol to do this.

if let url = URL.init(string: "xxx:///market? params=xxx&fromWidget=true") { self.extensionContext? .open(url, completionHandler: nil) }Copy the code

If you don’t have a Router, use Targets->WidgetDemo-> Info->Url Types to add Url Schemes TodayWidget. Then implement the jump in the AppDelegate:

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {if url.scheme == "TodayWidget" {// Handle the jump event retuen True} return false}Copy the code

Six, possible problems

  1. The first step is to see if the Capabilities of the project is enabled, then check for groupKey errors, and then check for UserDefaults keys.

  2. Since we are a dynamic tableView, the expansion height is calculated according to the data source. The previous method was calculated according to the number returned by the interface, but the problem is that the request is returned asynchronously, and the switch will call the height proxy function first, resulting in the problem of height flicker and expansion height is not low.

    The solution is: local storage of a number of records how many data, using local data to calculate the height

  3. The network request failure was discovered during the integration test. Packets were captured and no interface was invoked, and no data was found in the widget. After a check, the version of the private repository was incorrect, resulting in a data synchronization failure.

  4. I haven’t had a problem with the widget showing “not loading”, but many of my friends have reported that the widget crashes or is using too much memory.

  5. Apple introduced dark mode for iOS13; After setting the dark mode, the overall system color will be dimmed, resulting in unclear display of small parts. Here, we need to obtain the current display mode of the system before the display, and set different font colors according to different systems when refreshing the UI