---
# System prepended metadata

title: swift
tags: [Coding]

---


# swift

## 加入第三方庫
![截圖 2025-02-27 下午6.36.50](https://hackmd.io/_uploads/HJuybTa9Jx.png)
按右下角add package
![截圖 2025-02-27 下午6.37.15](https://hackmd.io/_uploads/HkF1bpp91g.png)
按add package
![截圖 2025-02-27 下午6.37.27](https://hackmd.io/_uploads/BktJZaac1e.png)
這點很重要 要到Frameworks,Libraries,and Embedded Content按\+號
![截圖 2025-02-27 下午6.37.42](https://hackmd.io/_uploads/H1cJZ6pcye.png)
把Alamofire add進去
![截圖 2025-02-27 下午6.37.55](https://hackmd.io/_uploads/ryt1bpTqJl.png)

## 語法

## 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錯誤
![image](https://hackmd.io/_uploads/H1W0npPaJl.png)
```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
```
之後跳出這個代表成功
![截圖 2025-04-17 晚上11.48.48](https://hackmd.io/_uploads/Hy14XjC0yl.png)
![截圖 2025-04-17 下午4.33.49](https://hackmd.io/_uploads/r19WTNCRye.png)
到這裡把這個貼上
```json
{
  "mcpServers": {
    "TalkToFigma": {
      "command": "bun",
      "args": [
        "/path/to/cursor-talk-to-figma-mcp/src/talk_to_figma_mcp/server.ts"
         //這個要改成自己下載的server.ts位置
      ]
    }
  }
}
```
![image](https://hackmd.io/_uploads/BJP5di1kle.png)
啟動server
```bash
bun run src/socket.ts
```
到figma專案
![image](https://hackmd.io/_uploads/BJDWFoy1ex.png)
![截圖 2025-04-18 下午6.28.19](https://hackmd.io/_uploads/SkCdKjyJel.png)
![截圖 2025-04-18 下午6.28.47](https://hackmd.io/_uploads/SyRdYo1Jll.png)
成功連線
![image](https://hackmd.io/_uploads/SkCiKsJkll.png)
然後這是channel ID
![截圖 2025-04-18 晚上8.24.21](https://hackmd.io/_uploads/H1S5ETJkeg.png)

有package.json代表要安裝套件
![image](https://hackmd.io/_uploads/HJfnIWp1le.png)
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
![截圖 2025-04-17 下午3.52.17](https://hackmd.io/_uploads/rJbkvNCRkg.png)

![截圖 2025-04-17 下午4.33.49](https://hackmd.io/_uploads/r19WTNCRye.png)
![截圖 2025-04-17 下午4.35.15](https://hackmd.io/_uploads/SyLv6ER0Je.png)
把mcp代碼複製過來
![image](https://hackmd.io/_uploads/rkWF6VARke.png)

到figma中的settings

![image](https://hackmd.io/_uploads/SJ64_BC0yx.png)
![截圖 2025-04-17 下午5.19.55](https://hackmd.io/_uploads/ByuW_rRRJx.png =400x)
產生一個apiKey（記得權限都要開）
key再貼到到json
![截圖 2025-04-17 下午5.28.01](https://hackmd.io/_uploads/HJwCFrCRyg.png)


接著到網站下載node.js https://nodejs.org/
![image](https://hackmd.io/_uploads/rkGSwSCCkx.png)
 
 運行伺服器(測試mcp伺服器可不可以用)
 ```
npx -y figma-developer-mcp --figma-api-key=自己的key --stdio
```
按重新載入就可以了
![截圖 2025-04-17 下午6.21.09](https://hackmd.io/_uploads/rkELILACkx.png)

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

官方prompt:
Try Figma's AI to automatically generate names


![image](https://hackmd.io/_uploads/S1BBsr001e.png)


# 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
```
![image](https://hackmd.io/_uploads/rJrnq7ARyg.png =500x)
### 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圖片與載入頁面
### 加入圖片

![截圖 2025-03-31 下午2.21.59](https://hackmd.io/_uploads/H15iVhDp1g.png =400x)

![截圖 2025-03-31 下午2.19.42](https://hackmd.io/_uploads/ByFwE2v6kg.png =400x)

![截圖 2025-03-31 下午2.19.29](https://hackmd.io/_uploads/rkmTE2w61x.png =600x)

### 設定app封面與名子圖片
在appicon加入圖片
![截圖 2025-05-12 下午3.50.45](https://hackmd.io/_uploads/HJiY_7J-xx.png)
設定app名子
![截圖 2025-05-12 下午3.52.24](https://hackmd.io/_uploads/BJpA_71-xe.png)


### 設定載入頁面
**法一（推薦）**
加入Launch screen 檔案
![截圖 2025-05-12 下午3.40.22](https://hackmd.io/_uploads/SyolPQkZgx.png)
設定圖片以及排版
![截圖 2025-05-12 下午3.46.21](https://hackmd.io/_uploads/H1C9P71Zlx.png)
在targets設定
![截圖 2025-05-12 下午3.48.32](https://hackmd.io/_uploads/HyoQ_QkZxl.png)

**法二**
```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()
}
```
![image](https://hackmd.io/_uploads/HJWuwWE6Je.png =300x)

## api範例
![image](https://hackmd.io/_uploads/HJhery8ggg.png =300x)

```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)//這邊是浮點數
        }
    }
}
```
![image](https://hackmd.io/_uploads/HkSYXfETkg.png)

### @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()
    }
}
```
![image](https://hackmd.io/_uploads/HJadhQ_T1g.png =300x)

### @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
    }
}
```
**法三：用新檔案**
![截圖 2025-03-31 下午2.38.05](https://hackmd.io/_uploads/SkDjO3vTkg.png)

## 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)")
            }
        }
    }
}
```
![image](https://hackmd.io/_uploads/B1DuLQupye.png =300x)

## 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) // ⬅︎ 只有這裡加陰影
)
```
![image](https://hackmd.io/_uploads/H1I9JELggg.png)

![image](https://hackmd.io/_uploads/Bk7GgEIxlx.png)

![image](https://hackmd.io/_uploads/S1Y4e4Igeg.png)


