Installation - Getting Started - Building a Request - Codable - Combine - How it Works - Request Groups - Request Chains - Json - Contributing - License
swift-request
can be installed via the Swift Package Manager
.
In Xcode 11, go to File > Swift Packages > Add Package Dependency...
, then paste in https://github.com/carson-katri/swift-request
Now just import Request
, and you're ready to Get Started
The old way:
var request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos")!)
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: url!) { (data, res, err) in
if let data = data {
...
} else if let error = error {
...
}
}
task.resume()
The declarative way:
Request {
Url("https://jsonplaceholder.typicode.com/todo")
Header.Accept(.json)
}
.onData { data in
...
}
.onError { error in
...
}
.call()
The benefit of declaring requests becomes abundantly clear when your data becomes more complex:
Request {
Url("https://jsonplaceholder.typicode.com/posts")
Method(.post)
Header.ContentType(.json)
Body(Json([
"title": "foo",
"body": "bar",
"usedId": 1
]).stringified)
}
Once you've built your Request
, you can specify the response handlers you want to use.
.onData
, .onString
, .onJson
, and .onError
are available.
You can chain them together to handle multiple response types, as they return a modified version of the Request
.
To perform the Request
, just use .call()
. This will run the Request
, and give you the response when complete.
Request
also conforms to Publisher
, so you can manipulate it like any other Combine publisher (read more):
let cancellable = Request {
Url("https://jsonplaceholder.typicode.com/todo")
Header.Accept(.json)
}
.sink(receiveCompletion: { ... }, receiveValue: { ... })
There are many different tools available to build a Request
:
Url
Exactly one must be present in each Request
Url("https://example.com")
Url(protocol: .secure, url: "example.com")
Method
Sets the MethodType
of the Request
(.get
by default)
Method(.get) // Available: .get, .head, .post, .put, .delete, .connect, .options, .trace, and .patch
Header
Sets an HTTP header field
Header.Any(key: "Custom-Header", value: "value123")
Header.Accept(.json)
Header.Authorization(.basic(username: "carsonkatri", password: "password123"))
Header.CacheControl(.noCache)
Header.ContentLength(16)
Header.ContentType(.xml)
Header.Host("en.example.com", port: "8000")
Header.Origin("www.example.com")
Header.Referer("redirectfrom.example.com")
Header.UserAgent(.firefoxMac)
Query
Creates the query string
Query(["key": "value"]) // ?key=value
Body
Sets the request body
Body(["key": "value"])
Body("myBodyContent")
Body(myJson)
Timeout
Sets the timeout for a request or resource:
Timeout(60)
Timeout(60, for: .request)
Timeout(30, for: .resource)
RequestParam
Add a param directly
Important: You must create the logic to handle a custom
RequestParam
. You may also consider adding a case toRequestParamType
. If you think your custom parameter may be useful for others, see Contributing
Let's look at an example. Here we define our data:
struct Todo: Codable {
let title: String
let completed: Bool
let id: Int
let userId: Int
}
Now we can use AnyRequest
to pull an array of Todo
s from the server:
AnyRequest<[Todo]> {
Url("https://jsonplaceholder.typicode.com/todos")
}
.onObject { todos in ... }
In this case, onObject
gives us [Todo]?
in response. It's that easy to get data and decode it.
Request
is built on AnyRequest
, so they support all of the same parameters.
If you use
onObject
on a standardRequest
, you will receiveData
in response.
Request
and RequestGroup
both conform to Publisher
:
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
.sink(receiveCompletion: { ... }, receiveValue: { ... })
RequestGroup {
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
Request {
Url("https://jsonplaceholder.typicode.com/posts")
}
Request {
Url("https://jsonplaceholder.typicode.com/todos/1")
}
}
.sink(receiveCompletion: { ... }, receiveValue: { ... })
Request
publishes the result using URLSession.DataTaskPublisher
. RequestGroup
collects the result of each Request
in its body, and publishes the array of results.
You can use all of the Combine operators you'd expect on Request
:
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
.map(\.data)
.decode([Todo].self, decoder: JSONDecoder())
.sink(receiveCompletion: { ... }, receiveValue: { ... })
However, Request
also comes with several convenience Publishers
to simplify the process of decoding:
objectPublisher
- Decodes the data of an AnyRequest
using JSONDecoder
stringPublisher
- Decodes the data to a String
jsonPublisher
- Converts the result to a Json
objectHere's an example of using objectPublisher
:
AnyRequest<[Todo]> {
Url("https://jsonplaceholder.typicode.com/todos")
}
.objectPublisher
.sink(receiveCompletion: { ... }, receiveValue: { ... })
This removes the need to constantly use .map.decode
to extract the desired Codable
result.
To handle errors, you can use the receiveCompletion
handler in sink
:
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
.sink(receiveCompletion: { res in
switch res {
case let .failure(err):
// Handle `err`
case .finished: break
}
}, receiveValue: { ... })
The body of the Request
is built using the RequestBuilder
@resultBuilder
.
It merges each RequestParam
in the body into one CombinedParam
object. This contains all the other params as children.
When you run .call()
, the children are filtered to find the Url
, and any other optional parameters that may have been included.
For more information, see RequestBuilder.swift and Request.swift
RequestGroup
can be used to run multiple Request
s simulataneously. You get a response when each Request
completes (or fails)
RequestGroup {
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
Request {
Url("https://jsonplaceholder.typicode.com/posts")
}
Request {
Url("https://jsonplaceholder.typicode.com/todos/1")
}
}
.onData { (index, data) in
...
}
.call()
RequestChain
is used to run multiple Request
s one at a time. When one completes, it passes its data on to the next Request
, so you can use it to build the Request
.
RequestChain.call
can optionally accept a callback that gives you all the data of every Request
when completed.
Note: You must use
Request.chained
to build yourRequest
. This gives you access to the data and errors of previousRequest
s.
RequestChain {
Request.chained { (data, errors) in
Url("https://jsonplaceholder.typicode.com/todos")
}
Request.chained { (data, errors) in
let json = Json(data[0]!)
return Url("https://jsonplaceholder.typicode.com/todos/\(json?[0]["id"].int ?? 0)")
}
}
.call { (data, errors) in
...
}
.update
is used to run additional calls after the initial one. You can pass it either a number or a custom Publisher
. You can also chain together multiple .update
s. The two .update
s in the following example are equivalent, so the end result is that the Request
will be called once immediately and twice every 10 seconds thereafter.
Request {
Url("https://jsonplaceholder.typicode.com/todo")
}
.update(every: 10)
.update(publisher: Timer.publish(every: 10, on: .main, in: .common).autoconnect())
.call()
If you want to use Request
as a Publisher
, use updatePublisher
:
Request {
Url("https://jsonplaceholder.typicode.com/todo")
}
.updatePublisher(every: 10)
.updatePublisher(publisher: ...)
.sink(receiveCompletion: { ... }, receiveValue: { ... })
Unlike update
, updatePublisher
does not send a value immediately, but will wait for the first value from the Publisher
.
swift-request
includes support for Json
.
Json
is used as the response type in the onJson
callback on a Request
object.
You can create Json
by parsing a String
or Data
:
Json("{\"firstName\":\"Carson\"}")
Json("{\"firstName\":\"Carson\"}".data(using: .utf8))
You can subscript Json
as you would expect:
myJson["firstName"].string // "Carson"
myComplexJson[0]["nestedJson"]["id"].int
It also supports dynamicMemberLookup
, so you can subscript it like so:
myJson.firstName.string // "Carson"
myComplexJson[0].nestedJson.id.int
You can use .string
, .int
, .double
, .bool
, and .array
to retrieve values in a desired type.
Note: These return non-optional values. If you want to check for
nil
, you can use.stringOptional
,.intOptional
, etc.
See CONTRIBUTING
See LICENSE
link |
Stars: 722 |
Last commit: 1 year ago |
Request
is now a Publisher
. This lets you use Combine operators on your Request:
Request {
Url("https://jsonplaceholder.typicode.com/todos")
}
.map(\.data)
.decode([Todo].self, decoder: JSONDecoder())
.sink(receiveCompletion: { ... }, receiveValue: { ... })
There are also properties for objectPublisher
, stringPublisher
, and jsonPublisher
. Using AnyRequest
, that previous example could be rewritten as:
AnyRequest<[Todo]> {
Url("https://jsonplaceholder.typicode.com/todos")
}
.objectPublisher
.sink(receiveCompletion: { ... }, receiveValue: { ... })
buildEither
support in RequestBuilder
(#39)Url
s with +
(#39) - Url("https://baseurl.com") + Url("/path")
If you would like to add your own parameters to be used in a Request, you can simply implement the RequestParam
protocol:
struct MyCustomParam: RequestParam {
func buildParam(_ request: inout URLRequest) {}
}
You can also conform to SessionParam
to modify the URLSessionConfiguration
:
struct MyCustomSessionParam: SessionParam {
func buildConfiguration(_ configuration: URLSessionConfiguration) {}
}
multipart/form-data
support (#43, #51)Use the Form
parameter in your Request body to send form data. You can send Form.Data
for file content, and key-value pairs with Form.Value
.
Request {
Url("http://example.com/send")
Method(.post)
Form {
Form.Data(data, named: "image.txt", withType: .text)
Form.Value(key: "email", "[email protected]")
}
}
@Requested
property wrapper and reimplementation of RequestView
(#46, iOS 14+, macOS 11+)This allows a Request to be specified in a property wrapper, and it will automatically update your View's body when the Request finishes. The projectedValue contains the status of the Request as a RequestStatus
enum:
struct TodoList: View {
@Requested private var allTodos = AnyRequest<[Todo]> {
Url("https://jsonplaceholder.typicode.com/todos")
}
var body: some View {
switch $allTodos {
case .loading: ProgressView()
case let .failure(error): Text(error.localizedDescription)
case let .success(todos):
List(todos) { todo in
Label(todo.title, systemImage: "checkmark.circle\(todo.completed ? ".fill" : "")")
}
}
}
}
RequestView
was reimplemented for iOS 14+ and macOS 11+ to provide more reliability. It also provides a new initializer that uses a RequestStatus
enum in the body:
RequestView(AnyRequest<[Todo]> {
Url("https://jsonplaceholder.typicode.com/todos")
}) { result in
switch result {
case .loading: ProgressView()
case let .failure(error): Text(error.localizedDescription)
case let .success(todos):
List(todos) { todo in
Label(todo.title, systemImage: "checkmark.circle\(todo.completed ? ".fill" : "")")
}
}
}
@Requested
property wrapper and reimplementation of RequestView (#46) by @carson-katriSwiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics