I'm writing a simple password manager as a learning exercise to get familiar with SwiftUI. So far so good, however I'm running into an issue where my View
is not being updated as expected after editing a value. There are 2 View
s that I'm dealing with right now: one for displaying a secret, and another for editing its properties. I'm using the MVVM pattern for my underlying data model, with Core Data/Combine being used for accessing the datastore. My "display" View Model looks like this:
class SecretDisplayViewModel: BaseViewModel, ObservableObject {
private let secretIdentifier: String
@Published var secret: Secret? = nil
private var cancellables = [AnyCancellable]()
init(managedObjectContext: NSManagedObjectContext, secretIdentifier: String) {
self.secretIdentifier = secretIdentifier
super.init(managedObjectContext: managedObjectContext)
}
init(managedObjectContext: NSManagedObjectContext, secret: Secret) {
self.secretIdentifier = secret.id
self.secret = secret
super.init(managedObjectContext: managedObjectContext)
}
func loadSecret() {
let _ = secretsService.loadSecret(secretIdentifier: self.secretIdentifier)
.map({ (entities: [SecretEntity]) -> [Secret] in
entities.compactMap { $0.toSecret() }
})
.replaceError(with: []) // FIXME: How should I handle errors?
.sink { completion in
if case .failure(let error) = completion {
// TODO: Handle error
print("ERROR: \(error)")
}
} receiveValue: { [weak self] secrets in
guard let strongSelf = self else {
return
}
if secrets.count > 0 {
strongSelf.secret = secrets[0]
print("strongSelf.secret=\(String(describing: strongSelf.secret))")
}
}.store(in: &cancellables)
}
}
...and the display View
is set to call loadSecret()
through onAppear
like this:
struct SecretDisplayView: View {
<snip>
@ObservedObject var viewModel: SecretDisplayViewModel
@State private var isEditing: Bool = false
var body: some View {
buildViewForSecret(secret: viewModel.secret)
.listStyle(GroupedListStyle())
.navigationTitle("Secret")
.onAppear {
if isPreview {
// Do nothing
} else {
self.viewModel.loadSecret()
}
}
.toolbar {
Button("Edit", action: { self.isEditing.toggle() })
}
.sheet(isPresented: $isEditing, onDismiss: {
// FIXME: Secret not refreshed after dismiss
self.viewModel.loadSecret()
}, content: {
if let secret = self.viewModel.secret {
SecretCreateEditView(editViewModel: SecretEditViewModel(managedObjectContext: self.viewContext,
secret: secret))
} else {
EmptyView()
}
})
}
@ViewBuilder
private func buildViewForSecret(secret: Secret?) -> some View {
List {
if let secretImpl = secret {
SecretDisplayHeaderView(name: secretImpl.name, category: secretImpl.category.title)
} else {
EmptyView()
}
if let secretImpl = secret as? LoginSecret {
LoginSecretDisplayForm(secret: secretImpl)
} else if let secretImpl = secret as? BankAccountSecret {
BankAccountSecretDisplayForm(secret: secretImpl)
} else if let secretImpl = secret as? CreditCardSecret {
CreditCardSecretDisplayForm(secret: secretImpl)
} else if let secretImpl = secret as? WifiNetworkSecret {
WifiNetworkSecretDisplayForm(secret: secretImpl)
} else if let secretImpl = secret as? RewardProgramSecret {
RewardProgramSecretDisplayForm(secret: secretImpl)
} else {
// TODO: Display a loading view or something
EmptyView()
}
if let notes = secret?.notes {
Section(header: Text("Notes")) {
TextEditor(text: Binding.constant(notes))
}
.isHidden(notes.isEmpty)
}
// TODO: Secret actions (favorite/delete/share/etc.)
}
}
<snip>
}
This works just fine, and my secret is displayed as expected. As you see, the edit View
is presented as a sheet when the user hits the Edit button. The edit view model looks like this:
class SecretEditViewModel: SecretCreateViewModel {
@Published var secret: Secret
init(managedObjectContext viewContext: NSManagedObjectContext, secret: Secret) {
self.secret = secret
super.init(managedObjectContext: viewContext)
self.name = secret.name
self.notes = secret.notes
<snip>
}
func updateSecret() {
let updatedSecret = self.buildSecret(secretCategory: secret.category,
secretIdentifier: secret.id,
createdDate: secret.createdDate)
secretsService.updateSecret(secret: updatedSecret)
}
}
...and my edit View
looks like this:
struct SecretCreateEditView: View {
<snip>
@ObservedObject var viewModel: SecretCreateViewModel
@State var secretCategory: SecretCategory?
private let editMode: Bool
init(viewModel: SecretCreateViewModel) {
self.viewModel = viewModel
self.editMode = false
}
init(viewModel: SecretCreateViewModel, secretCategory: SecretCategory) {
self.viewModel = viewModel
_secretCategory = State(initialValue: secretCategory)
self.editMode = false
self.viewModel.secretCategory = secretCategory
}
init(editViewModel: SecretEditViewModel) {
self.viewModel = editViewModel
_secretCategory = State(initialValue: editViewModel.secret.category)
self.editMode = true
}
var body: some View {
NavigationView {
List {
buildViewForSecretCategory(secretCategory: self.secretCategory)
Section(header: Text("Notes")) {
TextEditor(text: $viewModel.notes)
.lineLimit(10)
}
.isHidden(secretCategory == nil)
}
.listStyle(GroupedListStyle())
.navigationTitle("Edit Secret")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: saveSecret) {
Text(self.editMode ? "Done" : "Save")
}
.disabled(self.secretCategory == nil || self.viewModel.name.isEmpty)
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - Private methods
private func saveSecret() {
if let model = self.viewModel as? SecretEditViewModel {
model.updateSecret()
self.presentationMode.wrappedValue.dismiss()
} else {
if let _ = self.secretCategory {
self.viewModel.saveSecret()
self.presentationMode.wrappedValue.dismiss()
} else {
// TODO: Handle error
}
}
}
<snip>
}
As you can see in the 2 View
s, when my edit View is dismissed the display View
will call self.viewModel.loadSecret()
to refresh the View Model with the value from Core Data. If I set a breakpoint or print out a description of the View Model I see the values being updated as expected in the underlying data model. However, my View
is not updated! For example, if I change the name of the secret or the username, the data in the View Model is shown to be correct after the edit View
is dismissed, but the display View
itself is not updated with the appropriate new values. If I go back in my NavigationView
stack to the list of secrets (code not shown because it's not necessary) and then view the secret in question again the values are updated as expected. Just not immediately following an edit, even though the data itself is updated.
Am I missing something here? I am new to @State
, @Published
, @ObservedObject
, etc. so I very well could be misunderstanding something. Any help you can give is greatly appreciated!