# 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)