![ws](https://raw.githubusercontent.com/freshOS/ws/master/banner.png) # โš  Important Notice: Farewell ws... hello Networking ! [Networking](https://github.com/freshOS/Networking) is the next generation of the [ws](https://github.com/freshOS/ws) project. Think of it as ws 2.0 built for iOS13. It uses Combine native Apple's framework over [Then](https://github.com/freshOS/Then) Promise Library, removes [Arrow](https://github.com/freshOS/Arrow) dependency to favour Codable (Arrow can still be adapted easily though) and removes the [Alamofire](https://github.com/Alamofire/Alamofire) dependency in favour of a simpler purely native [URLSession](https://developer.apple.com/documentation/foundation/urlsession) implementation. In essence, less dependencies and more native stuff with an almost identical api. If your app supports iOS13 and up, it is strongly advised to migrate to Networking. WS will be "maintained" for backwards compatibility reasons but consider it deprected starting iOS13. # ws [![Language: Swift 5](https://img.shields.io/badge/language-swift5-f48041.svg?style=flat)](https://developer.apple.com/swift) ![Platform: iOS 8+](https://img.shields.io/badge/platform-iOS%208%2B-blue.svg?style=flat) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Cocoapods compatible](https://img.shields.io/badge/Cocoapods-compatible-4BC51D.svg?style=flat)](https://cocoapods.org) [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/freshOS/ws/blob/master/LICENSE) [![Build Status](https://app.bitrise.io/app/a6d157138f9ee86d/status.svg?token=W7-x9K5U976xiFrI8XqcJw&branch=master)](https://app.bitrise.io/app/a6d157138f9ee86d) [![codebeat badge](https://codebeat.co/badges/78d86c16-aa61-4a5e-8342-1aea8d437453)](https://codebeat.co/projects/github-com-freshos-ws) ![Release version](https://img.shields.io/github/release/freshos/ws.svg) [Reason](#why) - [Example](#usage) - [Installation](#installation) ```swift let ws = WS("http://jsonplaceholder.typicode.com") ws.get("/users").then { json in // Get back some json \o/ } ``` Because JSON apis are used in **99% of iOS Apps**, this should be **simple**. We developers should **focus on our app logic** rather than *boilerplate code* . *Less* code is *better* code ## Try it! ws is part of [freshOS](http://freshos.org) iOS toolset. Try it in an example App ! Download Starter Project ## How By providing a lightweight client that **automates boilerplate code everyone has to write**. By exposing a **delightfully simple** api to get the job done simply, clearly, quickly. Getting swift models from a JSON api is now *a problem of the past* ## What - [x] Build concise Apis - [x] Automatically maps your models - [x] Built-in network logger - [x] Stands on the shoulder of giants (Alamofire & Promises) - [x] Pure Swift, Simple & Lightweight ## Usage ### Bare JSON ```swift import ws // Import ws at the top of your file import Arrow // Import Arrow to get access to the JSON type class ViewController: UIViewController { // Set webservice base URL let ws = WS("http://jsonplaceholder.typicode.com") override func viewDidLoad() { super.viewDidLoad() // Get back some json instantly \o/ ws.get("/users").then { (json:JSON) in print(json) } } } ``` ### Set up Model parsing Create a `User+JSON.swift` file and map the JSON keys to your model properties ```swift import Arrow extension User: ArrowParsable { mutating func deserialize(_ json: JSON) { identifier <-- json["id"] username <-- json["username"] email <-- json["email"] } } ``` *Note: `ws` uses `Arrow` for JSON Parsing https://github.com/freshOS/Arrow* ### Choose what you want back Here you are going to create a function that wraps your request. There are different ways of writing that function depending on what you want back. An empty block, the JSON, the model or the array of models. ```swift func voidCall() -> Promise { return ws.get("/users") } func jsonCall() -> Promise { return ws.get("/users") } func singleModelCall() -> Promise { return ws.get("/users/3") } func modelArrayCall() -> Promise<[User]> { return ws.get("/users") } ``` As you can notice, only by changing the return type, ws *automatically* knows what to do, for instance, try to parse the response into `User` models. This enables us to stay concise without having to write extra code. \o/ *Note: `ws` uses `then` for Promises https://github.com/freshOS/then* ### Get it! ```swift voidCall().then { print("done") } jsonCall().then { json in print(json) } singleModelCall().then { user in print(user) // Strongly typed User \o/ } modelArrayCall().then { users in print(users) // Strongly typed [User] \o/ } ``` ## Settings Want to log all network calls and responses? ```swift ws.logLevels = .debug ``` Want to hide network activity indicator? ```swift ws.showsNetworkActivityIndicator = false ``` Want to override the default session manager to customize trust policies? ```swift import Alamofire ws.sessionManager = SessionManager(serverTrustPolicyManager: ServerTrustPolicyManager( policies: ["myspecialhostname.com" : .disableEvaluation] )) ``` ## Api Example Here is a Typical CRUD example for Articles : ```swift extension Article { static func list() -> Promise<[Article]> { return ws.get("/articles") } func save() -> Promise
{ return ws.post("/articles", params: ["name":name]) } func fetch() -> Promise
{ return ws.get("/articles/\(id)") } func update() -> Promise { return ws.put("/articles/\(id)", params: ["name":name]) } func delete() -> Promise { return ws.delete("/articles/\(id)") } } ``` Here is how we use it in code : ```swift // List Articles Article.list().then { articles in } // Create Article var newArticle = Article(name:"Cool story") newArticle.save().then { createdArticle in } // Fetch Article var existingArticle = Article(id:42) existingArticle.fetch().then { fetchedArticle in } // Edit Article existingArticle.name = "My new name" existingArticle.update().then { } // Delete Article existingArticle.delete().then { } ``` ### HTTP Status code When a request fails, we often want to know the reason thanks to the HTTP status code. Here is how to get it : ```swift ws.get("/users").then { // Do something }.onError { e in if let wsError = e as? WSError { print(wsError.status) print(wsError.status.rawValue) // RawValue for Int status } } ``` You can find the full `WSError` enum here -> https://github.com/freshOS/ws/blob/master/ws/WSError.swift ## Bonus - Load More pattern Very often we deal we lists and the ability to `load more` items. Here we are going to see an example implementation of this pattern using `ws`. This is not included because the logic itself depends on your backend implementation. This will give you an example for you to roll out your own version. ### Implementation ```swift import ws import then import Arrow class LoadMoreRequest { var limit = 12 private var params = [String:Any]() private var offset = 0 private var call: WSRequest! private var canLoadMore = true private var aCallback:((_ ts: [T]) -> [T])? = nil init(_ aCall: WSRequest) { call = aCall } func resetOffset() { offset = 0 canLoadMore = true } func hasMoreItemsToload() -> Bool { return canLoadMore } func fetchNext() -> Promise<[T]> { params = call.params params["limit"] = limit params["offset"] = offset call.params = params offset += limit return call.fetch() .registerThen(parseModels) .resolveOnMainThread() } private func parseModels(_ json: JSON) -> [T] { let mapper = WSModelJSONParser() let models = mapper.toModels(json) if models.count < limit { canLoadMore = false } return models } } ``` As you can see, we have a strongly typed request. The limit is adjustable. It encapsulates a WSRequest. It handles the offset logic and also wether or not there are more items to load. And that's all we need! Now, this is how we build a `LoadMoreRequest` ```swift func loadMoreUsersRequest() -> LoadMoreRequest { return LoadMoreRequest(ws.getRequest("/users")) } ``` ### Usage And here is how we use it in our controllers : ```swift class ViewController: UIViewController { // Get a request let request = api.loadMoreUsersRequest() override func viewDidLoad() { super.viewDidLoad() request.limit = 5 // Set a limit if needed } func refresh() { // Resets the request, usually plugged with // the pull to refresh feature of a tableview request.resetOffset() } func loadMore() { // Get the next round of users request.fetchNext().then { users in print(users) } } func shouldDisplayLoadMoreSpinner() -> Bool { // This asks the requests if there are more items to come // This is useful to know if we show the "load more" spinner return request.hasMoreItemsToload() } } ``` Here you go you now have a simple way to deal with load more requests in your App ๐ŸŽ‰ ## Bonus - Simplifying restful routes usage When working with a `RESTFUL` api, we can have fun and go a little further. By introducing a `RestResource` protocol ```swift public protocol RestResource { static func restName() -> String func restId() -> String } ``` We can have a function that builds our `REST` URL ```swift public func restURL(_ r:T) -> String { return "/\(T.restName())/\(r.restId())" } ``` We conform our `User` Model to the protocol ```swift extension User:RestResource { static func restName() -> String { return "users" } func restId() -> String { return "\(identifier)" } } ``` And we can implement a version of `get` that takes our a `RestResource` ```swift public func get(_ restResource:T, params:[String:Any] = [String:Any]()) -> Promise { return get(restURL(restResource), params: params) } ``` then ```swift ws.get("/users/\(user.identifier)") ``` Can be written like : ```swift ws.get(user) ``` Of course, the same logic can be applied to the all the other ws functions (`post`, `put` `delete` etc) ! ๐ŸŽ‰ ## Installation ### Swift Package Manager (SPM) Due to the challenge of supporting all package manager at once, SPM support is availlable on a separate branch `spm-only`. ### Carthage In your Cartfile ``` github "freshOS/ws" ``` - Run `carthage update` - Drag and drop `ws.framework` from `Carthage/Build/iOS` to `Linked Frameworks and Libraries` (โ€œGeneralโ€ settings tab) - Go to `Project` > `Target` > `Build Phases` + `New run Script Phase` `/usr/local/bin/carthage copy-frameworks` Add input files ``` $(SRCROOT)/Carthage/Build/iOS/ws.framework $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework $(SRCROOT)/Carthage/Build/iOS/Arrow.framework $(SRCROOT)/Carthage/Build/iOS/then.framework ``` This links ws and its dependencies. ### Manually Carthage is pretty useful since it takes care of pulling dependencies such as Arrow, then and Alamofire. What's cool is that it really is transparent. What I mean is that you could just use carthage on the side to pull and build dependencies and manually link frameworks to your Xcode project. Without Carthage, I'd see 2 solutions : 1 - Copy paste all the source code : ws / then / Arrow / Alamofire which doesn't sound like a lot of fun ;) 2 - Manually link the frameworks (ws + dependencies) by A grabbing .frameworks them on each repo, or B use Carthage to build them ### Cocoapods ``` target 'MyApp' pod 'ws' use_frameworks! ``` ## Swift Version Swift 2 -> version [**1.3.0**](https://github.com/freshOS/ws/releases/tag/1.3.0) Swift 3 -> version [**2.0.4**](https://github.com/freshOS/ws/releases/tag/2.0.4) Swift 4 -> version [**3.0.0**](https://github.com/freshOS/ws/releases/tag/3.0.0) Swift 4.1 -> version [**3.1.0**](https://github.com/freshOS/ws/releases/tag/3.1.0) Swift 4.2 -> version [**3.2.0**](https://github.com/freshOS/ws/releases/tag/3.2.0) Swift 5.0 -> version [**5.0.0**](https://github.com/freshOS/ws/releases/tag/5.0.0) Swift 5.1 -> version [**5.1.0**](https://github.com/freshOS/ws/releases/tag/5.1.0) Swift 5.1.3 -> version [**5.1.1**](https://github.com/freshOS/ws/releases/tag/5.1.1) ### Backers Like the project? Offer coffee or support us with a monthly donation and help us continue our activities :) ### Sponsors Become a sponsor and get your logo on our README on Github with a link to your site :)