# swift
## 加入第三方庫

按右下角add package

按add package

這點很重要 要到Frameworks,Libraries,and Embedded Content按\+號

把Alamofire add進去

## 語法
## var let
```swift
//練習一
let pi: Double = 3.14
var radius: Double = 10.0
var circleArea: Double = radius * radius * pi
var a: String = "dfdf"
print("圓面積為: \(circleArea)")
print("aa","bb")
```
```
圓面積為: 314.0
aa bb
```
## if
```swift
//練習五
var examSore = 80
if(examSore >= 60){
print("恭喜你通過考試")
}
examSore = 40
if(examSore >= 60){
print("恭喜你通過考試")
}
```
```
恭喜你通過考試
```
## function
```swift
//練習六
func scorefunc(score: Int){
if score >= 85{
print("恭喜你通過考試~表現得很好喔")
}
else if score >= 60{
print("恭喜你通過考試")
}
else{
print("再好好加油")
}
}
var examSore2: Int = 90
scorefunc(score: examSore2)
examSore2 = 70
scorefunc(score: examSore2)
examSore2 = 50
scorefunc(score: examSore2)
func scorefunc2(_: Int){ //也可留白
}
scorefunc2(1)
```
```
恭喜你通過考試~表現得很好喔
恭喜你通過考試
再好好加油
```
```swift
//練習七
func scorefunc2(score: Int){
if score >= 85 && score <= 100{
print("你本學期成積為A")
}
else if score >= 75{
print("你本學期成積為B")
}
else if score >= 60{
print("你本學期成積為C")
}
else if score >= 0{
print("你本學期成積為D")
}
}
var examSore3: Int = 90
scorefunc2(score: examSore3)
examSore3 = 80
scorefunc2(score: examSore3)
examSore3 = 65
scorefunc2(score: examSore3)
```
```
你本學期成積為A
你本學期成積為B
你本學期成積為C
```
```swift
//function
func greeting1(){
print("Hello,你好")
}
func greeting2(name:String){
print("Hello,你好\(name)")
}
func greeting3()->String{
return "Hello,你好"
}
func greeting4(name:String)->String{
return "Hello,你好\(name)"
}
func greeting5(name1:String, name2:String)->String{
return "Hello,你好\(name1)和\(name2)"
}
greeting1()
greeting2(name: "Alice")
var str1:String = greeting3()
print(str1)
print(greeting4(name: "Alice"))
var str2:String = greeting5(name1:"Alice", name2:"Joyce")
print(str2)
```
```
Hello,你好
Hello,你好Alice
Hello,你好
Hello,你好Alice
Hello,你好Alice和Joyce
```
```swift
//練習十
func plus(a:Int, b:Int)->Int{
return a+b
}
var v1:Int = 4,v2:Int = 9;
var result:Int = plus(a: v1, b: v2)
print("\(v1) + \(v2) = \(result)")
```
```
4 + 9 = 13
```
### 可以傳tuple出來
```swift
func aaa(_ a:Int, _ b:Int)->(Int, Int){
return (a+1,b+1)
}
let result = aaa(1,2)
print(result)
```
```
(2, 3)
```
### 在函式裡面放函式
```swift
func add(_ n1:Int,_ n2:Int)->Int{
return n1+n2;
}
func sub(_ n1:Int,_ n2:Int)->Int{
return n1-n2;
}
//法一
func printResult(_ op:(Int,Int)->Int, _ a:Int, _ b:Int){
print(op(a,b))
}
//法二
typealias operate = (Int,Int) -> Int //跟typedef一樣
func printResult2(_ op:operate, _ a:Int, _ b:Int){//取代原本冗長部分
print(op(a,b))
}
printResult(add, 3, 4)
printResult(sub, 3, 4)
```
```
7
-1
```
### 含式同名不同參數
完全一樣會出現ambiguous錯誤

