SwiftUI教程(五)使用List创建列表应用程序

SwiftUI教程系列文章汇总

通过构建经典的待办事项应用程序来学习List、NavigationView的使用。实现动态填充List、编辑List、添加Item、界面导航功能。

主要内容:

  1. 填充列表
  2. 导航
  3. 编辑列表
  4. 生成新的项

1. 填充列表

1.1 创建列表

要拥有一个显示待办事项列表的List视图,请在ContentView中的代码输入以下命令:

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")
        }
    }
}

说明:

  • 通过List添加多行数据,
  • 每一行包含一个图像和一个水平文本,通过HStack来包装
  • 因为图像大小不同,大的图像会被扩展,除了屏幕大小,只显示了一部分。为了解决这个问题,我们应用. resizable修改器使图像适合于使用面积。
  • 然后应用.frame修饰符将图像的大小限制为一个自定义的框架。

1.2 动态添加列表

目前,我们有一个静态列表视图,其中有5个固定的数据。现在让我们看看如何从一个数组拿出值填充每一行。
因为现在每一行都包含一个类别和一个文本,我们需要创建一个结构体来存储它们。
结构体允许我们创建多个值组成的复杂数据类型。然后,我们可以创建该结构的实例并填充值,在代码中中传递这些值。

1.2.1 定义一个Todo结构类型

struct Todo { 
    let name: String 
    let category: String 
}

说明:

  • 我们定义了一个Todo结构类型,它包含两个属性:名称和类别。

1.2.2 todos状态数组:

在ContentView.swift中声明Todo结构体的状态数组

@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") 
]

说明:

  • 我们使用一个状态变量todos数组,以便List视图中的项可以动态更新。
  • 这里创建好每一项数据的数组,之后通过数组动态更新。
  • 注意Swift是如何让创建Todo结构体的实例变得简单的。我们只需传入名称和类别的初始值。

1.2.3 List动态显示数组数据

var body: some View { 
    List{ 
        ForEach(todos, id:\.name){ (todo) in 
            HStack{ 
                Image(todo.category) .resizable().frame(width: 50, height: 50)
                Text(todo.name) 
            } 
        } 
    } 
}

说明:

  • 在List视图中,我们使用ForEach,它接收一个数组,然后创建多个子视图。
  • 我们必须提供id来唯一标识每一项。现在,我们提供.name来使用todo名称作为每一行的标识符。
  • 当预览应用程序时,它显示的内容应该是要和以前一样的,只是这一次,将以编程方式填充行。

1.2.4 ID标识

上面是通过itme的名称来标识todo项的,现在,如果我们有多个名称相同的todo项,该怎么办?
如果有多个名称相同的待办事项,这将导致当我们试图删除行,列表视图会因为有多个相同名称的cell而不知道的要删除哪一行。为了解决这个问题,我们将添加一个唯一的标识符到Todo结构体。

struct Todo: Identifiable { 
    let id = UUID() 
    let name: String 
    let category: String 
}
  • 使用id来唯一标识一个item,这里使用UUID()函数来生成一个随机标识符
  • 此时我们就是不需要手动设置id了。List视图将会自动使用id作为每行的标识符

2. 导航

接下来,我们将实现一个待办事项详细信息界面。即当用户点击一个待办事项,我们会跳转到一个单独的待办事项详细信息界面。
我们通过将我们的List包装在一个NavigationView中来实现这一点。

2.1 导航页面

代码:

var body: some View { 
    NavigationView{ 
        List{
            ForEach(todos, id:\.name){ (todo) in 
                … 
            } 
        }.navigationBarTitle("Todo Items") 
    } 
}

说明:

  • 使用navigationBarTitle方法给控件设置导航栏的标题
  • 注意navigationBarTitle修饰符属于列表视图,而不是导航视图。
  • 这是因为导航视图从右边通过push来显示新界面
  • 每个界面都有自己的标题。如果标题是附加到导航视图,标题就被固定了。
  • 通过附加的标题到导航视图的里面内容,标题可以更改为其内容的变化。

2.2 导航跳转

通过NavigationView包装的List视图允许我们在点击一行时导航到待办事项界面上。
我们可以通过给NavigationLink函数包装row item来导航到详情界面:

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") 
 }

说明:

  • 注意,我们必须向NavigationLink提供一个参数destination,也就是点击项目时显示的视图。
  • 这里代码中可以看到,视图将包括:Text和Image
  • 当运行应用程序,点击一个item就会跳转到另一个界面,界面显示选择的项目的详细信息。
  • 新界面的顶部栏也会显示带有上一个项目的符号

3. 编辑列表

3.1 删除项

在iOS中,要删除特定的行,我们通常向左滑动到显示一行上显示的Delete按钮。

要启用此功能,我们需要在控件末尾添加.onDelete()修饰符

.onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加给单个item的

说明:

  • onDelete提供了一个indexSet参数,它包含了ForEach视图中的项的位置。供我们查找删除项
  • 我们将indexSet传递给todos的remove函数来删除特定的行。

3.2 重新安排行

控件允许用户重新排列List视图中的行,在ForEach视图末尾设置. onmove()修饰符。

NavigationView{
    List{
        ...
        }
        .onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加给单个item的
        .onMove(perform: { indices, newOffset in 
        todos.move(fromOffsets: indices, toOffset: newOffset) 
        })
    }
    .navigationBarTitle("Todo Items")
    .navigationBarItems(trailing: EditButton())
}

