开源项目源码分析(Kickstarter-iOS )(一)

  • 1.Kickstarter开源项目简介
  • 2. Kickstarter项目结构
    • 2.1 Makefile 文件
    • 2.2 Git submodule
    • 2.3 脚本工具
      • 2.3.1 ColorScript脚本
      • 2.3.2 StringsScript脚本
    • 2.4 测试工具
    • 2.5 独立的代码库
  • 3. Kickstarter项目MVVM架构
    • 3.1 MVVM架构思想简介
    • 3.2 MVVM架构实际运用
      • 3.2.1 使用 [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift)
      • 3.2.2 UIView
      • 3.2.3 UIViewController
      • 3.2.4 ViewModel
      • 3.2.5 Model
      • 3.2.6 bindViewModel()
      • 3.2.7 使用 inputs 和 outputs 区分数据的输入和输出
      • 3.2.8
    • 3.3 Environment
      • 3.3.1 Environment
      • 3.3.2 AppEnvironment
    • 3.4 网络请求的处理
      • 3.4.1 Service+RequestHelpers
      • 3.4.2 Deep Linking
    • 3.5 用 Storyboard / Xib 创建 UI
    • 3.6 PDF 格式的图标
    • 3.7 单元测试
      • 3.7.1 Model
      • 3.7.2 ViewModel
      • 3.7.3 UI 测试

1.Kickstarter开源项目简介

  • 2016年12月15日,知名众筹平台Kickstarter在工程博客中宣布,将开源Android和iOS端的源代码,从而为初创企业提供更多的便利和帮助。由于这个伟大的决定我们这些小白才能有机会学习大神之作,Kickstarter是一个非常NB的项目,值得我们去研究它的源码:点击这里下载Kickstarter项目源码
  • Kickstarter IOS app源码下载
  • Kickstarter Android app源码下载

我们自己去啃这些开源源码是有一点难度的,这里介绍一本很好的书籍: Raywenderlich 的一本书 《Advanced iOS App Architecture》在介绍 MVVM 架构的时候,说到 Kickstarter 很彻底地遵循了 MVVM 架构。

  • 首先第一感觉是代码非常令人爽心悦目,因为代码非常整洁。再仔细一看,发现里面有很多值得学习的地方,项目使用swift5.0编写,真正的使用MVVM架构模式,架构清晰,非常值得学习。

  • 下面看几张项目架构图片:

  • 用到的框架:

  • 用到的第三方工具:

    • CircleCI:是一个持续集成的持续部署的工具,可以让开发者们更容易、更快地构建、测试和部署应用程序。
    • SwiftLint:一个检查 Swift 代码风格的工具,这可以说是 Swift 开发必备的工具。使用方法大家可以查看文档。
    • fastlane: 是一个开源平台,旨在简化 Android 和 iOS 的部署。他可以让我们自动化开发和发布的工作流程。
  • MVVM模式

2. Kickstarter项目结构

2.1 Makefile 文件

  • 在把项目 clone 下来之后,我们一般首先会想着怎么把它运行起来。在项目的 readme 中的 Getting Started 我们可以看到,运行 make bootstrap安装工具和依赖,运行 make test-all 构建项目并进行测试。而这两个命令就是在 Makefile 中定义的。

ios git clone地址:https://github.com/kickstarter/ios-oss
android git clone地址:https://github.com/kickstarter/android-oss

  • 打开 Makefile 文件,我们可以从中看到:1)文件的开头定义了各种变量;2)剩下的是项目中用到的命令。我们以 make bootstrap 为例:
bootstrap: hooks dependenciesbrew update || brew updatebrew unlink swiftlint || truebrew install https://raw.githubusercontent.com/Homebrew/homebrew-core/686375d8bc672a439ca9fcf27794a394239b3ee6/Formula/swiftlint.rbbrew switch swiftlint 0.29.2brew link --overwrite swiftlint
  • 执行 make bootstrap ,就会依次执行 bootstrap 下面包含的所有命令。
  • 使用 Makefile 的好处是,我们可以把项目相关的一些命令操作都放到这个文件,即便是刚刚接手项目的同事也一目了然。

