Since iOS8, apple has supported Extension development, which allows developers to provide additional functionality for system-specific services through Extension points provided to us by the system.

But after iOS14, apple updated the Extension and introduced a new UI component: WidgetKit instead of the Today Extension for iOS14 and later

Development of guidelines

  • WidgetExtension uses the new WidgetKit, unlike the Today Widget, which can only be developed using SwiftUI, so it requires SwiftUI and Swift basics
  • Widgets support only 3 sizes systemSmall (2×2), systemMedium (4X2), and systemLarge (4×4)
  • By default, tap the Widget to open the main application
  • Like the Today Widget, a Widget is a standalone application that needs to be set to App Groups in the project to communicate with the main application
  • 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

Official Instructions for Widgets


The Widget implementation

1. Create add Widget Extension

File -> New -> Target -> Widget Extension

Include Configuration Intent

Check this if you are creating a Widget that needs to support user-defined configuration properties (for example, weather components, users can select cities; Notepad component, user record information, etc.), do not check if it is not supported

This paper is mainly based onThis optionDescription of user configuration properties

2.Widget file function parsing

Provider

A structure that provides all the necessary information for Widget display, complies with the TimelineProvider protocol, and generates a timeline that tells WidgetKit when to render and refresh widgets. The timeline contains a custom TimelineEntry type that you define. The timeline target identifies the date you want WidgetKit to update the Widget content. The view that contains your Widget in a custom type needs to render properties.

struct Provider: TimelineProvider {
    // placeholder view
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date()}// This method is called when the edit screen selects Add Widget in the upper left corner for the first display
    func getSnapshot(in context: Context.completion: @escaping (SimpleEntry) - > ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }
    // Preprocess data into 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(a)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
  • Placeholder: Provide a default view that will be displayed when a network request fails, an unknown error occurs, and the widget is displayed for the first time

  • GetSnapshot: To display widgets in the widget library, WidgetKit requires the provider to provide a preview snapshot that can be seen on the component’s Add page

  • GetTimeline: Within this method, network requests can be made, and the data received is saved in the corresponding entry. After completion is called, the widget will be refreshed

    • Parameter policy: refresh time

    . Never: not refreshed

    AtEnd: The Timeline is automatically refreshed after the last Entry is displayed. The Timeline method is re-invoked

    . After (date) : automatically refreshes after a specified time is reached

    • !!!!!!!!! Widget refresh time is determined by the unified system, if need to refresh the compulsory Widget that can be used in the App WidgetCenter to reload all time line: WidgetCenter. Shared. ReloadAllTimelines ()

Timeline’s refresh policy is deferred and may not always be based on the exact time you set. Each widget widget is limited to the number of refreshes it can receive per day

Entry

The data model required to render the Widget complies with the TimelineEntry protocol.

struct SimpleEntry: TimelineEntry {
    let date: Date
}
Copy the code

EntryView

You can set different views for widgets of different sizes for the content displayed on the screen.

struct NowWidgetEntryView : View {
    var entry: Provider.Entry
    // Set different views for different sizes of widgets
    @Environment(\.widgetFamily) var family // Size environment variable

    @ViewBuilder
    var body: some View {
        switch family {
        case .systemSmall:
            / / small size
            Text(entry.date, style: .time)
        case .systemMedium:
            / / size
        default:
            / / big size}}}Copy the code

@ the main main entrance

@main
struct NowWidget: Widget {
    let kind: String = "NowWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            NowWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        //.supportedFamilies([.systemSmall,.systemMedium])}}Copy the code
  • @main: represents the main entrance to the Widget from which the system loads and can be used for multiple Widget implementations
  • kind: is the unique identifier of the Widget
  • WidgetConfiguration : Initializes configuration code
    • StaticConfiguration: Can be parsed without any input from the user, and data can be retrieved from the Widget’s App and sent to the Widget
    • IntentConfiguration:This applies to widgets that have user-configurable properties

Siri Intents, which rely on apps, automatically receive these intents and use them to update widgets for building dynamic widgets

