Tempura 9.0.3

Tempura 9.0.3

Bending Spoons 维护。



 
依赖
HydraAsync< 3, >= 2.0.6
Katana< 7, >= 6.0
 

Tempura 9.0.3

  • Bending Spoons

Tempura by Bending Spoons

Build Status CocoaPods PRs Welcome Licence

Tempura 是一种全面的 iOS 开发方法,它借鉴了 Redux(通过 Katana)和 MVVM 的概念。

🎯安装

Tempura 通过 CocoaPods 提供。

要求

  • iOS 11+
  • Xcode 11.0+
  • Swift 5.0+

CocoaPods

CocoaPods 是 Cocoa 项目的依赖管理器。您可以使用以下命令安装它

$ sudo gem install cocoapods

为了使用 CocoaPods 在您的 Xcode 项目中集成 Tempura,您需要创建一个包含以下内容的 Podfile

use_frameworks!
source 'https://cdn.cocoapods.org/'
platform :ios, '11.0'

target 'MyApp' do
  pod 'Tempura'
end

现在只需运行

$ pod install

Swift 包管理器

自版本 9.0.0 以来,Tempura 亦支持 Swift Package Manager (SPM)

🤔为什么我应该使用这个?

Tempura 允许您

  1. 模型化应用状态
  2. 定义可以改变它的动作
  3. 创建 UI
  4. 享受状态和UI之间的自动同步
  5. 发货,迭代

我们开始在弯曲勺子的小团队内部使用Tempura。它为我们工作得非常好,以至于我们最终开发并维护了超过二十个高质量的APP,去年有超过1000万活跃用户使用这种方法。崩溃率和开发时间下降,用户参与度和质量提高。我们非常满意,想与iOS社区分享这一消息,希望您也能像我们一样兴奋。❤️

Splice Thirty Day Fitness Pic Jointer Yoga Wave

👩‍💻给我看看代码

Tempura使用Katana来处理应用逻辑。APP状态由单个结构体定义。

struct AppState: State {

  var items: [Todo] = [
    Todo(text: "Pet my unicorn"),
    Todo(text: "Become a doctor.\nChange last name to Acula"),
    Todo(text: "Hire two private investigators.\nGet them to follow each other"),
    Todo(text: "Visit mars")
  ]
}

您只能通过状态更新器操作状态。

struct CompleteItem: StateUpdater {
  var index: Int

  func updateState(_ state: inout AppState) {
    state.items[index].completed = true
  }
}

屏幕UI所需的状态部分由ViewModelWithState选择。

struct ListViewModel: ViewModelWithState {
  var todos: [Todo]

  init(state: AppState) {
    self.todos = state.todos
  }
}

应用每个屏幕的UI由ViewControllerModellableView组成。它公开回调(我们称之为交互),表示发生了用户操作。它根据ViewModelWithState渲染自身。

class ListView: UIView, ViewControllerModellableView {
  // subviews
  var todoButton: UIButton = UIButton(type: .custom)
  var list: CollectionView<TodoCell, SimpleSource<TodoCellViewModel>>

  // interactions
  var didTapAddItem: ((String) -> ())?
  var didCompleteItem: ((String) -> ())?

  // update based on ViewModel
  func update(oldModel: ListViewModel?) {
    guard let model = self.model else { return }
    let todos = model.todos
    self.list.source = SimpleSource<TodoCellViewModel>(todos)
  }
}

每个应用屏幕由ViewController管理。默认情况下,它将自动监听状态更新并保持UI同步。ViewController的唯一其他职责是监听来自UI的交互并将动作分发给更改状态。

class ListViewController: ViewController<ListView> {
  // listen for interactions from the view
  override func setupInteraction() {
    self.rootView.didCompleteItem = { [unowned self] index in
      self.dispatch(CompleteItem(index: index))
    }
  }
}

注意,ViewController的dispatch方法与由 Katana store 公开的略有不同:它接受简单的Dispatchable且不返回任何内容。这样做是为了避免在ViewController中实现逻辑。

