import WidgetKit
import SwiftUI
import Alamofire
import HandyJSON
/// 定义模型
struct Model {
var coin: String
var USD: String
var CNY: String
var Range: String
}
/********************************************
// 定义网络请求
********************************************** */
struct ApiRequest {
/// 网络请求
/// - Parameters:
/// - coins: 请求的币种数组
/// - completion: 处理的模型数组
static func apiRequest(coins: [[String: String]],completion: @escaping (Result<[Model], Error>) -> Void) {
var models: [Model] = []
for (_,dic) in coins.enumerated() {
let url = URL(string: "http://xxxxxxxxxxxxxx?symbol=\(dic["symbol"]!)")
/// URLSession 设置头文件
var request = URLRequest.init(url: url!)
request.method = .get
request.addValue("IOS", forHTTPHeaderField: "way")
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {
/// MARK: -- 即使数据请求失败,也需要创建一个默认的模型传递过去, 不然会出现列表模糊不清的情况
models.append(Model(coin: dic["symbol"]!, USD: "-- error", CNY: "--", Range: "--"))
/// MARK:-- 强制返回数据的数量 和 需要请求的数量一致,否则会出现数据丢失的情况
if models.count == coins.count {
completion(.success(models))
}
return
}
/// 数据的处理在这里要全部完成, 需要图片下载也应同步处理下载并处理。
var poetry = poetryFromJson(fromData: data!)
if poetry.coin == "加载失败" {
/// 数据解析失败后。展示名称 ,其余展示 “ -- ” 即可
poetry.coin = dic["symbol"]!
}
models.append(poetry)
/// MARK:-- 强制返回数据的数量 和 需要请求的数量一致,否则会出现数据丢失的情况
/// 不能以 idx == coins.count - 1 的判断来返回数据。 必须等到数据全部处理完毕
if models.count == coins.count {
completion(.success(models))
}
}
task.resume()
}
}
/// 解析 网络请求 数据
/// - Parameter data: 请求的数据
/// - Returns: 数据模型
static func poetryFromJson(fromData data: Data) -> Model {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
guard let data = json["data"] as? [String: Any] else {
/// 数据解析失败时, 返回默认的model
return Model(coin: "加载失败", USD: "--", CNY: "--", Range: "--")
}
/// 测试显示和刷新的时间
let currentDate = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH时mm分ss秒"
let dateStr = dateFormatter.string(from: currentDate)
let coin = data["symbol"] as! String
let open = dateStr
let amount = data["amount"] as! String
let vol = "-2.2%"
return Model(coin: coin, USD: open, CNY: amount, Range: vol)
}
/// 模拟数据
/// 只做本地数据展示用。调试列表样式时可用
static func modelForJson() -> [Model] {
var models: [Model] = []
let currentDate = Date()
//设定15秒更新一次数据
let updateDate = Calendar.current.date(byAdding: .second, value: 15, to: currentDate)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH时mm分ss秒"
let dateStr = dateFormatter.string(from: updateDate)
let titles = [["coin":dateStr,"USD":"11111","CNY":"¥1111","Range":"+5.5%"],
["coin":"ETH/USDT","USD":"222222","CNY":"¥22222","Range":"+1.5%"],
["coin":"HT/USDT","USD":"33333","CNY":"¥33333","Range":"-6.1%"],
["coin":"ICP/USDT","USD":"4444","CNY":"¥444444","Range":"+3.50%"],
["coin":"XRP/USDT","USD":"55555","CNY":"¥55555","Range":"-5.56%"]]
for item in titles {
let temp = Model(coin: item["coin"]!, USD: item["USD"]!, CNY: item["CNY"]!, Range: item["Range"]!)
models.append(temp)
}
return models
}
}
struct Provider: TimelineProvider {
/// 预览数据
static let titles = [["coin":"BTC/USDT","USD":"48059","CNY":"¥248202","Range":"+5.5%"],
["coin":"ETH/USDT","USD":"3399.52","CNY":"¥33885","Range":"+1.5%"],
["coin":"ETC/USDT","USD":"9.2991","CNY":"¥48822","Range":"-6.1%"],
["coin":"ICP/USDT","USD":"22.19","CNY":"¥120052","Range":"+3.50%"],
["coin":"XRP/USDT","USD":"0.008527","CNY":"¥8522","Range":"-5.56%"],
["coin":"ADA/USDT","USD":"0.008527","CNY":"¥8522","Range":"-5.56%"]]
/// 占位视图
func placeholder(in context: Context) -> SimpleEntry {
var models: [Model] = []
for item in Provider.titles {
let temp = Model(coin: item["coin"]!, USD: item["USD"]!, CNY: item["CNY"]!, Range: item["Range"]!)
models.append(temp)
}
return SimpleEntry(date: Date(), models: models)
}
/// 快照
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
var models: [Model] = []
for item in Provider.titles {
let temp = Model(coin: item["coin"]!, USD: item["USD"]!, CNY: item["CNY"]!, Range: item["Range"]!)
models.append(temp)
}
let entry = SimpleEntry(date: Date(), models: models)
completion(entry)
}
/// 数据更新
func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
// 设定5分钟更新一次数据,设置5秒更新也不起作用,系统决定更新时间,大概在5分钟左右
let currentDate = Date()
let updateDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)
let titles = [["symbol":"BTC/USDT"],
["symbol":"ZRX/USDT"],
["symbol":"VET/USDT"],
["symbol":"XMR/USDT"],
["symbol":"XLM/USDT"],
["symbol":"XLM/USDT"]]
ApiRequest.apiRequest(coins: titles) { result in
var models: [Model]
if case .success(let response) = result {
models = response
} else {
models = [Model(coin: "--", USD: "--", CNY: "--", Range: "--")]
}
let entry = SimpleEntry(date: currentDate, models: models)
/// MARK:-- 此处需要特别注意⚠️
/// MARK:-- 在数据全部处理完毕之后进行传递。
/// MARK:-- .after(updateDate!) 在设定的 5分钟后更新数据,实际更新时间是由系统控制的。大概在 5-6 分钟之间
let timeline = Timeline(entries: [entry], policy: .after(updateDate!))
completion(timeline)
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
/// 相当于 模型数组
let models: [Model]
}
/// 自定义View
struct StarexWidgetListView: View {
/// MARK:-- 貌似直接设置Model 取值时会有错误。
var coin: String
var USD: String
var CNY: String
var Range: String
var RangeColor: Color
var body: some View {
let height: CGFloat = 30
/// MARK: -- 添加点击事件。
/// 尝试在 StarexWidgetEntryView 的 ForEach 中添加时会导致布局错乱
Link(destination: URL(string: "url://Coin---\(coin)")!) {
HStack(alignment: .center, spacing: 0) {
Text(coin)
.font(.system(size: 15))
.multilineTextAlignment(.leading)
.frame(height: height, alignment: .center)
.foregroundColor(Color.black)
.padding(10)
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(USD)
.font(.system(size: 15))
.multilineTextAlignment(.trailing)
.frame(height: height/2, alignment: .top)
.foregroundColor(Color.black)
Text(CNY)
.font(.system(size: 12))
.multilineTextAlignment(.trailing)
.frame(height: height/2, alignment: .top)
.foregroundColor(Color.gray)
}.frame(maxHeight: height)
Text(Range)
.font(.system(size: 14))
.multilineTextAlignment(.center)
.frame(width: 70,height: height, alignment: .center)
.foregroundColor(Color.white)
.background(RangeColor)
.cornerRadius(4)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
.frame(maxHeight: height,alignment: .center)
.environment(\.colorScheme, .dark)
}
}
}
// View
struct StarexWidgetEntryView : View {
var entry: Provider.Entry
@State private var selected = 0
var body: some View {
GeometryReader { geometry in
VStack {
HStack(alignment: .center, spacing: 4) {
Image("icon")
.resizable()
.frame(width: 28, height: 28)
.scaledToFill()
.ignoresSafeArea(.all)
Text("xxxx行情")
.foregroundColor(Color.gray)
.frame(width: 100,height: 40)
.multilineTextAlignment(.leading)
.font(.system(size: 15))
Spacer()
}
if entry.models.count == 0 {
Text("请先添加自选币种")
.foregroundColor(Color.gray)
.font(.title)
.multilineTextAlignment(.center)
} else {
// 循环添加视图。
ForEach(0 ..< entry.models.count) { idx in
// Link 需要在 自定义 view 中添加,在这里添加时会造成布局错乱
// Link(destination:URL(string: "url://Coin ----- \(self.entry.models[idx].coin)")!) {
StarexWidgetListView(coin: self.entry.models[idx].coin,
USD: self.entry.models[idx].USD,
CNY: self.entry.models[idx].CNY,
Range: self.entry.models[idx].Range,
RangeColor: .red)
// widget 在此添加,点击时获取 URl 固定是最后一行url。 会导致数据获取不准确。
// 比如 ForEach 添加 5 行数据, 点击了 第一行, 获取的还是第五行的下标
// .widgetURL(URL(string: "url://Coin---\(self.entry.models[idx].coin)"))
if idx != entry.models.count - 1 {
Divider().padding(EdgeInsets(top: 2, leading: 10, bottom: 0, trailing: 0))
}
// }
}
}
// padding 给整个视图添加边距。由系统自动设置
}.padding()
}
}
}
@main
struct StarexWidget: Widget {
/// widget 主程序入口,这里只支持最大控件,需要支持中小控件,
/// 可通过 @Environment(\.widgetFamily) var family: WidgetFamily 获取不同样式,然后分别返回不用的View
let kind: String = "StarexWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
StarexWidgetEntryView(entry: entry)
}
.configurationDisplayName("币种行情")
.description("行情速递,行情趋势")
/// 设置只支持大控件
.supportedFamilies([.systemLarge])
}
}
// MARK: 字符串转字典
extension String {
func toDictionary() -> [String : Any] {
var result = [String : Any]()
guard !self.isEmpty else { return result }
guard let dataSelf = self.data(using: .utf8) else {
return result
}
if let dic = try? JSONSerialization.jsonObject(with: dataSelf,
options: .mutableContainers) as? [String : Any] {
result = dic
}
return result
}
}
刷新Widget
刷新widget 的语法只支持 swift。主项目是 OC时, 需要创建桥接文件
import Foundation
import WidgetKit
@available(iOS 14.0, *)
@objcMembers class WidgetManage: NSObject {
static func reloadWidget(){
/// 刷新全部 widget。还可以指定刷新其中一个widget
WidgetCenter.shared.reloadAllTimelines()
}
}
然后在 Appdelegate
的 didFinishLaunchingWithOptions
调用刷新,也可在 app 退出后刷新一次。
URL
Link 点击跳转处理
OC
// widget
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options{
if ([url.scheme isEqualToString:@"StarexWidget"]){
//执行跳转后的操作
}
NSLog(@"widget 跳转:\n url.absoluteURL:%@ - \nurl.absoluteString:%@ - \nurl.relativeString%@ - \nurl.scheme%@",url.absoluteURL,url.absoluteString,url.relativeString,url.scheme);
return YES;
}
Swift -- 没有 ScreenDelegate
的情况下
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if url.relativeString == "qwer" {
print("touchup!!!")
}
if url.relativeString == "url://123" {
//TODO:
print("哈哈哈哈哈哈哈url://123")
}
print("跳转链接: \(url.absoluteURL) --- \(url.scheme) -- \(url.relativeString)")
return true
}
Pods 共享
目前好像只支持 swift 库。 OC库即使共享了也导入不了
platform :ios, '9.0'
use_frameworks!
# 共享 Pods
# 需要共享给 widget 的库需要单独写在外部
def share_pods
pod 'HandyJSON'
end
# 第一个 Widget
target 'TestWidgetExtension' do
share_pods
end
# 第二个 Widget
target 'CafeineDrinkExtension' do
share_pods
end
# 主工程
target 'WWXHCamera' do
pod 'Alamofire'
share_pods
end