app ios 仿商城
2017-06-22 17:27:44 shihuboke 阅读数 612

//联系人:石虎  QQ: 1224614774昵称:嗡嘛呢叭咪哄

iOS高仿爱鲜蜂

前言

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

关于项目(代码下载地址在文章最下面点击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,有段日子没有发布什么资源了.这个项目就作为新年礼物送给大家吧,还有两个小时,开网小熊回家的列车就出发了,在这里提前祝大家新年快乐~



2016-03-15 18:18:40 sevenquan 阅读数 1328

code4app中看到的仿战旗 TV app的Demo,值得收藏。

 战旗 TV app (可以播放视频),可以学习到其中视图的布局处理技巧。


  代码源:http://www.code4app.com/ios/仿照战旗TV-app-持续更新中-可以播放视频/25b6718c-e455-11e5-82b2-00163e0606f4


iOS广州、深圳群496146057

0
2016-02-02 11:05:00 weixin_34302561 阅读数 27

前记

写文前总要说明目的或者吐槽些什么的。
入门iOS也快一年了,到现在还没一个自己独立完成的App,说起来也是惭愧。移动应用,说实在话,我也还在摸索中,实在没什么比较好的idea,但自己又想找一些事干,那么就仿呗。要仿一个App也不是那么简单,撇开移动端的技术,其他需要完成的就有产品、设计、测试、后端接口、服务器,这还不扯到运营、内容、市场。找了一阵子的App后,决定做一个记账方面的App。刚开始是秉持锻炼技术的目的,后来用Charles抓了一下包,发现我每记的一笔账都会被Post到对方的服务器而且还发送到pingma.qq.com做统计,顿时有内裤被人扒了的感觉。恐怕这就是业内的作风吧,冯管数据有用没用,先存起来再说,说不定又可大肆鼓吹大数据处理进入下一个风口继续骗钱过日子。

本文的面向读者

已入门iOS,且用Mac进行开发。
如果你没接触过Mac,那恐怕以下内容都不用看了。

正文

接下来就是怎么获取App素材的步骤了。
1、打开iTunes(mac自带应用),按照下图,先找到App Store,然后搜索框输入想要找的App,比如我输入的是约约(自家产品、避免纠纷),搜索出来后点击获取就可以了,期间可能需要你输入账号密码,填一下就可以了,反正免费。

1374747-ca17cfe0622b062a.png
iTunes.png

2、点击我的应用,找到刚才下载的应用约约,右键点击后选择“在Finder”中显示

1374747-1409d42ea36cd9a0.png
我的应用.png

3、如下图所示,我们得到的是一个ipa的安装包,不要直接双击打开,右键点击后选择打开方式:归档实用工具,然后就可以获得一个同名的文件夹,在Payload中找到一个包,右键点击后选择显示包内容。

1374747-215f11a675312f02.png
显示包内容.png

4、到这一步,就算ok了。所有资源都在这个包里面,这里就有App所有的本地素材,当然有一些素材是通过网络加载的(通常是一些需要经常换的素材)。

1374747-9987b11d49a4ad99.png
图片资源.png

5、不过我们可能还会遇到找不到素材的情况,这时候就要留意显示的包中有没有Assets.car文件。当开发者使用Image Assets管理图片资源后,我们下载App解包后无法看到原始的图片,所有的图片都被存储在Assets.car文件中。

1374747-28969d50e7380681.png
images.xcassets.png

那么这时怎么办呢?在stackOverflow上找了一下,有个家伙提供了这么一个方法,用的是开源的cartool工具,专门用于提取.car文件中的资源。

打开终端:输入git clone https://github.com/steventroughtonsmith/cartool
用Xcode打开下载下来的工程,编译后自动生成可执行文件(在工程目录下的product文件夹中),然后找到可执行文件(右键点击后选择show in finder),将cartool拖进控制台(感谢@neobuger提醒),按照下面的命令执行即可。

1374747-a6755b4f758c47fe.png
cartool.png

命令格式: ./cartool “待解压的文件路径” “解压后的文件路径”
示例:

$ ./cartool /Users/admin/Documents/Assets.car /Users/admin/Documents/

这么一来就可以获取Assets.car中的所有素材了。

首先声明这种做法在一定程度上是侵权的。毕竟每个公司的产品都是每个team的心血,我们极易拿到的素材有可能是设计部门加班加点后的产出,所以我呼吁大家仅作为学习用,切勿用于商业用途。

2016-02-02 11:11:01 fjienigui 阅读数 2624

前记

写文前总要说明目的或者吐槽些什么的。
入门iOS也快一年了,到现在还没一个自己独立完成的App,说起来也是惭愧。移动应用,说实在话,我也还在摸索中,实在没什么比较好的idea,但自己又想找一些事干,那么就仿呗。要仿一个App也不是那么简单,撇开移动端的技术,其他需要完成的就有产品、设计、测试、后端接口、服务器,这还不扯到运营、内容、市场。找了一阵子的App后,决定做一个记账方面的App。刚开始是秉持锻炼技术的目的,后来用Charles抓了一下包,发现我每记的一笔账都会被Post到对方的服务器而且还发送到pingma.qq.com做统计,顿时有内裤被人扒了的感觉。恐怕这就是业内的作风吧,冯管数据有用没用,先存起来再说,说不定又可大肆鼓吹大数据处理进入下一个风口继续骗钱过日子。

本文的面向读者

已入门iOS,且用Mac进行开发。
如果你没接触过Mac,那恐怕以下内容都不用看了。

正文

接下来就是怎么获取App素材的步骤了。
1、打开iTunes(mac自带应用),按照下图,先找到App Store,然后搜索框输入想要找的App,比如我输入的是约约(自家产品、避免纠纷),搜索出来后点击获取就可以了,期间可能需要你输入账号密码,填一下就可以了,反正免费。

iTunes.png

2、点击我的应用,找到刚才下载的应用约约,右键点击后选择“在Finder”中显示

我的应用.png

3、如下图所示,我们得到的是一个ipa的安装包,不要直接双击打开,右键点击后选择打开方式:归档实用工具,然后就可以获得一个同名的文件夹,在Payload中找到一个包,右键点击后选择显示包内容。

显示包内容.png

4、到这一步,就算ok了。所有资源都在这个包里面,这里就有App所有的本地素材,当然有一些素材是通过网络加载的(通常是一些需要经常换的素材)。

图片资源.png

5、不过我们可能还会遇到找不到素材的情况,这时候就要留意显示的包中有没有Assets.car文件。当开发者使用Image Assets管理图片资源后,我们下载App解包后无法看到原始的图片,所有的图片都被存储在Assets.car文件中。

images.xcassets.png

那么这时怎么办呢?在stackOverflow上找了一下,有个家伙提供了这么一个方法,用的是开源的cartool工具,专门用于提取.car文件中的资源。

打开终端:输入git clone https://github.com/steventroughtonsmith/cartool
用Xcode打开下载下来的工程,编译后自动生成可执行文件(在工程目录下的product文件夹中),然后找到可执行文件(右键点击后选择show in finder),按照下面的命令执行即可。

cartool.png

命令格式: ./cartool “待解压的文件路径” “解压后的文件路径”
示例:

$ ./cartool /Users/admin/Documents/Assets.car /Users/admin/Documents/

这么一来就可以获取Assets.car中的所有素材了。

首先声明这种做法在一定程度上是侵权的。毕竟每个公司的产品都是每个team的心血,我们极易拿到的素材有可能是设计部门加班加点后的产出,所以我呼吁大家仅作为学习用,切勿用于商业用途。

2017-03-25 20:38:00 weixin_34258078 阅读数 15

iOS 商城类 app 首页的实现

    很多人做 iOS开发的人员都应该写过这样的界面,但是呢,具体怎么去优化写这样的界面是我们需要考虑的,对于这样的界面动辄上千行代码在控制器里面"活跃"着,后期维护成本大大增加,现在我简单的说一下我的实现思路,这样的界面无非就是 collectionView 或者 tableView. 举个简单的例子,以 tableView 为例吧,绝大数 app 首页基本都是第一个分区是个 bannar, 其实是分类,其次可能还是分类,最后是列表,一般这个时候,我的习惯是将每一块自定义 view, 然后当做 tableView 的 sectionHeaderView, 最后一个是 cell,当然你也可以将每一块携程 cell 也是可以的,因为 sectionHeaderView 也是可以复用的,当然,如果就是三四个分区是否复用其实差不多(如果大家觉得有必要复用,请记得在下面留言,然后我们一起探讨一下嘛). 上个图,很丑,简单的看一下就好了,不要介意

1.第一部分代码: 

#import "SectionHeaderViewOne.h"

@interface SectionHeaderViewOne ()

@property (nonatomic,strong) UIImageView *hotel_image;
@property (nonatomic,strong) UILabel *hotel_type;
@property (nonatomic,strong) UIButton *hotel_rate;
@property (nonatomic,strong) UIButton *hotel_address;

@end

@implementation SectionHeaderViewOne

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupUI];
    }
    return self;
}

