SwiftUI 支持即时模式绘制视图 - Canvas

前言

在 SwiftUI 中,你可以使用 Shape 的 API 去绘制你所需要的 2D 图形。但最终,SwiftUI 框架会将你绘制的所有图形转换为 SwiftUI 视图并去渲染它们。这种方法有利有弊,当我们需要绘制复杂的图形时,我们需要组合多个简单图形去实现。

但现在,我们可以在不组合多个形状的情况下绘制丰富的 2D 图形。这就需要使用到我们接下来要介绍的 Canvas 视图。

简单使用

Canvas 视图支持即时模式绘制,无需使用 Shape API。我们可以用它来画任何我们想要的东西,以一种程序化的方式,逐行绘制。让我们看一个下面的这个小例子。示例代码如下:

less 复制代码
struct ContentView: View {
    var body: some View {
        Canvas(
            opaque: true,
            colorMode: .linear,
            rendersAsynchronously: false
        ) { context, size in
            let rect = CGRect(origin: .zero, size: size)
            

            var path = Circle().path(in: rect)
            context.fill(path, with: .color(.blue))
        }
    }
}

效果图如下:

正如你在上面的例子中看到的,我们创建了一个 Canvas 视图作为 ContentView 的根视图。它接受一些参数,允许我们用不透明、颜色模式和异步渲染选项配置画布。

我们应该把所有的绘图逻辑放在传递给 Canvas 视图的闭包中。这个闭包称为渲染器。渲染器闭包为我们提供了一个 GraphicalContext 的实例,我们用它来绘制内容和画布的大小。

GraphicsContext 类型的实例是渲染器闭包的 inout 参数。这意味着我们可以在绘制内容时对其进行适当的修改。比如我们在当前圆形的左上角再绘制一个紫色小圆形,示例代码如下:

less 复制代码
struct ContentView: View {
    var body: some View {
        Canvas(
            opaque: true,
            colorMode: .linear,
            rendersAsynchronously: false
        ) { context, size in
            context.opacity = 0.3
            
            let rect = CGRect(origin: .zero, size: size)
            
            var path = Circle().path(in: rect)
            context.fill(path, with: .color(.red))

            let newRect = rect.applying(.init(scaleX: 0.5, y: 0.5))
            path = Circle().path(in: newRect)
            context.fill(path, with: .color(.red))
        }
    }
}

效果图如下:

如上图所示,我们调整了上下文的不透明度,它影响了该线之后出现的所有绘图逻辑。GraphicsContext 类型允许我们调整许多绘图过程参数,如不透明度、缩放和混合模式。它还允许我们使用 addFilter 函数添加不同的过滤器。

GraphicsContext 类型提供描边、填充和剪辑功能,允许我们绘制任何需要的路径。但它也提供了绘制功能,允许我们绘制文本和图像。

绘制文本和图像

需要注意的是,我们不能直接通过 Canvas 绘制文本或图像类型的实例。我们应该使用 GraphicsContext 类型上的 resolve 函数将它们转换为 draw 函数接受的格式。resolve 函数返回一个 ResolvedTextResolvedImage 类型的实例,它允许我们调整已转换类型对象的阴影。代码示例如下:

css 复制代码
struct ContentView: View {
    var body: some View {
        Canvas(
            opaque: true,
            colorMode: .linear,
            rendersAsynchronously: false
        ) { context, size in
            let rect = CGRect(origin: .zero, size: size)
            let text = Text(verbatim: "Canvas Text").font(.title)
            var resolvedText = context.resolve(text)
            resolvedText.shading = .color(.white)
            context.draw(resolvedText, in: rect)
        }
    }
}

效果图如下:

你不仅可以使用 Canvas 类型来绘制文本和图像,还可以绘制任何 SwiftUI 视图。但在此之前,我们应该在创建画布时使用符号闭包来注册它们。符号闭包中的每个 SwiftUI 视图都应该有其唯一的标签,以便我们稍后在渲染器闭包中通过 id 解析视图。示例代码如下:

css 复制代码
struct ContentView: View {
    private let symbolID = 1
    var body: some View {
        Canvas(
            opaque: true,
            colorMode: .linear,
            rendersAsynchronously: false
        ) { context, size in
            
            let rect = CGRect(origin: .zero, size: size)
            
            if let symbol = context.resolveSymbol(id: symbolID) {
                context.draw(symbol, in: rect)
            }
        } symbols: {
            Text(verbatim: "Canvas Text")
                .foregroundColor(.blue)
                .tag(symbolID)
        }
    }
}

效果图如下:

动画

Canvas 视图不支持动画,但是你可以用动画调度器把它嵌入到 TimelineView中。示例代码如下:

less 复制代码
struct ContentView: View {
    var body: some View {
        TimelineView(.animation) { timelineContext in
            let value = secondsValue(for: timelineContext.date)
            
            Canvas(
                opaque: true,
                colorMode: .linear,
                rendersAsynchronously: false
            ) { context, size in
                let newSize = size.applying(.init(scaleX: value, y: 1))
                let rect = CGRect(origin: .zero, size: newSize)
                
                context.fill(
                    Rectangle().path(in: rect),
                    with: .color(.purple)
                )
            }
        }
    }
    
    private func secondsValue(for date: Date) -> Double {
        let seconds = Calendar.current.component(.second, from: date)
        return Double(seconds) / 60
    }
}

该代码的实际效果大家可以自行在 SwiftUI 中的预览中自行查看。

相关推荐
小溪彼岸2 天前
【iOS小组件】小组件尺寸及类型适配
swiftui·swift
文件夹__iOS7 天前
[SwiftUI 开发] @dynamicCallable 与 callAsFunction:将类型实例作为函数调用
ios·swiftui·swift
小溪彼岸7 天前
【iOS小组件】iOS17与低版本兼容适配
swiftui·swift
Mamong8 天前
SwiftUI疑难杂症(1):sheet content多次执行
ios·swiftui·swift
AUV110712 天前
Mac剪贴板历史全记录!
macos·swiftui·mac·效率工具·实用工具·剪贴板·clipboard
AUV110712 天前
Mac 上哪个剪切板增强工具比较好用? 好用剪切板工具推荐
macos·swiftui·mac·剪贴板·clipboard·剪贴板增强·app 推荐
多彩电脑14 天前
SwiftUI里的ForEach使用的注意事项
macos·ios·swiftui·swift
Swift社区16 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
冯志浩16 天前
Harmony NEXT:如何给数据库添加自定义分词
harmonyos·掘金·金石计划
humiaor17 天前
Xcode报错:No exact matches in reference to static method ‘buildExpression‘
swiftui·xcode