iOS SwiftUI实现小组件开发(Widget Extension)

1.jpg

发文初衷

话说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就非常便捷了


image.png

知识点三

如何实现小组件。类似于支付宝的效果


支付宝小组件效果

首先看图 是一个左右布局大致的层次架构如下:

// 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

那么刷新机制到底是怎么样的

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

推荐阅读更多精彩内容