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

iOS SwiftUI实现小组件开发(Widget Extension)_第1张图片
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就非常便捷了


iOS SwiftUI实现小组件开发(Widget Extension)_第2张图片
image.png

知识点三

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


iOS SwiftUI实现小组件开发(Widget Extension)_第3张图片
支付宝小组件效果

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

// 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) -> ()) {
        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) -> 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

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

iOS SwiftUI实现小组件开发(Widget Extension)_第4张图片
小组件刷新机制.png
3种机制
 .atEnd      在缓存包最后一个时间之后刷新
 .never      不刷新
 .after(_ date: Date) 指定某个时间后刷新

你可能感兴趣的:(iOS SwiftUI实现小组件开发(Widget Extension))