[2025/01/30更新] 第2單元已整理到Notion網站:Swift[第2單元]SwiftUI基礎Swift程式設計[第2單元]: SwiftUI

前言

第2單元將開始設計「圖形介面」的程式,這是設計App軟體的基礎,使用SwiftUI設計的App可以在 Mac, iPhone, iPad, Apple Watch, Apple TV 全系列Apple產品上執行。

本單元的課程大綱如下,10課內容預計8月底暑假結束前完成。
單元#2. SwiftUI 入門
2.1 什麼是SwiftUI (顯示文字Text)
2.2 顯示圖片(Image)
2.3 垂直、水平排列(VStack/HStack)
2.4 圖層(ZStack)
2.5 幾何形狀與顏色(ShapeStyle)
2.6 下滑頁面(ScrollView)
2.7 JSON 傑森解碼器
2.8 Emoji 表情符號(LazyVGrid)
2.9 SF Symbols 系統圖示(List)
2.10 手勢操作(TapGesture)

本系列課程設想的讀者是高中程度、沒有程式設計經驗的初學者。

學習路線
第2單元需要先了解Swift基本語法以及熟悉 Swift Playgrounds App操作使用,如果不熟悉的話,請參考第1單元(共10課)。
後續單元還有:
💡註解
  1. 用SwiftUI設計的App可以在所有Apple產品上執行,似乎是理所當然的事,但技術上卻很不簡單,因為螢幕尺寸從Apple Watch 的1-2吋、iPhone 4-7吋、iPad 7-12吋、Macbook 13-16吋,到Apple TV的65吋,都得一體適用,光是版面控制就很困難。
  2. 在SwiftUI之前,設計iPhone/iPad App是用UIKit,設計 Mac App 則必須用 AppKit,兩者無法混用。
#2 第1課 什麼是SwiftUI ? 顯示文字(Text)

什麼是SwiftUI? SwiftUI 與 Swift 有何關係?

簡單的說,Swift 是程式語言,提供基本的指令、語法以及內建的函式與資料類型(物件)。目前 Swift 由第三方非營利基金會負責維護與發展,全部是開放原始碼。

若只用 Swift 基本語法,就只能寫「文字模式」(輸出到主控台)的程式,像我們第1單元的內容。但不要小看文字模式,有很多科學計算、大數據資料分析或人工智慧的軟體只要文字模式就夠。

但如果想設計App,必須用到圖形模式,就得藉助於額外的「物件庫」或「框架」(Framework)。

SwiftUI 是Apple 公司官方開發的專屬框架,提供Apple全系列產品(Mac, iPhone, iPad, Watch, TV, etc)圖形介面功能,是開發這些產品App最基礎的物件庫。

所以單純的Swift 語言可以跨平台,除了支援 Apple產品之外,也能在 Linux, Windows 甚至網頁上面執行,但是 SwiftUI 只能在 Apple 產品環境中執行。

SwiftUI 非常新,在2年前2019年7月才初次發表,在這之前都是使用 UIKit (2008年發表)來開發App。簡單的說,SwiftUI 是準備取代 UIKit 的物件庫,但目前還無法完全取代,因此兩者會繼續並存幾年。

SwiftUI 是全新的設計方法,比 UIKit 更容易使用、更有效率,原來的 UIKit 通常需配合 Xcode/Storyboard 來設計使用者操作介面(User Interface, UI),而新的 SwiftUI 則可在 Swift Playgrounds 中很直覺的撰寫 UI 程式碼。

當然 SwiftUI 也可以配合 Xcode/Storyboard,與 UIKit 混合使用,但本單元主要介紹 SwiftUI 搭配 Swift Playgrounds App 來設計程式,所以並不會使用 UIKit。

我們先來看一個簡單範例,如何用 SwiftUI 在圖形介面中顯示文字,只有短短6句程式碼。
// 2-1a 我愛你(Text)
// Created by Heman, 2021/08/03
import PlaygroundSupport
import SwiftUI

struct 我愛你: View {
var body: some View {
Text("I ❤️ U")
.font(.title)
}
}

PlaygroundPage.current.setLiveView(我愛你())

這6句程式碼的結構,在第2單元會一直重複出現,這是 SwiftUI 基本的句型結構。
① import PlaygroundSupport
⑥ PlaygroundPage.current.setLiveView(我愛你())

