• 4

Swift程式設計[第2單元]: SwiftUI

#11 自動對齊與手動對齊

當我們使用 VStack/HStack/ZStack 時,包在其中的 View 物件會自動置中排列,不過三者的中軸線不同,示意圖如下:


若是要 VStack 改成向右對齊或向左對齊,或是 HStack 向上方對齊或下方對齊,則需手動給參數,對齊的參數值如下圖。這些名稱也適用於 SwiftUI 其他需要對齊的物件。



ZStack 比較特別,除了對齊四個方向之外,四個角落也可用來對齊,所以總共有9種對齊方式。



VStack/HStack 除了手動對齊(alignment)的參數之外,還有一個調整「間距」(spacing)的參數(ZStack沒有spacing參數),範例2-4b顯示文字 Text 物件的預設字體大小,垂直排列向左對齊,上下間距20點。

// 2-4b 手動對齊
// Created by Heman, 2021/08/09
import PlaygroundSupport
import SwiftUI

struct 標語: View {
var body: some View {
let 內容 = "Kazakhstan Girl"
VStack(alignment: .leading, spacing: 20) {
Text(內容)
.font(.system(size: 48))
.border(Color.red)
Text(內容)
.font(.largeTitle)
.border(Color.red)
Text(內容)
.font(.title)
.border(Color.red)
Text(內容)
.font(.title2)
.border(Color.red)
Text(內容)
.font(.title3)
.border(Color.red)
Text(內容)
.font(.headline)
.border(Color.red)
Text(內容)
.font(.subheadline)
.border(Color.red)
Text(內容)
.font(.body)
.border(Color.red)
Text(內容)
.font(.callout)
.border(Color.red)
Text(內容)
.font(.caption)
.border(Color.red)
}
}
}
PlaygroundPage.current.setLiveView(標語())



從執行結果看起來,雖然都有向左對齊,但似乎並未對齊到螢幕左邊界,為什麼會這樣呢?這牽涉到 View 物件的階層關係,下一節會詳細說明。

順便說明一點,SwiftUI 字體大小的預設值,例如 .largeTitle, .title 等等,大小值在不同設備可能是不一樣的,系統會根據螢幕解析度去設定,也就是說,從65寸Apple TV、21寸iMac、13寸Macbook、10寸 iPad、6寸iPhone、到3寸的Apple Watch,看到的 .largeTitle 未必是同樣大小,但都是相對的「大標」尺寸。

也就是說,如果手動設定字體大小,例如 font(.system(size: 40)),可能在 Macbook 看起來很合適,但是在 iPhone 卻太大,影響整個版面的協調,若使用預設的字體大小,在跨不同設備時就比較沒有問題。
#12 View 階層關係(View Hierarchy)

當我們利用 VStack/HStack/ZStack 編排版面時,就會出現一層套一層的現象,以範例2-4a為例,最外層VStack包著兩個ZStack(標題與大圖),其中一個 ZStack又包著大圖與HStack裡面的2個View物件。

這樣的View物件關係,會形成一個階層狀的關係,如下圖,其中最外層的VStack也會有上層的父視圖,稱為「根視圖」,根視圖控制整個螢幕。



像這樣的物件階層,對於畫面自動排版,是非常重要的,因為每一個子視圖的位置,其實是由上一層的父視圖決定的。

我們以一個範例程式來說明,View階層是如何決定版面配置。

// 2-4c 手動對齊
// Created by Heman, 2021/08/09
import PlaygroundSupport
import SwiftUI

struct 標語: View {
var body: some View {
let 內容 = "Kazakhstan Girl"
VStack(alignment: .leading, spacing: 20) {
Text(內容)
.font(.system(size: 40))
.border(Color.red)
Text(內容)
.font(.largeTitle)
.border(Color.red)
Text(內容)
.font(.title)
.border(Color.red)
Text(內容)
.font(.title2)
.border(Color.red)
Text(內容)
.font(.title3)
.border(Color.red)
Text(內容)
.font(.headline)
.border(Color.red)
Text(內容)
.font(.subheadline)
.border(Color.red)
Text(內容)
.font(.body)
.border(Color.red)
Text(內容)
.font(.callout)
.border(Color.red)
Text(內容)
.font(.caption)
.border(Color.red)
} .border(Color.blue)
}
}
PlaygroundPage.current.setLiveView(標語())

與2-4b的差別,只在 VStack 後面加一個修飾語 border(Color.blue),畫出藍色邊界線。

範例2-4c 結果示意圖與View階層關係如下。



