Drawing

  • SwiftUI的内置形状
  • 制作自界说途径
  • 制作多边形和星星
  • 制作一个棋盘
  • 在SwiftUI中运用UIBezierPath和CGPath
  • 将SwiftUI视图转化为图画
  • 将SwiftUI视图烘托成PDF
  • 增加视觉作用含糊
  • 在SwiftUI视图上运用layerEffects增加Metal(Shaders)

概述

文章主要共享SwiftUI Modifier的学习过程,将运用事例的方式进行说明。内容深入浅出,Drawing展现部分调试成果,不过测验代码是彻底的。假如想要运行成果,能够移步Github下载code -> github事例链接

1、SwiftUI的内置形状

SwiftUI供给了五种常用内置形状:矩形,圆角矩形,圆形,椭圆形和胶囊形状。特别是后三个,他们的行为会依据巨细而有奇妙的不同。

struct FFShapesBuiltin: View {
    var body: some View {
        VStack {
            //长方形
            Rectangle()
                .fill(.gray)
                .frame(width: 200, height: 200)
            //4个圆角长方形
            RoundedRectangle(cornerRadius: 25.0)
                .fill(.red)
                .frame(width: 200, height: 200)
            //可独立独自设置圆角的长方形。
            UnevenRoundedRectangle(cornerRadii: .init(topLeading: 50, topTrailing: 50))
                .fill(.orange)
                .frame(width: 200, height: 200)
            //胶囊
            Capsule()
                .fill(.green)
                .frame(width: 100, height: 50)
            //椭圆形
            Ellipse()
                .fill(.blue)
                .frame(width: 100, height: 50)
            //圆形
            Circle()
                .fill(.black)
                .frame(width: 100, height: 50)
        }
    }
}

总共制作了五个图形,两个200*200,三个100*50,因为各种形状的特殊性,即使运用ZStack容器,也是能够全部显现的,我这儿运用的是VStack,更直观。

  • Rectangle制作一个基本款式的矩形。
  • RoundedRectangle同样制作的也是矩形,只不过能够将拐角按照一定的数值设置为圆角。
  • UnevenRoundedRectangle是一个圆角矩形,能够针对单个角设定为圆角。关于任何的角,默认值为0,即直角。但是能够更改值。
  • Capsule制作一个盒子,UI更像一个胶囊,其间较短的边会彻底圆化,比方界说一个100*50的图形,那么短边50便是编程圆边。
  • Ellipse制作一个基本款式的椭圆。
  • Circle制作一个高度和宽度相等的椭圆,即圆形。

调试成果

SwiftUI基础篇Drawing

2、制作自界说途径

SwiftUI能够依据Shape协议制作自界说途径,这样就能够创立自界说形状,能够是矩形、胶囊图形、圆形等。恪守这个协议并不难,因为需求做的便是支持一个接受CGRect并回来path的path(in:)办法。能够运用之前用CGPathUIBezierPath构建的任何途径,然后将成果转化为SwiftUI途径。

假如想运用SwiftUI的原生途径类型,创立他的一个实例变量,然后依据需求增加尽或许多的点或形状。不需求考虑色彩、填充或边框宽度。这儿只重视原始类型,这些设置是在运用自界说path时设定的。

struct ShrinkingSquares: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        for i in stride(from: 1, to: 100, by: 5.0) {
            let rect = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
            let insetRect = rect.insetBy(dx: i, dy: i)
            path.addRect(insetRect)
        }
        return path
    }
}
struct FFDramCustomPath: View {
    var body: some View {
        ShrinkingSquares()
            .stroke()
            .frame(width: 200, height: 200)
    }
}

调试成果

SwiftUI基础篇Drawing

3、制作多边形和星星

假如了解了SwiftUI的基本途径制作原理,就能够轻松的增加各种形状了。例如,创立一个星星,能够表示各种各样的图形,乃至多边形,经过一些数学模型。

构建星星