第一句與最後一句是為了在 Swift Playgrounds 顯示執行結果,PlaygroundPage 是從 PlaygroundSupport 物件庫取出的物件類型,我們在 Swift Playgrounds 中開啟的空白頁面(或新增頁面),就是對應一個 PlaygroundPage 物件,current 指的是目前頁面,setLiveView() 則是在目前頁面顯示「View 物件」的方法。下文會解釋什麼是 View 物件。

未來如果改在 Xcode 環境,或是在Swift Playgrounds產出App的時候,這兩句程式碼是不需要的,但在本單元所有範例中是必要的。
② import SwiftUI

第二句是匯入 SwiftUI 物件庫,我們將取用 SwiftUI 物件庫裡面的 Text 物件來顯示文字。
③ struct 我愛你: View {
④ var body: some View {
⑤ Text("I ❤️ U")
.font(.title)
}
}

所以真正的程式碼,是這個 struct 宣告,還記得 struct 是用來定義新類型,或稱為物件類型,在此我們定義了「我愛你」物件類型。而這個物件類型用在最後一句中,以「我愛你()」物件實例當作PlaygroundPage.current.setLiveView() 的參數。

還記得物件類型可比喻為「模具」,括號 () 相當於模具的啟動開關,所以「我愛你()」會啟動開關,複製產出一個「我愛你」的物件實例來。

這裡的 struct 有個新語法,就是物件類型名稱「我愛你」後面的 : View,這是什麼意思呢?

View 中文稱為「視圖」,是 SwiftUI 的一個核心概念,指的是一個虛擬方形,表示物件的「可視範圍」,通常對應螢幕的一塊方形區域,就像我們拿著一個空心方框看世界,方框就是我們的視野。這個View可視範圍的長寬,最小可以是零(隱藏不見),最大可以超出螢幕(超出部分暫時看不到)。

SwiftUI 裡面每個「圖形介面元件」都會形成一個 View,而整個螢幕畫面,就是由大大小小的 View 組成。例如我們想要顯示的一段文字,會有一個方形的顯示範圍,就是這段文字的 View。
struct 我愛你: View { } 

所以這句 struct 宣告,在此處是指物件類型「我愛你」必須符合 View 的「規範」,這個規範只有一個規定,就是 { } 裡面必須定義一個變數,名為 body (視圖的「主體」),其值是「某個 View」(some View) 物件實例,這也就是:
var body: some View { } 

此處宣告物件屬性(變數) body 並給予初始化,我們不用等號 = 來指定值,而是一個大括號段落 { },這是本單元 Swift 的新語法,官方術語稱為 "computed property"。

在此處,我們取用 SwiftUI 物件庫裡面的 Text 物件:
        Text("I ❤️ U")
.font(.title)

要注意 Text 和 String 有所不同,雖然都是與文字有關,String 字串是基本資料類型,而 Text 則是 SwiftUI 裡面的物件類型。

Text 物件傳入一個 String 字串 "I ❤️ U" 作爲初始化參數,並且呼叫一個方法 .font() 來調整屬性,將字串的字型放大為 .title 。Text 物件定義了幾個預設的字型大小,由大到小分別為
 .largeTitle
.title
.title2
.title3
.headline
.subheadline
.body
.callout
.caption
.caption2

程式執行的結果如下圖。注意我們並未指定 Text 的位置, SwiftUI 會自動將 View 盡量放在螢幕的中間位置。
#3 什麼是 View 物件?

上面提到 View 概念上是一個假想的方形可視範圍,但表現在Swift語法上,View 是一個 protocol,中文稱為「規範」,這是什麼意思呢?

我們知道「資料類型」(Data Type)是對資料的分類,String 是一類,Int 是另外一類。用 struct 可以組合出新的分類,定義新的資料類型。

規範(protocol)則是對「資料類型」往上再做一層分類,符合規範 A 的資料類型是一大類,符合規範 B 的資料類型是另外一大類。就如同動植物的分類,有「界門綱目科屬種」不同層次。

所以 View 規範是一個大類別,只要符合 View 規範條件的物件類型,例如 SwiftUI 提供的 Text, Image, ....以及我們定義的「我愛你」,都屬於 View 這個大類別,這些物件我們通稱為 View 物件,就像鯨魚、大象、貓狗通稱哺乳動物一樣。

View 規範、物件類型、物件實例等階層概念的示意圖如下:


所以範例程式2-1a中的 var body: some View { } ,指的是變數 body 屬於「某些」 View 規範的資料類型,簡單地說,變數 body 只要屬於 View 物件就行。some 是 Swift 表示「某些」大類別的關鍵字。

