icerockdev / moko-mvvm

Model-View-ViewModel architecture components for mobile (android & ios) Kotlin Multiplatform development
https://moko.icerock.dev/
Apache License 2.0
994 stars 95 forks source link

iOS action handling not working more times (maybe lifecycle issue) #267

Open kostapostolakis opened 3 months ago

kostapostolakis commented 3 months ago

I have the NewGameViewModel in commonMain Shared code of KMM.

interface NewGameIntents {
    fun updateUserName(userName: String)
    fun startNewGameButtonTapped()
    fun newGameCreated(newGameID: String)
    fun newGameCreationFailed()
}

sealed interface NewGameActions {
    data class CreateNewGameAction(val userName: String): NewGameActions
    data class GoToMainScreenAction(val gameID: String, val userName: String) : NewGameActions
}

class NewGameViewModel: BaseViewModel(), NewGameIntents {
    private val _actions = Channel<NewGameActions>()
    val actions: CFlow<NewGameActions> get() = _actions.receiveAsFlow().cFlow()

    private val _userName: CMutableStateFlow<String> = MutableStateFlow("").cMutableStateFlow()
    val userName: CStateFlow<String> = _userName.cStateFlow()

    init {
        val previousName = SettingsHelper().lastGameName ?: SettingsHelper().userName
        previousName?.let {
            updateUserName(it)
        }
    }

    override fun updateUserName(userName: String) {
        _userName.value = userName
    }

    override fun startNewGameButtonTapped() {
        updateUserName(_userName.value.trim())
        createNewGame()
    }

    override fun newGameCreated(newGameID: String) {
        showLoading(false)
        viewModelScope.launch {
            _actions.send(NewGameActions.GoToMainScreenAction(newGameID, _userName.value))
        }
    }

    override fun newGameCreationFailed() {
        showLoading(false)
        showErrorAlert()
    }

    private fun createNewGame() {
        showLoading(true)
        viewModelScope.launch {
            _actions.send(NewGameActions.CreateNewGameAction(_userName.value))
        }
    }
}

I have also the NewGameScreen in iOS:

struct NewGameScreen: View {

    @ObservedObject var newGameViewModel: NewGameViewModel = NewGameViewModel()

    var body: some View {        
        ScrollView {
            VStack(spacing: 0) {
                CustomTextField(
                    value: newGameViewModel.state(\.userName),
                    label: getKmmString(stringResource: \.your_name),
                    onTextChange: { text in
                        newGameViewModel.updateUserName(userName: text)
                    }
                )

                Spacer().frame(height: 32)
                CustomButton(
                    text: getKmmString(stringResource: \.start_game),
                    onClick: {
                        newGameViewModel.startNewGameButtonTapped()
                    }
                )
            }
            .padding(16)
        }
        .navigationBarBackButtonHidden(true)
        .onReceive(createPublisher(newGameViewModel.actions)) { action in
            handleNewGameAction(action: action)
        }
    }

    private func handleNewGameAction(action: NewGameActions) {
        switch action {
        case let action as NewGameActionsCreateNewGameAction: do {
            let newGameMap = getMapFromFunction(name: action.userName) // Create map here

            var documentReference: DocumentReference? = nil
            documentReference = Firestore.firestore()
                .collection(Game.companion.COLLECTION_GAMES)
                .addDocument(data: newGameMap) { error in
                    if let error {
                        newGameViewModel.doNewGameCreationFailed()
                    } else if let documentId = documentReference?.documentID, !documentId.isEmpty {
                        newGameViewModel.doNewGameCreated(newGameID: documentId)
                    } else {
                        newGameViewModel.doNewGameCreationFailed()
                    }
                }
        }

        case let action as NewGameActionsGoToMainScreenAction: do {
            closeScreen()
            PalermoHelper().goToMainGameScreen(gameID: action.gameID, name: action.userName)
        }

        default: return
        }
    }
}

When I tap the button, startNewGameButtonTapped() function of viewModel is called and then the action CreateNewGameAction should be handled by iOS.

Sometimes it works and sometimes not. Maybe it is a lifecycle issue, but I am not sure and I do not know how to solve it. Do you have any ideas?