SwiftUI MVVM - 在不直接修改核心数据的情况下处理编辑视图中的临时更改

问题描述 投票:0回答:1

我正在开发一个遵循 MVVM 模式的 SwiftUI 应用程序。我有一个

ProfileDataManager
,其中包含用户配置文件的核心数据模型。这是我正在使用的结构:

用户视图模型和用户视图

UserViewModel
使用
ProfileDataManager
来获取并提供用户的个人资料数据:

class UserViewModel: ObservableObject {
    @Published var userProfile: UserProfile?
    private var dataManager: ProfileDataManager

    init(dataManager: ProfileDataManager = .shared) {
        self.dataManager = dataManager
        dataManager.$userProfile
            .assign(to: &$userProfile)
    }
}

我的

UserView
可以访问所有个人资料数据:

struct UserView: View {
    @StateObject private var viewModel = UserViewModel()
    @State private var showEditProfile = false

    var body: some View {
        // ...
        Button("Edit Profile") {
            showEditProfile = true
        }
        .sheet(isPresented: $showEditProfile) {
            if let userProfile = viewModel.userProfile {
                EditView(userProfile: userProfile)
            }
        }
    }
}

我将核心数据模型

UserProfile
传递给
EditView

编辑视图和编辑视图模型

EditView
允许用户编辑他们的个人资料。

struct EditView: View {
    @Environment(\.presentationMode) var presentationMode
    @StateObject private var viewModel: EditViewModel
    @State private var showImagePicker = false
    @State private var showGenderModal = false

