Swift: Declarative request builder
In 2019, I was looking for a way to make requests in Swift without using Alamofire or Moya, which both used declarative syntax. That’s when I found Request by Carson Katri, which allowed me to define all the necessary parameters for making a request in Swift.
After gaining more experience with SwiftUI and declarative syntax, I began to require additional functionality from Request. Specifically, I needed a way to manipulate the request result, process the response, and even repeat the request under certain conditions. Additionally, with iOS 13’s actor implementations available, I sought a library that utilized Actors and the async await syntax.
To revamp the core concepts of Request, I turned to the tree data structure to break down each request declaration. By treating each attribute as a node within the tree — such as Payload(_:), Authentication(_:token:), and Query(_:forKey:) — I could easily locate nodes within the tree and perform complex operations, such as FormGroup or QueryGroup, that required specific object types.
Off-topic: if you’re a fan of declarative syntax like myself, I recommend taking a look at the way I constructed the RequestDL package. This solution is particularly useful for declarative libraries and offers a number of great features.
The request
Staying true to the original implementation, I have kept these objects within the RequestDL package. With a range of objects available to specify each request attribute, you can easily modify the URLRequest and URLSessionConfiguration by leveraging URLRequestRepresentable and URLSessionConfigurationRepresentable. This is similar to the way SwiftUI works, making it a breeze to navigate and manipulate your requests.
Here’s an example of how to use it:
Here’s another example that utilizes FormGroup:
By combining various objects, you can create your own Property object capable of performing multiple operations in a single action. Here’s an example:
RequestDL.Task
To provide developers with more options during request operations, similar to Moya and Alamofire’s use of Plugins and Interceptors, I have implemented the Task protocol. This protocol is implemented by DataTask, DownloadTask, BytesTask, and GroupTask. Each one of these tasks has a single result() async throws
function that returns an object of the associated type. With the exception of GroupTask, all tasks return a TaskResult<T> where T can be a Data, URL, or AsyncBytes. Depending on the specified Task, the results will differ, but they are all encapsulated by TaskResult, which even implements the TaskResultPrimitive protocol.
These details are important as they allow for the implementation of new methods through extensions from the Task protocol using the where Element and some conditions. This enables various methods from the package to be available to process or alter the operation’s result.
Additionally, we have the GroupTask that can make a group request from an array. Although I have not yet received much feedback on its practical use, I believe it can be a useful feature in certain cases. If you have any suggestions or feedback, please don’t hesitate to contribute and help improve this library.
We also have the option to perform requests using Combine, which can be done as follows:
Operations after request
With declarative programming, we can define a sequence of sequential steps to modify the initial operation. A clear example of this is RxSwift or Combine, where we start with an initial publisher and then add functions that modify the result, such as map(_:)
, filter(_:)
, flatMap(_:)
, drop(while:)
, and other methods.
To take advantage of this feature of declarative programming, two types of protocols were developed: TaskModifier and TaskInterceptor. TaskModifier functions like a formula, taking the result of the previous operation, applying changes to it, and returning a new result. TaskInterceptor, on the other hand, only receives the result of the previous operation, without changing the next result.
These protocols allow for the implementation of objects that read the result, and those that change the result according to specific rules. Within the package, we have interceptors such as breakpoint()
, detach(_:)
, and logInConsole(_:)
. The breakpoint()
issues a SIGTRAP to pause the running thread and allows for stepping through the code. The detach(_:)
method takes a closure as a parameter that receives the result of the Task, allowing operations to be performed without affecting or preventing the result of the request. The logInConsole(_:)
method receives a flag to activate direct printing in the console with information about the request result.
Regarding modifiers, we have several interesting options, such as decode(_:decoder:)
, flatMap(_:)
, and onStatusCode(_:_:)
, in addition to other implemented methods. The decode(_:decoder:)
method converts the Data result into a model, while flatMap(_:)
changes the result, allowing the closure to throw errors. The onStatusCode(_:_:)
method reads the HTTP Status Code, calls the closure to handle the result and convert it into a specific error, if necessary.
Other Modifiers
The goal is to provide developers with a set of base modifiers and protocols that allow them to achieve their desired outcomes with the available tools. The Task can be accessed within the modifier, enabling the implementation of features like task repetition in case of errors.
Expanding the use of modifiers, developers can implement specific ones to handle cases such as invalid tokens. For example, a custom modifier could refresh the token and then execute the request again. Moreover, if an application has control of an external token, a method from that other library could be called to obtain a new token and then perform the operation again. All of this can be done while maintaining the integrity of the modifiers and interceptors specified in the original request.
Conclusion
I believe this approach represents a significant step towards the future of Swift and declarative programming. While there are mature libraries such as Moya and Alamofire with more features available, and they are benchmarks for the iOS community, I am not aware of any other library that provides the features discussed in this article. Although there is still much to be implemented to cover all usage scenarios in an HTTP request, RequestDL is a continuous improvement challenge that allowed me to better understand declarative programming concepts and how to apply them to code.
I am excited about the possibilities of this paradigm, whether it is for making requests or building interfaces, and I look forward to continuing to learn and build complex solutions using this approach.
I would like to acknowledge the many articles available on Medium, technical discussions on the Swift.org and StackOverflow forums, and the contributions of other developers, without whom building this library would not have been possible. I also express my gratitude to Carson Katri 🫰 for initiating this project in 2019 and allowing me to contribute and develop this alternative version.
Head over to RequestDL’s Github repository to explore and download the library, and start building your Swift applications with declarative programming and powerful HTTP request features!
Thank you for reading! I hope that this article has contributed to your knowledge. ✌️
If you would like to contribute so that I can continue producing more technical content, please feel free to buy me a coffee ☕️ through the Buy me a Coffee platform.
Your support is essential to maintain my work and contribute to the development community.