struct Star: Shape {
    //存储星星有多少个角,以及它的平滑度
    let corners: Int
    let smoothness: Double
    func path(in rect: CGRect) -> Path {
        //首先要保证制作的途径上至罕见两个角,不然途径无效
        guard corners >= 2 else { return Path() }
        //确定要制作图形的中心点(假设制作的是矩形,就获取矩形中心)
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        //制作方向是上方开端
        var currentAngle = -CGFloat.pi / 2
        //核算每个星星图形角需求移动多少距离
        let angleAdjustment = .pi * 2 / Double(corners * 2)
        //核算星星图形内部点需求移动多少X/Y
        let innerX = center.x * smoothness
        let innerY = center.y * smoothness
        //创立途径
        var path = Path()
        //移动到初始方位
        path.move(to: CGPoint(x: center.x * cos(currentAngle), y: center.y * sin(currentAngle)))
        //创立一个制作最低点,用来图形的居中核算
        var bottomEdge: Double = 0
        //循环遍历一切的点
        for corner in 0..<corners * 2 {
            //核算该点的方位
            let sinAngle = sin(currentAngle)
            let cosAngle = cos(currentAngle)
            let bottom: Double
            //当是2的倍数,制作的便是星星的外边际
            if corner.isMultiple(of: 2) {
                //存储Y点方位
                bottom = center.y * sinAngle
                //制作点与点之间的线段
                path.addLine(to: CGPoint(x: center.x * cosAngle, y: bottom))
            } else {
                //假如不是2的倍数,那么就制作内边
                bottom = innerY * sinAngle
                path.addLine(to: CGPoint(x: innerX * cosAngle, y: bottom))
            }
            //判别当时bottom是否是最地点的值,假如不是就更新
            if bottom > bottomEdge {
                bottomEdge = bottom
            }
            //移动到下一个点
            currentAngle += angleAdjustment
        }
        //核算画布(外面传递的frame)底部还有多少未运用的空间
        let unusedSpace = (rect.height / 2 - bottomEdge) / 2
        //创立transform,将途径向下移动,使图形垂直居中
        let transform = CGAffineTransform(translationX: center.x, y: center.y + unusedSpace)
        return path.applying(transform)
    }
}

3.1、制作五角星

struct FFDrawPolygons: View {
    var body: some View {
        Star(corners: 5, smoothness: 0.45)
            .fill(.red)
            .frame(width: 200, height: 200)
            .background(.green)
    }
}

3.2、制作多边形

struct FFDrawPolygons: View {
    var body: some View {
        Star(corners: 5, smoothness: 1)
            .fill(.red)
            .frame(width: 200, height: 200)
            .background(.green)
    }
}

因为星星是多边形,只需求将平滑度调整到1,就能够制作多边形,代码没变化。

调试成果

SwiftUI基础篇Drawing

4、制作一个棋盘

SwiftUI的途径不需求时连续的、孤立的形状,而是能够为多个矩形、椭圆或更多的形状,一切这些都能够组成一个。

构建棋盘

struct CheckerBorad: Shape {
    let rows: Int
    let columns: Int
    func path(in rect: CGRect) -> Path {
        var path = Path()
        //核算出行和列的空间巨细
        let rowSize = rect.height / Double(rows)
        let columnsSize = rect.height / Double(columns)
        //循环遍历一切行和列,使方块交替上色。
        for row in 0 ..< rows {
            for column in 0 ..< columns {
                if (row + column).isMultiple(of: 2) {
                    //满意条件制作方块
                    let startX = columnsSize * Double(column)
                    let startY = columnsSize * Double(row)
                    let rect = CGRect(x: startX, y: startY, width: columnsSize, height: rowSize)
                    path.addRect(rect)
                }
            }
        }
        return path
    }
}

因为集成链关系直接继承View,即与其他View视图运用方式一致

struct FFDrawCheckerboard: View {
    var body: some View {
        CheckerBorad(rows: 10, columns: 10)
            .fill(.gray)
            .frame(width: 300, height: 300)
    }
}

调试成果

SwiftUI基础篇Drawing

5、在SwiftUI中运用UIBezierPath和CGPath