2.2 Git submodule

  • 下载源码用xcode打开后你肯定会好奇,这么NB的工程居然没有用cocoapod,那么项目没有使用第三方框架么?
  • 答案是否定的。把项目 clone 下来之后,我们确实会发现文件夹里面没有我们常用的 Podfilexcworkspace 文件。然而,Kickstarter 不是用 Cocoapods 来管理第三方库的,而是使用 git submodule
  • 其实除了上面提到的两个管理第三方框架的工具之后,还可以用 Carthage 来管理第三方库。找到一篇文章:对比Carthage和Cocoapods和Git submodule,描述了这三种工具的优缺点。
  • 至于选择哪一种,就看我们更看重的是什么了。我一般都是使用的cocoapods.

2.3 脚本工具

  • 在根目录下的 bin 目录,我们可以看到两个用 Swift 编写的脚本:ColorScriptStringsScript

2.3.1 ColorScript脚本

  • 开发者把项目中用到的颜色,保存在Colors.json文件,然后通过 ColorScript 转换成 Colors.swift文件。开发者在使用的时候只需要通过 UIColor.ksr_dark_grey_400就能得到相应的颜色了。后续如果 UI 设计师想要微调颜色,直接修改颜色, json 中的 key 的值不变,我们只需要重新生成 Colors.swift就都搞定了,而不需要更改代码。
{"apricot_600": "FFCBA9","cobalt_500": "4C6CF8","dark_grey_400": "9B9E9E","dark_grey_500": "656868","facebookBlue": "3B5998",...
}
  • 这种统一管理颜色的方法,我觉得其实就是把颜色管理的工作交给 UI 设计师了。设计师写好 json 文件,交给开发者,开发者用脚本生成 Colors.swift,就一切都搞定了(如果颜色名字有变动或有新添加的颜色,还是需要开发者手动更改和添加)。如果不通过这种方法去做,而是开发者自己手动去写,那么可能会经常去手动修改 Colors.swift,这样就麻烦一些。

2.3.2 StringsScript脚本

  • 做过国际化的开发者应该知道,如果不通过其他处理的话,我们需要通过 NSLocalizedString("Hello_World", comment: "") 去获取对应的本地化字符串,这种写法非常麻烦,而且很容易出错。
  • 在 Kickstarter-iOS 中,开发者用 StringsScriptLocalizable.strings转换生成 Strings.swift 文件,然后我们在使用的时候,就可以像这样去获取想要的字符串 Strings. Hello_World()。这个脚本把 key 变成了方法名,让我们避免了在使用的时候出现错误,而且使用起来非常方便。
  • 如果有做本地化的项目,采用这种方法可以给开发者带来很大的便利。

2.4 测试工具

  • 测试,是软件开发中非常重要的一个环节。甚至有些公司执行 TDD (测试驱动开发(Test-Driven Development)),可以见测试的重要性。
  • 在 Kickstarter-iOS 中,我们可以看到大量的 xxxTests.swift文件,包括了 Unit TestUI Test

2.5 独立的代码库

  • 用 Xcode 打开 Kickstarter-iOS 的项目,你会发现 KsApiLibraryLiveStream这三个文件夹不是存放在 Kickstarter-iOS文件夹里面的,而是跟它处于同一个目录。因为这三个文件夹存放的是独立于 Kickstarter-iOS 之外的 framework

  • 这么做的好处当然是代码可以复用。目前我看 iPad 上的 Kickstarter 应用是跟 iPhone 共用一个的,如果以后要为 iPad 单独做一个 app,这三个 frameworks 就可以直接拿过去用。

3. Kickstarter项目MVVM架构

3.1 MVVM架构思想简介

  • MVVM架构思想
    这里有一个讲解 MVVM & TDD 的视频。感兴趣的可以看一下。

  • 函数响应式编程思想
    代表主要有Rxswift, RAC, ReactiveSwift

  • ReactiveSwift 是一个响应式编程的库,与 RxSwift 类似,这两个库非常适用于 MVVM 架构。至于要选择哪一种,可以先去了解下他们的差别,然后再决定.这里有篇很好的文章讲解了他们的区别:How does ReactiveSwift relate to RxSwift?

  • Kickstarter-iOS 把 MVVM 模式贯彻地非常彻底。MVVM 的全称是 Model-View-ViewModel,所以我们可能会觉得要有 View 存在的地方,才可以用 ViewModel。但是 Kickstarter-iOS 在 AppDelegate 中也使用了 ViewModel,把很多在 AppDelegate 处理的逻辑剥离到 AppDelegateViewModelType 中。

