一个 Swift Package,通过将 SwiftUI 形状重新定义为样式化角点的数组,使其更易于创建。(上面的徽标是用 100 行代码 + SwiftUI Text 创建的)
功能
Corner
,一个带有 style
属性的 CGPoint
。CornerStyle
选项:.point
、.rounded
、.straight
、.cutout
和 .concave
CornerRectangle
、CornerTriangle
和 CornerPentagon
。CornerShape
,一个协议,用于使用角点数组创建您自己的开放或闭合形状。CornerCustom
,用于内联构建角点形状,而无需创建新类型。NotchStyle
的 Notch
(凹口)。.addOpenCornerShape()
或 .addClosedCornerShape()
,用于向 SwiftUI Path
添加一些角点Vector2
,一种类似于 CGPoint
的新类型,但用于执行向量数学运算。Vector2Representable
协议,添加了符合其他 Vector2 相关协议所需的 .vector
属性。Vector2Algebraic
协议,用于向 Vector2
添加向量代数功能Vector2Transformable
协议,包含用于转换点数组的方法。RectAnchor
,一个枚举,用于表示矩形上的所有主要锚点。用于变换函数。RelatableValue
,一个枚举,用于存储 .relative
或 .absolute
值。SketchyLine
,一个可动画的线条 Shape
,可对齐到框架边缘,并且可以延伸到框架之外。.emboss()
或 .deboss()
任何 SwiftUI Shape
或 View
。AnimatablePack
,作为 AnimatablePair
的替代方案,可以接受任意数量的属性。Example
文件夹中有一个演示此软件包功能的应用程序。
此软件包与 iOS 14+、macOS 11+、watchOS 7+、tvOS 14+ 和 visionOS 兼容。
File -> Add Packages
https://github.com/ryanlintott/ShapeUp
并按版本选择。import ShapeUp
导入软件包实际上,这取决于您。我目前在自己的 Old English Wordhord app 中使用此软件包。
此外,如果您发现错误或想要新功能,请添加 issue,我会尽快回复您。
ShapeUp 是开源且免费的,但如果您喜欢使用它,请考虑支持我的工作。
或者您可以购买一件带有 ShapeUp 徽标的 T 恤
一个带有指定 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()
一个枚举,用于存储 Corner
的样式信息。在所有情况下,半径都是一个 RelatableValue
,可以是绝对值,也可以是相对于该角点最短线段长度的相对值。
.rounded(radius: RelatableValue)
一个带半径的圆角。
.concave(radius: RelatableValue, radiusOffset: CGFloat)
一个凹角,其中半径决定切口的起点和终点,半径偏移量是凹角半径与半径之间的差值。半径偏移量主要用于内嵌凹角,通常保留默认值零。
.straight(radius: RelatableValue, cornerStyles: [CornerStyle] = [])
一个直线倒角角,其中半径决定切口的起点和终点。附加的角点样式可以用于倒角产生的两个角点。(您可以继续递归嵌套。)
.cutout(radius: RelatableValue, cornerStyles: [CornerStyle] = [])
一个切口角,其中半径决定切口的起点和终点。附加的角点样式可以用于切口产生的三个角点。(同样,您可以继续递归嵌套。)
CornerRectangle
、CornerTriangle
和 CornerPentagon
是预构建的形状,您可以在其中自定义任何角点的样式。
示例
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()
一个协议,用于创建由 Corner
数组构建的形状。符合 SwiftUI InsettableShape 所需的路径和内嵌函数已经实现。
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
可以像 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
是一个 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
具有 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)
两种基本样式是 .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)
]
}
完全用角点制成的形状有其局限性。只有直线和弧线是可能的。如果您只想使用角点绘制形状的一部分,您也可以使用添加到 Path
的 .addOpenCornerShape()
和 .addClosedCornerShape()
函数来完成。
一种向量类型,用作 CGPoint 的替代方案,它符合所有 Vector2 协议。
一个协议,添加了 vector: Vector2
属性。Vector2
、CGPoint
和 Corner
都符合此协议,并且这是任何其他 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]
一个添加向量数学运算的协议。默认情况下仅应用于 Vector2
,但如果需要,可以添加到任何其他 Vector2Representable
。
函数包括:magnitude(大小)、direction(方向)、normalized(归一化)、addition(加法)、subtraction(减法)以及与标量的 multiplication(乘法)或 division(除法)。
一个协议,将变换函数(move(移动)、rotate(旋转)、flip(翻转)、inset(内嵌))添加到任何 Vector2Representable
或该类型的数组。应用于 Vector2
、CGPoint
和 Corner
。
一个枚举,用于指示矩形上的 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)
一个方便的枚举,表示相对值或绝对值。这在 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
符合 ExpressibleByIntegerLiteral
和 ExpressibleByFloatLiteral
。这意味着在许多情况下,编写绝对值时可以省略 .absolute()
。
// Both are the same
let cornerStyle = .rounded(radius: .absolute(5))
let cornerStyle = .rounded(radius: 5)
一个可动画的线条 Shape,其端点可以延伸,并且位置可以垂直于其方向偏移。
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
)
用于 InsettableShape
和 View
的扩展,用于创建浮雕或凹雕效果。
*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() }
}
}