How to make a Published property call map only when it receives data

Viewed 71

I'm pretty new in Combine, I'm trying to create a login flow where:

  • When I click on the login button the app fires the GoogleSignIn sign function
  • Then, when the session data is received on GoogleDelegate it needs to start a request to my server-side login method.

To do that I have a GoogleSignInController that controls the sign-in process:

class GoogleSignInController: GIDSignInDelegate {
    @Published var onSessionAcquired: GoogleSignInSessionData = ("", "")

    func signIn() {
        if (GIDSignIn.sharedInstance()?.presentingViewController == nil) {
            GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
        }
        GIDSignIn.sharedInstance()?.signIn()
    }

    func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
        if let error = error {
            if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
                print("The user has not signed in")
            } else {
                print("\(error.localizedDescription)")
            }
            return
        }
        
        onSessionAcquired = GoogleSignInSessionData(idToken: user.authentication.idToken ?? "", serverAuthCode: user.serverAuthCode ?? "")
        
        print("Successful sign-in!")
        signedIn = true
    }
}

Then in the ViewModel, I'm using some states to control the process:

    static func whenStartedLogin() -> Feedback<State, Event> {
        Feedback { (state: State) -> AnyPublisher<Event, Never> in
            guard case .loadingGoogleSignIn = state else { return Empty().eraseToAnyPublisher() }

            GoogleSignInController.shared.signIn()
            
            return GoogleSignInController.shared.$onSessionAcquired
                .receive(on: DispatchQueue.main)
                .map { Event.onGoogleSessionAcquired($0) }
                .eraseToAnyPublisher()
        }
    }

    static func whenReceivedGoogleSession() -> Feedback<State, Event> {
        Feedback { (state: State) -> AnyPublisher<Event, Never> in
            guard case let .loadingServerSession(googleSession) = state else { return Empty().eraseToAnyPublisher() }

            return self.signIn(idToken: googleSession.idToken, serverAuthCode: googleSession.serverAuthCode)
              .map { (userInfo, error) in
                  if let error = error {
                      return Event.onFailedToAcquireSession(error)
                  } else if let userInfo = userInfo {
                      return Event.onUserInfoAcquired(userInfo)
                  } else {
                      return Event.onFailedToAcquireSession(AppError.unknown)
                  }
              }
              .eraseToAnyPublisher()
        }
    }

The problem is: when I click on the login button, the whenStatedLogin function is fired, and it maps my event to be Event.onGoogleSessionAcquired. This should be mapped only when onSessionAcquired is changed (after the sign delegate is called). How can I do that?

1 Answers

Just figured out the solution for this problem. The correct data type to be used in my case is a PassthroughSubject.

let sessionSubject = PassthroughSubject<GoogleSignInSessionData, Never>()

...

func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
    if let error = error {
        if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
            print("The user has not signed in")
        } else {
            print("\(error.localizedDescription)")
        }
        return
    }
    
    sessionSubject.send(GoogleSignInSessionData(idToken: user.authentication.idToken ?? "", serverAuthCode: user.serverAuthCode ?? ""))
    
    print("Successful sign-in!")
    signedIn = true
}

Having it this way will make the map is called only when the subject receives an event.

Related