如果交互处理程序需要执行多项操作,请将所有这些逻辑打包在副作用中并分发。

对于极少数需要在小控制台中包含一些逻辑的情况(例如在无需完全重构所有逻辑的情况下更新旧应用)您可以使用以下方法:

  • open func __unsafeDispatch<T: StateUpdater>(_ dispatchable: T) -> Promise<Void>
  • open func __unsafeDispatch<T: ReturningSideEffect>(_ dispatchable: T) -> Promise<T.ReturningValue>

然而请注意,强烈不鼓励使用这些方法,它们将在将来的版本中移除。

导航

真正的应用是由多个屏幕组成的。如果一个屏幕需要展示另一个屏幕,它的ViewController必须符合RoutableWithConfiguration协议。

extension ListViewController: RoutableWithConfiguration {
  var routeIdentifier: RouteElementIdentifier { return "list screen"}

  var navigationConfiguration: [NavigationRequest: NavigationInstruction] {
    return [
      .show("add item screen"): .presentModally({ [unowned self] _ in
        let aivc = AddItemViewController(store: self.store)
        return aivc
      })
    ]
  }
}

然后您可以使用ViewController中的导航操作来触发展示。

self.dispatch(Show("add item screen"))

有关导航的更多信息,请参阅这里

ViewController包含

您可以在ViewController内部嵌套其他ViewController,如果您想重用它的一部分UI及逻辑,这会非常方便。为了实现这一点,在父ViewController中您需要提供一个ContainerView,它将接收子ViewController的视图作为子视图。

class ParentView: UIView, ViewControllerModellableView {
    var titleView = UILabel()
    var childView = ContainerView()
    
    func update(oldModel: ParentViewModel?) {
      // update only the titleView, the childView is managed by another VC
    }
}

然后,在父ViewController中,您只需添加子ViewController

class ParentViewController: ViewController<ParentView> {
  let childVC: ChildViewController<ChildView>!
    
  override func setup() {
    self.childVC = ChildViewController(store: self.store)
    self.add(childVC, in: self.rootView.childView)  
  }
}

所有自动化设置都会自动完成。现在您将在ParentViewController中拥有一个ChildViewController,子ViewController的视图将托管在childView中。

📸UI 快照测试

Tempura具有一个快照测试系统,可用于捕获您的视图在所有可能状态、所有设备和所有支持的语言下的屏幕截图。

使用说明

您需要在应用测试目标中包含TempuraTesting pod

target 'MyAppTests' do
  pod 'TempuraTesting'
end

在您的plist中指定截图的放置位置

UI_TEST_DIR: $(SOURCE_ROOT)/Demo/UITests

在Xcode中创建一个新的UI测试用例类

文件 -> 新建 -> 文件... -> UI 测试用例类

在这里,您可以使用test函数对具有特定ViewModelViewControllerModellableView进行快照。

import TempuraTesting

class UITests: XCTestCase, ViewTestCase {
  
  func testAddItemScreen() {
    self.uiTest(testCases: [
      "addItem01": AddItemViewModel(editingText: "this is a test")
    ])
  }
}

标识符将定义文件系统中快照图像的名称。

您还可以通过上下文参数个性化视图渲染方式(例如,您可以嵌套视图到UINavigationController实例中)。以下是一个将视图嵌入到标签栏中的示例。

import TempuraTesting

class UITests: XCTestCase, ViewTestCase {
  
  func testAddItemScreen() {
    var context = UITests.Context<AddItemView>()
    context.container = .tabBarController


    self.uiTest(testCases: [
      "addItem01": AddItemViewModel(editingText: "this is a test")
    ], context: context)
  }
}

如果在UIScrollView中的重要内容没有完全可见,则可以借助scrollViewsToTest(in view: V, identifier: String)方法。这将产生额外的快照,渲染每个返回的UIScrollView实例的完整内容。

