swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
项目需求:做一个图片浏览器,点击图片查看大图,大图模式下,左右滚动能查看不同的图片.
项目的主要核心技术:图片的弹出和消失动画
项目源代码: Photo-Browser
一.对代码进行重构
1.对代码进行抽取划分
1.1 为什么要对代码进行抽取?
swift中,代码全部写在一起,阅读性极差
2.如何对代码进行抽取?
2.1在oc中,可以把功能模块抽取一个个方法
2.2swift中,专门提供 extension ,可以对原有的类进行扩展
3.怎么使用extension 抽取代码?
3.1 把一些方法写在extension(扩展)里面,这样能减少viewDidLoad里面的代码
3.2 extension可以写多个,这样就可以把不同的功能模块 ,写在不同的扩展里面
二.项目基本设置
1.修改bundleID
2.部署版本
3.设置项目图片,启动图片
4.对文件夹目录进行划分
三.首页布局
1.让首页为UICollectionViewController
2.设置数据源
3.自定义布局
3.1 创建一个源文件,继承自UICollectionViewFlowLayout
3.2 重写 prepareLayout
3.3 设置布局的相关属性
4.如何设置StoryBoard中的UICollectionViewController的布局
4.1 在StoryBoard中选中collectionView
4.2 在属性里找到 layout 设置为自定义 custom
4.3 在下面的class里面 把自定义布局的类名写进去即可
四.网络工具类的封装
1.集成CocoaPods, 并导入AFNetworking框架
1.1 打开终端,进入项目路径下 cd 路径
1.2 创建PodFile文件 pod init
1.3 配置PodFile文件 ,写入要导入的框架
1.4 导入框架 pod install —no-repo-update / 或 pod intall
1.41 pod install 会更新本地库(本地已有的框架也会更新) 速度相对较慢
1.42 pod install —no-repo-update 不会更新本地库,速度相对来说快点
2.封装工具类
2.1 将工具类设计成单例对象
防止别人修改
防止多线程访问,创建多个对象
2.2 swift中单例的设置方式
static let shareInstance : NetworkTools = NetworkTools()
2.3 可以让工具类,直接继承自用到框架的一个类
好处:自己就是这个类的子类,拥有这个类的所有方法和属性,用的时候直接自己就能调用
3.封装网络请求方法
1 func requestData (type : Int , urlString : String , parameters : [ String : NSObject] , callBack : (result : AnyObject? , error : NSErroe?) -> () ) 2 func reqeustData(type : RequestType, urlString : String, parameters : [String : NSObject], finishedCallback : (result : AnyObject?, error : NSError?) -> ()) { }
4.把方法里面的闭包抽取出来
4.1 为什么要抽取?
方法里面闭包很长,代码很乱,造成阅读性差
4.2 怎么抽取?
定义一个成员属性 为闭包类型 把方法里面的闭包,用属性名 替换
五.项目集成工具类
把封装好的工具类,直接拖到项目文件中
六.请求网络数据
1.在控制器中调用工具类封装好的网络请求方法
2.解析数据
要对获取到的数据进行类型转换,应为从网络加载的数据类型为AnyObject
guard let resultDict = result as? [String : NSObject] else { return } guard let dataArray = resultDict["data"] as? [[String : NSObject]] else { return }
3.字典转模型
3.1 创建模型
3.2 通过kvc手动转模型 , 要重写 override func setValue(value: AnyObject?, forUndefinedKey key: String) {}
3.3 注意: 在闭包中 self. 也不可以省略
七,自定义cell,展示数据
1.创建cell继承自UICollectionViewCell
2.在cell里面定义模型属性
3.监听属性改变(相当于oc的重写set方法)
在属性监听器(willSet, didSet) 这里用didSet方法里面给模型里面的属性赋值
八.加载更多数据
1.什么时候加载更多的数据?
当最后一个cell出现的时候
2.怎么监听最后一个cell是否出现在屏幕上
通过cell(item)的下标值(从0开始)是否等于数组长度 - 1
// 最后一个cell已经出现 if indexPath.item == shops.count - 1 { indexPath.item 相当于 tableView 的 indexPath.row loadHomeData(shops.count) }
3.怎么加载更多数据
和加载数据一样,只不过多传一个参数offset
九.弹出图片浏览器
1.创建图片浏览器的控制器对象UIViewController
2.弹出控制器
2.1 监听cell的点击
2.2 创建图片浏览器控制器对象
2.3 设置图片浏览器控制器对象的弹出样式
photoBrowserVc.modalTransitionStyle = .FlipHorizontal
2.4 把控制器modal出来
十.布局图片浏览器
1.布局UICollectionView
1.1 创建UICollectionView
1.2 把UICollectionView添加到控制器的View上
1.3 设置数据源
1.4 自定义布局
2.布局两个按钮
2.1 创建两个按钮
2.2 设置按钮的frame
2.3 对UIButton进行extension(扩展)
2.31 为什么要进行扩展
创建出来的按钮,要设置图片,字体,和文字,一个个设置太麻烦,想让按钮创建出来就有这些属性
2.32 怎么进行扩展?
对UIButton进行extension(扩展) 扩充一个类型方法,在类方法里面封装好这些属性
class func createBtn(title : String, bgColor : UIColor, fontSize : CGFloat) -> UIButton { let btn = UIButton() btn.backgroundColor = bgColor btn.setTitle(title, forState: .Normal) btn.titleLabel?.font = UIFont.systemFontOfSize(fontSize) return btn }
2.4 这样创建还不是很方便,我们可以给UIbutton扩展构造函数,创建的时候直接设置这些属性
2.41 注意:在extension中扩充构造函数,只能扩充便利构造函数
2.42 什么是便利构造函数?
1.必须在init前面加上convenience
2.必须在init方法中 调用self.init()
1 convenience init(title : String, bgColor : UIColor, fontSize : CGFloat) { 2 self.init() 3 4 setTitle(title, forState: .Normal) 5 backgroundColor = bgColor 6 titleLabel?.font = UIFont.systemFontOfSize(fontSize) 7 }
3.监听按钮的点击
3.1 xcode7.2 和xcode7.3中监听方法的写法不太一样
Xcode7.2 --> 1> Selector("方法的名称") 2> ""
Xcode7.3 --> #selector(类.方法名称)
3.2 如果点击按钮调用的方法前面加上private 调用会报错
3.21 为什么会报错
找不到方法
3.22 监听事件实质就是发送一条消息
3.23 发送消息的过程是:
1.将消息包装成@SEL 2.通过@SEL去类中的方法列表中找对相应的方法(函数)
3.34 在swift中,如果一个函数前面加上private,那么该函数就不会被添加到消息(映射)列表中
3.35 如果在private前面加上@objc ,就会保留oc的特性, 该方法依然会添加到消息列表中
3.3 解决问题的方法就是 在private前面加上@objc 或者不写private
十一.传递数据
1.传递什么数据?
要在PhotoBrowserVc中查看大图,首先要拿到图片数据
2.怎么传递数据?直接传递图片?
直接传递图片url也可以,不过要从模型数组中抽离出来,不太好
最好的做法是:直接把模型数组传递给PhotoBrowserVc
十二.自定义PhotoBrowserCell,用于展示数据
1.PhotoBrowserVc是UICollectionViewController,要想展示图片,需要在cell上添加UIImageView
注意:如果一个构造函数前有required,那么重写了其他构造函数时,那么该构造函数也必须被重写
1 // MARK:- 重写构造函数 2 override init(frame: CGRect) { 3 super.init(frame: frame) 4 5 setupUI() 6 } 7 // required : 如果一个构造函数前有required,那么重写了其他构造函数时,那么该构造函数也必须被重写 8 required init?(coder aDecoder: NSCoder) { 9 fatalError("init(coder:) has not been implemented") 10 }
2.要想展示图片,需要设置什么?
2.1 设置UIImageView的image
直接从 SDWebImage缓存中取出原来cell的图片(小图)
注意:取出的图片类型是可选类型,要先进行判断再使用
2.2 设置UIImageView的frame
2.21 根据取出的图片的尺寸,计算图片的frame(设置UIImageView的宽度等于屏幕宽度)
2.22 让图片的宽度等于UIImageView的宽度
2.23 UIImageView的高度,就等于 图片高度 * UIImageView的宽度 / 图片宽度 (让图片等宽高比拉伸)
3.加载高清图片
3.1 为什么要加载高清图片?
上面取出的图片是小图,不清晰. 查看大图的时候,要换成高清图片
3.2 怎么设置?
用SDWebImage加载大图,把小图设置为占位图片
占位图片:图片还没加载的时候,先用内存中的一张图片显示到屏幕上,加载好图片, 就显示加载的图片
4.设置完成后,查看大图,发现滚动到后面,发现图片被压缩了,为什么?
4.1 因为在MainVc(首页)展示小图的时候,给小图也设置了占位图片
4.2 在PhotoBrowserVc中查看大图,滚动到后面的时候,MainVc中的cell还没显示,小图就不会被加载,就把占位图片赋值给小图
1 // 2.获取小图片 2 var smallImage = SDWebImageManager.sharedManager().imageCache.imageFromDiskCacheForKey(shop.q_pic_url) 3 if smallImage == nil { 4 smallImage = UIImage(named: "empty_picture") 5 }
4.3 这是UIImageView的尺寸就是根据占位图片的尺寸计算出来的,跟实际图片的尺寸会有差别,实际显示的图片就可能被压缩
5.怎么解决图片压缩为题?
大图请求成功时,重新计算UIImageView的尺寸就可以了
十三.把collectionView滚动到正确的位置
1.为什么要滚动collectionView?
PhotoBrowserVc的cell是从第0个cell开始显示的, 所以每次点击查看大图都是从第0张图片开始显示
当点击MainVc(首页)cell的时候,要显示对应的大图,不一定是第0张图片
2.怎么滚动?
2.1 在PhotoBrowserVc中定义indexPath属性, 在MainVc中拿到cell的indexPath,对 PhotoBrowserVc的indexPath赋值
2.2 滚动到对应的位置(用下面这个方法)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .CenteredHorizontally, animated: false)
2.3 滚动代码应该写到哪里?
点击MainVc的cell,就弹出查看大图控制器(PhotoBrowserVc),就要滚动要对应的位置
所以,代码可以写到viewDidLoad里面
3. ??的使用
1 // ?? : 先判断前面的可选链是否有值, 如果有值,解包并且获取对应类型的值. 如果没有值直接取后面的值 2 return shops?.count ?? 0
十四.设置大图之间的间距
1.怎么设置大图之间的间距?用 minimumLineSpacing?
不可以,虽然能让大图之间有间距,但是会把后面的cell往后移 ,后面的cell就不能完全显示在屏幕上
2.思考:可以collectionView的cell的宽度比屏幕宽度大一点,多出来的宽度就当做间距
不可行,collectionView(scrollView)的分页效果,会让用户看到多出来的那部分
scrollView分页效果的滚动距离 是由scrollView的宽度来决定的
3.最终解决方案
3.1 只用一句代码就可以搞定,把控制器的view的宽度增大一点就可以了
view.frame.size.with += 15
3.2 注意collectionView的宽度和 cell的宽度 要等于控制器的view的宽度才可以
4.全局函数的定义
4.1 什么是全局函数?
就是在工程目录下的任何地方都能使用的函数
4.2 怎么定义全局函数?
只要把函数定义到AppDelegate里面就可以了
十五.保存图片
1.先要拿到对应的图片,根据indexPath拿?
点击查看大图后可能会被滚动,所以不能根据indexPath拿
2.怎么拿到正在显示的图片
2.1 先拿到正在显示的cell
2.2 cell里面保存的就有image
3.怎么拿到cell
可以通过苹果自带的api拿到正在显示的cell
1 // 1.1.拿到正在显示的Cell 2 // visibleCells 返回所有在屏幕中显示的Cell 3 let cell = collectionView.visibleCells()[0] as! PhotoBrowserViewCell 4 guard let image = cell.imageView.image else { 5 return 6 }
4.保存图片到相册
苹果自带api保存到相册
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
十六.点击大图关闭控制器
1.需求:点击大图或关闭按钮,把控制器dismiss掉
2.点击按钮关闭
给按钮设置点击方法就可以了 addTarget
3.点击大图关闭怎么实现?
点击大图相当于点击了cell,在cell代理方法里面dismiss即可
十七.自定义转场(淡入淡出)
1.怎么自定义转场动画?
遵守转场的代理协议UIViewControllerTransitioningDelegate,实现代理方法
2.实现了代理方法,发现程序还是报错,为什么?
代理方法都有一个返回值,返回值要遵守一个协议UIViewControllerContextTransitioning才能作为返回值
3.在UIViewControllerContextTransitioning代理方法里面设置动画(具体看代码)
4.设置消失动画
4.1 设置完显示动画后,消失的时候也自动会有一个动画效果,为什么?
因为,大图view消失的时候,也是主控制器的view显示的时候
看到的消失动画,实际上是主控制器的view的显示动画
4.2 怎么判断是显示,还是消失?
定义一个属性记录即可 在UIViewControllerTransitioningDelegate代理方法中记录
1 // MARK:- 遵守转场的代理协议,和实现对应的方法 2 extension HomeViewController : UIViewControllerTransitioningDelegate { 3 // 为弹出控制器做一个动画 4 func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 5 //记录当前为显示阶段 6 isPresented = true 7 return self 8 } 9 10 // 为消失控制器做一个动画 11 func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 12 //记录当前为消失阶段 13 isPresented = false 14 return self 15 } 16 } 17 18 extension HomeViewController : UIViewControllerAnimatedTransitioning { 19 // 返回动画执行的时间 20 func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { 21 return 3 22 } 23 24 // transitionContext : 转场上下文 25 // 作用 : 可以通过上下文获取到弹出的View和消失的View 26 // UITransitionContextFromViewKey : 获取消失的View 27 // UITransitionContextToViewKey : 获取弹出的View 28 func animateTransition(transitionContext: UIViewControllerContextTransitioning) { 29 if isPresented { 30 // 获取弹出的View 31 let presentedView = transitionContext.viewForKey(UITransitionContextToViewKey)! 32 //需要把view添加到父控件上,才能有动画效果 33 //父控件就是widow的containerView, 通过transitionContext.containerView()拿到 34 transitionContext.containerView()?.addSubview(presentedView) 35 36 // 修改View alpha值 37 presentedView.alpha = 0.0 38 39 // 执行动画 40 UIView.animateWithDuration(transitionDuration(transitionContext), animations: { 41 presentedView.alpha = 1.0 42 }) { (isFinished : Bool) in 43 //告诉控制器,转场动画完成 44 transitionContext.completeTransition(isFinished) 45 } 46 } else { 47 // 1.获取消失的View 48 let dismissedView = transitionContext.viewForKey(UITransitionContextFromViewKey)! 49 50 // 2.执行动画 51 UIView.animateWithDuration(transitionDuration(transitionContext), animations: { 52 dismissedView.alpha = 0.0 53 }, completion: { (isFinished : Bool) in 54 //移除view,显示主控制器的view 55 dismissedView.removeFromSuperview() 56 transitionContext.completeTransition(isFinished) 57 }) 58 } 59 } 60 }
5.性能优化,代码抽取
5.1 把转场动画的代理设置为主控制器,代理要全部写在主控制器中,代码臃肿,阅读性差
5.2 怎么优化?
把代理设置为其它对象,让其它对象实现代理方法即可
5.3 具体实现步骤
5.31 创建一个对象(任何对象)
5.32 设置这个对象为转场动画的代理
5.33 在对象中实现代理方法即可
5.34 注意:代理属性为弱引用,要让一个强引用指向它
十八.最终动画效果
1.想要做动画,必须要拿到三个元素
1.1 图片的起始位置(相对于控制器view的坐标系)
1.2 图片的终点位置(相对于控制器view的坐标系)
1.3 转场图片的父控件UIImageView
2.弹出动画
2.1 获取动画的三个元素
图片的起始位置,终点位置和UIImageView 只有主控制器(mainVc)最清楚,可以定义代理
2.2 mainVc成为动画代理对象的代理,提供三个元素
3.消失动画
3.1 消失的时候,图片的终点位置有可能发生变化,需要重新计算
3.2 怎么计算消失的图片的终点位置?
只要拿到对应cell的indexPath就可以计算位置
3.3 怎么拿到indexPath?
cell的indexPath只有PhotoBrowserVc最清楚,可以设置代理,让PhotoBrowserVc提供indexPath
indexPath就是最后显示在屏幕上cell的indexPath
// 1.获取在屏幕中显示的cell
let cell = collectionView.visibleCells()[0]
// 2.获取cell对应的indexPath
let indexPath = collectionView.indexPathForCell(cell)!
3.4 根据indexPath计算终点位置,完成动画
4.性能优化
4.1 当查看大图滚动的时候,indexPath会大于屏幕上显示的indexPath最大值,这个时候,就获取不到终点位置,就会没有消失动画(直接消失)
不滚动的时候消失的时候,图片是不清晰的图片
4.2 为什么直接消失?
不滚动的时候,设置的图片是小图,所以不清晰
返回的时候获取不到cell,获取不到cell就直接返回空的ImageView
ImageView还没有设置图片就直接返回了
4.3 怎么解决?
在PhotoBrowserVc中可以拿到高清图片
设置代理拿到Image,消失动画的时候,直接显示高清图片
4.4 消失的时候,发现还是直接消失为什么?
因为滚动到后面,mainVc的cell不在屏幕上,就获取不到cell, 所以消失时获取的startRect = CGRectZero
从0消失到0 所以没有动画
4.5 点击mainVc最后一个cell,看看大图,往后滚,返回的时候,发现消失动画最终消失到左上角为什么?
因为,后面的cell还没出现,就获取不到最终位置,系统默认在左上角
4.6 怎么解决?
方法一: 当超出的时候,给定一个终点位置,让它在指定的位置消失
效果可以,但是满足不了需求
4.7 最终方案(参考微信的解决方案)
当获取不到终点位置的时候,让图片消失的动画 设置为渐变消失动画
十九.版本适配bug的解决
1.当项目运行到6s Plus上的时候,collectionView只能显示两列(需求是三列)
产生bug的原因是苹果对临界值得处理不太好
具体来说就是,屏幕宽度三等分,得到的数值是无限循环小数,苹果会根据数据类型对小数向前进一位
这时,屏幕的宽度就不足以放三个cell,就会把第三个cell挤到下一行显示,就变成了两列
2.bug解决
让得到的cell的宽度减去一个临界值小数即可(0.000001) 这个数值随便写
项目源代码: Photo-Browser