前言
最近帮朋友写了一个电商+旅游的小demo,虽然功能不是特别完善,但是从做完的页面中也学习到了很多知识,想将其进行记录与总结。
页面大概是这样的
ESTabBarController中标签视图的封装
在看树食时就不理解为什么每个页面的视图需要封装一个导航栏控制器,每次都只在SceneDelegate中将标签页进行封装,但是标签页面无法正确显示导航栏,只有从标签页面跳转至下个页面时,才能显示导航栏。
正确的视图结构应该是,每个主要模块都被封装在一个 UINavigationController
中。这样每个模块的导航逻辑都是独立的,可以分别处理自己的页面跳转,而不干扰其他模块。
let firstVC = UINavigationController(rootViewController: entertainVC)
let secondVC = UINavigationController(rootViewController: shopVC)
let thirdVC = UINavigationController(rootViewController: TravelViewController())
let fourthVC = UINavigationController(rootViewController: mineVC)
tabBarController.viewControllers = [firstVC, secondVC, thirdVC ,fourthVC]
视图间的数据传递
同一层次视图间的数据传递
使用闭包来逐层传递数据。
在SetVC中修改信息之后,返回MineVC需要更新显示的相关数据。
在setVC中
在MineVC中
不同层次视图间的数据传递
在ShopVC中点击商品,进入详细页面DetailVC,点击星标可以进行收藏,与此同时,MineVC中的收藏商品个数也需要重新获取。同理在GameVC中积分发生变化,也需要在MineVC中更新数据。
此处选用代理来解决,以更新收藏数量为例。
因为ESTabBarController在一开始就会实例化各个模块的页面,导致在DetailVC中无法获取到MineVC对象,在MineVC中也无法获取DetailVC,因为无论是模态调用还是导航栏push,在页面被调用前都不会进行实例化,因此只能将ShopVC的代理设置为MineVC,DetailVC与ShopVC在同一个视图层次,则使用闭包来进行数据传递。
//第一步:定义协议
protocol ShopViewControllerDelegate: AnyObject {
func shopdidUpdateCollect()
}
//第二步:让MineVC遵循协议,并且设置 协议的方法
extension MineViewController:ShopViewControllerDelegate{
func shopdidUpdateCollect() {
print("代理生效,更新收藏数目")
handleCollectNumUpdate()//重新读取数据操作
}
}
//第三步,在ShopViewController中设代理变量
weak var delegate: ShopViewControllerDelegate?
//第四步,在Launch方法中 配置代理
let data_info = DataManager.shared.fetchNewestUserInfo()
let mineVC = MineViewController(infoData:
DataTrans.shared.UserinfoToinfoModel(data: data_info))
let data = DataManager.shared.fetchAllItems()
let shopVC = ShoppingViewController(ItemData:
DataTrans.shared.ItemToShopModel(ItemArray: data!))
shopVC.delegate = mineVC
//第五步,在ShopVC中,点击事件发生时,调用代理的方法
@objc func didTapStarButton() {
print("点击了收藏按钮,通知 MineVC 更新")
updateMineCollectData()
}
func updateMineCollectData() {
print("到达 shop 页面,即将调用代理方法")
delegate?.shopdidUpdateCollect()
}
在MineViewController更新积分视图也是有讲究的!
如果需要更新的控件下方的 控件自动布局是依赖于pointView的,则不能直接将pointView从父视图移除
当 pointsView 被 removeFromSuperview() 调用移除时,其自动布局约束也会被自动移除。
因为 NSLayoutConstraint 约束是附加到 superview 上的,当 view 被移除后,superview 也会自动清理与之相关的约束。
正确做法,应该是获取到pointView中的label,修改text就行。
(之前的自己总是觉得.符号定位不到的控件就无法获取到,原来还可以这样获取!)
if let valueLabel = pointsView.subviews.first(where: { $0 is UIStackView })?.subviews.last as? UILabel {
valueLabel.text = String(newPoints)
}
UIScrollView滑动视图没有按照想要的方向进行滑动
在懒加载中明确指定显示垂直,隐藏水平滚动条,但还是显示为水平滑动,问了很多AI,停滞了很久。
lazy var scrollView:UIScrollView = {
let view = UIScrollView()
view.showsVerticalScrollIndicator = true // 显示垂直滚动条
view.showsHorizontalScrollIndicator = false // 隐藏水平滚动条
view.isScrollEnabled = true
return view
}()
后来发现,是界面顶部的imageView自动布局出现问题,因为scrollView本来就是通过计算子视图中的内容来确定高度宽度的,但是我将imageView的leading、trailing自动布局也依赖于scrollView,导致imageView最终呈现的是原始图片的宽度大小。
消消乐页面总结
点击进入游戏,通过判断是否有游戏记录,选择是否显示教学label
如果显示教学label,设一个类型为UIView?的全局变量teachingOverlay,将配置操作封装起来,根据teachingStep显示不同的文本,点击一次之后就将其从父视图移除,并且赋值为nil,再由teachingStep判断是否结束教学,没结束就再次调用配置操作。
private var teachingOverlay: UIView? // 教学覆盖视图
private func showTeachingOverlay() {
let overlay = UIView(frame: self.view.bounds)
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.8) // 半透明黑色背景
self.view.addSubview(overlay)
self.teachingOverlay = overlay
let teachLabel = UILabel()
teachLabel.textAlignment = .center
teachLabel.font = UIFont(name: "sucaijishikangkangti", size: 28)
teachLabel.textColor = .yellow
teachLabel.numberOfLines = 0
switch teachingStep {
case 0:
teachLabel.text = "点击相同物品即可进行消除\n消除不同等级的物品\n
获得不同的熟练度\n\n要想获得积分\n就必须累计熟练度升级!"
case 1:
teachLabel.text = "下方的更新按钮\n每五分钟获得一次机会\n
每次机会随机获取五个物品!"
default:
break
}
overlay.addSubview(teachLabel)
// 布局教学文字
teachLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
teachLabel.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
teachLabel.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
])
// 添加点击手势
let tapGesture = UITapGestureRecognizer(target: self,
action:#selector(handleTeachingTap))
overlay.addGestureRecognizer(tapGesture)
}
// 处理教学视图点击事件
@objc private func handleTeachingTap() {
teachingOverlay?.removeFromSuperview() // 移除当前教学视图
teachingOverlay = nil
teachingStep += 1
if teachingStep < 2 {
showTeachingOverlay()//再次显示教学视图
} else {
startCountdown()//开始倒计时
}
}
消消乐单元格是使用UICollectionView实现排布的
使用二维数组products来存储每个格子的情况(可能为nil,因此要设置为可选),进行增删操作,只用修改数组,再collectionView.reloadData()即可;
一维数组selectedCells存放点击过的单元格的indexPath,在点击代理方法中,判断selectedCells数组的元素个数来判断是否需要进行消除操作。
- 点击单元格时有动画效果和重点显示,使页面更加丰富
- 第一次进入页面,会在单元格放置随机元素,使用map生成坐标,shuffle打乱,随机数实现放置不同等级的商品。点击更新按钮,则会在空的单元格放置。
进行一次游戏之后,退出页面需要将单元格的信息以及游戏信息保存至数据库。
添加一个Product实体,有name,row,col字段存放有物品的单元格。
添加一个GameInfo实体,有exitTime,remainedTime,magicNum,level,proficiency字段,前三个字段是为了,让用户离开页面时,更新的倒计时仍在计时,并且添加更新次数,直至达到上限。
- 在ViewDidDisappear()方法中添加记录,只在离开页面时,而不是一有改变就添加记录。
- 在进入页面时,获取数据库的数据,更新Products数组与其他信息。
- 获取数据库时要获取最新的数据,使用fetchRequest对exitTime字段排序。
- 更新次数与倒计时剩下的时间,其实是读取到数据之后,用当前进入页面的时间去与之作差再计算。
CoreData数据模型
数据模型中有多个实体,一个实体可以有多个实例。
在获取到数据之后,直接对数据进行修改操作会比较麻烦,将其转化为 相同的、自己设置的类型,在转换的基础上对数据进行修改操作,再更新数据库。
例如在个人页面中,创建的是一个UserInfo实体,有name,email,image,balance,points,updatedAt字段,在获取完数据之后,就进行类型转化,对数据的操作都是基于转换后的数据。
这样的好处是:
- 保证数据的安全性,避免直接修改数据库数据而导致不可预料的错误。
- 数据库实体和应用层的数据模型(转化后的类型)分离,数据库设计和业务逻辑独立。
- 所有的修改都是在转换后的数据上完成,只有在提交更新时才写入数据库,减少并发修改时的冲突,并且可以先进行对比,看数据是否发生变化,来决定是否更新数据库,减少不必要的写入操作
//DataManager.swift
func addUserInfo(with info:infoModel)->Userinfo{
let context = persistentContainer.viewContext
let newInfo = Userinfo(context:context)
newInfo.name = info.name
newInfo.email = info.email
newInfo.balance = info.balance
newInfo.points = info.points
newInfo.image = info.image
newInfo.updatedAt = Date()
self.saveContext()
print("信息添加成功")//debug
return newInfo
}
func fetchNewestUserInfo()->Userinfo{
let fetchRequest: NSFetchRequest<Userinfo> = Userinfo.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false)]
let datas = fetch(fetchRequest: fetchRequest)
return (datas?.first)!
}
func fetch<T: NSFetchRequestResult>(fetchRequest: NSFetchRequest<T>) -> [T]? {
let context = persistentContainer.viewContext
do {
return try context.fetch(fetchRequest)
} catch {
print("Failed to fetch: \(error)")
return nil
}
}
//自己创建的类型
struct infoModel{
var name:String
var email:String
var balance:Double
var points:Int16
var image:String
}
//DataTrans.swift中
func UserinfoToinfoModel(data:Userinfo)->infoModel{
return infoModel(name: data.name!, email: data.email!,
balance: data.balance, points: data.points, image: data.image!)
}
Core Data的Relationships
Relationship:关系名称
Destination(目标实体):当前关系所指向的实体,即目标对象
Inverse(反向关系):目标实体回指到当前实体的关系,用于保持Core Data的数据一致性,确保关系的双向性
例如有两个实体:1.Person 2.Dog
假设每一个人都有一只狗,则在Person实体中创建一个dog关系,Destination设置为Dog。
每一只狗也应该知道它属于哪个Person,因此可以在Dog实体中定义owner关系,并设置inverse为Person实体的dog关系。即目标实体指向Person,目标实体则可以通过dog关系回到当前Dog实体,定位是哪一个狗。
以此项目中的商品Item实体与购物车记录Cart实体来分析,Cart实体记录 加入购物车的商品以及个数,那么只需要添加数量Amount字段就足够,商品就可以使用关系 来获得。
在Cart实体中,建立cartItem关系(To One,一个记录指向一个Item),指向Item,inverse为cartRecord(商品可以通过此关系回到Cart实体定位加入的是哪一个购物车)
在Item实体中,建立cartRecord关系(To One,因为只设置了一个购物车),指向Cart,inverse为cartItem(购物车可以通过此关系回到Item实体定位是哪一个商品)
关系的使用:Cart类型的变量使用 .cartItem 就可以获取到 对应的Item类型的变量
总结一些基础常用的操作
1,UIColor的十六进制拓展
extension UIColor {
// 通过十六进制创建颜色
static func hexColor(hexValue: Int, alpha: CGFloat = 1.0) -> UIColor {
let red = CGFloat((hexValue >> 16) & 0xFF) / 255.0
let green = CGFloat((hexValue >> 8) & 0xFF) / 255.0
let blue = CGFloat(hexValue & 0xFF) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
2,UIButton的部分圆角
extension UIView{//部分圆角
func corner(byRoundingCorners corners: UIRectCorner, radii: CGFloat)
{
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radii, height: radii))
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = maskPath.cgPath
layer.mask = maskLayer
}
}
3,UIAlterController的显示
lazy var emailAlter:UIAlertController = {
let alter = UIAlertController(title: "更改邮箱",
message: nil, preferredStyle: .alert)
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.keyboardType = .default
alter.addTextField { (textField) in
textField.placeholder = "请输入新的邮箱"
}
let yesAction = UIAlertAction(title: "确定", style: .default)
{ [self](_) in
if let userInput = alter.textFields?.first?.text {
print("用户输入的内容是:\(userInput)")
//....数据更新操作
}
}
let noAction = UIAlertAction(title: "取消", style: .cancel)
alter.addAction(yesAction)
alter.addAction(noAction)
return alter
}()
4,控件从页面外滑动至页面内
private func setupUpdateNews() {
view.addSubview(updateNews)
// 设置初始位置(屏幕顶部之外)
updateNews.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
updateNews.topAnchor.constraint(equalTo: view.topAnchor, constant: -50),
updateNews.centerXAnchor.constraint(equalTo: view.centerXAnchor),
updateNews.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
])
}
func showUpdateNews(withText text: String) {
updateNews.text = text
UIView.animate(withDuration: 0.5, animations: {
//滑动到屏幕内
self.updateNews.transform = CGAffineTransform(translationX: 0, y: 120)
}) { _ in
// 停留 2 秒后滑出
DispatchQueue.main.asyncAfter(deadline: .now()+1.4) {
UIView.animate(withDuration: 1.0, animations: {
self.updateNews.transform = .identity // 恢复到初始位置
})
}
}
}
5,拓展UIButton实现点击动画
extension UIButton{
func animateButton(_ button: UIButton) {
UIView.animate(withDuration: 0.1, animations: {
button.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
}) { _ in
UIView.animate(withDuration: 0.1) {
button.transform = .identity
}
}
}
}
6,stackView
之前都没有怎么用过,但是发现能够自动排布就很方便,自动拥有自动布局式的
像个人页面中的余额和积分的显示,就是使用的stackView来管理,将两个label放在一起。
private func createInfoCard(title: String, value: String, color: UIColor) -> UIView {
let card = UIView()
card.backgroundColor = (title=="余额" ? UIColor.EnterTain_red : UIColor.EnterTain_pink)
card.layer.cornerRadius = 16
card.layer.shadowColor = UIColor.black.cgColor
card.layer.shadowOpacity = 0.1
card.layer.shadowOffset = CGSize(width: 0, height: 4)
card.layer.shadowRadius = 12
let titleLabel = UILabel()
let valueLabel = UILabel()//省略label的配置
let stack = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
stack.axis = .vertical
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
card.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: card.centerYAnchor),
stack.centerXAnchor.constraint(equalTo:card.centerXAnchor)
])
return card
}
7,CollectionView带header的配置
一直没有弄明白过.....
lazy var collectionView:UICollectionView = {
let layout = createCompositionalLayout()
collectionView = UICollectionView(frame:.zero, collectionViewLayout: layout)
collectionView.backgroundColor = .clear
collectionView.register(InputCell.self, forCellWithReuseIdentifier: "InputCell")
collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
collectionView.dataSource = self
collectionView.delegate = self
return collectionView
}()
enum SectionType: CaseIterable {
case personalInfo
case helpCenter
}
private func createCompositionalLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
let sectionType = SectionType.allCases[sectionIndex]
//先是配置item,定义单元格,必须被group包含
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(sectionType == .personalInfo ? 80 : 70)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
//再是group,group 是 UICollectionView 直接管理的单位
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(100)
)
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(sectionType == .personalInfo ? 20 : 10)
//再是section,由 group 组成的集合
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 15
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 30, trailing: 0)
//最后是header
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(40)
)
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [header]
return section
}
return layout
}
总结!
1,对数据的传递以及数据库的操作又更深入了一些
2,也会使用AI工具来辅助自己的UI设计
3,其实大部分都是自己花了一周的时间就写完了,寒假的时候拖拖延延,总觉得写起来很难,但是发现,一步一步写起来,结构与目标就会逐渐明了,到最后发现其实没有很难,所以不用害怕,先行动起来,在行动中去一步一步明确目标与解决问题!
总之,自己还有很多需要学习的地方,这个项目的总结就到此为止啦!写idea去了!