如何创建iOS锁屏小程序

[读取文件中]
以下是关于如何创建 iOS 锁屏小组件的详细介绍:


iOS 锁屏小组件(Lock Screen Widget)完整指南

一、基础概念

iOS 16 引入了锁屏小组件功能,基于 WidgetKit 框架实现,支持三种样式:

样式 Family 枚举 描述
圆形 .accessoryCircular 圆形区域,适合图标+简短信息
矩形 .accessoryRectangular 横向矩形,适合多行文字
内联 .accessoryInline 单行文字,显示在时间旁边

二、前置条件

  • Xcode 14+
  • iOS 16.0+ SDK
  • 项目中已有 Widget Extension Target

三、核心文件结构

LockScreenWidget/
└── LockScreenWidget.swift   # 核心实现(Provider + Entry + View + Widget)

四、完整实现步骤

1. 定义数据模型 TimelineEntry

struct LockScreenEntry: TimelineEntry {
    let date: Date
    let songName: String
    let artistName: String
    let isPlaying: Bool
}

2. 实现 TimelineProvider

struct LockScreenProvider: TimelineProvider {
    
    // 占位视图(首次加载时显示)
    func placeholder(in context: Context) -> LockScreenEntry {
        LockScreenEntry(date: Date(), songName: "Song Name", artistName: "Artist", isPlaying: false)
    }

    // 快照(小组件预览时使用)
    func getSnapshot(in context: Context, completion: @escaping (LockScreenEntry) -> Void) {
        let entry = LockScreenEntry(date: Date(), songName: "My Song", artistName: "My Artist", isPlaying: true)
        completion(entry)
    }

    // 时间线(实际数据更新逻辑)
    func getTimeline(in context: Context, completion: @escaping (Timeline<LockScreenEntry>) -> Void) {
        // 从 App Group 共享数据或 API 获取数据
        let entry = LockScreenEntry(date: Date(), songName: "My Song", artistName: "My Artist", isPlaying: true)
        
        // 设置下次刷新时间
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
        completion(timeline)
    }
}

3. 实现三种样式的 View

struct LockScreenWidgetEntryView: View {
    var entry: LockScreenEntry
    @Environment(\.widgetFamily) var family

    var body: some View {
        switch family {
        case .accessoryCircular:
            CircularView(entry: entry)
        case .accessoryRectangular:
            RectangularView(entry: entry)
        case .accessoryInline:
            InlineView(entry: entry)
        default:
            EmptyView()
        }
    }
}

// 圆形样式
struct CircularView: View {
    var entry: LockScreenEntry
    var body: some View {
        ZStack {
            AccessoryWidgetBackground() // 系统提供的背景
            VStack(spacing: 2) {
                Image(systemName: entry.isPlaying ? "play.circle.fill" : "pause.circle.fill")
                    .font(.system(size: 14))
                Text("JOOX")
                    .font(.system(size: 8, weight: .bold))
            }
        }
    }
}

// 矩形样式
struct RectangularView: View {
    var entry: LockScreenEntry
    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            Label(entry.songName, systemImage: entry.isPlaying ? "play.fill" : "pause.fill")
                .font(.system(size: 12, weight: .semibold))
                .lineLimit(1)
            Text(entry.artistName)
                .font(.system(size: 10))
                .foregroundColor(.secondary)
                .lineLimit(1)
        }
    }
}

// 内联样式
struct InlineView: View {
    var entry: LockScreenEntry
    var body: some View {
        Label("\(entry.songName) - \(entry.artistName)", 
              systemImage: entry.isPlaying ? "play.fill" : "pause.fill")
    }
}

4. 声明 Widget 并指定支持的 Family

struct LockScreenWidget: Widget {
    let kind: String = "LockScreenWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: LockScreenProvider()) { entry in
            LockScreenWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("JOOX 锁屏")
        .description("在锁屏上显示当前播放歌曲")
        .supportedFamilies([
            .accessoryCircular,
            .accessoryRectangular,
            .accessoryInline
        ])
    }
}

5. 注册到 WidgetBundle

@main
struct MyWidgetBundle: WidgetBundle {
    var body: some Widget {
        // 其他小组件...
        if #available(iOS 16.0, *) {
            LockScreenWidget()
        }
    }
}

五、主 App 与 Widget 共享数据

锁屏小组件无法直接访问主 App 的数据,需要通过 App Group 共享:

// 主 App 写入数据
let defaults = UserDefaults(suiteName: "group.com.yourapp.widget")
defaults?.set("My Song", forKey: "currentSongName")
defaults?.set("My Artist", forKey: "currentArtistName")

// Widget 读取数据
let defaults = UserDefaults(suiteName: "group.com.yourapp.widget")
let songName = defaults?.string(forKey: "currentSongName") ?? ""

六、主动刷新 Widget

当主 App 中歌曲切换时,主动通知 Widget 刷新:

import WidgetKit

// 在主 App 中调用
WidgetCenter.shared.reloadTimelines(ofKind: "LockScreenWidget")
// 或刷新所有 Widget
WidgetCenter.shared.reloadAllTimelines()

七、锁屏小组件的设计注意事项

  1. 颜色渲染模式:锁屏小组件默认使用 .accented 渲染模式,颜色会被系统统一处理,建议使用 AccessoryWidgetBackground() 作为背景
  2. 内容精简:锁屏空间有限,只展示最核心的信息
  3. 不支持动画:锁屏小组件不支持动画效果
  4. 点击跳转:可通过 LinkwidgetURL 设置点击后打开 App 的指定页面:
    .widgetURL(URL(string: "yourapp://nowplaying"))
    

八、完整流程图

graph TD
    A[主 App 播放歌曲] --> B[写入 App Group UserDefaults]
    B --> C[调用 WidgetCenter.reloadTimelines]
    C --> D[WidgetKit 触发 getTimeline]
    D --> E[Provider 读取 App Group 数据]
    E --> F[生成 LockScreenEntry]
    F --> G[渲染 LockScreenWidgetEntryView]
    G --> H{widgetFamily}
    H --> I[.accessoryCircular 圆形]
    H --> J[.accessoryRectangular 矩形]
    H --> K[.accessoryInline 内联]

九、常见问题

问题 原因 解决方案
小组件不显示 未添加到 WidgetBundle 确认 WidgetBundle 中已注册
数据不更新 未调用 reloadTimelines 在数据变化时主动调用刷新
颜色显示异常 锁屏颜色渲染限制 使用系统颜色或 .widgetAccentable()
编译报错 iOS 版本不兼容 添加 @available(iOS 16.0, *) 检查
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容