ShapeUp Logo

Swift Compatibility Platform Compatibility License - MIT Version GitHub last commit Mastodon Twitter

概述

一个 Swift Package,通过将 SwiftUI 形状重新定义为样式化角点的数组,使其更易于创建。(上面的徽标是用 100 行代码 + SwiftUI Text 创建的)

功能

演示应用

Example 文件夹中有一个演示此软件包功能的应用程序。

安装和使用

此软件包与 iOS 14+、macOS 11+、watchOS 7+、tvOS 14+ 和 visionOS 兼容。

  1. 在 Xcode 中,转到 File -> Add Packages
  2. 粘贴仓库的 URL:https://github.com/ryanlintott/ShapeUp 并按版本选择。
  3. 使用 import ShapeUp 导入软件包

这是否已准备好用于生产环境?

实际上,这取决于您。我目前在自己的 Old English Wordhord app 中使用此软件包。

此外,如果您发现错误或想要新功能,请添加 issue,我会尽快回复您。

支持此项目

ShapeUp 是开源且免费的,但如果您喜欢使用它,请考虑支持我的工作。

ko-fi

或者您可以购买一件带有 ShapeUp 徽标的 T 恤

ShapeUp T-Shirt


功能

Corner(角点)

一个带有指定 CornerStyle 的点,用于绘制路径和创建形状。

Corner(.rounded(radius: 5), x: 0, y: 10)

角点不存储有关其方向或先前和后续点位置的信息。当它们放入数组中时,其顺序被假定为它们的绘制顺序。这意味着您可以从此数组生成 Path。默认情况下,此路径被假定为闭合的,最后一个点连接回第一个点。

[
    Corner(.rounded(radius: 10), x: 0, y: 0),
    Corner(.cutout(radius: 5), x: 10, y: 0),
    Corner(.straight(radius: 5), x: 5, y: 10)
].path()

CornerStyle(角点样式)

一个枚举,用于存储 Corner 的样式信息。在所有情况下,半径都是一个 RelatableValue,可以是绝对值,也可以是相对于该角点最短线段长度的相对值。

Pink triangle with a point corner .point 一个简单的点角,没有属性。

Pink triangle with a rounded corner .rounded(radius: RelatableValue) 一个带半径的圆角。

Pink triangle with a concave cut corner .concave(radius: RelatableValue, radiusOffset: CGFloat) 一个凹角,其中半径决定切口的起点和终点,半径偏移量是凹角半径与半径之间的差值。半径偏移量主要用于内嵌凹角,通常保留默认值零。

Pink triangle with a straight cut corner .straight(radius: RelatableValue, cornerStyles: [CornerStyle] = []) 一个直线倒角角,其中半径决定切口的起点和终点。附加的角点样式可以用于倒角产生的两个角点。(您可以继续递归嵌套。)

Pink triangle with a cutout corner .cutout(radius: RelatableValue, cornerStyles: [CornerStyle] = []) 一个切口角,其中半径决定切口的起点和终点。附加的角点样式可以用于切口产生的三个角点。(同样,您可以继续递归嵌套。)

基本形状

CornerRectangleCornerTriangleCornerPentagon 是预构建的形状,您可以在其中自定义任何角点的样式。

示例

CornerRectangle([
    .topLeft: .straight(radius: 60),
    .topRight: .cutout(radius: .relative(0.2)),
    .bottomRight: .rounded(radius: .relative(0.8)),
    .bottomLeft: .concave(radius: .relative(0.2))
])
.fill()

CornerTriangle(
    topPoint: .relative(0.6),
    styles: [
        .top: .straight(radius: 10),
        .bottomRight: .rounded(radius: .relative(0.3)),
        .bottomLeft: .concave(radius: .relative(0.2))
    ]
)
.stroke()
    
CornerPentagon(
    pointHeight: .relative(0.3),
    topTaper: .relative(0.1),
    bottomTaper: .relative(0.3),
    styles: [
        .topRight: .concave(radius: 30),
        .bottomLeft: .straight(radius: .relative(0.3))
    ]
)
.fill()

