通過(guò)構(gòu)建經(jīng)典的待辦事項(xiàng)應(yīng)用程序來(lái)學(xué)習(xí)List、NavigationView的使用。實(shí)現(xiàn)動(dòng)態(tài)填充List、編輯List、添加Item、界面導(dǎo)航功能。
主要內(nèi)容:
- 填充列表
- 導(dǎo)航
- 編輯列表
- 生成新的項(xiàng)
1. 填充列表
1.1 創(chuàng)建列表
要擁有一個(gè)顯示待辦事項(xiàng)列表的List視圖,請(qǐng)?jiān)贑ontentView中的代碼輸入以下命令:
var body: some View {
List{
HStack{ Image("work").resizable().frame(width: 50, height: 50)
Text("Write SwiftUI book")
}
HStack{ Image("personal").resizable().frame(width: 50, height: 50)
Text("Read Bible")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Bring kids out to play")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Fetch wife")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Call mum")
}
}
}
說(shuō)明:
- 通過(guò)List添加多行數(shù)據(jù),
- 每一行包含一個(gè)圖像和一個(gè)水平文本,通過(guò)HStack來(lái)包裝
- 因?yàn)閳D像大小不同,大的圖像會(huì)被擴(kuò)展,除了屏幕大小,只顯示了一部分。為了解決這個(gè)問(wèn)題,我們應(yīng)用. resizable修改器使圖像適合于使用面積。
- 然后應(yīng)用.frame修飾符將圖像的大小限制為一個(gè)自定義的框架。
1.2 動(dòng)態(tài)添加列表
目前,我們有一個(gè)靜態(tài)列表視圖,其中有5個(gè)固定的數(shù)據(jù)。現(xiàn)在讓我們看看如何從一個(gè)數(shù)組拿出值填充每一行。
因?yàn)楝F(xiàn)在每一行都包含一個(gè)類(lèi)別和一個(gè)文本,我們需要?jiǎng)?chuàng)建一個(gè)結(jié)構(gòu)體來(lái)存儲(chǔ)它們。
結(jié)構(gòu)體允許我們創(chuàng)建多個(gè)值組成的復(fù)雜數(shù)據(jù)類(lèi)型。然后,我們可以創(chuàng)建該結(jié)構(gòu)的實(shí)例并填充值,在代碼中中傳遞這些值。
1.2.1 定義一個(gè)Todo結(jié)構(gòu)類(lèi)型
struct Todo {
let name: String
let category: String
}
說(shuō)明:
- 我們定義了一個(gè)Todo結(jié)構(gòu)類(lèi)型,它包含兩個(gè)屬性:名稱(chēng)和類(lèi)別。
1.2.2 todos狀態(tài)數(shù)組:
在ContentView.swift中聲明Todo結(jié)構(gòu)體的狀態(tài)數(shù)組
@State private var todos = [
Todo(name:"Write SwiftUI book",category: "work"),
Todo(name:"Read Bible",category: "personal"),
Todo(name:"Bring kids out to play",category: "family"),
Todo(name:"Fetch wife",category: "family"),
Todo(name:"family",category: "Call mum")
]
說(shuō)明:
- 我們使用一個(gè)狀態(tài)變量todos數(shù)組,以便List視圖中的項(xiàng)可以動(dòng)態(tài)更新。
- 這里創(chuàng)建好每一項(xiàng)數(shù)據(jù)的數(shù)組,之后通過(guò)數(shù)組動(dòng)態(tài)更新。
- 注意Swift是如何讓創(chuàng)建Todo結(jié)構(gòu)體的實(shí)例變得簡(jiǎn)單的。我們只需傳入名稱(chēng)和類(lèi)別的初始值。
1.2.3 List動(dòng)態(tài)顯示數(shù)組數(shù)據(jù)
var body: some View {
List{
ForEach(todos, id:\.name){ (todo) in
HStack{
Image(todo.category) .resizable().frame(width: 50, height: 50)
Text(todo.name)
}
}
}
}
說(shuō)明:
- 在List視圖中,我們使用ForEach,它接收一個(gè)數(shù)組,然后創(chuàng)建多個(gè)子視圖。
- 我們必須提供id來(lái)唯一標(biāo)識(shí)每一項(xiàng)?,F(xiàn)在,我們提供.name來(lái)使用todo名稱(chēng)作為每一行的標(biāo)識(shí)符。
- 當(dāng)預(yù)覽應(yīng)用程序時(shí),它顯示的內(nèi)容應(yīng)該是要和以前一樣的,只是這一次,將以編程方式填充行。
1.2.4 ID標(biāo)識(shí)
上面是通過(guò)itme的名稱(chēng)來(lái)標(biāo)識(shí)todo項(xiàng)的,現(xiàn)在,如果我們有多個(gè)名稱(chēng)相同的todo項(xiàng),該怎么辦?
如果有多個(gè)名稱(chēng)相同的待辦事項(xiàng),這將導(dǎo)致當(dāng)我們?cè)噲D刪除行,列表視圖會(huì)因?yàn)橛卸鄠€(gè)相同名稱(chēng)的cell而不知道的要?jiǎng)h除哪一行。為了解決這個(gè)問(wèn)題,我們將添加一個(gè)唯一的標(biāo)識(shí)符到Todo結(jié)構(gòu)體。
struct Todo: Identifiable {
let id = UUID()
let name: String
let category: String
}
- 使用id來(lái)唯一標(biāo)識(shí)一個(gè)item,這里使用UUID()函數(shù)來(lái)生成一個(gè)隨機(jī)標(biāo)識(shí)符
- 此時(shí)我們就是不需要手動(dòng)設(shè)置id了。List視圖將會(huì)自動(dòng)使用id作為每行的標(biāo)識(shí)符
2. 導(dǎo)航
接下來(lái),我們將實(shí)現(xiàn)一個(gè)待辦事項(xiàng)詳細(xì)信息界面。即當(dāng)用戶(hù)點(diǎn)擊一個(gè)待辦事項(xiàng),我們會(huì)跳轉(zhuǎn)到一個(gè)單獨(dú)的待辦事項(xiàng)詳細(xì)信息界面。
我們通過(guò)將我們的List包裝在一個(gè)NavigationView中來(lái)實(shí)現(xiàn)這一點(diǎn)。
2.1 導(dǎo)航頁(yè)面
代碼:
var body: some View {
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
…
}
}.navigationBarTitle("Todo Items")
}
}
說(shuō)明:
- 使用navigationBarTitle方法給控件設(shè)置導(dǎo)航欄的標(biāo)題
- 注意navigationBarTitle修飾符屬于列表視圖,而不是導(dǎo)航視圖。
- 這是因?yàn)閷?dǎo)航視圖從右邊通過(guò)push來(lái)顯示新界面
- 每個(gè)界面都有自己的標(biāo)題。如果標(biāo)題是附加到導(dǎo)航視圖,標(biāo)題就被固定了。
- 通過(guò)附加的標(biāo)題到導(dǎo)航視圖的里面內(nèi)容,標(biāo)題可以更改為其內(nèi)容的變化。
2.2 導(dǎo)航跳轉(zhuǎn)
通過(guò)NavigationView包裝的List視圖允許我們?cè)邳c(diǎn)擊一行時(shí)導(dǎo)航到待辦事項(xiàng)界面上。
我們可以通過(guò)給NavigationLink函數(shù)包裝row item來(lái)導(dǎo)航到詳情界面:
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
NavigationLink(destination:
VStack{
Text(todo.name)
Image(todo.category)
.resizable()
.frame(width: 200, height: 200)
}
){
HStack{
…
}
}
}
}.navigationBarTitle("Todo Items")
}
說(shuō)明:
- 注意,我們必須向NavigationLink提供一個(gè)參數(shù)destination,也就是點(diǎn)擊項(xiàng)目時(shí)顯示的視圖。
- 這里代碼中可以看到,視圖將包括:Text和Image
- 當(dāng)運(yùn)行應(yīng)用程序,點(diǎn)擊一個(gè)item就會(huì)跳轉(zhuǎn)到另一個(gè)界面,界面顯示選擇的項(xiàng)目的詳細(xì)信息。
- 新界面的頂部欄也會(huì)顯示帶有上一個(gè)項(xiàng)目的符號(hào)
3. 編輯列表
3.1 刪除項(xiàng)
在iOS中,要?jiǎng)h除特定的行,我們通常向左滑動(dòng)到顯示一行上顯示的Delete按鈕。
要啟用此功能,我們需要在控件末尾添加.onDelete()修飾符
.onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加給單個(gè)item的
說(shuō)明:
- onDelete提供了一個(gè)indexSet參數(shù),它包含了ForEach視圖中的項(xiàng)的位置。供我們查找刪除項(xiàng)
- 我們將indexSet傳遞給todos的remove函數(shù)來(lái)刪除特定的行。
3.2 重新安排行
控件允許用戶(hù)重新排列List視圖中的行,在ForEach視圖末尾設(shè)置. onmove()修飾符。
NavigationView{
List{
...
}
.onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加給單個(gè)item的
.onMove(perform: { indices, newOffset in
todos.move(fromOffsets: indices, toOffset: newOffset)
})
}
.navigationBarTitle("Todo Items")
.navigationBarItems(trailing: EditButton())
}
說(shuō)明:
- .navigationBarItems(trailing: EditButton())給list增加編輯按鈕
- onMove提供了索引參數(shù)fromOffsets和toOffset參數(shù),從開(kāi)始位置移動(dòng)到新位置
- 直接將數(shù)組todos進(jìn)行move操作,傳入位置參數(shù)即可
- 只有當(dāng)用戶(hù)輸入“Edit”模式時(shí)才能移動(dòng)項(xiàng)目。因此,我們需要在導(dǎo)航欄中添加一個(gè)EditButton按鈕,當(dāng)用戶(hù)點(diǎn)擊“編輯”時(shí),就可以繼續(xù)移動(dòng)了
- 另外,在Edit模式下,每個(gè)項(xiàng)目都顯示一個(gè)Delete按鈕,用戶(hù)可以通過(guò)點(diǎn)擊它快速刪除項(xiàng)目。這是在編輯模式下自動(dòng)啟用,我們不需要添加任何代碼!
結(jié)果:

