75142913在线留言
【SwiftUI实战】事项管理(ToDo类)的APP(全中文详细注释,适合入门学习)_IOS开发_网络人

【SwiftUI实战】事项管理(ToDo类)的APP(全中文详细注释,适合入门学习)

Kwok 发表于:2021-03-26 09:26:16 点击:120 评论: 0

学习完了Swift后需要在其基础上学习SwiftUI才能开发出APP,swift是对数据的逻辑处理,而SwiftUI是对视图处理。目前主流的开发模式是MVVM(Model + View + ViewModel)。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

下面的代码是我按照视频:https://www.bilibili.com/video/BV1Kg4y1i7dd 学习理解后增加了详细的注释,在代码命名、功能、视图上有少许的不一样。经过测试是可以完全运行的且功能正常。

 一、界面如下:

1、主界面 

SwiftUI实战事项管理ToDo类的APP全中文详细注释适合入门学习

2、只显示已收藏的事项

SwiftUI实战事项管理ToDo类的APP全中文详细注释适合入门学习

3、编辑、增加事项

SwiftUI实战事项管理ToDo类的APP全中文详细注释适合入门学习

从功能和界面上来看是非常简单的,做为入门练习和理解开发还是很我帮助的。可以通过代码学习到数据绑定、布局处理、推送通知等。

二、代码文件

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) //过度效果
    }
}

三、Debug信息

和视频作者写的代码差不多,只是文件结构会有一些不一样,根据个人习惯可以调整,经过测试编译后文件大小 800K左右,运行时内存30M左右,CPU占用偶尔跑到1%,代码没有做数据保护及静态修饰等。

最后非常感谢视频博主的贡献!!!推荐大家对照视频学习自己照着写一次代码,对入门有很大的帮助。

最后附上代码下载地址:http://www.55mx.com/data/attachment/2021/03/ToDo_SwiftUI.zip

除非注明,网络人的文章均为原创,转载请以链接形式标明本文地址:https://www.55mx.com/post/109
标签:SwiftUI事项管理ToDoKwok最后编辑于:2021-04-01 09:01:40
0
感谢打赏!

《【SwiftUI实战】事项管理(ToDo类)的APP(全中文详细注释,适合入门学习)》的网友评论(0)

本站推荐阅读

热门点击文章