在这个示例中,我们使用scrollViewsToTest(in view: V, identifier: String)来捕获屏幕底部情绪选择器的扩展快照。

func scrollViewsToTest(in view: V, identifier: String) -> [String: UIScrollView] {
  return ["mood_collection_view": view.moodCollectionView]
}

如果您必须在渲染UI和捕获截图之前等待异步操作,则可以借助isViewReady(view:identifier:)方法。例如,这里我们等待显示远程URL图片的假设视图准备工作。当图片显示(即,状态为loaded)时,则进行快照。

import TempuraTesting

class UITests: XCTestCase, ViewTestCase {
  
  func testAddItemScreen() {
    self.uiTest(testCases: [
      "addItem01": AddItemViewModel(editingText: "this is a test")
    ])
  }

  func isViewReady(_ view: AddItemView, identifier: String) -> Bool {
    return view.remoteImage.state == .loaded
  }
}

一旦快照被捕获,测试将通过。

上下文

您可以通过传递给 uiTest 方法的 context 对象来启用多个高级特性。

  • container 允许您在 UITests 期间将 VC 定义为视图的容器。基本的 navigationControllertabBarController 已经提供,或者您可以使用 custom 来自定义一个。
  • hooks 允许在发生某些生命周期事件时执行操作。可用的挂钩包括 viewDidLoadviewWillAppearviewDidAppearviewDidLayoutSubviewsnavigationControllerHasBeenCreated
  • screenSizeorientation 属性允许您定义测试期间要使用的自定义屏幕大小和方向。
  • renderSafeArea 允许您定义是否在测试期间将安全区域渲染为半透明的灰色叠加层。
  • keyboardVisibility 允许您定义是否将灰色叠加层渲染为键盘的占位符。

多设备

默认情况下,测试只在你从 Xcode (或你的设备,或 CI 系统中) 选择设备上运行。我们可以使用如下脚本来在所有设备上运行快照:

xcodebuild \
  -workspace <project>.xcworkspace \
  -scheme "<target name>" \
  -destination name="iPhone 5s" \
  -destination name="iPhone 6 Plus" \
  -destination name="iPhone 6" \
  -destination name="iPhone X" \
  -destination name="iPad Pro (12.9 inch)" \
  test

测试将在所有设备上并行运行。如果您想更改行为,请参阅 xcodebuild 文档。

如果您想在 UI 测试中测试特定语言,可以将 test 命令替换为 -testLanguage <ISO code639-1>。应用将以此语言启动,并使用该区域设置执行 UITests。例如:

xcodebuild \
  -workspace <project>.xcworkspace \
  -scheme "<target name>" \
  -destination name="iPhone 5s" \
  -destination name="iPhone 6 Plus" \
  -destination name="iPhone 6" \
  -destination name="iPhone X" \
  -destination name="iPad Pro (12.9 inch)" \
  -testLanguage it

远程资源

经常发生 UI 需要显示远程内容(即远程图像、远程视频等)。在执行 UITests 时,这可能是一个问题,因为:

  • 网络或服务器问题可能导致测试失败。
  • 系统应在远程资源加载时进行跟踪,将它们放入 UI,然后再截图。

为了解决这个问题,Tempura 提供了一个名为 URLProtocol 的子类 LocalFileURLProtocol,它尝试从您的本地包加载远程文件。

想法是在您的(测试)包中放入所有需要渲染 UI 的资源,而 LocalFileURLProtocol 将尝试加载它们而不是进行网络请求。

