iOS小组件Widget开发附案例倒计时时钟按秒刷新

文末是一个根据当天时间显示 早上,中午,晚上的案例,且用户可以更换背景图片,无需进入主App


51753930100_.pic_副本.jpeg

小组件是什么

21753924680_.pic_副本.jpeg

小组件分为静态(StaticConfiguration)和用户可编辑的(IntentConfiguration)。只有可编辑小组件,用户长按时才会出现编辑小组件选项

31753924851_.pic_副本.jpeg

用户点击我的位置,可以直接更换城市,无需进入主App


41753924852_.pic_副本.jpeg

系统允许每个App创建多个子App,也就是小组件(Widget Extension),一般创建一个然后适配不同的样式,App可以提供多款展示样式供用户选择。

左右滑动选择样式

小组件的尺寸

分为小,中,大,App可以只提供小尺寸,但建议全提供。

小组件的开发语言

小组件代码是swift,布局代码是swiftui。如果你是oc项目,需要与小组件交互时,要创建一个桥接文件。

小组件时间线Timeline

用户把小组件加到桌面以后,展示内容肯定需要更新,但与App不同的是,小组件代码的调用时机由系统决定,只有系统再次执行小组件的代码,小组件的显示内容才会刷新,这个机制叫Timeline。

每次系统调用你的小组件Timeline,你都需要告知系统两个信息。

  1. 本次需要渲染的内容
  2. 在未来的哪些时间点,重新来获取新的Timeline,或者永不来获取(适用于不需要更新内容的)。

你也通过以下代码主动要求系统来获取新的时间线,以更新小组件展示内容。

WidgetCenter.shared.reloadAllTimelines()

如果你是oc工程且需要主动触发系统调用,需要建桥接文件,因为WidgetCenter是swift中独有的类的。
系统要求小组件的更新间隔不得少于15分钟,如果你每秒都主动请求刷一次,小组件会被系统拉黑,但App处于前台时随便刷。不过别担心,要实现按秒更新系统有提供特定的实现方式。

按秒更新关键代码

let date = Date().getCurrentDayStart(true)
Text(date, style: .timer)
///.timer。 当前时间与date之间的时间差,注意还有一个枚举是.time,别用错了
/// 12:59:01

Text(date, style: .relative)
// Displays:
// 11 min, 14 sec

Text(date, style: .offset)
// Displays:
// -11 minutes

extension Date {
    func getCurrentDayStart(_ isDayOf24Hours: Bool)-> Date {
        let calendar:Calendar = Calendar.current;
        let year = calendar.component(.year, from: self);
        let month = calendar.component(.month, from: self);
        let day = calendar.component(.day, from: self);
    
        let components = DateComponents(year: year, month: month, day: day, hour: 0, minute: 0, second: 0)
        return Calendar.current.date(from: components)!
    }
}

Intent

Intent也叫意图,是开发者定义的小组件与系统通信的一种交互协议,告诉系统该如何调用你的小组件,内容包括你提供了哪些参数,分别是什么类型,例如 name 是字符串类型string。相当于系统是客户端,你是服务器。客户端调用服务器提供的api。

截屏2025-07-31 11.50.11.png
截屏2025-07-31 11.47.38.png
截屏2025-07-31 11.55.21.png

当你完成了所需Intent定义,执行Xcode build,系统会读取intentdefinition文件中定义的intent,会生成Intent名称+"Intent"的类文件,我这里会生成两个类文件UjanIntent.swift,SimpleTimeIntent.swift和TimeType.swift。这两个文件你在工程目录是看不到的,直接代码里引用就行。

它们在这个路径下
/Users/apple/Library/Developer/Xcode/DerivedData/MaoWidget-epashhpcvxzvtjforrskewekgifi/Index.noindex/Build/Intermediates.noindex/MaoWidget.build/Debug-iphoneos/MaoIntent.build/DerivedSources/IntentDefinitionGenerated/DM


截屏2025-07-31 12.47.18.png

你可以直接在项目里直接引用它,无需关心它们在哪,也不用拷贝到项目里。
当intentdefinition发生更改后,执行build,Xcode会更新它们。

SimpleTimeIntent.swift
//
// SimpleTimeIntent.swift
//
// This file was automatically generated and should not be edited.
//

#if canImport(Intents)

import Intents

@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SimpleTimeIntent)
public class SimpleTimeIntent: INIntent {

    @NSManaged public var type: TimeType?

}

/*!
 @abstract Protocol to declare support for handling a SimpleTimeIntent. By implementing this protocol, a class can provide logic for resolving, confirming and handling the intent.
 @discussion The minimum requirement for an implementing class is that it should be able to handle the intent. The confirmation method is optional. The handling method is always called last, after confirming the intent.
 */
