Minon Weerasinghe
Minon Weerasinghe

Reputation: 342

Swift Combine - Async calls

I have the following functions:

    func getUserProfile() -> AnyPublisher<UserProfileDTO?, Error> {
    return Future { [unowned self] promise in
        do {
            if let data = KeychainWrapper.standard.data(forKey: profileKey) {
                let profileDTO = try PropertyListDecoder().decode(UserProfileDTO.self, from: data)
                setCurrentSession(profileDTO)
                promise(.success(profileDTO))
            }
            else {
                promise(.success(nil))
            }
        }
        catch {
            // Delete current UserProfile if cannot decode
            let _ = KeychainWrapper.standard.removeAllKeys()
            promise(.failure(error))
        }
    }.eraseToAnyPublisher()
}

    func connect(userProfile: UserProfileDTO) -> AnyPublisher<UserProfileDTO, Error> {
    return Future { promise in
        SBDMain.connect(withUserId: userProfile.email) { (user, error) in
            if let error = error {
                promise(.failure(error))
            }
            else {
                promise(.success(userProfile))
            }
        }
    }.eraseToAnyPublisher()
}

What I want to do is to first call the getUserProfile() method and if the return value in not nil then call the connect() method. However, if the getUserProfile() has nil response it does not need to call the connect() and it should just return the nil response. Both these methods needs to be called from the autoLoginUser() method. The problem I'm having right now is figuring out how to do this in a clean swift way without writing too much nested statements.

I tried to use flatMaps but it didn't workout the way I expected. Any help is much appreciated.

A solution I've been working on at the moment is this. But it doesn't quite work.

    func autoLoginUser2() -> AnyPublisher<UserProfile?,Error> {
    getUserProfile()
        .tryMap { [unowned self] in
            if let currentProfile = $0 {
                return connect(userProfile: currentProfile)
                    .tryMap {
                        //Map from UserProfileDTO --> UserProfile
                        return UserProfileDTOMapper.map($0)
                        
                    }
            }
            return nil
        }.eraseToAnyPublisher()
}

Upvotes: 1

Views: 653

Answers (2)

LuLuGaGa
LuLuGaGa

Reputation: 14388

If you change the signature of connect to return Optional profile:

func connect(userProfile: UserProfileDTO) -> AnyPublisher<UserProfileDTO?, Error>

You could do something like this:

getUserProfile()
    .flatMap { userProfile -> AnyPublisher<UserProfileDTO?, Error> in
            
        if let userProfile = userProfile {
            return connect(userProfile: userProfile)
                .eraseToAnyPublisher()
        } else {
            return Just<UserProfileDTO?>(nil)
                .setFailureType(to: Error.self)
                .eraseToAnyPublisher()
        }
    }
    //.sink etc.

If you don't need the publisher to emit nil, you could leave the connect signature as is and use compactMap:

getUserProfile()
    .compactMap { $0 }
    .flatMap {
        connect(userProfile: $0)
            .eraseToAnyPublisher()
    }

Upvotes: 0

Andrea
Andrea

Reputation: 26385

With some adjustment for used types and error types this should work. First you ask for the profile, then you force unwrap the profile if it is nil you throw an error that will be sent to the sink as a failure. If the profile is present you call connect.

getUserProfile()
.tryMap { userDTO -> UserProfileDTO in
    if let id = userDTO {
        return id
    }
    throw MyError.noProfileDT
}
.flatMap { id in
    connect(id)
}
.sink {
    //.....
}

Upvotes: 1

Related Questions