【iOS小组件实战】gif动态小组件

1,534 阅读2分钟

前言


前面对小组件动画有了基本了解,最后讲到 clockHandRotationEffect 私有API的限制问题,这两天发现有大佬对 clockHandRotationEffect 私有API限制研究出了新的对策,可以采用 XCFramework 形式导入使用,根据这一思路尝试实现一个动态小组件效果。末尾附上完整示例。

引入Framework


注意:

  • 有大佬提供了编译好的 Framework 我们可以选择直接使用,也可以自己编译自己的 ClockHandRotation Framework。

  • 项目中需要设置 Framework 为 embed**,**否则会提示找不到

ClockHandRotationKit 已放到文末。我们可以使用 Package 引用也可以下载到本地。

dependencies: [
     .package(url: "https://github.com/octree/ClockHandRotationKit", from: "1.0.0")
]

我这里选择本地引用

image.png

处理gif展示


小组件是不支持加载 gif 图的,这里需要对 gif 进行处理,可以参考 DynamicWidget 的实现思路。

func getGif(_ gifName: String) -> UIImage.GifResult? {
    guard gifName.count > 0 else { return nil }

    guard let gifPath = Bundle.main.path(forResource: gifName, ofType: "gif") else {
        return nil
    }

    guard FileManager.default.fileExists(atPath: gifPath),
          let gifData = try? Data(contentsOf: URL(fileURLWithPath: gifPath))
    else {
        return nil
    }

    return UIImage.decodeGIF(gifData)
}

封装一个展示 gif 的控件,提供 gif 名称即可识别加载

import SwiftUI
import ClockHandRotationKit

...

struct GifImageView: View {
    var gifName: String // Bundle中 gif图片的名称
    var defaultImage: String // 默认图片

    var body: some View {
        if let gif = getGif(gifName) {
            GeometryReader { proxy in
                let width = proxy.size.width
                let height = proxy.size.height

                let arcWidth = max(width, height)
                let arcRadius = arcWidth * arcWidth
                let angle = 360.0 / Double(gif.images.count)

                ZStack {
                    ForEach(1...gif.images.count, id: \.self) { index in
                        Image(uiImage: gif.images[(gif.images.count - 1) - (index - 1)])
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(minWidth: 0, minHeight: 0)
                            .mask(
                                ArcView(arcStartAngle: angle * Double(index - 1),
                                        arcEndAngle: angle * Double(index),
                                        arcRadius: arcRadius)
                                .stroke(style: .init(lineWidth: arcWidth * 1.1, lineCap: .square, lineJoin: .miter))
                                .frame(width: width, height: height)
                                .clockHandRotationEffect(period: .custom(gif.duration))
                                .offset(y: arcRadius) // ⚠️ 需要先进行旋转,再设置offset
                            )
                    }
                }
                .frame(width: width, height: height)
            }
        } else {
            Image(systemName: defaultImage)
                .resizable()
                .aspectRatio(contentMode: .fill)
        }
    }
}

struct ArcView: Shape {
    var arcStartAngle: Double
    var arcEndAngle: Double
    var arcRadius: Double
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
                    radius: arcRadius,
                    startAngle: .degrees(arcStartAngle),
                    endAngle: .degrees(arcEndAngle),
                    clockwise: false)
        return path
    }
}

小组件UI


小组件中主要是UI布局展示,修改为自己想要的样式,我这里参考了 OneDay 项目的样式风格。

import SwiftUI
import WidgetKit

struct GifAnimateWidgetEntryView : View {
    var entry: CommonProvider.Entry

    var body: some View {
        VStack(alignment: .leading) {
            VStack(alignment: .leading, spacing: 0) {
                Text(dateInfo.day)
                    .font(.custom("DINAlternate-Bold", size: 28))
                    .foregroundColor(.white)
                + Text(" / \(dateInfo.month)")
                    .font(.custom("DINAlternate-Bold", size: 14))
                    .foregroundColor(.white)
                Text("\(dateInfo.year), \(dateInfo.weekday)")
                    .font(.custom("PingFangSC", size: 10))
                    .foregroundColor(.white.opacity(0.9))
            }

            Spacer()

            Text("不与伪君子争名,不与真小人争利")
                .font(.custom("PingFangSC", size: 14))
                .foregroundColor(.white)
                .lineSpacing(4)
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
        .baseShadow()
        .padding(.horizontal, 14)
        .padding(.top, 13)
        .padding(.bottom, 18)
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .background(
            Color.black.opacity(0.15)
        )
        .background(
            GifImageView(gifName: "transformer", defaultImage: "")
        )
    }
}

struct GifAnimateWidget: Widget {
    let kind: String = "GifAnimateWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: CommonProvider()) { entry in
            GifAnimateWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Gif动画小组件")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall])
        .adoptableWidgetContentMargin()
    }
}

#Preview(as: .systemSmall) {
    GifAnimateWidget()
} timeline: {
   CommonEntry(date: .now)
}

效果

v2-bd5fb7b4aeb33bfd7c4e4ca662b1fd74_1440w.gif

项目链接


代码量有点多,我放到了github上,需要的自取
github.com/MisterZhouZ…

参考资料


本文同步自微信公众号 "程序员小溪" ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。