Group Group Group Group Group Group Group Group Group

REDUX - Edit User Profile

In the Koober app, the ProfileViewControllerState takes a UserProfile struct which also lives on the UserSession class. Lets say in the ProfileViewController, we allowed a user to update their information. How would that be handled in the example project?

Would the ProfileUserInteraction dispatch actions which the ProfileReducer would update its UserProfile and then on dismissal, it dispatches an action with the UserProfile that someone else uses to update the UserSession?

Or should the actions dispatched from the ProfileUserInteraction go to a higher up reducer that owns the UserSession, like the SignedInReducer and that will update the UserSession as well as the child UserProfile of the ProfileViewControllerState

@rcach Can you please help with this when you get a chance? Thank you - much appreciated! :]

Hi @kaptain_k1rk, the last couple of days I’ve been tweaking the Redux example project to add profile editing. I’ll share the code once it’s cleaned up a bit. In the meantime I can share what I did to give you an idea…

The first step is to make the UserProfile properties mutable. UserProfile is a struct so there’s no big downside to making these properties variable:

public struct UserProfile: Equatable, Codable {

  // MARK: - Properties, from let to var:
  public var name: String
  public var email: String
  public var mobileNumber: String
  public var avatar: URL

  // MARK: - Methods
  public init(name: String, email: String, mobileNumber: String, avatar: URL) {
    self.name = name
    self.email = email
    self.mobileNumber = mobileNumber
    self.avatar = avatar
  }
}

Next, add an update(profile:) method to the ProfileUserInteractions protocol:

public protocol ProfileUserInteractions {
  
  func signOut()
  func dismissProfile()
  func finishedPresenting(_ errorMessage: ErrorMessage)
  
  // New method here:
  func update(profile: UserProfile)
}

The method will need to dispatch an action so we also need a new Action type in ProfileActions.swift:

struct ProfileActions {

  struct SignOutFailed: Action {

    // MARK: - Properties
    let errorMessage: ErrorMessage
  }

  struct FinishedPresentingError: Action {

    // MARK: - Properties
    let errorMessage: ErrorMessage
  }

  // New Action type here:
  struct EditedProfile: Action {

    let newProfile: UserProfile
  }
}

Then in the ReduxProfileUserInteractions implementation, update(:profile):

  1. calls the RemoteAPI to make the profile change in the server
  2. on successful API call, dispatches a new EditedProfile action: ProfileActions.EditedProfile(newProfile: newProfile)

To process this new edited profile Redux action, the next step is to add some logic to the ProfileReducer:

extension Reducers {

  static func profileReducer(action: Action, state: ProfileViewControllerState) -> ProfileViewControllerState {
    var state = state

    switch action {
    case let action as ProfileActions.SignOutFailed:
      state.errorsToPresent.insert(action.errorMessage)
    case let action as ProfileActions.FinishedPresentingError:
      state.errorsToPresent.remove(action.errorMessage)
      
    // New case for new action:
    case let action as ProfileActions.EditedProfile:
      state.profile = action.newProfile
    default:
      break
    }

    return state
  }
}

This gets the state change in place. The next part is to update the profile table view to be able to observe and update itself when the user’s profile changes.

The userProfile property on ProfileTableView needs to be mutable:

class ProfileTableView: NiblessTableView {

  // MARK: - Properties
  var userProfile: UserProfile // From let to var
  let userInteractions: ProfileUserInteractions
  
  ...
}

Next, ProfileContentRootView's user profile property is already mutable and will already get changed when the Redux subscription fires in the view controller with a new state. The only thing missing is changing the userProfile property on the table view. This code is ugly and I’ll clean this up in the example I’ll share soon. This change is in ProfileContentRootView:

  func presentTableView(_ userProfle: UserProfile) {
    guard self.tableView == nil else {
      // update profile on table view here:
      (self.tableView! as! ProfileTableView).userProfile = userProfle 
      return
    }

    let newTableView = makeTableView(userProfile: userProfle,
                                  userInteractions: userInteractions)
    addSubview(newTableView)
    self.tableView = newTableView
  }

So far so good, this matches how other parts of Koober work like the SignUp flow. This will get the profile changed and updated in the table view. But here’s where I ran into issues. In Koober the UserSession is persisted into the keychain with the profile. Up to this point the profile table updates but the changes to the profile are not persisted.

