This is the fourth step of a serie named iOS at Scale based on the next steps:

On the previous post we’ve seen how a simple Package could make our life easier and help us to see parts of our code highly coupled. A good excercise to see if our coude it’s coupled (along make unit testing to see if we can mock without effort our dependencies) is to think, what should I do if I wan’t to make from this function a framework? Some times our code could look fine, but we can still have highly coupled things passing unnoticed.

For this chapter, the first thing we’re going to do it’s make our app compile again so…

Go to the step-2 tag.

Start refactor DataProvidersKit

The first class to refactor will be CharacterService since other classes are depending from them.

protocol CharacterServiceProtocol: AnyObject {
    func characters(nameStartsWith: String?,
                    offset: Int?,
                    completion: @escaping CharactersCompletion)
    func character(with id: Int, completion: @escaping CharacterCompletion)
}

This kind of names could be avoid if we follow  guidelines, so let’s change the name to, CharacterServicing. (commit: 5396064)

Also, we’re going to remove this extra complexity typealiases: (commit: 7f86c80)

public typealias CharacterCompletion = (_ result: Result<Character, CharacterRepositoryError>) -> Void
public typealias CharactersCompletion = (_ result: Result<[Character], CharacterRepositoryError>) -> Void

To finally use the Done<T, E> provided by Support on the previous chapter. This way we see on all methods whats happening and don’t hide info under aliases, that are on other files.

protocol CharacterServicing: AnyObject {
    func characters(nameStartsWith: String?, offset: Int?, completion: @escaping Done<[Character], CharacterRepositoryError>)
    func character(with id: Int, completion: @escaping Done<Character, CharacterRepositoryError>)
}

Not it’s time for the Service itself. And we’re starting from:

class CharacterService {
    let apiClient: MarvelAPIClient
    
    init(apiClient: MarvelAPIClient) {
        self.apiClient = apiClient
    }
}

extension CharacterService: CharacterServicing {
    func characters(nameStartsWith: String?,
                    offset: Int?,
                    completion: @escaping Done<[Character], CharacterRepositoryError>) {
        
        let request = GetCharacters(name: nil,
                                    nameStartsWith: nameStartsWith,
                                    offset: offset)
        apiClient.send(request) { response in
            switch response {
            case .success(let dataContainer):
                let networkCharacters = dataContainer.results
                guard networkCharacters.count != 0 else {
                    completion(.failure(.notFound))
                    return
                }
                
                let characters = networkCharacters.map { Character(withResponse: $0)}
                
                completion(.success(characters))
            case .failure(let error):
                completion(.failure(CharacterRepositoryError(withResponseError: error)))
            }
        }
    }
    
    func character(with id: Int, completion: @escaping Done<Character, CharacterRepositoryError>) {
        let request = GetCharacter(characterId: id)
        apiClient.send(request) { response in
            switch response {
            case .success(let dataContainer):
                let networkCharacters = dataContainer.results
                guard let networkCharacter = networkCharacters.first else {
                    completion(.failure(.notFound))
                    return
                }
                let character = Character(withResponse: networkCharacter)
                
                completion(.success(character))
            case .failure(let error):
                completion(.failure(CharacterRepositoryError(withResponseError: error)))
            }
        }
    }
}

As we see, nameStartsWith isn’t used, so we’re going to remove it. Also, like CharacterService isn’t a class to make subclasses of it, we’re going to mark it like final.

Adding final to your method it means more than just a compile time error. It will internally optimise your code, it will generate a more efficient representation, furthermore it can result into a faster execution. Next time that you create a method and you are not expecting to be overridden remember to mark as final. (Ref)

At this point, we see our characters method a little bit simpler. (commit: 5e3f0d0)

func characters(offset: Int?, completion: @escaping Done<[Character], CharacterRepositoryError>) {
        provider.characters { result in
            switch result {
            case .success(let dataContainer):
                let networkCharacters = dataContainer.results
                guard networkCharacters.count != 0 else {
                    completion(.failure(.notFound))
                    return
                }
                
                let characters = networkCharacters.map { Character(withResponse: $0)}
                
                completion(.success(characters))
            case .failure(let error):
                completion(.failure(CharacterRepositoryError(withResponseError: error)))
            }
        }
        
    }

Wee need to add the offset value to our provider on MarvelAPI (commit: dd21dfc)

public func characters(offset: Int?, _ done: @escaping (Result<[Character], MarvelError>) -> Void) {
    client.perform(Endpoint(path: "/v1/public/characters", parameters: ["offset": offset ?? 0])) { (result: Result<Page<[Character]>, Error<MarvelError>>) in
        done(result.map(\.results).mapTerror(\.marvelError))
    }
}

And then, lets adapt to this CharacterService. Like in the previous post, we need an extension of MarvelError to map it easily to CharacterRepositoryError.

extension MarvelError {
    var repositoryError: CharacterRepositoryError {
        switch self {
        case .server(let code, let message): return .marvelError(code: code, message: message)
        case .underlying(let error): return .unknow(error)
        }
    }
}