假如你有运用UIBeizerPathCGPath的现有途径,转化他们在SwiftUI中运用很简略,因为Path结构体有一个直接来自CGPath的初始化器。

留意:UIBezierPath在macOS中不可用,假如想让SwiftUI代码跨平台,应该迁移到CGPath。

制作贝塞尔

extension UIBezierPath {
    //Unwrap标识为贝塞尔途径
    static var logo: UIBezierPath {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0.534, y: 0.5816))
        path.addCurve(to: CGPoint(x: 0.1877, y: 0.088), controlPoint1: CGPoint(x: 0.534, y: 0.5816), controlPoint2: CGPoint(x: 0.2529, y: 0.4205))
        path.addCurve(to: CGPoint(x: 0.9728, y: 0.8259), controlPoint1: CGPoint(x: 0.4922, y: 0.4949), controlPoint2: CGPoint(x: 1.0968, y: 0.4148))
        path.addCurve(to: CGPoint(x: 0.0397, y: 0.5431), controlPoint1: CGPoint(x: 0.7118, y: 0.5248), controlPoint2: CGPoint(x: 0.3329, y: 0.7442))
        path.addCurve(to: CGPoint(x: 0.6211, y: 0.0279), controlPoint1: CGPoint(x: 0.508, y: 1.1956), controlPoint2: CGPoint(x: 1.3042, y: 0.5345))
        path.addCurve(to: CGPoint(x: 0.6904, y: 0.3615), controlPoint1: CGPoint(x: 0.7282, y: 0.2481), controlPoint2: CGPoint(x: 0.6904, y: 0.3615))
        return path
    }
}

构建一个恪守Shape协议的贝塞尔View

运用的操控点被归化为0-1的规模,这样就能够在任何类型的容器中烘托它并将其缩放以适应可用空间。在SwiftUI中,这意味着创立一个transform,将贝塞尔途径锁放到最小的宽度或高度,然后将其运用到途径上。


struct ScaledBezier: Shape {
    let bezierPath: UIBezierPath
    func path(in rect: CGRect) -> Path {
        let path = Path(bezierPath.cgPath)
        //核算出咱们需求多大的途径来填充可用空间而不进行剪切
        let multiplier = min(rect.width, rect.height)
        //创立一个仿射transform,对两个维度运用相同的乘数
        let transform = CGAffineTransform(scaleX: multiplier, y: multiplier)
        //运用该份额回来成果
        return path.applying(transform)
    }
}

运用自界说贝塞尔图形制作

struct FFUIBezierPathAndCGPath: View {
    var body: some View {
        ScaledBezier(bezierPath: .logo)
            .stroke(lineWidth: 2)
            .frame(width: 200, height: 200)
    }
}

假如运用CGPath而不是UIBezierPath,操作就愈加的简略,能够直接运用 let path = path(…)来直接制作途径。

调试成果

SwiftUI基础篇Drawing

6、将SwiftUI视图转化为图画

wiftUI的ImageRenderer类能将任何SwiftUI视图烘托成图画,然后能够保存、共享或以其他方式重用。

6.1、基础款式

用这种方式进行图画的烘托,有四个要害留意点:

  1. 假如你没有指定,你的图形将以1倍的份额烘托,在2倍和3倍的分辨率的屏幕上看起来含糊
  2. 不能企图在主role之外运用ImageRenderer,这或许意味着用@MainActor符号你的烘托代码。
  3. 能够把想要烘托的SwiftUI视图放到ImageRenderer(conteng:)初始化器中,但发现把他们分离到一个专门的视图中会产生更简略的代码。
  4. 不像旧的UIGraphicsImageRenderer,没有简略的办法直接从ImageRenderer读取PNG或JPEG数据,所以能够在代码中看到,需求读取UIImage的成果,然后调用它的pngData()办法。这样的代码关于跨平台用户来说更杂乱。