在此例中,VStack 可以決定下層 Text 視圖(紅色邊線)的排版位置,也就是垂直排列、向左對齊、間距20點,但整個 VStack 的位置,則是由上一層,看不見的「根視圖」所決定,依照預設將整個 VStack 視圖(藍色邊線),放在螢幕中央。
第5課 幾何形狀與顏色

#13 顏色(Color)

數位3C設備的液晶螢幕,每個「畫素」(pixel)都是由三個單色小光點構成,分別顯示紅(Red)、綠(Green)、藍(Blue),簡稱RGB三原色,每個光點由8位元數值來控制亮度,數值範圍從 0~255,0 最暗、255最亮。

螢幕能顯示的所有顏色,均由RGB這三原色構成,例如 R=255, G=0, B=0 簡寫為 (255, 0, 0)就顯示紅色,(255, 255, 0)顯示黃色,(255, 255, 255) 顯示白色,(128, 128, 128)顯示中階灰色,等等等。

在SwiftUI 中,代表顏色的 Color 也是一個 View物件類型,Color 有3個參數分別為 red, green, blue,不過參數值並非整數0~255,而是經過「正規化」(Normalization)為實數0.0~1.0。也就是說,Color(red: 0.5, green: 0.5, blue: 0.5) 相當於 RGB (128, 128, 128) 會顯示中階灰色。

表2-5a Color 參數
Color 參數名稱 參數值 說明
red 0.0~1.0 (Double實數)
green 0.0~1.0 (Double實數)
blue 0.0~1.0 (Double實數)

例如某著名珠寶公司的商標顏色「蒂芬尼藍」(Tiffany Blue),RGB 值為 (129, 216, 208),除以255正規化之後約等於(0.506, 0.847, 0.816),在下面的範例程式會用到這個顏色。

除了設定 RGB 值之外,SwiftUI 也預設了幾個常用的顏色名稱,可以透過名稱來取色,例如 Color.red,Color.yellow 等等,我們在前幾課已經用過。

下表是預設的顏色名稱,其他顏色只能透過 RGB參數來設定。有幾種新顏色是下個月新版 iPadOS 15.0 才會啟用的名稱,下個月再回頭來測試看看。

表2-5b Color 系統預設名稱(值)
# Color 值 說明
1 red
2 orange
3 yellow
4 green
5 blue
6 pink 粉紅
7 purple
8 white
9 gray
10 black
11 brown 棕 (iPadOS 15)
12 indigo 靛藍(iPadOS 15)
13 mint 薄荷綠 (iPadOS 15)
14 teal 藍綠 (iPadOS 15)
15 clear 清除顏色(清漆、透明)
16 primary 預設前景色
17 secondary 預設前景次要色

範例程式如下。因為 Color 也是 View 物件類型,所以很容易搭配 VStack/HStack 列出所有預設的顏色,最後在背景使用了 RGB 設定的蒂芬尼藍。
// 2-5a 顏色(Color)
// Created by Heman, 2021/08/10
import PlaygroundSupport
import SwiftUI

struct 橫七彩: View {
var body: some View {
VStack {
Color.red
Color.orange
Color.yellow
Color.green
Color.blue
Color.pink
Color.purple
Color.white
Color.gray
Color.black
}
}
}

struct 豎七彩: View {
var body: some View {
HStack {
Color.red
Color.orange
Color.yellow
Color.green
Color.blue
Color.pink
Color.purple
Color.white
Color.gray
Color.black
}
}
}
let 蒂芬尼藍 = Color(red: 0.506, green: 0.847, blue: 0.816)
struct 七彩: View {
var body: some View {
VStack {
豎七彩()
.padding()
橫七彩()
.padding()
} .background(蒂芬尼藍)
}
}

PlaygroundPage.current.setLiveView(七彩())

執行結果顯示如下,注意背景的蒂芬尼藍。
let 蒂芬尼藍 = Color(red: 0.506, green: 0.847, blue: 0.816)


註解
  1. 細心的同學會發現,既然 Color 是物件類型,為什麼取用物件屬性時不用寫括號,像是 Color().red,而是直接寫 Color.red 呢?

    在語法上,Color 是物件類型(Object Type),而 Color() 則是物件實例(Object Instance)。在前面幾課,曾透過物件實例來取用屬性,例如 Date().timeIntervalSinceReferenceDate,必須先產出 Date 的物件實例,才能取得秒數的屬性。

    不過就 Color 而言,red, yellow, black 這些預設屬性其實是固定的值(不會隨實例而變動),稱為 "Type Property"「類型屬性」,這種情況下,直接用物件類型來取得屬性即可,也就是用 Color.red,而不是 Color().red。
  2. 蒂芬尼藍的顏色源自綠松石,是西方歷史上非常受歡迎的寶石。
