{"id":796,"url":"https://github.com/BendingSpoons/tempura-swift","last_synced_at":"2025-08-06T13:32:33.289Z","repository":{"id":46726293,"uuid":"98112046","full_name":"BendingSpoons/tempura-swift","owner":"BendingSpoons","description":"A holistic approach to iOS development, inspired by Redux and MVVM","archived":true,"fork":false,"pushed_at":"2023-04-03T16:02:24.000Z","size":9289,"stargazers_count":719,"open_issues_count":4,"forks_count":31,"subscribers_count":34,"default_branch":"master","last_synced_at":"2024-11-16T03:27:33.934Z","etag":null,"topics":["ios","katana","mvvm","redux","uikit"],"latest_commit_sha":null,"homepage":"http://bendingspoons.com/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BendingSpoons.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-07-23T16:49:45.000Z","updated_at":"2024-11-02T18:01:12.000Z","dependencies_parsed_at":"2023-10-20T18:21:42.995Z","dependency_job_id":null,"html_url":"https://github.com/BendingSpoons/tempura-swift","commit_stats":null,"previous_names":[],"tags_count":75,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BendingSpoons%2Ftempura-swift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BendingSpoons%2Ftempura-swift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BendingSpoons%2Ftempura-swift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BendingSpoons%2Ftempura-swift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BendingSpoons","download_url":"https://codeload.github.com/BendingSpoons/tempura-swift/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905476,"owners_count":17989773,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","katana","mvvm","redux","uikit"],"created_at":"2024-01-05T20:15:31.498Z","updated_at":"2024-12-09T14:30:53.556Z","avatar_url":"https://github.com/BendingSpoons.png","language":"Swift","funding_links":[],"categories":["Architecture Patterns","Libs","Uncategorized","Open source projects","Swift","Events [🔝](#readme)"],"sub_categories":["Events","Uncategorized","Other free courses","Getting Started"],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/tempura_header.png\" alt=\"Tempura by Bending Spoons\" width=\"400\" /\u003e\n\u003c/p\u003e\n\n[![Build Status](https://github.com/BendingSpoons/tempura-swift/workflows/Build%20and%20Test/badge.svg)](https://github.com/BendingSpoons/tempura-swift/actions/workflows/build_and_test.yml)\n[![CocoaPods](https://img.shields.io/cocoapods/v/Tempura.svg)]()\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields)](http://makeapullrequest.com)\n[![Licence](https://img.shields.io/badge/Licence-MIT-lightgrey.svg)](https://github.com/BendingSpoons/tempura-swift/blob/master/LICENSE.md)\n\nTempura is a holistic approach to iOS development, it borrows concepts from [Redux](https://redux.js.org/) (through [Katana](https://github.com/BendingSpoons/katana-swift)) and [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel).\n\n- [🎯 Installation](#-installation)\n  - [Requirements](#requirements)\n  - [CocoaPods](#cocoapods)\n  - [Swift Package Manager](#swift-package-manager)\n- [🤔 Why should I use this?](#-why-should-i-use-this)\n- [👩‍💻 Show me the code](#-show-me-the-code)\n  - [Navigation](#navigation)\n  - [ViewController containment](#viewcontroller-containment)\n- [📸 UI Snapshot Testing](#-ui-snapshot-testing)\n  - [Usage](#usage)\n  - [Context](#context)\n    - [Multiple devices](#multiple-devices)\n  - [Remote Resources](#remote-resources)\n  - [UI Testing with ViewController containment](#ui-testing-with-viewcontroller-containment)\n- [🧭 Where to go from here](#-where-to-go-from-here)\n  - [Example application](#example-application)\n  - [Check out the documentation](#check-out-the-documentation)\n- [📄 Swift Version](#-swift-version)\n- [📬 Get in touch](#-get-in-touch)\n- [🙋‍♀️ Contribute](#️-contribute)\n- [👩‍⚖️ License](#️-license)\n- [❓ About](#-about)\n\n## State of the project\n\nWe wrote several successful applications using the layer that `Katana` and `Tempura` provide. We still think that their approach is really a good one for medium-sized applications but, as our app grows, it becomes increasingly important to have a more modular architecture. For this reason, we have migrated our applications to use [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture).\n\n## 🎯 Installation\n\nTempura is available through [CocoaPods](https://cocoapods.org).\n\n### Requirements\n\n- iOS 11+\n- Xcode 11.0+\n- Swift 5.0+\n\n### CocoaPods\n\n[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command:\n\n```shell\n$ sudo gem install cocoapods\n```\n\nTo integrate Tempura in your Xcode project using CocoaPods you need to create a `Podfile` with this content:\n\n```ruby\nuse_frameworks!\nsource 'https://cdn.cocoapods.org/'\nplatform :ios, '11.0'\n\ntarget 'MyApp' do\n  pod 'Tempura'\nend\n```\n\nNow you just need to run:\n\n```shell\n$ pod install\n```\n\n### Swift Package Manager\n\nSince version `9.0.0`, `Tempura` also supports [Swift Package Manager (SPM)](https://swift.org/package-manager/).\n\n## 🤔 Why should I use this?\n\nTempura allows you to:\n\n1. Model your app state\n2. Define the actions that can change it\n3. Create the UI\n4. Enjoy automatic sync between state and UI\n5. Ship, iterate\n\nWe started using Tempura in a small team inside [Bending Spoons](http://bndspn.com/2HOnxis). It worked so well for us, that we ended up developing and maintaining more than twenty high quality apps, with more than 10 million active users in the last year using this approach. Crash rates and development time went down, user engagement and quality went up. We are so satisfied that we wanted to share this with the iOS community, hoping that you will be as excited as we are. ❤️\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://itunes.apple.com/app/id1099771240\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/icon1.png\" alt=\"Splice\" width=\"200\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://itunes.apple.com/it/app/id509987785\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/icon2.png\" alt=\"Thirty Day Fitness\" width=\"200\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://itunes.apple.com/it/app/id1310491340\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/icon3.png\" alt=\"Pic Jointer\" width=\"200\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://itunes.apple.com/app/id1214593569\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/icon4.png\" alt=\"Yoga Wave\" width=\"200\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## 👩‍💻 Show me the code\n\nTempura uses [Katana](https://github.com/BendingSpoons/katana-swift) to handle the logic of your app.\nYour app state is defined in a single struct.\n\n```swift\nstruct AppState: State {\n\n  var items: [Todo] = [\n    Todo(text: \"Pet my unicorn\"),\n    Todo(text: \"Become a doctor.\\nChange last name to Acula\"),\n    Todo(text: \"Hire two private investigators.\\nGet them to follow each other\"),\n    Todo(text: \"Visit mars\")\n  ]\n}\n```\n\nYou can only manipulate state through [State Updater](https://bendingspoons.github.io/katana-swift/latest/Protocols/StateUpdater.html)s.\n\n```swift\nstruct CompleteItem: StateUpdater {\n  var index: Int\n\n  func updateState(_ state: inout AppState) {\n    state.items[index].completed = true\n  }\n}\n```\n\nThe part of the state needed to render the UI of a screen is selected by a [ViewModelWithState](https://bendingspoons.github.io/tempura-swift/latest/Protocols/ViewModelWithState.html).\n\n```swift\nstruct ListViewModel: ViewModelWithState {\n  var todos: [Todo]\n\n  init(state: AppState) {\n    self.todos = state.todos\n  }\n}\n```\n\nThe UI of each screen of your app is composed in a [ViewControllerModellableView](https://bendingspoons.github.io/tempura-swift/latest/Protocols/ViewControllerModellableView.html). It exposes callbacks (we call them interactions) to signal that a user action occurred. It renders itself based on the ViewModelWithState.\n\n```swift\nclass ListView: UIView, ViewControllerModellableView {\n  // subviews\n  var todoButton: UIButton = UIButton(type: .custom)\n  var list: CollectionView\u003cTodoCell, SimpleSource\u003cTodoCellViewModel\u003e\u003e\n\n  // interactions\n  var didTapAddItem: ((String) -\u003e ())?\n  var didCompleteItem: ((String) -\u003e ())?\n\n  // update based on ViewModel\n  func update(oldModel: ListViewModel?) {\n    guard let model = self.model else { return }\n    let todos = model.todos\n    self.list.source = SimpleSource\u003cTodoCellViewModel\u003e(todos)\n  }\n}\n```\n\nEach screen of your app is managed by a [ViewController](https://bendingspoons.github.io/tempura-swift/latest/Classes/ViewController.html). Out of the box it will automatically listen for state updates and keep the UI in sync. The only other responsibility of a ViewController is to listen for interactions from the UI and dispatch actions to change the state.\n\n```swift\nclass ListViewController: ViewController\u003cListView\u003e {\n  // listen for interactions from the view\n  override func setupInteraction() {\n    self.rootView.didCompleteItem = { [unowned self] index in\n      self.dispatch(CompleteItem(index: index))\n    }\n  }\n}\n```\n\nNote that the `dispatch` method of view controllers is a bit different than the one exposed by the Katana store: it accepts a simple `Dispatchable` and does not return anything. This is done to avoid implementing logic inside the view controller.\n\nIf your interaction handler needs to do more than one single thing, you should pack all that logic in a side effect and dispatch that.\n\nFor the rare cases when it's needed to have a bit of logic in a view controller (for example when updating an old app without wanting to completely refactor all the logic) you can use the following methods:\n\n- `open func __unsafeDispatch\u003cT: StateUpdater\u003e(_ dispatchable: T) -\u003e Promise\u003cVoid\u003e`\n- `open func __unsafeDispatch\u003cT: ReturningSideEffect\u003e(_ dispatchable: T) -\u003e Promise\u003cT.ReturningValue\u003e`\n\n**Note however that usage of this methods is HIGHLY discouraged, and they will be removed in a future version.**\n\n### Navigation\n\nReal apps are made by more than one screen. If a screen needs to present another screen, its ViewController must conform to the [RoutableWithConfiguration](https://bendingspoons.github.io/tempura-swift/latest/Protocols/RoutableWithConfiguration.html) protocol.\n\n```swift\nextension ListViewController: RoutableWithConfiguration {\n  var routeIdentifier: RouteElementIdentifier { return \"list screen\"}\n\n  var navigationConfiguration: [NavigationRequest: NavigationInstruction] {\n    return [\n      .show(\"add item screen\"): .presentModally({ [unowned self] _ in\n        let aivc = AddItemViewController(store: self.store)\n        return aivc\n      })\n    ]\n  }\n}\n```\n\nYou can then trigger the presentation using one of the navigation actions from the ViewController.\n\n```swift\nself.dispatch(Show(\"add item screen\"))\n```\n\nLearn more about the navigation [here](https://bendingspoons.github.io/tempura-swift/latest/Classes/Navigator.html)\n\n### ViewController containment\n\nYou can have ViewControllers inside other ViewControllers, this is useful if you want to reuse portions of UI including the logic. To do that, in the parent ViewController you need to provide a `ContainerView` that will receive the view of the child ViewController as subview.\n\n```swift\nclass ParentView: UIView, ViewControllerModellableView {\n    var titleView = UILabel()\n    var childView = ContainerView()\n    \n    func update(oldModel: ParentViewModel?) {\n      // update only the titleView, the childView is managed by another VC\n    }\n}\n```\n\nThen, in the parent ViewController you just need to add the child ViewController:\n\n```swift\nclass ParentViewController: ViewController\u003cParentView\u003e {\n  let childVC: ChildViewController\u003cChildView\u003e!\n    \n  override func setup() {\n    self.childVC = ChildViewController(store: self.store)\n    self.add(childVC, in: self.rootView.childView)  \n  }\n}\n```\n\nAll the automation will work out of the box.\nYou will now have a `ChildViewController` inside the `ParentViewController`, the ChildViewController's view will be hosted inside the `childView`.\n\n## 📸 UI Snapshot Testing\n\nTempura has a Snapshot Testing system that can be used to take screenshots of your views in all possible states, with all devices and all supported languages.\n\n### Usage\n\nYou need to include the `TempuraTesting` pod in the test target of your app:\n\n```ruby\ntarget 'MyAppTests' do\n  pod 'TempuraTesting'\nend\n```\n\nSpecify where the screenshots will be placed inside your `plist` :\n\n```plist\nUI_TEST_DIR: $(SOURCE_ROOT)/Demo/UITests\n```\n\nIn Xcode, create a new UI test case class:\n\n`File -\u003e New -\u003e File... -\u003e UI Test Case Class`\n\nHere you can use the `test` function to take a snapshot of a `ViewControllerModellableView`  with a specific `ViewModel`.\n\n```swift\nimport TempuraTesting\n\nclass UITests: XCTestCase, ViewTestCase {\n  \n  func testAddItemScreen() {\n    self.uiTest(testCases: [\n      \"addItem01\": AddItemViewModel(editingText: \"this is a test\")\n    ])\n  }\n}\n```\n\nThe identifier will define the name of the snapshot image in the file system.\n\nYou can also personalize how the view is rendered (for instance you can embed the view in an instance of UITabBar) using the context parameter. Here is an example that\nembeds the view into a tabbar:\n\n```swift\nimport TempuraTesting\n\nclass UITests: XCTestCase, ViewTestCase {\n  \n  func testAddItemScreen() {\n    var context = UITests.Context\u003cAddItemView\u003e()\n    context.container = .tabBarController\n\n\n    self.uiTest(testCases: [\n      \"addItem01\": AddItemViewModel(editingText: \"this is a test\")\n    ], context: context)\n  }\n}\n```\n\nIf some important content inside a UIScrollView is not fully visible, you can leverage the `scrollViewsToTest(in view: V, identifier: String)` method.\nThis will produce an additional snapshot rendering the full content of each returned UIScrollView instance.\n\nIn this example we use `scrollViewsToTest(in view: V, identifier: String)`  to take an extended snapshot of the *mood picker* at the bottom of the screen.\n\n```swift\nfunc scrollViewsToTest(in view: V, identifier: String) -\u003e [String: UIScrollView] {\n  return [\"mood_collection_view\": view.moodCollectionView]\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/screen1.png\" height=\"400\" /\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/BendingSpoons/tempura-swift/master/.github/Assets/screen2.png\" /\u003e\n\nIn case you have to wait for asynchronous operations before rendering the UI and take the screenshot, you can leverage the `isViewReady(view:identifier:)` method.\nFor instance, here we wait until an hypothetical view that shows an image from a remote URL is ready. When the image is shown (that is, the state is `loaded`, then the snapshot is taken)\n\n```swift\nimport TempuraTesting\n\nclass UITests: XCTestCase, ViewTestCase {\n  \n  func testAddItemScreen() {\n    self.uiTest(testCases: [\n      \"addItem01\": AddItemViewModel(editingText: \"this is a test\")\n    ])\n  }\n\n  func isViewReady(_ view: AddItemView, identifier: String) -\u003e Bool {\n    return view.remoteImage.state == .loaded\n  }\n}\n```\n\nThe test will pass as soon as the snapshot is taken.\n\n### Context\n\nYou can enable a number of advanced features through the `context` object that you can pass to the `uiTest` method:\n\n- the `container` allows you to define a VC as a container of the view during the UITests. Basic `navigationController` and `tabBarController` are already provided, or you can define your own using the `custom` one \n- the `hooks` allows you to perform actions when some lifecycle events happen. Available hooks are `viewDidLoad`, `viewWillAppear`, `viewDidAppear`, `viewDidLayoutSubviews`, and `navigationControllerHasBeenCreated`\n- the `screenSize` and `orientation` properties allows you to define a custom screen size and orientation to be used during the test\n- the `renderSafeArea` allows you to define whether the safe area should be rendered as semitransparent gray overlay during the test\n- the `keyboardVisibility` allows you to define whether a gray overlay should be rendered as a placeholder for the keyboard\n\n#### Multiple devices\n\nBy default, tests are run only in the device you have choose from xcode (or your device, or CI system). We can run the snapshotting in all the devices by using a script like the following one:\n\n```bash\nxcodebuild \\\n  -workspace \u003cproject\u003e.xcworkspace \\\n  -scheme \"\u003ctarget name\u003e\" \\\n  -destination name=\"iPhone 5s\" \\\n  -destination name=\"iPhone 6 Plus\" \\\n  -destination name=\"iPhone 6\" \\\n  -destination name=\"iPhone X\" \\\n  -destination name=\"iPad Pro (12.9 inch)\" \\\n  test\n```\n\nTests will run in parallel on all the devices. If you want to change the behaviour, refer to the `xcodebuild` documentation\n\nIf you want to test a specific language in the ui test, you can replace the `test` command with the `-testLanguage \u003ciso code639-1\u003e`.\nThe app will be launched in that language and the UITests will be executed with that locale. An example:\n\n```bash\nxcodebuild \\\n  -workspace \u003cproject\u003e.xcworkspace \\\n  -scheme \"\u003ctarget name\u003e\" \\\n  -destination name=\"iPhone 5s\" \\\n  -destination name=\"iPhone 6 Plus\" \\\n  -destination name=\"iPhone 6\" \\\n  -destination name=\"iPhone X\" \\\n  -destination name=\"iPad Pro (12.9 inch)\" \\\n  -testLanguage it\n```\n\n### Remote Resources\n\nIt happens often that the UI needs to show remote content (that is, remote images, remote videos, ...). While executing UITests this could be a problem as:\n\n- tests may fail due to network or server issues\n- system should take care of tracking when remote resources are loaded, put them in the UI and only then take the screenshots\n\nTo fix this issue, Tempura offers a [URLProtocol](https://developer.apple.com/documentation/foundation/urlprotocol) subclass named `LocalFileURLProtocol` that tries to load remote files from your local bundle.\n\nThe idea is to put in your (test) bundle all the resources that are needed to render the UI and `LocalFileURLProtocol` will try to load them instead of making the network request.\n\nGiven an url, `LocalFileURLProtocol` matches the file name using the following rules:\n\n- search a file that has the url as a name (e.g., \u003chttp://example.com/image.png\u003e)\n- search a file that has the last path component as file name (e.g., image.png)\n- search a file that has the last path component without extension as file name (e.g., image)\n\nif a matching file cannot be retrieved, then the network call is performed.\n\nIn order to register `LocalFileURLProtocol` in your application, you have to invoke the following API as soon as possible in your tests lifecycle:\n\n```swift\nURLProtocol.registerClass(LocalFileURLProtocol.self)\n```\n\nNote that if you are using [Alamofire](https://github.com/Alamofire/Alamofire/) this won't work. [Here](https://github.com/Alamofire/Alamofire/issues/1247) you can find a related issue and a link on how to configure Alamofire to deal with `URLProtocol` classes.\n\n### UI Testing with ViewController containment\n\n`ViewTestCase` is centred about the use case of testing `ViewControllerModellableView`s with the automatic injection of `ViewModel`s representing testing conditions for that View.\n\nIn case you are using ViewController containment (like in our `ParentView` example above) there is part of the View that will not be updated when injecting the ViewModel, as there is another ViewController responsible for that.\n\nIn that case you need to scale up and test at the ViewController's level using the `ViewControllerTestCase` protocol:\n\n```swift\nclass ParentViewControllerUITest: XCTestCase, ViewControllerTestCase {\n  /// provide the instance of the ViewController to test\n  var viewController: ParentViewController {\n    let fakeStore = Store\u003cAppState, EmptySideEffectDependencyContainer\u003e()\n    let vc = ParentViewController(store: testStore)\n    return vc\n  }\n  \n  /// define the ViewModels\n  let viewModel = ParentViewModel(title: \"A test title\")\n  let childVM = ChildViewModel(value: 12)\n  \n  /// define the tests we want to perform\n  let tests: [String: ParentViewModel] = [\n    \"first_test_vc\": viewModel\n  ]\n    \n  /// configure the ViewController with ViewModels, also for the children VCs\n  func configure(vc: ParentViewController, for testCase: String, model: ParentViewModel) {\n    vc.viewModel = model\n    vc.childVC.viewModel = childVM\n  }\n    \n  /// execute the UI tests\n  func test() {\n    let context = UITests.VCContext\u003cParentViewController\u003e(container: .none)\n    self.uiTest(testCases: self.tests, context: context)  \n  }\n}\n```\n\nIn case you don't have child ViewControllers to configure, it's even easier as you don't need to supply a `configure(:::)` method:\n\n```swift\nclass ParentViewControllerUITest: XCTestCase, ViewControllerTestCase {\n  /// provide the instance of the ViewController to test\n  var viewController: ParentViewController {\n    let fakeStore = Store\u003cAppState, EmptySideEffectDependencyContainer\u003e()\n    let vc = ParentViewController(store: testStore)\n    return vc\n  }\n  \n  /// define the ViewModel\n  let viewModel = ParentViewModel(title: \"A test title\")\n  \n  /// define the tests we want to perform\n  let tests: [String: ParentViewModel] = [\n    \"first_test_vc\": viewModel\n  ]\n    \n  /// execute the UI tests\n  func test() {\n    let context = UITests.VCContext\u003cParentViewController\u003e(container: .tabbarController)\n    self.uiTest(testCases: self.tests, context: context)  \n  }\n}\n```\n\n## 🧭 Where to go from here\n\n### Example application\n\nThis repository contains a demo of a todo list application done with Tempura. To generate an Xcode project file you can use [Tuist](https://tuist.io/). Run `tuist generate`, open the project and run the `Demo` target.\n\n### Check out the documentation\n\n[Documentation](https://bendingspoons.github.io/tempura-swift)\n\n## 📄 Swift Version\n\nCertain versions of Tempura only support certain versions of Swift. Depending on which version of Swift your project is using, you should use specific versions of Tempura.\nUse this table in order to check which version of Tempura you need.\n\n| Swift Version  | Tempura Version |\n| ------------- | ------------- |\n| Swift 5.0 | Tempura 4.0+ |\n| Swift 4.2 | Tempura 3.0 |\n| Swift 4 | Tempura 1.12 |\n\n## 📬 Get in touch\n\nIf you have any **questions** or **feedback** we'd love to hear from you at [opensource@bendingspoons.com](mailto:opensource@bendingspoons.com)\n\n## 🙋‍♀️ Contribute\n\n- If you've **found a bug**, open an issue;\n- If you have a **feature request**, open an issue;\n- If you **want to contribute**, submit a pull request;\n- If you **have an idea** on how to improve the framework or how to spread the word, please [get in touch](https://github.com/BendingSpoons/tempura-swift#get-in-touch);\n- If you want to **try the framework** for your project or to write a demo, please send us the link of the repo.\n\n## 👩‍⚖️ License\n\nTempura is available under the [MIT license](https://github.com/BendingSpoons/tempura-swift/blob/master/LICENSE).\n\n## ❓ About\n\nTempura is maintained by [Bending Spoons](http://bndspn.com/2HOnxis).\nWe create our own tech products, used and loved by millions all around the world.\nSounds cool? [Check us out](http://bndspn.com/2ELtTxf)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBendingSpoons%2Ftempura-swift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FBendingSpoons%2Ftempura-swift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBendingSpoons%2Ftempura-swift/lists"}