Simple and Robust API Calls, using the Fetchable protocol
Note: BFWFetch now uses async/await. For the Combine version, check out the older feature/combine branch.
The Fetchable
protocol provides a simple and flexible way to fetch data from API calls.
Read more detail about Fetchable (for Combine) in the Better Programming publication on Medium.
A Fetchable
type must provide:
- A
baseURL
- The fetch parameter keys as an
enum Key: FetchKey
. - The
Fetched
type expected in the response.
Fetchable takes care of all the inner workings. Fetchable uses async/await, which we can use to update our model and UI.
According to the Open Weather API docs, to fetch weather data for a city, we need to provide:
- Base URL: https://api.openweathermap.org/data/2.5
- End URL path: weather
- Parameter keys:
appID
,q
(the site's city and country code) ,units
.
The API will respond with a JSON payload containing a Site
.
For example, we might call the API with these values for each parameter key:
appID
=1234567890abcdef
q
=Sydney, AU
(for Sydney, Australia)units
=metric
Open Weather will provide you with your own appID, after you sign up. It's free.
In order to fetch the weather, with the example request above, we can simply create a Weather type that conforms to Fetchable, like this:
import BFWFetch
struct Weather: Fetchable {
static let baseURL = URL(string: "https://api.openweathermap.org/data/2.5")!
enum Key: String, FetchKey {
case appID
case site = "q"
case system = "units"
}
typealias Fetched = Site
}
That's it!
Fetchable takes care of mapping the keys to request parameters, creating the URL, request and network connection.
To initiate the actual fetch from the API, we just ask our Fetchable type for the fetched result:
try await Weather.fetched(
keyValues: [
.appID: "1234567890abcdef",
.site: "Sydney,AU",
.system: .metric
]
)
Since the keys are cases of an enum, the compiler forces us to enter the keys correctly, and lists them in the code completion popup menu (when we type the leading dot).
We would typically expose the keys as parameters in a custom Swift function, such as:
extension Weather {
static func fetched(
city: String,
countryCode: String?,
system: System
) async throws -> Fetched {
try await fetched(
keyValues: [
.appID: "1234567890abcdef",
.site: [city, countryCode]
.compactMap { $0 }
.joined(separator: ","),
.system: system
]
)
}
}
Let's define our units system as enum System
, and let Fetchable
know that it conforms to FetchValue
, so we can use it in the keyValues
dictionary, as shown above.
/// Measurement system, such as metric, imperial.
enum System {
case metric
case imperial
}
extension System: FetchValue {}
We would call that function from our view model, such as:
struct WeatherScene {
@State var city: String = "Sydney"
@State var countryCode: String = "AU"
@State var system: System = .metric
@State var site: Site?
}
private extension WeatherScene {
func fetch() async {
...
self.site = try await API.Request.Weather.fetched(
city: city,
countryCode: countryCode,
system: system
)
...
}
}