masking遮罩是一種強大的技術(shù),我們可以使用它將應用程序的設(shè)計推向下一個層次。SwiftUI提供了多種方式來做這件事:讓我們從SwiftUI的剪輯開始。
Clipping遮罩
每個視圖都有一個受到綁定的frame。這個frame用于組成整個視圖層次結(jié)構(gòu)布局。當進入繪圖階段時,視圖內(nèi)容可能會超出它的frame。
例如,以下視圖:

Text("Five stars")
.background(Color.yellow)
.font(.system(size: 90))
.border(Color.red)
紅色邊框顯示內(nèi)容frame,在本例中,內(nèi)容frame也與綁定的frame一致。
讓我們來看另一個例子:

Text("Five stars")
.background(Color.yellow)
.font(.system(size: 90))
.fixedSize()
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.red)
由于fixedSize()視圖修飾符,Text需要多少空間就占用多少空間。
然而,我們也應用另一個.frame(width: 200, height: 50)視圖修改器在上面。
對于視圖層次結(jié)構(gòu)的其余部分,這個視圖限制由紅色邊框表示,而藍色邊框表示視圖內(nèi)容占用的空間。
在布局階段:
- 只有綁定的框架/紅色邊框?qū)⒈豢紤]
- 內(nèi)容frame/藍色邊框完全被忽略
由于SwiftUI默認允許內(nèi)容溢出,即使內(nèi)容超出了視圖的邊緣也會被繪制。為了避免這種情況,我們可以使用剪輯:

Text("Five stars")
.background(Color.yellow)
.font(.system(size: 90))
.fixedSize()
.border(Color.blue)
.frame(width: 200, height: 100)
.border(Color.red)
.clipped() // ????
clipped()視圖修改器將視圖的繪制限制在其綁定frame內(nèi),其他所有內(nèi)容將被隱藏。
換句話說,clipped()應用一個等效于綁定幀“矩形”的遮罩,從而隱藏超出該矩形的任何內(nèi)容。
SwiftUI提供了兩個clipped()替代方法:cornerRadius(_:)和clipShape(_:style)。
Corner Radius
cornerRadius(_:)的行為與clipped()完全相同,但它并沒有與綁定frame 1:1匹配,而是讓我們指定一個用于最終蒙版的角半徑:

Text("Five stars")
.background(Color.yellow)
.font(.system(size: 90))
.fixedSize()
.frame(width: 200, height: 100)
.cornerRadius(50) // ????
使用與之前相同的思維過程,cornerRadius(_:)應用一個等效于視圖綁定框架矩形的蒙版,這次是圓角。
.clipped()比.cornerradius(0)有更好的性能。
Clip Shape
到目前為止,我們一直在使用矩形,clipShape(_:style:)消除了這個限制,讓我們使用任何形狀作為剪輯蒙版:

Text("Five stars")
...
.clipShape(Circle())
形狀盡可能適合包含它們的視圖(即視圖綁定frame)的自然大小。
我們并不局限于SwiftUI提供的形狀。我們也可以聲明:

Text("Five stars")
...
.clipShape(Star())
struct Star: Shape {
@Clamping(0...Int.max) var points: Int = 5
var innerRatio = 0.4
func path(in rect : CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let angle: Double = .pi / Double(points)
var path = Path()
var startPoint: CGPoint = rect.origin
let outerRadius = min(rect.width / 2, rect.height / 2)
let innerRadius = outerRadius * innerRatio
let maxCorners = 2 * points
for corner in 0 ..< maxCorners {
let radius = (corner % 2) == 0 ? outerRadius : innerRadius
let x = center.x + cos(Double(corner) * angle) * radius
let y = center.y + sin(Double(corner) * angle) * radius
let point = CGPoint(x: x, y: y)
if corner == 0 {
startPoint = point
path.move(to: point)
} else {
path.addLine(to: point)
}
if corner == (maxCorners - 1) {
path.addLine(to: startPoint)
}
}
return path
}
}
類似于.clipped()可以被看作是.cornerRadius(0)上的便利,.cornerRadius(x)可以被看作是.clipshape(RoundedRectangle(cornerRadius: x))上的便利。
@Clamping為自定義修飾器:
@propertyWrapper
public struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
public init(wrappedValue: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(wrappedValue))
self.range = range
self.value = wrappedValue
}
public var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
奇偶規(guī)則
當定義一個Shape形狀時,它的一些部分可以多次繪制。我們可以將這些部分視為“重疊區(qū)域”。例如,以這個DoubleEllipse Shape定義為例,它由兩個任意數(shù)量重疊的橢圓組成:

struct FSView: View {
@State var overlapping: Double = 0.1
var body: some View {
VStack {
DoubleEllipse(overlapping: overlapping)
.frame(width: 300, height: 100)
HStack {
Text("Overlapping")
Slider(value: $overlapping, in: 0.0...1.0)
}
}
}
}
struct DoubleEllipse: Shape {
/// 1 = complete overlap
/// 0 = no overlap
@Clamping(0.0...1.0) var overlapping: Double = 0
func path(in rect: CGRect) -> Path {
let rectSize = CGSize(width: (rect.width / 2) * (1 + overlapping), height: rect.height)
var path = Path()
path.addEllipse(in: CGRect(origin: .zero, size: rectSize))
let secondEllipseOrigin = CGPoint(x: (rect.width / 2) * (1 - overlapping), y: rect.origin.y)
path.addEllipse(in: CGRect(origin: secondEllipseOrigin, size: rectSize))
return path
}
}
默認情況下,SwiftUI按照定義繪制所有內(nèi)容。然而,我們也可以應用一個fill(style:) Shape修飾符,以不同的方式填充那些重疊的區(qū)域:

struct FSView: View {
@State var overlapping: Double = 0.1
var body: some View {
VStack {
DoubleEllipse(overlapping: overlapping)
.fill(style: FillStyle(eoFill: true, antialiased: true)) // ????
.frame(width: 300, height: 100)
HStack {
Text("Overlapping")
Slider(value: $overlapping, in: 0.0...1.0)
}
}
}
}
神奇之處在于oeFill參數(shù),其中eo代表奇偶(規(guī)則),描述如下:《形狀中的一個“內(nèi)部”點是通過在任意方向繪制一條從該點到無窮遠的射線,并計算射線穿過給定形狀的路徑段的數(shù)量來確定的。如果這個數(shù)是奇數(shù),點在里面;否則的話,那就是在外面》
定義不僅僅是重疊,但這很可能是它在SwiftUI掩蔽時的用法。
fill(style:) Shape修飾符返回some View,這意味著我們不能在clipShape(_:style:)中使用它,因為后者需要一個Shape實例。也就是說,.clipShape(_:style:)第二個參數(shù)解決了這個問題,讓我們傳遞一個FillStyle:

VStack {
Text("Five stars")
.background(Color.yellow)
.font(.system(size: 90))
.clipShape(
OverlappingEllipses(ellipsesNumber: ellipsesNumber, overlapping: overlapping),
style: FillStyle(eoFill: true, antialiased: false) // ????
)
Stepper("Ellipses number:", value: $ellipsesNumber, in: 2...16)
HStack {
Text("Overlapping")
Slider(value: $overlapping, in: 0.0...1.0)
}
}
動畫剪輯遮罩
Shapes形狀既符合View視圖又符合Animatable動畫,我們可以在形狀中聲明var animatableData: CGFloat,以利用這一點:
struct OverlappingEllipses: Shape {
@Clamping(1...Int.max) var ellipsesNumber: Int = 2
@Clamping(0.0...1.0) var overlapping: Double = 0
var animatableData: CGFloat { // ????
get { overlapping }
set { overlapping = newValue }
}
func path(in rect: CGRect) -> Path {
let rectWidth = (rect.width / Double(ellipsesNumber)) * (1 + Double(ellipsesNumber - 1) * overlapping)
let rectSize = CGSize(width: rectWidth, height: rect.height)
var path = Path()
for index in 0..<ellipsesNumber {
let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(ellipsesNumber - 1), y: rect.origin.y)
path.addEllipse(in: CGRect(origin: ellipseOrigin, size: rectSize))
}
return path
}
}
有了這個,我們就可以把我們目前所介紹的所有內(nèi)容都放到一起,輕松地獲得一些迷幻效果:

struct FSView: View {
@State var overlapping: Double = 0
var body: some View {
VStack(spacing: 16) {
Text("Five stars")
...
.clipShape(
OverlappingEllipses(ellipsesNumber: 8, overlapping: overlapping),
style: FillStyle(eoFill: true, antialiased: false)
)
Text("Five stars")
...
.clipShape(
OverlappingRectangles(rectanglesNumber: 8, overlapping: overlapping),
style: FillStyle(eoFill: true, antialiased: false)
)
Button("Show/Hide") {
withAnimation(.easeInOut(duration: 2)) {
overlapping = overlapping == 1 ? 0 : 1
}
}
}
}
}
OverlappingRectangles定義如下:
struct OverlappingRectangles: Shape {
@Clamping(1...Int.max) var rectanglesNumber: Int = 2
@Clamping(0.0...1.0) var overlapping: Double = 0
var animatableData: CGFloat {
get { overlapping }
set { overlapping = newValue }
}
func path(in rect: CGRect) -> Path {
let rectWidth = (rect.width / Double(rectanglesNumber)) * (1 + Double(rectanglesNumber - 1) * overlapping)
let rectSize = CGSize(width: rectWidth, height: rect.height)
var path = Path()
for index in 0..<rectanglesNumber {
let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(rectanglesNumber - 1), y: rect.origin.y)
path.addRect(CGRect(origin: ellipseOrigin, size: rectSize))
}
return path
}
}struct OverlappingRectangles: Shape {
@Clamping(1...Int.max) var rectanglesNumber: Int = 2
@Clamping(0.0...1.0) var overlapping: Double = 0
var animatableData: CGFloat {
get { overlapping }
set { overlapping = newValue }
}
func path(in rect: CGRect) -> Path {
let rectWidth = (rect.width / Double(rectanglesNumber)) * (1 + Double(rectanglesNumber - 1) * overlapping)
let rectSize = CGSize(width: rectWidth, height: rect.height)
var path = Path()
for index in 0..<rectanglesNumber {
let ellipseOrigin = CGPoint(x: (rect.width - rectWidth) * Double(index) / Double(rectanglesNumber - 1), y: rect.origin.y)
path.addRect(CGRect(origin: ellipseOrigin, size: rectSize))
}
return path
}
}