希紋的部落格

APP範例:優雅筆記(ElegantNote)

新建專案

  1. 開啟 Xcode → File → New → Project
  2. 選擇 iOS → App,點擊 Next
  3. 填入:
    • Product Name: ElegantNotes
    • Interface: SwiftUI(重要!)
    • Language: Swift
    • Storage: SwiftData(如果沒有此選項,先選 None,之後手動加)
  4. 選擇儲存位置,完成建立

💡 選 SwiftData 後,Xcode 會自動幫你建立基本的 ModelContainer 設定。不過我們會自己手動控制,所以之後會修改 App 進入點。

建立資料夾結構

在專案目錄(Package Navigator)右鍵 → New Group,建立三個資料夾:Models、Views、Utilities。然後把以下檔案分別放入:

功能介紹

ElegantNotes 是一個帶有莫蘭迪粉彩配色的筆記 App,主要功能包括:

檔案結構

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 驗證:

  1. 建立 Xcode 專案(SwiftUI + SwiftData)
  2. 建立 Models/Note.swift — 確認編譯通過
  3. 建立 Models/Tag.swift — 確認編譯通過
  4. 建立 Utilities/Color+Hex.swift — 加入 Color 擴充
  5. 建立 Utilities/PreviewContainer.swift — 準備假資料
  6. 建立 Views/NoteCardView.swift — 在 Preview 確認卡片外觀
  7. 建立 Views/TagSelectionView.swift — 在 Preview 確認標籤 sheet
  8. 建立 Views/NoteEditorView.swift — 在 Preview 確認編輯頁
  9. 建立 Views/NotesGridView.swift — 在 Preview 確認主畫面
  10. 修改 ElegantNotesApp.swift — 完成 App 進入點與預載資料
  11. 在模擬器執行,測試完整流程

常見問題排查

問題 可能原因 解決方式
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好像也不是不行。

#swift