SwiftUI教程(五)使用List創(chuàng)建列表應(yīng)用程序

SwiftUI教程系列文章匯總

通過(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)容:

  1. 填充列表
  2. 導(dǎo)航
  3. 編輯列表
  4. 生成新的項(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ō)明:

  1. .navigationBarItems(trailing: EditButton())給list增加編輯按鈕
  2. onMove提供了索引參數(shù)fromOffsets和toOffset參數(shù),從開(kāi)始位置移動(dòng)到新位置
  3. 直接將數(shù)組todos進(jìn)行move操作,傳入位置參數(shù)即可
  4. 只有當(dāng)用戶(hù)輸入“Edit”模式時(shí)才能移動(dòng)項(xiàng)目。因此,我們需要在導(dǎo)航欄中添加一個(gè)EditButton按鈕,當(dāng)用戶(hù)點(diǎn)擊“編輯”時(shí),就可以繼續(xù)移動(dòng)了
  5. 另外,在Edit模式下,每個(gè)項(xiàng)目都顯示一個(gè)Delete按鈕,用戶(hù)可以通過(guò)點(diǎn)擊它快速刪除項(xiàng)目。這是在編輯模式下自動(dòng)啟用,我們不需要添加任何代碼!

結(jié)果:

結(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ō)明:

  1. 在按鈕的動(dòng)作中,我們調(diào)用showAddTodoView的toggle()來(lái)進(jìn)行切換
  2. 要顯示sheet,我們使用sheet修飾符,并將其附加到按鈕上。
  3. 我們將showAddTodoView狀態(tài)變量綁定到.sheet()修飾符的參數(shù)。
  4. 當(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ō)明:

  1. 通過(guò)將showAddTodoView聲明為@Binding,我們就聲明了它的值將來(lái)自其他地方,并將被共享到AddTodoView和其他地方。
  2. 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ō)明:

  1. 數(shù)組categoryTypes將存儲(chǔ)我們將要定義的類(lèi)別,這些類(lèi)別會(huì)顯示在pickerview中。
  2. 狀態(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ō)明:

  1. Picker視圖允許用戶(hù)從一組已定義的項(xiàng)中選擇某一項(xiàng)。
  2. ForEach循環(huán)遍歷categoryTypes數(shù)組和選擇器。
  3. 注意,我們?cè)谶x擇器視圖的末尾添加了一個(gè)修飾符. pickerstyle (SegmentedPickerStyle())。
  4. 這實(shí)際上將我們的Picker視圖顯示為SegmentedControl。
  5. 注意如果我們不寫(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é)果:

運(yùn)行結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容