一同养成写作习惯!这是我参与「日新方案 4 月更文应战」的第21天,点击检查活动详情。

今天职言:外表干净是尊重别人,内心干净是尊重自己,言行干净是尊重魂灵。

承接上一章的内容,咱们持续完结运用CoreData结构搭建一个简单的ToDo待办事项App

这一章节,咱们完结一下NewToDoView新建事项页面。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

咱们先新建一个新页面,命名为NewToDoView

点击Xcode顶部导航栏,File文件,New新建,挑选File创立文件,挑选iOS中的SwiftUI File类型的文件,命名为NewToDoView.swift

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

页面UI设计

咱们还是从上往下构建UI页面。

TopNavBar顶部导航栏视图

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

首先是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)
}
}
}
}

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

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预览视图中增加参数。

运转一下,咱们看下作用。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

下面咱们持续,接下来是事项的优先级挑选,咱们先完结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()
}

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

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

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

NewToDoView页面方位调整

然后,咱们调整下方位,咱们希望这个NewToDoView页面是从底部弹出来,然后内容也都在底部展现而不是居中,咱们能够调整下整个NewToDoView页面的方位。

咱们用VStackSpacerNewToDoView视图顶到底部,然后依据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)

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

交互逻辑设计

好了,咱们完结了NewToDoView页面的制作了,下面是逻辑部分。

PrioritySelectView优先级挑选逻辑

首先是咱们的PrioritySelectView优先级的挑选,咱们希望点击挑选哪个优先级,哪个优先级就“亮起”,这样咱们好知道选中的是哪一个。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

相同,咱们需求贮存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)
}
}

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

页面弹出逻辑

让咱们回到ContentView主页,咱们将两个页面联动起来。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

页面弹出的交互逻辑是,当咱们点击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)

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

如同基本完结了作用,但由于咱们是运用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
 }

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

好!咱们完成了怎样弹出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)
}
}

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

咱们发现体系报错了,这是由于咱们运用@Binding绑定了是否展现页面showNewTask的布尔值,还需求在ContentView主页建立相关。

//ContentView视图
NewToDoView(name: "", priority: .normal, showNewTask: $showNewTask)

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

这样,咱们就完结了第一种封闭弹窗的交互:点击封闭按钮封闭弹窗。

另一种封闭弹窗的交互是,咱们新建一个事项,满意条件后(内容不为空),这是咱们点击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文件中,运转模拟器体验下。

咱们完结了基础的封闭弹窗操作,能够点击封闭按钮封闭,也能够输入新建事项后,点击保存封闭弹窗。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

增加新事项逻辑

咱们在NewToDoView增加完事项后,输入的内容和挑选的优先级就会在ContentView主页List列表中创立一条数据,下面咱们来完结增加新事项逻辑。

SwiftUI极简教程21:CoreData数据持久化框架的使用(中)

咱们看回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)
}
}

快来动手试试吧!

假如本专栏对你有协助,无妨点赞、评论、关注~