一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
今日职言:外表干净是尊重别人,内心干净是尊重自己,言行干净是尊重灵魂。
承接上一章的内容,我们继续完成使用CoreData
框架搭建一个简单的ToDo
待办事项App
。
这一章节,我们完成一下NewToDoView
新建事项页面。
我们先新建一个新页面,命名为NewToDoView
。
点击Xcode
顶部导航栏,File
文件,New
新建,选择File
创建文件,选择iOS
中的SwiftUI File
类型的文件,命名为NewToDoView.swift
。
页面UI设计
我们还是从上往下构建UI
页面。
TopNavBar顶部导航栏视图
首先是TopNavBar
顶部导航栏,名称不能和之前创建的重复,它由一个Text
标题一个closeButton
关闭按钮组成。
// 顶部导航栏
struct TopNavBar: View {
var body: some View {
HStack {
Text("新建事项")
.font(.system(.title))
.bold()
Spacer()
Button(action: {
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
.font(.title)
}
}
}
}
InputNameView输入框视图
然后是事项名称的输入框TextField
。
TextField
输入框需要有两个参数绑定,一个是内容绑定,即我们TextField
输入框需要记录什么内容。第二个是isEditing
输入状态绑定,帮助我们检测它是否正在输入,后面我们会用到输入的状态的检测。
我们在NewToDoView
视图中,使用@State
声明两个变量。
@State var name: String
@State var isEditing = false
然后我们再构建InputNameView
输入框视图的内容,再绑定参数。
//输入框
struct InputNameView: View {
@Binding var name: String
@Binding var isEditing: Bool
var body: some View {
TextField("请输入", text: $name, onEditingChanged: { (editingChanged) in
self.isEditing = editingChanged
})
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.bottom)
}
}
最后在NewToDoView
视图中展示InputNameView
输入框视图的内容,这里用VStack
垂直排布将InputNameView
输入框视图和TopNavBar
顶部导航栏排在一起。
VStack {
TopNavBar()
InputNameView(name: $name, isEditing: $isEditing)
}
由于我们NewToDoView
视图需要预览,因此要想在模拟器中看到效果,还需要在NewToDoView_Previews
预览视图中添加参数。
运行一下,我们看下效果。
下面我们继续,接下来是事项的优先级选择,我们先完成UI
的部分。
PrioritySelectView优先级选择视图
我们命名一个PrioritySelectView
优先级选择视图,这里当然也可以用代码整合的方式减少下代码量,我们将相同的修饰符抽离出来,然后再在PrioritySelectView
优先级选择视图展示内容。
// 选择优先级
struct PrioritySelectView: View {
var body: some View {
HStack {
PrioritySelectRow(name: "高", color: Color.red)
PrioritySelectRow(name: "中", color: Color.orange)
PrioritySelectRow(name: "低", color: Color.green)
}
}
}
// 选择优先级
struct PrioritySelectRow: View {
var name: String
var color:Color
var body: some View {
Text(name)
.frame(width: 80)
.font(.system(.headline))
.padding(10)
.background(color)
.foregroundColor(.white)
.cornerRadius(8)
}
}
我们把PrioritySelectView
加到NewToDoView
视图中看下效果。
VStack {
TopNavBar()
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView()
}
SaveButton保存按钮视图
接下来是SaveButton
保存按钮的绘制,我们让按钮下面留点底边距。
我们也加进去NewToDoView
视图看看效果。
// 保存按钮
struct SaveButton: View {
var body: some View {
Button(action: {
}) {
Text("保存")
.font(.system(.headline))
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding([.top,.bottom])
}
}
NewToDoView页面位置调整
然后,我们调整下位置,我们希望这个NewToDoView
页面是从底部弹出来,然后内容也都在底部展示而不是居中,我们可以调整下整个NewToDoView
页面的位置。
我们用VStack
和Spacer
将NewToDoView
视图顶到底部,然后根据InputNameView
输入框是否处于输入状态isEditing
,来进行偏移,也就是当我们点击InputNameView
输入框正在输入的时候,整个视图可以向上移动,这样我们的keyboard
键盘输入就有位置正对应了,这是一个小技巧
。
然后整个NewToDoView
页面页面我们再置底一点,使用.edgesIgnoringSafeArea
安全区域空出底部区域,这样好看很多。
VStack {
Spacer()
VStack {
TopNavBar()
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView()
SaveButton()
}
.padding()
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isEditing ? -320 : 0)
}.edgesIgnoringSafeArea(.bottom)
交互逻辑设计
好了,我们完成了NewToDoView
页面的绘制了,下面是逻辑部分。
PrioritySelectView优先级选择逻辑
首先是我们的PrioritySelectView
优先级的选择,我们希望点击选择哪个优先级,哪个优先级就“亮起”
,这样我们好知道选中
的是哪一个。
同样,我们需要储存priority
优先级状态,priority
优先级是存储在NewToDoView
新增事项页面里的,这里用@State
状态。
//NewToDoView视图中定义
@State var priority: Priority
然后,我们完善下PrioritySelectView
优先级的选择页面,根据选中状态展示背景颜色,如果没选中,我们就变成.systemGray4
灰色。
// 选择优先级
struct PrioritySelectView: View {
@Binding var priority: Priority
var body: some View {
HStack {
PrioritySelectRow(name: "高", color: priority == .high ? Color.red : Color(.systemGray4))
.onTapGesture { self.priority = .high }
PrioritySelectRow(name: "中", color: priority == .normal ? Color.orange : Color(.systemGray4))
.onTapGesture { self.priority = .normal }
PrioritySelectRow(name: "低", color: priority == .low ? Color.green : Color(.systemGray4))
.onTapGesture { self.priority = .low }
}
}
}
我们完善下NewToDoView
视图的绑定关系,顺便给个示例数据
预览下模拟器结果。
struct NewToDoView: View {
@State var name: String
@State var isEditing = false
@State var priority: Priority
var body: some View {
VStack {
Spacer()
VStack {
TopNavBar()
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView(priority: $priority)
SaveButton()
}
.padding()
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isEditing ? -320 : 0)
}.edgesIgnoringSafeArea(.bottom)
}
}
struct NewToDoView_Previews: PreviewProvider {
static var previews: some View {
NewToDoView(name: "", todoItems: .constant([]), priority: .normal)
}
}
页面弹出逻辑
让我们回到ContentView
首页,我们将两个页面联动
起来。
页面弹出的交互逻辑是,当我们点击ContentView
首页右上角的添加按钮时,打开NewToDoView
新增事项页面。
明白了逻辑之后,我们现在ContentView
首页写逻辑,先声明一个变量showNewTask
,表示我们是否打开了NewToDoView
新增事项页面,默认是false
。
@State private var showNewTask = false
@State private var offset: CGFloat = .zero //使用.animation防止报错,iOS15的特性
如果showNewTask
状态为true
时,我们显示NewToDoView
新增事项页面,我们可以把NewToDoView
新增事项页面放在ContentView
首页的ZStack
包裹着。
//点击添加时打开弹窗
if showNewTask {
NewToDoView(name: "", priority: .normal)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0),value: offset)
}
然后我们增加点击事件,当我们在ContentView
首页点击添加按钮的时候,showNewTask
状态变为为true
。
// 顶部导航栏
struct TopBarMenu: View {
@Binding var showNewTask: Bool
var body: some View {
HStack {
Text("待办事项")
.font(.system(size: 40, weight: .black))
Spacer()
Button(action: {
//打开弹窗
self.showNewTask = true
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle).foregroundColor(.blue)
}
}
.padding()
}
}
//ContentView视图
TopBarMenu(showNewTask: $showNewTask)
好像基本完成了效果,但由于我们是使用ZStack
包裹的方式,而不是用ModelView
模态弹窗或者 NavigationView
导航栏进入新的页面,所以我们还需要做一个MaskView
蒙层遮住背景,让它看起来像是弹窗
的效果。
MaskView蒙层逻辑
//蒙层
struct MaskView : View {
var bgColor: Color
var body: some View {
VStack {
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(bgColor)
.edgesIgnoringSafeArea(.all)
}
}
然后把MaskView
蒙层加到打开NewToDoView
新增事项页面的逻辑里,同时,支持我们点击MaskView
蒙层关闭弹窗。
//蒙层
MaskView(bgColor: .black)
.opacity(0.5)
.onTapGesture {
self.showNewTask = false
}
好!我们实现了怎么弹出NewToDoView
新增事项页面,我们回到NewToDoView.swift
文件,我们实现如何点击关闭弹窗。
页面关闭逻辑
在NewToDoView
新增事项页面关闭有两种
,一种是点击关闭按钮
关闭弹窗。
// 顶部导航栏
struct TopNavBar: View {
@Binding var showNewTask: Bool
var body: some View {
HStack {
Text("新建事项")
.font(.system(.title))
.bold()
Spacer()
Button(action: {
//关闭弹窗
self.showNewTask = false
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
.font(.title)
}
}
}
}
//NewToDoView视图
struct NewToDoView: View {
@State var name: String
@State var isEditing = false
@State var priority: Priority
@Binding var showNewTask: Bool
var body: some View {
VStack {
Spacer()
VStack {
TopNavBar(showNewTask: $showNewTask)
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView(priority: $priority)
SaveButton()
}
.padding()
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isEditing ? -320 : 0)
}.edgesIgnoringSafeArea(.bottom)
}
}
我们发现系统报错
了,这是因为我们使用@Binding
绑定了是否展示页面showNewTask
的布尔值,还需要在ContentView首页
建立关联。
//ContentView视图
NewToDoView(name: "", priority: .normal, showNewTask: $showNewTask)
这样,我们就完成
了第一种关闭弹窗的交互:点击关闭按钮
关闭弹窗。
另一种关闭弹窗的交互是,我们新建一个事项,满足条件后(内容不为空),这是我们点击saveButton
保存按钮,关闭弹窗
。
我们再回到NewToDoView.swift
文件。首先我们保存要校验下InputNameView
输入框内容是否为空
,为空
的时候我们不关闭
弹窗。当InputNameView
输入框内容不为空
的时候,我们才允许关闭弹窗
。
// 保存按钮
struct SaveButton: View {
@Binding var name:String
@Binding var showNewTask: Bool
var body: some View {
Button(action: {
//判断输入框是否为空
if self.name.trimmingCharacters(in: .whitespaces) == "" {
return
}
//关闭弹窗
self.showNewTask = false
}) {
Text("保存")
.font(.system(.headline))
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding([.top,.bottom])
}
}
//NewToDoView视图
SaveButton(name: $name, showNewTask: $showNewTask)
我们回到ContentView.swift
文件中,运行模拟器体验下。
我们完成了基础的关闭弹窗操作,可以点击关闭按钮关闭,也可以输入新建事项后,点击保存关闭弹窗。
添加新事项逻辑
我们在NewToDoView
添加完事项后,输入的内容
和选择的优先级
就会在ContentView
首页List
列表中创建一条数据,下面我们来完成添加新事项逻辑。
我们看回NewToDoView.swift
文件,我们实现了有输入内容时,点击保存按钮关闭弹窗,但没有实现addTask
新增数据,下面我们来实现它。
// 保存按钮
struct SaveButton: View {
@Binding var name:String
@Binding var showNewTask: Bool
@Binding var todoItems: [ToDoItem]
@Binding var priority:Priority
var body: some View {
Button(action: {
//判断输入框是否为空
if self.name.trimmingCharacters(in: .whitespaces) == "" {
return
}
//添加一条新数据
self.addTask(name: self.name, priority: self.priority)
//关闭弹窗
self.showNewTask = false
}) {
Text("保存")
.font(.system(.headline))
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding([.top,.bottom])
}
//添加新事项方法
private func addTask(name: String, priority: Priority, isCompleted: Bool = false) {
let task = ToDoItem(name: name, priority: priority, isCompleted: isCompleted)
todoItems.append(task)
}
}
我们定义一个addTask
添加事项的private
私有方法,添加的参数是name
内容、priority
优先级、isCompleted
是否完成,默认为否false
。然后实例化它,调用方法的时候在 todoItems
数组中增加一条数据。然后,我们点击SaveBotton
保存成功时调用addTask
添加新事项方法。
//NewToDoView视图
struct NewToDoView: View {
@State var name: String
@State var isEditing = false
@State var priority: Priority
@Binding var showNewTask: Bool
@Binding var todoItems: [ToDoItem]
var body: some View {
VStack {
Spacer()
VStack {
TopNavBar(showNewTask: $showNewTask)
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView(priority: $priority)
SaveButton(name: $name, showNewTask: $showNewTask, todoItems: $todoItems, priority: $priority)
}
.padding()
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isEditing ? -320 : 0)
}.edgesIgnoringSafeArea(.bottom)
}
}
struct NewToDoView_Previews: PreviewProvider {
static var previews: some View {
NewToDoView(name: "", priority: .normal, showNewTask: .constant(true), todoItems: .constant([]))
}
}
同时,我们在NewToDoView
视图绑定关联关系,并在NewToDoView_Previews
预览视图中也绑定好关系。
当然别忘了,还要在 ContentView
首页视图绑定参数。
// ContentView视图
NewToDoView(name: "", priority: .normal, showNewTask: $showNewTask, todoItems: $todoItems)
恭喜你,我们就完成了ContentView
首页视图和NewToDoView
新建事项视图的全部交互逻辑!
未完待续
但还没有全部完成,我们只是完成了一个简单的ToDo
待办事项的App
,还没有实现CoreData
数据持久化。
由于篇幅过长,上篇
我们完成了ContentView
首页视图的制作,中篇
我们完成NewToDoView
新建事项视图的制作,当然还有他们之间的交互
。
CoreData
数据持久化框架的使用将再分出下篇
,我们看看如何使用CoreData
数据持久化框架,真正实现一个可以保存数据
的App
。
本章完整代码如下:
//ToDoItem.swift
import Foundation
enum Priority: Int {
case low = 0
case normal = 1
case high = 2
}
class ToDoItem: ObservableObject, Identifiable {
var id = UUID()
@Published var name: String = ""
@Published var priority: Priority = .high
@Published var isCompleted: Bool = false
init(name: String, priority: Priority = .normal, isCompleted: Bool = false) {
self.name = name
self.priority = priority
self.isCompleted = isCompleted
}
}
//ContentView.swift
import CoreData
import SwiftUI
struct ContentView: View {
@State var todoItems: [ToDoItem] = []
@State private var showNewTask = false
@State private var offset: CGFloat = .zero //使用.animation防止报错,iOS15的特性
//去掉List背景颜色
init() {
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
}
var body: some View {
ZStack {
VStack {
TopBarMenu(showNewTask: $showNewTask)
ToDoListView(todoItems: $todoItems)
}
//判断事项数量为0时展示缺省图
if todoItems.count == 0 {
NoDataView()
}
//点击添加时打开弹窗
if showNewTask {
//蒙层
MaskView(bgColor: .black)
.opacity(0.5)
.onTapGesture {
self.showNewTask = false
}
NewToDoView(name: "", priority: .normal, showNewTask: $showNewTask, todoItems: $todoItems)
.transition(.move(edge: .bottom))
.animation(.interpolatingSpring(stiffness: 200.0, damping: 25.0, initialVelocity: 10.0),value: offset)
}
}
}
}
//蒙层
struct MaskView : View {
var bgColor: Color
var body: some View {
VStack {
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(bgColor)
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// 顶部导航栏
struct TopBarMenu: View {
@Binding var showNewTask: Bool
var body: some View {
HStack {
Text("待办事项")
.font(.system(size: 40, weight: .black))
Spacer()
Button(action: {
//打开弹窗
self.showNewTask = true
}) {
Image(systemName: "plus.circle.fill")
.font(.largeTitle).foregroundColor(.blue)
}
}
.padding()
}
}
// 缺省图
struct NoDataView: View {
var body: some View {
Image("image01")
.resizable()
.scaledToFit()
}
}
// 列表
struct ToDoListView: View {
@Binding var todoItems: [ToDoItem]
var body: some View {
List {
ForEach(todoItems) { todoItem in
ToDoListRow(todoItem: todoItem)
}
}
}
}
// 列表内容
struct ToDoListRow: View {
@ObservedObject var todoItem: ToDoItem
var body: some View {
Toggle(isOn: self.$todoItem.isCompleted) {
HStack {
Text(self.todoItem.name)
.strikethrough(self.todoItem.isCompleted, color: .black)
.bold()
.animation(.default)
Spacer()
Circle()
.frame(width: 20, height: 20)
.foregroundColor(self.color(for: self.todoItem.priority))
}
}.toggleStyle(CheckboxStyle())
}
// 根据优先级显示不同颜色
private func color(for priority: Priority) -> Color {
switch priority {
case .high:
return .red
case .normal:
return .orange
case .low:
return .green
}
}
}
// checkbox复选框样式
struct CheckboxStyle: ToggleStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return HStack {
Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(configuration.isOn ? .purple : .gray)
.font(.system(size: 20, weight: .bold, design: .default))
.onTapGesture {
configuration.isOn.toggle()
}
configuration.label
}
}
}
// NewToDoView.swift
import SwiftUI
struct NewToDoView: View {
@State var name: String
@State var isEditing = false
@State var priority: Priority
@Binding var showNewTask: Bool
@Binding var todoItems: [ToDoItem]
var body: some View {
VStack {
Spacer()
VStack {
TopNavBar(showNewTask: $showNewTask)
InputNameView(name: $name, isEditing: $isEditing)
PrioritySelectView(priority: $priority)
SaveButton(name: $name, showNewTask: $showNewTask, todoItems: $todoItems, priority: $priority)
}
.padding()
.background(Color.white)
.cornerRadius(10, antialiased: true)
.offset(y: isEditing ? -320 : 0)
}.edgesIgnoringSafeArea(.bottom)
}
}
struct NewToDoView_Previews: PreviewProvider {
static var previews: some View {
NewToDoView(name: "", priority: .normal, showNewTask: .constant(true), todoItems: .constant([]))
}
}
// 顶部导航栏
struct TopNavBar: View {
@Binding var showNewTask: Bool
var body: some View {
HStack {
Text("新建事项")
.font(.system(.title))
.bold()
Spacer()
Button(action: {
//关闭弹窗
self.showNewTask = false
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
.font(.title)
}
}
}
}
//输入框
struct InputNameView: View {
@Binding var name: String
@Binding var isEditing: Bool
var body: some View {
TextField("请输入", text: $name, onEditingChanged: { (editingChanged) in
self.isEditing = editingChanged
})
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.bottom)
}
}
// 选择优先级
struct PrioritySelectView: View {
@Binding var priority: Priority
var body: some View {
HStack {
PrioritySelectRow(name: "高", color: priority == .high ? Color.red : Color(.systemGray4))
.onTapGesture { self.priority = .high }
PrioritySelectRow(name: "中", color: priority == .normal ? Color.orange : Color(.systemGray4))
.onTapGesture { self.priority = .normal }
PrioritySelectRow(name: "低", color: priority == .low ? Color.green : Color(.systemGray4))
.onTapGesture { self.priority = .low }
}
}
}
// 选择优先级
struct PrioritySelectRow: View {
var name: String
var color:Color
var body: some View {
Text(name)
.frame(width: 80)
.font(.system(.headline))
.padding(10)
.background(color)
.foregroundColor(.white)
.cornerRadius(8)
}
}
// 保存按钮
struct SaveButton: View {
@Binding var name:String
@Binding var showNewTask: Bool
@Binding var todoItems: [ToDoItem]
@Binding var priority:Priority
var body: some View {
Button(action: {
//判断输入框是否为空
if self.name.trimmingCharacters(in: .whitespaces) == "" {
return
}
//添加一条新数据
self.addTask(name: self.name, priority: self.priority)
//关闭弹窗
self.showNewTask = false
}) {
Text("保存")
.font(.system(.headline))
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(8)
}
.padding([.top,.bottom])
}
//添加新事项方法
private func addTask(name: String, priority: Priority, isCompleted: Bool = false) {
let task = ToDoItem(name: name, priority: priority, isCompleted: isCompleted)
todoItems.append(task)
}
}
快来动手试试吧!
如果本专栏对你有帮助,不妨点赞、评论、关注~