Is there a way to detect when a publisher has a new subscriber? | Swift, Combine

I'm developing a MVVM structure with API calls. I have this structure now:

    //Get publisher
    loginPublisher = LoginService.generateLoginPublisher()
        
    //Create a subscriber
    loginSubscriber = loginPublisher!
        .sink { error in
            print("Something bad happened")
            self.isLoading = false
        } receiveValue: { value in               
            
            self.saveClient(value)
            self.client = value
            self.isLoading = false
        }
    
    //Asking service to start assync task and notify its result on publisher
    LoginService.login(email, password, loginPublisher!)

Basically what I do is obtain certain publisher from a LoginService, then I create a subscriber on loginPublisher, and then I tell LoginService to make some assync logic and send it result to loginPublisher this way I manage sent data with loginSubscriber.

I would like to execute LoginService.login() internally when I execute LoginService.generateLoginPublisher(), but if I do that, there is a chance that LoginService.login() logic finish before I create loginSubscriber, that's why I was forced to control when to call LoginService.login().

How could I detect from LoginService when its publisher has a new subscriber?

This is my LoginService class:

class LoginService{
static func generateLoginPublisher() -> PassthroughSubject<Client, NetworkError>{
    return PassthroughSubject<Client, NetworkError>()
}

static func login(_ email: String,_ password: String,_ loginPublisher: PassthroughSubject<Client, NetworkError>){
    let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
    
    print(url)
    
    let parameters: [String: String] = [
        "password": password,
        "login": email
    ]
    
    print(parameters)
    
    let request = AF.request(
        url, method: .post,
        parameters: parameters,
        encoder: JSONParameterEncoder.default
    )
    
    request.validate(statusCode: 200...299)
    request.responseDecodable(of: Client.self) { response in
        if let loginResponse = response.value{//Success
            loginPublisher.send(loginResponse)
        }
        else{//Failure
            loginPublisher.send(completion: Subscribers.Completion<NetworkError>.failure(.thingsJustHappen))
        }
    }
}

}

Upvotes: 0

Views: 2334

Answers (2)

I finally solved my problem using Future instead of PassthroughtSubject as Dávid Pásztor suggested. Using Future I don't have to worried about LoginService.login() logic finish before I create loginSubscriber.

LoginSevice.login() method:

static func login(_ email: String,_ password: String) -> Future<Client, NetworkError>{        
    return Future<Client, NetworkError>{ completion in
        let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
        
        print(url)
        
        let parameters: [String: String] = [
            "password": password,
            "login": email
        ]
        
        print(parameters)
        
        let request = AF.request(
            url, method: .post,
            parameters: parameters,
            encoder: JSONParameterEncoder.default
        )
        
        request.validate(statusCode: 200...299)
        request.responseDecodable(of: Client.self) { response in
            if let loginResponse = response.value{//Success
                completion(.success(loginResponse))
            }
            else{//Failure
                completion(.failure(NetworkError.thingsJustHappen))
            }
        }
    }
}

Implementation:

        loginSubscriber = LoginService.login(email, password)
        .sink { error in
            print("Something bad happened")
            self.isLoading = false
        } receiveValue: { value in
            self.saveClient(value)
            self.client = (value)
            self.isLoading = false
        }

Upvotes: 1

David Pasztor
David Pasztor

Reputation: 54755

If you want full control over subscriptions, you can create a custom Publisher and Subscription.

Publisher's func receive<S: Subscriber>(subscriber: S) method is the one that gets called when the publisher receives a new subscriber.

If you simply want to make a network request when this happens, you just need to create a custom Publisher and return a Future that wraps the network request from this method.

In general, you should use Future for one-off async events, PassthroughSubject is not the ideal Publisher to use for network requests.

Upvotes: 1

Related Questions