条件回想
上一章节发布之后,有不少开发的童鞋评论:
本来好好的,经过上一章节的调整后就各个页面开始报错了?
这很正常,刚开始学习SwiftUI
的时分,有时分改了一个参数
,或许少了一个花括号
,愣是找不到哪里写错了。
后边写多了就基本知道哪里需求调整,并且遇到Bug
不可怕,就怕的是明明没有报错,并且是跟着项目教程来的,项目运转后便是没作用,也不知道哪里出错了。
找不到自己的薄缺点,这才是最可怕的。
因此不用太过于担心,本章咱们将持续根据完结的Model
、ViewModel
,来完结View
相关的内容。
实战编程-主页
单条笔记
咱们回到View文件夹下的ContentView视图,先从NoteListRow
单条笔记视图开始调整。
单条笔记涵盖哪些交互逻辑?一个是点击单条笔记的时分翻开修正笔记弹窗,二是点击笔记右侧的“更多”按钮,唤起二次承认弹窗,并可进行操作删去。如下图所示:
在上一章建立ViewModel功能的时分,咱们提到对单条笔记进行操作需求取得笔记的ID
,然后根据单条数据的ID进行操作。那么这儿需求先取得笔记的ID,如下代码所示:
// 引证viewModel
@EnvironmentObject var viewModel: ViewModel
// 取得项目仅有ID
var itemId: UUID
// 从模型类中找ID
var item: NoteModel? {
return viewModel.getItemById(itemId: itemId)
}
上述代码中,咱们先运用@EnvironmentObject
全局环境变量引进ViewModel
类,并赋值给viewModel。
@EnvironmentObject是一个动态视图特点,为了不管任何时分可绑定目标发生改动时停用当时视图的特点。
紧接着,声明一个变量itemId
,遵循UUID
格局,作为要运用到的ID。之前咱们运用@ObservedObject取得NoteItem模型类,这儿咱们运用ViewModel就能够弃用本来的这块内容了,直接声明一个变量item,并经过调用viewModel中的getItemById
办法取得对应的笔记ID。
在取得笔记ID后,体系可能无法返回相关的数据内容,也便是参数为空的情况导致体系报错,因此咱们运用“?”,当返回的参数值为空的时分,就能够运用默认值填充,防止体系奔溃。
说回正题,由于咱们运用item
替换了本来的noteItem,在下面视图对应的参数也需求调整,如下代码所示:
var body: some View {
HStack {
HStack {
VStack(alignment: .leading, spacing: 10) {
Text(item?.writeTime ?? "")
.font(.system(size: 14))
.foregroundColor(.gray)
Text(item?.title ?? "")
.font(.system(size: 17))
.foregroundColor(.black)
Text(item?.content ?? "")
.font(.system(size: 14))
.foregroundColor(.gray)
.lineLimit(1)
.multilineTextAlignment(.leading)
}
}
Spacer()
// 更多操作
Button(action: {
}) {
Image(systemName: "ellipsis")
.foregroundColor(.gray)
.font(.system(size: 23))
}
}
}
上述代码中,替换单条笔记绑定的参数,运用item替换noteItem,替换如下:
- noteItem.writeTime 替换为
item?.writeTime ?? ""
- noteItem.title 替换为
item?.title ?? ""
- noteItem.content 替换为
item?.content ?? ""
替换后,咱们来完结两个基本功能,一个是点击笔记的时分,翻开修正笔记弹窗,如下代码所示:
var body: some View {
HStack {
HStack {
VStack(alignment: .leading, spacing: 10) {
Text(item?.writeTime ?? "")
.font(.system(size: 14))
.foregroundColor(.gray)
Text(item?.title ?? "")
.font(.system(size: 17))
.foregroundColor(.black)
Text(item?.content ?? "")
.font(.system(size: 14))
.foregroundColor(.gray)
.lineLimit(1)
.multilineTextAlignment(.leading)
}
}
//点击修正
.onTapGesture {
self.viewModel.isAdd = false
self.viewModel.showEditNoteView = true
}
Spacer()
// 更多操作
Button(action: {
viewModel.showActionSheet = true
}) {
Image(systemName: "ellipsis")
.foregroundColor(.gray)
.font(.system(size: 23))
}
}
}
上述代码中,咱们在笔记内容的HStack横向容器中添加了onTapGesture
,当点击笔记内容的时分,阐明咱们需求修正笔记,这儿需求更新isAdd是否新增笔记状况为false,然后更新showEditNoteView
翻开修正笔记弹窗的触发条件为true。
当用户点击“更多”操作时,更新showActionSheet
翻开二次承认弹窗的触发条件为true。
然后咱们完结翻开修正弹窗和翻开删去的二次承认弹窗的操作,如下代码所示:
// 修正笔记
.sheet(isPresented: $viewModel.showEditNoteView) {
//修正笔记弹窗
}
// 删去笔记
.actionSheet(isPresented: self.$viewModel.showActionSheet) {
ActionSheet(
title: Text("你确定要删去此项吗?"),
message: nil,
buttons: [
.destructive(Text("删去"), action: {
self.viewModel.deleteItem(itemId: itemId)
}),
.cancel(Text("撤销")),
])
}
上述代码中,咱们运用sheet
办法,绑定showEditNoteView
翻开修正弹窗的触发参数,修正笔记页面后边咱们会和新建笔记页面功用,这儿还没有修正,就先放着。
删去笔记的办法咱们运用actionSheet
弹窗,绑定showActionSheet
翻开删去二次承认弹窗触发参数,在ActionSheet
弹窗内,咱们设置好标题,以及删去按钮的操作,当点击删去的时分,调用viewModel中的deleteItem
办法,指定单条笔记的itemId
找到对应的笔记进行删去。
完结后,咱们单条笔记部分,除了翻开修正弹窗,其他内容现已修正完结。
笔记列表
回到ContentView
视图,咱们修正了单条笔记的内容,因此笔记列表noteListView
视图也需求调整,首要引进ViewModel,如下代码所示:
// 引证viewModel
@EnvironmentObject var viewModel: ViewModel
紧接着,咱们换一种办法完结笔记列表,如下代码所示:
// MARK: 笔记列表
func noteListView() -> some View {
List {
ForEach(viewModel.noteModels) { noteItem in
NoteListRow(itemId: noteItem.id)
}
}
.listStyle(InsetListStyle())
}
上述代码中,咱们换成了运用func
办法声明视图,这是另一种创立视图的办法,这样创立视图的好处是,咱们需求声明的参数能够放在ContentView视图中,就不需求在每一个视图中声明。
笔记列表仅有的改动便是NoteListRow
单条笔记遍历循环的时分,数组来源于viewModel中的noteModels
,然后NoteListRow中的ID为noteItem中的ID
。
顶部查找栏
再往上是顶部查找栏,如下代码所示:
// MARK: 查找栏
func searchBarView() -> some View {
TextField("查找内容", text: $viewModel.searchText)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
// 修正时显现铲除按钮
if viewModel.searchText != "" {
Button(action: {
self.viewModel.searchText = ""
self.viewModel.loadItems()
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onChange(of: viewModel.searchText) { _ in
if viewModel.searchText != "" {
self.viewModel.isSearching = true
self.viewModel.searchContet()
} else {
viewModel.searchText = ""
self.viewModel.isSearching = false
self.viewModel.loadItems()
}
}
}
查找栏改动的内容有三部分,首要是绑定的输入内容换成了viewModel中的searchText
。
然后是当查找栏输入时,显现删去的按钮操作,关联的参数也换成viewModel中的searchText,当点击铲除查找内容时,需求将查找栏输入的内容清空,然后再调用loadItems
从头加载列表中的数据。
最终是查找栏的查找办法,当输入时,读取searchText输入的内容,假如输入内容不为空,则更新isSearching
是否正在查找的状况为true,然后调用searchContet
查找办法。假如输入的内容为空,那么更新isSearching
是否正在查找的状况为false
,并调用loadItems
从头加载列表数据。
新建笔记按钮
新建笔记按钮的操作是翻开新建笔记弹窗,修正内容如下代码所示:
// MARK: 新建笔记按钮
func newBtnView() -> some View {
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.viewModel.isAdd = true
self.viewModel.writeTime = viewModel.getCurrentTime()
self.viewModel.title = ""
self.viewModel.content = ""
self.viewModel.showNewNoteView = true
}) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 48))
.foregroundColor(.blue)
}
}
}
.padding(.bottom, 32)
.padding(.trailing, 32)
}
新建笔记按钮修正点就只有点击时的交互动作,当点击新建笔记按钮时,需求更新viewModel中的是否新增状况isAdd
为true
,标明点击这个按钮是新增,而咱们在单条笔记列表设置isAdd为false,表示当时是在修正笔记。
当新增笔记的时分,调用getCurrentTime
设置新建笔记的时刻为当时时刻,设置title
标题、content
内容为空,并且更新showNewNoteView
为true
,作为翻开新建笔记弹窗的触发参数。
主视图
缺省图视图基本就不用改了,最终回到body
部分,修正如下代码所示:
var body: some View {
NavigationView {
ZStack {
if viewModel.isSearching == false && viewModel.noteModels.count == 0 {
noDataView()
} else {
VStack {
searchBarView()
noteListView()
}
}
newBtnView()
}
.navigationBarTitle("想法笔记", displayMode: .inline)
}
.sheet(isPresented: $viewModel.showNewNoteView) {
//翻开新建笔记弹窗
}
}
上述代码中,咱们经过判别isSearching
当时是否处于查找状况,以及noteModels
数组是是否有数据,来判别当时应该展现缺省视图noDataView
,仍是展现searchBarView
查找栏和noteListView
笔记列表。
现已在主页添加sheet
绑定showNewNoteView
触发翻开新建笔记弹窗。
最终,咱们还需求在视图预览的时分引证viewModel,如下代码所示:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ViewModel())
}
}
好了,主页基本修正完毕了,是不是有点疲劳?回看下整个ContentView的代码,是不是简练了许多,基本没有声明什么参数,因为需求用到的参数都放在了ViewModel里,并且用到的完结功能的办法也都放在ViewModel。
Model
用来界说数据模型,View
视图用来完结根底交互和页面款式,然后ViewModel
用来做数据处理和功能完结,这便是MVVM开发模式
。
休息好了,咱们就持续吧~
实战编程-新建笔记页
标题&内容输入
首要仍是需求引证ViewModel,才能运用里边声明好的参数。如下代码所示:
// 引证viewModel
@EnvironmentObject var viewModel: ViewModel
引证viewModel后,其他声明的参数都能够删掉了。当咱们在主页笔记列表点击单条笔记时,会翻开新建笔记弹窗,并把内容传递过来,因此咱们需求声明模型类参数,如下代码所示:
// 引证viewModel
@EnvironmentObject var viewModel: ViewModel
// 声明参数
@State var noteModel: NoteModel
// 封闭弹窗
@Environment(.presentationMode) var presentationMode
声明好需求的参数后,咱们来到标题输入框和内容输入框的部分,这是需求绑定的参数就不是之前声明的参数了。当咱们是新建笔记的时分,标题和内容应该是为空,也便是绑定在viewModel
声明的title
、content
,而假如是修正笔记,则咱们需求绑定的是来自于点击的单条笔记的内容。如下代码所示:
// MARK: 标题输入框
func titleView() -> some View {
TextField("请输入标题", text: viewModel.isAdd ? $viewModel.title : $noteModel.title)
.padding()
}
// MARK: 内容输入框
func contentView() -> some View {
ZStack(alignment: .topLeading) {
TextEditor(text: viewModel.isAdd ? $viewModel.content : $noteModel.content)
.font(.system(size: 17))
.padding()
if viewModel.isAdd ? (viewModel.content.isEmpty) : (noteModel.content.isEmpty) {
Text("请输入内容")
.foregroundColor(Color(UIColor.placeholderText))
.padding(20)
}
}
}
完结按钮
完结按钮这块,回想下咱们前几章学习的内容,它应该包括几块内容:
一是判别条件,当咱们标题或许内容输入为空的时分,应该提示输入。
二是点击完结操作的时分,也需求判别当时是新增操作仍是修正操作,假如是新增操作,则插入一条新笔记,假如是修正操作,则需求更新笔记的内容。
咱们修正代码如下所示:
// MARK: 完结按钮
func saveBtnView() -> some View {
Button(action: {
//判别当时是新增仍是修正
if viewModel.isAdd {
//判别标题是否为空
if viewModel.isTextEmpty(text: viewModel.title) {
viewModel.showToast = true
viewModel.showToastMessage = "请输入标题"
}
//判别内容是否为空
else if viewModel.isTextEmpty(text: viewModel.content) {
viewModel.showToast = true
viewModel.showToastMessage = "请输入内容"
}
//校验经过
else {
// 新增一条笔记
self.viewModel.addItem(writeTime: viewModel.getCurrentTime(), title: viewModel.title, content: viewModel.content)
//封闭弹窗
self.presentationMode.wrappedValue.dismiss()
}
} else {
//判别标题是否为空
if viewModel.isTextEmpty(text: noteModel.title) {
viewModel.showToast = true
viewModel.showToastMessage = "标题不能为空"
}
//判别内容是否为空
else if viewModel.isTextEmpty(text: noteModel.content) {
viewModel.showToast = true
viewModel.showToastMessage = "内容不能为空"
}
//校验经过
else {
// 保存一条新笔记
self.viewModel.editItem(item: noteModel)
//封闭弹窗
self.presentationMode.wrappedValue.dismiss()
}
}
}) {
Text("完结")
.font(.system(size: 17))
}
}
代码如同许多的样子,其实不然,逻辑很简单。
当点击“完结”按钮时,首要需求isAdd
状况判别当时是新增仍是删去。
假如是新增,则判别viewModel中的输入的标题title和内容content是否为空
,假如为空,则更改showToast
翻开提示信息,现已更新showToastMessage
提示信息的内容。假如不为空时,则调用addItem
办法新增一条笔记。
假如点击“完结”按钮时的操作为修正操作,则和上面一样的判别,只是判别为空的参数变成了来源于noteModel
的标题title
和内容content
,当为空判别经过后,则调用editItem
修正办法更新笔记内容。
最终都是调用presentationMode.wrappedValue.dismiss
封闭弹窗,当然直接点击封闭按钮时也能够调用这个办法封闭弹窗。
主视图
最终来到新建笔记的body
部分,修正部分就只有标题和toast
绑定的参数,如下代码所示:
var body: some View {
NavigationView {
VStack {
Divider()
titleView()
Divider()
contentView()
}
.navigationBarTitle(viewModel.isAdd ? "新建笔记" : "修正笔记", displayMode: .inline)
.navigationBarItems(leading: closeBtnView(), trailing: saveBtnView())
.toast(present: $viewModel.showToast, message: $viewModel.showToastMessage)
}
}
由于新建笔记页面运用了ViewModel
和声明了noteModel
模型类,因此咱们假如需求预览该页面,则需求在预览的代码中设置默认值,如下代码所示:
struct NewNoteView_Previews: PreviewProvider {
static var previews: some View {
NewNoteView(noteModel: NoteModel(writeTime: "", title: "", content: "")).environmentObject(ViewModel())
}
}
最终,新建笔记页面修正好后,需求回到ContentView
主页,咱们翻开弹窗的路径还没有配置呢。
在新建笔记时,跳转的页面时NewNoteView
,如下代码所示:
// 新增笔记
.sheet(isPresented: $viewModel.showNewNoteView) {
NewNoteView(noteModel: NoteModel(writeTime: "", title: "", content: ""))
}
在修正笔记时,跳转的页面也是NewNoteView
,如下代码所示:
// 修正笔记
.sheet(isPresented: $viewModel.showEditNoteView) {
NewNoteView(noteModel: self.item ?? NoteModel(writeTime: "", title: "", content: ""))
}
项目预览
完结后,运转预览作用如下图所示:
本章小结
祝贺你,完结了运用SwiftUI
从0到1完结一款笔记APP的全部内容!
在整个项目过程中,咱们首要学习怎么完结一个个独自的视图,再将一块块的代码组合成一个页面,最终再根底页面和交互的根底上运用Model-View-ViewModel
的办法进行开发调整,最终完结整个项目。
回想第一个项目的整个过程,咱们会发现咱们构建视图的办法都是自上而下构建,而完结交互功能、逻辑是自下而上建立。这便是专栏开始之初提到的编程逻辑:
自顶向下逐步求精的模块化规划思想、面向目标的办法自底而上进行开发思想。
编程技巧固然重要,但更重要的是思想办法,办法很简单学会,但观念和习气就没有那么简单改动了。
编程本便是一条没有那么风趣的路,无妨沉下心来,写好每一段代码,写好每一块事务。
看着最终成功运转的项目,感受着心底的高兴喷涌而出~
接下来,咱们将持续完结和完结其他项目,请坚持期待吧~
版权声明
本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!