开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用 (上)git
开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(二)第三方框架使用 (中)github
@[TOC]算法
这篇博客是上篇博客“开源项目分析(SwiftHub)Rxswift + MVVM + Moya 架构分析(一)第三方框架使用” 的续集,因为篇幅过程,拆成几部分了。spring
先回顾一下第三方框架图:json
它提供了一些预约义的动画绑定,并提供了一种灵活的机制,让您能够添加本身的预约义动画,并在与RxCocoa绑定时使用它们。swift
当与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握手,握手过程大体以下:
- 客户端发起握手请求,携带随机数、支持算法列表等参数。
- 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
- 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥加密。
- 服务端经过私钥获取随机数信息。
- 双方根据以上交互的信息生成session ticket,用做该链接后续数据传输的加密密钥。
第3步中,客户端须要验证服务端下发的证书,验证过程有如下两个要点:
- 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
- 客户端须要检查证书的domain域和扩展域,看是否包含本次请求的host。 若是上述两点都校验经过,就证实当前的服务端是可信任的,不然就是不可信任,应当中断当前链接。
当客户端直接使用IP地址发起请求时,请求URL
中的host
会被替换成HTTP
DNS
解析出来的IP,因此在证书验证的第2步,会出现domain
不匹配的状况,致使SSL/TLS握手不成功。
更多详情请参考我以前写的一篇关于HTTPS自签名证书上传下载文件的博客:
IOS 网络协议(一) 自签名证书HTTPS文件上传下载(上)
以下图:
过程详解: ![]()
- ①客户端的浏览器向服务器发送请求,并传送客户端SSL 协议的版本号,加密算法的种类,产生的随机数,以及其余服务器和客户端之间通信所须要的各类信息。
- ②服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其余相关信息,同时服务器还将向客户端传送本身的证书。
- ③客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过时,发行服务器证书的CA 是否可靠,发行者证书的公钥可否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。若是合法性验证没有经过,通信将断开;若是合法性验证经过,将继续进行第四步。
- ④用户端随机产生一个用于通信的“对称密码”,而后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中得到)对其加密,而后将加密后的“预主密码”传给服务器。
- ⑤若是服务器要求客户的身份认证(在握手过程当中为可选),用户能够创建一个随机数而后对其进行数据签名,将这个含有签名的随机数和客户本身的证书以及加密过的“预主密码”一块儿传给服务器。
- ⑥若是服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥可否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验若是没有经过,通信马上中断;若是验证经过,服务器将用本身的私钥解开加密的“预主密码”,而后执行一系列步骤来产生主通信密码(客户端也将经过一样的方法产生相同的主通信密码)。
- ⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通信的加解密通信。同时在SSL 通信过程当中还要完成数据通信的完整性,防止数据通信中的任何变化。
- ⑧客户端向服务器端发出信息,指明后面的数据通信将使用的步骤. ⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
- ⑨服务器向客户端发出信息,指明后面的数据通信将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
- ⑩SSL 的握手部分结束,SSL 安全通道的数据通信开始,客户和服务器开始使用相同的对称密钥进行数据通信,同时进行通信完整性的检验。
//取出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 目前支持这些类型的资源:
没有使用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
- 在Xcode中:单击文件列表中的项目,在“目标”下选择目标,单击“构建阶段”选项卡,经过单击左上角的小加号图标添加新的运行脚本阶段
- 将新的运行脚本阶段拖动到编译源阶段之上,并在检查pod清单之下。锁定,展开并粘贴如下脚本:
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"
- 将
$TEMP_DIR/rswift-lastrun
添加到“输入文件”中,将$SRCROOT/R.generated.swift
添加到构建阶段的“输出文件”中- 创建你的项目,在Finder中你会看到一个
R.generated.swift
在$SRCROOT
文件夹中,拖动R.generated.swift
文件到你的项目中,若是须要,取消勾选Copy项