雪白西丘斯 wrote:
第5課 幾何形狀與顏(恕刪)


樓主加油,不要被酸民擊落…
一路看到這裡,小弟覺得已經比市面上許多口水爛書或是補教業的教材好多了啊

人家樓主的出發點只是要和女兒藉由程式的學習增加互動
和培養程式設計的興趣
弄出來的品質看起來還不錯啊
先入門,有程度後觸類旁通不就容易多了嗎?
不懂有什麼好酸的

swift有apple這塊招牌,比較容易吸引小女生來玩吧
好不好是其次,先有興趣入門比較重要吧
沒事拿個JAVA在那邊J吐一一還什麼學院派的吸、吸佳佳、吸普啦絲、吸夏普還是狗鍊、派森的
人家早退避三舍了好不好

樓主請繼續,加油,不要被酸民擊落了
雪白西丘斯

謝謝鼓勵,真是非常感動。

2021-08-12 19:05
感謝分享
#14 七彩蘋果

蘋果系列產品的作業系統,不管是 macOS 或 iOS 等,都使用一致的系統字型與圖示,這套系統字型稱為 San Franscisco (舊金山,蘋果公司總部),而圖示集稱為 SF Symbols (「舊金山圖示集」),兩者都在2014年首次發表,SF Symbols 之後又陸續更新,補充了很多圖示,目前已超過3000個。


在程式中要使用 SF Symbols 的圖示非常簡單,只要知道圖示的名稱,語法如下:
Image(systemName: "star")

SF Symbols 有一個蘋果商標的圖示,名稱為 "applelogo",下面範例程式使用這個圖示,當作 mask() 修飾語的參數,mask() 稱為遮罩,作用會將遮罩以外的內容遮住,只顯示遮罩內的圖案,如同眼罩的功能。
// 2-5b 七彩蘋果(Rainbow Apple)
// Created by Heman, 2021/08/11
// Inspired by https://twitter.com/jsngr/status/1405232521256841219
import PlaygroundSupport
import SwiftUI

struct 蘋果: View {
var body: some View {
Image(systemName: "applelogo")
.resizable()
.scaledToFit()
}
}

struct 七彩蘋果: View {
var body: some View {
VStack(spacing: 0) {
Color.green
Color.green
Color.green
Color.yellow
Color.orange
Color.red
Color.purple
Color.blue
}
.frame(height: 400)
.mask(蘋果())
.padding()
}
}

PlaygroundPage.current.setLiveView(七彩蘋果())

這個程式很巧妙利用 mask() 遮罩與上一節課提到的七彩Color物件(只用到其中6個顏色),重現了蘋果公司最早的商標圖案。



如果將程式拆開來看,一開始是如同上一節課的七彩色條,然後用 VStack(spacing: 0) 參數將間距縮短為0,接下來使用 SF Symbols 創造一個可縮放的蘋果商標 View 物件,最後用 .mask() 將此物件當作遮罩,圖解如下:


程式使用 "applelogo" 圖示的部分,是當作一個 Image 物件,修飾語和第2課範例 2-2b 圖片一樣,還省了圖片檔案匯入,使用更方便。
        Image(systemName: "applelogo")
.resizable()
.scaledToFit()

SF Symbols 對程式設計師來說,是一座圖案寶庫,和 Unicode (Emoji) 類似,但一個是 Image 圖形,一個是 Text 文字。SF Symbols 內建於所有蘋果作業系統,不需要另外安裝,也不用匯入,但想要參考圖示名稱的話,須到蘋果官方網站下載 -- https://developer.apple.com/sf-symbols/

註解
此範例程式非筆者原創,修改自以下推文
https://twitter.com/jsngr/status/1405232521256841219
#15 幾何形狀

SwiftUI 提供5種基本款的幾何形狀,如表2-5c。幾何形狀的使用方法非常簡單,例如用 Rectangle() 直接呼叫 init() 初始化函式即可,預設大小會盡量佔滿可用空間,預設顏色則與作業系統有關,如果是淺色(白天)模式,則背景白、前景黑,若是深色(夜晚)模式,則是背景黑、前景白。

表2-5c SwiftUI預設的幾何形狀
# 幾何形狀元件 說明
1 Rectangle 矩形
2 RoundedRectangle 圓角矩形
3 Circle 圓形
4 Ellipse 橢圓形
5 Capsule 膠囊形

使用時,除了RoundedRectangle() 之外,其他都不需要任何參數,所有屬性都是透過修飾語調整。
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.overlay(標示("Rectangle()"))

