iOS 14 widget(时钟、日历)

github demo 附在结尾

本文主要介绍iOS14 widget开发,自定义时钟、日历,app中切换widget背景图片、颜色。
创建一个项目,增加widget extension


widget添加.png

系统默认代码(可以略过~~)

//  配置时间线
struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: ConfigurationIntent, 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()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}
// 数据源
struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}
// 主视图
struct CYWidgetExtensionEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}
// 入口
@main
struct CYWidgetExtension: Widget {
    let kind: String = "CYWidgetExtension"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            CYWidgetExtensionEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}
// Xcode 右侧快捷预览
struct CYWidgetExtension_Previews: PreviewProvider {
    static var previews: some View {
        CYWidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

(成功略过,以下是主要实现代码~~)

// 自定义时钟
iOS14 widget 主要是基于swiftUI 布局,所以需要一些swiftUI基础。
创建自定义时钟表盘,指针

private struct CYWidgetCalendarView: View {
    var entry: SimpleEntry
    
    var body: some View {
        ZStack {
            Image(uiImage: entry.data.image)
                .resizable()
                .scaledToFill()
                .edgesIgnoringSafeArea(.all)

            ForEach(1..<13, id: \.self){ i in
                let sinX = sin(CGFloat(i)*30.0/180.0*CGFloat.pi)
                let sinY = -cos(CGFloat(i)*30.0/180.0*CGFloat.pi)
                let color = Color(UIColor(hexStr: entry.data.colorStr))
                Text("\(i)")
                    .foregroundColor(color)
                    .font(.system(size: RatioLen(14)))
                    .frame(width: RatioLen(16), height: RatioLen(16), alignment: .center)
                    .offset(x: 60*sinX, y: 60*sinY)
            }
            
            // sec
            /*
            Rectangle()
                .fill(Color.red)
                .frame(width: 2, height: RatioLen(40), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                .offset(y: -10)
                .rotationEffect(.init(degrees: Double(entry.clockTime().sec*6)))*/

            // min
            Rectangle()
                .fill(Color.blue)
                .frame(width: 2, height: RatioLen(30), alignment: .center)
                .offset(y: -15)
                .rotationEffect(.init(degrees: Double(entry.clockTime().min*6)))
            
            let hour = Double(entry.clockTime().min)*0.5 + Double(entry.clockTime().hour)*30.0
            // hour
            Rectangle()
                .fill(Color.primary)
                .frame(width: 2, height: RatioLen(20), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                .offset(y: -10)
                .rotationEffect(.init(degrees: Double(hour)))
            // link 只支持 systemMedium systemLarge
            /*
            Link(destination: URL(string: "https://www.baidu.com/")!, label: {
                    /*@START_MENU_TOKEN@*/Text("Link")/*@END_MENU_TOKEN@*/
                })*/

        } // widgetURL 支持全尺寸 推荐
        .widgetURL(URL(string: "https://www.baidu.com/clock"))
    }
}

关于在app中切换widget背景图片 和表盘刻度颜色,需要在xcode targets增加app group, target和widget extension 都需要添加且需要一致,app group id 是bundle id 前加group. 就好了。

控制器中代码实现,存储图片到group沙盒路径中,保存配置颜色值到group UserDefault

// group id
public let groupBundleKey: String = "group.www.cyan.com.CYWidgetNew"

// widget group 指定路径
let widgetMainPath = "/Library/cyan/widget/"

// widget 时钟颜色
let widgetClockColor = "widgetClockColor"
// widget 日历颜色
let widgetCalendarColor = "widgetCalendarColor"

class ViewController: UIViewController {

    lazy var button1: UIButton = {
        let button = UIButton(type: .custom)
        button.backgroundColor = UIColor.purple
        button.setTitle("刷新 1", for: .normal)
        button.addTarget(self, action: #selector(save(btn:)), for:.touchUpInside)
        return button
    }()
    
    lazy var button2: UIButton = {
        let button = UIButton(type: .custom)
        button.backgroundColor = UIColor.purple
        button.setTitle("刷新 2", for: .normal)
        button.addTarget(self, action: #selector(save(btn:)), for:.touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        creatUI()
    }
    
    func creatUI() {
        button1.frame = CGRect(x: 30, y: 70, width: 100, height: 40)
        self.view.addSubview(button1)
    
        button2.frame = CGRect(x: 150, y: 70, width: 100, height: 40)
        self.view.addSubview(button2)
    }
    
    @objc func save(btn: UIButton) {
        let fileManager = FileManager.default
        let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupBundleKey)

        let mainPath = (url?.path ?? "") + widgetMainPath
        var isFolder: ObjCBool = false
        let isExists = fileManager.fileExists(atPath: mainPath, isDirectory: &isFolder)
        if isExists == false || isFolder.boolValue == false {
            try? fileManager.createDirectory(atPath: mainPath, withIntermediateDirectories: true, attributes: nil)
        }
        // temp1  temp2
        var name = ""
        var color = "000000"
        switch btn {
        case button1:
            name = "temp1"
            color = "000000"
        default:
            name = "temp2"
            color = "ffffff"
        }
        
        let data = UIImage(named: name)?.pngData()
        try? data?.write(to: URL(fileURLWithPath: mainPath + "widget1.jpg"), options: .atomic)

        print("mainPath : \(mainPath)")
        
        let userdefaults = UserDefaults.init(suiteName: groupBundleKey)
        userdefaults?.setValue(color, forKey: widgetClockColor)
        userdefaults?.setValue(color, forKey: widgetCalendarColor)
        userdefaults?.synchronize()
        
        // 立即刷新所有组件
        if #available(iOS 14.0, *) {
            DispatchQueue.main.async {
                WidgetCenter.shared.reloadAllTimelines()
                print("刷新成功")
            }
       
        } else {
            // Fallback on earlier versions
        }
    }

}

widget data 中从group沙盒中获取图片,从group userdefault中获取 配置颜色

// group id
public let groupBundleKey: String = "group.www.cyan.com.CYWidgetNew"

// 图片路径 存储名
let clockWidgetPath = "/Library/cyan/widget/widget1.jpg"

// widget 时钟颜色
let widgetClockColor = "widgetClockColor"
// widget 日历颜色
let widgetCalendarColor = "widgetCalendarColor"

let widgetTargetWidth: CGFloat = 329
let iPhoneHeight = UIScreen.main.bounds.size.height

enum CYWidgetType {
    case clock
    case calendar
    
    var path: String {
        clockWidgetPath
    }

}

struct CYWidgetData {
    let title: String
    let imageName : String
    let image   : UIImage
    let colorStr: String
}

struct CYWidgetDataLoader {
    
    static func getWidgetData(_ type: CYWidgetType) -> CYWidgetData {
        let userdefaults = UserDefaults.init(suiteName: groupBundleKey)
        var colorStr = ""
        switch type {
        case .calendar: colorStr = userdefaults?.string(forKey: widgetCalendarColor) ?? "ffffff"
        case .clock: colorStr = userdefaults?.string(forKey: widgetClockColor) ?? "ffffff"
        }
        
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupBundleKey)
        let imagePath = (url?.path ?? "") + type.path
        print("imagePath:\(imagePath)")
        if let image = UIImage(contentsOfFile: imagePath) {
            return CYWidgetData(title: "title:cyan", imageName: "", image: image, colorStr: colorStr)
        }
        return CYWidgetData(title: "title:cyan", imageName: "", image: UIImage(named: "widgetBackground")!, colorStr: colorStr)
    }
    
}

demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容