  • configurationDisplayName: Adds the title of the edit screen display
  • description: Adds the description displayed on the editing interface
  • supportedFamilies: Sets the size of the controls supported by the Widget. If this parameter is not set, all three styles are implemented by default
Widget control size
Screen Size (Portrait) Small widget Medium widget Large widget
414×896 pt 169×169 pt 360×169 pt 360×376 pt
375×812 pt 155×155 pt 329×155 pt 329×345 pt
414×736 pt 159×159 pt 348×159 pt 348×357 pt
375×667 pt 148×148 pt 322×148 pt 322×324 pt
320×568 pt 141×141 pt 291×141 pt 291×299 pt

3. Implementation of multi-widget components

A Widget can only be implemented in the form of three components of different sizes. If there are existing requirements for different functions and components of the same size, multiple components need to be implemented

1. Add more configuration to support multiple widgets by modifying the original Widget entry file method

@main
struct NowSwiftWidget: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        NowPosterWidget(a)NowRecommendWidget(a)NowDailyWidget()}}Copy the code

2. Create a SwiftUI file to implement component functions, remove @main, and change the same function name

struct NowPosterWidget: Widget {
    private let kind: String = "NowPosterWidget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: PosterProvider()) { entry in
            NowPosterWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Maxim")
        .description("Now Meditation Daily poster, Daily sign subgroup")
        .supportedFamilies([.systemSmall,.systemMedium])
    }
}
Copy the code

4.Widget data request and network image loading

1). Create a swift file for separate processing of data
2). Create a data model for network requests
struct Poster {
    let author: String
    let content: String
    var posterImage: UIImage? = UIImage(named: "plan_collect")
}
Copy the code

The corresponding model is bound to Entry in the Widget page

struct PosterEntry: TimelineEntry {
    let date: Date
    let poster: Poster
}
Copy the code
3). Create the request function, and call back the request parameters, declare a request tool, implement the data request and put the network picturesynchronousrequest

If written in Swift for the main APP, you can share the network request module file or pods library (method described below) posterFromJson. This data model transformation method only works for simple interfaces (for laziness 🤷♀️), complex data models are parsed using HandyJSON or KaKaJson

In the case of third-party model transformation, the image synchronization request processing is placed in the getTodayPoster request synchronization processing

struct PosterData {
    static func getTodayPoster(completion: @escaping (Result<Poster.Error- > >)Void) {
        let url = URL(string: "https://nowapi.navoinfo.cn/get/now/today")!
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard error= =nil else{
                completion(.failure(error!))
                return
            }
            let poster=posterFromJson(fromData: data!)
            completion(.success(poster))
        }
        task.resume()
    }
    
    static func posterFromJson(fromData data:Data) -> Poster {
        let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
        guard let result = json["result"] as? [String: Any] else{
            return Poster(author: "Now", content: "Load failed")}let author = result["author"] as! String
        let content = result["celebrated"] as! String
        let posterImage = result["poster_image"] as! String
        
        // Image synchronization request
        var image: UIImage? = nil
        if let imageData = try? Data(contentsOf: URL(string: posterImage)!) {
            image = UIImage(data: imageData)
        }
        
        return Poster(author: author, content: content, posterImage: image)
    }
}
Copy the code

Image in SwiftUI does not provide direct URL loading for Image display

After completion of completion of completion of data request in getTimeline, asynchronous callback of pictures is no longer supported, and network pictures cannot be loaded in asynchronous loading mode. Therefore, synchronization mode must be adopted in the processing of data request back to obtain data of pictures and convert it into UIImage. In the assignment to Image display

4). Data loading processing
func getTimeline(in context: Context.completion: @escaping (Timeline<Entry>) - > ()) {
        let currentDate = Date(a)// Update data every hour
        let updateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
        
        PosterData.getTodayPoster { result in
            let poster: Poster
            if case .success(let fetchedData) = result{
                poster = fetchedData
            }else{
                poster=Poster(author: "Now", content: "Now adage");
            }
            
            let entry = Entry(date: currentDate, poster: poster)
            let timeline = Timeline(entries: [entry], policy: .after(updateDate))
            completion(timeline)
        }
    }