在範例中,我們使用 stroke() 調整線條粗細,foregroundColor() 設定線條顏色,並用 overlay() 在上面標示物件名稱。範例程式如下。
// 2-5c 幾何形狀(Shpae)
// Created by Heman, 2021/08/11
import PlaygroundSupport
import SwiftUI

struct 標示: View {
var 標語: String
init(_ p: String) { 標語 = p }
var body: some View {
Text(標語)
.font(.largeTitle)
.foregroundColor(.white)
}
}

struct 圖示: View {
var 名稱: String
init(_ p: String) { 名稱 = p }
var body: some View {
Image(systemName: 名稱)
.resizable()
.scaledToFit()
}
}

struct 系統圖示: View {
var body: some View {
HStack {
圖示("star")
圖示("bell")
圖示("heart")
圖示("arrowshape.turn.up.left")
圖示("cloud.sun")
圖示("person.circle")
}
}
}

struct 預設形狀: View {
var body: some View {
VStack {
HStack {
Rectangle()
.stroke(lineWidth: 2)
.fill(Color.red)
.overlay(標示("Rectangle()"))
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.orange)
.overlay(標示("Circle()"))
}
Capsule()
.stroke(lineWidth: 6)
.foregroundColor(.yellow)
.overlay(標示("Capsule()"))
Ellipse()
.stroke(lineWidth: 8)
.foregroundColor(.green)
.overlay(標示("Ellipse()"))
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.overlay(標示("RoundedRectangle()"))
}
}
}

let 靛藍 = Color(red: 0.024, green: 0.322, blue: 0.475)
struct 幾何形狀: View {
var body: some View {
VStack {
預設形狀()
.padding()
系統圖示()
.padding()
} .background(靛藍)
}
}

PlaygroundPage.current.setLiveView(幾何形狀())


這5種幾何圖案,對於實際需求當然是不夠的,幸好還有 SF Symbols 提供3000多種圖示,此外 SwiftUI 還提供了一個萬能畫筆,稱為 Path 的物件,可以畫出任何平面圖案,這是第4單元課程的主要內容,在此先不多介紹。

此外,幾何形狀與 Path 所畫出來的圖案,最精彩的部分則是結合SwiftUI「動畫」(animation)效果,這也將在第4單元一併介紹。

註解
stroke() 與 fill() 是幾何形狀與 Path 畫出的圖案(合稱 Shape 圖案)專屬的修飾語,預計第4單元再詳細介紹。
#16 第6課 View 物件庫

第1課曾經提過,View 是 SwiftUI 的核心概念,我們到目前為止,學過 Text, Image, VStack/HStack/ZStack, Color 以及5種幾何圖形共11個View物件,而整個 SwiftUI 提供近百個View物件,提供我們設計 App 的使用者操作介面。

若依照類別,View 物件大致可分為以下幾類:
  1. 基本顯示元件,例如文字、圖片...
  2. 版面編排的元件,如水平排列、垂直排列...
  3. 平面圖形,例如幾何圖形、2D繪畫、顏色....
  4. 控制元件,如按鈕、選單、刻度....
  5. 手勢操作元件,如滑動、拖曳、點擊、長壓....

Apple 官方對 SwifUI 每年都會擴充新的 View 物件,而程式設計師又可以從這些 View 物件組合出自己定義的新物件,所以基本上 View 物件數量是會不斷增加的。

下表為目前 SwiftUI 提供的物件,分成五大類,本單元介紹入門的部分也註明在其中,其餘預計在第3單元或第4單元介紹。

表2-6a 基本顯示元件
# SwiftUI 顯示元件 說明 學習地圖
1 Text 顯示文字(字串) 2-1課
2 TextField 輸入文字欄位 3-3課
3 SecureField 輸入密碼欄位 -
4 TextEditor 輸入多行文字 -
5 Image 顯示圖片 2-2課
6 AsyncImage 顯示網路圖片 3-6課


表2-6b 版面配置元件
# 版面配置元件 說明 學習地圖
1 HStack 水平排列 2-3課
2 VStack 垂直排列 2-3課
3 ZStack 上下圖層 2-4課
4 LazyHStack 動態水平排列 -
5 LazyVStack 動態垂直排列 -
6 LazyHGrid 動態水平網格 2-8課
7 LazyVGrid 動態垂直網格 2-8課
8 GridItem 方格內容 2-8課
9 Form 表單 -
10 Group View群組 -
11 GroupBox 群組框 -
12 ScrollView 螢幕捲動(垂直或水平) 2-7課
13 List 列表 2-9課
14 Section 分欄 -
15 ForEach View迴圈 2-6課
16 Table 表格 -
17 NavigationView 導覽目錄 3-3課
18 NavigationLink 導覽連結 3-3課
19 TabView 頁籤顯示 3-10課
20 TimelineView 按時間軸顯示(動畫效果) -
21 Spacer 留白 3-2課
22 Divider 分隔 -