4. 生成新的項(xiàng)
4.1 添加Add項(xiàng)
為了向List視圖添加行,我們向todos數(shù)組中添加了一個(gè)新的todo項(xiàng)。
.navigationBarItems(
leading: Button(action: addTodo,
label: {
Text("Add")
}),
trailing: EditButton()
)
- 我們將在NavigationView左上角添加一個(gè)導(dǎo)航欄按鈕項(xiàng),用于添加待辦事項(xiàng):
- 我們必須在按鈕的動(dòng)作中指定一個(gè)函數(shù)來(lái)添加一個(gè)新的待辦事項(xiàng)對(duì)待辦事項(xiàng)。
- 實(shí)現(xiàn)addTodo函數(shù)用于添加item
4.2 創(chuàng)建AddTodoView
我們當(dāng)前的新待辦事項(xiàng)的名稱(chēng)和類(lèi)別硬編碼為" newTodo "和" work ",實(shí)際開(kāi)發(fā)中是不會(huì)這樣的,
因此,現(xiàn)在注釋掉addTodo()函數(shù),專(zhuān)門(mén)創(chuàng)建AddTodoView,之后通過(guò)ContentView來(lái)呈現(xiàn)的AddTodoView,供用戶(hù)添加待辦事項(xiàng),這樣就可以由用戶(hù)來(lái)自定義添加愛(ài)辦事項(xiàng)的具體內(nèi)容。
struct AddTodoView: View {
var body: some View {
Text("Add Todo view")
}
}
- 設(shè)置有一個(gè)狀態(tài)變量showAddTodoView來(lái)決定是否顯示AddTodoView。它最初默認(rèn)為false(不顯示)。
@State private var showAddTodoView = false
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView()
},
trailing: EditButton()
)
說(shuō)明:
- 在按鈕的動(dòng)作中,我們調(diào)用showAddTodoView的toggle()來(lái)進(jìn)行切換
- 要顯示sheet,我們使用sheet修飾符,并將其附加到按鈕上。
- 我們將showAddTodoView狀態(tài)變量綁定到.sheet()修飾符的參數(shù)。
- 當(dāng)如果showAddTodoView為true,則顯示sheet
- 用戶(hù)可以向下拖動(dòng)工作表來(lái)關(guān)閉它。
- 但在我們的案例中,我們希望有兩個(gè)文本字段和一個(gè)Add按鈕,
- 當(dāng)單擊按鈕時(shí),以編程方式退出界面。
4.3 @Binding
我們首先添加一個(gè)Button視圖和一個(gè)綁定到AddTodoView中的綁定變量showAddTodoView
代碼:
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView(showAddTodoView: self.$showAddTodoView )
},
trailing: EditButton()
)
struct AddTodoView: View {
@Binding var showAddTodoView: Bool
var body: some View {
Text("Add Todo view")
Button(action: {
self.showAddTodoView = false
},
label: {
Text("Done")
})
}
}
說(shuō)明:
- 通過(guò)將showAddTodoView聲明為@Binding,我們就聲明了它的值將來(lái)自其他地方,并將被共享到AddTodoView和其他地方。
- ContentView和AddTodoView共享showAddTodoView值。當(dāng)修改“AddTodoView”中的“showAddTodoView”時(shí),則變化也會(huì)反射回ContentView,然后它會(huì)解散sheet。
4.4 添加用戶(hù)輸入框
接下來(lái),我們將為T(mén)odo名稱(chēng)添加一個(gè)TextField,為用戶(hù)添加一個(gè)Picker用來(lái)選擇一個(gè)待辦事項(xiàng)類(lèi)別(“工作”、“家庭”、“個(gè)人”),還有一個(gè)“完成”按鈕。
@State private var name: String = ""
@State private var selectedCategory = 0
var categoryTypes = ["family","personal","work"]
說(shuō)明:
- 數(shù)組categoryTypes將存儲(chǔ)我們將要定義的類(lèi)別,這些類(lèi)別會(huì)顯示在pickerview中。
- 狀態(tài)變量selectedCategory將存儲(chǔ)所選類(lèi)別的數(shù)組索引。
添加一個(gè)VStack,其中包含輸入控件TextFiled
VStack{
Text("Add Todo").font(.largeTitle)
TextField("To Do name",text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.black).padding()
Text("Select Category")
Picker("",selection: $selectedCategory){
ForEach(0 ..< categoryTypes.count){
Text(self.categoryTypes[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
說(shuō)明:
- Picker視圖允許用戶(hù)從一組已定義的項(xiàng)中選擇某一項(xiàng)。
- ForEach循環(huán)遍歷categoryTypes數(shù)組和選擇器。
- 注意,我們?cè)谶x擇器視圖的末尾添加了一個(gè)修飾符. pickerstyle (SegmentedPickerStyle())。
- 這實(shí)際上將我們的Picker視圖顯示為SegmentedControl。
- 注意如果我們不寫(xiě)這個(gè)修飾符會(huì)發(fā)生什么
4.5 將Todo項(xiàng)添加到Todos數(shù)組
我們有接受用戶(hù)輸入的字段,讓我們實(shí)現(xiàn)添加待辦事項(xiàng)到待辦事項(xiàng)數(shù)組。要做到這一點(diǎn),AddTodoView需要能夠訪問(wèn)待辦事項(xiàng)ContentView。
Button(action: {
self.showAddTodoView = false
todos.append(Todo(name: name, category: categoryTypes[selectedCategory]))
},
label: {
Text("Done")
})
- 在AddTodo視圖中,我們通過(guò)@Binding接收todo:
- 記住@Binding允許我們?cè)L問(wèn)ContentView中的待辦事項(xiàng)
- 在Button的操作中,我們創(chuàng)建一個(gè)Todo,然后將其添加到待辦事項(xiàng):
注意:
- 注意,當(dāng)使用append()時(shí),新項(xiàng)會(huì)被添加到末尾,這意味著新的待辦事項(xiàng)顯示在最后一行。
- 這里還能看到數(shù)據(jù)源綁定的操作,SwiftUI的理念是“真正的簡(jiǎn)單數(shù)據(jù)來(lái)源”。也就是說(shuō),視圖背后的數(shù)據(jù)只有一個(gè)源。
- 在我們的例子中,todos只有一個(gè)來(lái)源ContentView。AddTodoView中的待辦事項(xiàng)指ContentView中的待辦事項(xiàng)
- 通過(guò)@Binding關(guān)鍵字。使用@Binding允許我們進(jìn)行綁定之間的數(shù)據(jù)視圖。這可以防止同一文件的多個(gè)或多個(gè)副本導(dǎo)致數(shù)據(jù)不一致的數(shù)據(jù)。
總結(jié)
整體代碼實(shí)現(xiàn):
struct Todo {
let name: String
let category: String
}
struct ContentView: View {
@State private var todos = [
Todo(name:"Write SwiftUI book",category: "work"),
Todo(name:"Read Bible",category: "personal"),
Todo(name:"Bring kids out to play",category: "family"),
Todo(name:"Fetch wife",category: "family"),
Todo(name:"family",category: "family")
]
@State private var showAddTodoView = false
func addTodo(){
todos.append(Todo(name: "newTodo", category: "work"))
}
var body: some View {
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
NavigationLink(destination:
VStack{
Text(todo.name)
Image(todo.category)
.resizable()
.frame(width: 200, height: 200)
}
){
HStack{
Image(todo.category) .resizable().frame(width: 50, height: 50)
Text(todo.name)
}
}
}
.onDelete(perform: { indexSet in
todos.remove(atOffsets: indexSet)
})//加給單個(gè)item的
.onMove(perform: { indices, newOffset in
todos.move(fromOffsets: indices, toOffset: newOffset)
})//設(shè)置位置可移動(dòng)
}
.navigationBarTitle("Todo Items")
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView(showAddTodoView: self.$showAddTodoView, todos:self.$todos)
},
trailing: EditButton()
)
}
}
}
struct AddTodoView: View {
@Binding var showAddTodoView: Bool
@State private var name: String = ""
@State private var selectedCategory = 0
var categoryTypes = ["family","personal","work"]
@Binding var todos: [Todo]
var body: some View {
// Text("Add Todo view")
VStack{
Text("Add Todo").font(.largeTitle)
TextField("To Do name",text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.black).padding()
Text("Select Category")
Picker("",selection: $selectedCategory){
ForEach(0 ..< categoryTypes.count){
Text(self.categoryTypes[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
Button(action: {
self.showAddTodoView = false
todos.append(Todo(name: name, category: categoryTypes[selectedCategory]))
},
label: {
Text("Done")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewInterfaceOrientation(.portraitUpsideDown)
}
}
}
運(yùn)行結(jié)果:
