【iOS小组件】小组件尺寸及类型适配

小组件尺寸

注意

  • 超大号尺寸系统要求最低 iOS15及以后,专门在 iPad 上使用的
  • 从 iOS 16 开始,支持 accessoryCircularaccessoryRectangularaccessoryInline 类型,这几种小部件可以用在手表中,也可以出现在 iOS 的锁定屏幕上。

目前小组件支持设置基础的三个尺寸,即:超大(没有调出来)。

适配不同尺寸小组件

1.supportedFamilies

iOS14 上适配不同尺寸的小组件就是在 Widget 上添加 supportedFamilies 属性:

scss 复制代码
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            followWidgetEntryView(entry: entry)
                .padding()
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        // 设置支持的小组件类型
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        .adoptableWidgetContentMargin()
    }
}

2.available

如需在 iOS14 上支持 超大尺寸 ,可以通过 available 区分不同操作系统版本

scss 复制代码
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        if #available(iOSApplicationExtension 15.0, *) {
            StaticConfiguration(kind: kind, provider: Provider()) { entry in
                // 新增加
                MyWidgetEntryView(entry: entry)
            }
            .configurationDisplayName("My Widget")
            .description("This is an example widget.")
            .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
        } else {
            StaticConfiguration(kind: kind, provider: Provider()) { entry in
                // 新增加
                MyWidgetEntryView(entry: entry)
            }
            .configurationDisplayName("My Widget")
            .description("This is an example widget.")
            .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        }
    }
}

3.自定义supportedFamilies

同样可以通过自定义 supportedFamilies 函数返回值来达到适配不同尺寸小组件的目的。

swift 复制代码
struct MyWidget: Widget {
    let kind: String = "MyWidget"
    
    
    private var customSupportedFamilies: [WidgetFamily] {
        if #available(iOSApplicationExtension 15.0, *) {
           return [.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]
        } else {
           return [.systemSmall, .systemMedium, .systemLarge]
        }
    }

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            // 新增加
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies(customSupportedFamilies)
    }
}

适配多种类型的小组件

不同小组件类型需要不同的系统版本支持,如果在 iOS14 上添加 iOS16 锁屏小组件就会报错。

解决方案可根据不同系统版本设置不同的小组件类型支持

swift 复制代码
struct MyWidget: Widget {
    let kind: String = "MyWidget"
    
    private var customSupportedFamilies: [WidgetFamily] {
        if #available(iOSApplicationExtension 16.0, *) {
            return [.systemSmall, .systemMedium, .systemLarge, .accessoryInline, .accessoryCircular, .accessoryRectangular]
        } else {
           return [.systemSmall, .systemMedium, .systemLarge]
        }
    }

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            // 新增加
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies(customSupportedFamilies)
    }
}

Environment

MyWidgetEntryView 中可以从环境变量(Environment)中获取当前小组件类型,根据类型可以做不同的 UI 适配:

swift 复制代码
struct MyWidgetEntryView : View {
    var entry: Provider.Entry
    // 环境变量获取当前的小组件类型
    @Environment(\.widgetFamily) var family: WidgetFamily
    
    var familyString: String {
        switch family {
        case .systemSmall:
            return "小组件"
        case .systemMedium:
            return "中等组件"
        case .systemLarge:
            return "大号组件"
        case .systemExtraLarge:
            return "超大号组件"
        case .accessoryCircular:
            return "圆形组件"
        case .accessoryRectangular:
            return "方形组件"
        case .accessoryInline:
            return "内联小组件"
        @unknown default:
            return "其他类型小组件"
        }
    }

    var body: some View {
        VStack(spacing: 10) {
            Image(systemName: entry.time.icon)
                .imageScale(.large)
                .foregroundColor(.red)
            HStack {
                Text("现在是:")
                Text(entry.time.text)
            }
            .font(.title3)
            Text("小组件类型: \(familyString)")
        }
        .widgetBackground(Color.white)
    }
}

完整代码

scss 复制代码
import WidgetKit
import SwiftUI

extension View {
    // 背景
    @ViewBuilder
    func widgetBackground(_ backgroundView: some View) -> some View {
        // 如果是 iOS 17,则使用 containerBackground
        if #available(iOS 17.0, *) {
            containerBackground(for: .widget) {
                backgroundView
            }
        } else {
            background(backgroundView)
        }
    }
}

enum Time {
    case morning, afternoon, night
    
    var text: String {
        switch self {
        case .morning:
            return "上午"
        case .afternoon:
            return "下午"
        case .night:
            return "晚上"
        }
    }
    
    var icon: String {
        switch self {
        case .morning:
            return "sunrise"
        case .afternoon:
            return "sun.max.fill"
        case .night:
            return "sunset"
        }
    }
}