struct RenderView: View {
    let text: String
    var body: some View {
        Text(text)
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
    }
}
struct FFConvertToImage: View {
    @State private var text = "Your text here"
    @State private var renderedImage = Image(systemName: "photo")
    var body: some View {
        VStack {
            renderedImage
            ShareLink("Export", item: renderedImage, preview: SharePreview(Text("Shared image"), image: renderedImage))
            TextField("Enter some text", text: $text)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .onChange(of: text) { oldValue, newValue in
            render()
        }
        //正如所看到的,它在显现视图时调用render(),也在文本改动时调用render().
    }
    @MainActor func render() {
        let renderer = ImageRenderer(content: RenderView(text: text))
        if let uiImage = renderer.uiImage {
            renderedImage = Image(uiImage: uiImage)
        }
    }
}

6.2、将视图转化为图画保存在相册中

假如版别在iOS15以下,那么SwiftUI的视图没有内置功能将视图烘托成图画,只能自己创造一个。要害点为运用UIHostingContontroller来包装视图,然后将其视图层次结构烘托到UIGraphicsImageRenderer中。

这最好运用View上的扩展来完成,这样就能够正常调用了。应该将视图封装在保管操控器中,调整保管操控器视图的巨细,使其成为SwitUI视图的内涵内容巨细,清除任何背景色以坚持烘托图画的洁净,然后将视图烘托成图画并回来。

extension View {
    func snapshot() -> UIImage {
        let controller = UIHostingController(rootView: self)
        let view = controller.view
        let targetSize = controller.view.intrinsicContentSize
        view?.bounds = CGRect(origin: .zero, size: targetSize)
        view?.backgroundColor = .clear
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        return renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
    }
}

要在SwiftUI中运用这个场景,应该把视图创立作为一个特点,这样就能够在需求的时候引用它,比方,在按钮被点击时。例如,将烘托一个文本视图编程一个图画,然后将其保存在用户的相册中。

struct FFConvertToImage: View {
    @Environment(\.displayScale) var displayScale
    var textView: some View {
        Text("Hello, metaBBLv")
            .padding()
            .foregroundStyle(.white)
            .background(.blue)
            .clipShape(Capsule())
    }
    var body: some View {
        VStack {
            textView
            Button("Save to image") {
                let image = textView.snapshot()
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
        }
    }
}

调试成果

  • 前两张图,当输入时能够动态更改视图Text,然后能够导出为Image到其他的运用
  • 第三张图是第二个例子,将视图转化为Image保存在相册中,目前还有些问题(横屏能够,竖屏GG)。有兴趣的小伙伴能够自己获取demo玩一下。
SwiftUI基础篇Drawing
SwiftUI基础篇Drawing
SwiftUI基础篇Drawing

7、将SwiftUI视图烘托成PDF

SwiftUI的ImageRenderer类能够将任何SwiftUI视图烘托为PDF。运用ImageRenderer创立PDF需求八个过程:

  1. 决定要烘托那些视图
  2. 创立一个SwiftUI能够写入图画数据的URL
  3. 在图画烘托器上调用render()来发动烘托代码。
  4. 告知SwiftUI你想要多大的PDF。或许是一个固定的巨细,例如A4或US Letter,也或许是你正在呈现的视图巨细。
  5. 创立一个CGContext目标来处理PDF页面
  6. 创立新的page
  7. 将SwiftUI视图呈现到该页面上
  8. 结束页面,并封闭PDF文档。
@MainActor
struct FFViewToPDF: View {
    var body: some View {
        Text("Hello, metaBBLv")
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
        ShareLink("Export PDF", item: render())
    }
    func render() -> URL {
        //1. 烘托文本
        let renderer = ImageRenderer(content:
                                        Text("Hello, metaBBLv")
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
        )
        //2. 保存到文档目录
        let url = URL.documentsDirectory.appending(path: "output.pdf")
        //3. 发动烘托进程
        renderer.render { size, context in
            //4. 告知SwiftUI,咱们的PDF应该和咱们烘托的视图一样大
            var box = CGRect(x: 0, y: 0, width: size.width, height: size.height)
            //5. 为PDF页面创立CGContext
            guard let pdf = CGContext(url as CFURL, mediaBox: &box, nil) else {
                return
            }
            //6. 创立一个新的PDF页面
            pdf.beginPDFPage(nil)
            //7. 将SwiftUI视图数据烘托到页面
            context(pdf)
            //8. 结束操作并封闭文件
            pdf.endPDFPage()
            pdf.closePDF()
        }
        return url
    }
}
@MainActor
#Preview {
    FFViewToPDF()
}

调试成果

SwiftUI基础篇Drawing
SwiftUI基础篇Drawing
SwiftUI基础篇Drawing

8、增加视觉作用含糊

SwiftUI有一个非常简略的特效UIVisualEffectView,它结合了ZStack和background修饰符的一些特性。 能够调整“厚度”,原料从最薄到最厚依次是:

  • .ultraThimMaterial
  • .thinNaterial
  • .regularMaterial
  • .thickMaterial
  • .ultraThickMaterial
struct FFVisualEffectBlurs: View {
    var body: some View {
        //将一些文本放在图画上,对文本运用标准含糊作用,
        ZStack {
            Image(.chrysanthemumTea)
            Text("Hi, metaBBLv")
                .padding()
                .background(.thinMaterial)
        }
        //假如运用的是非必须前景款式,SwiftUI会主动调整文本色彩,使其作用更杰出
        ZStack {
            Image(.chrysanthemumTea)
            Text("Hi, metaBBLv")
                .foregroundStyle(.secondary)
                .padding()
                .background(.ultraThinMaterial)
        }
    }
}

调试成果

SwiftUI基础篇Drawing

9、在SwiftUI视图上运用layerEffects增加Metal(Shaders)

SwiftUI供给了与Metal上色器的广泛集成,就在视图等级,能够以杰出的性能操作色彩、形状等等。过程分为三步:

  1. 用你的作色器创立一个Metal文件。这必须有一个切当的函数签名,取决于你想要运用的作用。
  2. 创立你的SwiftUI视图,并附加一个或多个作用。
  3. 可选的为视图增加视觉作用,以便在不改动布局的情况下读取视图的巨细。

Metal文件

这儿面的每一个上色器作用都在下面SwiftUI文件中运用了,因为我偷闲了,并没有把每个都分脱离做成一组一组的,哈哈哈哈。

//
//  FFMetal.metal
//  FFModifier
//
//  Created by BBLv on 2023/8/23.
//
//  要构建需求从视图冲采样色彩的上色器,将Metal文件导入头文件#include <SwiftUI/SwiftUI_Metal.h>,然后确保你的作色器签名接受方位和实例(SwiftUI::Layer)
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
//在该上色器中,SwiftUI需求前两个参数,它将主动传递视图的方位即当时色彩,第二个和其他参数都是我创立的,需求手动发送,这个上色器,我传递了相似棋盘的方块
[[ stitchable ]] half4 checkerboard(float2 position, half4 currentColor, float size, half4 newColor) {
    uint2 posInChecks = uint2(position.x / size, position.y / size);
    bool isColor = (posInChecks.x ^ posInChecks.y) & 1;
    return isColor ? newColor * currentColor.a : half4(0.0, 0.0, 0.0, 0.0);
}
//首先,你能够经过上色器放置在TimeLineView内并发送日期值来制作动画上色器。例如,能够创立一个开端日期并发送该开端日期和当时日期之间的差异来为shader供给动力
[[ stitchable ]] half4 noise(float2 position, half4 currentColor, float time) {
    float value = fract(sin(dot(position + time, float2(12.9898, 78.233))) * 43758.5453);
    return half4(value, value, value, 1) * currentColor.a;
}
//像素化的上色器:将函数的输入限制为下限0.0001,以避免除以0,然后将每个像素的方位除以强度,四舍五入,然后再次相乘,导致像素数据被丢弃。真正的作业是调用layer.sample(),它从附加了该上色器的视图中读取一种色彩。
[[ stitchable ]] half4 pixellate(float2 position, SwiftUI::Layer layer, float strength) {
    float min_strength = max(strength, 0.0001);
    float coord_x = min_strength * round(position.x / min_strength);
    float coord_y = min_strength * round(position.y / min_strength);
    return layer.sample(float2(coord_x, coord_y));
}
//能够将一个像素从一个方位移动到赢一个方位,而坚持其他方位不变。着意味着上色器只需求接受最小值的像素方位,因而能够创立波形实例
[[ stitchable ]] float2 simpleWave(float2 position, float time) {
    return position + float2 (sin(time + position.y / 20), sin(time + position.x / 20)) * 5;
}
//假如想要一个更像水下视图的杂乱波涛上色器,那么就需求读取图画的整体巨细。这需求更多的思考,因为需求将歪曲作用包装在视觉作用中以供给视图的size。首先,这是一个更杂乱的波涛作用,需求视图的巨细,但也有速度、强度和波涛频率的选项,以便愈加可定制,
[[ stitchable ]] float2 complexWave(float2 position, float time, float2 size, float speed, float strength, float frequency) {
    float2 normalizedPosition = position / size;
    float moveAmount = time * speed;
    position.x += sin((normalizedPosition.x + moveAmount) * frequency) * strength;
    position.y += cos((normalizedPosition.y + moveAmount) * frequency) * strength;
    return  position;
}
//浮雕过滤器上色器
[[ stitchable ]] half4 emboss(float2 position, SwiftUI::Layer layer, float strength) {
    half4 current_color = layer.sample(position);
    half4 new_color = current_color;
    new_color += layer.sample(position + 1) * strength;
    new_color -= layer.sample(position - 1) * strength;
    return half4(new_color);
}

实际运用作用

此处无需多言,享受视觉作用,不得不说,Metal是真的牛boi

struct FFMetalShaders: View {
    let startDate = Date()
    let startDate1 = Date()
    let startDate2 = Date()
    @State private var strength = 3.0
    var body: some View {
        ScrollView {
            Image(systemName: "figure.run.circle.fill")
                .font(.system(size: 300))
                .colorEffect(ShaderLibrary.checkerboard(.float(10), .color(.blue)))
            TimelineView(.animation) { context in
                Image(systemName: "figure.run.circle.fill")
                    .font(.system(size: 300))
                    .colorEffect(ShaderLibrary.noise(.float(startDate.timeIntervalSinceNow)))
            }
            //上色器需求作为涂层作用来调用,它告知SwiftUI传入整个涂层以及咱们正在处理的当时像素的方位。
            Image(systemName: "figure.run.circle.fill")
                .font(.system(size: 300))
                .layerEffect(ShaderLibrary.pixellate(.float(10)), maxSampleOffset: .zero)
            //别的一种作用是运用distortionEffect()修改器激活的
            TimelineView(.animation) { context in
                Image(systemName: "figure.run.circle.fill")
                    .font(.system(size: 300))
                    .distortionEffect(ShaderLibrary.simpleWave(.float(startDate1.timeIntervalSinceNow)), maxSampleOffset: .zero)
            }
            //要运用它,同时需求运用visualEffect()和distortionEffect()
            TimelineView(.animation) { context in
                Image(systemName: "figure.run.circle.fill")
                    .font(.system(size: 300))
                    .visualEffect { content, proxy in
                        content
                            .distortionEffect(ShaderLibrary.complexWave(
                                .float(startDate2.timeIntervalSinceNow),
                                .float2(proxy.size),
                                .float(0.5),
                                .float(8),
                                .float(10)
                            ), maxSampleOffset: .zero)
                    }
            }
            //创立一个简略的浮雕过滤器,包含一个slider操控器,用户操控浮雕强度的SwiftUI
            Image(systemName: "figure.run.circle.fill")
                .foregroundStyle(.linearGradient(colors: [.orange, .red], startPoint: .top, endPoint: .bottom))
                .font(.system(size: 300))
                .layerEffect(ShaderLibrary.emboss(.float(strength)), maxSampleOffset: .zero)
            Slider(value: $strength, in: 0...20)
            //成果可得,将Metal上色器增加到SwiftUI视图中非常简略,无需很多代码即可解锁非常杂乱的特效。
        }
    }
}

调试成果

SwiftUI基础篇Drawing