protocol 是用來宣告(或定義)一個規範(大類別)的關鍵字,本單元並不需要定義新規範,只會用到Apple原廠定義好的。

了解 View 規範的意義後,範例2-1a的程式碼就能做出一些變化,有好幾種寫法可以達到同樣的畫面,如下例。

// 2-1b 我愛你(Text)
// Revised by Heman, 2021/08/04
import PlaygroundSupport
import SwiftUI

struct 我愛你: View {
var body: some View = Text("I ❤️ U").font(.title)
}

var 情書 = 我愛你()
PlaygroundPage.current.setLiveView(情書)



最精簡只要3句,就能在圖形介面中顯示文字,如下例。

// 2-1c 我愛你(Text)
// Revised by Heman, 2021/08/04
import PlaygroundSupport
import SwiftUI
PlaygroundPage.current.setLiveView(Text("I ❤️ U").font(.title))


總結來說,setLiveView() 參數需要一個 View 物件的實例,可以是現成的 Text(),也可以是用自行定義的 View 物件。

如果是自行定義的物件,setLiveView() 會去抓物件屬性 body (主體)的值,當然也必須是一個(已存在的) View 物件實例,然後把它顯示出來。

但是要注意,2-1b及2-1c都不是我們要學習的目的,因為實際程式通常比這些複雜很多,只有上一節2-1a的語法結構才能應付未來的需要。
#4 調整文字屬性

圖形介面的 Text,和文字模式的主控台輸出 String,最大區別是什麼呢?應該就是 Text 可以調整不同的字型、字體、大小、顏色....等等,這些都是 Text 物件的屬性,在程式裡面該如何變更呢?

利用範例2-1a的句型,加上前面學過的 Unicode,再結合「Google 翻譯」網頁提供全世界100多種文字的翻譯,我們可以寫出相當豐富有趣的內容,以下我們有請 Google 小姐來翻譯「你愛我嗎?」

// 2-1d 你愛我嗎(Text)
// Created by Heman, 2021/08/03
import PlaygroundSupport
import SwiftUI

let 各國文字 = [
"Do you love me?",
"Est-ce que tu m'aimes?",
"តើអ្នកស្រលាញ់ខ្ញុំទេ?",
"私を愛していますか?",
"Ты любишь меня?",
"당신은 나를 사랑합니까?",
"എന്നെ ഇഷ്ടമാണോ?",
"Дали ме сакаш?",
"ትወጂኛለሽ?",
"Გიყვარვარ?",
"Bạn có yêu tôi không?",
"Դու սիրում ես ինձ?",
"אתה אוהב אותי?",
"هل تحبني؟",
"👩🦰💕🧑🦰?"]

var 國際版 = ""
for 字串 in 各國文字 {
國際版 = 國際版 + 字串 + "\n"
}

struct 你愛我嗎: View {
var body: some View {
Text(國際版)
.font(.largeTitle)
.foregroundColor(.blue)
}
}

print(國際版)
PlaygroundPage.current.setLiveView(你愛我嗎())


注意陣列「各國文字」倒數第3行以色列「希伯來文」與倒數第2行「阿拉伯文」,這兩種中東地區使用的文字是由右向左書寫,與西方文字方向相反。而最後一個表情符號是筆者添加,不是Google小姐翻譯的。

執行結果如下圖。


在此範例程式中,我們先用一個 for 迴圈將陣列裡面的字串連在一起,指定給「國際版」變數,這是第1單元的基礎知識。
var 國際版 = ""
for 字串 in 各國文字 {
國際版 = 國際版 + 字串 + "\n"
}

接下來在 View 結構裡面將「國際版」字串顯示出來,與2-1a同樣的寫法。
struct 你愛我嗎: View {
var body: some View {
Text(國際版)
.font(.largeTitle)
.foregroundColor(.blue)
}
}

不過這次的 Text() 物件有兩個調整屬性的函式,分別將字體設為大標、顏色為藍色:
        Text(國際版)
.font(.largeTitle)
.foregroundColor(.blue)

這種可以調整物件自身屬性的方法(函式),我們特稱為物件的 "Modifier",或稱「修飾語」。通常修飾語的撰寫風格,就像這例子,一個修飾語單獨寫一行。

SwiftUI 每一個 View 物件,都有相關的修飾語,用以改變屬性,呈現不同的畫面風格,所以學習修飾語也是本單元的重點。

