-
自定义单元格
import UIKit
import RxSwift
import RxCocoa
//单元格类
class MyTableCell: UITableViewCell {
var disposeBag: DisposeBag?
//单元格尾部的活动指示器
var indicator:UIActivityIndicatorView!
//当前显示的数据
var movieRecord:MovieRecord! {
didSet{
let disposeBag = DisposeBag() //保证 cell 被重用的时候不会被多次订阅
//设置标题
self.textLabel?.text = movieRecord.name
//先判断图片当前的状态
if movieRecord.state == .filtered || movieRecord.state == .failed {
//之前已经处理完毕的就直接显示出来
self.imageView?.image = movieRecord.image
// 也可以保存到缓存中,在这里从缓存中拿数据
// let img = QSFileManager.qs_readFromFile(in: .cache, contentType: .data, fileName: movieRecord.name + ".txt")
// self.imageView?.image = UIImage.init(data: img as! Data)
}else{
//只要图片状态是没处理完毕的,一律先显示个占位符图片
self.imageView?.image = UIImage(named: "placeholder")
//显示活动指示器
self.indicator.startAnimating()
//订阅序列
Observable.of(movieRecord)
.delay(0.3, scheduler: MainScheduler.instance) //延迟0.3秒
.observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))//后台
.flatMap { return self.fetchImage($0) } //下载电影图片
.flatMap { self.applySepiaFilter($0) } //给图片添加滤镜
.observeOn(MainScheduler.instance) //回到主线程显示图片
.subscribe(onNext: {
self.imageView?.image = $0.image
self.indicator.stopAnimating()
})
.disposed(by: disposeBag) //cell离开可视区域后自动取消订阅
}
self.disposeBag = disposeBag
}
}
//单元格重用时调用
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil //保证 cell 被重用的时候不会被多次订阅
}
//初始化
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
//为了提示用户,将cell的accessory view设置为UIActivityIndicatorView。
indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.accessoryView = indicator
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//获取图片
func fetchImage(_ movie:MovieRecord) -> Observable {
//如果当前的电影条目还未获取图片则去获取图片
if movie.state == .new {
if let imageData = try? Data(contentsOf: movie.url){
movie.image = UIImage(data:imageData)
movie.state = .downloaded //图片状态改成下载成功
}else{
movie.image = UIImage(named: "failed")! //未加载到海报显示默认的“暂无图片”
movie.state = .failed //图片状态改成失败
}
}
return Observable.of(movie)
}
//给图片添加棕褐色滤镜
func applySepiaFilter(_ movie:MovieRecord) -> Observable {
//只有刚下载的图片需要处理
if movie.state == .downloaded {
let inputImage = CIImage(data:UIImagePNGRepresentation(movie.image!)!)
let context = CIContext(options:nil)
let filter = CIFilter(name:"CISepiaTone")
filter!.setValue(inputImage, forKey: kCIInputImageKey)
filter!.setValue(0.8, forKey: "inputIntensity")
if let outputImage = filter!.outputImage {
let outImage = context.createCGImage(outputImage, from: outputImage.extent)
movie.image = UIImage(cgImage: outImage!)
movie.state = .filtered //图片状态改成添加滤镜成功
} else {
movie.image = UIImage(named: "failed")! //处理失败的海报显示默认的“暂无图片”
movie.state = .failed //图片状态改成失败
}
// 图片处理完保存到缓存中
// _ = QSFileManager.qs_writeToFile(in: .cache, data: NSData.init(data: UIImagePNGRepresentation(movie.image!)!) as AnyObject, fileName: movie.name + ".txt")
}
return Observable.of(movie)
}
}
-
QSFileManager
/**
* 把数据写入文件
*
* @param pathType 放在哪个路径下,Document或Cache,默认 unKnown
* @param data 要写入的数据
* @param fileName 文件名,可以一个文件名,也可以传一个路径,如果pathType为unKnown,需要传入完整路径
*
* return 是否写入成功
*/
class func qs_writeToFile(in pathType: QSPathType = .unKnown, data: AnyObject, fileName: String) -> Bool {
// 获取路径
var path = String.init()
switch pathType {
case .document:
path = self.qs_getDocumentPath()
case .cache:
path = self.qs_getCachePath()
case .unKnown:
path = fileName
}
// 文件路径
if !fileName.hasPrefix("/") {
path.append("/")
}
let filePath = path + fileName
return data.write(toFile: filePath, atomically: true)
}
/**
* 读取文件内容
*
* @param pathType 放在哪个路径下,Document或Cache,默认 unKnown
* @param contentType 文件内容的类型,默认 string
* @param fileName 文件名,可以一个文件名,也可以传一个路径,如果pathType为unKnown,需要传入完整路径
*
* return 返回文件中数据,拿不到数据返回nil
*/
class func qs_readFromFile(in pathType: QSPathType = .unKnown, contentType: QSFileContentType = .string, fileName: String) -> AnyObject? {
// 获取路径
var path = String.init()
switch pathType {
case .document:
path = self.qs_getDocumentPath()
case .cache:
path = self.qs_getCachePath()
case .unKnown:
path = fileName
}
// 文件路径
if !fileName.hasPrefix("/") {
path.append("/")
}
let filePath = path + fileName
switch contentType {
case .array:
return NSArray.init(contentsOfFile: filePath)
case .dictionary:
return NSDictionary.init(contentsOfFile: filePath)
case .string:
return try? String.init(contentsOfFile: filePath, encoding: String.Encoding.utf8) as AnyObject
case .data:
return try? NSData.init(contentsOfFile: filePath) as AnyObject
}
}
-
Cell属性的记录(MovieRecord)
import UIKit
// 这个枚举包含所有电影图片的状态
enum MovieRecordState {
case new, downloaded, filtered, failed
}
// 电影条目类
class MovieRecord {
let name:String //电影标题
let url:URL //电影海报图片地址
var image:UIImage? //电影海报图片
var state = MovieRecordState.new //图片当前状态
init(name:String, url:URL) {
self.name = name
self.url = url
}
}
-
ViewController
//创建表格视图
self.tableView = UITableView(frame: self.view.frame, style:.plain)
//创建一个重用的单元格
self.tableView!.register(MyTableCell.self, forCellReuseIdentifier: "Cell")
//单元格无法选中
self.tableView.allowsSelection = false
//设置单元格高度
self.tableView!.rowHeight = 100
self.view.addSubview(self.tableView!)
//数据源地址
let dataSourcePath = Bundle.main.path(forResource: "movies", ofType: "plist")
let datasourceDictionary = NSDictionary(contentsOfFile: dataSourcePath!)
//初始化数据
for(key,value) in datasourceDictionary!{
let name = key as? String
let url = URL(string:value as? String ?? "")
if name != nil && url != nil {
let movieRecord = MovieRecord(name:name!, url:url!)
self.movies.append(movieRecord)
}
}
//设置单元格数据(其实就是对 cellForRowAt 的封装)
Observable.just(movies)
.bind(to: tableView.rx.items) { (tableView, row, element) in
//初始化cell
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
as! MyTableCell
//设置单元格数据
cell.movieRecord = element
//返回单元格
return cell
}
.disposed(by: disposeBag)
参考文章:Swift - RxSwift的使用详解65(表格图片加载优化)
Swift - 表格图片加载优化(拖动表格时不加载,停止时只加载当前页图片)