iOS 开源一个简单的订餐 app UI 框架

前言

学 Swift 也有一段时间了,做了一些小的 demo,有兴趣的可以看我的100 Days of Swift。一直想做个完整的项目,发现这边学校的外卖订餐也逐渐流行起来,不像中国有那么多强大的外卖软件,美国也有,但不多,起码中国人对那些软件都不太熟知也不怎么用。打算专门针对午餐的外卖做个app,做了几天,只做出个 UI,看上去很小的软件,新手做起来感觉东西还是有点多。Swift 如何与后端交互之类的之后再慢慢学吧,有大神愿意在评论区给几个教程就更好了。数据库之类的我都挺熟悉,SQL 或者 MongoDB。

BTW, 想了解 MongoDB 的可以看我的这两篇文章-Part 1Part 2,我之前做了个完整的网站 demo,前后端都实现了,建于 Heroku,感觉挺酷的。

目录

在这个 app 中,所有 UI 都是用代码创建的,你可以在100 Days of Swift 看到,我之前练习的时候都是用的 storyboard,但是到了10页以上感觉 storyboard 就开始有点乱了,特别是那些 segue 的线牵得满屏幕都是的时候。之后我就开始用 SnapKit 做 UI 了,虽然比起 CSS 来,还是有点不方便,但用起来感觉还行。下面我大概罗列了一些实现的基本功能:

  • 引导页
  • 午餐菜单(tableView)
  • 购物车,动画
  • 下拉刷新
  • 自定义个人主页 (collectionView)
  • Reminder 和 Setting 需要后台,就用了 Alert 来简单响应了
  • 全屏右滑退出

具体代码请看我的 Github, 下面我就主要展示一下效果,稍微讲一下实现过程,代码中已有很多注释。

引导页

guide

引导页我是用 collectionView 做的,刚开始先判断要不要进入引导页,如果版本更新,则进入。collectionView 滑动方向设置为 .horizontal,设置任意数量的页数。添加一个启动的 startButton,设置前几页都为 startButton.isHidden = true,最后一页的时候显示出来,再添加一个渐出的显示动画。

菜单和购物车

shoppingCart

菜单可以下拉刷新,本打算自定义下拉刷新,就像 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,还挺好用的。

自定义个人主页

profile

本来打算做成简书那样,但是。。作为新手感觉还是有点难度。也是因为我这 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 大神的开源项目花田小憩。他造了很多小轮子,我有些是参考他的。站在巨人的肩膀上就是爽哈哈哈。

下面有几篇文章分享给大家,其实很多时候我们都可以从一个人说的话,写的字里看出一个人的水平。期待哪天自己也能写出高水平的文章。加油!


喜欢的请随意打赏,点点♥️ 和关注。 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容