下表為 Text 的相關修飾語,大家可以自行實驗看看,每個修飾語 () 內的參數不盡相同,但Swift Playgrounds 都會有提示,按照提示去做就對了。

表2-1 Text 修飾語
# Text 修飾語 說明
1 font() 字型與大小
2 bold() 粗體
3 italic() 斜體
4 strikethrough() 刪除線
5 underline() 底線
6 tracking() 字距
7 multilineTextAlignment() 對齊方式
8 lineSpacing() 行距

為什麼上表沒有提到 foregroundColor() 修飾語呢?因為這個是所有 View 通用的,我們在下一課詳細說明。

程式最後(倒數第二句),用 print() 輸出到主控台,在 Swift Playgrounds 中可以同時顯示圖形介面與主控台的內容。

通常在App 螢幕上,不會顯示主控台輸出的文字,因此,我們可以利用主控台當作 "debug" 用途,也就是說,即便是在圖形介面的程式中,我們也可以寫 print() 輸出一些文字,讓我們確認程式碼的正確性,不會影響到App的畫面。

筆者大多在 Mac mini 使用 Swift Playgrounds寫程式,這些程式會自動同步到 iCloud上面,所以也可用另一台 iPad 修改程式,如以下影片示範。
#5 第2課 顯示圖片

想在程式裡面顯示圖片的話,先要了解圖片檔案從哪裡讀取,有幾種方式:

(1) 先將圖片檔案匯入,與程式碼捆綁在一起
(2) 用程式讀取設備的圖片檔案
(3) 用程式從網路上抓圖
(4) 用程式控制設備的相機拍照

本單元只介紹第一種基本做法,就是先將圖片檔案準備好,在 Swift Playgrounds 中,將圖片檔案匯入,然後用程式去讀取。匯入的操作過程,請參考最下面影片示範。

匯入圖片檔案後,程式就很簡單,跟2-1a 的 6句程式碼結構幾乎一樣。
// 2-2a 顯示圖片(Image)
// Revised by Heman, 2021/08/05
import PlaygroundSupport
import SwiftUI

struct 相框: View {
var body: some View {
Image(uiImage: UIImage(named: "IMG_8935.jpg")!)
.resizable()
}
}

PlaygroundPage.current.setLiveView(相框())

只是將其中 Text 物件換成 Image 物件:
        Image(uiImage: UIImage(named: "IMG_8935.jpg")!)
.resizable()

不過用法有幾個差異:
  1. Image 需要一個「照片物件」當作參數,在此沿用 UIKit 的照片物件: UIImage(named: "檔案名稱")
  2. UIImage()! 後面有個驚嘆號,這表明 UIImage 物件可能會初始化失敗,是個 Optional 類型(參考第1單元第9課),在此必須加上 ! 來強制取用。
  3. 圖片的修飾語跟文字當然有所不同,在這裡用 resizable() 配合螢幕調整圖片大小。


此範例如果執行失敗,最可能就是圖片檔案匯入沒有成功,或是檔案名稱不一致。記得要匯入圖片,修改檔案名稱 "IMG_8935.jpg" 後才能執行。(範例圖片來源:筆者拍攝)

匯入圖片檔案的步驟如下:


在 iPad 上的新增頁面以及匯入圖片檔案的操作示範影片:

影片中圖片來源:Photo by Sultan Samid on Unsplash
#6 隨機照片

從 2-1a, 2-2a 兩個範例中,我們知道要定義一個 View 物件類型,基本句型就是:
struct 名稱: View {
var body: some View { }
}
宣告一個符合 View 規範的 struct 類型,屬性裡面必須包含一個名為 body (主體) 的變數,變數類型則是某些已定義的 View 物件。

所以其實 View 物件就是一種 struct 定義的結構化資料,我們可以善用第一單元學過 struct 物件的知識,來加以變化。

在第一單元第10課還學過「時間」(Date),我們來設計一個隨機選擇照片的相框,如果現在時間秒數是偶數,就顯示照片A,如果是奇數就顯示照片B。

// 2-2b 照片二選一
// Revised by Heman, 2021/08/05
import PlaygroundSupport
import SwiftUI

struct 相框: View {
var 檔名: String
init(_ p: String) {
檔名 = p
}
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}

let 風景 = 相框("unsplash01.jpg")
let 人物 = 相框("unsplash02.jpg")
let 偶數秒 = Int(Date().timeIntervalSinceReferenceDate) % 2 == 0
var 選擇 = 偶數秒 ? 人物 : 風景
PlaygroundPage.current.setLiveView(選擇)

