#6 4-2b 屬性容器 AttributeContainer
上一節提到,帶屬性字串(AttributedString)與一般字串(String)的區別,就是前者的任何一部份(術語稱為”Substring”「子字串」)都可以各自帶不同的外觀屬性。
Swift字串(String)是可以包含「換行字元」“\n”,所以一個字串可能包含很多行,甚至一整篇文章可當作一個字串來處理,其中標題或小標的字體通常要比較大、比較醒目,內文中有些專有名詞或關鍵字想用不同顏色或斜體,某些地名要加上底線等等,在文書編輯中,這稱為「標示」或「標記」,英文為 ”markup”,可當作動詞或名詞。
帶屬性文字AttributedString就可用在這種情境,不管字串長短,都可以對個別子字串標示不同的外觀。
在AttributedString內部,若指定子字串的屬性,會有一個「屬性容器(AttributeContainer)」來記錄個別子字串的屬性,AttributeContainer 也是一個物件類型,注意這裡的Attribute是一般名詞「屬性」,不用被動語態,Container 是裝東西的容器、貨櫃的意思。
本節範例4-2b就利用屬性容器(AttributeContainer)來做一個自製的標示,命名為「上標」。
另外,上一節用到的動畫效果Animation.spring(),除了做出類似伸縮彈簧或是果凍效果之外,也能做出重物墜地的感覺,本節就來試試看。
// 4-2b 屬性容器 AttributeContainer
// Created by Heman, 2022/03/20
// Minor revision (replace .animation -> withAnimation) by Heman, 2023/01/30
import PlaygroundSupport
import SwiftUI
let 雪山輯 = "雪溶後 花香流過司介欄溪的森林"
struct 現代詩選: View {
@State var 詩句 = AttributedString(雪山輯)
@State var 長寬位移 = CGSize(width: 150, height: -300)
let 動畫效果 = Animation.spring(response: 0.6, dampingFraction: 0.4, blendDuration: 0)
var body: some View {
Text(詩句)
.frame(width: 160, height: 160)
.border(Color.red)
.font(.system(size: 30))
.offset(長寬位移)
.onAppear {
if let 範圍 = 詩句.range(of: "雪") {
詩句[範圍].backgroundColor = .brown
詩句[範圍].foregroundColor = .white
詩句[範圍].font = .system(size: 40).bold()
}
var 上標 = AttributeContainer()
上標.baselineOffset = 15
上標.font = .system(size: 24)
上標.foregroundColor = .cyan
if let 範圍 = 詩句.range(of: "花香") {
詩句[範圍].mergeAttributes(上標)
}
if let 範圍 = 詩句.range(of: "司介欄溪") {
詩句[範圍].mergeAttributes(上標)
}
if let 範圍 = 詩句.range(of: "森林") {
詩句[範圍].foregroundColor = .green
詩句[範圍].font = .system(size: 40).bold()
}
withAnimation(動畫效果.repeatForever(autoreverses: false)) {
長寬位移 = CGSize(width: 0, height: 0)
}
}
}
}
PlaygroundPage.current.setLiveView(現代詩選())
我們首先想做出這樣的效果,每個名詞標示不同的外觀:

這是1962年詩人鄭愁予所做的現代詩《浪子麻沁─雪山輯之二》首句。雪山海拔3886m,是台灣第二高峰,「司介欄溪」(或稱司界蘭溪、四季郎溪)是大甲溪支流,在武陵農場南方環山部落附近,過去曾是泰雅族原住民攀登雪山獵場的傳統路線,現在則是爬志佳陽大山(海拔3345m)的登山口。
要為名詞標示不同的外觀,可以仿照上一節的做法:
let 雪山輯 = "雪溶後 花香流過司介欄溪的森林"
var 詩句 = AttributedString(雪山輯)
if let 範圍 = 詩句.range(of: "雪") {
詩句[範圍].backgroundColor = .brown
詩句[範圍].foregroundColor = .white
詩句[範圍].font = .system(size: 40)
}
if let 範圍 = 詩句.range(of: "森林") {
詩句[範圍].foregroundColor = .green
詩句[範圍].font = .system(size: 40).bold()
}
這樣就可以將「雪」標示為反白(前景白、背景棕)且字體放大為40點,「森林」標示為綠色、字體40點、粗體。
除此之外,我們還可以將同一組「屬性」透過「屬性容器(AttributeContainer)」綁在一起,取個名字(如「上標」),外觀屬性包括「底部提高15點、字體縮小為24點、顏色為靛青(cyan, 讀音 /ˈsaɪ.ən/)」,然後將這組屬性標示到「花香」「司介欄溪」上面:
var 上標 = AttributeContainer()
上標.baselineOffset = 15
上標.font = .system(size: 24)
上標.foregroundColor = .cyan
if let 範圍 = 詩句.range(of: "花香") {
詩句[範圍].mergeAttributes(上標)
}
if let 範圍 = 詩句.range(of: "司介欄溪") {
詩句[範圍].mergeAttributes(上標)
}
mergeAttributes() 是子字串的物件方法,用來將屬性容器(參數「上標」)的整組屬性加到自己的屬性容器中,merge 是合併、融合的意思。
標示完文字之後,接下來加入動畫效果:
struct 現代詩選: View {
@State var 長寬位移 = CGSize(width: 150, height: -300)
let 動畫效果 = Animation.spring(response: 0.6, dampingFraction: 0.4, blendDuration: 0)
var body: some View {
Text(詩句)
.frame(width: 160, height: 160)
.offset(長寬位移)
.onAppear {
withAnimation(動畫效果.repeatForever(autoreverses: false)) {
長寬位移 = CGSize(width: 0, height: 0)
}
}
}
}
我們先用視圖修飾語 .frame() 將視圖改為正方形,可想像成一顆石塊,要從空中墜下:

之前用過的「位移」.offset()參數 x, y 座標,也可以改用長寬尺寸 CGSize(),只不過將參數名稱 x, y 改成 width, height,這裡的寬度(width)和高度(height)是有方向性的,與螢幕座標的 x, y 方向一致,所以 CGSize(width: 150, height: -300) 就表示往右150點、往上300點(y軸向下為正),石塊會從螢幕右上方砸下來。
同樣用彈簧動畫效果,與上一節的參數比較起來,上一節的果凍效果回彈較慢(response: 1.0)、阻尼較小(dampingFraction: 0.2),感覺起來比較Q彈。本節是石塊墜地,所以回彈要快要硬(response: 0.6),阻尼稍大(dampingFraction: 0.4):
//4-2a 回彈較慢、阻尼較小
let 動畫效果 = Animation.spring(response: 1.0, dampingFraction: 0.2, blendDuration: 0)
//4-2b 回彈較快、阻尼較大
let 動畫效果 = Animation.spring(response: 0.6, dampingFraction: 0.4, blendDuration: 0)
執行結果如下,如果再配上音效「砰」一聲,就更完美了:
註解
- 靛青(cyan, 讀音 /ˈsaɪ.ən/)是一種藍綠色,也是印刷用的四種基本墨色(CMYK)之一,與RGB三原色發光原理不同,墨色是經由吸收與反射光線來呈現色彩,本身並不發光。
- CMYK四種基本墨色,靛青(C)染料會吸收紅光(R),反射藍光與綠光;洋紅(M)則吸收綠光(G),反射紅光與藍光;黃色(Y)油墨會吸收藍光(B),反射紅光與綠光,紅綠光混合後變黃光;黑色(K)是紅藍綠通通吸收,不反射任何光線。