```swift
func add(_ n1:Int,_ n2:Int)->Int{
return n1+n2;
}
func add(n1:Int,_ n2:Int)->Int{//些微不一樣也可以 參數或回傳不一樣都可以
return n1+n2+1;
}
func add(_ n1:String,_ n2:String)->String{
return n1+n2;
}
print(add(1, 2))
print(add(n1: 1, 2))
print(add("aa","bb"))
```
```
3
4
aabb
```
### 系統其實幫你隱藏的預設了回傳 -> Void
都不會報錯
```swift
func printHello1(){
print("hello")
}
printHello1()
func printHello2()->Void{
print("hello")
}
printHello1()
func printHello3(){
print("hello")
return Void()
}
printHello1()
func printHello4(){
print("hello")
return ()
}
printHello1()
func printHello5(){
print("hello")
return
}
printHello1()
```
## for
```swift
//練習八
for i in 1 ... 9{
print("5 * \(i) = \(5*i)")
}
```
```
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
5 * 6 = 30
5 * 7 = 35
5 * 8 = 40
5 * 9 = 45
```
```swift
//for(int i=10;i<=1010;i+=100)
for i in stride(from: 10,through: 1010,by: 100){
print(i)
}
```
```
10
110
210
310
410
510
610
710
810
910
1010
```
```swift
var arr1=["a","b","c"]
for name in arr1{
print(name)
}
```
```
a
b
c
```
```swift
for i in 1...10 where i%2 == 1{
print(i)
}
```
```
1
3
5
7
9
```
## while
```swift
//練習九
var index = 1
while index <= 9{
print("5 * \(index) = \(5*index)")
index += 1
}
```
```
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
5 * 6 = 30
5 * 7 = 35
5 * 8 = 40
5 * 9 = 45
```
```swift
repeat{
code
}while condition //就是do while
```
## class
```swift
//練習十一
class Person{
var firstname:String
var lastname:String
init(firstname:String, lastname:String){
self.firstname = firstname
self.lastname = lastname
}
func sayHello(){
print("Hello,my name is \(lastname) \(firstname)")
}
}
var alice:Person = Person(firstname: "Alice", lastname: "Lin")
var joyce:Person = Person(firstname: "Joyce", lastname: "Lu")
alice.sayHello()
joyce.sayHello()
```
```
Hello,my name is Lin Alice
Hello,my name is Lu Joyce
```
## struct
```swift
//練習十二
struct Movie{
var title:String
var director:String
func describe(){
print("\(title)這部電影是由\(director)所執導")
}
}
var lucy:Movie = Movie(title: "露西",director: "盧・貝松")
var anna:Movie = lucy
lucy.describe()
anna.describe()
anna.title = "安娜"
lucy.describe()
anna.describe()
```
```
露西這部電影是由盧・貝松所執導
露西這部電影是由盧・貝松所執導
露西這部電影是由盧・貝松所執導
安娜這部電影是由盧・貝松所執導
```
## class與struct的比較
```swift
struct PersonStruct {
var name: String
}
class PersonClass {
var name: String
init(name: String) {
self.name = name
}
}
var structPerson = PersonStruct(name: "John")
var classPerson = PersonClass(name: "John")
var copiedStruct = structPerson
var copiedClass = classPerson
copiedStruct.name = "Mike"
copiedClass.name = "Mike"
print(structPerson.name) // John (不受影響)
print(classPerson.name) // Mike (受到影響)
```
```
使用 struct 時,改變 copiedStruct 不會影響原始值。
使用 class 時,改變 copiedClass 會影響原始物件。
```
* 使用 struct:
* 當需要一個 簡單且不可變的物件(例如資料模型、座標點、顏色等)。
* 當希望提升效能時,因為結構體是 輕量且效能較佳。
* 當不需要繼承功能時。
* 使用 class:
* 當需要 共享狀態(例如管理應用程式中的單一實例)。
* 當需要使用 多態與繼承。
* 當涉及 複雜物件與大量資料處理。
## String
```swift
//練習二
var myGreeting = "Hello, "
var name = "John"
myGreeting += name
print(myGreeting)
let wordCount = myGreeting.count
print("myGreeting 長度為 \(wordCount)")
```
```
Hello, John
myGreeting 長度為 11
```
## array
```swift
//練習三
var studentScores3: [Int] = []
studentScores3.append(85)
studentScores3.append(90)
studentScores3.append(78)
print("目前有\(studentScores3.count)筆學生資料")
studentScores3[0] = 100;
print("3號同學分數為\(studentScores3[0])")
```
```
目前有3筆學生資料
3號同學分數為100
```
複製陣列
```swift
var arr = [1,2,3,4]
var newArr = Array(arr[1...3])
print(newArr)//[2, 3, 4]
```
初始化
```swift
//var arr1 = []//錯誤
//var arr2: [Int]//錯誤
var arr3: [Int] = []//正確
//arr2.append(1)//錯誤 沒有初始值
arr3.append(1)
print(arr3) // [1]
```
元素控制
```swift
var arr3: [Int] = []
arr3.append(1)
arr3 += [3] //也是加在最後面
arr3.insert(2, at: 1)
print(arr3)
arr3 = [4,5,6]//可以直接指定成另一個陣列
print(arr3)
arr3.swapAt(0, 2)//交換
print(arr3)
arr3.remove(at: 1) //移除指定
print(arr3)
arr3.removeFirst() //popfront
print(arr3)
arr3.removeLast() //popback
print(arr3)
print(arr3.isEmpty)//看是否空
```
```
[1, 2, 3]
[4, 5, 6]
[6, 5, 4]
[6, 4]
[4]
[]
true
```
查看
```swift
var arr3: [Int] = [1,5,6,5,7]
print(arr3.count) //數有多少個元素
print(arr3.first) //Optional(1) 要解開
if let i = arr3.first ,let j = arr3.last ,let k = arr3.max(){
print(i,j,k) //1
}
print(arr3.contains(2))
if let i = arr3.firstIndex(of: 5), let j = arr3.lastIndex(of: 5){//找5這個元素 第一次出現與最後一次出現的位置 也要解開
print(i,j)
}
for i in arr3.enumerated(){
print(i) // i是一個tupple
}
//法ㄧ
for i in arr3.enumerated(){
print(i.offset,i.element)
}
print()
//法二
for (i,j) in arr3.enumerated(){
print(i,j)
}
```
```
5
Optional(1)
1 7 7
false
1 3
(offset: 0, element: 1)
(offset: 1, element: 5)
(offset: 2, element: 6)
(offset: 3, element: 5)
(offset: 4, element: 7)
0 1
1 5
2 6
3 5
4 7
0 1
1 5
2 6
3 5
4 7
```
陣列放tuple
```swift
let people: [(String, Int)] = [
("Alice", 25),
("Bob", 30),
("Charlie", 22)
]
print(people)
// [("Alice", 25), ("Bob", 30), ("Charlie", 22)]
```
```swift
let products: [(name: String, price: Double)] = [
(name: "iPhone", price: 999.99),
(name: "MacBook", price: 1299.99),
(name: "iPad", price: 799.99)
]
print(products[0].name) // iPhone
print(products[1].price) // 1299.99
```
## ditionary
```swift
//練習四
var days:[String:String] = ["Sunday":"星期天","Monday":"星期一","Tuesday":"星期二"]
print("Monday為\(days["Monday"] ?? "")")
// 那這行程式碼會輸出:
// Monday為星期一
// 如果 days 中沒有 "Monday",則輸出:
// Monday為
days["Sunday"] = "星期日"
print("Sunday為\(days["Sunday"] ?? "")")
days["Wednesday"] = "星期三"
days["Thursday"] = "星期四"
days["Friday"] = "星期五"
days["Saturday"] = "星期六"
print("一星期有\(days.count)天")
days["Sunday"] = nil
print("一星期有\(days.count)天")
print("Sunday為\(days["Sunday"] ?? "")")
```
```
Monday為星期一
Sunday為星期日
一星期有7天
一星期有6天
Sunday為
```
新增刪除
```swift
var a: [String:Int] = [:]//賦予空值
a = ["aa":5,"bb":2,"cc":3]
print(a)//字典沒有順序
print(a["aa"])//可能有nil所以是optional
print(a["dd"])//nil 不會新增"dd"進去
print(a.isEmpty)
print(a.count)
a["aa"] = 1 //修改
a["dd"] = 2 //加新的值進去
a.removeValue(forKey: "bb")//刪除某個值
a["cc"] = nil//或是這樣刪除
```
```
["bb": 2, "cc": 3, "aa": 5]
Optional(5)
nil
false
3
```
遍歷
```swift
var a: [String:Int] = [:]//賦予空值
a = ["aa":5,"bb":2,"cc":3]
for (i,j) in a {
print(i,j)
}
for (_,j) in a{
print(j)
}
for i in a.keys{
print(i)
}
for j in a.values{
print(j)
}
```
```
bb 2
aa 5
cc 3
2
5
3
bb
aa
cc
2
5
3
```
## tuple
```swift
var myTuple = (4,"老王","老牛")//像是pair這種結構
print("\(myTuple.0) \(myTuple.1) \(myTuple.2)")
var (score1, name1, _) = myTuple//直接賦值 沒有要賦值的就用_
print("\(score1) \(name1)")
let myTuple1 = (score:3, name:"李四")//可以命名 let是const的意思
print("\(myTuple1.score) \(myTuple1.name)")
```
## set
```swift
var a: Set<Int> //宣告方式比較特別
a = [1,2,3,1] //重複的數不會再被放進去
print(a)
print(a.contains(2))
a.insert(5)
a.remove(2)
var b: Set<Int> = [3]
print("a:\(a)")
print("b:\(b)")
print(a.intersection(b))//兩個set的交集
print(a.symmetricDifference(b))//全部剪掉交集(對稱差集)
print(a.union(b))//連集
a.formUnion(b)//把b的都加進a
print(a)
```
```
[3, 1, 2]
true
a:[3, 1, 5]
b:[3]
[3]
[1, 5]
[3, 1, 5]
[3, 1, 5]
```
## enum
```swift
enum myenum1 {
case a
case b
}
enum myenum2:String {
case c = "abc"
case d = "def"
}
enum myenum3:Int {
case e = 3 //指定第一個的rawValue
case f
}
enum myenum4:Int {
case g //沒有指定rawValue的話系統會給0
case h //rawValue = 1
}
print("myenum1 \(myenum1.a)") //只會印出a
//print(myenum.a.rawValue) //要加rawValue 的話要先指定成變數
print("myenum2 \(myenum2.c.rawValue)")
print("myenum3 \(myenum3.e.rawValue)")
print("myenum4 \(myenum4.g.rawValue)")
enum Obj {
case a,b,c,d;
}
let tmp:Obj = .a;
if(tmp == .a){
print("good")
}
```
```
myenum1 a
myenum2 abc
myenum3 3
myenum4 0
good
```
## Optional
nil是空值但不代表0
0也是一個值
### 封裝
```swift
var a:String?
//var a:String? = nil 預設會給nil值
print(a)
a="abc"
print(a)
a=nil
print(a)
let b = Int("2")//將字串轉成變數
print(b)//但其實是optional
let c = Int("abc")//因為可能轉錯東西 所以要用optional包裝
print(c)//轉錯了所以是nil
```
```
nil
Optional("abc")
nil
Optional(2)
nil
```
```swift
let weather = wxElement?["time"][0]["parameter"]["parameterName"].stringValue ?? "未知"
//第一個?是怕wxElement會是nil,以optional處理。第二個??是如果是nil就回傳"未知"
```
### 暴力拆箱(有風險)
```swift
var a: Int? = 2
print(a!)
var b: Int? = nil
print(b!)//因為是nil 所以會報錯
```
```
2
報錯
```
### 安全開箱
**if拆箱**
開完箱之後 就不能用(不再同一個scope)
要裡面不是nil才會進入if
```swift
var a: Int? = 2
if(a != nil){
let k = a!
print(k)
}
if let k = a{//簡化成這樣
print(k)
}
if(a != nil){//在scope內變數可以取一樣名子
let a = a!
print(a)
}
if let a = a{//在scope內變數可以取一樣名子
print(a)
}
print(a)
```
```
2
2
2
2
Optional(2)
```
**多重開箱**
```swift
var a: Int? = 2
var b: Int? = 3
if let a = a, let b = b {
print(a + b)
}else{
print("error")
}
```
**guard開箱**
是開完箱之後還可以繼續用那個變數
但是是區域變數
**法一**
```swift
func printNumber(optionalNumber: Int?) {
guard let number = optionalNumber else { //如果開失敗就跳出
print("數字是 nil,結束執行")
return
}
print("數字是:\(number)")
}
```
```
printNumber(optionalNumber: 10)
printNumber(optionalNumber: nil)
```
**法二**
1. 檢查 newChatName 是否為空
2. 如果為空,則立即退出當前函數
3. 如果不為空,則繼續執行後面的代碼
```swift
guard !inputText.isEmpty else { return }
```
**??開箱**
```swift
var a:Int?
var b = a ?? 1 // 如果a是nil 就賦予??後面的值
print(b)
var c:Int? = 3
var d = c ?? 1 // 如果a有值 就賦予a的值 並且是開箱完的值
print(d)
```
```
1
3
```
## closure
```swift
let a = {
(n1:Int, n2:Int) -> Int in
return n1 * n2
}
print(a(2,3))
let b:(Int, Int) -> Int = { //或是先宣告型別再寫函式
(n1,n2) in
return n1 * n2
}
print(b(2,3))
let c:(Int, Int) -> Int = { //只有一行可以不用寫return
(n1,n2) in
n1 * n2
}
print(c(2,3))
let d:(Int, Int) -> Int = { //可以用$0 $1代替
$0 * $1
}
print(d(2,3))
```
```
6
6
6
6
```
## map
`map` 是一個用來「逐一轉換」序列(如陣列)元素的函式。當你對陣列呼叫 `map` 並提供一個閉包(closure)時,Swift 會將陣列中的每個元素丟進該閉包進行運算,並組成一個新的陣列返回。它不會改變原本的陣列,而是產生一個新的。
```swift
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
// doubled 會是 [2, 4, 6, 8, 10]
```
## $0 $1 $2
它們分別代表閉包所接收到的第一個、第二個、第三個等參數。最常見的使用情境是陣列或其它序列型別呼叫 `map`、`filter`、`reduce` 等高階函式時,我們常用 `$0` 代表「目前正在處理的陣列元素」。例如:
```swift
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }
// 這裡 $0 就代表陣列中的每一個元素
```
```swift
let sum = numbers.reduce(0) { partialResult, current in
return partialResult + current
}
簡約後變成這樣
let sum = numbers.reduce(0) { $0 + $1 }
```
## where
`where` 關鍵字可以出現於多種情境下,用來「進一步設定條件或限制」。
### 例子一
在 `for-in` 迴圈後面加上 `where` 來過濾或指定只有符合條件的元素才會進入迴圈內。例如,只想遍歷陣列中是偶數的部分:
```swift
let numbers = [1, 2, 3, 4, 5, 6]
for number in numbers where number % 2 == 0 {
print(number)
}
// 輸出:2, 4, 6
```
### 例子二
- 呼叫了 Swift Array 的一個內建方法 `lastIndex(where:)`,會從陣列最後面往前搜尋,找到**符合條件**(在這裡是 `role == "user"`)的最後一個元素索引(Index),然後返回該索引。
- **`where`** 在這裡的作用:它讓我們可以給一個閉包(`{ $0.role == "user" }`)來描述「想要尋找陣列裡那些符合條件」的元素。
- 如果找不到任何符合條件的元素,`lastIndex(where:)` 就會回傳 `nil`。
```swift
if let lastUserMessageIndex = apiMessages.lastIndex(where: { $0.role == "user" }) {
//code
}
```
## typealies
```swift
typealias 新名稱 = 原型別
```
更語意化的命名
```swift
typealias UserID = String
let userId: UserID = "abc123"
````
簡化複雜型別
```swift
typealias CompletionHandler = (Bool, String) -> Void
func loadData(completion: CompletionHandler) {
// 假設完成時回傳成功與訊息
completion(true, "下載成功")
}
```
系統也會用 typealias(例如 Codable)
```swift
typealias Codable = Encodable & Decodable
//等同於
struct MyData: Encodable, Decodable { ... }
```
# mcp
## cursor mcp 控制figma
指令
```
bun run src/socket.ts
```
```
talk to figma channel id:hqixdob0
```
https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp
下載完這歌專案後
進入專案
安裝 bun 套件管理工具
```
curl -fsSL https://bun.sh/install | bash
```
之後跳出這個代表成功


到這裡把這個貼上
```json
{
"mcpServers": {
"TalkToFigma": {
"command": "bun",
"args": [
"/path/to/cursor-talk-to-figma-mcp/src/talk_to_figma_mcp/server.ts"
//這個要改成自己下載的server.ts位置
]
}
}
}
```

啟動server
```bash
bun run src/socket.ts
```
到figma專案



成功連線

然後這是channel ID

有package.json代表要安裝套件

cd到src/talk_to_figma_mcp
這樣跟cursor講,id要記得改
```
talk to figma channel id:hqixdob0
請先安裝需要的套件後再執行
```
之後執行這個就好了
```
talk to figma channel id:hqixdob0
```
## cursor mcp 把 figma 轉成swiftUI
到這個專案
https://github.com/GLips/Figma-Context-MCP



把mcp代碼複製過來

到figma中的settings


產生一個apiKey(記得權限都要開)
key再貼到到json

接著到網站下載node.js https://nodejs.org/

運行伺服器(測試mcp伺服器可不可以用)
```
npx -y figma-developer-mcp --figma-api-key=自己的key --stdio
```
按重新載入就可以了

**使用方法**
根據官方最佳實踐指南
這個MCP目前還不擅長處理浮動或絕對定位元素
一次改少部分的程式就好
命名框架和群組讓cmp便於識別
官方prompt:
Try Figma's AI to automatically generate names

# swiftUI
## 界面指南
https://github.com/yuaotian/go-cursor-help/blob/master/README_CN.md
## xcode 出錯
### 權限錯誤
遇到這種問題
```
Couldn't create workspace arena folder '/Users/wangguanzhe/Library/Developer/Xcode/DerivedData/studyAssistant-dsearxdlrxxwakbfgbwthxgkdjsd': Unable to write to info file '<DVTFilePath:0x6000134e59e0:'/Users/wangguanzhe/Library/Developer/Xcode/DerivedData/studyAssistant-dsearxdlrxxwakbfgbwthxgkdjsd/info.plist'>'.
```
到這個資料夾 把對應的讀取檔刪掉
```
cd ~/Library/Developer/Xcode/DerivedData
```

### preview crash 的原因
preview沒加環境參數
```swift
#Preview {
ContentView()
}
```
preview加環境參數就正常了
```swift
#Preview {
ContentView()
.environmentObject(TimerManager())
}
```
### 重置
刪除快取
```
rm -rf ~/Library/Developer/Xcode/DerivedData
rm -rf ~/Library/Caches/com.apple.dt.Xcode
```
## 功能介紹
```swift
import SwiftUI
@main //標記應用程式的入口點。
struct chatApp: App { //chatApp 型別是 App protocol 的結構體,表示這個struct是一個應用程式。
var body: some Scene { //body 是必要屬性,類似於 SwiftUI 的 View。
//它的型別是 some Scene,表示需要一個視圖場景來顯示 UI。通常代表應用程式的主要視窗或多視窗環境。
WindowGroup { //用來定義應用程式的主要視窗群組,每當使用者開啟應用程式時,會顯示一個新的視窗。
ContentView()//ContenView是主要視圖
}
}
}
```
```swift
import SwiftUI
struct ContentView: View {//用來定義 SwiftUI 中的畫面。是View protocol,代表它可以作為畫面或元件使用。SwiftUI 中,每個畫面、按鈕、文字等 UI 元件都是 View。
var body: some View {//body以外可以放宣告的變數或函式
//some View 是一種不透明回傳型別,表示 body 會回傳某種符合 View protocol 的物件,但具體的型別對外部不可見。
VStack{ //VStack 也是一種 View
}
}
}
#Preview {//讓右邊可以顯示預覽話面 不會被編譯進去
ContentView()
}
```
## icon圖片與載入頁面
### 加入圖片



### 設定app封面與名子圖片
在appicon加入圖片

設定app名子

### 設定載入頁面
**法一(推薦)**
加入Launch screen 檔案

設定圖片以及排版

在targets設定

**法二**
```swift
import SwiftUI
@main
struct classroomApp: App {
@State private var isLoading = true
var body: some Scene {
WindowGroup {
if isLoading {
LaunchView()
.onAppear {
// 模擬啟動過程,2秒後進入主畫面
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isLoading = false
}
}
} else {
ContentView()
}
}
}
}
```
```swift
import SwiftUI
struct LaunchView: View {
// 初始位置在螢幕左側外面
@State private var offsetX: CGFloat = -UIScreen.main.bounds.width
var body: some View {
ZStack{
Color.red.opacity(0.5).ignoresSafeArea()
Text("myclassroom")
.font(.largeTitle)
.offset(x: offsetX) // 設定 X 軸偏移量
.onAppear {
withAnimation(.easeOut(duration: 1.0)) {
offsetX = 0 // 飛入到螢幕中央
}
}
}
}
}
#Preview {
LaunchView()
}
```
## 排版練習
```swift
//
// ContentView.swift
// chat
//
// Created by Bruce on 2025/3/28.
//
import SwiftUI
struct ContentView: View {
var myword = Text("helloaaaasd")
var myButton = Button(action:{} , label: {Text("my first button")})
var body: some View {
VStack(alignment: .trailing) {//垂直排列 最多10個 向右對齊
Spacer()
HStack(alignment: .top, spacing: 20){//水平排列 20是間距 向上對齊 很龜毛要按照順序
Rectangle().foregroundColor(.yellow)
.frame(width: 10,height: 100)
myword.blur(radius: /*@START_MENU_TOKEN@*/3.0/*@END_MENU_TOKEN@*/)
myword
}
Spacer()
myword
.rotation3DEffect(Angle(degrees: 50), axis: (x:10 , y:10 , z:20))
Spacer()//這三個spacer會讓他們自己調整間距
Button("first Button"){
}
Button(action: {}){
Text("button")
}
.background(Color.orange)
.cornerRadius(.infinity)
.padding(30)
myButton.background( Image(systemName: "button.programmable"))
}
}
}
#Preview {
ContentView()
}
```

## api範例

```swift
import SwiftUI
import SwiftyJSON
struct ContentView: View {
@State var weather = "無"
@State var minT = "無"
@State var maxT = "無"
@State var rainProb = "無"
var body: some View {
ZStack {
Color.blue
.opacity(0.1)
.ignoresSafeArea()
VStack {
Text("台中市36HR天氣資訊")
.foregroundStyle(.purple)
.font(.system(size: 35, weight: .bold, design: .rounded))
.padding()
Spacer()
.frame(height: 100)
VStack {
Text("天氣狀況:\(weather)")
Text("最低氣溫:\(minT)")
Text("最高氣溫:\(maxT)")
Text("降雨機率:\(rainProb)")
}
.padding()
.font(.title)
Spacer()
.frame(height: 200)
Button("獲取天氣資料"){
getWeatherData()
}
.font(.title2)
.buttonStyle(.borderedProminent)
.padding()
}
}
}
func getWeatherData() {
let apiKey = "CWA-91B244C0-08CD-4F28-9F33-24629198359C"
let urlString = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=\(apiKey)&locationName=臺中市"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
print("無法獲得數據")
return
}
let json = JSON(data)
print(json)
if let locationArray = json["records"]["location"].array,
let firstLocation = locationArray.first {
let weatherElement = firstLocation["weatherElement"].arrayValue
//print(weatherElement)
// weather
let wxElement = weatherElement.first { $0["elementName"].stringValue == "Wx" }
let weather = wxElement?["time"][0]["parameter"]["parameterName"].stringValue ?? "未知"
// minT
let minTElement = weatherElement.first { $0["elementName"].stringValue == "MinT" }
let minT = minTElement?["time"][0]["parameter"]["parameterNmae"].stringValue ?? "未知"
// maxT
let maxTElement = weatherElement.first { $0["elementName"].stringValue == "MaxT" }
let maxT = maxTElement?["time"][0]["parameter"]["parameterName"].stringValue ?? "未知"
// rainProb
let popElement = weatherElement.first { $0["elementName"].stringValue == "PoP" }
let rainProb = popElement?["time"][0]["parameter"]["parameterName"].stringValue ?? "未知"
DispatchQueue.main.async {
self.weather = weather
self.minT = "\(minT)°C"
self.maxT = "\(maxT)°C"
self.rainProb = "\(rainProb)%"
}
}
}.resume()
}
}
#Preview {
ContentView()
}
```
json檔長這樣
```jsonld
{
"success" : "true",
"result" : {
"fields" : [
{
"type" : "String",
"id" : "datasetDescription"
},
{
"type" : "String",
"id" : "locationName"
},
{
"type" : "String",
"id" : "parameterName"
},
{
"type" : "String",
"id" : "parameterValue"
},
{
"type" : "String",
"id" : "parameterUnit"
},
{
"type" : "Timestamp",
"id" : "startTime"
},
{
"type" : "Timestamp",
"id" : "endTime"
}
],
"resource_id" : "F-C0032-001"
},
"records" : {
"location" : [
{
"locationName" : "臺中市",
"weatherElement" : [
{
"time" : [
{
"startTime" : "2025-05-05 12:00:00",
"parameter" : {
"parameterName" : "晴時多雲",
"parameterValue" : "2"
},
"endTime" : "2025-05-05 18:00:00"
},
{
"startTime" : "2025-05-05 18:00:00",
"parameter" : {
"parameterName" : "多雲時陰",
"parameterValue" : "5"
},
"endTime" : "2025-05-06 06:00:00"
},
{
"startTime" : "2025-05-06 06:00:00",
"parameter" : {
"parameterValue" : "22",
"parameterName" : "多雲午後短暫雷陣雨"
},
"endTime" : "2025-05-06 18:00:00"
}
],
"elementName" : "Wx"
},
{
"time" : [
{
"startTime" : "2025-05-05 12:00:00",
"endTime" : "2025-05-05 18:00:00",
"parameter" : {
"parameterName" : "0",
"parameterUnit" : "百分比"
}
},
{
"startTime" : "2025-05-05 18:00:00",
"endTime" : "2025-05-06 06:00:00",
"parameter" : {
"parameterName" : "10",
"parameterUnit" : "百分比"
}
},
{
"startTime" : "2025-05-06 06:00:00",
"endTime" : "2025-05-06 18:00:00",
"parameter" : {
"parameterUnit" : "百分比",
"parameterName" : "30"
}
}
],
"elementName" : "PoP"
},
{
"time" : [
{
"startTime" : "2025-05-05 12:00:00",
"parameter" : {
"parameterName" : "30",
"parameterUnit" : "C"
},
"endTime" : "2025-05-05 18:00:00"
},
{
"startTime" : "2025-05-05 18:00:00",
"parameter" : {
"parameterUnit" : "C",
"parameterName" : "25"
},
"endTime" : "2025-05-06 06:00:00"
},
{
"startTime" : "2025-05-06 06:00:00",
"parameter" : {
"parameterName" : "25",
"parameterUnit" : "C"
},
"endTime" : "2025-05-06 18:00:00"
}
],
"elementName" : "MinT"
},
{
"time" : [
{
"startTime" : "2025-05-05 12:00:00",
"endTime" : "2025-05-05 18:00:00",
"parameter" : {
"parameterName" : "悶熱"
}
},
{
"startTime" : "2025-05-05 18:00:00",
"endTime" : "2025-05-06 06:00:00",
"parameter" : {
"parameterName" : "舒適至悶熱"
}
},
{
"startTime" : "2025-05-06 06:00:00",
"endTime" : "2025-05-06 18:00:00",
"parameter" : {
"parameterName" : "舒適至悶熱"
}
}
],
"elementName" : "CI"
},
{
"time" : [
{
"startTime" : "2025-05-05 12:00:00",
"parameter" : {
"parameterName" : "32",
"parameterUnit" : "C"
},
"endTime" : "2025-05-05 18:00:00"
},
{
"startTime" : "2025-05-05 18:00:00",
"parameter" : {
"parameterUnit" : "C",
"parameterName" : "30"
},
"endTime" : "2025-05-06 06:00:00"
},
{
"startTime" : "2025-05-06 06:00:00",
"parameter" : {
"parameterName" : "31",
"parameterUnit" : "C"
},
"endTime" : "2025-05-06 18:00:00"
}
],
"elementName" : "MaxT"
}
]
}
],
"datasetDescription" : "三十六小時天氣預報"
}
}
```
## MVVM架構
MVVM = Model - View - ViewModel
把「資料結構」與「畫面呈現」徹底分離,讓程式更容易維護、測試與擴充。
| 層級 | 主要職責 | 關鍵技術/常見寫法 |
| ------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Model** | - 保存純粹的業務資料與商業邏輯<br>- 不知道畫面長什麼樣子 | `struct / class`、`Codable`、資料庫實體、網路回傳 DTO |
| **View** | - 直接顯示 UI、接收使用者互動<br>- 只做「渲染」與「事件轉發」 | **SwiftUI**:`View` + `@State`<br>**UIKit**:`UIView / UIViewController` |
| **ViewModel** | - 負責把 Model 轉成 View 需要的格式<br>- 對外提供「可繫結」的狀態<br>- 封裝使用者動作 → 呼叫 Use Case / Service | **Combine**/`@Published`<br>`ObservableObject` / `@StateObject`<br>非同步:`async/await`、Task |
### 資料流示意
1. **View → ViewModel**
使用者點擊/輸入 → ViewModel 的函式(e.g. save(), fetch())
2. ViewModel ↔ Model / Service
讀取或更新資料(可能發 API)
將回傳結果寫入 @Published 屬性
3. View ← ViewModel
@StateObject/ObservedObject 監聽變化,畫面自動重新繪製
這是一條「單向資料流」,能避免狀態錯亂。
```swift
// 1️⃣ Model
struct Todo: Identifiable, Codable {
let id: UUID = .init()
var title: String
var done: Bool
}
// 2️⃣ ViewModel
final class TodoListVM: ObservableObject {
@Published private(set) var todos: [Todo] = []
// 假裝是網路請求
func fetch() async {
// 這裡只是示範,實務上請呼叫 API Service
await Task.sleep(300_000_000) // 0.3 秒
let sample = [Todo(title: "買牛奶", done: false),
Todo(title: "寫 MVVM 筆記", done: true)]
// 在主執行緒更新
await MainActor.run { self.todos = sample }
}
func toggleDone(id: UUID) {
if let i = todos.firstIndex(where: { $0.id == id }) {
todos[i].done.toggle()
}
}
}
// 3️⃣ View
struct TodoListView: View {
@StateObject private var vm = TodoListVM()
var body: some View {
List {
ForEach(vm.todos) { todo in
HStack {
Image(systemName: todo.done ? "checkmark.circle.fill"
: "circle")
.onTapGesture { vm.toggleDone(id: todo.id) }
Text(todo.title)
.strikethrough(todo.done)
}
}
}
.task { await vm.fetch() } // 首次出現時載入
.navigationTitle("待辦清單")
}
}
```
## 共用變數與物件
只有一般變數可以直接初始化賦予值(但沒給就使用會編譯錯誤)
其他像是@binding都要在資料結構內賦值不然就是外部傳入
```swift
struct HelloView: View {
var name: String = "訪客" // ✅ 這樣可以
var body: some View {
Text("哈囉,\(name)")
}
}
```
@State與ObservableObject中的@publish一定要初始化
```swift
struct MyView: View {
@State var count: Int // ❌ Compile error:Missing argument for parameter 'wrappedValue'
var body: some View {
Text("Count: \(count)")
}
}
```
**@StateObject 和 @ObservedObject 和 @EnvironmentObject 一定要搭配 ObservableObject,不然會出錯**
錯誤寫法:
1. 有@StateObject無ObservableObject
```swift
class Counter {
var count = 0
}
struct MyView: View {
@StateObject var counter = Counter() // ❌ 錯誤:'Counter' 沒有 conform 'ObservableObject'
//會編譯錯誤
var body: some View {
Text("\(counter.count)")
}
}
```
2. 無@StateObject有ObservableObject
```swift
class Counter: ObservableObject {
@Published var count = 0
}
struct MyView: View {
let counter = Counter() // ❌ 沒有用 @StateObject / @ObservedObject
var body: some View {
VStack {
Text("數字:\(counter.count)") // 改變不會觸發更新
Button("加一") {
counter.count += 1
//雖然值可以變 但畫面不會刷新
}
}
}
}
```
### @state
swift的class與struct幾乎一樣
差別在於struct的內容不太能改變但class可以
但swiftUI剛好就是struct的結構
所以加上@State才會更新內容並且渲染到swiftUI上面
```swift
struct ContentView: View {
@State var score1 = 1
var score2 = 1
var body: some View {
Button(action:{
self.score1 += 1 //不會報錯
self.score2 += 1 //會報錯
}){
Text("button")
}
}
}
```
實時更新例子
```swift
struct ContentView: View {
@State var score1 = 1.0//一定要打1.0才知道是浮點數打1只會覺得是整數int
var body: some View {
VStack{
Text("\(score1)")
Slider(value: $score1, in: 1...100)//這邊是浮點數
}
}
}
```

### @Binding
```swift
struct ContentView: View {
var body: some View {
VStack {
Text("歡迎來到 Chat App")
ParentView()
}
}
}
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
ToggleView(isOn: $isOn) //把isOn這個變數傳給ToggleView取使用
Text(isOn ? "開啟中" : "關閉中")
}
}
}
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("開關狀態", isOn: $isOn) //借由按鈕去改變狀態
.padding()
}
}
```

### @enviroment
@Environment 讓視圖可以訪問系統環境變量或自定義環境變量。
```swift
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text("當前模式: \(colorScheme == .dark ? "深色" : "淺色")")
.padding()
.background(colorScheme == .dark ? Color.black : Color.white)
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
}
}
```
### @StateObject 與 @ObservedObject
就像是@State與@Binding的關係(只能用在變數字串之類的簡單結構)
只是@StateObject與@ObservedObject是用在class(物件)
```swift
import SwiftUI
import Combine
//Step 1:資料模型
class CounterModel: ObservableObject {
@Published var count = 0
}
//Step 2:父視圖(擁有 model,用 `@StateObject`)
struct ParentView: View {
@StateObject private var model = CounterModel() // ⬅️ 擁有者
var body: some View {
VStack {
Text("👨👧 父視圖 Count: \(model.count)")
Button("🔼 父視圖加一") {
model.count += 1
}
Divider()
ChildView(model2: model) // 傳給子視圖
}
.padding()
}
}
//Step 3:子視圖(使用傳進來的 model,用 `@ObservedObject`)
struct ChildView: View {
@ObservedObject var model2: CounterModel // ⬅️ 使用者 也可以取名叫做model這裡是教學用途所以不這樣改
var body: some View {
VStack {
Text("👶 子視圖 Count: \(model2.count)")
Button("➕ 子視圖加一") {
model2.count += 1
}
}
}
}
```
### @EnvironmentObject
只要用了.environmentObject()之後子圖與子子圖...只要用@EnvironmentObject var settings: AppSettings 就可以共用變數資料並且更改
簡單來講就是@ObservedObject的加強版
**但一定要用ObservableObject物件**
```swift
import SwiftUI
// 根視圖注入環境對象
@main
struct chatApp: App {
@StateObject private var settings = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
//如果不加environemntObject(settings)的話,當程式執行到需要使用 @EnvironmentObject 的視圖時,例如 SettingsView 或 DetailView,它們將無法找到對應的環境物件 (AppSettings),並且會導致 程式崩潰(Crash)。
//但所有子視圖,包括 SettingsView 和 DetailView,都可以使用 @EnvironmentObject 直接存取 AppSettings。
}
}
}
// 定義共享的數據模型
class AppSettings: ObservableObject {
@Published var theme = "默認"
@Published var fontSize = 14
}
// 主視圖
struct ContentView: View {
var body: some View {
NavigationView {
SettingsView()
}
}
}
// 設置視圖 - 不需要傳入 settings
struct SettingsView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
VStack {
Text("當前主題: \(settings.theme)")
Text("字體大小: \(settings.fontSize)")
Button("更改主題") {
settings.theme = "暗黑"
}
Button("增大字體") {
settings.fontSize += 2
}
NavigationLink("前往詳情頁", destination: DetailView())
}
}
}
// 詳情視圖 - 同樣可以訪問環境對象
struct DetailView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Text("詳情頁")
.font(.system(size: CGFloat(settings.fontSize)))
.foregroundColor(settings.theme == "暗黑" ? .white : .black)
.background(settings.theme == "暗黑" ? Color.black : Color.white)
}
}
#Preview {
ContentView()
.environmentObject(AppSettings()) // 注入環境物件 不然preview會崩潰
}
```
**環境物件是否互通**
情況一:真的共用同一個實例(傳進去的) ✅ 會互通!
```swift
let shared = MyModel()
ViewA(model: shared)
ViewB(model: shared)
```
情況二:各自宣告 @StateObject var model = MyModel()(兩個不一樣的實例) ❌ 不會互通!
```swift
struct ViewA: View {
@StateObject var model = MyModel()
var body: some View {
SubviewA().environmentObject(model)
}
}
struct ViewB: View {
@StateObject var model = MyModel()
var body: some View {
SubviewB().environmentObject(model)
}
}
```
**@EnvironmentObject 不能直接是一個陣列(例如 [TodoTask])**
正確做法:用 class 包裝陣列,然後這個 class 遵守 ObservableObject
```swift
import SwiftUI
//定義模型資料 TodoTask
class TodoTask: Identifiable, ObservableObject {
let id = UUID()
@Published var title: String
@Published var isCompleted: Bool
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
//用 class 包裝陣列(這個 class 會被當作 @EnvironmentObject)
class AllTasks: ObservableObject {
@Published var tasks: [TodoTask] = []
}
struct TaskListView: View {
@EnvironmentObject var allTasks: AllTasks
var body: some View {
List {
ForEach(allTasks.tasks) { task in
TaskRowView(task: task)
}
}
}
}
//在畫面中注入 @EnvironmentObject
struct TaskRowView: View {
@ObservedObject var task: TodoTask
var body: some View {
HStack {
Text(task.title)
Spacer()
Button(task.isCompleted ? "✅" : "⭕️") {
task.isCompleted.toggle()
}
}
}
}
//在 App 中注入這個環境物件
@main
struct MyApp: App {
@StateObject var allTasks = AllTasks()
var body: some Scene {
WindowGroup {
TaskListView()
.environmentObject(allTasks)
}
}
}
```
### @Published
用在 @ObservableObject/ @StateObject 類別的屬性上
使用 Combine 框架的通知系統(Publisher)
```swift
import SwiftUI
import Combine
class Counter: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@StateObject var counter = Counter()
var body: some View {
VStack {
Text("目前計數:\(counter.count)")
.font(.largeTitle)
Button("加一") {
counter.count += 1
}
}
}
}
```
### view的刷新機制
| 條件 | SwiftUI 行為 |
| ------------------------------------ | --------------------------- |
| `@State` 改變 | 對應的 `View` 的 `body` **會重繪** |
| `@Binding` 改變 | 來源的 `@State` 改變,View 同樣重繪 |
| `@ObservedObject` 中的 `@Published` 改變 | View 重繪 |
| 普通變數(非 @State)改變 | ❌ 不會觸發重繪 |
**如果 EnvironmentObject 改變,所有 View 都會改變嗎?**
要是有用到同一個EnvironmentObject且是同一個published變數的view才會被刷新
```swift
class AppData: ObservableObject {
@Published var username = "未登入"
@Published var score = 0
}
struct MainView: View {
@EnvironmentObject var data: AppData
var body: some View {
VStack {
HeaderView()
ScoreView()
}
}
}
struct HeaderView: View {
@EnvironmentObject var data: AppData
var body: some View {
Text("使用者:\(data.username)")
}
}
struct ScoreView: View {
@EnvironmentObject var data: AppData
var body: some View {
Text("分數:\(data.score)")
}
}
```
🧪 假設只改 data.score += 1,會怎樣?
* ✅ ScoreView 被更新
* ❌ HeaderView 不會更新(因為 username 沒變)
**小技巧:可以在 View 裡加 print() 來確認哪些 View 被重算:**
```swift
var body: some View {
print("🔄 HeaderView 更新")
return Text("使用者:\(data.username)")
}
```
## 流程等待
@MainActor @async @await要互相搭配使用
### @async
async 是 Swift 中處理非同步程式的關鍵字,讓你可以更清楚地寫出「等待某個操作完成再繼續」的流程。搭配 await 使用,常見於網路請求、讀寫檔案、或任何需要等待結果的操作。
```swift
func fetchUserData() async -> String {
// 模擬網路延遲
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 秒
return "使用者資料"
}
@MainActor
func showData() {
Task {
let result = await fetchUserData()
print("拿到的資料是:\(result)")
}
}
```
### @await
sync 函式時,它不會馬上回傳值,而是「稍後才會有結果」。await 的意思就是:「等 async 函式做完,再繼續往下執行。」
=> await 後面的函式也要是async的function
| 函式 | 要不要 async |
| ------------------------- | ----------------------- |
| 被 await 的函式(例如 fetchName) | ✅ 一定要是 async |
| 呼叫 await 的地方 | ✅ 也必須在 async 環境或 Task 裡 |
#### await 一定要在`Task{...}`或`async`或`.task {...}`裡面
```swift
//情況一
Button("載入") {
Task {
let data = await loadData() //會等待拿到資料
print(data)
}
}
//情況二
func fetchSomething() async {
let result = await getDataFromServer()
print(result)
}
//情況三
Text("載入中")
.task {
let result = await fetchSomething()
print(result)
}
```
#### 需要await的情況
| 情境 | 說明 |
| --------------------------------- | ------------------------------------------------- |
| 🌐 **等伺服器資料** | 最常見情況:例如用 `URLSession` 拿 API 回傳值。 |
| 🕒 **等一段時間(計時器)** | 如 `Task.sleep()`,等待幾秒後再做某事。 |
| 💾 **讀取本地檔案或資料庫** | 有些 async 函式會從本地取出資料(例如 iCloud、CoreData async 支援)。 |
| 📷 **從相機或系統功能取得結果** | 比如從使用者選擇相片、錄音,這些系統會「回傳結果」的流程。 |
| 📲 **使用 Bluetooth、網路狀態更新等系統事件監聽** | 等待某事件觸發後才繼續動作。 |
```swift
func fetchUserProfile() async throws -> String {
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? "解析失敗"
}
Task {
do {
let profile = try await fetchUserProfile() // ⬅️ 這裡要用 await
print("拿到使用者資料:\(profile)")
} catch {
print("錯誤:\(error)")
}
}
```
### @MainActor
**為什麼需要 @MainActor?**
因為 Swift 的 async/await 會自動把任務放到背景執行緒(避免卡住畫面),但 UI 更新只能在主執行緒上做,否則會當機或有異常行為。
await 完一個非同步操作後,要改動 UI(或改變影響 UI 的變數),那段程式就必須在主執行緒執行,要用 @MainActor(或 MainActor.run {})來保證安全。
**需要使用的時機**
| 地點 | 要不要加 `@MainActor` | 原因 |
| --------------------- | ----------------- | --------------------------- |
| SwiftUI `body` 中的程式 | ❌ 不需要 | SwiftUI 自動處理在主執行緒 |
| `Task` 裡直接改 @State | ❌ 不需要 | SwiftUI 懂得幫你處理 |
| 你寫的外部 `async` 函式內改 UI | ✅ 要加 | 否則會出錯或警告「background thread」 |
#### 正常寫法(不用手動加 @MainActor)
```swift
struct ContentView: View {
@State private var name = "尚未載入"
var body: some View {
VStack {
Text(name)
Button("載入") {
Task {
let newName = await fetchName()
name = newName // ✅ SwiftUI 自動處理主執行緒
}
}
}
}
}
```
#### 在把 UI 更新的邏輯抽出來寫成其他 async 函式時(例如你寫了一個 updateUI() 函式,裡面有改 @State 變數),就需要加 @MainActor。
錯誤示範:
```swift
class ViewModel: ObservableObject {
@Published var title = ""
func loadData() async {
let result = await fetchTitleFromServer()
// ⚠️ 這裡如果不是在主執行緒,會造成 UI 異常或閃爍
title = result
}
}
```
**方法一:整個 class 用 @MainActor**
```swift
@MainActor
class ViewModel: ObservableObject {
@Published var title = ""
func loadData() async {
let result = await fetchTitleFromServer()
title = result // ✅ 保證在主執行緒
}
}
```
**方法二:用 MainActor.run**
```swift
class ViewModel: ObservableObject {
@Published var title = ""
func loadData() async {
let result = await fetchTitleFromServer()
await MainActor.run {
self.title = result // ✅ 這裡切回主執行緒
}
}
}
```
## Protocol協定
### 各種協定
#### Swift 語言內建協定(標準庫)
| 類別 | 協定名稱 | 說明 |
| ------- | --------------------------------------------------------------- | -------------------------- |
| 序列處理 | `Sequence`, `IteratorProtocol` | 提供迭代(for-in)能力 |
| 集合類型 | `Collection`, `MutableCollection`, `RangeReplaceableCollection` | 提供索引、修改等功能 |
| 比較相關 | `Equatable`, `Comparable`, `Hashable` | 等於、不等、排序、放入 Set/Dictionary |
| 描述與轉換 | `CustomStringConvertible`, `Codable` | 自訂描述、編碼解碼 |
| 錯誤與資源管理 | `Error`, `Identifiable`, `CaseIterable` | 表示錯誤、唯一辨識、列舉所有 case |
| 操作符相關 | `AdditiveArithmetic`, `Numeric`, `BinaryInteger` | 運算操作支援 |
#### SwiftUI 常見協定
| 協定名稱 | 說明 |
| ------------------ | ------------------------------------ |
| `View` | 所有 SwiftUI 視圖元件都要實作它 |
| `Identifiable` | 提供 `id` 屬性,可用於 `List` 或 `ForEach` |
| `ObservableObject` | 可觀察資料模型 |
| `DynamicProperty` | 系統用來處理像 `@State`、`@ObservedObject` 等 |
| `App` | SwiftUI App 入口點協定 |
| `Scene` | 表示 App 的一個 UI 場景 |
#### Combine 框架常見協定
| 協定名稱 | 說明 |
| ------------- | ------------------------------ |
| `Publisher` | 資料發布者 |
| `Subscriber` | 資料訂閱者 |
| `Subject` | 同時是 `Publisher` 和 `Subscriber` |
| `Cancellable` | 可取消訂閱的物件 |
| `Scheduler` | 控制執行時機與執行緒
### ObservableObject
```swift
📦 Model class (ObservableObject)
└── @Published var count = 0
🔍 View 使用 @ObservedObject 或 @StateObject
└── 當 count 改變時 → 自動更新畫面
```
```swift
import SwiftUI
import Combine
// 被觀察的資料類型
class Counter: ObservableObject {
@Published var value: Int = 0
}
// 畫面
struct CounterView: View {
@StateObject var counter = Counter() // 或 @ObservedObject,取決於生命週期控制
var body: some View {
VStack {
Text("計數值:\(counter.value)")
.font(.largeTitle)
Button("加一") {
counter.value += 1
}
}
}
}
```
@StateObject vs @ObservedObject 差異?
| 屬性 | 建議使用時機 |
| ----------------- | -------------------------- |
| `@StateObject` | 該物件 **由這個 View 建立並擁有** |
| `@ObservedObject` | 該物件 **由其他地方傳入,不由 View 擁有** |
### App View Scene
🔁 整體關係圖:
```
App
└─ Scene(WindowGroup, Settings, 等)
└─ View(Text, VStack, CustomView, 等)
```
**App 協定:應用程式的進入點**
SwiftUI app 的主體
```swift
@main //標記啟動點。
struct MyApp: App {
var body: some Scene {
WindowGroup { //是一種 Scene(視窗群組),代表一個 app 畫面。
ContentView() //是 app 起始畫面。
}
}
}
```
**Scene 協定:App 的畫面場景**
表示 app 的「一個畫面場景」
```swift
//多場景範例:
var body: some Scene {
WindowGroup {
MainView()
}
Settings {
SettingsView()
}
}
```
**View 協定:畫面元件的最基本單位**
SwiftUI 元件都 conform View
定義 var body: some View
```swift
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
```
可以組合多個 View 成新的 View
搭配 @State, @Binding, @ObservedObject 等資料變更驅動更新
**不同scene**
✅ WindowGroup
用於顯示一個或多個視窗(iOS 會是多份畫面、macOS 是多視窗)
```swift
WindowGroup {
ContentView()
}
```
特點:
iOS 中代表一個 app 的主畫面
macOS 上可以產生多視窗
每次啟動一個新的 WindowGroup 會產生新的 View 實例
✅ DocumentGroup
適用於「文件型 app」(如:文字處理、圖檔編輯)— 自動處理檔案開啟/儲存等。
```swift
@main
struct MyDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { file in
ContentView(document: file.$document)
}
}
}
```
特點:
適合 macOS / iPadOS 上支援多檔案
自動整合 SwiftUI 文件系統存取(支援 .fileImporter)
✅ Settings
macOS 專用,用於設定畫面
```swift
Settings {
SettingsView()
}
```
特點:
在 macOS 上會自動出現在 App > 偏好設定 的選單裡
只能有一個 Settings Scene
適合用於控制 app 行為(例如:切換深色模式、設定帳號)
### Codable
```swift
typealias Codable = Encodable & Decodable
```
也就是說,一個型別如果採用了 Codable,就同時具備:
* Encodable: 能夠「轉成」JSON、Plist 等格式(例如上傳給 API)
* Decodable: 能夠「從」JSON、Plist 等格式轉成實體物件(例如從 API 回傳資料轉成模型)
```swift
import Foundation
// 定義符合 Codable 的資料模型
struct User: Codable {
var name: String
var age: Int
}
// 將資料轉成 JSON
let user = User(name: "王小明", age: 28)
if let jsonData = try? JSONEncoder().encode(user),
let jsonString = String(data: jsonData, encoding: .utf8) {
print("轉成 JSON:\(jsonString)")
}
// 將 JSON 轉回 model
let json = """
{
"name": "陳美麗",
"age": 30
}
"""
if let data = json.data(using: .utf8),
let decodedUser = try? JSONDecoder().decode(User.self, from: data) {
print("解碼結果:\(decodedUser.name), \(decodedUser.age)")
}
```
Swift 的 Date 預設不支援直接轉 JSON,要加 dateDecodingStrategy 才能正確處理格式。
如果你的 key 名稱跟 JSON 不一致,可以用 CodingKeys 自訂對應:
```swift
struct Article: Codable {
var title: String
var publishedDate: Date
enum CodingKeys: String, CodingKey {
case title
case publishedDate = "published_date"
}
}
```
### Identifiable
這是一個協定,用來保證你的資料「有唯一 ID」。
在 SwiftUI 的 List 或 ForEach 中,每一筆資料都需要唯一的識別符來做差異化更新。
例如畫面有一個動態清單,當你刪除其中一筆資料,系統需要知道「是哪一筆」被刪除或更新,這就是靠 id。
```swift
import SwiftUI
struct Task: Identifiable {
var id = UUID()
var title: String
}
struct TaskListView: View {
let tasks = [
Task(title: "買牛奶"),
Task(title: "看牙醫"),
Task(title: "練習吉他")
]
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
}
```
**如果沒有 id 怎麼辦?**
可以在 ForEach 明確指定哪個屬性當作 ID:
```swift
ForEach(myArray, id: \.name) { item in
Text(item.name)
}
```
但如果你讓型別 conform Identifiable,就可以省略 id: 這段。
### 同時使用 Identifiable + ObservableObject
```swift
import SwiftUI
import Combine
class TodoTask: ObservableObject, Identifiable {
let id = UUID() // Identifiable 需要的唯一識別符
@Published var title: String
@Published var isCompleted: Bool
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
```
## function
**法一:在Struct內部**
```swift
struct ContentView: View {
var body: some View {
VStack{
}
}
// 在 Struct 內部宣告的函式
func incrementCount() {
//code
}
}
```
**法二:用Extension**
```swift
struct ContentView: View {
var body: some View {
VStack{
}
}
}
// 使用 Extension 定義功能
extension ContentView {
func incrementCount(currentCount: Int) -> Int {
return currentCount + 1
}
}
```
**法三:用新檔案**

## extension
**為 View 新增功能**
```swift
import SwiftUI
// 為 View 新增擴展功能
extension View {
func roundedBackground(color: Color = .blue, cornerRadius: CGFloat = 12) -> some View {
self
.padding()
.background(color)
.cornerRadius(cornerRadius)
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, SwiftUI!")
.roundedBackground(color: .green, cornerRadius: 20)
Text("擴展功能好方便!")
.roundedBackground()
}
}
}
```
**使用 Extension 分離邏輯**
```swift
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("增加") {
count = count.incremented()
}
}
}
}
// 使用 extension 分離邏輯
extension Int {
func incremented() -> Int {
return self + 1
}
}
```
**使用 Extension 提取 View 元件**
```swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
CustomButton(title: "確認", action: {
print("確認按鈕點擊")
})
CustomButton(title: "取消", action: {
print("取消按鈕點擊")
})
}
}
}
// 使用 Extension 提取重複元件
extension View {
func CustomButton(title: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(title)
.bold()
.frame(width: 100, height: 40)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
```
**為系統類別新增功能**
```swift
import SwiftUI
// 擴展 String 型別
extension String {
func toInt() -> Int? {
return Int(self)
}
}
struct ContentView: View {
var body: some View {
Text("123".toInt() != nil ? "轉換成功" : "轉換失敗")
}
}
```
**使用 Protocol 和 Extension 結合**
```swift
import SwiftUI
// 定義一個協定
protocol CustomizableView {
func customText() -> String
}
// 提供預設實作
extension CustomizableView {
func customText() -> String {
return "這是預設文字"
}
}
// 套用協定並使用預設實作
struct ContentView: View, CustomizableView {
var body: some View {
Text(customText())
}
}
```
## closure
```swift
import SwiftUI
struct ContentView: View {
let numbers = [1, 2, 3, 4, 5]
var squaredNumbers: [Int] {
numbers.map { $0 * $0 }
}
var body: some View {
VStack {
ForEach(squaredNumbers, id: \.self) { number in
Text("平方數:\(number)")
}
}
}
}
```

## print在console的方法
**法一:在按鈕動作中使用 print()**
```swift
struct ContentView: View {
var body: some View {
VStack {
Text("歡迎來到 Chat App")
Button("點我") {
print("hi")
}
ParentView()
}
}
}
```
**法二:在 onAppear() 中使用 print()**
在畫面顯示時執行 print()
```swift
struct ContentView: View {
var body: some View {
VStack {
Text("歡迎來到 Chat App")
ParentView()
}
.onAppear {
print("hi")
}
}
}
```
**法三:使用 init() 中的 print()**
在視圖初始化時執行 print()
```swift
struct ContentView: View {
init() {
print("hi")
}
var body: some View {
VStack {
Text("歡迎來到 Chat App")
ParentView()
}
}
}
```
## VStack ZStack HStask
**VStack:垂直堆疊容器**
垂直排列的
```swift
VStack(alignment: .leading, spacing: 12) {
Text("今日待辦")
.font(.title2).bold()
Text("• 手寫筆記\n• 完成專案報告")
}
.padding()
.background(.yellow.opacity(0.2))
````
**HStack:水平堆疊容器**
由左到右
```swift
HStack(spacing: 20) {
Image(systemName: "heart.fill")
Text("按讚")
Spacer() // 推到最右
Text("99+")
}
.padding()
.background(.pink.opacity(0.2))
```
**ZStack:疊層容器**
後放的視圖會蓋住先放的
越後面 放在越上面
```swift
ZStack(alignment: .bottomTrailing) {
Image("travel_photo")
.resizable()
.aspectRatio(contentMode: .fill)
Text("Kobe • 2025")
.font(.caption)
.padding(6)
.background(.black.opacity(0.6))
.foregroundColor(.white)
.clipShape(Capsule())
.padding(8)
}
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
```
如果你想要改變 View 的上下順序,只需要調整在 ZStack 裡的宣告順序。另外,zIndex(_:) 也可以明確指定層級(預設是 0,數字越大越上面):
```swift
ZStack {
Text("底層文字")
.zIndex(0)
Text("上層文字")
.zIndex(1)
}
```
## 修飾符解釋
```swift
.lineLimit(1) //確保標題只顯示一行
.truncationMode(.tail)//用來在文字超出寬度時在末尾顯示"..."
.frame(maxWidth: .infinity, alignment: .leading)
//1.讓文字容器佔據所有可用的寬度(maxWidth: .infinity)
//2.將文字靠左對齊(alignment: .leading)
Text(.init(text)) //會自動啟用 Markdown 渲染
.textSelection(.enabled) //讓用戶可以選擇和複製文字
Spacer().frame(height: 40) //控制Spacere高度為40
.padding(.vertical, 10) //控制垂直間距
.toggle() //ㄥ是 Swift 中專門給 布林值 (Bool) 使用的一個方法,用來「切換」它的值,true 變 false,false 變 true。
var flag = true
flag.toggle()
// 現在 flag == false
//監聽某個值的變化
.onChange(of: someValue) { newValue in
// 當 someValue 改變時執行這裡的程式
}
.padding() //讓物件邊邊往外擴 不然邊界會貼到物件
.task //是一種「當這個 View 出現時要做的非同步工作」。
ZStack{
//code...
}
.background( // 🔧 把背景與陰影與顏色包在一起,陰影只會畫在這塊背景矩形
Color.hex(hex: "F3DCC8")
backgroundColor
.shadow(color: .black.opacity(0.09), radius: 10, x: 0, y: -2) // ⬅︎ 只有這裡加陰影
)
```


