发文初衷
话说Apple Developer推出Widget Extension(iOS14.0以上支持)和SwiftUI之后,听是听说过,但是并没有去了解它。刚好公司最近有一个项目需要上线这个功能,发现网上这一块资源很稀缺。很多知识点都需要通过官方的开发文档来了解学习。特整理一番!
SwiftUI有点类似Flutter语法,都是一个一个的小组件,拼接成一个最终UI效果。
VStack 代表的是Vertical 垂直布局 ,相当于Y轴
HStack 代表的是Horizon 水平布局,相当于X轴
ZStack 代表的是层次布局,相当于Z轴
GeometryReader 的作用是可以获取到父视图的proxy ,proxy.size.width,proxy.size.height
其它的控件比较好理解,Text,Image,Button之类的
下面的代码是布局小组件的代码,中组件的就不粘出来了.有需要的可以联系我。
import SwiftUI
struct WidgetSmallView : View {
@Environment(\.colorScheme) private var colorScheme
var entry: WWProvider.Entry
var body: some View {
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 22, content: {
HStack(alignment: .center, spacing: 6, content: {
Image("widget_location")
.resizable()
.frame(width: 25.5, height: 25.5, alignment: .center)
VStack(alignment: .leading, spacing: 2, content: {
Text("AAAAA").font(.custom("DIN Alternate", size: 10)).fontWeight(.semibold).foregroundColor(Color(hex: 0x9b9b9b))
Text("BBBBBB").font(.custom("DIN Alternate", size: 10)).fontWeight(.semibold).foregroundColor(Color(hex: 0x9b9b9b))
})
})
Text("CCCCC")
.font(.custom("DINAlternate-Bold", size: 24))
.fontWeight(.bold)
.foregroundColor(colorScheme == .dark ? Color.white : Color(hex: 0x231916))
+ Text(" ccc")
.font(.custom("DIN Alternate", size: 10))
.fontWeight(.semibold)
.foregroundColor(colorScheme == .dark ? Color.white : Color(hex: 0x231916))
HStack(alignment: .center, spacing: 12.5, content: {
Text("DDDDD").font(.custom("DIN Alternate", size: 10)).fontWeight(.semibold).foregroundColor(Color(hex: 0x9b9b9b))
Text("EEEEE").font(.custom("DIN Alternate", size: 10)).fontWeight(.semibold).foregroundColor(Color(hex: 0x9b9b9b))
})
}).frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
.background(colorScheme == .dark ? Color(hex: 0x2C2C30) : Color.white)
// 跳转链接, 需要在AppDelegate里面去接收这个值
}.widgetURL(URL.init(string: "widget://jump-small-action"))
}
}
知识点一:
在点击小组件之后,App如何响应指定的代码,或者跳转相应的链接、页面,以下两种方法可以实现
可以加在任意Widget上面
.widgetURL(URL.init(string: "widget://jump-small-action"))
一开始我是想用Button去实现按钮点击效果发现这个更好用, 下面的效果就类似一个图片在上文字在下的按钮,点击之后跳转到App
Link(destination: URL(string: "widget://xxxxxxxx")!, label: {
VStack {
Image("xxxxxxx")
.resizable()
.frame(width: 18, height: 18, alignment: .center)
Text("xxxxxx").font(.custom("DIN Alternate", size: 9)).fontWeight(.semibold).foregroundColor(.white)
}
})
知识点二:
如何实现富文本效果,在OC需要用到NSAttributeText这个属性,但是SwiftUI就非常便捷了
知识点三
如何实现小组件。类似于支付宝的效果
首先看图 是一个左右布局大致的层次架构如下:
// ZStack 用来放天气背景图
ZStack {
// 横向的布局 1、左边的天气数据+去支付宝看看 2、右边的4个按钮
HStack {
// 左边的天气数据+去支付宝看看
VStack {
}
//右边的4个按钮
VStack {
// 上面的2个按钮 扫一扫、收付款
HStack {
Link{}
Link{}
}
// 下面的2个按钮 出行、健康码
HStack {
Link{}
Link{}
}
}
}
}
ok,回到正文。
先粘贴代码
struct TestWidgetEntryView : View {
var entry: WWProvider.Entry
@Environment(\.widgetFamily) var family : WidgetFamily
var body: some View {
// default_no recommend
switch family {
case .systemSmall:
WidgetSmallView(entry: entry)
case .systemMedium:
// ZStack(alignment: .center, content: {
// Image("cake").resizable(capInsets: EdgeInsets.init(top: 0, leading: 0, bottom: 0, trailing: 0), resizingMode: .stretch)
WidgetMediumView(entry: entry)
// }) //ZStack
default:
Text("hello world")
}
}
}
@main
struct TestWidget: Widget {
let kind: String = "TestWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WWProvider()) { entry in
TestWidgetEntryView(entry: entry)
}
// .previewContext(WidgetPreviewContext(family: .systemSmall))
.supportedFamilies([.systemSmall, .systemMedium])
.configurationDisplayName("xxx")
.description("xxxxxx")
}
}
struct TestWidget_Previews: PreviewProvider {
static var previews: some View {
TestWidgetEntryView(entry: PosterEntry(date: Date(), poster: Poster(author: "Kcl3", content: "测试1")))
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
下面是Provider的代码
//
// Provider.swift
// Test
//
// Created by admin on 2021/4/27.
//
import WidgetKit
import SwiftUI
typealias Entry = PosterEntry
struct WWProvider: TimelineProvider {
func placeholder(in context: Context) -> PosterEntry {
// SimpleEntry(date: Date(), configuration: ConfigurationIntent())
// print("placeholder")
PosterEntry(date: Date(), poster: Poster(author: "Kcl1", content: "测试1"))
}
func getSnapshot(in context: Context, completion: @escaping (PosterEntry) -> ()) {
let entry = PosterEntry(date: Date(),poster: Poster(author: "Kcl21", content: "测试1"))
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [PosterEntry] = []
// PosterData.getTodayPoster { (result) in
// let poster: Poster
// print(result);
// switch result {
// case .success(let posterr):
// poster = posterr
// print(poster)
// case .failure(let error):
// print(error)
//
//
// poster=Poster(author: "Now", content: "Now格言");
// }
//
//
// let entryDate = Calendar.current.date(byAdding: .hour, value: 0, to: Date())!
// entries.append(PosterEntry(date: entryDate, poster: poster))
//
//
// let timeline = Timeline(entries: entries, policy: .atEnd)
// completion(timeline)
// }
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 1 ..< 20 {
let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
print(entryDate)
let entry = PosterEntry(date: entryDate, poster: Poster(author: "Kcl\(1+hourOffset)", content: "测试1"))
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// MARK: - 网络请求数据
struct PosterData {
static func getTodayPoster(completion: @escaping (Result<Poster, Error>) -> Void) {
// URLSession.shared.dataTask(with: <#T##URLRequest#>)
print("test")
let url = URL(string: "https://nowapi.navoinfo.cn/get/now/today")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error==nil else{
completion(.failure(error!))
return
}
let poster=posterFromJson(fromData: data!)
completion(.success(poster))
}
task.resume()
}
static func posterFromJson(fromData data:Data) -> Poster {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
guard let result = json["result"] as? [String: Any] else{
return Poster(author: "Now", content: "加载失败")
}
let author = result["author"] as! String
let content = result["celebrated"] as! String
let posterImage = result["poster_image"] as! String
//图片同步请求
var image: UIImage? = nil
if let imageData = try? Data(contentsOf: URL(string: posterImage)!) {
image = UIImage(data: imageData)
}
return Poster(author: author, content: content, posterImage: image)
}
}
继续完善
很多人好奇或者是有这么一个需求,需要1s去刷新一次小组件的UI, 其实是做不到的。那么您又会说:为什么有一些时钟,定时器小组件可以做到实时更新呢。其实如果你沉静下来去看官方文档你就知道了
下面的链接是告诉你,如何去显示动态的一个日期,开发文档也教你,如何实现类似时钟小组件的UI变换
https://developer.apple.com/documentation/widgetkit/displaying-dynamic-dates
开发文档很明确的说了:
每日预算通常包括40到70次刷新。该速率大致可转换为小部件每15至60分钟重新加载一次
https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date
那么刷新机制到底是怎么样的
3种机制
.atEnd 在缓存包最后一个时间之后刷新
.never 不刷新
.after(_ date: Date) 指定某个时间后刷新