- (void)setupUI {
    [self addSubview:self.hotel_image];
    [self addSubview:self.hotel_type];
    [self addSubview:self.hotel_rate];
    [self addSubview:self.hotel_address];
}

#pragma mark --- 懒加载 ----
- (UIImageView *)hotel_image {
    if (!_hotel_image) {
        _hotel_image = [[UIImageView alloc] init];
        [_hotel_image setFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height - 50)];
        _hotel_image.image = [UIImage imageNamed:@"timg.jpg"];
        _hotel_image.contentMode =  UIViewContentModeScaleToFill;
    }
    return _hotel_image;
}

-(UILabel *)hotel_type {
    if (!_hotel_type) {
        _hotel_type = [[UILabel alloc] init];
        [_hotel_type setFrame:CGRectMake(10, self.frame.size.height - 100, 100, 50)];
        _hotel_type.font = [UIFont systemFontOfSize:18];
        _hotel_type.text = @"7天";
        _hotel_type.textColor = [UIColor whiteColor];
    }
    return _hotel_type;
}

- (UIButton *)hotel_rate {
    if (!_hotel_rate) {
        _hotel_rate = [UIButton buttonWithType:UIButtonTypeCustom];
        [_hotel_rate setFrame:CGRectMake(self.frame.size.width - 100, self.hotel_type.frame.origin.y, 100, 50)];
        [_hotel_rate setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        _hotel_rate.titleLabel.font = [UIFont systemFontOfSize:18];
        [_hotel_rate setTitle:@"查看评价" forState:UIControlStateNormal];
    }
    return _hotel_rate;
}

- (UIButton *)hotel_address {
    if (!_hotel_address) {
        _hotel_address = [UIButton buttonWithType:UIButtonTypeCustom];
        [_hotel_address setFrame:CGRectMake(10, CGRectGetMaxY(self.hotel_image.frame), self.frame.size.width - 20, 50)];
        [_hotel_address setTitle:@"地址: 北京天安门" forState:UIControlStateNormal];
        [_hotel_address setContentHorizontalAlignment:UIControlContentHorizontalAlignmentLeft];
        [_hotel_address setTitleColor:[UIColor purpleColor] forState:UIControlStateNormal];
        _hotel_address.titleLabel.font = [UIFont systemFontOfSize:17];
    }
    return _hotel_address;
}

2.第二部分的话,就稍微麻烦点,因为上面嵌套了一个 collectionView

先自定义 collectionView, 在自定义 collectionViewCell, 最后也可以自定义一个 view, 将其作为 collectionView 的父视图 (我是这样写的,我个人觉得没有多大的必要, 简单事物复杂化了)

collectiomView.m 的代码 

#import "CollectionView.h"
#import "CollectionViewCell.h"


@interface CollectionView () <UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout>

@end

static NSString *kIdentify = @"CollectionViewCell";

@implementation CollectionView

-(instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {
    if (self = [super initWithFrame:frame collectionViewLayout:layout]) {
        self.delegate = self;
        self.dataSource = self;
        self.alwaysBounceVertical = YES;
        [self registerNib:[UINib nibWithNibName:kIdentify bundle:[NSBundle mainBundle]] forCellWithReuseIdentifier:kIdentify];
    }
    return self;
}


#pragma mark --- 代理 ----
#pragma mark  设置CollectionView的组数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}

#pragma mark  设置CollectionView每组所包含的个数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.imgArrays.count;
}

#pragma mark  设置CollectionCell的内容
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kIdentify forIndexPath:indexPath];
    cell.type_img.image = [UIImage imageNamed:(self.imgArrays[indexPath.row])];
        return cell;
}
    


