github demo 附在结尾
本文主要介绍iOS14 widget开发,自定义时钟、日历,app中切换widget背景图片、颜色。
创建一个项目,增加widget extension
系统默认代码(可以略过~~)
// 配置时间线
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)
}
}