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

2019-08-25  本文已影响0人  孔雨露

@[TOC](开源项目源码分析(Kickstarter-iOS )(一))

1.Kickstarter开源项目简介

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

Kickstarter MVVM架构

2. Kickstarter项目结构

2.1 Makefile 文件

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

bootstrap: hooks dependencies
    brew update || brew update
    brew unlink swiftlint || true
    brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/686375d8bc672a439ca9fcf27794a394239b3ee6/Formula/swiftlint.rb
    brew switch swiftlint 0.29.2
    brew link --overwrite swiftlint

2.2 Git submodule

2.3 脚本工具

2.3.1 ColorScript脚本

{
  "apricot_600": "FFCBA9",
  "cobalt_500": "4C6CF8",
  "dark_grey_400": "9B9E9E",
  "dark_grey_500": "656868",
  "facebookBlue": "3B5998",
    ...
}

2.3.2 StringsScript脚本

2.4 测试工具

2.5 独立的代码库

Kickstarter-iOS 独立框架

3. Kickstarter项目MVVM架构

3.1 MVVM架构思想简介

3.2 MVVM架构实际运用

3.2.1 使用 ReactiveSwift

3.2.2 UIView

extension UIView {
  open override func awakeFromNib() {
    super.awakeFromNib()
    self.bindViewModel()
  }
  @objc open func bindViewModel() {
  }
}

3.2.3 UIViewController

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 in

      guard 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 = false

extension UIViewController {
  final public class func doBadSwizzleStuff() {
    guard !hasSwizzled else { return }

    hasSwizzled = true
    swizzle(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() {
  }
}
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    UIViewController.doBadSwizzleStuff()
}

3.2.4 ViewModel

import Library
import Prelude
import ReactiveSwift
import Result

internal 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 nil
  case .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")
  }
}

3.2.5 Model

3.2.6 bindViewModel()

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

3.2.8

3.3 Environment

3.3.1 Environment

public let apiService: ServiceType
public let cookieStorage: HTTPCookieStorageProtocol
public let device: UIDeviceType
public let ubiquitousStore: KeyValueStoreType
public let userDefaults: KeyValueStoreType

3.3.2 AppEnvironment

public struct AppEnvironment : AppEnvironmentType {

    internal static let environmentStorageKey: String

    internal static let oauthTokenStorageKey: String

    public 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) -> Environment

    internal static func saveEnvironment(environment env: Environment = AppEnvironment.current, ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType)
}
#if DEBUG
      if KsApi.Secrets.isOSS {
        AppEnvironment.replaceCurrentEnvironment(apiService: MockService())
      }
#endif

3.4 网络请求的处理

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"] = count
      return (.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)

     // ...
    }
  }
}

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.DecodedType

  func request<M: Argo.Decodable>(_ route: Route)
    -> SignalProducer<M, ErrorEnvelope> where M == M.DecodedType

  func request<M: Argo.Decodable>(_ route: Route)
    -> SignalProducer<[M], ErrorEnvelope> where M == M.DecodedType

  func request<M: Argo.Decodable>(_ route: Route)
    -> SignalProducer<M?, ErrorEnvelope> where M == M.DecodedType
}

3.4.2 Deep Linking

extension Navigation {
  public static func match(_ url: URL) -> Navigation? {
    return allRoutes.reduce(nil) { accum, templateAndRoute in
      let (template, route) = templateAndRoute
      return accum ?? parsedParams(url: url, fromTemplate: template).flatMap(route)?.value
    }
  }
}

3.5 用 Storyboard / Xib 创建 UI

import UIKit
public enum Storyboard: String {
  case Activity
  case Backing
  case BackerDashboard
  // ...
  
  public func instantiate<VC: UIViewController>(_ viewController: VC.Type,
                                                inBundle bundle: Bundle = .framework) -> VC
}
import UIKit

public enum Nib: String {
  case BackerDashboardEmptyStateCell
  case BackerDashboardProjectCell
  case CreditCardCell
  // ...
}

extension UITableView {
  public func register(nib: Nib, inBundle bundle: Bundle = .framework) 
  public func registerHeaderFooter(nib: Nib, inBundle bundle: Bundle = .framework)
}

protocol NibLoading {
  associatedtype CustomNibType
  static func fromNib(nib: Nib) -> CustomNibType?
}

extension NibLoading {
  static func fromNib(nib: Nib) -> Self?
  func view(fromNib nib: Nib) -> UIView?
}

3.6 PDF 格式的图标

3.7 单元测试

3.7.1 Model

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

func withEnvironment(_ env: Environment, body: () -> Void) {
    AppEnvironment.pushEnvironment(env)
    body()
    AppEnvironment.popEnvironment()
}

func withEnvironment(...) # 具体看文件

3.7.3 UI 测试

internal func combos<A, B>(_ xs: [A], _ ys: [B]) -> [(A, B)] {
  return xs.flatMap { x in
    return ys.map { y in
      return (x, y)
    }
  }
}
internal func traitControllers(device: Device = .phone4_7inch,
                               orientation: Orientation = .portrait,
                               child: UIViewController = UIViewController(),
                               additionalTraits: UITraitCollection = .init(),
                               handleAppearanceTransition: Bool = true)
  -> (parent: UIViewController, child: UIViewController)
func testAddNewCard() {
    combos(Language.allLanguages, Device.allCases).forEach { language, device in
      withEnvironment(language: language) {
        let controller = AddNewCardViewController.instantiate()
        let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)

        FBSnapshotVerifyView(parent.view, identifier: "lang_\(language)_device_\(device)")
      }
    }
}
上一篇 下一篇

猜你喜欢

热点阅读