Giter Club home page Giter Club logo

modernmvvm's People

Contributors

v8tr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

modernmvvm's Issues

why reduce is static?

Hi Vadim

First I have to thank you for your great article.

I'm wondering is there any specific reason that you used static function for reduce or feedbacks (whenLoading, userInput)?

Publishers.system signature changed - feedbacks type changed

New in Swift, loved the blog post, big fan of functional reactive! Thanks for the example :)

In Publishers.system they removed the argument where you can pass a scheduler (although I always thought it's a good ide) and this was an easy fix but now I get the below error message.

Cannot convert value of type '[Feedback<AViewModel.State, AViewModel.Event>]' to expected argument type 'Array<Feedback<_, _>>'

// 1. 
    Publishers.system(
      initial: state,
      feedbacks: [
        // 2.
        Self.whenLoading(),
        Self.userInput(input: input.eraseToAnyPublisher())
      ],
      reduce: Self.reduce
    )
      .assignNoRetain(to: \.state, on: self)
      .store(in: &bag)

Your help is appreciated.

return state from .submitting state doesn't work

Hi @V8tr
I'm returning a state on .submitting state for a particular event, but it doesn't return the expected state, here's my code, what am I doing wrong here?

here on .submitting state from an API call I'm calling onSignedIn event, but even after calling return .signedIn(token) , II am not getting this state case let .signedIn(token)

Here's the shortcode

        case .submitting:
            switch event {
            case let .onFailedToLogin(error):
                return .error(error)
            case let .onSignedIn(token):
                return .signedIn(token)
            default:
                return state
            }

Here's the full code

class LoginViewModel: LoginViewModelProtocol, Identifiable {
    let authRepository: AuthRepository

    @Published var phoneNumber: String = ""
    @Published var password: String = ""
    @Published var isValid: Bool = false
    @Published var phoneNumberError: String?
    @Published var passwordError: String?

    private var bag = Set<AnyCancellable>()
    private let input = PassthroughSubject<Event, Never>()

    @Published private(set) var state = State.initial
    @Published var phoneNumberValidator = PhoneNumberValidation.empty
    @Published var passwordValidator = PasswordValidation.empty

    @Inject var router: Router

    init() {
        authRepository = AuthRepository()

        Publishers.system(
            initial: state,
            reduce: reduce,
            scheduler: RunLoop.main,
            feedbacks: [
                whenSubmitting(),
                Self.userInput(input: input.eraseToAnyPublisher())
            ]
        )
        .assign(to: \.state, on: self)
        .store(in: &bag)

        Publishers.CombineLatest(validPhoneNumberPublisher, passwordValidatorPublisher)
            .dropFirst()
            .sink { _emailError, _passwordValidator in
                self.isValid = _emailError.errorMessage == nil &&
                    _passwordValidator.errorMessage == nil
                if self.isValid {
                    self.send(event: .onSubmitable)
                }
                print("LoginViewModel combine \(self.state)")
            }
            .store(in: &bag)

        validPhoneNumberPublisher
            .dropFirst()
            .sink { _error in
                self.phoneNumberError = _error.errorMessage
            }
            .store(in: &bag)

        passwordValidatorPublisher
            .dropFirst()
            .sink { _error in
                self.passwordError = _error.errorMessage
            }
            .store(in: &bag)
    }

    deinit {
        bag.removeAll()
    }

    func send(event: Event) {    
        input.send(event)
    }

    private var validPhoneNumberPublisher: AnyPublisher<PhoneNumberValidation, Never> {
        $phoneNumber
            .debounce(for: 0.5, scheduler: RunLoop.main)
            .removeDuplicates()
            .map { _value in
                if _value.isEmpty {
                    return .empty
                } else if !_value.isPhoneNumber() {
                    return .inValidPhoneNumber
                } else {
                    return .validPhoneNumber
                }
            }
            .eraseToAnyPublisher()
    }

    private var passwordValidatorPublisher: AnyPublisher<PasswordValidation, Never> {
        $password
            .removeDuplicates()
            .debounce(for: 0.5, scheduler: RunLoop.main)
            .map { password in
                if password.isEmpty {
                    return .empty
                } else {
                    return passwordStrengthChecker(forPassword: password)
                }
            }
            .eraseToAnyPublisher()
    }
}

// MARK: - Inner Types

extension LoginViewModel {
    enum State {
        case initial
        case submitable
        case submitting
        case signedIn(OauthToken)
        case error(HttpError)
    }

    enum Event {
        case onStart
        case login
        case onSubmitable
        case onSignedIn(OauthToken)
        case onFailedToLogin(HttpError)
    }
}

// MARK: - State Machine

extension LoginViewModel {
    
    private func handleError(error: HttpError) {
        switch error {
        case let .errors(apiError):
            if let errors = apiError.errors {
                for error in errors {
                    if error.key == "invalid_resource_owner_password" {
                        passwordError = error.message
                    }
                }
            }

        default:
            break
        }
    }
    
    func reduce(_ state: LoginViewModel.State, _ event: LoginViewModel.Event) -> LoginViewModel.State {

        switch state {
        case .initial:
            switch event {
            case .onSubmitable:
                return .submitable

            default:
                return state
            }
        case .submitting:
            switch event {
            case let .onFailedToLogin(error):
                handleError(error: error)
                return .error(error)
            case let .onSignedIn(token):
                return .signedIn(token)
            default:
                return state
            }
        case let .signedIn(token):
            return state
        case let .error(error):
            handleError(error: error)
            return state
        case .submitable:
            switch event {
            case .login:
                return .submitting
                
            default:
                return state
            }
        }
    }

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

            return self.authRepository
                .login(phoneNumber: self.phoneNumber, password: self.password)
                .map(Event.onSignedIn)
                .catch { Just(Event.onFailedToLogin($0)) }
                .eraseToAnyPublisher()
        }
    }

    static func userInput(input: AnyPublisher<Event, Never>) -> Feedback<State, Event> {
        Feedback { _ in input }
    }
}

@StateObject for ViewModels instead of @ObservedObject

I think to be safe we should use @StateObject property wrapper instead of @ObservedObject for the ViewModels.

What I observed in my app when adopting this pattern is, that in some cases SwiftUI completely reinstantiates the whole View hierarchy. This happened in my app e.g. after dismissing a modally presented view again. With @ObservedObjectall viewModel states would be reset to .idle again. For some reason onAppear however is not called again which leaves us forever in the idle state. Using @StateObject will preserve the ViewModel state throughout these reinstantiation cycles

What does this userInput() do?

Hi Vadim and all experts,

Thank you very much for your great tutorial. I have a question here is, the userInput() feedback seems just pass through the input without doing anything. If this is the case, why do we still reed it?

Also another question is, for multiple feedbacks, are they processed in parallel or sequential? Or as for declarative principle, we should not care about the order and they should be independent?

Thank you for your answer, and take care in this challenging time.

Cheers,
Ming

Parallel states

Hi, I read the article while programming in the last few days. It was very useful to better understand Combine + SwiftUI in the MVVM pattern. And also a very good chance to merge several concepts that I studied in the past but never realised I could combine effectively (like FSM and reactive programming).

My question is, at the beginning of the article you talk about a user registration view and since I am trying to do something similar I was wondering how could you manage parallel states...

I mean while in idle, waiting for a valid user input, the application should continuously validate the input (eg: no empty username, strong pw, correct retype of pw). I added some @published variables in ViewModel for username and password(s) and valid (to enable button) but the change in view is still managed by the single state and those are computed independently from it. Is there a better way to manage a state composed by more variables?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.