学习完了Swift后需要在其基础上学习SwiftUI才能开发出APP,swift是对数据的逻辑处理,而SwiftUI是对视图处理。目前主流的开发模式是MVVM(Model + View + ViewModel)。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。
下面的代码是我按照视频:https://www.bilibili.com/video/BV1Kg4y1i7dd 学习理解后增加了详细的注释,在代码命名、功能、视图上有少许的不一样。经过测试是可以完全运行的且功能正常。
1、主界面
2、只显示已收藏的事项
3、编辑、增加事项
从功能和界面上来看是非常简单的,做为入门练习和理解开发还是很我帮助的。可以通过代码学习到数据绑定、布局处理、推送通知等。
1、主界面(默认文件View) ContentView.swift
import SwiftUI
//数据初始化,尝试从本地读取数据
func initUserData() -> [SingleToDo] {
var outPut:[SingleToDo] = [] //定义一个空SingleToDo数组
//从用户存储位置读取数据
if let dataStored = UserDefaults.standard.object(forKey: "myTests") as? Data{
let myData = try! deCoder.decode([SingleToDo].self, from: dataStored) //将读取到的数据转码为[SingleToDo]数组
for item in myData{
//如果没有删除就追加数据
if !item.isDelete{
outPut.append(SingleToDo(id: outPut.count, title: item.title, myData: item.myData, isChecked: item.isChecked, isFavorite: item.isFavorite, sendNotifications: item.sendNotifications))
}
}
}
return outPut //返回格式化后的数据
}
//首屏主视图
struct ContentView: View {
//ObservedObject 用于修改了数据后刷新视图(及容易与ObservableObject混淆)
@ObservedObject var toDoList:ToDo = ToDo(initUserData()) //通过initUserData从本地读取数据
@State var goSetting:Bool = false //是否进入设置/多选模式
@State var multiSelectArr:[Int] = [] //编辑、多选模式下的选中的ID数组
@State var favoriteOnly = false
var body: some View {
ZStack{ //ZStack 二维层叠布局,覆盖其子视图,并在两个轴上将它们对齐的视图。
VStack{//VStack 在垂直线上排列其子视图。
NavigationView{//导航视图 导航层次结构中一个可见路径的视图堆栈
ScrollView{ //可滚动视图
ForEach(toDoList.todolist){ item in
if !item.isDelete{//已标记删除不显示
if !favoriteOnly || item.isFavorite{ //判断是否只显示收藏事项
CardView(i:item.id,goSetting:$goSetting,setChecked:$multiSelectArr) //从父视图传入相关绑定数据
.environmentObject(toDoList)//将toDoList数据传送给子视图CardView
}
}
}
.padding(.bottom,100) //防止“增加按钮”挡住点击框
}
.padding(.top) //与导航拉开距离
.navigationTitle("提醒事项") //页面标题
//在标题右上角显示按钮
.navigationBarItems(trailing:
HStack{
if goSetting{//编辑模式下显示回收按钮
SelecAall(multiSelectArr: $multiSelectArr) //全选
.environmentObject(toDoList)//将toDoList数据传送给子视图SelecAall
trashAll(multiSelectArr: $multiSelectArr,goSetting:$goSetting) //传入toDoList 调用方法,绑定$multiSelectArr 读取选中的ID
.environmentObject(toDoList)//将toDoList数据传送给子视图trashAll
FavoriteToggle(multiSelectArr: $multiSelectArr,goSetting:$goSetting) //传入toDoList
.environmentObject(toDoList)//将toDoList数据传送给子视图trashAll
}else{ //非编辑模式显示收藏按钮
Favorite(favoriteOnly: $favoriteOnly) //传入绑定数据
}
SetButton(goSetting: $goSetting,multiSelectArr:$multiSelectArr) //将绑定的goSetting传给子视图
})
}
}
Spacer() //此Spacer可有可无,当不使用时默认会叠放到上一个VStack上,容易挡住上个视图
VStack{ //垂直布局
Spacer() //将子视图推向底部
HStack{ //HStack 在水平线上排列其子视图。
Spacer() //推向最右面
AddNewCard() //右下角增加按钮
.environmentObject(toDoList) //向子视图发送数据
}
}
}
}
}
//下面代码作用是显示右侧的实时预览
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(toDoList: ToDo(
[
SingleToDo(title: "吃饭"),
SingleToDo(title: "睡觉"),
SingleToDo(title: "打豆豆"),
SingleToDo(title: "做作业", isChecked: true),
SingleToDo(title: "去广场", isChecked: true),
SingleToDo(title: "跑步"),
SingleToDo(title: "浇花", isChecked: true),
]
))
}
}
2、所有按钮的集合文件(View) Buttons.swift
// 所有的按钮集合
// Created by Kwok on 2021/3/25.
import SwiftUI
//右下角的增加按钮
struct AddNewCard:View {
@State var showEditingPage:Bool = false//是否显示编辑页面
@EnvironmentObject var toDoList:ToDo //接收父视图传过来的数据
var buttonSize:CGFloat = 80.0 //定义按钮尺寸
var body: some View{
Button(action: {
showEditingPage = true //显示编辑页面
}) {
Image(systemName: "plus.circle.fill")
.resizable() //设置SwiftUI调整图像大小以适应其空间的模式
.aspectRatio(contentMode: .fit) //将此视图的尺寸限制为指定的宽高比。
.background(Color.white) //防止中间透明
.frame(width: buttonSize,height: buttonSize)
.cornerRadius(buttonSize) //防止背影益处
.padding(.trailing)
}
//向上拉起EditingPage页面
.sheet(isPresented: $showEditingPage, content: {
EditingPage()
.environmentObject(toDoList)//向EditingPage发送数据
})
}
}
//设置及多选按钮
struct SetButton:View {
@Binding var goSetting:Bool //与父视图的数据绑定
@Binding var multiSelectArr:[Int]
var body: some View{
Button(action: {
goSetting.toggle()//切换编辑模式
print("切换编辑模式:(goSetting)")
}) {
Image(systemName: "gear")
.imageScale(.large)
}
}
}
//批量删除按钮
struct trashAll:View {
@EnvironmentObject var toDoList:ToDo //需要从父视图接收调用删除方法( .environmentObject)
@Binding var multiSelectArr:[Int] //从父视图接收
@State var showAlert = false //弹出开关
@Binding var goSetting:Bool
var body:some View{
Button(action: {
if multiSelectArr.isEmpty{
showAlert = true
}else{
for i in multiSelectArr{
toDoList.delete(id: i) //循环删除ID
print("删除ID:(i)")
}
multiSelectArr.removeAll() //清空已选择,单个删除容易 Index out of range
goSetting = false //关闭批量模式
}
}) {
Image(systemName: "trash")
.imageScale(.large)
}
//弹出一个提示
.alert(isPresented: $showAlert){
Alert(title: Text("提示:"), message: Text("请选择您要指删除的事项~"), dismissButton: .default(Text("OK")))
}
}
}
//只显示收藏事项的按钮
struct Favorite:View {
@Binding var favoriteOnly:Bool
var body: some View{
Button(action: {
favoriteOnly.toggle() //切换显示收藏
print("只显示收藏事项")
}) {
Image(systemName: "star.fill")
.imageScale(.large)
.foregroundColor(.yellow)
}
}
}
//收藏取反按钮
struct FavoriteToggle:View {
@EnvironmentObject var toDoList:ToDo //需要从父视图接收要处理的数据
@Binding var multiSelectArr:[Int] //从父视图接收
@State var showAlert = false //弹出开关
@Binding var goSetting:Bool
var body: some View{
Image(systemName: "star.leadinghalf.fill")
.imageScale(.large)
.foregroundColor(.yellow)
.onTapGesture {
if multiSelectArr.isEmpty{
showAlert = true
}else{
for i in multiSelectArr{
toDoList.todolist[i].isFavorite.toggle() //切换选中项的收藏状态
}
multiSelectArr.removeAll() //清空已选择,单个删除容易 Index out of range
goSetting = false //关闭批量模式
}
}
//弹出一个提示
.alert(isPresented: $showAlert){
Alert(title: Text("提示:"), message: Text("请选择您要反转收藏的事项~"), dismissButton: .default(Text("OK")))
}
}
}
//全选按钮
struct SelecAall:View {
@EnvironmentObject var toDoList:ToDo //需要从父视图接收要处理的数据
@Binding var multiSelectArr:[Int] //父视图的编辑选中的ID追加
@State var allChecked = false
var body: some View{
Image(systemName: allChecked ? "circlebadge.2.fill" :"circlebadge.2")
.imageScale(.large)
.foregroundColor(.green)
.onTapGesture{
allChecked.toggle()//切换选择状态
if allChecked{
for item in toDoList.todolist{
multiSelectArr.append(item.id)
}
}else{
multiSelectArr.removeAll()
}
}
}
}
3、编辑、增加事项页面(View) EditingPage.swift
//编辑事项页
import SwiftUI
struct EditingPage: View {
@EnvironmentObject var userData:ToDo //接收上层页面传输过来的数据
@State var title:String = "" //默认的标题为空
@State var myDate:Date = Date()//默认时间为当前时间 + 60秒
@State var showAlert:Bool = false //是否显示警告
@State var isFavorite:Bool = false //默认为不收藏
@State var isChecked:Bool = false //是否已完成(编辑后不改变其状态,新增默认为否)
@State var sendNotifications:Bool = true //是否发送通知
@Environment(.presentationMode) var presentation //定义一个从视图环境读取值的属性包装器。presentationMode绑定到与此环境关联的视图的当前表示模式。
@State var alertMessage = ""
var id:Int? = nil //定义一个接收ID
var body: some View {
NavigationView{
Form{ //表单视图、用于对用于数据输入的控件进行分组,例如在设置或检查器中。
Section{//创建层次化视图内容。
TextField("输入事项", text: $title)//显示可编辑文本界面的控件。
.frame(height: 60)//增高一点
Toggle(isOn: $isFavorite) {
Text("收藏此事项")
}
Toggle(isOn: $sendNotifications) {
Text("发送通知")
}
}
Section{//创建层次化视图内容。
//用于选择绝对日期的控件。
Text("请选择一个截至时间:")
.fontWeight(.bold)
.foregroundColor(.orange)
DatePicker(
"时间:",
selection: $myDate,
in: Date().addingTimeInterval(60.0)... //限制时间范围为最少60秒后
)
.datePickerStyle(GraphicalDatePickerStyle()) //日期样式
.labelsHidden() //隐藏“时间”label,部分样子会自动把label的文字隐藏
}
Section{
Button(action: {
if title.count < 1{
showAlert = true //显示警告
alertMessage = "请输入一个事项~"
}else{
if self.myDate.timeIntervalSinceNow < 0{
showAlert = true //显示警告
alertMessage = "请修改一个大于当前时间的“提醒时间”~"
}else{
print("传入的时间:" + self.myDate.description)
if let hasID = id{
userData.edit(id: hasID, data: SingleToDo(title: self.title,myData:self.myDate,isChecked:self.isChecked, isFavorite: self.isFavorite, sendNotifications: self.sendNotifications)) //编辑
}else{
userData.add(SingleToDo(title: self.title,myData:self.myDate,isChecked:false, isFavorite: self.isFavorite, sendNotifications: self.sendNotifications))//新增
}
presentation.wrappedValue.dismiss()//关闭当前页面
}
}
}) {
Text("确定")
}
//弹出一个警告
.alert(isPresented: $showAlert){
Alert(title: Text("提示信息"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
}
Button(action: {
presentation.wrappedValue.dismiss()//关闭当前页面(如果视图是当前显示的,则解散视图)
}) {
Text("取消")
}
}
}
.navigationTitle(id == nil ? "增加一个事项" : "编辑一个事项")
}
}
}
//实时预览
struct EditingPage_Previews: PreviewProvider {
static var previews: some View {
EditingPage()
}
}
4、数据结构页(Model) SingleToDo.swift
// 数据结构
import Foundation
//Identifiable协议是通过定义一个id来做为一个可识别的对象,Codable表示可编码的
struct SingleToDo:Identifiable,Codable {
var id:Int = 0
var title:String
var myData:Date = Date()
var isChecked:Bool = false{
willSet{
print(id,newValue)//打印改变
}
}
var isDelete = false //删除标志
var isFavorite:Bool = false //是否收藏
var sendNotifications:Bool = true //默认发送通知
}
5、功能操作页(ViewModel) TodoList.swift
// 数据操作
import Foundation
import UserNotifications //导入通知框架
var enCoder = JSONEncoder() //编码为JSON格式
var deCoder = JSONDecoder() //将Json解码回来
//ObservableObject协议需要符合可观察的对象、该发布者在对象更改之前发出。接受方使用@ObservedObject单词不一样!!!
class ToDo: ObservableObject {
@Published var todolist:[SingleToDo]//Published发布带有属性的属性。
var count = 0
init(_ data:[SingleToDo]) {
self.todolist = []
for item in data{
self.todolist.append(SingleToDo(id: count, title: item.title, myData: item.myData, isChecked: item.isChecked, isFavorite: item.isFavorite, sendNotifications: item.sendNotifications))
count += 1
}
}
//修改事项状态
func check(_ i:Int) {
self.todolist[i].isChecked.toggle()
self.save()//保存到数据
}
//增加一个事项
func add(_ data:SingleToDo) {
self.todolist.append(SingleToDo(id: count, title: data.title, myData: data.myData, isChecked: data.isChecked, isFavorite: data.isFavorite, sendNotifications: data.sendNotifications))
count += 1
self.sort()//排序
self.save()//保存到数据
//数据需要保存后才能发布通知
if data.sendNotifications{
if sendNoNotification(count - 1){
print("新增发送通知成功")
}
}
}
//编辑一个事项
func edit(id:Int,data:SingleToDo) {
WithdrawalNoNotification(self.todolist[id].id)//编辑前撤回消息
self.todolist[id].title = data.title
self.todolist[id].myData = data.myData
self.todolist[id].isChecked = data.isChecked //完成状态
self.todolist[id].isFavorite = data.isFavorite //收藏状态
self.todolist[id].sendNotifications = data.sendNotifications //发送通知开关
self.sort()//排序
self.save()//保存到数据
//数据需要保存后才能发布通知
if data.sendNotifications{
if sendNoNotification(self.todolist[id].id){
print("编辑发送通知成功")
}
}
}
//对事项按时间排序
func sort() {
self.todolist.sort(by:{$0.myData.timeIntervalSince1970 > $1.myData.timeIntervalSince1970})
for i in 0 ..< self.todolist.count{
self.todolist[i].id = i
}
print("改了排序")
}
//增加一个删除标志
func delete(id:Int) {
WithdrawalNoNotification(id) //撤回消息
self.todolist[id].isDelete = true
print("删除了")
self.sort()
self.save()//保存u数据
}
//将数据编码后保存到本机
func save() {
let dataStored = try! enCoder.encode(todolist) //encode是可能抛出错误的,使用try!强制忽略错误
UserDefaults.standard.set(dataStored, forKey: "myTests") //将编码为JSON格式的数据存到Key= myTest_文件里
print("保存成功")//打印日志
}
//发送通知
func sendNoNotification(_ id:Int) -> Bool{
var isOk = false
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success && self.todolist[id].myData.timeIntervalSinceNow > 10{ //推送时间大于10秒才发送通知
print("(self.todolist[id].myData.timeIntervalSinceNow)秒后将发送通知:" + self.todolist[id].title)
isOk = true
let content = UNMutableNotificationContent()//定义通知内容格式
content.title = self.todolist[id].title
content.subtitle = "这是副标题"
content.sound = UNNotificationSound.default //提示音
//触发时间
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: self.todolist[id].myData.timeIntervalSinceNow , repeats: false)
//触发器
let request = UNNotificationRequest(identifier: self.todolist[id].title + self.todolist[id].myData.description , content: content, trigger: trigger)
//添加通知请求
UNUserNotificationCenter.current().add(request)
} else if let error = error {
print(error.localizedDescription)//用户可能关闭了通知
}else{
print("通知发送失败,可能时间不对:"+self.todolist[id].myData.description,self.todolist[id].myData.timeIntervalSinceNow)
}
}
return isOk
}
//撤回一条通知
func WithdrawalNoNotification(_ id:Int){
let identifier:String = self.todolist[id].title + self.todolist[id].myData.description
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])//撤回已发出的消息
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])//撤回还没有发出的消息
print("尝试撤回一条消息",id)
}
}
6、单个卡片(View) SingleCard.swift
//单个卡片
import SwiftUI
struct CardView:View {
@EnvironmentObject var userData:ToDo //EnvironmentObject协议 接收父视图提供的可观察对象的属性包装类型。
var i:Int //数据的ID
@State var showEditingPage:Bool = false
@Binding var goSetting:Bool //从父视图获取绑定数据
@Binding var setChecked:[Int] //父视图的编辑选中的ID追加
//定义一个时间格式化器
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" //初始化日期格式
//formatter.dateStyle = .long
return formatter
}()
var body: some View {
HStack {//在水平线上排列其子视图。
Rectangle() //在包含它的视图框架内对齐的矩形形状。
.frame(width:6)
.padding(.trailing)
.foregroundColor(userData.todolist[i].isChecked ? .purple :.green)
//编辑模式下能显示删除按钮
if goSetting{
//删除按钮
Button(action: {
self.userData.delete(id: i) //标记为删除状态
}) {
Image(systemName: "trash")
.imageScale(.large)
.padding(.trailing)
}
}
//点击标题及空白编辑
Button(action: {
//非批量模式下有效
if !goSetting{
showEditingPage = true //sheet为真向上拉页面
}
}) {
//Group将文字与留白合并为一个可点击修改的组
Group {
VStack(alignment:.leading, spacing: 8.0){
Text(userData.todolist[i].title)
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.black)
Text(self.dateFormatter.string(from: userData.todolist[i].myData)) //对时间格式化后显示
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
}
}
//MARK sheet里的第一个参数isPresented经常会被搞成item,这里要注意一下s
.sheet(isPresented: $showEditingPage, content: {
EditingPage(
title:userData.todolist[i].title,
myDate:userData.todolist[i].myData,
isFavorite: userData.todolist[i].isFavorite,
isChecked: userData.todolist[i].isChecked,
sendNotifications: userData.todolist[i].sendNotifications,
id:i
)
.environmentObject(userData)//将数据传入EditingPage页面
})
//单个切换收藏的图标
Image(systemName: userData.todolist[i].isFavorite ? "star.fill" : "star")
.imageScale(.large)
.foregroundColor(userData.todolist[i].isFavorite ? .yellow : .gray)
.onTapGesture {
userData.todolist[i].isFavorite.toggle() //切换是否收藏
userData.save()//调用保存
}
//卡片选择框
if goSetting {
//进入批量编辑模式
Image(systemName: setChecked.firstIndex(where: {$0 == i}) == nil ? "circle" : "checkmark.circle.fill")
.imageScale(.large)
.padding(.horizontal)
.onTapGesture {
if setChecked.firstIndex(where: {$0 == i}) == nil{
setChecked.append(i)//没有找到就增加
}else{
setChecked.remove(at: i)//否则就移除
}
}
}else{
//下面是完成情况的点击监听
Image(systemName: self.userData.todolist[i].isChecked ? "checkmark.square.fill" : "square")
.imageScale(.large) //在视图中缩放图像
.padding(.horizontal)
//在视图识别点击手势时执行的操作
.onTapGesture {
self.userData.check(i)//使用ToDo的方法修改
}
}
}
.frame(height:90) //定义矢量的框架值
.background(Color.white) //背影颜色
.cornerRadius(6) //圆角
.shadow(radius: 10,x:0,y:10 ) //阴影
.padding(.horizontal) //填充
.animation(.spring()) //弹簧动画效果
.transition(.slide) //过度效果
}
}
和视频作者写的代码差不多,只是文件结构会有一些不一样,根据个人习惯可以调整,经过测试编译后文件大小 800K左右,运行时内存30M左右,CPU占用偶尔跑到1%,代码没有做数据保护及静态修饰等。
最后非常感谢视频博主的贡献!!!推荐大家对照视频学习自己照着写一次代码,对入门有很大的帮助。
最后附上代码下载地址:http://www.55mx.com/data/attachment/2021/03/ToDo_SwiftUI.zip
除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/109
《【SwiftUI实战】事项管理(ToDo类)的APP(全中文详细注释,适合入门学习)》的网友评论(0)