To get this to work I needed to rethink value vs reference types of UserSession and RemoteUserSession. In the book’s copy of the code, UserSession is a reference type. We did this so that the auth token could be replaced with a token refresh without mutating the UserSession. But this causes problems because Redux cannot detect when the profile changes, when we change the profile on the UserSession class object, Redux will get the same instance before and after state change and so the same UserSession reference is holding on to the same new profile value and therefore no subscription change event fires. UserSession needs to be a value type and RemoteUserSession can be a reference type to allow for token refreshes.

So first, UserSession needs to be a struct instead of a class, adding a method for creating a new session value with a new profile:

// struct instead of class:
public struct UserSession: Codable, Equatable {

  // MARK: - Properties
  public let profile: UserProfile
  public let remoteSession: RemoteUserSession

  // MARK: - Methods
  public init(profile: UserProfile, remoteSession: RemoteUserSession) {
    self.profile = profile
    self.remoteSession = remoteSession
  }

  // New method here for creating a new session value with same reference to remoteSession:
  public func make(withNewprofile newProfile: UserProfile) -> UserSession {
    return UserSession(profile: newProfile, remoteSession: self.remoteSession)
  }
}

Then update RemoteUserSession to a class:

// class instead of struct:
public class RemoteUserSession: Codable, Equatable {

  // MARK: - Properties
  // From let to var to be able to change this value when token is refreshed:
  var token: AuthToken

  // MARK: - Methods
  public init(token: AuthToken) {
    self.token = token
  }

  // No longer struct, so we don't get auto-synthesized equatable:
  public static func ==(lhs: RemoteUserSession, rhs: RemoteUserSession) -> Bool {
    return lhs.token == rhs.token
  }
}

Those last couple of changes make it so that ReSwift can detect the user profile change. The very last step is to replace the user session value in the state store when the profile changes, for now I’ve done this in the AppRunningReducer:

extension Reducers {

  static func appRunningReducer(action: Action, state: AppRunningState?) -> AppRunningState {
    var state = state ?? .onboarding(.welcoming)

    switch state {
    case let .onboarding(onboardingState):
      state = .onboarding(Reducers.onboardingReducer(action: action,
                                                     state: onboardingState))
    case .signedIn(let signedInViewControllerState, var userSession): // user session needs to be var here

      // New switch here to detect edited profile action, and swap the user session:
      switch action {
      case let action as ProfileActions.EditedProfile:
        userSession = userSession.make(withNewprofile: action.newProfile)
      default:
        break
      }


      state = .signedIn(Reducers.signedInReducer(action: action, state: signedInViewControllerState),
                        userSession)


    }

    switch action {
    case let action as SignInActions.SignedIn:
      let initialSignedInViewControllerState = SignedInViewControllerState(userSession: action.userSession,
                                                                           newRideState: .gettingUsersLocation(GettingUsersLocationViewControllerState(errorsToPresent: [])),
                                                                           viewingProfile: false,
                                                                           profileViewControllerState: ProfileViewControllerState(profile: action.userSession.profile, errorsToPresent: []))
      state = .signedIn(Reducers.signedInReducer(action: action, state: initialSignedInViewControllerState),
                        action.userSession)
    case _ as AppRunningActions.SignOut:
      state = .onboarding(.welcoming)
    default:
      break
    }

    return state
  }
}

Now the state persister will get a change event with the new user profile wrapped in a new user session value.

Let me know if there’s any part of this that I could expand on. I know it’s a bit rough, but I’ll share a full copy of Koober with this built in and will update this post with a cleaner explanation of everything. Hope this helps, cheers.

Thanks for that. I think that makes total sense, and being able to handle 1 action in multiple places was something I was not sure if it was an anti pattern or not.

One pattern I’ve been applying as I’ve been working with this is splitting up my state into 3 sections: Navigation State, UI State, and Domain state. So something like user session would live in domain state, and the state of the profile UI would live down in UI State.

Then whenever I create the state observable for a particular view controller I pluck off the pieces of state it needs to derive the final result from the Domain State and UI State. In this case the key paths might be like domainState.userSession and uiState.profileUIState. This seems to help with normalizing my state and overall organization of my whole app state.

I am a little concerned with the state updates firing for all the observables since its harder for them to be .outOfScope since the navigation state and ui state are not coupled like in the Koober example, but I think in my state getters I could check the navigation state and make sure it matches the piece of state I’m trying to select.

@rcach Can you please help with this when you get a chance? Thank you - much appreciated! :]