表2-6c 幾何圖形與2D繪圖
# 2D繪圖元件 說明 學習地圖
1 Rectangle 矩形 2-5課
2 RoundedRectangle 圓角矩形 2-5課
3 Circle 圓形 2-5課
4 Ellipse 橢圓形 2-5課
5 Capsule 膠囊形 2-5課
6 Canvas 畫布 -
7 GraphicsContext 畫布參數 -
8 Shape 形狀規範 -
9 Path 畫筆、描線 -
10 ScaledShape 放大圖案 -
11 RotatedShape 旋轉圖案 -
12 OffsetShape 位移圖案 -
13 TransformedShape 變換圖案 -
14 ShapeStyle 線條與顏色風格 2-5課
15 Animation 動態效果 2-10課
16 GeometryReader 螢幕座標 -
17 GeometryProxy 座標代理 -
18 Angle 角度 -
19 ProjectionTransform 投射變換 -


表2-6d 控制元件
# View 控制元件 說明 學習地圖
1 Button 按鈕 -
2 Link 網址連結 -
3 Menu 選單 -
4 Toggle 開關 4-5b
5 Slider 滑竿 -
6 Stepper 刻度調整 -
7 Picker 選擇器(日期、顏色) -
8 DatePicker 日期選擇器 -
9 ColorPicker 顏色選擇器 -
10 Label 文字標籤 2-9課
11 ProgressView 顯示進度 3-2課
12 Gauge 顯示讀數 -


表2-6e 手勢操作元件
# 手勢操作元件 說明 學習地圖
1 Gesture 手勢規範(protocol) 3-5課
2 TapGesture 點按 2-10課
3 LongPressGesture 長壓 -
4 DragGesture 拖曳 3-5課
5 MagnificationGesture 放大 -
6 RotationGesture 旋轉 -
7 SequenceGesture 手勢組合 -


若依照一個App的整體設計而言,第2單元所學的 View 物件與 View 修飾語,通常是屬於「前端工程師」與「設計師」負責的部分,也就是設計一個App的使用者操作介面,有時簡稱為 UI/UX (User Interface/User eXperience)。

對於比較單純的App,若不牽涉到網路、資料庫或商業模式,其實這樣也就夠了,往上還可以延伸到 AR/VR、遊戲、教學等領域。

如果比較複雜一點的App,牽涉到網路、GPS位置、或是商業應用等等,還會有「後端工程師」與「架構師」,來負責網路端(所謂的「後端」)資料庫與整體架構設計。

相對來說,前端的程式設計是比較單純容易的,所以也適合非本科系的人學習,是非常好的跨領域合作基礎。
#17 台灣特有種鳥類

第2單元的前半段,我們從 SwiftUI 的視覺元素開始,包括文字Text、圖片Image、色彩Color、幾何圖案,到單頁的排版配置 VStack/HStack/ZStack,接下來由簡而繁,要學一些比較複雜的版面配置與手勢操作。

本章先以「台灣特有種鳥類」為內容,來學習如何使用 ScrollView 展現多頁面的內容。

台灣的地質年代約6百萬年,在整個地球演化歷史,算是非常年輕的區域,但因為受到歐亞板塊與菲律賓板塊持續擠壓,形成眾多高山與複雜的地形,造就非常豐富的生態多樣性,短短6百萬年就演化出兩百多種特有動物以及一千多種特有植物。其中特有種鳥類有30種,如下表。