说明:

  1. .navigationBarItems(trailing: EditButton())给list增加编辑按钮
  2. onMove提供了索引参数fromOffsets和toOffset参数,从开始位置移动到新位置
  3. 直接将数组todos进行move操作,传入位置参数即可
  4. 只有当用户输入“Edit”模式时才能移动项目。因此,我们需要在导航栏中添加一个EditButton按钮,当用户点击“编辑”时,就可以继续移动了
  5. 另外,在Edit模式下,每个项目都显示一个Delete按钮,用户可以通过点击它快速删除项目。这是在编辑模式下自动启用,我们不需要添加任何代码!

结果:

结果

4. 生成新的项

4.1 添加Add项

为了向List视图添加行,我们向todos数组中添加了一个新的todo项。

.navigationBarItems(
    leading: Button(action: addTodo,
    label: {
        Text("Add")
    }),
    trailing: EditButton()
)
  • 我们将在NavigationView左上角添加一个导航栏按钮项,用于添加待办事项:
  • 我们必须在按钮的动作中指定一个函数来添加一个新的待办事项对待办事项。
  • 实现addTodo函数用于添加item

4.2 创建AddTodoView

我们当前的新待办事项的名称和类别硬编码为" newTodo "和" work ",实际开发中是不会这样的,

因此,现在注释掉addTodo()函数,专门创建AddTodoView,之后通过ContentView来呈现的AddTodoView,供用户添加待办事项,这样就可以由用户来自定义添加爱办事项的具体内容。

struct AddTodoView: View {
    var body: some View {
        Text("Add Todo view")
    }
}
  • 设置有一个状态变量showAddTodoView来决定是否显示AddTodoView。它最初默认为false(不显示)。
@State private var showAddTodoView = false
.navigationBarItems(
    leading: Button(action: {
        self.showAddTodoView.toggle()
    },
    label: {
        Text("Add")
    }).sheet(isPresented: $showAddTodoView){
        AddTodoView()
    },
    trailing: EditButton()
)

说明:

  1. 在按钮的动作中,我们调用showAddTodoView的toggle()来进行切换
  2. 要显示sheet,我们使用sheet修饰符,并将其附加到按钮上。
  3. 我们将showAddTodoView状态变量绑定到.sheet()修饰符的参数。
  4. 当如果showAddTodoView为true,则显示sheet
  • 用户可以向下拖动工作表来关闭它。
  • 但在我们的案例中,我们希望有两个文本字段和一个Add按钮,
  • 当单击按钮时,以编程方式退出界面。

4.3 @Binding

我们首先添加一个Button视图和一个绑定到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")
        })
    }
}

说明:

  1. 通过将showAddTodoView声明为@Binding,我们就声明了它的值将来自其他地方,并将被共享到AddTodoView和其他地方。
  2. ContentView和AddTodoView共享showAddTodoView值。当修改“AddTodoView”中的“showAddTodoView”时,则变化也会反射回ContentView,然后它会解散sheet。

4.4 添加用户输入框

接下来,我们将为Todo名称添加一个TextField,为用户添加一个Picker用来选择一个待办事项类别(“工作”、“家庭”、“个人”),还有一个“完成”按钮。

@State private var name: String = ""
@State private var selectedCategory = 0
var categoryTypes = ["family","personal","work"]

说明:

  1. 数组categoryTypes将存储我们将要定义的类别,这些类别会显示在pickerview中。
  2. 状态变量selectedCategory将存储所选类别的数组索引。

添加一个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()

说明:

  1. Picker视图允许用户从一组已定义的项中选择某一项。
  2. ForEach循环遍历categoryTypes数组和选择器。
  3. 注意,我们在选择器视图的末尾添加了一个修饰符. pickerstyle (SegmentedPickerStyle())。
  4. 这实际上将我们的Picker视图显示为SegmentedControl。
  5. 注意如果我们不写这个修饰符会发生什么

4.5 将Todo项添加到Todos数组

我们有接受用户输入的字段,让我们实现添加待办事项到待办事项数组。要做到这一点,AddTodoView需要能够访问待办事项ContentView。

Button(action: {
    self.showAddTodoView = false
    todos.append(Todo(name: name, category: categoryTypes[selectedCategory]))
},
label: {
    Text("Done")
})
  • 在AddTodo视图中,我们通过@Binding接收todo:
  • 记住@Binding允许我们访问ContentView中的待办事项
  • 在Button的操作中,我们创建一个Todo,然后将其添加到待办事项:

注意:

  • 注意,当使用append()时,新项会被添加到末尾,这意味着新的待办事项显示在最后一行。
  • 这里还能看到数据源绑定的操作,SwiftUI的理念是“真正的简单数据来源”。也就是说,视图背后的数据只有一个源。
  • 在我们的例子中,todos只有一个来源ContentView。AddTodoView中的待办事项指ContentView中的待办事项
  • 通过@Binding关键字。使用@Binding允许我们进行绑定之间的数据视图。这可以防止同一文件的多个或多个副本导致数据不一致的数据。

总结

整体代码实现:

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)
                })//加给单个item的
                .onMove(perform: { indices, newOffset in
                    todos.move(fromOffsets: indices, toOffset: newOffset)
                })//设置位置可移动
            }
            .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)
        }
    }
}

运行结果:

运行结果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容