CornerShape(角点形状)

一个协议,用于创建由 Corner 数组构建的形状。符合 SwiftUI InsettableShape 所需的路径和内嵌函数已经实现。

如何构建 CornerShape

public struct MyShape: CornerShape {
    public var insetAmount: CGFloat = .zero
    public let closed = true
   
    public func corners(in rect: CGRect) -> [Corner] {
        [
            Corner(x: rect.midX, y: rect.minY),
            Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY),
            Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY)
        ]
    }
}

使用 CornerShape

CornerShape 可以像 RoundedRectangle 或类似形状一样在 SwiftUI View 中使用。

MyShape()
    .fill()

角点也可以直接访问,以便在更复杂的形状中使用

public func corners(in rect: CGRect) -> [Corner] {
    MyShape()
        .corners(in: rect)
        .inset(by: 10)
        .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0)
}

CornerCustom(自定义角点)

有时您可能希望内联创建一个形状,而无需定义新的结构体。CornerCustom 是一个 CornerShape,它接受一个闭包,该闭包返回一个 Corner 数组。闭包本身需要是 Sendable 的,以便可以用于为 SwiftUI Shape 生成路径。

CornerCustom { rect in
    [
        Corner(x: rect.midX, y: rect.minY),
        Corner(.rounded(radius: 10), x: rect.maxX, y: rect.maxY),
        Corner(.rounded(radius: 10), x: rect.minX, y: rect.maxY)
    ]
}
.strokeBorder(lineWidth: 5)

Notch(凹口)

有时您想在形状的侧面切出一个凹口。当线条处于奇怪的角度时,这可能很难做到,但 Notch 使其变得容易。Notch 具有 NotchStyle、位置、长度和深度。

以下代码在第二个和第三个角点之间添加一个矩形凹口。addingNotch() 函数执行所有必要的计算,以将表示该凹口的角点添加到 Corner 数组中。

let notch = Notch(.rectangle, position: .relative(0.5), length: .relative(0.2), depth: .relative(0.1))

let corners = corners.addingNotch(notch, afterCornerIndex: 1)

NotchStyle(凹口样式)

两种基本样式是 .triangle.rectangle,两者都允许自定义 2 个或 3 个生成的凹口角点的角点样式。