func getDate(hour: Int) -> Date {
    let calendar = Calendar.current
    var components = calendar.dateComponents([.year, .month, .day], from: Date())
    components.hour = hour
    components.minute = 0
    components.second = 0

    return calendar.date(from: components)!
}

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), time: .morning)
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), time: .morning)
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        let hour = Calendar.current.component(.hour, from: Date())
        switch hour {
        case 8..<12:
            entries.append(SimpleEntry(date: Date(), time: .morning))
            entries.append(SimpleEntry(date: getDate(hour: 12), time: .afternoon))
            entries.append(SimpleEntry(date: getDate(hour: 18), time: .night))
        case 12..<18:
            entries.append(SimpleEntry(date: Date(), time: .afternoon))
            entries.append(SimpleEntry(date: getDate(hour: 18), time: .night))
        default:
            entries.append(SimpleEntry(date: Date(), time: .night))
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    // 表示上午、下午、晚上
    let time: Time
}

struct MyWidgetSmallEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack(spacing: 10) {
            Image(systemName: entry.time.icon)
                .imageScale(.large)
                .foregroundColor(.red)
            HStack {
                Text("现在是:")
                Text(entry.time.text)
            }
            .font(.title3)
            Text("小号组件类型")
        }
        .widgetBackground(Color.white)
    }
}

struct MyWidgetMiddleEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack(spacing: 10) {
            Image(systemName: entry.time.icon)
                .imageScale(.large)
                .foregroundColor(.red)
            HStack {
                Text("现在是:")
                Text(entry.time.text)
            }
            .font(.title3)
            Text("中号组件类型")
        }
        .widgetBackground(Color.white)
    }
}

struct MyWidgetLargeEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack(spacing: 10) {
            Image(systemName: entry.time.icon)
                .imageScale(.large)
                .foregroundColor(.red)
            HStack {
                Text("现在是:")
                Text(entry.time.text)
            }
            .font(.title3)
            Text("大号组件类型")
        }
        .widgetBackground(Color.white)
    }
}

func AccessoryCircularView() -> some View {
    Image(systemName: "brain.head.profile")
}

func AccessoryInlineView() -> some View {
    HStack {
        Image(systemName: "brain.head.profile")
            .padding(.trailing, 5)
        Text("内联锁屏小组件")
    }
}

func AccessoryRectangularView() -> some View {
    VStack {
        Image(systemName: "brain.head.profile")
            .padding(.bottom, 5)

        Text("矩形锁屏小组件")
    }
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    // 环境变量获取当前的小组件类型
    @Environment(\.widgetFamily) var family: WidgetFamily
    
    var body: some View {
        switch family {
        case .systemSmall:
            MyWidgetSmallEntryView(entry: entry)
        case .systemMedium:
            MyWidgetMiddleEntryView(entry: entry)
        case .systemLarge:
            MyWidgetLargeEntryView(entry: entry)
        case .accessoryCircular:
            AccessoryCircularView()
        case .accessoryRectangular:
            AccessoryRectangularView()
        case .accessoryInline:
            AccessoryInlineView()
        default:
            Text("其他类型小组件")
        }
    }
}


struct MyWidget: Widget {
    let kind: String = "MyWidget"
    
    
    private var customSupportedFamilies: [WidgetFamily] {
        if #available(iOSApplicationExtension 16.0, *) {
            return [.systemSmall, .systemMedium, .systemLarge, .accessoryInline, .accessoryCircular, .accessoryRectangular]
        } else {
           return [.systemSmall, .systemMedium, .systemLarge]
        }
    }

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            // 新增加
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies(customSupportedFamilies)
    }
}

struct MyWidgetEntryView_Previews: PreviewProvider {
    static var previews: some View {
        MyWidgetEntryView(entry: SimpleEntry(date: Date(), time: .morning))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

本文同步自微信公众号 "程序员小溪" ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。

相关推荐
sakiko_12 小时前
OC基础语法(与Swift对比)-1
开发语言·ios·objective-c·swift
2501_9160088920 小时前
Flutter 开发 iOS,项目打包成 IPA,命令与免 Xcode两种方法
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子2 天前
举手之劳 -- 肘子的 Swift 周报 #151
人工智能·swiftui·swift
sakiko_2 天前
Swift学习笔记42-SwiftUI的属性包装器(讲解+面试)
笔记·学习·ios·swiftui·swift
weixin_531670895 天前
Interlude起来:动作动画和文字通知哪个更适合休息提醒?Mac上有没有用桌宠做动作引导的软件?
人工智能·macos·mac·swift
Daniel_Coder5 天前
我用 SwiftUI + GRDB 做了一个「按时吃药·服药提醒」App
ios·swiftui·swift·widgetkit
末代iOS程序员华仔6 天前
iOS个人开发者账号申请为啥会被卡住
ios·swift·figma
denggun123457 天前
信号量(DispatchSemaphore vs AsyncSemaphore)、swift协作式线程池 and python信号量
开发语言·python·swift
2501_915918417 天前
Swift IDE 有哪些?按平台和需求选择Swift 开发工具
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
懋学的前端攻城狮8 天前
把 SwiftUI List 换成 NSTableView:一次目录树卡顿的排查记录
macos·swiftui·swift