@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SimpleTimeIntentHandling)
public protocol SimpleTimeIntentHandling: NSObjectProtocol {

    /*!
     @abstract Dynamic options methods - provide options for the parameter at runtime
     @discussion Called to query dynamic options for the parameter and this intent in its current form.

     @param  intent The input intent
     @param  completion The response block contains options for the parameter
     */
    @available(iOS 14.0, macOS 11.0, watchOS 7.0, *)
    @available(*, renamed: "provideTypeOptionsCollection(for:)")
    @objc(provideTypeOptionsCollectionForSimpleTime:withCompletion:)
    func provideTypeOptionsCollection(for intent: SimpleTimeIntent, with completion: @escaping (INObjectCollection<TimeType>?, Error?) -> Swift.Void)

    @available(iOS 14.0, macOS 11.0, watchOS 7.0, *)
    @objc(provideTypeOptionsCollectionForSimpleTime:withCompletion:)
    func provideTypeOptionsCollection(for intent: SimpleTimeIntent) async throws -> INObjectCollection<TimeType>

    @available(*, renamed: "confirm(intent:)")
    @objc(confirmSimpleTime:completion:)
    optional func confirm(intent: SimpleTimeIntent, completion: @escaping (SimpleTimeIntentResponse) -> Swift.Void)

    /*!
     @abstract Confirmation method - Validate that this intent is ready for the next step (i.e. handling)
     @discussion Called prior to asking the app to handle the intent. The app should return a response object that contains additional information about the intent, which may be relevant for the system to show the user prior to handling. If unimplemented, the system will assume the intent is valid, and will assume there is no additional information relevant to this intent.

     @param  intent The input intent
     @param  completion The response block contains a SimpleTimeIntentResponse containing additional details about the intent that may be relevant for the system to show the user prior to handling.

     @see SimpleTimeIntentResponse
     */
    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @objc(confirmSimpleTime:completion:)
    optional func confirm(intent: SimpleTimeIntent) async -> SimpleTimeIntentResponse

    /*!
     @abstract Handling method - Execute the task represented by the SimpleTimeIntent that's passed in
     @discussion Called to actually execute the intent. The app must return a response for this intent.

     @param  intent The input intent
     @param  completion The response handling block takes a SimpleTimeIntentResponse containing the details of the result of having executed the intent

     @see  SimpleTimeIntentResponse
     */
    @available(*, renamed: "handle(intent:)")
    @objc(handleSimpleTime:completion:)
    optional func handle(intent: SimpleTimeIntent, completion: @escaping (SimpleTimeIntentResponse) -> Swift.Void)
    
    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @objc(handleSimpleTime:completion:)
    optional func handle(intent: SimpleTimeIntent) async -> SimpleTimeIntentResponse

    /*!
     @abstract Default values for parameters with dynamic options
     @discussion Called to query the parameter default value.
     */
    @available(iOS 14.0, macOS 11.0, watchOS 7.0, *)
    @objc(defaultTypeForSimpleTime:)
    optional func defaultType(for intent: SimpleTimeIntent) -> TimeType?

    /*!
     @abstract Deprecated dynamic options methods.
     */
    @available(iOS, introduced: 13.0, deprecated: 14.0, message: "")
    @available(watchOS, introduced: 6.0, deprecated: 7.0, message: "")
    @objc(provideTypeOptionsForSimpleTime:withCompletion:)
    optional func provideTypeOptions(for intent: SimpleTimeIntent, with completion: @escaping ([TimeType]?, Error?) -> Swift.Void)

}

/*!
 @abstract Constants indicating the state of the response.
 */
@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc public enum SimpleTimeIntentResponseCode: Int {
    case unspecified = 0
    case ready
    case continueInApp
    case inProgress
    case success
    case failure
    case failureRequiringAppLaunch
}

@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(SimpleTimeIntentResponse)
public class SimpleTimeIntentResponse: INIntentResponse {

    /*!
     @abstract The response code indicating your success or failure in confirming or handling the intent.
     */
    @objc public fileprivate(set) var code: SimpleTimeIntentResponseCode = .unspecified

    /*!
     @abstract Initializes the response object with the specified code and user activity object.
     @discussion The app extension has the option of capturing its private state as an NSUserActivity and returning it as the 'currentActivity'. If the app is launched, an NSUserActivity will be passed in with the private state. The NSUserActivity may also be used to query the app's UI extension (if provided) for a view controller representing the current intent handling state. In the case of app launch, the NSUserActivity will have its activityType set to the name of the intent. This intent object will also be available in the NSUserActivity.interaction property.

     @param  code The response code indicating your success or failure in confirming or handling the intent.
     @param  userActivity The user activity object to use when launching your app. Provide an object if you want to add information that is specific to your app. If you specify nil, the system automatically creates a user activity object for you, sets its type to the class name of the intent being handled, and fills it with an INInteraction object containing the intent and your response.
     */
    @objc(initWithCode:userActivity:)
    public convenience init(code: SimpleTimeIntentResponseCode, userActivity: NSUserActivity?) {
        self.init()
        self.code = code
        self.userActivity = userActivity
    }

}

