My goal is to create a modular network service layer which can easily be unit tested.
In the quest for that, I started simply with the following protocol:
protocol NetworkService {
associatedtype Response
func fetchData() async throws -> Response
}
I then proceed to create a service to fetch the news feed like so:
struct FetchNewsService: NetworkService {
typealias Response = [NewsAssetModel]
func fetchData() async throws -> Response {
guard let fetchNewsURL = URL(string: SystemConstants.NetworkConstants.fetchNewsEndpoint) else {
throw NetworkError.invalidURL
}
let urlRequest = URLRequest(url: fetchNewsURL)
let session = URLSession(configuration: .default)
guard let (data, response) = try? await session.data(for: urlRequest) else {
// Would need to handle no internet error as well
throw NetworkError.server
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode > 400 {
throw NetworkError.server
}
guard let newsResponse = try? JSONDecoder().decode(NewsResponseModel.self,
from: data) else {
throw NetworkError.decode
}
return newsResponse.assets
}
}
In my view model, I would like to make a request to fetch the news feed items and so I wanted to inject a type of NetworkService
into my view model.
class NewsAssetManager {
private(set) var newsAssets: [NewsAssetModel] = []
private var newsService: NetworkService // 1 - error on this line
// 2 - and this line below
init(withNewsService newsService: NetworkService = FetchNewsService()) {
}
}
The first error that I get at the property declaration is Protocol 'NetworkService' can only be used as a generic constraint because it has Self or associated type requirements
The second error that I get at the initializer is also the same Protocol 'NetworkService' can only be used as a generic constraint because it has Self or associated type requirements
What am I doing incorrectly and how can I fix / improve this ?
Update with generics
protocol NetworkService {
func fetchData<T>() async throws -> T
}
struct FetchNewsService: NetworkService {
func fetchData<T>() async throws -> T {
guard let fetchNewsURL = URL(string: SystemConstants.NetworkConstants.fetchNewsEndpoint) else {
throw NetworkError.invalidURL
}
let urlRequest = URLRequest(url: fetchNewsURL)
let session = URLSession(configuration: .default)
guard let (data, response) = try? await session.data(for: urlRequest) else {
// Would need to handle no internet error as well
throw NetworkError.server
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode > 400 {
throw NetworkError.server
}
guard let newsResponse = try? JSONDecoder().decode(NewsResponseModel.self, from: data)
else {
throw NetworkError.decode
}
return newsResponse.assets // Compile error
}
}
I get an error on the last line Cannot convert return expression of type '[NewsAssetModel]' to return type 'T'
Update based on comments & Answer
Adding a combination of Any & Some to the property or initialiser still doesn't get rid of the compilation errors faced.
I also confirmed via terminal and this code that I at least am running Swift 5.6:
#if swift(>=5.6)
print("swift 5.6")
#endif
This is how I would define the protocol when not using an associated type. Make the function generic instead and the type should conform to Decodable
protocol NetworkService {
func fetchData<Response: Decodable>(url: URL) async throws -> Response
}
Then I would change the implementation of the conforming type to
struct FetchNewsService: NetworkService {
func fetchData<Response: Decodable>(url: URL) async throws -> Response {
let urlRequest = URLRequest(url: url)
let session = URLSession(configuration: .default)
guard let (data, response) = try? await session.data(for: urlRequest) else {
throw NetworkError.server
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode > 400 {
throw NetworkError.server
}
do {
return try JSONDecoder().decode(Response.self, from: data)
} catch {
throw NetworkError.decode(error)
}
}
}
Now the implementation isn't so hardcoded to one solution, actually it is so general that we can move it into an extension of the protocol
extension NetworkService {
func fetchData<Response: Decodable>(url: URL) async throws -> Response {
// same code as above
}
}
Which means in this case FetchNewsService
isn't needed anymore and it can be deleted.
The NewsAssetManager
can then be changed to below where all the code that is specific to this particular api and response type now resides.
class NewsAssetManager {
private(set) var newsAssets: [NewsAssetModel] = []
private var newsService: NetworkService
init(withNewsService newsService: NetworkService) {
self.newsService = newsService
}
func fetchNews() async throws {
guard let fetchNewsURL = URL(string: SystemConstants.NetworkConstants.fetchNewsEndpoint) else {
throw NetworkError.invalidURL
}
let model: NewsResponseModel = try await newsService.fetchData(url: fetchNewsURL)
newsAssets = model.assets
}
}