表2-6a 台灣特有種鳥類(2021年)
編號 中文名 別名 科名 英文名 登錄年份
1 台灣山鷓鴣 深山竹雞 雉科 Taiwan Partridge 1862
2 藍腹鷴 臺灣藍鷳 雉科 Swinhoe's Pheasant 1862
3 台灣紫嘯鶇 琉璃鳥 鶲科 Formosan Whistling Thrush 1864
4 白耳畫眉 白耳奇鶥 噪眉科 White-eared Sibia 1864
5 台灣藍鵲 長尾山娘 鴉科 Formosan Magpie 1864
6 黃胸藪眉 藪鳥 噪眉科 Steere's Liocichla 1877
7 烏頭翁 台灣鵯 鵯科 Styan's Bulbul 1893
8 黃山雀 師公鳥 山雀科 Yellow Tit 1894
9 帝雉 黑長尾雉 雉科 Mikado Pheasant 1906
10 栗背林鴝 阿里山鴝 鶲科 Collared Bush Robin 1906
11 台灣噪眉 金翼白眉 噪眉科 White-whiskered Laughingthrush 1906
12 紋翼畫眉 台灣斑翅鶥 噪眉科 Taiwan Barwing 1906
13 冠羽畫眉 冠羽鳳眉 繡眼科 Taiwan Yuhina 1906
14 火冠戴菊鳥 台灣戴菊 戴菊科 Taiwan Firecrest 1906
15 台灣叢樹鶯 褐色叢樹鶯 蝗鶯科 Taiwan Bush-Warbler 2003
16 五色鳥 臺灣擬啄木 鬚鴷科 Taiwan Barbet 2006
17 台灣畫眉 - 噪眉科 Taiwan Hwamei 2006
18 台灣白喉噪眉 白喉笑鶇 噪眉科 Rufous-crowned laughingthrush 2011
19 大彎嘴 大彎嘴畫眉 畫眉科 Black-necklaced scimitar babbler 2011
20 小彎嘴 小彎嘴畫眉 畫眉科 Taiwan scimitar babbler 2011
21 台灣鷦眉 鱗胸鷦鷯 鷦眉科 Taiwan wren-babbler 2011
22 褐頭花翼 灰頭花翼 鸚嘴科 Taiwan fulvetta 2011
23 台灣棕噪眉 竹鳥 噪眉科 Rusty Laughingthrush 2012
24 台灣朱雀 朱雀 雀科 Taiwan rosefinch 2012
25 繡眼畫眉 灰眶雀鶥 噪眉科 Morrison's Fulvetta 2014
26 台灣竹雞 - 雉科 Taiwan bamboo partridge 2017
27 赤腹山雀 - 山雀科 Chestnut-bellied tit 2017
28 白頭鶇 島鶇 鶇科 Island Thrush 2019
29 小翼鶇 藍短翅鶇 鶲科 Taiwan shortwing 2019
30 台灣灰鷽 灰鷽 雀科 Taiwan bullfinch 2021
參考資料:
1 http://www.dasyueshan.org/?itemID=8&struID=35&contentID=84
2 https://zh.wikipedia.org/wiki/台灣鳥類列表

這些資料要如何呈現呢?我們先設計一個簡單的外觀框架:

希望用往下滑動的方式,來瀏覽列表。當點選某個鳥種的時候,可顯示該鳥種的圖片集、影片、或是在背景播放鳴叫聲,不過這些是後續單元的課程內容,本單元先學習如何展示列表。

有了外觀框架,就可以開始著手撰寫程式,一開始,我們先設計單項鳥種的展現。範例程式如下:
// 2-6a 黃山雀
// Created by Heman, 2021/08/15
import PlaygroundSupport
import SwiftUI

struct 鳥類 {
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}