And after that, we only need to map a couple of things. At the first time, this could be a little confusing, but lets explain what’s happening inside.

  • We receive Result<[MarvelClient.Character], MarvelError>
  • Pass it to the completion, but mutated, look at the chain. result.map { }.map { }. Each map take the previous value and returns a new value.
  • The first map will take that result and transform it to Result<[DataProvidersKit.Character], MarvelError>
  • The second map will take the first maping result Result<[DataProvidersKit.Character], MarvelError> and mutate it to Result<[DataProvidersKit.Character], CharacterRepositoryError>

As wee see, one map mutates the .success. branch of the result and the other one mutates the .failure branch.

(commit: dabee48)

Note: I’m allowing empty array results

func characters(offset: Int?, completion: @escaping Done<[Character], CharacterRepositoryError>) {
    provider.characters(offset: offset) { result in
        completion(result
            .map { $0.map { Character(withResponse: $0) } }
            .mapTerror(\.repositoryError)
        )
    }
}

Now, lets do the single character method.

  • We receive Result<[MarvelClient.Character], MarvelError>
  • The first map will take that result and transforming it taking only the first value it Result<Character, MarvelError>
  • The second map will take the first maping result Result<Character, MarvelError> and mutate it to Result<[DataProvidersKit.Character], CharacterRepositoryError>
provider.character(by: id) { result in
    completion(result
        .map { $0.first.map { Character(withResponse: $0) } ?? "Error" }
        .mapTerror(\.repositoryError)
    )
}

But as we can see, what if our response has an Empty array, as you can see above, we need to throw an error, but Result map doesn’t allow us to do that, so let’s add one more function to our previous extension on support.

func tryMap<NewSuccess, NewFailure>(transformSuccess: (Success) throws -> NewSuccess,
                                    transformFailure: (Failure) -> NewFailure) -> Result<NewSuccess, NewFailure> {
    do {
        switch self {
        case .success(let success): return .success(try transformSuccess(success))
        case .failure(let error): return .failure(transformFailure(error))
        }
    } catch {
        return .failure(error as! NewFailure)
    }
}

And an Optional extension to throw errors like ?? error, I really like this extension. (commit: ed9d6dd)

public extension Optional {
    static func ?? (optional: Self, error: Error) throws -> Wrapped {
        switch optional {
        case .some(let value): return value
        case .none: throw error
        }
    }
}

And our final result of character(by: "id") it’s: (commit: 584ba51)

func character(with id: Int, completion: @escaping Done<Character, CharacterRepositoryError>) {
    provider.character(by: id) { result in
        completion(result.tryMap(
            transformSuccess: { try $0.first.map { Character(withResponse: $0) } ?? CharacterRepositoryError.notFound },
            transformFailure: { $0.repositoryError })
        )
    }
}

And the last step, fix the Assemblies (commit: c7909b2).

And then, lest fixt the dependency graph and the parameter startsWith from all layers. (commit: 8112897)

Now our app it’s compiling again. Let’s rename DataProvidersKit

Renaming

Since we’re using Tuist, rename a Module, is quite simple. Let’s change the folder name, the name inside the Project.swift, and then update dependencies. (commit: 23680f2)

Applying Inversion Of Control

Inversion of Control it’s a way to avoid coupling our Core to our Modules/Libraries, instead of that, our modules will depend on our Core, this way, will be the Core the responsible of expose those protocols that the other libraries must conform. Giving us the ability to replace those libraries with painless, due that our application code doesn’t need to change anything.

In order to do this, we’re going to change the dependency graph. Actually it’s Features -> Core -> MarvelClient, and it will be Features -> MarvelClient -> Core, but really, our features won’t use code under MarvelClient, it will use only MarvelClient to take the Core.

So again, let’s edit the Project.swift of those libraries. (commit: dace026)

Now, we need to move the CharacterProviding inside the Core Module. (commit: fc5a1e5) Then remove the dependencies to MarvelClientfrom core. (commit: fd8f8eb)

Models mapping must be moved from MarvelClient to Core, must be done inside MarvelClient. (commit: 67afea5)

The last, find & replace from old DataProvidersKit to the new Core (commit: 9a7af3e), as you can see, without touching anything of the code inside the Features or AppCore, we keep compiling the app, and we’ve gained a lot of flexibility to change how the network client works or how the MarvelClient works.

Conclusions

At this post finally we’ve some key points about how modern techniques could reduce dramastically the code quantity and give us more code quality. Not only that, now we are more flexibles to change, and with the possibility of share our HTTPClient with the world if we want to.

Also, we’ve gained some functionality. Before we haven’t the ability of read the Error body in our code and be able to do that it’s something powerful. How many times we don’t need to know the http code and some parameter in the body to do one action or other? This way we can archieve that.

In the middle way, we are killing some bad Modules responsabilities, excesive classes and nested ifs, making the data flow easier to read than before and harder to introduce bugs. Each layer do a minimal task, take some data, map those data. That’s the most complex layer, and if something fails, we’re mapping the Error with known Types. Without forget the easiness to make test, test-doubles and fake dependencies

Steps