在宣告 View 結構中,我們用 init() 初始化函式來簡化「相框」的參數,只要給檔案名稱就行。
    init(_ p: String) {
檔名 = p
}

同時給 Image 增加一個修飾語 scaledToFit() 在縮放照片時維持長寬比例。
        Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()

最後取得目前時間的秒數,判斷是否為偶數。在運算式中
Int(Date().timeIntervalSinceReferenceDate) % 2 == 0
必須將Date().timeIntervalSinceReferenceDate 的 Double 實數類型轉成 Int 類型,才能用 % 計算除以2的餘數,然後判斷是否等於(==) 0。

還記得 ? : 的用法嗎?
var 選擇 = 偶數秒 ? 人物 : 風景
如果「偶數秒」為真(true),就指定值為「人物」,否則就選「風景」。

執行結果顯示如下(圖片選自Unsplash (Photo by OSPAN ALI)):


利用這樣的設計,我們可以再匯入更多(n張)照片,擴充成一個相框陣列,然後根據時間秒數(除以n)的餘數,選擇陣列中的相框加以顯示,這樣每次執行就會出現更多隨機照片,就當成作業了。


註解
  1. 用網路圖片來創作要注意其著作權的使用範圍,自己練習的話,用手機隨手拍張照片最方便。
  2. Unsplash 是一個提供自由授權照片的網站,其授權說明如下:
    https://unsplash.com/license
    License
    Unsplash photos are made to be used freely. Our license reflects that.

    All photos can be downloaded and used for free
    Commercial and non-commercial purposes
    No permission needed (though attribution is appreciated!)

    What is not permitted 👎
    Photos cannot be sold without significant modification.
    Compiling photos from Unsplash to replicate a similar or competing service.
#7 視圖修飾語(View Modifiers)

我們對 View (視圖)已有初步的認識,Text 文字與 Image 圖片都是一種 View 物件,可以顯示在螢幕上。每種 View物件都有專屬的修飾語,以調整它們的屬性。

此外 SwiftUI 也提供了一些通用的修飾語,稱為 View Modifiers(視圖修飾語),只要是 View 物件都可以適用,這些視圖修飾語對於UI畫面的設計非常重要。

下表列出基本常用的視圖修飾語。

表2-2 常用的視圖修飾語
# 視圖修飾語(View Modifiers) 用途說明 第2單元
學習地圖
1 frame() 調整 View 範圍大小(預設根據實際內容自動設定視圖範圍) 第4課
2 position() 調整View中心位置(預設在螢幕中心) -
3 offset() 調整 View 位置偏移量(預設水平垂直均對齊中線) 第2課
4 border() 設定邊框顏色與線條寬度(預設無邊框) 第3課
5 padding() 設定四週間距(預設無間距) 第3課
6 background() 設定底層背景顏色或圖案(或任何View物件) -
7 overlay() 設定上層重疊視圖(任何View物件) 第2課
8 blendMode() 重疊時混合模式 -
9 foregroundColor() 調整物件顏色 第1課
10 clipShape() 剪裁圖案 -
11 cornerRadius() 四角設定為圓弧 第3課
12 mask() 任何形狀的遮罩 第5課
13 scaleToFit() 圖案大小配合邊框 第2課
14 scaleEffect() 調整縮放效果 -
15 rotationEffect() 設定旋轉效果 -
16 projectionEffect() 設定投影效果 -
17 transformEffect() 設定圖型轉置矩陣 -
18 blur() 設定模糊化 第2課
19 opacity() 設定透明度 第6課
20 brightness() 調整亮度 -
21 contrast() 調整對比 -
22 saturation() 調整飽和度 -
23 grayscale() 調整灰階 -
24 shadow() 設定陰影 第3課
25 animation() 設定動態效果 第10課
26 transition() 設定轉場效果 -
27 hidden() 將 View 隱藏,不出現在畫面中 -
其他還有更多未列在表中,包括提供無障礙(accessibility)功能的修飾語、控制元件修飾語、以及今年iOS 15新增的修飾語....等等,多達上百個。

我們用一個範例程式來練習修飾語,將第1課文字與第2課圖片的內容整合在一起。
// 2-2c View 修飾語
// Created by Heman, 2021/08/06
import PlaygroundSupport
import SwiftUI

