iOS高仿爱鲜蜂

作者: Top_熊 | 来源:发表于2016-02-04 13:07 被阅读28948次

    iOS高仿爱鲜蜂

    前言

    2015年匆匆的就过去了,又老了一岁,这一年起起伏伏,有笑声也有眼泪,感谢陪伴在我身边的人.

    关于项目(代码下载地址在文章最下面点击GitHub链接)

    本次开源项目为爱鲜蜂,一款电商APP,使用语言Swift2.0,开发工具Xcode7.0.1.

    项目为纯代码开发,没有使用XIB和StoryBoard.开发周期大概为2个月左右(工作闲暇之余).

    数据都是本地数据,辅助开发软件:PhotoShop CS6(图片处理),Charles(抓包工具).

    写的比较匆忙,很多地方无法尽善尽美,如果有建议和可优化的地方可在文章底部留言,我会一一查看的并回复的.

    项目效果图

    效果图1 效果图2 效果图3 效果图4 效果图5 效果图6 效果图7 效果图8 效果图9

    项目详细讲解(根据启动流程)

    引导页和AD(广告)页

    当程序被打开时,在创建KeyWindow的RootViewController时判断是否是首次登陆,这里的逻辑是如果用户是首次打开应用的话显示引导页,当点击引导页最后一页的立即体验直接进入TabBarController,不显示广告页(效果如下图)

    引导页

    如果用户不是首次打开应用的话,则显示广告页,并且在4秒后以放大并且透明的效果进入TabBarController(效果如下图)

    广告页

    逻辑代码如下

        // MARK: - Public Method
        private func buildKeyWindow() {
            
            window = UIWindow(frame: ScreenBounds)
            window!.makeKeyAndVisible()
            
            let isFristOpen = NSUserDefaults.standardUserDefaults().objectForKey("isFristOpenApp")
            
            if isFristOpen == nil {
                window?.rootViewController = GuideViewController()
                NSUserDefaults.standardUserDefaults().setObject("isFristOpenApp", forKey: "isFristOpenApp")
            } else {
                loadADRootViewController()
            }
        }
    

    引导页考虑到循环利用,使用的是UICollectionView实现

    AD(广告)页需要注意的是ADViewController有时会存在加载广告图片失败的情况,如果加载失败,发送加载图片失败的通知,window直接将tarBarController作为keyWindow即可

    关于引导页和AD页的实现代码,我就不详细讲述了,源代码都有,有兴趣的读者可以打开代码自行研究

    AnimationTabBarController(带有动画的TabBarItem)

    这里有个小故事,我是无意中发现爱鲜蜂底部TabBarItem有点击的动画,感觉挺有意思的,尝试着自己实现了同样的功能,然后才突发奇将后续的功能都给实现了.先看下底部UITabBarItem的动画(效果如下图)

    TaBarItem动画效果

    底部的TabBarItem动画使用了三方框架RAMAnimatedTabBar,由于原来的框架 只能通过StoryBoard初始化控件,并且无法满足项目需求,所以对框架进行了大量修改,其原理很简单,就是在TabBarController初始化时,通过拦截Items,重新创建一套相同的View,并且在每个View上添加ImageView和Label,在View的点击事件中,控制动画即可.用这种方式也可以轻松完成一些看似复杂的动画,如下图所示,其实通过ImageView的序列动画就可以轻松完成,只是需要在AE中做出动画序列图即可.

    通过序列动画达到的效果

    首页

    首页由三部分构成,顶部的轮播图(PageScrollView),轮播图下面的活动按钮,以及UICollectionView.(如下图所示)

    首页效果图

    其中将PageScrollView与活动按钮封装成了UICollectionView的headView,通过代理方法将点击的事件,需要注意的是活动按钮并不是固定只有四个,这里是根据服务器返回的数据创建的,有时候会有多个,需要根据数量控制列数,通过代理方法将高度传给首页的控制器.

    PageScrollView(轮播图)

    这里采用循环利用机制写的,只创建3个ImageView即可,在同一时刻屏幕中最多只会显示2个ImageView,当需要展示新的ImageView,只需要将缓存数组中的没有展示的ImageView拿出来展示即可,这里为了方便大家将项目移植到自己的项目中使用,我将代码全部拷贝过来了,需要替换数据修改headData内的数据

    import UIKit
    
    class PageScrollView: UIView {
        
        private let imageViewMaxCount = 3
        private var imageScrollView: UIScrollView!
        private var pageControl: UIPageControl!
        private var timer: NSTimer?
        private var placeholderImage: UIImage?
        private var imageClick:((index: Int) -> ())?
        var headData: HeadResources? {
            didSet {
                
                if timer != nil {
                    timer!.invalidate()
                    timer = nil
                }
                
                if headData?.data?.focus?.count >= 0 {
                    pageControl.numberOfPages = (headData?.data?.focus?.count)!
                    pageControl.currentPage = 0
                    updatePageScrollView()
                    
                    startTimer()
                }
            }
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            
            buildImageScrollView()
            
            buildPageControl()
            
        }
        
        convenience init(frame: CGRect, placeholder: UIImage, focusImageViewClick:((index: Int) -> Void)) {
            self.init(frame: frame)
            placeholderImage = placeholder
            imageClick = focusImageViewClick
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            imageScrollView.frame = bounds
            imageScrollView.contentSize = CGSizeMake(CGFloat(imageViewMaxCount) * width, 0)
            for i in 0...imageViewMaxCount - 1 {
                let imageView = imageScrollView.subviews[i] as! UIImageView
                imageView.userInteractionEnabled = true
                imageView.frame = CGRectMake(CGFloat(i) * imageScrollView.width, 0, imageScrollView.width, imageScrollView.height)
            }
            
            let pageW: CGFloat = 80
            let pageH: CGFloat = 20
            let pageX: CGFloat = imageScrollView.width - pageW
            let pageY: CGFloat = imageScrollView.height - pageH
            pageControl.frame = CGRectMake(pageX, pageY, pageW, pageH)
            
            updatePageScrollView()
        }
        
        // MARK: BuildUI
        private func buildImageScrollView() {
            imageScrollView = UIScrollView()
            imageScrollView.bounces = false
            imageScrollView.showsHorizontalScrollIndicator = false
            imageScrollView.showsVerticalScrollIndicator = false
            imageScrollView.pagingEnabled = true
            imageScrollView.delegate = self
            addSubview(imageScrollView)
            
            for _ in 0..<3 {
                let imageView = UIImageView()
                let tap = UITapGestureRecognizer(target: self, action: "imageViewClick:")
                imageView.addGestureRecognizer(tap)
                imageScrollView.addSubview(imageView)
            }
        }
        
        private func buildPageControl() {
            pageControl = UIPageControl()
            pageControl.hidesForSinglePage = true
            pageControl.pageIndicatorTintColor = UIColor(patternImage: UIImage(named: "v2_home_cycle_dot_normal")!)
            pageControl.currentPageIndicatorTintColor = UIColor(patternImage: UIImage(named: "v2_home_cycle_dot_selected")!)
            addSubview(pageControl)
        }
        
        //MARK: 更新内容
        private func updatePageScrollView() {
            for var i = 0; i < imageScrollView.subviews.count; i++ {    
                let imageView = imageScrollView.subviews[i] as! UIImageView
                var index = pageControl.currentPage
                
                if i == 0 {
                    index--
                } else if 2 == i {
                    index++
                }
                
                if index < 0 {
                    index = self.pageControl.numberOfPages - 1
                } else if index >= pageControl.numberOfPages {
                    index = 0
                }
                
                imageView.tag = index
                if headData?.data?.focus?.count > 0 {
                    imageView.sd_setImageWithURL(NSURL(string: headData!.data!.focus![index].img!), placeholderImage: placeholderImage)
                }
            }
            
            imageScrollView.contentOffset = CGPointMake(imageScrollView.width, 0)
        }
        
    
        // MARK: Timer
        private func startTimer() {
            timer = NSTimer(timeInterval: 3.0, target: self, selector: "next", userInfo: nil, repeats: true)
            NSRunLoop.mainRunLoop().addTimer(timer!, forMode: NSRunLoopCommonModes)
        }
        
        private func stopTimer() {
            timer?.invalidate()
            timer = nil
        }
        
        func next() {
            imageScrollView.setContentOffset(CGPointMake(2.0 * imageScrollView.frame.size.width, 0), animated: true)
        }
        
        // MARK: ACTION
        func imageViewClick(tap: UITapGestureRecognizer) {
            if imageClick != nil {
                imageClick!(index: tap.view!.tag)
            }
        }
    }
    
    // MARK:- UIScrollViewDelegate
    extension PageScrollView: UIScrollViewDelegate {
        
        func scrollViewDidScroll(scrollView: UIScrollView) {
            var page: Int = 0
            var minDistance: CGFloat = CGFloat(MAXFLOAT)
            for i in 0..<imageScrollView.subviews.count {
                let imageView = imageScrollView.subviews[i] as! UIImageView
                let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
                
                if distance < minDistance {
                    minDistance = distance
                    page = imageView.tag
                }
            }
            pageControl.currentPage = page
        }
        
        func scrollViewWillBeginDragging(scrollView: UIScrollView) {
            stopTimer()
        }
        
        func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
            startTimer()
        }
        
        func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
            updatePageScrollView()
        }
        
        func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
            updatePageScrollView()
        }
    }
    
    

    首页UICollectionView

    首页的CollectionView采用了两种Cell,一种是只有ImageView的Cell,一种是商品的Cell(如图所示)

    首页Cell样式一 首页Cell样式二

    通过判断indexPath.section展示对应Cell即可.

    新Cell出现的停靠动画,如图

    停靠动画

    通过实现UICollectionViewDelegate,在willDisplayCell代理方法完成动画,代码如下

        func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
            
            if indexPath.section == 0 && (indexPath.row == 0 || indexPath.row == 1) {
                return
            }
            
            if isAnimation {
                startAnimation(cell, offsetY: 80, duration: 1.0)
            }
        }
        
        private func startAnimation(view: UIView, offsetY: CGFloat, duration: NSTimeInterval) {
            
            view.transform = CGAffineTransformMakeTranslation(0, offsetY)
            
            UIView.animateWithDuration(duration, animations: { () -> Void in
                view.transform = CGAffineTransformIdentity
            })
        }
    

    添加商品动画

    当用户点击加号时,会出现如下如所示动画

    添加商品到购物车动画效果

    添加商品到购物车基于CoreAnimation(核心动画)实现,通过对ImageView的layer添加缩放,透明度以及路径动画实现.代码如下

    import UIKit
    
    class AnimationViewController: BaseViewController {
        
        var animationLayers: [CALayer]?
        
        var animationBigLayers: [CALayer]?
        
        // MARK: 商品添加到购物车动画
        func addProductsAnimation(imageView: UIImageView) {
            
            if (self.animationLayers == nil)
            {
                self.animationLayers = [CALayer]();
            }
            
            let frame = imageView.convertRect(imageView.bounds, toView: view)
            let transitionLayer = CALayer()
            transitionLayer.frame = frame
            transitionLayer.contents = imageView.layer.contents
            self.view.layer.addSublayer(transitionLayer)
            self.animationLayers?.append(transitionLayer)
            
            let p1 = transitionLayer.position;
            let p3 = CGPointMake(view.width - view.width / 4 - view.width / 8 - 6, self.view.layer.bounds.size.height - 40);
            
            let positionAnimation = CAKeyframeAnimation(keyPath: "position")
            let path = CGPathCreateMutable();
            CGPathMoveToPoint(path, nil, p1.x, p1.y);
            CGPathAddCurveToPoint(path, nil, p1.x, p1.y - 30, p3.x, p1.y - 30, p3.x, p3.y);
            positionAnimation.path = path;
            
            let opacityAnimation = CABasicAnimation(keyPath: "opacity")
            opacityAnimation.fromValue = 1
            opacityAnimation.toValue = 0.9
            opacityAnimation.fillMode = kCAFillModeForwards
            opacityAnimation.removedOnCompletion = true
            
            let transformAnimation = CABasicAnimation(keyPath: "transform")
            transformAnimation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
            transformAnimation.toValue = NSValue(CATransform3D: CATransform3DScale(CATransform3DIdentity, 0.2, 0.2, 1))
            
            let groupAnimation = CAAnimationGroup()
            groupAnimation.animations = [positionAnimation, transformAnimation, opacityAnimation];
            groupAnimation.duration = 0.8
            groupAnimation.delegate = self;
            
            transitionLayer.addAnimation(groupAnimation, forKey: "cartParabola")
        }
        
        override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
            
            if self.animationLayers?.count > 0 {
                let transitionLayer = animationLayers![0]
                transitionLayer.hidden = true
                transitionLayer.removeFromSuperlayer()
                animationLayers?.removeFirst()
                view.layer.removeAnimationForKey("cartParabola")
            }
        }
    }
    

    闪电超市

    闪电超市很明显由有2个TableView构成,如图所示

    闪电超市效果图

    这里采用了2个控制器分别管理各自的TableView,将TableView2添加到VC2上,将VC2.view添加到VC1.view上,然后再通过VC1.addChildViewController将VC2添加到VC1的子控制器中,这样既降低了代码的复杂性,有提升了代码维护性,各自管理各自的TableView.

    很多联动的操作都是通过UITableViewDelegate中实现的,有兴趣的同学可参照代码自行研究.

    购物车

    购物车采用了modal的形式出现.样式上有两种情况,当购物车里没有商品时,购物车显示为空(如下图)

    当购物车为空

    当购物车中有商品时候,显示商品信息(如下图)

    有商品时的购物车

    这里封装了一个UserShopCar单利类,专门用来管理用户购物车,保存用户添加到购物车的商品种类,商品总数,商品价格等,在购物车VC将要出现时候,判断购物车是否为空,如果为空则显示去逛逛,如果不为空则显示商品的信息.内部细节请参考代码

    购物车上红色圆圈

    购物车红色圆圈也是通过单利类来实现的,内部提供俩个方法,添加商品和移除商品,方法内部包含动画效果,当添加商品或者减掉商品时,调用对象对应的方法即可.

    我的

    先看下效果

    我的效果图

    我的页面是项目中最为复杂的页面,包含了许多效果.我大体讲一下思路,我的页面是由顶部的View以及一个TableView构成,TableView有一个headView,分别是我的订单,优惠劵以及我的消息,通过闭包的回调完成点击的事件.这个页面比较简单,不过多叙述了.

    右上角设置按钮

    设置效果图

    设置页面没有使用TableView,单纯的用View搭建的.

    清理缓存这稍微说一下吧,同样也封装了一个工具类,提供四个方法,分别是参看单个文件的大小,查看全部文件大小文件大小,同步将文件夹清除以及异步清除文件夹.有需要的同学可以直接copy走,那去使用,path是文件的路径

    import UIKit
    
    class FileTool: NSObject {
        
        static let fileManager = NSFileManager.defaultManager()
        
        /// 计算单个文件的大小
        class func fileSize(path: String) -> Double {
            
            if fileManager.fileExistsAtPath(path) {
                var dict = try? fileManager.attributesOfItemAtPath(path)
                if let fileSize = dict![NSFileSize] as? Int{
                    return Double(fileSize) / 1024.0 / 1024.0
                }
            }
            
            return 0.0
        }
        
        /// 计算整个文件夹的大小
        class func folderSize(path: String) -> Double {
            var folderSize: Double = 0
            if fileManager.fileExistsAtPath(path) {
                let chilerFiles = fileManager.subpathsAtPath(path)
                for fileName in chilerFiles! {
                    let tmpPath = path as NSString
                    let fileFullPathName = tmpPath.stringByAppendingPathComponent(fileName)
                    folderSize += FileTool.fileSize(fileFullPathName)
                }
                return folderSize
            }
            return 0
        }
        
        /// 清除文件 同步
        class func cleanFolder(path: String, complete:(str: String) -> ()) {
            var str: String?
            let chilerFiles = self.fileManager.subpathsAtPath(path)
            for fileName in chilerFiles! {
                let tmpPath = path as NSString
                let fileFullPathName = tmpPath.stringByAppendingPathComponent(fileName)
                if self.fileManager.fileExistsAtPath(fileFullPathName) {
                    do {
                        try self.fileManager.removeItemAtPath(fileFullPathName)
                        str = "清理成功"
                    } catch _ {
                        str = "清理失败"
                    }
                }
            }
            
            complete(str: str!)
        }
        
        /// 清除文件 异步
        class func cleanFolderAsync(path: String, complete:(str: String) -> ()) {
            var str: String?
            let queue = dispatch_queue_create("cleanQueue", nil)
            dispatch_async(queue) { () -> Void in
                let chilerFiles = self.fileManager.subpathsAtPath(path)
                for fileName in chilerFiles! {
                    let tmpPath = path as NSString
                    let fileFullPathName = tmpPath.stringByAppendingPathComponent(fileName)
                    if self.fileManager.fileExistsAtPath(fileFullPathName) {
                        do {
                            try self.fileManager.removeItemAtPath(fileFullPathName)
                            str = "清理成功"
                        } catch _ {
                            str = "清理失败"
                        }
                    }
                }
                
                complete(str: str!)
            }
        }
    }
    

    我的订单

    我的订单由2个控制器构成,分别是MyOrderViewController(我的订单)和OrderViewDetailViewController(订单详情)

    我的订单效果
    我的订单

    一个TableView搞定,cell的样式也并不复杂,这里有个小细节就是需要判断订单商品种类的个数,如果是4个以上,只显示五张图片,并且第五张图片以...图片显示,在设置cell的model时,判断商品种类个数即可实现.发福利等按钮是根据服务器返回的数据创建的,不同类型的Button会有不同的Type,type的值为int类型的,这里可以将button的tag设置对应的typr,这点击的时候判断button的tag,通过Swith语法执行对应的操作就可以了.

    订单详情页

    订单详情页有两部分,分别是订单状态以及订单详情,通过导航栏的titleView(也就是UISegmentedControl)来切换显示不同界面.

    订单详情页也是一个TableView就可以搞定,服务器返回是一个数组,这里的逻辑是,当前状态是0时,圆形图片为黄色,并且没有上面的线,最下面的状态没有下边的线,这里的做法是给cell的model赋值的时候,将indexPath一同传入给Cell,判断indexPath.row是多少,如果是0,就将圆形图片显示为黄色,并且隐藏上半部分线,同理当indexPath.row等于状态数组的count-1时,隐藏下半部分的线即可搞定.

    订单详情也是通过TableView实现的,采用tableView是考虑到商品种类的cell可以循环利用,顶部的订单细信息和收货人地址为TableHeadView(效果如下图)

    订单详情结构

    底部评价为tableFootView(效果如下图)

    订单详情结构

    优惠劵

    优惠劵效果图

    优惠劵也是由TableView构成的,有两种cell,一种是可以使用的优惠劵,另一种为不可使用的优惠劵,这里通过模型判断cell的展示的类型.

    使用规则为H5页面,直接在webView上loadURL就OK了.

    我的消息

    我的消息效果图

    依然是tableView,不过这里会根据用户操作动态改变cell的高度.做法是在给cell设置模型的同时,计算出cell全部展示的高度,在模型中创建辅助参数,保存cell的真实高度以及未打开时的高度,在UITableViewDelegate获取cell的高度时,判断当前cell的状态,根据状态返回对应的高度.

    cell内部的显示全部按钮通过闭包回调告诉控制器点击事件,同时将cell的IndexPath作为参数传出来,当用户点击显示全部时,根据当前cell的状态取反,同时tableView.reloadData就动态的改变cell的高度了.

    我的收货地址

    我的收货地址效果图

    额,还是tableView,不说了.

    编辑我的地址有两种情况,一种是修改现有的收货地址,进入时对应的选项都已经存在,并且有删除当前地址的View在底部.另外一种是添加新地址,没有参数和删除当前地址view,这里在EditAdressViewController写一个枚举,并且搞一个成员变量type

    enum EditAdressViewControllerType: Int {
        case Add
        case Edit
    }
    

    在push进入EditAdressViewController时,将EditAdressViewController的类型传入,根据type的类型,显示对应的数据即可.

    常见问题

    效果如下图:

    常见问题效果图

    常见问题这里UI的搭建相信读者都了然于心,不过多介绍,只讲一下点击出现动画的逻辑.这里也是一个TableView,常见问题为tableView的headView,详细问题View为tableView的Cell,默认cell的个数为零,当用户点击了headView,记录点击的headView的indexPath.section,刷新tableView,将点击行的Cell个数返回为1,并且单独给这一行的cell返回cell的高度.代码如下

    extension HelpDetailViewController: UITableViewDelegate, UITableViewDataSource, HelpHeadViewDelegate {
        
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell = AnswerCell.answerCell(tableView)
            cell.question = questions![indexPath.section]
            return cell
        }
        
        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            if lastOpenIndex == section && isOpenCell {
                return 1
            }
            return 0
        }
        
        func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
            if lastOpenIndex == indexPath.section && isOpenCell {
                return questions![indexPath.section].cellHeight
            }
            
            return 0
        }
        
        func numberOfSectionsInTableView(tableView: UITableView) -> Int {
            return questions?.count ?? 0
        }
        
        func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            let headView = tableView.dequeueReusableHeaderFooterViewWithIdentifier("headView") as? HelpHeadView
            headView!.tag = section
            headView?.delegate = self
            let question = questions![section]
            headView?.question = question
            
            return headView!
        }
        
        func headViewDidClck(headView: HelpHeadView) {
            if lastOpenIndex != -1 && lastOpenIndex != headView.tag && isOpenCell {
                let headView = questionTableView?.headerViewForSection(lastOpenIndex) as? HelpHeadView
                headView?.isSelected = false
                
                let deleteIndexPaths = [NSIndexPath(forRow: 0, inSection: lastOpenIndex)]
                isOpenCell = false
                questionTableView?.deleteRowsAtIndexPaths(deleteIndexPaths, withRowAnimation: UITableViewRowAnimation.Automatic)
            }
            
    
            if lastOpenIndex == headView.tag && isOpenCell {
                let deleteIndexPaths = [NSIndexPath(forRow: 0, inSection: lastOpenIndex)]
                isOpenCell = false
                questionTableView?.deleteRowsAtIndexPaths(deleteIndexPaths, withRowAnimation: UITableViewRowAnimation.Automatic)
                return
            }
            
            lastOpenIndex = headView.tag
            isOpenCell = true
            let insertIndexPaths = [NSIndexPath(forRow: 0, inSection: headView.tag)]
            questionTableView?.insertRowsAtIndexPaths(insertIndexPaths, withRowAnimation: UITableViewRowAnimation.Top)
        }
        
    }
    

    其他功能

    三方分享分享

    这里我使用了友盟分享SDK,需要在真机上才可以分享,不过Sina微博需要在后台配置测试账号,大家可能无法测试新浪微博分享~,关于分享也是封装了ShareManager工具类,定义好分享枚举类型

    enum ShareType: Int {
        case WeiXinMyFriend = 1
        case WeiXinCircleOfFriends = 2
        case SinaWeiBo = 3
        case QQZone = 4
    }
    

    将弹出的样式也ActionSheet也封装成单个类

    class LFBActionSheet: NSObject, UIActionSheetDelegate {
        
        private var selectedShaerType: ((shareType: ShareType) -> ())?
        private var actionSheet: UIActionSheet?
        
        func showActionSheetViewShowInView(inView: UIView, selectedShaerType: ((shareType: ShareType) -> ())) {
            
            actionSheet = UIActionSheet(title: "分享到",
                delegate: self, cancelButtonTitle: "取消",
                destructiveButtonTitle: nil,
                otherButtonTitles: "微信好友", "微信朋友圈", "新浪微博", "QQ空间")
            
            self.selectedShaerType = selectedShaerType
            
            actionSheet?.showInView(inView)
            
        }
        
        func actionSheet(actionSheet: UIActionSheet, clickedButtonAtIndex buttonIndex: Int) {
            print(buttonIndex)
            if selectedShaerType != nil {
                
                switch buttonIndex {
    
                case ShareType.WeiXinMyFriend.rawValue:
                    selectedShaerType!(shareType: .WeiXinMyFriend)
                    break
                    
                case ShareType.WeiXinCircleOfFriends.rawValue:
                    selectedShaerType!(shareType: .WeiXinCircleOfFriends)
                    break
                    
                case ShareType.SinaWeiBo.rawValue:
                    selectedShaerType!(shareType: .SinaWeiBo)
                    break
                    
                case ShareType.QQZone.rawValue:
                    selectedShaerType!(shareType: .QQZone)
                    break
                    
                default:
                    break
                }
            }
        }
        
    }
    
    

    当外部需要调用分享的时候,只需要调用一句代码即可

         shareActionSheet.showActionSheetViewShowInView(view) { (shareType) -> () in
            ShareManager.shareToShareType(shareType, vc: self)
        }
    

    扫一扫

    扫一扫效果图

    注意需要在真机上才可以测试,模拟器没有摄像头的.这里用的iOS7.0以后苹果自带框架AVFoundation,使用非常简单,这也就不过多叙述,讲一下如何实现中间区域亮,四边为黑色的效果,其实原理很简单,在view上创建四个view,如下图

    将View的背景色改为黑色,透明度为0.5,添加到View上,搞定.
    设置captureMetadataOutput.rectOfInterest的范围,控制扫描区域的敏感范围.

    搜索控制器

    效果如下

    搜索效果图

    搜索控制器导航栏上的搜索条使用的UISearchBar,下面为按钮,需要动态的布局按钮的位置,这里有热搜索和历史搜索,考虑到复用性,将搜索View封装成一个View,在便利构造方法中将按钮的名字数组传入,自定在内部布局,计算高度,通过闭包回调将按钮点击的事件通知给控制器,具体代码如下

    
    import UIKit
    
    class SearchView: UIView {
        
        private let searchLabel = UILabel()
        private var lastX: CGFloat = 0
        private var lastY: CGFloat = 35
        private var searchButtonClickCallback:((sender: UIButton) -> ())?
        var searchHeight: CGFloat = 0
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            
            searchLabel.frame = CGRectMake(0, 0, frame.size.width - 30, 35)
            searchLabel.font = UIFont.systemFontOfSize(15)
            searchLabel.textColor = UIColor.colorWithCustom(140, g: 140, b: 140)
            addSubview(searchLabel)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        convenience init(frame: CGRect, searchTitleText: String, searchButtonTitleTexts: [String], searchButtonClickCallback:((sender: UIButton) -> ())) {
            self.init(frame: frame)
            
            searchLabel.text = searchTitleText
            
            var btnW: CGFloat = 0
            let btnH: CGFloat = 30
            let addW: CGFloat = 30
            let marginX: CGFloat = 10
            let marginY: CGFloat = 10
            
            for i in 0..<searchButtonTitleTexts.count {
                let btn = UIButton()
                btn.setTitle(searchButtonTitleTexts[i], forState: UIControlState.Normal)
                btn.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
                btn.titleLabel?.font = UIFont.systemFontOfSize(14)
                btn.titleLabel?.sizeToFit()
                btn.backgroundColor = UIColor.whiteColor()
                btn.layer.masksToBounds = true
                btn.layer.cornerRadius = 15
                btn.layer.borderWidth = 0.5
                btn.layer.borderColor = UIColor.colorWithCustom(200, g: 200, b: 200).CGColor
                btn.addTarget(self, action: "searchButtonClick:", forControlEvents: UIControlEvents.TouchUpInside)
                btnW = btn.titleLabel!.width + addW
                
                if frame.width - lastX > btnW {
                    btn.frame = CGRectMake(lastX, lastY, btnW, btnH)
                } else {
                    btn.frame = CGRectMake(0, lastY + marginY + btnH, btnW, btnH)
                }
                
                lastX = CGRectGetMaxX(btn.frame) + marginX
                lastY = btn.y
                searchHeight = CGRectGetMaxY(btn.frame)
                
                addSubview(btn)
            }
            
            self.searchButtonClickCallback = searchButtonClickCallback
        }
        
        func searchButtonClick(sender: UIButton) {
            if searchButtonClickCallback != nil {
                searchButtonClickCallback!(sender: sender)
            }
        }
    }
    
    

    唠叨一下

    关于项目的内容,并不是短短几千文字就能给大家讲明白的,想要了解更多内容,请打开代码仔细研究,小熊还是抱着为了大家能看懂的套路基本没有使用三方框架,应小东同学的要求使用了纯代码开发.希望对大家有所帮助.记着点个Star哈~

    最近有点忙,加上整个人回到北京好像变懒了T_T,有段日子没有发布什么资源了.这个项目就作为新年礼物送给大家吧,还有两个小时,开网小熊回家的列车就出发了,在这里提前祝大家新年快乐~

    代码下载地址

    代码下载地址,欢迎点赞和反馈

    小熊的技术博客

    点击链接我的博客

    小熊的新浪微博

    我的新浪微博

    本文为作者原著,欢迎转载,转载请注明作者出处

    相关文章

      网友评论

      • 小小云喵:66666666666666666
      • 642757a98ce5:谢谢楼主分享~我正在学习移动开发,有个问题想请教您,像这样的购物app的后台是怎么运行的如何接收用户下单之类的相关数据,想要实现的话需要什么学习哪方面的知识,如何编写?期待您帮我解惑
        642757a98ce5:@维尼的小熊好的,谢谢您
        Top_熊:@geyuhao222 操作后台数据库里的表单。数据库管理着整个流程的运作,有库存表,商品表,订单表一系列数据
      • T92:我想问一下在SwiftDicModel文件中的代码
        // 检查类是否实现了协议
        var mappingDict: [String: String]?
        if cls.respondsToSelector("customClassMapping") {
        mappingDict = cls.customClassMapping()
        }
        在swift3中如何实现呢,mappingDict = cls.customClassMapping()会报错,如果我想在if中让cls执行customClassMapping应该如何操作呢
      • 布袋的世界:看到”没有摄像头你描个蛋啊~换真机试试 “这个快笑死了。。。
      • 韩大熊宝要姓张:感谢分享.
      • 一个很帅的蓝孩子:想看OC的但是是swift的 :joy: :joy: :joy: 顺便说一下写这篇文章到今天刚好一年了哦
        Top_熊:@一个很帅的蓝孩子 这都被你发现啦:monkey_face:
      • Calvin_Shen:牛拌,佩服!
      • 2e0af36b404f:现在才开始看,写的 真好 :pray: 是看楼主的cocos2d过来的 楼主全能
        Top_熊:@somesOne swift升级了,有些旧的语法不支持了,我没有去改。。你在xcode里设置下,使用旧的swift就不会有错误,或者用xcode7打开:sweat:
        somesOne:能否给一个最新的吖,下载了,全是错,改了些,不知道如何改了 。新手
        Top_熊:@2e0af36b404f :blush::blush:
      • 再来一份果子狸:熊大,问个问题,我看你通过CollectionView加载了HomeTableHeadView,HomeTableHeadView包含了pageScrollView和hotView,我想知道的是熊大把HomeTableHeadView加载到CollectionView中之后,给了HomeTableHeadView一个CGRect(0,-height,ScreenWidth,Height),那么为什么我在代码中按照同样方法写的时候,HomeTableHeadView会超出页面顶部,导致轮播及下面的按钮根本看不见,麻烦熊大了!!
      • 不够流氓:大神您好 就是分享的时候 分享到微信左下角的图标您是再哪里修改的啊 现在项目里面分享出去还是你的头像和昵称分享出去的 这个要在哪里修改呢
        Top_熊:@不够流氓 在封装的分享类里面,数据我给写死的,替换就行了
      • 祢的笑靥夏了夏天:楼主能将代码发到我邮箱吗,想学习一下
        Top_熊:@祢的笑靥夏了夏天 文章最后面,有下载链接,自己下一下就行了
      • 0d183d5f7398:地址上,手机没有使用正则。
      • b6cf7996bff2:有BUG,侧滑返回,在没有全部返回情况下,导航栏出现三个蓝色的点
      • 一叶知秋110:有oc的 电商么
      • 动物园园长熊熊酱:你好,请问tabbaritem的动画用oc怎么实现呢,swift看不懂
      • zyzxrj:感觉大神分享这么好的文章,这么好的例子,刚开始用swift做项目
        Top_熊:@zyzxrj :smile:
      • 823d26170f32:大哥,,你这个工程,每一个页面prensent一个新controller,在dismiss之后,原tabbarItem的tittle都跑到屏幕最左上角去了,你有发现么?
      • Sunxb:楼主, 我这几天刚开始研究Swift, 对JOSN --- >>> model 这一部分实在是搞不懂 , 看你的代码了, 也不是很理解 , 能不能请楼主大致说一下json数据转为model的一个思路 ... google好像也没有太多讲解的 :cry:
      • Ko_Neko:似乎主页下半段的Cell文字没有显示完全哦
        823d26170f32:@IGARASHI 改一下cell高度就可以了
      • b5760daaefd3:非常感谢你的无私奉献,以前一直忙于工作,现在要开始看了,希望会有收获
      • 豆丶浆油条:仔细研究一下
      • 384356f785cc:好cool ,自学完swif语言后第一个要临摹仔细拜读下这个文章!感谢!!! :smile:
      • 七月喝雪碧:要是能有视频讲解就好了,这个项目对于新手入门会受益很大的
        71640cd26721:@维尼的小熊 求视频教程,跪谢大神!
        七月喝雪碧:@维尼的小熊 好的,如果开课了,记得告诉下我哦 :smiley:
        Top_熊:@麽布一漾 这个吧。。。我可以考虑考虑
      • YHWXQ简简单单的生活:小熊,您好,请教您一个问题,点击TabBar有动画效果,但是我将AnimationTabBarController拖入到另一个项目中,然后完善运行,发现报错,不能实现动画效果,谢谢!
      • 张三疯疯子:涛哥,编辑地址界面,手机输入之后,无法删除.还有就是,删除到一半删不动了
      • 张三疯疯子:您的那个 清除小数点后面多余的0的分类,有BUG,在购物车显示价格的时候不准确,麻烦您看一下
        Top_熊:@张三疯疯子 哦,是这样的字符串在转换为double时候,会有精准度损失,固定下精准度位数,或者自己在四舍五入下就可以了
      • b13cd3e68764:大神,我发现你那个添加到购物车有个bug,如果imageView的contentMode设置为UIViewContentModeScaleToFill没有什么问题,如果设置为UIViewContentModeScaleAspectFill或其他layer从界面移除的时候会闪动一下,求解决 :+1:
      • Bear大大:哥们你写得太好了,如果有oc版本的就更加好
      • bdb91d5465c5:写的非常好,值得学习
      • HJXu:非常有用,感谢.
      • 32b6a4d7be27:大神有时间可否提供一个Stroyboard版的?
      • Rickie_Lambert:神一样的人物存在着, 可不可以有个OC版的啊, swift还没有学,看不懂啊, 刚着手做OC 开发
      • 玻璃瓶外的水:大神,我现在遇到了一个问题,我在商品详情页使用storyboard来绘制商品详情页,并使用如下代码来载入:
        let storyboard: UIStoryboard = UIStoryboard(name: "SelectedStoryboard", bundle: nil)
        let selectedViewController = storyboard.instantiateViewControllerWithIdentifier("SelectedStoryboard") as! SelectedViewController
        self.presentViewController(selectedViewController, animated: true, completion: nil)
        并且这段代码可以正常载入画图板中的内容,但dismiss退出的时候,主页中的tab上的字就不能不见啦,并且在屏幕的最上面显示了一些字符,不知道是怎么回事,请帮我分析原因,谢谢
        823d26170f32:@玻璃瓶外的水 我也越到了同样的bug,这个我觉得是大神用那个第三方的tabbar的原因,我把他改成系统的tabbar之后就没很好,但是动画效果没了
      • Calabash_Boy:这只维尼熊不简单,初级小程序猿来膜拜~!
      • hehe_Android:楼主可以模仿个社区类的应用,包括登录注册模块
      • tanlai38397:首页广告正好可以参考一下 :pray:
      • 张三疯疯疯子:大神,收下我的膝盖,跪舔
      • seongbrave:不错,学习下
      • b13cd3e68764:为啥不弄个OC版本的呢,swift还不是很熟 :joy:
        Top_熊:@wonin 是用uikit写的游戏
        b13cd3e68764:@维尼的小熊 也是电商的吗:+1:
        Top_熊:@wonin 我刚写了一个oc的项目,你可以看下
      • liyaoyao:楼主 , HomeTableHeadView里面的协议代理,你的协议是写在类下面 ,我的写在下面会报错,原因是未声明 ,在oc 中 可以 @class 类名; 声明类,用 @protocol 协议名; 声明一个协议 ,请问在swift中如何声明类和协议
      • 大花头:能说说那个模型管理器的原理么
      • misswuyang: 全部都用代码布局!!好吧利害了!
        不过这样效率比代码和storyboard结合效率低点。
      • misswuyang:支持下!
      • yaoyao妖妖:大神你好,我想问一下项目中的exec文件是怎样创建的呢?谢谢啦
      • wendytingstar:现在正在学习swift,很好的的学习方向,期待后续的更新 :wink:
      • 玻璃瓶外的水:非常感谢楼主的文章和代码分享,由于本人还是初学ios,现在想在楼主的代码基础上进行修改,遇到了些问题:
        ProductDetailViewController这个产品详情页面,本人想将原来的detailImageView用一个detailWebView: UIWebView?来替代,于是编写了如下代码:
        if let url = NSURL(string: "http://www.baidu.com&quot;) {
        let request = NSURLRequest(URL: url)
        detailWebView?.loadRequest(request)
        }
        scrollView?.addSubview(detailWebView!)
        但运行时候一直提示,fatal error: unexpectedly found nil while unwrapping an Optional value,不明白这是什么原因,希望能帮小弟解答下,谢谢
      • 飞翔的小骑兵:在来个OC版就完美了 :smiley:
      • 尼古拉斯赵四爷:楼主好 cool
      • a476362dddef:问下大神怎么运行会出现 Command /Applications/Xcode.app/Contents/Developer/usr/bin/ibtool failed with exit code 255. The tool may have crashed. Please file a bug report at http://bugreport.apple.com with the above output and attach any crash logs for ibtool, ibtoold, Xcode, and Interface Builder Cocoa Touch Tool created around the time of this failure. These logs can be found in ~/Library/Logs/DiagnosticReports or /Library/Logs/DiagnosticReports. 这个错误呢,求解呀
        kinmo:请问问题解决了没有?
        着魔的毛豆:@a476362dddef 我也遇到这个问题了 请问有解决办法吗
      • a476362dddef:感动,我们近期开发的项目就是要仿这个app做一个, :kissing_heart:
      • liyuhong165:mark 模仿app
      • Marshall_Yin:你好,我想问一下实际开发中storyboard用的多吗?我看好多仿写的app都完全不用storyboard:sweat:
        Top_熊:@Marshall_Yin 不一定,看公司需求了,个人觉得SB更方便
      • selfim:楼主写的真棒,,,大神!!
      • 清水昏昏:大神好牛, :joy:
      • 6324fa693259:楼主好棒, 现在我正学习你的代码。非常感激。 目前发现两个bug:
        1, 首页头部 有个自动循环滚动的 scrollView。 我觉着当主页 viewDidDisappear后把定时器关了比较好。 优化性能嘛。
        2, 主页中的 HotView.swift文件中,第34行, 每一次刷新数据时都会添加子 View
        // MARK: 模型的Set方法
        var headData: HeadData? {
        didSet {
        if headData?.icons?.count > 0 {

        if headData!.icons!.count % 4 != 0 {
        self.rows = headData!.icons!.count / 4 + 1
        } else {
        self.rows = headData!.icons!.count / 4
        }
        var iconX: CGFloat = 0
        var iconY: CGFloat = 0

        for i in 0..<headData!.icons!.count {
        iconX = CGFloat(i % 4) * iconW + HotViewMargin
        iconY = iconH * CGFloat(i / 4)
        let icon = IconImageTextView(frame: CGRectMake(iconX, iconY, iconW, iconH), placeholderImage: UIImage(named: "icon_icons_holder")!)

        icon.tag = i
        icon.activitie = headData!.icons![i]
        let tap = UITapGestureRecognizer(target: self, action: "iconClick:")
        icon.addGestureRecognizer(tap)
        addSubview(icon)
        }
        }
        }
        }
        6324fa693259:@hejunm 大哥, 不会介意吧?
      • 江将蒋酱:你好,小熊哥,目前正在看你的代码,我遇到一个我觉得很困难的问题,就是项目闪电超市这个板块里面,我自己写的是2个tableview,但是右边的tableview里面的数据,我分出了section以后后面出现了很多重复的产品 应该怎么处理?
      • Developer_Yancy:想问下作者,平时你们开发的时候也是这么频繁的使用Notification的吗?想了解下与KVO相比,哪个更好一些?求指教
        Developer_Yancy:@Coder_CYX 比如说分哪些场合使用?
      • LeeOuf:商品列表页中,如果把tableview的section header删掉,下拉刷新的mj_header图标就会被挡住大家发现了吗?想破脑袋也没想出为啥,求指教
      • 小豆豆苗:刚从OC转Swift,我想在解析数据的时候把JSONDict转model,然后用了你写的SwiftDictModel,在转为普通模型的时候没问题,若模型中包含了另外一个模型的时候,仿照你写的方法,报错了,说是EXC_BAD_INSTRUCTION,一直找不到原因也找不到解决办法,实在不想敲代码一一映射,所以才来请教一下。如果可以,能否给我指点迷津?
        小豆豆苗:@hejunm 不好意思,现在看到,是我的问题,因为我的工程命名空间的原因,现在已经解决了…… :smile:
        6324fa693259:@捣蛋猪八戒 看我的字典转模型
      • 醉看红尘这场梦:好像没有登入功能 :no_mouth:
        823d26170f32:@醉看红尘这场梦 我自己补上了登录——注册——忘记密码
      • 韩七夏:收藏
      • 开发仔小广:请大神收下我的膝盖!!! :+1: :+1: :+1:
      • Mg明明就是你:XCode7.0可以打开吗?
        Top_熊:@Mg明明就是你 可以
      • 光明程辉:很详细!😄😄
      • xxttw:干货 给力
      • 822dc803f3af:那个 Theme.swift 是怎么创建呢 ? 我看着什么类也不继承呀???
      • ae2e7caddf5a:现阶段你就是我的学习对象了,特意注册个账号来关注你 :stuck_out_tongue_winking_eye:
      • 9d74e8c718e3:看了你的代码,大写的服字。要是能有网络请求数据就更好啦,小白对swift 网络请求这块太模糊了。感谢大神的分享 :+1:
      • 8b7b3318da42:下了你的代码,用Xcode7.2运动,有一个报错
        ld: library not found for -lSVProgressHUD
        clang: error: linker command failed with exit code 1 (use -v to see invocation)
        能不能说下怎么解决,小白,求解
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • sunlin1234:nihao
      • 483bdf8ba6bd:我启动项目报错,找不到ld: library not found for -lSVProgressHUD
        能把lSVProgressHUD发出来吗
      • 483bdf8ba6bd:楼主你好
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • sunlin1234:下载后发现内存激增是怎么回事?
      • 8164cf2594cd:libPods.app
        LoveFreshBeenTests.xctest
        LoveFreshBeenUITests.xctest,右边导航这几个显示红色。我是菜菜鸟。求大神指点哇,嚯嚯!
      • 8164cf2594cd:Ld /Users/renzuoxin/Library/Developer/Xcode/DerivedData/LoveFreshBeen-fughfbpwggtsqpfzaqzaytekxnxz/Build/Products/Debug-Ldld: library not found for -lSVProgressHUD
        clang: error: linker command failed with exit code 1 (use -v to see invocation)
      • 3e2766286eaa:给力作品!
        请你相信介绍一下 商品的详细页面思路可用嘛
      • b7e9401f7a35:请问作者,如何在真机上进行运行呢
      • 40ec58416917:太厉害了
      • 91af9dda6970:想请教,怎么让UICollectionView中的SubView优先显示,其次在SubView下面加载Cell?我现在的状况是SubView盖在Cell的上层,遮挡住了下面的Cell
        再来一份果子狸:@王府二少 麻烦问一下,我在熊大代码中没看到有单独给轮播图做一个Section,是我搞错了么?
        91af9dda6970:@维尼的小熊 搞明白了,大神,我用CollectionView的时候轮播图以为是单独一个控件add到collection的view上面去的,之后他就挡住了cell的布局,当时蒙了,后来解决了给轮播图单独做一个section就好了,感谢,支持小熊再出新作
        Top_熊:@王府二少 subView是指??每太看明白你说的啥
      • 792a8f5984f8:您好,我看您在ViewController中一般都是private var titleNameLabel: UILabel?以这种形式声明新的属性,而我一般习惯private lazy var labPrdName = UILabel()这样声明,请问这样两种不同的写法有什么区别吗?

        Top_熊:@SOOBEI 你的是懒加载
      • 我的发:你好,账号登陆是已经注册好的是吗? 抓包不能抓注册的接口吧?
      • 醉看红尘这场梦:好痛苦,switf不会写项目,只会简单基础
      • shuipingsixeu:大神爱你么么哒,,,可否加个扣扣,我想抱你大腿
        shuipingsixeu:@shuipingsixeu 1614388569
      • 15b1b0a86541:有OC版本的该多好啊
      • 牧童s:你好,请问下首页滚动视图传一张图片也循环滚动啊?
        Top_熊:@Nortons 哦忘记考虑只有一个图片了, 可以在传入数据里面加个判断,scrollView.userInteractionEnabled = data.count > 1

      本文标题:iOS高仿爱鲜蜂

      本文链接:https://www.haomeiwen.com/subject/bcqdkttx.html