这篇博客是上篇博客“ 开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用 ” 的续集,由于篇幅过程,拆成几部分了。
先回顾一下第三方框架图:
为RxCocoa的绑定提供了动画接口
它提供了一些预定义的动画绑定,并提供了一种灵活的机制,让您可以添加自己的预定义动画,并在与RxCocoa绑定时使用它们。
当与RxCocoa绑定值时,你可以这样写:
textObservable .bind(to: labelFlip.rx.text) 复制代码
每当observable发出一个新的字符串值时,它都会更新标签的文本。但这是突然发生的,没有任何过渡。使用RxAnimated,你可以使用animated扩展来绑定值和动画,就像这样:
textObservable .bind(animated: labelFlip.rx.animated.flip(.top, duration: 0.33).text) 复制代码
“不同之处在于”您使用 bind(animated:) 而不是 bind(to:) ,然后您插入 animated.flip(.top, duration: 0.33) (或其他提供或自定义动画方法之一)之间的rx和属性接收器你想使用。
UIView.rx.animated...isHidden UIView.rx.animated...alpha UILabel.rx.animated...text UILabel.rx.animated...attributedText UIControl.rx.animated...isEnabled UIControl.rx.animated...isSelected UIButton.rx.animated...title UIButton.rx.animated...image UIButton.rx.animated...backgroundImage UIImageView.rx.animated...image NSLayoutConstraint.rx.animated...constant NSLayoutConstraint.rx.animated...isActive 复制代码
UIView.rx.animated.fade(duration: TimeInterval) UIView.rx.animated.flip(FlipDirection, duration: TimeInterval) UIView.rx.animated.tick(FlipDirection, duration: TimeInterval) UIView.rx.animated.animation(duration: TimeInterval, animations: ()->Void) NSLayoutConstraint.rx.animated.layout(duration: TimeInterval) 复制代码
您可以轻松地添加自定义绑定动画来匹配应用程序的视觉风格。
// This is your class `UILabel`
extension AnimatedSink where Base: UILabel {
// This is your property name `text` and value type `String`
public var text: Binder<String> {
let animation = self.type!
return Binder(self.base) { label, text in
animation.animate(view: label, block: {
guard let label = label as? UILabel else { return }
// Here you update the property
label.text = text
})
}
}
}
复制代码
// This is your class `UIView`
extension AnimatedSink where Base: UIView {
// This is your animation name `tick`
public func tick(_ direction: FlipDirection = .right, duration: TimeInterval) -> AnimatedSink<Base> {
// use one of the animation types and provide `setup` and `animation` blocks
let type = AnimationType<Base>(type: RxAnimationType.spring(damping: 0.33, velocity: 0), duration: duration, setup: { view in
view.alpha = 0
view.transform = CGAffineTransform(rotationAngle: direction == .right ? -0.3 : 0.3)
}, animations: { view in
view.alpha = 1
view.transform = CGAffineTransform.identity
})
//return AnimatedSink
return AnimatedSink<Base>(base: self.base, type: type)
}
}
复制代码
UIImageView.rx.image 如下 imageObservable
.bind(to: imageView.rx.image)
复制代码
结果是非动画绑定的效果:
如果你使用你的新自定义动画绑定像这样:
imageObservable
.bind(to: imageView.rx.animated.tick(.right, duration: 0.33).image)
复制代码
修改后的效果是这样的:
如果你在UILabel上使用相同的动画:
textObservable
.bind(to: labelCustom.rx.animated.tick(.left, duration: 0.75).text)
复制代码
效果如下:
pod "RxAnimated" 复制代码
url 会话的网络或本地提供的数据加载图像。 UIImageView , NSImageView , NSButton 和 UIButton 的视图扩展来直接从 URL 设置图像。 SwiftUI 支持。 let url = URL(string: "https://example.com/image.png") imageView.kf.setImage(with: url) 复制代码
Kingfisher 将从 url 下载图像,将其发送到内存缓存和磁盘缓存,并在 imageView 中显示。当您稍后使用相同的 URL 设置时,图像将从缓存中检索并立即显示。
如果你使用SwiftUI也可以这样写:
import KingfisherSwiftUI
var body: some View {
KFImage(URL(string: "https://example.com/image.png")!)
}
复制代码
此外, Kingfisher 还提供了一些高阶用法,用于解决复杂的问题,有了这些强大的选项,您可以用简单的方式用 Kingfisher 完成困难的任务。 例如,下面的代码:
let url = URL(string: "https://example.com/high_resolution_image.png")
let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
>> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.indicatorType = .activity
imageView.kf.setImage(
with: url,
placeholder: UIImage(named: "placeholderImage"),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(1)),
.cacheOriginalImage
])
{
result in
switch result {
case .success(let value):
print("Task done for: /(value.source.url?.absoluteString ?? "")")
case .failure(let error):
print("Job failed: /(error.localizedDescription)")
}
}
复制代码
上面代码做了这些操作:
func clearCache() {
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache()
}
复制代码
//显示菊花
imageView.kf.indicatorType = .activity
imageView.kf.setImage(with: url, placeholder: nil, options: [.transition(ImageTransition.fade(1))], progressBlock: { (receviveeSize, totalSize) in
print("/(receviveeSize)//(totalSize)")
}) { (image, error, cacheType, imageURL) in
print("Finished")
// 加载完成的回调
// image: Image? `nil` means failed
// error: NSError? non-`nil` means failed
// cacheType: CacheType
// .none - Just downloaded
// .memory - Got from memory cache
// .disk - Got from disk cache
// imageUrl: URL of the image
}
复制代码
imageView.kf.indicatorType = .activity imageView.kf.setImage(with: url) //使用自己的gif图片作为下载指示器 let path = Bundle.main.path(forResource: "loader", ofType: "gif")! let data = try! Data(contentsOf: URL(fileURLWithPath: path)) imageView.kf.indicatorType = .image(imageData: data) imageView.kf.setImage(with: url) 复制代码
struct KYLIndicator: Indicator {
let view: UIView = UIView()
func startAnimatingView() {
view.isHidden = false
}
func stopAnimatingView() {
view.isHidden = true
}
init() {
view.backgroundColor = .red
}
}
let indicator = KYLIndicator()
imageView.kf.indicatorType = .custom(indicator: indicator)
复制代码
imageView.kf.setImage(with: url, options: [.transition(.fade(0.2))]) 复制代码
let processor = RoundCornerImageProcessor(cornerRadius: 20) imageView.kf.setImage(with: url, placeholder: nil, options: [.processor(processor)]) 复制代码
let uiButton: UIButton = UIButton()
uiButton.kf.setImage(with: url, for: .normal, placeholder: nil, options: nil, progressBlock: nil, completionHandler: nil)
uiButton.kf.setBackgroundImage(with: url, for: .normal, placeholder: nil, options: nil, progressBlock: nil, completionHandler: nil)
复制代码
// 设置磁盘缓存大小
// Default value is 0, which means no limit.
// 50 MB
ImageCache.default.maxDiskCacheSize = 50 * 1024 * 1024
复制代码
// 设置缓存过期时间
// Default value is 60 * 60 * 24 * 7, which means 1 week.
// 3 days
ImageCache.default.maxCachePeriodInSecond = 60 * 60 * 24 * 3
复制代码
// Default value is 15.
// 30 second
ImageDownloader.default.downloadTimeout = 30.0
复制代码
其他设置相关
// 设置磁盘缓存大小
// Default value is 0, which means no limit.
// 50 MB
ImageCache.default.maxDiskCacheSize = 50 * 1024 * 1024
// 获取缓存磁盘使用大小
ImageCache.default.calculateDiskCacheSize { size in
print("Used disk size by bytes: /(size)")
}
// 设置缓存过期时间
// Default value is 60 * 60 * 24 * 7, which means 1 week.
// 3 days
ImageCache.default.maxCachePeriodInSecond = 60 * 60 * 24 * 3
// 设置超时时间
// Default value is 15.
// 30 second
ImageDownloader.default.downloadTimeout = 30.0
// Clear cache manually
// Clear memory cache right away.
cache.clearMemoryCache()
// Clear disk cache. This is an async operation.
cache.clearDiskCache()
// Clean expired or size exceeded disk cache. This is an async operation.
cache.cleanExpiredDiskCache()
复制代码
imageView.kf.setImage(with: url, options: [.forceRefresh]) 复制代码
let resource = ImageResource(downloadURL: url!, cacheKey: "kyl_cache_key") imageView.kf.setImage(with: resource) 复制代码
Kingfisher 主要由两部分组成, ImageDownloader 用于管理下载; ImageCache 用于管理缓存,你可以单独使用其中一个.
//使用ImageDownloader下载图片
ImageDownloader.default.downloadImage(with: url!, options: [], progressBlock: nil) { (image, error, url, data) in
print("Downloaded Image: /(image)")
}
// 使用ImageCache缓存图片
let image: UIImage = UIImage(named: "xx.png")!
ImageCache.default.store(image, forKey: "key_for_image")
// Remove a cached image
// From both memory and disk
ImageCache.default.removeImage(forKey: "key_for_image")
// Only from memory
ImageCache.default.removeImage(forKey: "key_for_image",fromDisk: false)
复制代码
Downloader 和 cache 代替默认的 let kyldownloader = ImageDownloader(name: "kongyulu_image_downloader")
kyldownloader.downloadTimeout = 150.0
let cache = ImageCache(name: "kyl_longer_cache")
cache.maxDiskCacheSize = 60 * 60 * 24 * 30
imageView.kf.setImage(with: url, options: [.downloader(kyldownloader), .targetCache(cache)])
// 取消下载
imageView.kf.cancelDownloadTask()
复制代码
// MARK:- 下载图片
imageView.kf.indicatorType = .activity
let cachePath = ImageCache.default.cachePath(forKey: PhotoConfig.init().cachePath)
guard let path = (try? ImageCache.init(name: "cameraPath", cacheDirectoryURL: URL(fileURLWithPath: cachePath))) ?? nil else { return }
imageView.kf.setImage(with: URL(string: smallUrlStr), placeholder:UIImage(named: "PhotoRectangle") , options: [.targetCache(path)], progressBlock: { (receivedData, totolData) in
// 这里用进度条或者绘制view都可以,然后根据 percentage% 表示进度就行了
//let percentage = (Float(receivedData) / Float(totolData)) * 100.0
//print("downloading progress is: /(percentage)%")
}) { result in
// switch result {
//
// case .success(let imageResult):
// print(imageResult)
//
// case .failure(let aError):
// print(aError)
// }
}
复制代码
let urls = ["http://www.baidu.com/image1.jpg", "http://www.baidu.com/image2.jpg"]
.map { URL(string: $0)! }
let prefetcher = ImagePrefetcher(urls: urls) {
skippedResources, failedResources, completedResources in
print("These resources are prefetched: /(completedResources)")
}
prefetcher.start()
// Later when you need to display these images:
imageView.kf.setImage(with: urls[0])
anotherImageView.kf.setImage(with: urls[1])
复制代码
可能有些朋友不太熟悉HTTPS握手的过程,要理解证书认证机制,有必要理解一下HTTPS握手过程:
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下:
第3步中,客户端需要验证服务端下发的证书,验证过程有以下两个要点:
当客户端直接使用IP地址发起请求时,请求 URL 中的 host 会被替换成 HTTP DNS 解析出来的IP,所以在证书验证的第2步,会出现 domain 不匹配的情况,导致SSL/TLS握手不成功。
更多详情请参考我之前写的一篇关于HTTPS自签名证书上传下载文件的博客:
IOS 网络协议(一) 自签名证书HTTPS文件上传下载(上)
IOS音视频(四十五)HTTPS 自签名证书 实现边下边播
如下图:
过程详解:
//取出downloader单例 let downloader = KingfisherManager.shared.downloader //信任ip为106的Server,这里传入的是一个数组,可以信任多个IP downloader.trustedHosts = Set(["192.168.1.106"]) //使用KingFisher给ImageView赋网络图片 iconView.kf.setImage(with: iconUrl) 复制代码
安装环境要求: iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ Swift 4.0+
Pod install
pod 'Kingfisher' 复制代码
后续补充...
R.swift 在Swift项目中获得强类型、自动完成的资源,如图像、字体和segue。
R.swift 使你的代码使用资源具有如下特性:
例如,使用 R.swift 之前你可能会这样写你的代码:
let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")
复制代码
使用 R.swift 之后,你可以这样写代码:
let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")
复制代码
这里有官方提供的Demo: Examples , 在realm中使用
看看自动填充的效果多酷:
自动完成图片:
编译时检查图片:
安装 R.swift 到您的项目后,您可以使用 R-struct 访问资源。如果结构是过时的,只是建立和 R.swift 将纠正任何失踪/改变/增加的资源。
R.swift 目前支持这些类型的资源:
Images
Fonts
Resource files
Colors
Localized strings
Storyboards
Segues
Nibs
Reusable cells
没有使用 R.swift 这样访问图片
let settingsIcon = UIImage(named: "settings-icon") let gradientBackground = UIImage(named: "gradient.jpg") 复制代码
使用 R.swift 后这样访问:
let settingsIcon = R.image.settingsIcon() let gradientBackground = R.image.gradientJpg() 复制代码
此外 R.swift 还支持文件夹中分组的方式:
选择“提供名称空间”分组资产结果:
然后这样使用:
let image = R.image.menu.icons.first() 复制代码
没有使用 R.swift 这样访问:
let lightFontTitle = UIFont(name: "Acme-Light", size: 22) 复制代码
使用 R.swift 后这样访问:
let lightFontTitle = R.font.acmeLight(size: 22) 复制代码
提示:系统字体也需要这个吗? 看一下 UIFontComplete 库,它有一个类似的解决方案,用于苹果公司发布的iOS字体。
没有使用 R.swift 这样访问:
let jsonURL = Bundle.main.url(forResource: "seed-data", withExtension: "json") let jsonPath = Bundle.main.path(forResource: "seed-data", ofType: "json") 复制代码
使用 R.swift 后这样访问:
let jsonURL = R.file.seedDataJson() let jsonPath = R.file.seedDataJson.path() 复制代码
没有使用 R.swift 这样访问:
view.backgroundColor = UIColor(named: "primary background") 复制代码
使用 R.swift 后这样访问:
view.backgroundColor = R.color.primaryBackground() 复制代码
没有使用 R.swift 这样访问:
let welcomeMessage = NSLocalizedString("welcome.message", comment: "")
let settingsTitle = NSLocalizedString("title", tableName: "Settings", comment: "")
// Formatted strings
let welcomeName = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Alice")
// Stringsdict files
let progress = String(format: NSLocalizedString("copy.progress", comment: ""), locale: NSLocale.current, 4, 23)
复制代码
使用 R.swift 后这样访问:
// Localized strings are grouped per table (.strings file)
let welcomeMessage = R.string.localizable.welcomeMessage()
let settingsTitle = R.string.settings.title()
// Functions with parameters are generated for format strings
let welcomeName = R.string.localizable.welcomeWithName("Alice")
// Functions with named argument labels are generated for stringsdict keys
let progress = R.string.localizable.copyProgress(completed: 4, total: 23)
复制代码
没有使用 R.swift 这样访问:
let storyboard = UIStoryboard(name: "Main", bundle: nil) let initialTabBarController = storyboard.instantiateInitialViewController() as? UITabBarController let settingsController = storyboard.instantiateViewController(withIdentifier: "settingsController") as? SettingsController 复制代码
使用 R.swift 后这样访问:
let storyboard = R.storyboard.main() let initialTabBarController = R.storyboard.main.initialViewController() let settingsController = R.storyboard.main.settingsController() 复制代码
没有使用 R.swift 这样访问:
// Trigger segue with:
performSegue(withIdentifier: "openSettings", sender: self)
// And then prepare it:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let settingsController = segue.destination as? SettingsController,
let segue = segue as? CustomSettingsSegue, segue.identifier == "openSettings" {
segue.animationType = .LockAnimation
settingsController.lockSettings = true
}
}
复制代码
使用 R.swift 后这样访问:
// Trigger segue with:
performSegue(withIdentifier: R.segue.overviewController.openSettings, sender: self)
// And then prepare it:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let typedInfo = R.segue.overviewController.openSettings(segue: segue) {
typedInfo.segue.animationType = .LockAnimation
typedInfo.destinationViewController.lockSettings = true
}
}
复制代码
提示:看看 SegueManager 库,它使segues块为基础,并与r.s ft兼容。
没有使用 R.swift 这样访问:
let nameOfNib = "CustomView" let customViewNib = UINib(nibName: "CustomView", bundle: nil) let rootViews = customViewNib.instantiate(withOwner: nil, options: nil) let customView = rootViews[0] as? CustomView let viewControllerWithNib = CustomViewController(nibName: "CustomView", bundle: nil) 复制代码
使用 R.swift 后这样访问:
let nameOfNib = R.nib.customView.name let customViewNib = R.nib.customView() let rootViews = R.nib.customView.instantiate(withOwner: nil) let customView = R.nib.customView.firstView(owner: nil) let viewControllerWithNib = CustomViewController(nib: R.nib.customView) 复制代码
(1) 重用TableViewCell
没有使用 R.swift 这样访问:
class FaqAnswerController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textCellNib = UINib(nibName: "TextCell", bundle: nil)
tableView.register(textCellNib, forCellReuseIdentifier: "TextCellIdentifier")
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let textCell = tableView.dequeueReusableCell(withIdentifier: "TextCellIdentifier", for: indexPath) as! TextCell
textCell.mainLabel.text = "Hello World"
return textCell
}
}
复制代码
使用 R.swift 后这样访问:
在可重用单元格界面生成器“属性”检查面板上,将单元格“标识符”字段设置为要注册和退出队列的相同值。
class FaqAnswerController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(R.nib.textCell)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let textCell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.textCell, for: indexPath)!
textCell.mainLabel.text = "Hello World"
return textCell
}
}
复制代码
(2) 重用CollectionViewCell
没有使用 R.swift 这样访问:
class RecentsController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
let talkCellNib = UINib(nibName: "TalkCell", bundle: nil)
collectionView?.register(talkCellNib, forCellWithReuseIdentifier: "TalkCellIdentifier")
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TalkCellIdentifier", for: indexPath) as! TalkCell
cell.configureCell("Item /(indexPath.item)")
return cell
}
}
复制代码
使用 R.swift 后这样访问:
在可重用单元格界面生成器“属性”检查面板上,将单元格“标识符”字段设置为要注册和退出队列的相同值。
class RecentsController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(R.nib.talkCell)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.talkCell, for: indexPath)!
cell.configureCell("Item /(indexPath.item)")
return cell
}
}
复制代码
CocoaPods是推荐的安装方式,因为这样可以避免在项目中包含任何二进制文件。
注意: R.swift 是一个用于构建步骤的工具,它不是一个动态库。因此,它不可能安装Carthage。
CocoaPods(推荐) 安装步骤:
pod 'R.swift' 到您的Podfile和运行 pod install "$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift" $TEMP_DIR/rswift-lastrun 添加到“输入文件”中,将 $SRCROOT/R.generated.swift 添加到构建阶段的“输出文件”中 R.generated.swift 在 $SRCROOT 文件夹中,拖动 R.generated.swift 文件到你的项目中,如果需要,取消勾选Copy项