上一章都在是講解新增商品頁面的 Layout,本章會談論到新增商品的功能。我們會談論到提示窗 (Alert) 和導覽列 (Navigation Bar) 的使用。另外,我們還會介紹另一個 Property Wrapper 叫 @Environment。
SwiftUI Coffee Shop
新增商品功能
下面的程式碼中,我們省略了一些部分,並新增我們這章要教學的。
struct AddProductView: View {
@State var name: String = ""
@State var price: String = ""
@State var isFailed = false
private var onAddAction: ((Item) -> Void)?
var body: some View {
VStack {
...
Button(action: {
let isSuccessful = self.addProduct()
self.isFailed = !isSuccessful
}, label: { Text("Add") })
...
.alert(isPresented: $isFailed) {
Alert(title: Text("Input data is incorrect"), dismissButton: .default(Text("OK")))
}
...
}
}
private func addProduct() -> Bool {
guard !name.isEmpty else {
return false
}
guard let parsedPrice = Double(price) else {
return false
}
let item = Item(name: name, price: parsedPrice)
onAddAction?(item)
return true
}
}
extension AddProductView {
func onAdd(perform action: ((Item) -> Void)?) -> Self {
var copy = self
copy.onAddAction = action
return copy
}
}可以看到 Button 被按下後,馬上就呼叫 addProduct()。它會檢查欄位是否有都有輸入值。沒有的話則回傳 false,有的話則新建 Item。然後,呼叫 onAddAction() 回調 (Callback),並回傳 true。
onAddAction Callback 會是由 ProductListView 呼叫 onAdd() 傳過來的。在 onAdd() 中,有先對自己做個備份,再將 action 設給 onAddAction。為何不直接設定呢?而是要先拷貝再設定呢?因為 struct 內的函式是不可以改變自己內部的 Property。將 self 設給 copy 後,copy 和 self 就是不同的實體。在 Swift 中,struct 是傳值而不是傳址。
Alert
isFailed 被宣告為 State。當 addProduct() 回傳 false,就會把 isFailed 設為 true,.alert() 就會被觸發。
Alert 會顯示提示窗。參數 title: 是提示窗的標題,而參數 dismissButton: 是按鈕上的字。所以 Alert 會顯示輸入資料不正確。

導覽列 (Navigation Bar)
本章至目前為止我們都還單獨在 AddProductView 上開發。現在我們來將 ProductListView 和 AddProductView 串連起來。讓我們來為 Coffee Shop App 加上導覽列 (Navigation Bar) 的功能吧。
在 SwiftUI 裡加上 Navigation Bar 是非常簡單。我們回到最上層的 View,也就是 ContentView。在 ContentView 中,我們只要用 NavigationView 將 ProductListView 包起來就可以了。因為是加在最上層,所以現在整個 App 都有 Navigation Bar 的功能。
struct ContentView: View {
var body: some View {
NavigationView {
ProductListView()
}
}
}既然已經為 App 加上 Navigation Bar 的功能。那我們來為 ProductListView 加上導覽列標題 (Navigation Bar Title)。在 ProductListView 裡面的最外層的 List 上,我們呼叫 .navigationBarTitle() 就可以了。
在 Navigation Bar 的左右兩邊,呼叫 .navigationBarItems() 可以加上按鈕。trailing: 表示是要加在右邊。按鈕上的 UI 是 Image(systemName: "plus"),這是個+號。iOS 有提供一些系統預設的 Image。用這些 Image 的名稱就可以取用。可以在這邊找到這些預先安裝的 Image 列表。
然後我們希望點擊這個+號後,可以導覽到 AddProductView。要實現這功能,我們只要將這個+號用 NavigationLink 包起來即可。參數 destination: 是指要導覽的目的地,所以這裡填入 AddProductView。
還記得上面我們有替 AddProductView 加上 onAdd() Callback 嗎?這是當新增商品時,AddProductView 將新增的商品,透過 onAdd() 傳回來給 ProductListView。然後 ProductListView 就可以將新增的商品加入到商品列表中。
struct ProductListView: View {
var body: some View {
List {
...
}
.navigationBarTitle("Product List")
.navigationBarItems(trailing: NavigationLink(destination: AddProductView().onAdd(perform: onAdd)) {
Image(systemName: "plus")
})
}
private func onAdd(_ item: Item) {
products.append(item)
}
}現在你可以從 ProductListView 導覽到 AddProductView。但是還沒有辦法返回。讓我們回到 AddProductView。
首先我們先為 AddProductView 加上 Navigation Bar Title。
Property presentationMode 被宣告為 Environment。@Environment 是讓我們可以存取一些預先定義好的 View’s Environment 變數。\.presentationMode 就是其中一個預先定義好的 Key。我們只要在 Button 的動作裡面,呼叫 presentationMode.dismiss() 就可以導覽回上一頁。
struct AddProductView: View {
@Environment(\.presentationMode) var presentationMode
...
var body: some View {
VStack {
...
Button(action: {
let isSuccessful = self.addProduct()
self.isFailed = !isSuccessful
if isSuccessful {
self.presentationMode.wrappedValue.dismiss()
}
}, label: { Text("Add") })
...
...
}
.navigationBarTitle("Add a product")
}
}新增商品功能終於完成了!

結語
本章節介紹了 Navigation Bar 的使用。SwiftUI 使得 Navigation Bar 非常易於使用。至目前為止,我們已經介紹了很多 UI 元件的使用,而且它們的使用方式都非常的相似。如果你有熟悉這些使用方式的話,之後當你遇到新的 UI 時,一定會可以很容易的就上手。