struct 標示: View {
var body: some View {
Text("こんにちは!\n我的名字叫莎莎")
.font(.system(size: 36))
.multilineTextAlignment(.center)
.offset(y: 150)
.blur(radius: 2)
}
}

struct 相框: View {
var 檔名: String
init(_ p: String) {
檔名 = p
}
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}
let 組合 = 相框("IMG_8935.jpg").overlay(標示())
PlaygroundPage.current.setLiveView(組合)


文字的部分,我們用了四個修飾語,來設計「標示」物件:
        Text("こんにちは!\n我的名字叫莎莎")
.font(.system(size: 36))
.multilineTextAlignment(.center)
.offset(y: 150)
.blur(radius: 2)

先將字體放大到36(比 .largeTitle 更大),兩行文字向中對齊,offset(y: 150)位置往下移150點,最後將字體模糊化。


offset(y: 150) 用到螢幕座標。與標準的數學座標稍有不同,螢幕座標是以螢幕左上角為坐標原點,X軸往右為正、Y軸往下為正。原來預設會將文字放在螢幕正中央,但我們希望往下偏移150點(y: 150),不要擋到貓咪的眼睛。

blur(radius: 2) 可將 View 模糊化,模糊程度由參數(radius: 2)半徑多寡來決定,半徑越大越模糊。

照片相框的修飾語跟範例2-2b相同。

然後新定義一個常數「組合」,將這兩個 View 物件用 overlay() 修飾語組合在一起:
let 組合 = 相框("IMG_8935.jpg").overlay(標示())

顯示結果如下圖。
#8 第3課 垂直、水平排列(VStack/HStack)

上一課介紹視圖修飾語(View Modifier)時,曾利用 overlay() 將兩個 View 結合(重疊)在一起,其實這並不是修飾語的主要用途,「修飾語」顧名思義,主要目的還是為了「調整」物件屬性。

想要將多個View物件整合在螢幕上面,更好的做法是利用 SwiftUI 提供編排版面的View物件,如本課介紹的 VStack 與 HSatck,分別用來作垂直排列與水平排列。

VStack 是 "Vertical Stack" 的簡寫,字面上是「垂直堆疊」的意思,也就是將多個View物件,在螢幕上垂直排列的意思。HStack 類似,是 "Horizontal Stack" 水平堆疊之意。

我們以 VStack 為例,來寫一個範例程式。
// 2-3a HStack/VStack
// Created by Heman, 2021/08/07
import PlaygroundSupport
import SwiftUI

struct 標示: View {
var 標語: String
init(_ p: String) { 標語 = p }
var body: some View {
Text(標語)
.font(.system(size: 36))
.multilineTextAlignment(.center)
.shadow(color: .yellow, radius: 2, x: 0, y: 0)
.blur(radius: 1)
}
}

struct 相框: View {
var 檔名: String
init(_ p: String) { 檔名 = p }
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
}
}

struct 圖文組合: View {
var body: some View {
VStack {
相框("IMG_8935.jpg")
.cornerRadius(30)
.padding()
.border(Color.red)
標示("こんにちは!\n我的名字叫莎莎")
.border(Color.red)
}
}
}
PlaygroundPage.current.setLiveView(圖文組合())

我們先設計一個文字樣式,仿照範例2-2b的做法,將字串改為輸入參數,加上4個修飾語:
        Text(標語)
.font(.system(size: 36))
.multilineTextAlignment(.center)
.shadow(color: .yellow, radius: 2, x: 0, y: 0)
.blur(radius: 1)

多出一個 shadow() 修飾語會在文字周圍產生黃色光暈,配合 blur() 稍微模糊化,最後做出這樣的效果:


雖然 shadow() 字面意思是「陰影」,但是用亮色就是光暈,用深色就是陰影,語法是一樣的。

表2-3a shadow(color: .yellow, radius: 2, x: 0, y: 0) 參數說明
shadow() 參數 參數值 作用
color: .yellow 陰影顏色: 黃色 產生光暈的效果
radius: 2 陰影半徑: 2 半徑越大陰影範圍越大
x: 0, y: 0 偏移座標: (0, 0) 改變光源/陰影位置


接下來再設計一個圖片樣式,跟2-2b一樣就行。
        Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()

最後,如何利用 VStack 將兩者結合在一起?其實也很簡單,因為我們已經定義好文字「標示」、圖片「相框」兩個View物件類型,直接初始化(如同按下模具的「複製」按鈕)產生物件實例即可,VStack會將物件按出現順序由上而下垂直排列。
        VStack {
相框("IMG_8935.jpg")
.cornerRadius(30)
.padding()
.border(Color.red)
標示("こんにちは!\n我的名字叫莎莎")
.border(Color.red)
}