对于给定的 URL,LocalFileURLProtocol 使用以下规则匹配文件名:

  • 搜索与 URL 同名的文件(例如,http://example.com/image.png
  • 搜索与最后一个路径组件同名的文件(例如,image.png)
  • 搜索最后一个路径组件不带扩展名的文件名(例如,image)

如果无法检索到匹配的文件,则执行网络调用。

要在您的应用程序中注册 LocalFileURLProtocol,您必须在测试生命周期中尽快调用以下 API。

URLProtocol.registerClass(LocalFileURLProtocol.self)

请注意,如果您使用的是 Alamofire,则可能会导致无法正常工作。请在此 找到相关问题和配置Alamofire以处理 URLProtocol 类的链接。

使用 ViewController containment 进行 UI 测试

ViewTestCase 主要用于测试带有自动注入代表着视图测试条件的 ViewModelViewControllerModellableView

如果您使用的是 ViewController containment(如上面的 ParentView 示例),则在注入 ViewModel 时,视图的一部分将不会更新,因为存在另一个 ViewController 负责这部分。

在这种情况下,您需要使用 ViewControllerTestCase 协议在 ViewController 层级上进行扩展和测试。

class ParentViewControllerUITest: XCTestCase, ViewControllerTestCase {
  /// provide the instance of the ViewController to test
  var viewController: ParentViewController {
    let fakeStore = Store<AppState, EmptySideEffectDependencyContainer>()
    let vc = ParentViewController(store: testStore)
    return vc
  }
  
  /// define the ViewModels
  let viewModel = ParentViewModel(title: "A test title")
  let childVM = ChildViewModel(value: 12)
  
  /// define the tests we want to perform
  let tests: [String: ParentViewModel] = [
    "first_test_vc": viewModel
  ]
    
  /// configure the ViewController with ViewModels, also for the children VCs
  func configure(vc: ParentViewController, for testCase: String, model: ParentViewModel) {
    vc.viewModel = model
    vc.childVC.viewModel = childVM
  }
    
  /// execute the UI tests
  func test() {
    let context = UITests.VCContext<ParentViewController>(container: .none)
    self.uiTest(testCases: self.tests, context: context)  
  }
}

如果您没有要配置的子 ViewController,那就更简单了,您不需要提供 configure(:::) 方法。

class ParentViewControllerUITest: XCTestCase, ViewControllerTestCase {
  /// provide the instance of the ViewController to test
  var viewController: ParentViewController {
    let fakeStore = Store<AppState, EmptySideEffectDependencyContainer>()
    let vc = ParentViewController(store: testStore)
    return vc
  }
  
  /// define the ViewModel
  let viewModel = ParentViewModel(title: "A test title")
  
  /// define the tests we want to perform
  let tests: [String: ParentViewModel] = [
    "first_test_vc": viewModel
  ]
    
  /// execute the UI tests
  func test() {
    let context = UITests.VCContext<ParentViewController>(container: .tabbarController)
    self.uiTest(testCases: self.tests, context: context)  
  }
}

🧭从这里开始

示例应用程序

该存储库包含了一个用 Tempura 实现的任务清单应用程序的演示。要生成 Xcode 项目文件,您可以使用 Tuist。运行 tuist generate,打开项目并运行 Demo 目标。

查看文档

文档

📄Swift 版本

Tempura 的某些版本仅支持某些 Swift 版本。根据您的项目使用的 Swift 版本,您应使用 Tempura 的特定版本。使用此表来检查您需要的 Tempura 版本。

Swift 版本 Tempura 版本
Swift 5.0 Tempura 4.0+
Swift 4.2 Tempura 3.0
Swift 4 Tempura 1.12

📬取得联系

如果您有任何 疑问反馈,我们很乐意在 [email protected]听到您的声音。

🙋‍♀️贡献

  • 如果您 发现了错误,请创建一个问题;
  • 如果您有 功能请求,请创建一个问题;
  • 如果您 想要贡献,请提交一个拉取请求;
  • 如果您 有关于改进框架或传播知识的好想法,请 联系我们
  • 如果您想 尝试将该框架用于您的项目 或编写一个演示,请发送 repo 的链接。

👩‍⚖️许可证

Tempura遵循MIT许可证

关于

Tempura由Bending Spoons维护。我们创建了自身的技术产品,这些产品被世界各地数百万人所使用和喜爱。听起来很酷?了解我们