APP範例:優雅筆記(ElegantNote)
新建專案
- 開啟 Xcode → File → New → Project
- 選擇 iOS → App,點擊 Next
- 填入:
- Product Name: ElegantNotes
- Interface: SwiftUI(重要!)
- Language: Swift
- Storage: SwiftData(如果沒有此選項,先選 None,之後手動加)
- 選擇儲存位置,完成建立
💡 選 SwiftData 後,Xcode 會自動幫你建立基本的 ModelContainer 設定。不過我們會自己手動控制,所以之後會修改 App 進入點。
建立資料夾結構
在專案目錄(Package Navigator)右鍵 → New Group,建立三個資料夾:Models、Views、Utilities。然後把以下檔案分別放入:
- Models 資料夾:Note.swift、Tag.swift
- Views 資料夾:NotesGridView.swift、NoteCardView.swift、NoteEditorView.swift、TagSelectionView.swift
- Utilities 資料夾:Color+Hex.swift、PreviewContainer.swift
功能介紹
ElegantNotes 是一個帶有莫蘭迪粉彩配色的筆記 App,主要功能包括:
- 雙欄網格顯示所有筆記(NoteCardView)
- 點擊卡片進入編輯頁(NoteEditorView),可編輯標題、內文、背景顏色
- 底部工具列有色票選擇器,即時更換筆記背景色
- 標籤系統:為每篇筆記加上彩色標籤,並在主畫面篩選
- 搜尋功能:即時搜尋標題或內文
- 長按筆記卡片顯示 Context Menu,可刪除筆記
檔案結構
ElegantNotes/
├── ElegantNotesApp.swift ← App 進入點,設定 SwiftData 容器
├── Models/
│ ├── Note.swift ← 筆記資料模型 (@Model)
│ └── Tag.swift ← 標籤資料模型 (@Model)
├── Views/
│ ├── NotesGridView.swift ← 主畫面:雙欄網格 + 搜尋 + 標籤篩選
│ ├── NoteCardView.swift ← 單張筆記卡片元件
│ ├── NoteEditorView.swift ← 筆記編輯頁
│ └── TagSelectionView.swift ← 標籤管理 Sheet
└── Utilities/
├── Color+Hex.swift ← Color extension,支援 #RRGGBB 字串
└── PreviewContainer.swift ← Xcode Preview 用的假資料容器
建構順序
建議按照以下順序從零建立,每一步都可以在 Xcode Preview 驗證:
- 建立 Xcode 專案(SwiftUI + SwiftData)
- 建立 Models/Note.swift — 確認編譯通過
- 建立 Models/Tag.swift — 確認編譯通過
- 建立 Utilities/Color+Hex.swift — 加入 Color 擴充
- 建立 Utilities/PreviewContainer.swift — 準備假資料
- 建立 Views/NoteCardView.swift — 在 Preview 確認卡片外觀
- 建立 Views/TagSelectionView.swift — 在 Preview 確認標籤 sheet
- 建立 Views/NoteEditorView.swift — 在 Preview 確認編輯頁
- 建立 Views/NotesGridView.swift — 在 Preview 確認主畫面
- 修改 ElegantNotesApp.swift — 完成 App 進入點與預載資料
- 在模擬器執行,測試完整流程
常見問題排查
| 問題 | 可能原因 | 解決方式 |
|---|---|---|
| Preview 無法預覽 | 沒有 .modelContainer() | 確認 #Preview 有加 .modelContainer(PreviewContainer.shared) |
| 資料無法儲存 | 沒有呼叫 context.save() | 確認每次修改後有 try? modelContext.save() |
| App 閃退(fatalError) | ModelContainer 初始化失敗 | 確認 Schema 包含所有 @Model class |
| @Bindable 無法用 $ | Note 不是 @Model class | 確認 Note 有 @Model 標記 |
| 搜尋不即時更新 | filteredNotes 是計算屬性 | 正常,計算屬性會在 @State 改變時重算 |
| navigationDestination 沒反應 | path 型別不符 | 確認 .navigationDestination(for: Note.self) 的型別 |
資料模型(SwiftData)
資料模型關係
Note 和 Tag 是多對多(Many-to-Many)關係:一篇筆記可以有多個標籤,一個標籤也可以屬於多篇筆記。
| 模型 | 主要屬性 | 說明 |
|---|---|---|
| Note | id, title, content, colorHex, updatedAt, tags | 一篇筆記,colorHex 存莫蘭迪背景色 |
| Tag | id, name, colorHex, notes | 一個標籤,colorHex 存標籤顯示顏色 |
SwiftUI 常用速查表
Property Wrappers
| Wrapper | 用途 | 使用場景 |
|---|---|---|
| @State | View 內部的局部狀態 | searchText、isShowingSheet 等 UI 狀態 |
| @Binding | 父子 View 共用狀態(雙向) | 傳入子 View 讓子 View 可以修改 |
| @Bindable | SwiftData @Model 物件的屬性綁定 | NoteEditorView 中的 note |
| @Environment | 從環境取得注入的值 | modelContext、dismiss、colorScheme |
| @Query | 從 SwiftData 查詢並訂閱 | 所有需要顯示資料庫資料的 View |
| @StateObject | 擁有 ObservableObject 的生命週期 | View 自己建立的 ViewModel |
| @ObservedObject | 觀察外部傳入的 ObservableObject | 由外部建立的 ViewModel |
常用 Layout 元件
| 元件 | UIKit 對應 | 特性 |
|---|---|---|
| VStack | UIStackView(.vertical) | 垂直排列 |
| HStack | UIStackView(.horizontal) | 水平排列 |
| ZStack | addSubview | 層疊 Z 軸層疊,最後寫的在最上面 |
| LazyVStack | UITableView (自動) | 延遲載入,用在 ScrollView 內 |
| LazyVGrid | UICollectionView | 網格布局 |
| List | UITableView | 有內建分隔線與滑動刪除 |
| ScrollView | UIScrollView | 可橫向或縱向滾動 |
| Spacer | 無直接對應 | 彈性空間,把元素推開 |
| GeometryReader | frame/bounds | 讀取父容器尺寸 |
常用 Modifiers
| Modifier | 效果 |
|---|---|
| .font(.headline) | 設定字型 |
| .foregroundColor(.primary) | 設定前景色(文字、圖示) |
| .background(Color.blue) | 設定背景色 |
| .padding(16) | 設定內邊距 |
| .frame(width:height:) | 設定尺寸 |
| .cornerRadius(12) | 設定圓角 |
| .shadow(radius:x:y:) | 設定陰影 |
| .overlay { } | 疊加另一個 View |
| .ignoresSafeArea() | 延伸到安全區外(例如背景色) |
| .onAppear { } | View 出現時執行 |
| .onDisappear { } | View 消失時執行 |
| .onTapGesture { } | 點擊手勢 |
| .sheet(isPresented:) { } | 彈出 Sheet |
| .navigationTitle("標題") | 設定導航列標題 |
| .searchable(text:) | 加入搜尋列 |
| .contextMenu { } | 長按選單 |
| .swipeActions { } | List cell 的滑動動作 |
| .animation(.spring(), value:) | 當 value 改變時執行動畫 |
| .withAnimation { } | 在 block 內的狀態改變加上動畫 |
SwiftData 快速參考
// 1. 定義 Model
@Model class MyModel { var name: String; init(name: String) { self.name = name } }
// 2. 建立容器(在 App 進入點)
ModelContainer(for: MyModel.self)
// 3. 查詢(在 View 中)
@Query var items: [MyModel]
@Query(sort: \MyModel.name, order: .forward) var items: [MyModel]
// 4. 插入
modelContext.insert(MyModel(name: "test"))
try? modelContext.save()
// 5. 刪除
modelContext.delete(item)
try? modelContext.save()
// 6. 更新(直接修改屬性即可,@Model 自動追蹤)
item.name = "new name"
try? modelContext.save()
TextField vs TextEditor
| 元件 | UIKit 對應 | 特性 |
|---|---|---|
| TextField | UITextField | 單行輸入,可設定 submitLabel |
| TextEditor | UITextView | 多行輸入,需 .scrollContentBackground(.hidden) 才能改背景色 |
總結
太久沒做這個都忘了原本的講義省略很多,是靠著範例程式碼對照著看,詳細的程式碼暫時先不提供,只靠這些交給AI生一個能用的APP好像也不是不行。