3.2 MVVM架构实际运用

3.2.1 使用 ReactiveSwift

3.2.2 UIView

  • 对于 UIView,Kickstarter 通过扩展重写 awakeFromNib(),在内部调用 bindViewModel()。代码如下:
extension UIView {open override func awakeFromNib() {super.awakeFromNib()self.bindViewModel()}@objc open func bindViewModel() {}
}
  • 因为 Kickstarter 在整个项目中都是通过 xib 来构建 UI 的,所以 UI 在初始化时,awakeFromNib()会被调用,从而 bindViewModel() 也被调用。那么在其他继承自 UIViewview 中,只需要重写 bindViewModel(),就能达到绑定 ViewModel 的目的。

3.2.3 UIViewController

  • UIViewController 中就会稍微复杂一点。Kickstarter 通过 runtime,默认在 viewDidLoad() 中调用 bindViewModel()。那么在其他继承自 UIViewControllerViewController 中,只需要重写 bindViewModel(),就能达到绑定 ViewModel 的目的。

  • UIViewController-Preparation.swift相关代码如下:

private func swizzle(_ vc: UIViewController.Type) {[(#selector(vc.viewDidLoad), #selector(vc.ksr_viewDidLoad)),(#selector(vc.viewWillAppear(_:)), #selector(vc.ksr_viewWillAppear(_:))),(#selector(vc.traitCollectionDidChange(_:)), #selector(vc.ksr_traitCollectionDidChange(_:))),].forEach { original, swizzled inguard let originalMethod = class_getInstanceMethod(vc, original),let swizzledMethod = class_getInstanceMethod(vc, swizzled) else { return }let didAddViewDidLoadMethod = class_addMethod(vc,original,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod))if didAddViewDidLoadMethod {class_replaceMethod(vc,swizzled,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod))} else {method_exchangeImplementations(originalMethod, swizzledMethod)}}
}private var hasSwizzled = falseextension UIViewController {final public class func doBadSwizzleStuff() {guard !hasSwizzled else { return }hasSwizzled = trueswizzle(self)}@objc internal func ksr_viewDidLoad() {self.ksr_viewDidLoad()self.bindViewModel()}/**The entry point to bind all view model outputs. Called just before `viewDidLoad`.*/@objc open func bindViewModel() {}
}
  • 然后在 AppDelegate.swift中的 didFinishLaunchingWithOptions调用 doBadSwizzleStuff(),代码如下:
func application(_ application: UIApplication,didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {UIViewController.doBadSwizzleStuff()
}
  • 通过这两个处理,就能避免编写大量的重复代码

3.2.4 ViewModel

  • 我从项目中找了一个代码量比较少的 ViewModel 文件 HelpWebViewModel.swift,以这个文件为例。具体代码如下:
import Library
import Prelude
import ReactiveSwift
import Resultinternal protocol HelpWebViewModelInputs {/// Call to configure with HelpType.func configureWith(helpType: HelpType)/// Call when the view loads.func viewDidLoad()
}internal protocol HelpWebViewModelOutputs {/// Emits a request that should be loaded into the webview.var webViewLoadRequest: Signal<URLRequest, NoError> { get }
}internal protocol HelpWebViewModelType {var inputs: HelpWebViewModelInputs { get }var outputs: HelpWebViewModelOutputs { get }
}internal final class HelpWebViewModel: HelpWebViewModelType, HelpWebViewModelInputs, HelpWebViewModelOutputs {internal init() {self.webViewLoadRequest = self.helpTypeProperty.signal.skipNil().takeWhen(self.viewDidLoadProperty.signal).map { urlForHelpType($0, baseUrl: AppEnvironment.current.apiService.serverConfig.webBaseUrl) }.skipNil().map { AppEnvironment.current.apiService.preparedRequest(forURL: $0) }}internal var inputs: HelpWebViewModelInputs { return self }internal var outputs: HelpWebViewModelOutputs { return self }internal let webViewLoadRequest: Signal<URLRequest, NoError>fileprivate let helpTypeProperty = MutableProperty<HelpType?>(nil)func configureWith(helpType: HelpType) {self.helpTypeProperty.value = helpType}fileprivate let viewDidLoadProperty = MutableProperty(())func viewDidLoad() {self.viewDidLoadProperty.value = ()}
}private func urlForHelpType(_ helpType: HelpType, baseUrl: URL) -> URL? {switch helpType {case .cookie:return baseUrl.appendingPathComponent("cookies")case .contact:return nilcase .helpCenter:return baseUrl.appendingPathComponent("help")case .howItWorks:return baseUrl.appendingPathComponent("about")case .privacy:return baseUrl.appendingPathComponent("privacy")case .terms:return baseUrl.appendingPathComponent("terms-of-use")case .trust:return baseUrl.appendingPathComponent("trust")}
}
  • 对于 ViewModel,我想说两点:1)使用 ReactiveSwift;2)使用 inputs 和 outputs 区分数据的输入和输出。

3.2.5 Model

3.2.6 bindViewModel()

  • 在 MVVM 架构中,一般来说 ViewModel 是被 UIViewUIViewController 持有,而持有 ViewModel 的对象就需要绑定到 ViewModel,这样就能响应 ViewModel 中数据的变化,从而更新 UI。一般我们都会在持有 ViewModel 的对象中定义一个方法 bindViewModel(),并且在这个方法里面做绑定。
  • Kickstarter 分别在 UIViewUIViewController 做了一些处理,让程序在启动的时候就默认在各自内部的方法调用了 bindViewModel(),这样可以避免在很多的 ViewViewController 中写重复的代码。

3.2.7 使用 inputs 和 outputs 区分数据的输入和输出

  • ViewModel 中,我们需要接受外部的信息输入,并且告诉外部有哪些信息发生了变化。
  • Kickstarter-iOS 把信息的输入和输出分别用 HelpWebViewModelInputsHelpWebViewModelOutputs 分开,这样在使用 ViewModel 的时候就会非常清晰,不会把 inputsoutputs 混在一起。例如,我们在Xcode 中编写 viewModel.outputs. 时,Xcode 只会提示 webViewLoadRequest,而不会把属于 inputsviewDidLoad()也显示给我们。
  • 这在我们使用 ViewModel 的时候带来了极大的便利,并且让看代码的人一目了然,哪些代码处理输入,哪些代码处理输出,非常清晰。

3.2.8

3.3 Environment

  • 有经验的 iOS 开发者应该都知道,在开发过程中我们需要设计一些对象来存储应用的全局状态,例如当前的登录用户等等。而在 Kickstarter-iOS 中,EnvironmentAppEnvironment 就是干这事的。

3.3.1 Environment

  • 打开这个文件,从注释可以看到,Environment 是应用所需要的全局变量和单例的集合。仔细分析里面属性的定义,我们可以发现很多都是属于 protocol 类型的,例如:
public let apiService: ServiceType
public let cookieStorage: HTTPCookieStorageProtocol
public let device: UIDeviceType
public let ubiquitousStore: KeyValueStoreType
public let userDefaults: KeyValueStoreType
  • 这么做的好处是当有需要的时候,可以随时替换另外一个遵循对应 protocol的对象。这也就是我们所说的面向协议编程。

3.3.2 AppEnvironment

  • 刚开始看这个项目,看到有 Environment 和 AppEnvironment,可能会觉得有点困惑,为什么有了 Environment,还要搞一个AppEnvironment?下面我们来仔细看看。
  • 先看一下 AppEnvironment 里面的方法:
public struct AppEnvironment : AppEnvironmentType {internal static let environmentStorageKey: Stringinternal static let oauthTokenStorageKey: Stringpublic static func login(_ envelope: AccessTokenEnvelope)public static func updateCurrentUser(_ user: User)public static func updateServerConfig(_ config: ServerConfigType)public static func updateConfig(_ config: Config)public static func updateLanguage(_ language: Language)public static func logout()public static var current: Environment! { get }public static func pushEnvironment(_ env: Environment)public static func popEnvironment() -> Environment?public static func replaceCurrentEnvironment(_ env: Environment)// 参数太长,省略了public static func pushEnvironment(...)// 参数太长,省略了public static func replaceCurrentEnvironment(...)public static func fromStorage(ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType) -> Environmentinternal static func saveEnvironment(environment env: Environment = AppEnvironment.current, ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType)
}
  • 从上面的方法我们可以总结出,AppEnvironment是用来管理 Environment。如果我们不新建一个 AppEnvironment,那么这些管理代码就会放到 Environment,这会造成在一个 Model 上进行业务逻辑的处理,而这明显是不合理的。
  • 如果你在项目中全局搜索 pushEnvironmentpopEnvironment,你会发现,这两个方法都是在测试文件中被调用,说明这两个方法是为测试而生的。
  • 另外 AppEnvironment 还提供了 replaceCurrentEnvironment() 方法,携带了所有对应 Environment 的参数,这可以让我们很容易替换当前 Environment 的某个全局变量。例如在 AppDelegate.swift 我们可以看到:
#if DEBUGif KsApi.Secrets.isOSS {AppEnvironment.replaceCurrentEnvironment(apiService: MockService())}
#endif
  • KsApi.Secrets.isOSS 设置为 true 之后,我们就可以使用 MockService(),实在是非常方便。

3.4 网络请求的处理

  • Environment 中,可以了解到 Service 是处理应用中所有网络请求的。进入到 Service, 这里编写了所有的网络请求方法。再仔细看,你会发现很多请求是通过类似 request(.facebookConnect(facebookAccessToken: token)) 去调用的。我们就先来看看这个 request() 方法的参数 Route
  • Route 的部分代码如下:
internal enum Route {case activities(categories: [Activity.Category], count: Int?)case addImage(fileUrl: URL, toDraft: UpdateDraft)case addVideo(fileUrl: URL, toDraft: UpdateDraft)case backing(projectId: Int, backerId: Int)// ...internal var requestProperties:(method: Method, path: String, query: [String: Any], file: (name: UploadParam, url: URL)?) {switch self {case let .activities(categories, count):var params: [String: Any] = ["categories": categories.map { $0.rawValue }]params["count"] = countreturn (.GET, "/v1/activities", params, nil)case let .addImage(file, draft):return (.POST, "/v1/projects/\(draft.update.projectId)/updates/draft/images", [:], (.image, file))case let .addVideo(file, draft):return (.POST, "/v1/projects/\(draft.update.projectId)/updates/draft/video", [:], (.video, file))case let .backing(projectId, backerId):return (.GET, "/v1/projects/\(projectId)/backers/\(backerId)", [:], nil)// ...}}
}
  • 如果你打开源文件,你会发现,Route枚举编写了所有用到的请求,并且定义了 requestProperties 属性,这样我们就可以通过类似 .facebookConnect(facebookAccessToken: token)去获取到想要的请求,然后通过 requestProperties 属性,获取到请求参数,接着做进一步的网络请求。
  • 对于类似这种有多种可能情况的处理,用 enum 非常合适,而这也是开发过程中经常会遇到的。
  • 既然各种请求都准备好了,下一步就要进行真正的网络请求了,这些代码就藏在 Service+RequestHelpers.swift

3.4.1 Service+RequestHelpers

  • 这个文件暴露给外面的接口非常简单,如下:
extension Service {func fetch<A: Swift.Decodable>(query: NonEmptySet<Query>) -> SignalProducer<A, GraphError>func applyMutation<A: Swift.Decodable, B: GraphMutation>(mutation: B) -> SignalProducer<A, GraphError>func requestPagination<M: Argo.Decodable>(_ paginationUrl: String)-> SignalProducer<M, ErrorEnvelope> where M == M.DecodedTypefunc request<M: Argo.Decodable>(_ route: Route)-> SignalProducer<M, ErrorEnvelope> where M == M.DecodedTypefunc request<M: Argo.Decodable>(_ route: Route)-> SignalProducer<[M], ErrorEnvelope> where M == M.DecodedTypefunc request<M: Argo.Decodable>(_ route: Route)-> SignalProducer<M?, ErrorEnvelope> where M == M.DecodedType
}
  • 从这些方法的定义我们可以看到,全部使用了泛型,这就意味着一个方法就可以处理某一类型的请求。这六个方法就可以处理整个应用的请求,是不是觉得非常强大??
  • 这也是值得我们学习的地方。所以在开发过程中,如果发现自己在重复写类似的代码,那么可以考虑使用泛型能不能解决问题。

3.4.2 Deep Linking

  • 在开发中,我们通常需要通过 Universal LinkURL SchemePush Notification 等方式跳转到应用的某一个页面。我们来看一下 Kickstarter-iOS 是怎么处理的。
  • 打开 Navigation.swift ,跟网络请求一样,也是用 enum 定义了所有用户去往的目标页面。
  • 那在 Kickstarter-iOS 中,它是怎样通过 deep linking 传入的 url 来最终得到 Navigation 其中的一个 case,然后跳转到目标页面呢?
  • 首先,它用一个字典 allRoutes: [String: (RouteParams) -> Decoded<Navigation>] 保存了所有的 routes:其中 keyurl 的模板;value 是一个闭包,这个闭包是根据url 携带的参数解析成 Navigation
  • 然后用一个 match() 方法,把传入的 url,最终解析成Navigation 这里面最关键的一个方法是 parsedParams() ,大家可以去仔细看一下怎么实现的。
extension Navigation {public static func match(_ url: URL) -> Navigation? {return allRoutes.reduce(nil) { accum, templateAndRoute inlet (template, route) = templateAndRoutereturn accum ?? parsedParams(url: url, fromTemplate: template).flatMap(route)?.value}}
}

3.5 用 Storyboard / Xib 创建 UI

  • 以前,我们经常看到开发者们在争论:对于 UI 的创建,纯代码手写好还是用 Storyboard / Xib 好?这里就不对这个话题展开了,这么久过去了,相信各位开发者在自己的心里已经有了答案。下面我们看看 Kickstarter 是如何使用 Storyboard / Xib 来创建 UI 的。

  • 首先告诉大家,Kickstarter的 UI 几乎都是用 Storyboard / Xib 来完成的。打开 Kickstarter-iOS/Views/Storyboards 文件夹,这里存储了应用的全部 .storyboard.xib 文件。

  • 使用 Storyboard 创建 UI,最怕的就是一个 .storyboard 文件包含了太多的 ViewController。所以 Kickstarter 为每一个小模块的功能单独创建了一个 Storyboard,并且当你点开每一个 Storyboard,你会发现大部分 Storyboard 只有一个 ViewController。这也很好解决了多人同时编辑一个 Storyboard 时导致的代码冲突问题,因为我们一般不会多人同时去开发一个小模块,把 Storyboard 分得很细之后,就不会出现多人同时编辑一个 Storyboard 的情况。

  • 另外,Kickstarter 还定义了 StoryboardNib 枚举,列举了所有的 Storyboardxib 文件,方便 ViewControllerView 的初始化,这是一个非常漂亮的处理(以下代码省略了方法的具体实现)

import UIKit
public enum Storyboard: String {case Activitycase Backingcase BackerDashboard// ...public func instantiate<VC: UIViewController>(_ viewController: VC.Type,inBundle bundle: Bundle = .framework) -> VC
}
import UIKitpublic enum Nib: String {case BackerDashboardEmptyStateCellcase BackerDashboardProjectCellcase CreditCardCell// ...
}extension UITableView {public func register(nib: Nib, inBundle bundle: Bundle = .framework) public func registerHeaderFooter(nib: Nib, inBundle bundle: Bundle = .framework)
}protocol NibLoading {associatedtype CustomNibTypestatic func fromNib(nib: Nib) -> CustomNibType?
}extension NibLoading {static func fromNib(nib: Nib) -> Self?func view(fromNib nib: Nib) -> UIView?
}

3.6 PDF 格式的图标

  • 在过去的 iOS 项目中,一般都使用 png 格式的图标。而在 Kickstarter 中,使用的是 pdf 格式的图标。我们先来看下 pdf 格式的图标有什么优点?
  • PDF 的全称是 Portable Document Format,是用于正确显示文档和图形的图像格式。PDF文件具有强大的矢量图形基础,可以用来保矢量图像。矢量图像本质上是巨大的数学方程,每个点、线和形状都由自己的方程表示。每一个“方程式”都可以被指定一种颜色、笔画或厚度来将形状变成艺术。与光栅图像不同,矢量图像与分辨率无关。当你缩小或放大一个矢量图像时,你的形状会变大,但你不会丢失任何细节或得到任何像素。因为您的图像将始终以相同的方式呈现,无论大小如何,都不存在有损或无损矢量图像类型。矢量图像通常用于logo、图标、排版和数字插图。
  • 从上面我们可以了解到 pdf 格式的图标最大的优点是可以无损放大。还有,只需要一个 pdf 文件就可以代表一个图标,而png 图片一般至少需要两个(2x和 3x, 1x 一般不需要了)。除了这两个优点之外,我还发现 Kickstarter 中的 pdf 文件的大小只有 5k左右;而我们现有的项目中一个 png 图片就有 15k左右,两个 png 就 30k了,所以,使用 pdf 图片还可以一定程度上减少应用的大小。

3.7 单元测试

  • 在 Kickstarter-iOS 中,单元测试的对象主要分两类:Model 和 ViewModel。

3.7.1 Model

  • 在定义一个 Model 时,一般都会实现 Codable,并且要测试一下对于给定的 json 数据,是否可以解析成功。Kickstarter-iOS 也是这么做的:在每一个 Model 对应的测试文件里,利用假的 json 数据,测试是否可以解析成功。
  • 例如 AuthorTests.swift里:
func testJSONParsing_WithCompleteData() {let author = Author.decodeJSONDictionary(["id": 382491714,"name": "Nino Teixeira","avatar": ["thumb": "https://ksr-qa-ugc.imgix.net/thumb.jpg","small": "https://ksr-qa-ugc.imgix.net/small.jpg","medium": "https://ksr-qa-ugc.imgix.net/medium.jpg"],"urls": ["web": ["user": "https://staging.kickstarter.com/profile/382491714"],"api": ["user": "https://api-staging.kickstarter.com/v1/users/382491714"]]])XCTAssertNil(author.error)XCTAssertEqual(382491714, author.value?.id)
}

3.7.2 ViewModel

  • 在 Kickstarter-iOS 中,每个 ViewModel 都会有对应的测试。这里主要讲一下有哪些小技巧值得学习的。
  • XCTestCase+AppEnvironment.swift中, 通过扩展 XCTestCase 定义了 withEnvironment() 方法,用于替换某些全局变量,把替换后的 Environment pushstack 中作为当前的 Environment,执行完 body()后,再把刚刚 pushEnvironment 移除,这样可以保证不改变测试前后的 Environment
func withEnvironment(_ env: Environment, body: () -> Void) {AppEnvironment.pushEnvironment(env)body()AppEnvironment.popEnvironment()
}func withEnvironment(...) # 具体看文件
  • 基本上每一个 Model 都会定义一个 template 实例,用于在 ViewModel 中测试。

3.7.3 UI 测试

  • 在 Kickstarter-iOS 中,UI 测试主要是对 ViewController 的测试,看看 UI 的显示是否有问题。
  • 因为 Kickstarter 支持多语言,并且 iOS 设备有多种尺寸,所以定义了一个 combos 方法,用于组合各种语言和尺寸:
internal func combos<A, B>(_ xs: [A], _ ys: [B]) -> [(A, B)] {return xs.flatMap { x inreturn ys.map { y inreturn (x, y)}}
}
  • 另外还定义了一个方法,根据设备的大小和朝向最终把传入的 controller 转变成对应设备大小的 controller。
internal func traitControllers(device: Device = .phone4_7inch,orientation: Orientation = .portrait,child: UIViewController = UIViewController(),additionalTraits: UITraitCollection = .init(),handleAppearanceTransition: Bool = true)-> (parent: UIViewController, child: UIViewController)
  • 最后再用 FBSnapshotTestCase 生成各种尺寸语言组合的截图,具体代码如下:
func testAddNewCard() {combos(Language.allLanguages, Device.allCases).forEach { language, device inwithEnvironment(language: language) {let controller = AddNewCardViewController.instantiate()let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)FBSnapshotVerifyView(parent.view, identifier: "lang_\(language)_device_\(device)")}}
}
  • 这个测试就会生成以下截图:

开源项目源码分析(Kickstarter-iOS )(一)相关推荐

  1. 【Android Camera2】Camera2开源项目源码分析汇总

    一.简介 本篇文章为综述汇总类文章,包含后续文章即将分析的使用Camera2实现的开源项目 相关文章: Android Camera系列文章目录索引汇总 Android Camera2 综述 二.开源 ...

  2. 分析开源项目源码,我们该如何入手分析?(授人以渔)

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:牛人 20000 字的 Spring Cloud 总结,太硬核了~ 1 前言 本文接上篇文章跟大家聊聊我们为什么 ...

  3. java web开源项目源码_超赞!推荐一个专注于Java后端源码分析的Github项目!

    大家好,最近有小伙伴们建议我把源码分析文章及源码分析项目(带注释版)放到github上,这样小伙伴们就可以把带中文注释的源码项目下载到自己本地电脑,结合源码分析文章自己本地调试,总之对于学习开源项目源 ...

  4. GitHub上最火的22个Android开源项目源码(最少的一个也超过10k star)

    GitHub上最火的22个Android开源项目源码均超万星 chat图表 最全android工具类库 29.6k start Android智能下拉刷新框架-SmartRefreshLayout 2 ...

  5. halo 开源项目源码学习

    目的 看开源项目的目的无非就两个,看别人的代码组织结构.看别人的用到得到技术,还有就是看别人踩过的坑. 感受 就我看halo项目的感觉而言.感觉就是注释几乎就没有用.我看似乎这个项目国人挺多的怎么一句 ...

  6. bilibili go项目源码分析

    go 项目源码 约329个Go服务, 历史约170人左右贡献过Go代码. 代码和目录规范性比较好, 代码生成工具建设比较好, 大家可以借鉴一下. 对于一个Golang开发者来说, 入职B站, 我觉得大 ...

  7. Phenotips 项目源码分析 [0]

    PhenoTips™ is a software tool for collecting and analyzing phenotypic information for patients with ...

  8. Unity huatuo示例项目源码分析与启发

    上一节我们安装huatuo的开发环境,然后运行示例项目,体验了huatuo做热更新,这节课我们来分析示例项目的源码,掌握huatuo做热更新的主要的步骤,让你自己的项目很好的基于huatuo来组织热更 ...

  9. git bash here创建项目无法选择m_由GitLab用户切换引发的某程序员“暴动”,怒而开源项目源码...

    疯狂吐槽,咱也不知道为什么,原来GitHub用的好好的,我自己的项目也会上传到自己的码云,但是,谁想到,今天老大跟我说,让我把一个项目代码传到gitlab上,哎,又要切换账户了,所以,今天分享两部分内 ...