var 黃山雀 = 鳥類(
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc")

struct 相框: View {
var 檔名: String
init(_ p: String) {
檔名 = p
}
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}
struct 台灣特有種: View {
var body: some View {
HStack {
VStack {
Text(黃山雀.中文名)
.font(.title)
相框(黃山雀.圖片檔名)
}
VStack(alignment: .leading) {
Text("別名:" + 黃山雀.別名)
Text("科名:" + 黃山雀.科名)
Text("英文名稱:" + 黃山雀.英文名)
Text("圖片來源:" + 黃山雀.圖片來源)
Text("攝影者:" + 黃山雀.攝影者)
}
.font(.title2)
.lineLimit(1)
} .frame(height: 100)
}
}

PlaygroundPage.current.setLiveView(台灣特有種())

首先,要先想好鳥類要呈現的資料結構,包括中英文名稱、別名、科名等,因為還要顯示圖片,所以必須將匯入的圖片檔名,以及圖片來源與攝影者加進來。未來如果要加入影片或聲音,同樣需要考慮來源。
struct 鳥類 {
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}

用 struct 定義一個新的資料型態「鳥類」(物件型態),共有7個資料欄位(物件屬性)。決定資料結構之後,就可以先產出一個實例,我們以黃山雀為例,將圖片從維基百科下載並匯入Swift Playgrounds之後,填入以下資料:
var 黃山雀 = 鳥類(
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc")

用 struct 定義的資料型態,如何產出實例,也就是初始化每一個屬性,這是第1單元第7課的內容。

資料結構設計好之後,版面配置的方式,就用 HStack/VStack 來排列即可,在此須用 .frame() 修飾語來限制 HStack 的高度,讓圖片控制在規劃好的框架裡面。

執行結果如下,基本上符合設計框架的樣子。


注意到「圖片來源」的文字會比較長,所以加了一個針對 Text 物件的 .lineLimit(1) 修飾語,限制只能單行顯示,不要跨行。

註解
  1. 黃山雀圖片下載地址 https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Taiwan_tit.jpg/640px-Taiwan_tit.jpg
  2. 維基百科的文字與圖片的授權比較自由,很適合教學與練習之用。圖片授權條款如下,根據授權條款,本節內容也須以相同方式(CC BY-SA 2.0)分享。

    此檔案採用創用CC 姓名標示-相同方式分享 2.0 通用版授權條款。
    您可以自由:
    • 分享 – 複製、發佈和傳播本作品

    • 重新修改 – 創作演繹作品

    惟需遵照下列條件:
    • 姓名標示 – 您必須指名出正確的製作者,和提供授權條款的連結,以及表示是否有對內容上做出變更。您可以用任何合理的方式來行動,但不得以任何方式表明授權條款是對您許可或是由您所使用。

    • 相同方式分享 – 如果您利用本素材進行再混合、轉換或創作,您必須基於如同原先的相同或兼容的條款,來分布您的貢獻成品。
#18 多項目顯示(ForEach)

有了顯示單項內容的程式之後,接下來就可擴充為顯示多項內容。如何處理多項內容的資料呢?如果在第一單元,我們會用一個陣列,加上 for 迴圈來處理,就如同第1單元第4課範例1-4a的商王列表一樣。

不過在 View 結構裡面,無法直接用 for 迴圈指定值給主體 body,還好,SwiftUI 有 View 版本的替代方案-- ForEach,這個 ForEach 也是一個 View 物件類型,可用來將多個 View 物件組合起來,語法跟 for 迴圈有點類似。

上一節我們定義了「鳥類」的資料類型,假如我們再定義一個「特有種清單」的鳥類陣列,陣列每個元素都是鳥類物件的實例,例如黃山雀與台灣山鷓鴣,這個陣列的宣告如下:
let 特有種清單 = [
鳥類(
id: 1,
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc"),
鳥類(
id: 2,
中文名: "台灣山鷓鴣",
別名: "深山竹雞",
科名: "雉科 ",
英文名: "Taiwan Partridge",
圖片檔名: "Taiwan_partridge_(Arborophila_crudigularis).jpg",
圖片來源: "https://zh.wikipedia.org/wiki/台湾山鹧鸪",
攝影者: "Francesco Veronesi")
]

如果是用第1單元 for 迴圈的做法,我們可以用 print() 列印出每個鳥種的資料,像這樣:
for 鳥種 in 特有種清單 {
print(鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片檔名)
}

若要改成 View 結構,則可用 ForEach 語法如下:
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
}
和 for 迴圈語法稍有差異,但意思差不多。這句程式碼的意思其實是:

for each 特有種 in 特有種清單 that returns 單項顯示(鳥: 特有種)

可翻譯為:對「特有種清單」中每個元素,指定給「特有種」,傳回「特有種」的「單項顯示」視圖。

在實際的語法上,ForEach 需要一個陣列當作參數,在此為「特有種清單」。ForEach對這個陣列參數有一個特別的要求,就是陣列的「元素」要符合 Identifiable 規範,也就是須有一個欄位,名稱為 id,類型可以是 Int, String 或其他,只要它的值不會重複即可。

因此「鳥類」的定義須增加一個 id欄位,並宣告符合 Identifiable 規範:
struct 鳥類: Identifiable {
var id: Int
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}

然後ForEach() 接著一個大括號段落 { },比較特別的地方,是這個大括號段落會傳入一個臨時參數,就像 for 迴圈參數一樣,在這裡為「特有種」,這個參數會從陣列中依序取值,然後用在 { } 段落中。

類似這樣,會傳入參數的 { } 段落,是 Swift 非常重要的語法,術語稱為 Closure,中文稱為閉鎖、閉包或是封閉段落,本質上相當於一個未命名的函式,因此在本課程稱為「匿名函式」。

匿名函式可視為一種物件,能夠執行某些任務的物件,就像樂高積木的動力模組一樣,本身可作為變數值,或當作參數傳遞給其他函式或物件型態。在後面課程,若使用控制元件,如按鈕、刻度、選單,或是在第3單元,都會經常用到匿名函式。

上一節定義的「台灣特有種」物件,我們將它一般化,改稱為「單項顯示」,並增加一個屬性「鳥」,之後可當作初始化參數傳入,避免像上一節使用全域變數「黃山雀」。完整範例程式如下:
// 2-6b 鳥類清單
// Created by Heman, 2021/08/15
import PlaygroundSupport
import SwiftUI

struct 鳥類: Identifiable {
var id: Int
var 中文名: String
var 別名: String
var 科名: String
var 英文名: String
var 圖片檔名: String
var 圖片來源: String
var 攝影者: String
}

let 特有種清單 = [
鳥類(
id: 1,
中文名: "黃山雀",
別名: "師公鳥",
科名: "山雀科",
英文名: "Yellow Tit",
圖片檔名: "640px-Taiwan_tit.jpg",
圖片來源: "https://zh.wikipedia.org/wiki/黃山雀",
攝影者: "Robert tdc"),
鳥類(
id: 2,
中文名: "台灣山鷓鴣",
別名: "深山竹雞",
科名: "雉科",
英文名: "Taiwan Partridge",
圖片檔名: "Taiwan_partridge_(Arborophila_crudigularis).jpg",
圖片來源: "https://zh.wikipedia.org/wiki/台湾山鹧鸪",
攝影者: "Francesco Veronesi"),
鳥類(
id: 3,
中文名: "藍腹鷴",
別名: "臺灣藍鷳",
科名: "雉科",
英文名: "Swinhoe's Pheasant",
圖片檔名: "",
圖片來源: "",
攝影者: "")
]

for 鳥種 in 特有種清單 {
print(鳥種.中文名, "\t", 鳥種.英文名, "\t", 鳥種.圖片檔名)
}

struct 相框: View {
var 檔名: String
init(_ p: String) {
檔名 = p
}
var body: some View {
if 檔名 == "" {
Image(systemName: "camera.circle")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.opacity(0.4)
} else {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}
}

struct 單項顯示: View {
var 鳥: 鳥類
var body: some View {
HStack {
VStack {
Text(鳥.中文名)
.font(.title)
.foregroundColor(.blue)
相框(鳥.圖片檔名)
.frame(width: 120)
}
VStack(alignment: .leading) {
Text("別名:" + 鳥.別名)
Text("科名:" + 鳥.科名)
Text("英文名稱:" + 鳥.英文名)
Text("圖片來源:" + 鳥.圖片來源)
Text("攝影者:" + 鳥.攝影者)
}
.font(.title2)
.lineLimit(1)
} .frame(height: 100)
}
}

struct 台灣特有種鳥類: View {
var body: some View {
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
.padding()
}
}
}
PlaygroundPage.current.setLiveView(台灣特有種鳥類())

我們對於「相框」顯示,增加一個 if 條件句,如果圖片檔名是空字串的話,表示沒有照片可用,就顯示 SF Symbols 的系統圖示,如第三項藍腹鷴的圖片。

Image(systemName: "camera.circle")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.opacity(0.4)
在這裡我們用了 .opacity() 修飾語,將圖片打淡,opacity 是透明度的意思,參數值從0~1.0,0代表完全透明,1.0代表完全不透明。

對於多項資料的顯示,我們除了 ForEach 之外,還可在外層配合用 ScrollView,這樣如果陣列的元素比較多時,螢幕可以往下滑動。本節範例只有三項內容,用或不用 ScrollView,顯示結果是一樣的。

從這個例子可以看出,ForEach 可以將資料陣列轉換為一組View,只是它沒有辦法控制螢幕的捲動,View超出螢幕範圍的部分,ForEach 便無法顯示。圖解如下:


所以一個常用的句型是 ScrollView 配合 ForEach,由 ScrollView 來負責捲動螢幕,這樣的句型後面還會出現,其中ScrollView 可以換成其他的容器類型,例如 List, NavigationView 等等,就像這樣:

ScrollView {
ForEach(特有種清單) { 特有種 in
單項顯示(鳥: 特有種)
.padding()
}
}


註解
  1. 台灣山鷓鴣圖片下載地址為 https://upload.wikimedia.org/wikipedia/commons/thumb/c/c9/Taiwan_partridge_%28Arborophila_crudigularis%29.jpg/640px-Taiwan_partridge_%28Arborophila_crudigularis%29.jpg
  2. 本節仍為(CC BY-SA 2.0) 方式分享版權。
  3. 經過 ForEach 產出的 View,並非總是垂直排列,試著將最後一段改成:
    HStack {
    ForEach(特有種清單) { 特有種 in
    單項顯示(鳥: 特有種)
    .padding()
    }
    }
    就會知道,ForEach只是將View集合在一起,排列方式仍是由上一層View決定。
  4. ForEach 也可以接受陣列以外的參數,就像 for 迴圈一樣,不過在第2單元只用陣列就夠了。
  • 4
內文搜尋
X
評分
評分
複製連結
Mobile01提醒您
您目前瀏覽的是行動版網頁
是否切換到電腦版網頁呢?