    init(userProfile: UserProfile) {
        _viewModel = StateObject(wrappedValue: EditViewModel(userProfile: userProfile))
    }

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Avatar")) {
                    AvatarSection(avatarData: Binding(
                        get: { viewModel.userProfile.avatarData },
                        set: { viewModel.userProfile.avatarData = $0 }
                    )) {
                        showImagePicker = true
                    }
                }

                Section(header: Text("Personal Info")) {
                    AttributeButton(title: "Gender", value: viewModel.userProfile.gender) {
                        showGenderModal = true
                    }
                    .sheet(isPresented: $showGenderModal) {
                        GenderModalView(gender: $viewModel.userProfile.gender)
                    }
                }
            }
            .navigationBarTitle("Edit Profile", displayMode: .inline)
            .navigationBarItems(
                leading: Button("Cancel") {
                    viewModel.cancelChanges()
                    presentationMode.wrappedValue.dismiss()
                },
                trailing: Button("Save") {
                    viewModel.saveChanges()
                    presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

这是我处理保存和取消功能的

EditViewModel

class EditViewModel: ObservableObject {
    @Published var userProfile: UserProfile
    private var dataManager: ProfileDataManager

    init(userProfile: UserProfile, dataManager: ProfileDataManager = .shared) {
        self.userProfile = userProfile
        self.dataManager = dataManager
    }

    func saveChanges() {
        dataManager.saveContext()
    }

    func cancelChanges() {
        dataManager.rollback()
    }
}

问题

目前,在

EditView
中所做的更改会立即反映在
UserProfile
中,甚至在单击“保存”之前也是如此。当然,如果用户点击“取消”,它就会被丢弃。但属性仍然绑定到
UserProfile
。这种行为是违反直觉的,因为编辑页面不应该有权访问它。我希望更改仅在保存后应用。取消应该关闭视图而不修改
UserProfile
。我这样说是因为取消而不修改
EditView
中的任何内容可能会影响
UserView
数据!

我尝试将

UserProfile
的副本传递给
EditView
,但这导致了严重的内存和 CPU 使用问题。我通过对
NSManagedObject
进行扩展clone() 来做到这一点。将
UserProfile
克隆到
UserView
中,并将克隆的
UserProfile
传递给
EditView

NS托管对象:

extension NSManagedObject {
    func clone() -> NSManagedObject? {
        guard let context = self.managedObjectContext else { return nil }
        let entityName = self.entity.name ?? ""
        guard let clonedObject = NSEntityDescription.insertNewObject(forEntityName: entityName, into: context) as? Self else { return nil }

        for attribute in self.entity.attributesByName {
            clonedObject.setValue(self.value(forKey: attribute.key), forKey: attribute.key)
        }

        return clonedObject
    }
}

用户视图模型:

func cloneUserProfile() -> UserProfile? {
    return userProfile?.clone() as? UserProfile
}

func saveProfile(_ modifiedProfile: UserProfile) {
    guard let userProfile = userProfile else { return }
    userProfile.setValuesForKeys(modifiedProfile.dictionaryWithValues(forKeys: Array(modifiedProfile.entity.attributesByName.keys)))
    dataManager.saveContext()
}

用户查看:

.sheet(isPresented: $showEditProfile) {
    if let clonedProfile = viewModel.cloneUserProfile() {
        EditView(userProfile: clonedProfile) { modifiedProfile in
            viewModel.saveProfile(modifiedProfile)
        }
    }
}

编辑视图:

trailing: Button("Save") {
    onSave(viewModel.userProfile)
    presentationMode.wrappedValue.dismiss()
}

但是,95% 的 CPU 使用率和 40GB 内存使用率...

模态视图的临时数据

这是我如何在模态视图中使用临时数据的示例,例如性别选择器:

struct GenderModalView: View {
    @Environment(\.presentationMode) var presentationMode
    @State private var tempGender: String
    @Binding var gender: String?

    init(gender: Binding<String?>) {
        self._gender = gender
        self._tempGender = State(initialValue: gender.wrappedValue ?? "Other")
    }

    var body: some View {
        NavigationView {
            Form {
                Picker("Gender", selection: $tempGender) {
                    Text("Male").tag("Male")
                    Text("Female").tag("Female")
                    Text("Other").tag("Other")
                }
                .pickerStyle(.segmented)

                Section {
                    Button("Remove Gender") {
                        gender = nil
                        presentationMode.wrappedValue.dismiss()
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationBarTitle("Gender", displayMode: .inline)
            .navigationBarItems(
                leading: Button("Cancel") {
                    presentationMode.wrappedValue.dismiss()
                },
                trailing: Button("Save") {
                    gender = tempGender
                    presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

问题

  1. EditView
    中应该如何处理数据以避免模型过早改变?
  2. 我在这里遵循 MVVM 的最佳实践吗?
  3. 我做了什么蠢事吗?

任何有关数据管理、责任分离或性能优化的建议将不胜感激!我正在努力遵循最佳实践。

我的主要问题是:如何将

UserProfile
数据传递到编辑(编辑更改不直接影响存储)进行操作,以及当我“保存”时,将新数据发送回以在
中更新它UserProfile
以简单有效的方式同时遵循最佳实践?

ios swift iphone xcode swiftui
1个回答
0
投票

展示如何使用局部变量的编辑视图并在点击保存按钮时调用 saveAction 来更新用户配置文件的示例。

import SwiftUI

struct UserProfile {
    var pseudo: String
    var gender: String?
    
    static let defaultUser = UserProfile(pseudo: "pseudo", gender: "Male")
}

class UserViewModel: ObservableObject {
    @Published var userProfile: UserProfile
    
    init(userProfile: UserProfile) {
        self.userProfile = userProfile
    }
    
    // The view will call this action to save the user profile in the database
    func saveUserProfile(pseudo: String, gender: String?) {
        userProfile.pseudo = pseudo
        userProfile.gender = gender
        // Save the user profile in database
        // ...
    }
}


struct UserView: View {
    @StateObject private var viewModel: UserViewModel
    @State private var showEditProfile = false
    
    init(userProfile: UserProfile) {
        self._viewModel = StateObject(wrappedValue: UserViewModel(userProfile: userProfile))
        self.showEditProfile = showEditProfile
    }
    
    var body: some View {
        // ...
        Text("User pseudo = \(viewModel.userProfile.pseudo)")
        Text("User gender = \(viewModel.userProfile.gender ?? "nogender")")
        Button("Edit Profile") {
            showEditProfile = true
        }
        .sheet(isPresented: $showEditProfile) {
            EditView(userProfile: viewModel.userProfile, saveAction: viewModel.saveUserProfile(pseudo:gender:))
        }
    }
}

struct EditView: View {
    @Environment(\.presentationMode) var presentationMode
    @State private var showImagePicker = false
    @State private var showGenderModal = false
    
    // Following properties are modified by the view
    @State var pseudo: String = ""
    @State var gender: String? = nil
    
    // the save action will really save the modification
    var saveAction: (String, String?)->Void
    
    init(userProfile: UserProfile, saveAction: @escaping (String, String?)->Void) {
        pseudo = userProfile.pseudo
        gender = userProfile.gender
        self.saveAction = saveAction
    }
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Personal Info")) {
                    TextField("Pseudo", text: $pseudo)
                    Button("Gender \(gender ?? "no gender")") {
                        showGenderModal = true
                    }
                    .sheet(isPresented: $showGenderModal) {
                        GenderModalView(gender: $gender)
                    }
                }
            }
            .navigationBarTitle("Edit Profile", displayMode: .inline)
            .navigationBarItems(
                leading: Button("Cancel") {
                    presentationMode.wrappedValue.dismiss()
                },
                trailing: Button("Save") {
                    // This where you call the saveAction that will update the user profile in the calling view
                    saveAction(pseudo, gender)
                    presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

struct GenderModalView: View {
    @Environment(\.presentationMode) var presentationMode
    @State var tempGender: String? = nil
    @Binding var gender: String?
    
    var body: some View {
        NavigationView {
            Form {
                Picker("Gender", selection: $tempGender) {
                    Text("Male").tag("Male")
                    Text("Female").tag("Female")
                    Text("Other").tag("Other")
                }
                .pickerStyle(.segmented)
                
                Section {
                    Button("Remove Gender") {
                        gender = nil
                        presentationMode.wrappedValue.dismiss()
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationBarTitle("Gender", displayMode: .inline)
            .navigationBarItems(
                leading: Button("Cancel") {
                    presentationMode.wrappedValue.dismiss()
                },
                trailing: Button("Save") {
                    gender = tempGender
                    presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

#Preview("User view") {
    UserView(userProfile: .defaultUser)
}

注意:我没有放更新数据库代码,而是写在应该放的地方

© www.soinside.com 2019 - 2024. All rights reserved.