在這裡,我們還可以繼續用 View 修飾語來調整屬性,用 cornerRadius() 將相框的四角調整為圓弧形、padding() 在四週增加一點空間(圖片稍往內縮),最後用border() 顯示出 View 的範圍邊界。

這裡有個重要觀念,就是「View 修飾語的先後順序是有關係的」,例如在此例中,如果變更「相框」的修飾語順序:
            相框("IMG_8935.jpg")
.padding()
.cornerRadius(30)
.border(Color.red)
就會產生不同的結果,動手試試看!

執行結果加上說明如下圖:


要注意,我們並未指定個別View的位置與大小,這由 VStack 自動調整,位置中央對齊垂直中線,大小則儘量利用整個空間。此例中,文字已指定 size: 36,所以大小不會變,但圖片是 "resizable",所以按長寬比例放到剩餘空間的最大。所以如果排列的圖片越多,就會變越小。

總結來說,VStack/HStack 是用來組合已定義的 View 物件,組合之後的結果也是一個 View 物件,仍然可以適用 View 修飾語。那麼,View 修飾語應該放在個別的視圖元素中,還是放在 VStack/HStack 裡面呢?想想看。
#9 版面設計

了解 VStack 的用法之後,HStack 用法也完全一樣,差別只在 HStack 是將View物件水平排列,橫向中央對齊水平中線。

有了 VStack 和 HStack,我們就能夠同時進行垂直、水平排版,設計整個版面的結構,這樣愈來愈接近完整的 App版面設計功能。

// 2-3b 版面設計
// Created by Heman, 2021/08/07
import PlaygroundSupport
import SwiftUI

struct 標示: View {
var 標語: String
init(_ p: String) { 標語 = p }
var body: some View {
Text(標語)
.font(.system(size: 28))
.multilineTextAlignment(.center)
.shadow(color: .yellow, radius: 2, x: 0, y: 0)
.blur(radius: 1)
}
}

struct 相框: View {
var 檔名: String
init(_ p: String) { 檔名 = p }
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
.cornerRadius(20)
.shadow(color: .gray, radius: 2, x: 5, y: 5)
.padding()
}
}

struct 版面: View {
var body: some View {
VStack {
HStack {
標示("こんにちは!\n我的名字叫莎莎")
相框("IMG_8935.jpg")
}
HStack {
相框("unsplash01.jpg")
標示("SORRY\nEARTH")
}
HStack {
標示("Сәлем!\nМенің атым Анна")
相框("unsplash02.jpg")
}
}
}
}
PlaygroundPage.current.setLiveView(版面())

此範例中,文字「標示」與上一個2-3a一模一樣,照片「相框」多一個 shadow() 修飾語,在照片右下角加個陰影。
.shadow(color: .gray, radius: 2, x: 5, y: 5)
參數 x: 5, y: 5 表示陰影位置往右、往下各移5點,就變成右下角的陰影。

同時增加了一個「版面」的設計:
struct 版面: View {
var body: some View {
VStack {
HStack {
標示("こんにちは!\n我的名字叫莎莎")
相框("IMG_8935.jpg")
}
HStack {
相框("unsplash01.jpg")
標示("SORRY\nEARTH")
}
HStack {
標示("Сәлем!\nМенің атым Амина.")
相框("unsplash02.jpg")
}
}
}
}

最裡面是三組類似的水平排列,用 HStack 先做出一列文字與圖片的組合
            HStack {
標示("こんにちは!\n我的名字叫莎莎")
相框("IMG_8935.jpg")
}

然後外圍再利用 VStack 將三列組合垂直排列下來,形成一個完整的畫面。



有注意到照片右下角的陰影嗎?

註解
  1. 第三列的文字「標示("Сәлем!\nМенің атым Амина.")」是哈薩克文,意為「你好!我名叫阿米娜」。
  2. 哈薩克文使用「斯拉夫字母」(Cyrillic),是中亞、俄羅斯、蒙古、東歐等地區的主要文字,斯拉夫字母的 и 發 /ɪ/ 音、 н 發 /n/ 音,所以 Амина 可音譯為 Amina。
  3. 斯拉夫字母源自希臘字母,約西元九世紀由保加利亞傳教士所創造。
#10 第4課 圖層(ZStack)