最新文章

  1. CentOS7 自定义登录前后欢迎信息
  2. day12_oracle hint——SQL优化过程中常见Oracle中HINT的30个用法
  3. char varchar java_在数据库中varchar与char的区别
  4. 蓝桥杯-卡片-填空题
  5. td 内单选框不可用_在TD,我和曾经的老师变成了同事,也收获了最满意的“课外活动”...
  6. 工作128:element上传组件时候的钩子--event里面有数据参数
  7. mysql originator_MySQL数据库事件调度(Event)
  8. elementUI响应式布局@media:基于断点的隐藏类
  9. Nginx之location配置
  10. java邮件程序实例_java 发送邮件简单实例
  11. Git教程——临时修改 (stash)
  12. 网络基石 —— 双绞线、水晶头与 MIC
  13. BFS 算法框架套路详解
  14. linux如何设置显示器亮度调节软件,为 Linux 启用色温和亮度调节工具
  15. SpringBoot集成DM数据库
  16. CH模拟赛 皇后游戏
  17. Win10玩红警2突然就卡住不动?
  18. Javascript 之 事件冒泡(Event Bubbling)
  19. Opencv图像处理之平滑(Smoothing)模糊(Blurring)操作
  20. Button 按钮:防连点,节流防抖

热门文章

  1. 无线桥接(WDS)如何设置?
  2. WeakHashMap总结
  3. php 打印对象到文件,php中file_put_contents()将数组或对象写入到文件
  4. 答题类小程序(数学题类) 2018/4/27更新
  5. Javascript省份城市(html代码)
  6. 你的ES还在裸奔吗?还不赶紧开启X-Pack权限认证
  7. 《贪心算法》— NYOJ 586 疯牛
  8. 舞钢LY225钢板低屈服抗震钢板介绍
  9. 人大进仓v8r3安装
  10. 汽车安全转向柱有什么样的特点