/// Specify styles for each corner
let notch1 = .triangle(cornerStyles: [.rounded(radius: 10), .point, .straight(radius: 5)])
/// Or specify one style for all
let notch2 = .rectangle(cornerStyle: .rounded(radius: .relative(0.2))

还有一个 .custom 凹口样式,它接受一个闭包,该闭包基于与凹口的大小和方向匹配的 CGRect 返回一个 Corner 数组。

let notch = .custom { rect in
    [
        Corner(x: rect.minX, y: rect.minY),
        Corner(x: rect.minX, y: rect.midY),
        Corner(.rounded(radius: 15), x: rect.midX, y: rect.maxY),
        Corner(x: rect.maxX, y: rect.midY),
        Corner(x: rect.maxX, y: rect.minY)
    ]
}

Add CornerShape(添加角点形状)

完全用角点制成的形状有其局限性。只有直线和弧线是可能的。如果您只想使用角点绘制形状的一部分,您也可以使用添加到 Path.addOpenCornerShape().addClosedCornerShape() 函数来完成。

Vector2(向量2)

一种向量类型,用作 CGPoint 的替代方案,它符合所有 Vector2 协议。

Vector2Representable(可表示为向量2)

一个协议,添加了 vector: Vector2 属性。Vector2CGPointCorner 都符合此协议,并且这是任何其他 Vector2 协议所必需的。

其他属性和方法

point: CGPoint
corner(_ style:) -> Corner

数组扩展

points: [CGPoint]
vectors: [Vector2]
corners(_ style: CornerStyle?) -> [Corner]
corners(_ styles: [CornerStyle?]) -> [Corner]
bounds: CGRect
center: CGPoint
anchorPoint(_ anchor: RectAnchor) -> CGPoint
angles: [Angle]

Vector2Algebraic(向量2代数)

一个添加向量数学运算的协议。默认情况下仅应用于 Vector2,但如果需要,可以添加到任何其他 Vector2Representable

函数包括:magnitude(大小)、direction(方向)、normalized(归一化)、addition(加法)、subtraction(减法)以及与标量的 multiplication(乘法)或 division(除法)。

Vector2Transformable(可变换的向量2)

一个协议,将变换函数(move(移动)、rotate(旋转)、flip(翻转)、inset(内嵌))添加到任何 Vector2Representable 或该类型的数组。应用于 Vector2CGPointCorner

RectAnchor(矩形锚点)

一个枚举,用于指示矩形上的 9 个锚点位置之一。它主要用于从 CGRect 快速获取 CGPoint

// Current method
let point = CGPoint(x: rect.minX, y: rect.minY)
// ShapeUp method
let point = rect.point(.topLeft)

当获取点数组时,这尤其有用

// Current method
let points = [
    CGPoint(x: rect.minX, y: rect.midY),
    CGPoint(x: rect.midX, y: rect.midY),
    CGPoint(x: rect.maxX, y: rect.maxY)
]
// ShapeUp method
let points = rect.points(.left, .center, .bottomRight)

RelatableValue(相关值)

一个方便的枚举,表示相对值或绝对值。这在 ShapeUp 的许多情况下使用,以在定义参数时提供灵活性。

当设置圆角半径时,您可能想要一个固定值(如 20),或者您可能想要一个占最大值的 20% 的值,以便它可以按比例缩放。RelatableValue 为您提供了这两种选择。

let absolute = RelatableValue.absolute(20)
let relative = RelatableValue.relative(0.2)

稍后,该值通过运行 value(using total:) 函数来确定。绝对值将始终相同,但任何相对值都将被计算。在圆角半径的情况下,total 将是在给定两条线的长度和角点角度的情况下适合该角点的最大半径。

let radius = relatableRadius.value(using: maxRadius)

为了易于使用,RelatableValue 符合 ExpressibleByIntegerLiteralExpressibleByFloatLiteral。这意味着在许多情况下,编写绝对值时可以省略 .absolute()

// Both are the same
let cornerStyle = .rounded(radius: .absolute(5))
let cornerStyle = .rounded(radius: 5)

SketchyLine(草图线)

一个可动画的线条 Shape,其端点可以延伸,并且位置可以垂直于其方向偏移。

image

Text("Hello World")
    .alignmentGuide(.bottom) { d in
        // moves bottom alignment to text baseline
        return d[.firstTextBaseline]
    }
    .background(
        SketchyLines(lines: [
            .leading(startExtension: -2, endExtension: 10),
            .bottom(startExtension: 5, endExtension: 5, offset: .relative(0.05))
        ], drawAmount: 1)
            .stroke(Color.red)
        , alignment: .bottom
    )

Emboss or Deboss(浮雕或凹雕)

用于 InsettableShapeView 的扩展,用于创建浮雕或凹雕效果。

image

AnimatablePack(可动画包)

*Xcode 16+、iOS 17+、macOS 14+、watchOS 10+、tvOS 17+

使用 AnimatablePack 而不是嵌套 AnimatablePair 类型,在 Shape 中动画化大量属性

这是一个使用 AnimatablePair 的 animatableData 示例

struct MyShape: Animatable {
    var animatableData: AnimatablePair<CGFloat, AnimatablePair<RelatableValue, Double>> {
        get { AnimatablePair(insetAmount, AnimatablePair(cornerRadius, rotation)) }
        set {
            insetAmount = newValue.first
            cornerRadius = newValue.second.first
            rotation = newValue.second.second
        }
    }
}

您可以看到,一旦您开始添加多个属性,它会变得非常庞大。以下是如何改用 AnimatablePack

struct MyShape: Animatable {
    var animatableData: AnimatablePack<CGFloat, RelatableValue, Double> {
        get { AnimatablePack(insetAmount, cornerRadius, rotation) }
        set { (insetAmount, cornerRadius, rotation) = newValue() }
    }
}