前言
学 Swift 也有一段时间了,做了一些小的 demo,有兴趣的可以看我的100 Days of Swift。一直想做个完整的项目,发现这边学校的外卖订餐也逐渐流行起来,不像中国有那么多强大的外卖软件,美国也有,但不多,起码中国人对那些软件都不太熟知也不怎么用。打算专门针对午餐的外卖做个app,做了几天,只做出个 UI,看上去很小的软件,新手做起来感觉东西还是有点多。Swift 如何与后端交互之类的之后再慢慢学吧,有大神愿意在评论区给几个教程就更好了。数据库之类的我都挺熟悉,SQL 或者 MongoDB。
BTW, 想了解 MongoDB 的可以看我的这两篇文章-Part 1,Part 2,我之前做了个完整的网站 demo,前后端都实现了,建于 Heroku,感觉挺酷的。
目录
在这个 app 中,所有 UI 都是用代码创建的,你可以在100 Days of Swift 看到,我之前练习的时候都是用的 storyboard,但是到了10页以上感觉 storyboard 就开始有点乱了,特别是那些 segue 的线牵得满屏幕都是的时候。之后我就开始用 SnapKit 做 UI 了,虽然比起 CSS 来,还是有点不方便,但用起来感觉还行。下面我大概罗列了一些实现的基本功能:
- 引导页
- 午餐菜单(tableView)
- 购物车,动画
- 下拉刷新
- 自定义个人主页 (collectionView)
- Reminder 和 Setting 需要后台,就用了 Alert 来简单响应了
- 全屏右滑退出
具体代码请看我的 Github, 下面我就主要展示一下效果,稍微讲一下实现过程,代码中已有很多注释。
引导页
引导页我是用 collectionView 做的,刚开始先判断要不要进入引导页,如果版本更新,则进入。collectionView 滑动方向设置为 .horizontal
,设置任意数量的页数。添加一个启动的 startButton
,设置前几页都为 startButton.isHidden = true
,最后一页的时候显示出来,再添加一个渐出的显示动画。
菜单和购物车
菜单可以下拉刷新,本打算自定义下拉刷新,就像 ALin 的项目中那样,但是好像有点问题,我就用了自带的 UIRefreshControl
,下拉的时候显示刷新的时间,稍微调整了下时间的 format。代码很简单
let dateString = DateFormatter.localizedString(from: NSDate() as Date, dateStyle: .medium, timeStyle: .short)
self.refreshControl.attributedTitle = NSAttributedString(string: "Last updated on \(dateString)", attributes: attributes)
self.refreshControl.tintColor = UIColor.white
然后做了个购物车的动画,将菜单里的图片先放大后缩小“抛入”购物车,其实是沿着 UIBezierPath
走的一个路径,这段动画完了之后,在 animationDidStop()
里做购物车图片的抖动,和显示购买的物品数量,那个 countLabel 是个长宽都为 15 的在购物车图片右上角的 UILabel()
。
先实现一个回调方法,当点击了cell上的购买按钮后触发
func menuListCell(_ cell: MenuListCell, foodImageView: UIImageView)
{
guard let indexPath = tableView.indexPath(for: cell) else { return }
// retrieve the current food model, add it to shopping cart model
let model = foodArray[indexPath.section][indexPath.row]
addFoodArray.append(model)
// recalculate the frame of imageView, start animation
var rect = tableView.rectForRow(at: indexPath)
rect.origin.y -= tableView.contentOffset.y
var headRect = foodImageView.frame
headRect.origin.y = rect.origin.y + headRect.origin.y - 64
startAnimation(headRect, foodImageView: foodImageView)
}
这是点击购买之后的动画实现:
fileprivate func startAnimation(_ rect: CGRect, foodImageView: UIImageView)
{
if layer == nil {
layer = CALayer()
layer?.contents = foodImageView.layer.contents
layer?.contentsGravity = kCAGravityResizeAspectFill
layer?.bounds = rect
layer?.cornerRadius = layer!.bounds.height * 0.5
layer?.masksToBounds = true
layer?.position = CGPoint(x: foodImageView.center.x, y: rect.minY + 96)
KeyWindow.layer.addSublayer(layer!)
// animation path
path = UIBezierPath()
path!.move(to: layer!.position)
path!.addQuadCurve(to: CGPoint(x:SCREEN_WIDTH - 25, y: 35), controlPoint: CGPoint(x: SCREEN_WIDTH * 0.5, y: rect.origin.y - 80))
}
groupAnimation()
}
这是放大,缩小,抛入购物车的组动画
// start group animation: throw, larger, smaller image
fileprivate func groupAnimation()
{
tableView.isUserInteractionEnabled = false
// move path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path!.cgPath
animation.rotationMode = kCAAnimationRotateAuto
// larger image
let bigAnimation = CABasicAnimation(keyPath: "transform.scale")
bigAnimation.duration = 0.5
bigAnimation.fromValue = 1
bigAnimation.toValue = 2
bigAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
// smaller image
let smallAnimation = CABasicAnimation(keyPath: "transform.scale")
smallAnimation.beginTime = 0.5
smallAnimation.duration = 1
smallAnimation.fromValue = 2
smallAnimation.toValue = 0.5
smallAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
// group animation
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [animation, bigAnimation, smallAnimation]
groupAnimation.duration = 1.5
groupAnimation.isRemovedOnCompletion = false
groupAnimation.fillMode = kCAFillModeForwards
groupAnimation.delegate = self
layer?.add(groupAnimation, forKey: "groupAnimation")
}
组动画结束后的一些动画效果。
// end image animation, start other animations
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
{
if anim == layer?.animation(forKey: "groupAnimation")
{
// start user interaction
tableView.isUserInteractionEnabled = true
// hide layer
layer?.removeAllAnimations()
layer?.removeFromSuperlayer()
layer = nil
// if user buy any food, show the count label
if self.addFoodArray.count > 0 {
addCountLabel.isHidden = false
}
// show the count label
let goodCountAnimation = CATransition()
goodCountAnimation.duration = 0.25
addCountLabel.text = "\(self.addFoodArray.count)"
addCountLabel.layer.add(goodCountAnimation, forKey: nil)
// shopping cart shaking
let cartAnimation = CABasicAnimation(keyPath: "transform.translation.y")
cartAnimation.duration = 0.25
cartAnimation.fromValue = -5
cartAnimation.toValue = 5
cartAnimation.autoreverses = true
cartButton.layer.add(cartAnimation, forKey: nil)
}
}
购物车里面可以增加/减少购买数量,总价跟着会动态变动。主要是有用到了两个东西,一个是 selected
变量,一个是 reCalculateCount()
函数。根据 selected
来决定最后的总价,如果有变动,则重新计算 (reCalculateCount)。
fileprivate func reCalculateCount()
{
for model in addFoodArray! {
if model.selected == true {
price += Float(model.count) * (model.vipPrice! as NSString).floatValue
}
}
// assign price
let attributeText = NSMutableAttributedString(string: "Subtotal: \(self.price)")
attributeText.setAttributes([NSForegroundColorAttributeName: UIColor.red], range: NSMakeRange(5, attributeText.length - 5))
totalPriceLabel.attributedText = attributeText
price = 0
tableView.reloadData()
}
没有实现 Pay()
功能。打算之后尝试 Apple Pay,之前用惯了支付宝,刚来美国的时候很难受,其实很多地方中国都已经比美国好很多了。还好现在有了 Apple Pay,还挺好用的。
自定义个人主页
本来打算做成简书那样,但是。。作为新手感觉还是有点难度。也是因为我这 app 里没有必要实现那些,就没仔细研究。
如前面提到的这页用的 collectionView,两个 section,一个是 UserCollectionViewCell
, 下面是 HistoryCollectionViewCell
。 下面这个 section 像一个 table 的 section,有一个会自动悬浮的 header,这 header 用的是 ALin 大神的轮子,LevitateHeaderFlowLayout()
,当然这个文件的 copyright 是用他的名字的。
class CollectionViewFlowLayout: LevitateHeaderFlowLayout {
override func prepare() {
super.prepare()
collectionView?.alwaysBounceVertical = true
scrollDirection = .vertical
minimumLineSpacing = 5
minimumInteritemSpacing = 0
}
}
这项目总体来说应该算很小的,如果后端也实现了,也算一个蛮完整的小项目了吧。
最后
特别感谢 ALin 大神的开源项目花田小憩。他造了很多小轮子,我有些是参考他的。站在巨人的肩膀上就是爽哈哈哈。
下面有几篇文章分享给大家,其实很多时候我们都可以从一个人说的话,写的字里看出一个人的水平。期待哪天自己也能写出高水平的文章。加油!
- 讲 tableview 的代码布局:Refactoring table view data source and delegate methods
- 对我帮助超级大的 ALin 大神的开源项目:iOS高仿:花田小憩3.0.1
- 博主对架构与框架的看法挺有意思:架构(Architecture)和框架(Framework)杂谈
- ALin 大神的悬浮 header 也是从这里来的。
喜欢的请随意打赏,点点♥️ 和关注。 :)