swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)

时间:2022-04-22 23:16:27
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)

项目需求:做一个图片浏览器,点击图片查看大图,大图模式下,左右滚动能查看不同的图片.
项目的主要核心技术:图片的弹出和消失动画
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
项目源代码: Photo-Browser
一.对代码进行重构
1.对代码进行抽取划分
     1.1 为什么要对代码进行抽取?
          swift中,代码全部写在一起,阅读性极差
2.如何对代码进行抽取?
     2.1在oc中,可以把功能模块抽取一个个方法
     2.2swift中,专门提供 extension ,可以对原有的类进行扩展
3.怎么使用extension 抽取代码?
    3.1 把一些方法写在extension(扩展)里面,这样能减少viewDidLoad里面的代码
     3.2 extension可以写多个,这样就可以把不同的功能模块 ,写在不同的扩展里面
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
二.项目基本设置
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里面 把自定义布局的类名写进去即可
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
四.网络工具类的封装
1.集成CocoaPods, 并导入AFNetworking框架
     1.1 打开终端,进入项目路径下  cd  路径
     1.2 创建PodFile文件  pod init
     1.3 配置PodFile文件 ,写入要导入的框架
 swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入) 
     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 可以让工具类,直接继承自用到框架的一个类
          好处:自己就是这个类的子类,拥有这个类的所有方法和属性,用的时候直接自己就能调用
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
3.封装网络请求方法
 func requestData (type : Int , urlString : String , parameters : [ String : NSObject] , callBack : (result : AnyObject? , error : NSErroe?) -> ()   )
func reqeustData(type : RequestType, urlString : String, parameters : [String : NSObject], finishedCallback : (result : AnyObject?, error : NSError?) -> ()) { }
4.把方法里面的闭包抽取出来
     4.1 为什么要抽取?
          方法里面闭包很长,代码很乱,造成阅读性差
     4.2 怎么抽取?
          定义一个成员属性  为闭包类型   把方法里面的闭包,用属性名 替换
swift项目初体验--教你打造一款个性化图片浏览器(篇幅过大,慎入)
五.项目集成工具类
     把封装好的工具类,直接拖到项目文件中
六.请求网络数据
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 - {
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()
     convenience init(title : String, bgColor : UIColor, fontSize : CGFloat) {
self.init() setTitle(title, forState: .Normal)
backgroundColor = bgColor
titleLabel?.font = UIFont.systemFontOfSize(fontSize)
}
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,那么重写了其他构造函数时,那么该构造函数也必须被重写
     // MARK:- 重写构造函数
override init(frame: CGRect) {
super.init(frame: frame) setupUI()
}
// required : 如果一个构造函数前有required,那么重写了其他构造函数时,那么该构造函数也必须被重写
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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还没显示,小图就不会被加载,就把占位图片赋值给小图       
    // 2.获取小图片
var smallImage = SDWebImageManager.sharedManager().imageCache.imageFromDiskCacheForKey(shop.q_pic_url)
if smallImage == nil {
smallImage = UIImage(named: "empty_picture")
}
   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.  ??的使用
 // ?? : 先判断前面的可选链是否有值, 如果有值,解包并且获取对应类型的值. 如果没有值直接取后面的值
return shops?.count ??
十四.设置大图之间的间距
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.拿到正在显示的Cell
// visibleCells 返回所有在屏幕中显示的Cell
let cell = collectionView.visibleCells()[] as! PhotoBrowserViewCell
guard let image = cell.imageView.image else {
return
}
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代理方法中记录
 // MARK:- 遵守转场的代理协议,和实现对应的方法
extension HomeViewController : UIViewControllerTransitioningDelegate {
// 为弹出控制器做一个动画
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//记录当前为显示阶段
isPresented = true
return self
} // 为消失控制器做一个动画
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//记录当前为消失阶段
isPresented = false
return self
}
} extension HomeViewController : UIViewControllerAnimatedTransitioning {
// 返回动画执行的时间
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return
} // transitionContext : 转场上下文
// 作用 : 可以通过上下文获取到弹出的View和消失的View
// UITransitionContextFromViewKey : 获取消失的View
// UITransitionContextToViewKey : 获取弹出的View
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if isPresented {
// 获取弹出的View
let presentedView = transitionContext.viewForKey(UITransitionContextToViewKey)!
//需要把view添加到父控件上,才能有动画效果
//父控件就是widow的containerView, 通过transitionContext.containerView()拿到
transitionContext.containerView()?.addSubview(presentedView) // 修改View alpha值
presentedView.alpha = 0.0 // 执行动画
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
presentedView.alpha = 1.0
}) { (isFinished : Bool) in
//告诉控制器,转场动画完成
transitionContext.completeTransition(isFinished)
}
} else {
// 1.获取消失的View
let dismissedView = transitionContext.viewForKey(UITransitionContextFromViewKey)! // 2.执行动画
UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
dismissedView.alpha = 0.0
}, completion: { (isFinished : Bool) in
//移除view,显示主控制器的view
dismissedView.removeFromSuperview()
transitionContext.completeTransition(isFinished)
})
}
}
}

 

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