圖層(ZStack)的概念和 VStack/HStack 類似,但堆疊方向是垂直於螢幕畫面,往眼睛的方向一層一層堆上來,所以稱為「圖層」。

為什麼稱為 Z 呢?因為與螢幕座標有關,我們知道數學有三維座標,也就是 X, Y, Z 三軸,螢幕座標也類似,不過Y軸方向相反,是往下方為正。

螢幕座標以螢幕左上角為座標原點,X, Y軸通常以1點「畫素」(pixel)為單位。圖解如下。


所以ZStack就是往Z軸方向堆疊,也稱前後堆疊或圖層。這個功能和第2課的 overlay() 重疊修飾語相當,但更多彈性。舉例來說,我們想設計如下的版面,利用 VStack/HStack/ZStack 很容易就構想出來:



這樣寫起UI程式就很直觀,請參考下面範例程式。
// 2-4a 圖層(ZStack)
// Created by Heman, 2021/08/08
import PlaygroundSupport
import SwiftUI

struct 標示: View {
var 標語: String
var 大小: CGFloat
init(_ p: String, size: CGFloat) {
標語 = p
大小 = size
}
var body: some View {
Text(標語)
.font(.system(size: 大小))
.bold()
}
}

struct 相框: View {
var 檔名: String
init(_ p: String) { 檔名 = p }
var body: some View {
Image(uiImage: UIImage(named: 檔名)!)
.resizable()
.scaledToFit()
.cornerRadius(16)
}
}

struct 版面: View {
var body: some View {
VStack {
ZStack {
Rectangle()
.foregroundColor(.orange)
標示("Жерді сақта!", size: 40)
.multilineTextAlignment(.center)
}
.frame(height: 80)
ZStack {
相框("unsplash02.jpg")
HStack {
標示("Kazakhstan\nGirl Urge to\nSave the Earth", size: 32)
.multilineTextAlignment(.trailing)
.foregroundColor(.white)
.shadow(color: .black, radius: 10, x: 0, y: 0)
.blur(radius: 1)
相框("unsplash01.jpg")
.padding()
}
.frame(height: 200)
.offset(y: 180)
}
}
}
}
PlaygroundPage.current.setLiveView(版面())

先看看執行結果,是不是很接近我們原先構想的版面?

最內圈是包含兩個View物件的 HStack
HStack {
標示("Kazakhstan\nGirl Urge to \nSave the Earth", size: 32)
.multilineTextAlignment(.trailing)
.foregroundColor(.white)
.shadow(color: .black, radius: 10, x: 0, y: 0)
.blur(radius: 1)
相框("unsplash01.jpg")
.padding()
}
.frame(height: 200)
.offset(y: 180)

畫面左下方的「標示」和第3課差不多,但需增加一個 size 參數,並且向右對齊(.trailing)。與右下方小圖,兩者以 HStack 水平排列,然後利用 frame() 修飾語將 HStack 高度縮小到200點,再用 offset() 由螢幕中央往下移180點。

接下來這個 HStack與背後大圖以 ZStack前後圖層的方式組合一起。
ZStack {
相框("unsplash02.jpg")
HStack { }
.frame(height: 200)
.offset(y: 180)
}

最後再和上方的標題,用 VStack 垂直排列,就大功告成了。

上方標題的做法說明如下。
ZStack {
Rectangle()
.foregroundColor(.orange)
標示("Жерді сақта!", size: 40)
.multilineTextAlignment(.center)
}
.frame(height: 80)

其實就是利用ZStack將一個橙色矩形與一行文字標示組合在一起,並使用 frame() 設定View高度為80點。矩形 Rectangle() 第一次遇到,等下一課「幾何形狀與顏色」時再一起說明。

修飾語 frame() 也是第一次使用,這是用來設定 View物件的長寬範圍,3個參數均可省略,不必全寫,說明如下表。

表2-4 frame() 參數說明
frame() 參數 預設參數值 作用
width: 100 nil (自動調整) View 寬度(點/畫素)
height: 200 nil (自動調整) View 高度(點/畫素)
alignment = .center .center 預設向中央對齊


這裡的參數 width, height, 以及字體 font() 的 size,資料類型都是 "CGFloat",這是為了相容性,沿用早期物件庫 "Core Graphics" (字首CG)裡面的物件,CGFloat 相當於64位元 Double 實數類型。
文章分享
評分
評分
複製連結

今日熱門文章 網友點擊推薦!