Copy the code
5). Page building display
struct NowPosterWidgetEntryView : View {
    var entry: PosterProvider.Entry
    var body: some View {
        ZStack{
            Image(uiImage: entry.poster.posterImage!)
                .resizable()
                .frame(minWidth: 169, maxWidth: .infinity, minHeight: 169, maxHeight: .infinity, alignment: .center)
                .scaledToFill()
                .edgesIgnoringSafeArea(.all)
                .aspectRatio(contentMode: .fill)
            Text(entry.poster.content)
                .foregroundColor(Color.white)
                .lineLimit(4)
                .font(.system(size: 14))
                .padding(.horizontal)
        }
        .widgetURL(URL(string: "Jump link"))}}Copy the code

Then update the corresponding Entry at placeholder getSnapshot Previews to complete the Widget content display

5.Widget Click interaction

Click on the Widget window to invoke the APP for interaction. There are two ways to specify the jump:

  • WidgetURL: Click areas are all areas of the Widget, suitable for elemental, logically simple widgets
  • Link: The Link modifier allows different elements on the interface to generate click responses

Widget in three sizes

  • SystemSmall can only implement URL passing and receiving with widgetURL
var body: some View {
        ZStack{
            / / UI to write
        }
        .widgetURL(URL(string: "Jump link widgetURL"))}Copy the code
  • SystemMedium and systemLarge can be handled with Link or widgetUrl
var body: some View {
        Link(destination: URL(string: "Jump Link Link")!) {VStack{
                / / UI to write}}}Copy the code

Receive Mode Receives the returned URL in the APPDelegate

//swift
func application(_ app: UIApplication.open url: URL.options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool{}//OC
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
    if ([url.scheme isEqualToString:@"NowWidget"]) {// Perform the operation after the jump
    }
    return YES;
}
Copy the code

If the project implements SceneDelegate, the jump processing needs to be implemented inside SceneDelegate

func scene(_ scene: UIScene.openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for context in URLContexts {
        print(context.url)
    }
}
Copy the code


Data sharing

Since widgets and apps are independent of each other, if you want to use the same data, you need to share data between the widgets and create themApp GroupIn the main APPTarget -> Signing & Capability -> +Capability -> Add App Group

Automatically manage Signing will Automatically create the APPID for you

Data is shared between the two mainly in the form of UserDefaults and FileManager. For example, use UserDefaults in OC to share data

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.imoblife.now"]; [userDefaults setObject:@"content" forKey:@"widget"]; [userDefaults synchronize]; NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.imoblife.now"]; NSString *content = [userDefaults objectForKey:@"widget"];Copy the code

Oc, SWIFT mixed call


File sharing and Pods sharing
  • File sharing

Check the shared Widget option

  • The pods to share

For normal use, third-party SDKS such as those imported by the Pods will not be available in widgets, which will cause inconvenience to the layout. Therefore, you need to share the Pods and install them again in your Podfile

The source 'https://github.com/CocoaPods/Specs.git' platform: ios, '9.0' inhibit_all_warnings! use_frameworks! Def share_PODS pod 'HandyJSON' end target "targetName" do pod 'Alamofire' share_Pods end target "widgetTargetName" do share_pods endCopy the code

When you’re done, you can use the third-party SDK in pods

Pods Third-party SDK use error message if the Pods shared third-party library is imported or an error is reported using the [UIApplication sharedApplication] method

not available on iOS (App Extension) – Use view controller based solutions where appropriate instead.

You need to inpods TargetInside, select the SDK that failed and clickbuildSettingssearchRequireThen put theRequire Only App-Extension-Safe APIThen change YES to **NO* * canPs: Engineering projects can also follow this method to investigate the cause