You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
21 KiB

  1. ![Then](https://raw.githubusercontent.com/freshOS/then/master/banner.png)
  2. # then
  3. [![Language: Swift 2 and 3|4](https://img.shields.io/badge/language-swift2|3|4-f48041.svg?style=flat)](https://developer.apple.com/swift)
  4. ![Platform: iOS 8+/macOS10.11](https://img.shields.io/badge/platform-iOS%208%2B|OSX|tvOS-blue.svg?style=flat) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
  5. [![Cocoapods compatible](https://img.shields.io/badge/Cocoapods-compatible-4BC51D.svg?style=flat)](https://cocoapods.org)
  6. [![Build Status](https://www.bitrise.io/app/2c01c6f861c526d9.svg?token=H26l2Nish0WWF6yfDTL1kA&branch=master)](https://www.bitrise.io/app/2c01c6f861c526d9)
  7. [![codebeat badge](https://codebeat.co/badges/768d3017-1e65-47e0-b287-afcb8954a1da)](https://codebeat.co/projects/github-com-s4cha-then)
  8. [![Join the chat at https://gitter.im/s4cha/then](https://badges.gitter.im/s4cha/then.svg)](https://gitter.im/s4cha/then?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
  9. [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/freshOS/then/blob/master/LICENSE)
  10. ![Release version](https://img.shields.io/github/release/freshos/then.svg)
  11. [Reason](#why) - [Example](#example) - [Documentation](#documentation) - [Installation](#installation)
  12. ```swift
  13. fetchUserId().then { id in
  14. print("UserID : \(id)")
  15. }.onError { e in
  16. print("An error occured : \(e)")
  17. }.finally {
  18. print("Everything is Done :)")
  19. }
  20. ```
  21. ```swift
  22. let userId = try! await(fetchUserId())
  23. ```
  24. Because async code is hard to write, hard to read, hard to reason about. **A pain to maintain**
  25. ## Try it
  26. then is part of [freshOS](http://freshos.org) iOS toolset. Try it in an example App ! <a class="github-button" href="https://github.com/freshOS/StarterProject/archive/master.zip" data-icon="octicon-cloud-download" data-style="mega" aria-label="Download freshOS/StarterProject on GitHub">Download Starter Project</a>
  27. ## How
  28. By using a **then** keyword that enables you to write aSync code that *reads like an English sentence*
  29. Async code is now **concise**, **flexible** and **maintainable** ❤️
  30. ## What
  31. - [x] Based on the popular `Promise` / `Future` concept
  32. - [x] `Async` / `Await`
  33. - [x] `progress` `race` `recover` `validate` `retry` `bridgeError` `chain` `noMatterWhat` ...
  34. - [x] Strongly Typed
  35. - [x] Pure Swift & Lightweight
  36. ## Example
  37. ### Before
  38. ```swift
  39. fetchUserId({ id in
  40. fetchUserNameFromId(id, success: { name in
  41. fetchUserFollowStatusFromName(name, success: { isFollowed in
  42. // The three calls in a row succeeded YAY!
  43. reloadList()
  44. }, failure: { error in
  45. // Fetching user ID failed
  46. reloadList()
  47. })
  48. }, failure: { error in
  49. // Fetching user name failed
  50. reloadList()
  51. })
  52. }) { error in
  53. // Fetching user follow status failed
  54. reloadList()
  55. }
  56. 🙉🙈🙊#callbackHell
  57. ```
  58. ----
  59. ### After
  60. ```swift
  61. fetchUserId()
  62. .then(fetchUserNameFromId)
  63. .then(fetchUserFollowStatusFromName)
  64. .then(updateFollowStatus)
  65. .onError(showErrorPopup)
  66. .finally(reloadList)
  67. ```
  68. ## 🎉🎉🎉
  69. ## Going further 🤓
  70. ```swift
  71. fetchUserId().then { id in
  72. print("UserID : \(id)")
  73. }.onError { e in
  74. print("An error occured : \(e)")
  75. }.finally {
  76. print("Everything is Done :)")
  77. }
  78. ```
  79. If we want this to be **maintainable**, it should read *like an English sentence*
  80. We can do this by extracting our blocks into separate functions:
  81. ```swift
  82. fetchUserId()
  83. .then(printUserID)
  84. .onError(showErrorPopup)
  85. .finally(reloadList)
  86. ```
  87. This is now **concise**, **flexible**, **maintainable**, and it reads like an English sentence <3
  88. Mental sanity saved
  89. // #goodbyeCallbackHell
  90. ## Documentation
  91. 1. [Writing your own Promise](#writing-your-own-promise-💪)
  92. 2. [Progress](#progress)
  93. 3. [Registering a block for later](#registering-a-block-for-later)
  94. 4. [Returning a rejecting promise](#returning-a-rejecting-promise)
  95. 5. [Common Helpers](#common-helpers)
  96. 1. [race](#race)
  97. 2. [recover](#recover)
  98. 3. [validate](#validate)
  99. 4. [retry](#retry)
  100. 5. [bridgeError](#bridgeError)
  101. 6. [whenAll](#whenAll)
  102. 7. [chain](#chain)
  103. 8. [noMatterWhat](#nomatterwhat)
  104. 9. [unwrap](#unwrap)
  105. 6. [AsyncTask](#asynctask)
  106. 7. [Async/Await](#async/await)
  107. ### Writing your own Promise 💪
  108. Wondering what fetchUserId() is?
  109. It is a simple function that returns a strongly typed promise :
  110. ```swift
  111. func fetchUserId() -> Promise<Int> {
  112. return Promise { resolve, reject in
  113. print("fetching user Id ...")
  114. wait { resolve(1234) }
  115. }
  116. }
  117. ```
  118. Here you would typically replace the dummy wait function by your network request <3
  119. ### Progress
  120. As for `then` and `onError`, you can also call a `progress` block for things like uploading an avatar for example.
  121. ```swift
  122. uploadAvatar().progress { p in
  123. // Here update progressView for example
  124. }
  125. .then(doSomething)
  126. .onError(showErrorPopup)
  127. .finally(doSomething)
  128. ```
  129. ### Registering a block for later
  130. Our implementation slightly differs from the original javascript Promises. Indeed, they do not start right away, on purpose. Calling `then`, `onError`, or `finally` will start them automatically.
  131. Calling `then` starts a promise if it is not already started.
  132. In some cases, we only want to register some code for later.
  133. For instance, in the case of JSON to Swift model parsing, we often want to attach parsing blocks to JSON promises, but without starting them.
  134. In order to do that we need to use `registerThen` instead. It's the exact same thing as `then` without starting the promise right away.
  135. ```swift
  136. let fetchUsers:Promise<[User]> = fetchUsersJSON().registerThen(parseUsersJSON)
  137. // Here promise is not launched yet \o/
  138. // later...
  139. fetchUsers.then { users in
  140. // YAY
  141. }
  142. ```
  143. Note that `onError` and `finally` also have their non-starting counterparts : `registerOnError` and `registerFinally`.
  144. ### Returning a rejecting promise
  145. Oftetimes we need to return a rejecting promise as such :
  146. ```swift
  147. return Promise { _, reject in
  148. reject(anError)
  149. }
  150. ```
  151. This can be written with the following shortcut :
  152. ```swift
  153. return Promise.reject(error:anError)
  154. ```
  155. ### Common Helpers
  156. #### Race
  157. With `race`, you can send multiple tasks and get the result of the first one coming back :
  158. ```swift
  159. race(task1, task2, task3).then { work in
  160. // The first result !
  161. }
  162. ```
  163. #### Recover
  164. With `.recover`, you can provide a fallback value for a failed Promise.
  165. You can :
  166. - Recover with a value
  167. - Recover with a value for a specific Error type
  168. - Return a value from a block, enabling you to test the type of error and return distinct values.
  169. - Recover with another Promise with the same Type
  170. ```swift
  171. .recover(with: 12)
  172. .recover(MyError.defaultError, with: 12)
  173. .recover { e in
  174. if e == x { return 32 }
  175. if e == y { return 143 }
  176. throw MyError.defaultError
  177. }
  178. .recover { e -> Promise<Int> in
  179. // Deal with the error then
  180. return Promise<Int>.resolve(56)
  181. // Or
  182. return Promise<Int>.reject(e)
  183. }
  184. }
  185. .recover(with: Promise<Int>.resolve(56))
  186. ```
  187. Note that in the block version you can also throw your own error \o/
  188. #### Validate
  189. With `.validate`, you can break the promise chain with an assertion block.
  190. You can:
  191. - Insert assertion in Promise chain
  192. - Insert assertion and return you own Error
  193. For instance checking if a user is allowed to drink alcohol :
  194. ```swift
  195. fetchUserAge()
  196. .validate { $0 > 18 }
  197. .then { age in
  198. // Offer a drink
  199. }
  200. .validate(withError: MyError.defaultError, { $0 > 18 })`
  201. ```
  202. A failed validation will retrun a `PromiseError.validationFailed` by default.
  203. #### Retry
  204. With `retry`, you can restart a failed Promise X number of times.
  205. ```swift
  206. doSomething()
  207. .retry(10)
  208. .then { v in
  209. // YAY!
  210. }.onError { e in
  211. // Failed 10 times in a row
  212. }
  213. ```
  214. #### BridgeError
  215. With `.bridgeError`, you can intercept a low-level Error and return your own high level error.
  216. The classic use-case is when you receive an api error and you bridge it to your own domain error.
  217. You can:
  218. - Catch all errors and use your own Error type
  219. - Catch only a specific error
  220. ```swift
  221. .bridgeError(to: MyError.defaultError)
  222. .bridgeError(SomeError, to: MyError.defaultError)
  223. ```
  224. #### WhenAll
  225. With `.whenAll`, you can combine multiple calls and get all the results when all the promises are fulfilled :
  226. ```swift
  227. whenAll(fetchUsersA(),fetchUsersB(), fetchUsersC()).then { allUsers in
  228. // All the promises came back
  229. }
  230. ```
  231. #### Chain
  232. With `chain`, you can add behaviours without changing the chain of Promises.
  233. A common use-case is for adding Analytics tracking like so:
  234. ```swift
  235. extension Photo {
  236. public func post() -> Async<Photo> {
  237. return api.post(self).chain { _ in
  238. Tracker.trackEvent(.postPicture)
  239. }
  240. }
  241. }
  242. ```
  243. #### NoMatterWhat
  244. With `noMatterWhat` you can add code to be executed in the middle of a promise chain, no matter what happens.
  245. ```swift
  246. func fetchNext() -> Promise<[T]> {
  247. isLoading = true
  248. call.params["page"] = page + 1
  249. return call.fetch()
  250. .registerThen(parseResponse)
  251. .resolveOnMainThread()
  252. .noMatterWhat {
  253. self.isLoading = false
  254. }
  255. }
  256. ```
  257. #### Unwrap
  258. With `unwrap` you can transform an optional into a promise :
  259. ```swift
  260. func fetch(userId: String?) -> Promise<Void> {
  261. return unwrap(userId).then {
  262. network.get("/user/\($0)")
  263. }
  264. }
  265. ```
  266. Unwrap will fail the promise chain with `unwrappingFailed` error in case of a nil value :)
  267. ### AsyncTask
  268. `AsyncTask` and `Async<T>` typealisases are provided for those of us who think that Async can be clearer than `Promise`.
  269. Feel free to replace `Promise<Void>` by `AsyncTask` and `Promise<T>` by `Async<T>` wherever needed.
  270. This is purely for the eyes :)
  271. ### Async/Await
  272. `await` waits for a promise to complete synchronously and yields the result :
  273. ```swift
  274. let photos = try! await(getPhotos())
  275. ```
  276. `async` takes a block and wraps it in a background Promise.
  277. ```swift
  278. async {
  279. let photos = try await(getPhotos())
  280. }
  281. ```
  282. Notice how we don't need the `!` anymore because `async` will catch the errors.
  283. Together, `async`/`await` enable us to write asynchronous code in a synchronous manner :
  284. ```swift
  285. async {
  286. let userId = try await(fetchUserId())
  287. let userName = try await(fetchUserNameFromId(userId))
  288. let isFollowed = try await(fetchUserFollowStatusFromName(userName))
  289. return isFollowed
  290. }.then { isFollowed in
  291. print(isFollowed)
  292. }.onError { e in
  293. // handle errors
  294. }
  295. ```
  296. #### Await operators
  297. Await comes with `..` shorthand operator. The `..?` will fallback to a nil value instead of throwing.
  298. ```swift
  299. let userId = try await(fetchUserId())
  300. ```
  301. Can be written like this:
  302. ```swift
  303. let userId = try ..fetchUserId()
  304. ```
  305. ## Installation
  306. ### Cocoapods
  307. ```swift
  308. target 'MyApp'
  309. pod 'thenPromise'
  310. use_frameworks!
  311. ```
  312. #### Carthage
  313. ```
  314. github "freshOS/then"
  315. ```
  316. #### Manually
  317. Simply Copy and Paste `.swift` files in your Xcode Project :)
  318. #### As A Framework
  319. Grab this repository and build the Framework target on the example project. Then Link against this framework.
  320. ## Contributors
  321. [S4cha](https://github.com/S4cha), [Max Konovalov](https://github.com/maxkonovalov), [YannickDot](https://github.com/YannickDot), [Damien](https://github.com/damien-nd),
  322. [piterlouis](https://github.com/piterlouis)
  323. ## Swift Version
  324. - Swift 2 -> version [**1.4.2**](https://github.com/freshOS/then/releases/tag/1.4.2)
  325. - Swift 3 -> version [**2.2.5**](https://github.com/freshOS/then/releases/tag/2.2.5)
  326. - Swift 4 -> version [**3.1.0**](https://github.com/freshOS/then/releases/tag/3.1.0)
  327. - Swift 4.1 -> version [**4.0.0**](https://github.com/freshOS/then/releases/tag/4.0.0)
  328. ### Backers
  329. Like the project? Offer coffee or support us with a monthly donation and help us continue our activities :)
  330. <a href="https://opencollective.com/freshos/backer/0/website" target="_blank"><img src="https://opencollective.com/freshos/backer/0/avatar.svg"></a>
  331. <a href="https://opencollective.com/freshos/backer/1/website" target="_blank"><img src="https://opencollective.com/freshos/backer/1/avatar.svg"></a>
  332. <a href="https://opencollective.com/freshos/backer/2/website" target="_blank"><img src="https://opencollective.com/freshos/backer/2/avatar.svg"></a>
  333. <a href="https://opencollective.com/freshos/backer/3/website" target="_blank"><img src="https://opencollective.com/freshos/backer/3/avatar.svg"></a>
  334. <a href="https://opencollective.com/freshos/backer/4/website" target="_blank"><img src="https://opencollective.com/freshos/backer/4/avatar.svg"></a>
  335. <a href="https://opencollective.com/freshos/backer/5/website" target="_blank"><img src="https://opencollective.com/freshos/backer/5/avatar.svg"></a>
  336. <a href="https://opencollective.com/freshos/backer/6/website" target="_blank"><img src="https://opencollective.com/freshos/backer/6/avatar.svg"></a>
  337. <a href="https://opencollective.com/freshos/backer/7/website" target="_blank"><img src="https://opencollective.com/freshos/backer/7/avatar.svg"></a>
  338. <a href="https://opencollective.com/freshos/backer/8/website" target="_blank"><img src="https://opencollective.com/freshos/backer/8/avatar.svg"></a>
  339. <a href="https://opencollective.com/freshos/backer/9/website" target="_blank"><img src="https://opencollective.com/freshos/backer/9/avatar.svg"></a>
  340. <a href="https://opencollective.com/freshos/backer/10/website" target="_blank"><img src="https://opencollective.com/freshos/backer/10/avatar.svg"></a>
  341. <a href="https://opencollective.com/freshos/backer/11/website" target="_blank"><img src="https://opencollective.com/freshos/backer/11/avatar.svg"></a>
  342. <a href="https://opencollective.com/freshos/backer/12/website" target="_blank"><img src="https://opencollective.com/freshos/backer/12/avatar.svg"></a>
  343. <a href="https://opencollective.com/freshos/backer/13/website" target="_blank"><img src="https://opencollective.com/freshos/backer/13/avatar.svg"></a>
  344. <a href="https://opencollective.com/freshos/backer/14/website" target="_blank"><img src="https://opencollective.com/freshos/backer/14/avatar.svg"></a>
  345. <a href="https://opencollective.com/freshos/backer/15/website" target="_blank"><img src="https://opencollective.com/freshos/backer/15/avatar.svg"></a>
  346. <a href="https://opencollective.com/freshos/backer/16/website" target="_blank"><img src="https://opencollective.com/freshos/backer/16/avatar.svg"></a>
  347. <a href="https://opencollective.com/freshos/backer/17/website" target="_blank"><img src="https://opencollective.com/freshos/backer/17/avatar.svg"></a>
  348. <a href="https://opencollective.com/freshos/backer/18/website" target="_blank"><img src="https://opencollective.com/freshos/backer/18/avatar.svg"></a>
  349. <a href="https://opencollective.com/freshos/backer/19/website" target="_blank"><img src="https://opencollective.com/freshos/backer/19/avatar.svg"></a>
  350. <a href="https://opencollective.com/freshos/backer/20/website" target="_blank"><img src="https://opencollective.com/freshos/backer/20/avatar.svg"></a>
  351. <a href="https://opencollective.com/freshos/backer/21/website" target="_blank"><img src="https://opencollective.com/freshos/backer/21/avatar.svg"></a>
  352. <a href="https://opencollective.com/freshos/backer/22/website" target="_blank"><img src="https://opencollective.com/freshos/backer/22/avatar.svg"></a>
  353. <a href="https://opencollective.com/freshos/backer/23/website" target="_blank"><img src="https://opencollective.com/freshos/backer/23/avatar.svg"></a>
  354. <a href="https://opencollective.com/freshos/backer/24/website" target="_blank"><img src="https://opencollective.com/freshos/backer/24/avatar.svg"></a>
  355. <a href="https://opencollective.com/freshos/backer/25/website" target="_blank"><img src="https://opencollective.com/freshos/backer/25/avatar.svg"></a>
  356. <a href="https://opencollective.com/freshos/backer/26/website" target="_blank"><img src="https://opencollective.com/freshos/backer/26/avatar.svg"></a>
  357. <a href="https://opencollective.com/freshos/backer/27/website" target="_blank"><img src="https://opencollective.com/freshos/backer/27/avatar.svg"></a>
  358. <a href="https://opencollective.com/freshos/backer/28/website" target="_blank"><img src="https://opencollective.com/freshos/backer/28/avatar.svg"></a>
  359. <a href="https://opencollective.com/freshos/backer/29/website" target="_blank"><img src="https://opencollective.com/freshos/backer/29/avatar.svg"></a>
  360. ### Sponsors
  361. Become a sponsor and get your logo on our README on Github with a link to your site :)
  362. <a href="https://opencollective.com/freshos/sponsor/0/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/0/avatar.svg"></a>
  363. <a href="https://opencollective.com/freshos/sponsor/1/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/1/avatar.svg"></a>
  364. <a href="https://opencollective.com/freshos/sponsor/2/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/2/avatar.svg"></a>
  365. <a href="https://opencollective.com/freshos/sponsor/3/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/3/avatar.svg"></a>
  366. <a href="https://opencollective.com/freshos/sponsor/4/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/4/avatar.svg"></a>
  367. <a href="https://opencollective.com/freshos/sponsor/5/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/5/avatar.svg"></a>
  368. <a href="https://opencollective.com/freshos/sponsor/6/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/6/avatar.svg"></a>
  369. <a href="https://opencollective.com/freshos/sponsor/7/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/7/avatar.svg"></a>
  370. <a href="https://opencollective.com/freshos/sponsor/8/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/8/avatar.svg"></a>
  371. <a href="https://opencollective.com/freshos/sponsor/9/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/9/avatar.svg"></a>
  372. <a href="https://opencollective.com/freshos/sponsor/10/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/10/avatar.svg"></a>
  373. <a href="https://opencollective.com/freshos/sponsor/11/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/11/avatar.svg"></a>
  374. <a href="https://opencollective.com/freshos/sponsor/12/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/12/avatar.svg"></a>
  375. <a href="https://opencollective.com/freshos/sponsor/13/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/13/avatar.svg"></a>
  376. <a href="https://opencollective.com/freshos/sponsor/14/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/14/avatar.svg"></a>
  377. <a href="https://opencollective.com/freshos/sponsor/15/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/15/avatar.svg"></a>
  378. <a href="https://opencollective.com/freshos/sponsor/16/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/16/avatar.svg"></a>
  379. <a href="https://opencollective.com/freshos/sponsor/17/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/17/avatar.svg"></a>
  380. <a href="https://opencollective.com/freshos/sponsor/18/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/18/avatar.svg"></a>
  381. <a href="https://opencollective.com/freshos/sponsor/19/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/19/avatar.svg"></a>
  382. <a href="https://opencollective.com/freshos/sponsor/20/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/20/avatar.svg"></a>
  383. <a href="https://opencollective.com/freshos/sponsor/21/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/21/avatar.svg"></a>
  384. <a href="https://opencollective.com/freshos/sponsor/22/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/22/avatar.svg"></a>
  385. <a href="https://opencollective.com/freshos/sponsor/23/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/23/avatar.svg"></a>
  386. <a href="https://opencollective.com/freshos/sponsor/24/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/24/avatar.svg"></a>
  387. <a href="https://opencollective.com/freshos/sponsor/25/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/25/avatar.svg"></a>
  388. <a href="https://opencollective.com/freshos/sponsor/26/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/26/avatar.svg"></a>
  389. <a href="https://opencollective.com/freshos/sponsor/27/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/27/avatar.svg"></a>
  390. <a href="https://opencollective.com/freshos/sponsor/28/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/28/avatar.svg"></a>
  391. <a href="https://opencollective.com/freshos/sponsor/29/website" target="_blank"><img src="https://opencollective.com/freshos/sponsor/29/avatar.svg"></a>