#endif

小组件创建

静态小组件(StaticConfiguration)

只需创建一个Widget Extension

可编辑小组件(IntentConfiguration)

需要创建三个东西

  1. Widget Extension。 相当于子App

  2. DM.intentdefinition。该文件名字无所谓,我这里叫DM。你可以在里面定义一个或多个intent,我这里包含一个Ujan和SimpleTime


    截屏2025-07-31 12.48.41.png
  3. Intent Extension。当你需要用户从给定列表中选择一个,你需要创建它。然后工程目录会出现包含intentHandler.swift文件的文件夹。假设我的Intent定义名字为SimpleTime,你需要在该文件实现SimpleTimeIntentHandling协议并提供选项内容。
    例如让用户从给定的图片列表中选择小组件的背景图片。


    截屏2025-07-31 12.52.10.png

provideTypeOptionsCollection方法中的“type”,是我在intentdefinition文件中的SimpleTime中定义的参数,同时为type参数创建了一个自定义类型TimeType。定义Intent的过程就像在写服务器web接口,接受什么字段,什么类型。


截屏2025-07-31 13.20.21.png

TimeType默认自带identifier和displayString,time是我自己加的。TimeType就是一个数据模型model。


截屏2025-07-31 13.16.25.png

TimeType.swift(Xcode根据intentdefinition定义生成的)

//
// TimeType.swift
//
// This file was automatically generated and should not be edited.
//

#if canImport(Intents)

import Intents

@available(iOS 12.0, macOS 11.0, watchOS 5.0, *) @available(tvOS, unavailable)
@objc(TimeType)
public class TimeType: INObject {

    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @NSManaged public var time: String?

    override public class var supportsSecureCoding: Bool { true }

}

@available(iOS 13.0, macOS 11.0, watchOS 6.0, *) @available(tvOS, unavailable)
@objc(TimeTypeResolutionResult)
public class TimeTypeResolutionResult: INObjectResolutionResult {

    // This resolution result is for when the app extension wants to tell Siri to proceed, with a given TimeType. The resolvedValue can be different than the original TimeType. This allows app extensions to apply business logic constraints.
    // Use notRequired() to continue with a 'nil' value.
    @objc(successWithResolvedTimeType:)
    public class func success(with resolvedObject: TimeType) -> Self {
        return super.success(with: resolvedObject)
    }

    // This resolution result is to ask Siri to disambiguate between the provided TimeType.
    @objc(disambiguationWithTimeTypesToDisambiguate:)
    public class func disambiguation(with objectsToDisambiguate: [TimeType]) -> Self {
        return super.disambiguation(with: objectsToDisambiguate)
    }

    // This resolution result is to ask Siri to confirm if this is the value with which the user wants to continue.
    @objc(confirmationRequiredWithTimeTypeToConfirm:)
    public class func confirmationRequired(with objectToConfirm: TimeType?) -> Self {
        return super.confirmationRequired(with: objectToConfirm)
    }

    @available(*, unavailable)
    override public class func success(with resolvedObject: INObject) -> Self {
        fatalError()
    }

    @available(*, unavailable)
    override public class func disambiguation(with objectsToDisambiguate: [INObject]) -> Self {
        fatalError()
    }

    @available(*, unavailable)
    override public class func confirmationRequired(with objectToConfirm: INObject?) -> Self {
        fatalError()
    }

}

#endif

还要注意一点,如果创建的Widget Extension和Intent Extension 都必须符合运行手机的版本号,否则会报错“未提供此参数的选项”。假设Intent Extension为iOS 18, 而运行手机是 iOS16,就会出现该问题。

基本概念差不多了,下面进入案例

案例

效果展示

添加小组件
编辑小组件
选择背景图

选择

代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 8,523评论 0 5
  • 为了让我有一个更快速、更精彩、更辉煌的成长,我将开始这段刻骨铭心的自我蜕变之旅!从今天开始,我将每天坚持阅...
    李薇帆阅读 5,960评论 0 3
  • 似乎最近一直都在路上,每次出来走的时候感受都会很不一样。 1、感恩一直遇到好心人,很幸运。在路上总是...
    时间里的花Lily阅读 5,219评论 0 2
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 3,547评论 0 1
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 3,547评论 0 0