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)
:
- calls the RemoteAPI to make the profile change in the server
- 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.