本文记录一些开发Widget小组件中遇到的问题和解决方法。

onAppear/onDisappear 不执行

SwiftUI中onAppear/onDisappear在Widget中使用不会被执行,不能使用在Widget中,如果想要统计展示次数这种需求无法实现,Widget的设计理念是静态的。

数据共享

Widget与主工程之间共享数据可以配置AppGroup ,使用UserDefault 不能使用Keychain共享数据,无法获得数据

断点不走

如果在widget代码中加入断点,经常会走不到断点

解决办法: 1、在主工程中主动调用widget刷新

import WidgetKit
        if #available(iOS 14.2, *) {
            WidgetCenter.shared.reloadAllTimelines()
        } else {
            // Fallback on earlier versions
        }

2、在getTimeline函数中加入断点,运行主工程,这样会走到断点

黑暗模式

Widget会默认适配黑暗模式,如果你没有设置背景色,黑暗模式下就会变成黑色,如果你的字体指定了黑色就会显示不出来,如果没有指定颜色,则会跟随模式切换。

所以如果需要指定UI不同部分颜色,在Assets中添加Color Set,指定颜色名称,同时区分dark和light不同颜色

时间显示

Widget中如果需要显示时间,随系统时间变化,一般两种方法:

1、 在getTimeline函数中降低时间间隔,满足刷新时间需要,每次获得当前Date传入Timeline,调用回掉,这种方式的好处是展示时间的UI样式可以自由定制

2、使用iOS14新增的DateStyle,无需自己代码刷新

Text(event.startDate, style: .time)       //11:23PM
Text(event.startDate, style: .date)       //June 3, 2019
Text(event.startDate, style: .relative)   //2 hours, 23 minutes
Text(event.startDate, style: .offset)     //+2 hours
Text(event.startDate, style: .timer)      //36:59:01

但是展示样式不能定制,Text第一个参数必须是Date,不能是String

Widget中TimelineProviderContext

在widget中Provider的三个方法placeholder,getSnapshot,getTimeline都有一个参数context: Context,这个context的类型是TimelineProviderContext

public struct TimelineProviderContext {

    /// A structure containing all varieties of environments where a widget
    /// could appear.
    ///
    /// When changes occur in environment values that affect display, like
    /// `.colorScheme`, WidgetKit renders your widget's views. If your widget
    /// uses assets that take time to generate or depend on the specific
    /// environment they're rendered in, you can generate those assets in
    /// advance based on the new environment values.
    ///
    /// For example, in macOS, if the user has a mixture of @1x and @2x
    /// displays, the value for `.displayScale` includes both scales. With
    /// these values, you can prepare your content in advance, if needed, to
    /// handle either type of display.
    @dynamicMemberLookup public struct EnvironmentVariants {

        public subscript<T>(dynamicMember keyPath: WritableKeyPath<EnvironmentValues, T>) -> [T]? { get }

        public subscript<T>(keyPath: WritableKeyPath<EnvironmentValues, T>) -> [T]? { get }
    }

    /// All environment values that might be set when a widget appears.
    public let environmentVariants: TimelineProviderContext.EnvironmentVariants

    /// The user-configured family of the widget: small, medium, or large.
    public let family: WidgetFamily

    /// A Boolean value that indicates when the widget appears in the widget gallery.
    public let isPreview: Bool

    /// The size, in points, of the widget.
    public let displaySize: CGSize
}

其中:

  • displaySize可以获得到Widget在不同机型上具体的宽,高
  • family 可以获得到当前视图是小,中,大
  • isPreview 布尔值,是否在Review视图中

另外,它还实现了EnvironmentVariants,所以这些参数都可以通过Environment在SwiftUI的View中获得

struct widgetEntryView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack{
            switch family {
                case .systemSmall:
                    VStack(alignment: .leading){
                        
                    }
            case .systemMedium:

            default:
                    Text(entry.date, style: .time)
            }
        }
    }
}

网络请求

只有getTimeline中可以发网络请求,拿到的数据保存在对应的entry中,调用completion之后会刷新Widget

参数policy:刷新的时机

  • .never:不刷新
  • .atEnd:Timeline 中最后一个 Entry 显示完毕之后自动刷新。Timeline 方法会重新调用
  • .after(date):到达某个特定时间后自动刷新

Widget 刷新的时间由系统统一决定,如果需要强制刷新Widget,可以在 App 中使用 WidgetCenter 来重新加载所有时间线:WidgetCenter.shared.reloadAllTimelines()

如果你请求一个接口,返回数据,其中包含图片链接,你需要执行同步操作先下载完图片,data转UIImage,然后再传递给Timeline,不能多次异步调用Timeline的回掉completion