#pragma mark  定义每个UICollectionView的大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    return CGSizeMake([UIScreen mainScreen].bounds.size.width / 5, 100);
}



#pragma mark  定义整个CollectionViewCell与整个View的间距
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
    return UIEdgeInsetsMake(0, 0, 0, 0);//(上、左、下、右)
}


#pragma mark  定义每个UICollectionView的横向间距
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
    return 0;
}

#pragma mark  定义每个UICollectionView的纵向间距
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
    return 0;
}

#pragma mark  点击CollectionView触发事件
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    
    
}

#pragma mark  设置CollectionViewCell是否可以被点击
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    return YES;
}

collectionViewCell 用 xib 就好了,这个就不再啰嗦了

3. 自定义 tableView

#import "HotelTableView.h"
#import "SectionHeaderViewOne.h"
#import "SectionHeaderViewTwo.h"
#import "CollectionView.h"


@interface HotelTableView () <UITableViewDelegate,UITableViewDataSource>

@end

@implementation HotelTableView

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.dataSource = self;
        self.delegate = self;
        UIView *footerView = [[UIView alloc] init];
        footerView.backgroundColor = [UIColor lightGrayColor];
        self.tableFooterView = footerView;
    }
    return self;
}


- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section == 0) {
        return 0;
    }else if (section == 1){
        return 0;
    }else {
        return 10;
    }
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cell_id = @"cell_id";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cell_id];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cell_id];
    }
    cell.textLabel.text = @"简单的测试";
    return cell;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    if (section == 0) {
        SectionHeaderViewOne *headerView = [[SectionHeaderViewOne alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 200)];
        return headerView;
    }else if (section == 1){
        SectionHeaderViewTwo *headerView = [[SectionHeaderViewTwo alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 200)];
        headerView.imgArrays = @[@"04",@"10",@"42",@"51",@"70",@"80-2",@"80",@"10",@"04",@"51"];
        return headerView;
    }else {
        UILabel *label = [[UILabel alloc] init];
        [label setFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 60)];
        label.text = @"入住日期,3月24日";
        label.font = [UIFont systemFontOfSize:18];
        return label;
    }
}


- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 100;
}


- (CGFloat )tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    if (section == 2) {
        return 60;
    }else {
        return 200;
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    return 10;
}

4. 将 tableView 添加到控制器上面就好了

#import "ViewController.h"
#import "HotelTableView.h"

@interface ViewController ()

@property (nonatomic,strong) HotelTableView *tableView;


@end

@implementation ViewController

-(HotelTableView *)tableView {
    if (!_tableView) {
        _tableView = [[HotelTableView alloc] init];
        [_tableView setFrame:self.view.bounds];
    }
    return _tableView;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self.view addSubview:self.tableView];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

 

以上就可以简单实现上面图的界面了,这样控制器里面看起来是不是很简洁,因为逻辑处理可以相应的分散到 tableView 以及子视图当中, 哪怕界面逻辑修改,维护起来成本也不至于太高,这是我的想法,欢迎各路大神拍砖

 

转载于:https://my.oschina.net/alanTang123/blog/867363

没有更多推荐了,返回首页