Sonix
Sonix

Reputation: 29

Problem with updating Realm Object using Objects DAO in Swift

Hi guys i have a specific problem with updating realm object. I have an object ToDo which have properties and ToDoDAO:

public struct ToDo: LocalStorable {
    public let id: String
    public let name: String
    public var symbol: Icon
    public let image: Data?
    public let executedDate: Date
    public let executedTime: Date
    public let remindTime: Date?
    public let reminderRepetition: Repetition
    public let tag: String
    public var subtasks: [SubToDo]
    public let isArchived: Bool
    public var isDone: Bool
    
    public init(
        id: String = UUID().uuidString,
        name: String,
        symbol: Icon,
        image: Data?,
        executedDate: Date,
        executedTime: Date,
        remindTime: Date?,
        reminderRepetition: Repetition,
        tag: String,
        subtasks: [SubToDo],
        isArchived: Bool = false,
        isDone: Bool = false
    ) {
        self.id = id
        self.name = name
        self.symbol = symbol
        self.image = image
        self.executedDate = executedDate
        self.executedTime = executedTime
        self.remindTime = remindTime
        self.reminderRepetition = reminderRepetition
        self.tag = tag
        self.subtasks = subtasks
        self.isArchived = isArchived
        self.isDone = isDone
    }
    
    public init(task: ToDo, isDone: Bool) {
        self.id = task.id
        self.name = task.name
        self.symbol = task.symbol
        self.image = task.image
        self.executedDate = task.executedDate
        self.executedTime = task.executedTime
        self.remindTime = task.remindTime
        self.reminderRepetition = task.reminderRepetition
        self.tag = task.tag
        self.subtasks = task.subtasks
        self.isArchived = task.isArchived
        self.isDone = isDone
    }
    
    public init(from dao: ToDoDAO) {
        self.id = dao.id
        self.name = dao.name
        if let iconDAO = dao.symbol {
            print("DEBUG - ToDo iconDAO:", iconDAO)
            self.symbol = Icon(from: iconDAO)
        } else {
            print("DEBUG - ToDo fail")
            self.symbol = .clipboardIcon
        }
        self.image = dao.image
        self.executedDate = dao.executedDate
        self.executedTime = dao.executedTime
        self.remindTime = dao.remindTime
        self.reminderRepetition = Repetition(rawValue: dao.reminderRepetition ?? "") ?? .daily
        self.tag = dao.tag
        self.subtasks = dao.subtasks.map { SubToDo(from: $0) }
        self.isArchived = dao.isArchived
        self.isDone = dao.isDone
    }
    
    public enum CodingKeys: String, CodingKey {
        case id
        case name
        case symbol
        case image
        case executedDate
        case executedTime
        case remindTime
        case reminderRepetition
        case tag
        case subtasks
        case isArchived
        case isDone
    }
}

This is ToDoDAO:

public final class ToDoDAO: RealmSwift.Object, LocalDAOInterface {
    @Persisted(primaryKey: true) public var id: String
    @Persisted public var name: String
    @Persisted public var symbol: IconDAO?
    @Persisted public var image: Data?
    @Persisted public var executedDate: Date
    @Persisted public var executedTime: Date
    @Persisted public var remindTime: Date?
    @Persisted public var reminderRepetition: String
    @Persisted public var tag: String
    @Persisted public var subtasks: RealmSwift.List<SubToDoDAO>
    @Persisted public var isArchived: Bool
    @Persisted public var isDone: Bool
    
    override public init() {
        super.init()
        self.name = ""
        self.symbol = IconDAO()
        self.image = Data()
        self.executedDate = Date()
        self.executedTime = Date()
        self.remindTime = nil
        self.reminderRepetition = ""
        self.tag = Tag.otherEvent.rawValue
        self.subtasks = RealmSwift.List<SubToDoDAO>()
        self.isArchived = false
        self.isDone = false
    }
    
    public init(from todo: ToDo) {
        super.init()
        self.id = todo.id
        self.name = todo.name
        self.symbol = IconDAO(from: todo.symbol)
        self.image = todo.image
        self.executedDate = todo.executedDate
        self.executedTime = todo.executedTime
        self.remindTime = todo.remindTime
        self.reminderRepetition = todo.reminderRepetition.rawValue
        self.tag = todo.tag
        self.subtasks.append(objectsIn: todo.subtasks.map { SubToDoDAO(from: $0) })
        self.isArchived = todo.isArchived
        self.isDone = todo.isDone
    }
}

and i cannot update two properies: symbol and subtasks and this is Icon model:

public struct Icon: LocalStorable {
    public let id: String
    public let symbol: Data
    public let circleColor: Color

    public init(id: String = UUID().uuidString, symbol: Data, circleColor: Color) {
        self.id = id
        self.symbol = symbol
        self.circleColor = circleColor
    }
    
    public init(from dao: IconDAO) {
        self.id = dao.id
        self.symbol = dao.symbol
        self.circleColor = Color(hex: dao.circleColor) ?? .accentColor
    }
    
    public init(icon: Icon) {
        self.id = icon.id
        self.symbol = icon.symbol
        self.circleColor = icon.circleColor
    }
    
    public enum CodingKeys: String, CodingKey {
        case id
        case symbol
        case circleColor
    }

    public static let tapIcon       = Icon(symbol: Icons.tap?.pngData() ?? Data(), circleColor: .blue.opacity(0.2))
    public static let dishIcon      = Icon(symbol: Icons.dinner?.pngData() ?? Data(), circleColor: .brown)
    public static let trayIcon      = Icon(symbol: Icons.tray?.pngData() ?? Data(), circleColor: .blue.opacity(0.2))
    public static let clipboardIcon = Icon(symbol: Icons.clipboard?.pngData() ?? Data(), circleColor: .yellow)
    public static let photoIcon     = Icon(symbol: Icons.photo?.pngData() ?? Data(), circleColor: .blue.opacity(0.1))
    public static let birthdayIcon  = Icon(symbol: Icons.birthday?.pngData() ?? Data(), circleColor: .yellow.opacity(0.5))
    public static let catIcon       = Icon(symbol: Icons.cat?.pngData() ?? Data(), circleColor: .green.opacity(0.6))
    public static let cinemaIcon    = Icon(symbol: Icons.cinema?.pngData() ?? Data(), circleColor: .red.opacity(0.5))
    public static let dentist       = Icon(symbol: Icons.dentist?.pngData() ?? Data(), circleColor: .green.opacity(0.7))
    public static let dog           = Icon(symbol: Icons.dog?.pngData() ?? Data(), circleColor: .yellow.opacity(0.7))
    public static let football      = Icon(symbol: Icons.football?.pngData() ?? Data(), circleColor: .purple.opacity(0.4))
    public static let learning      = Icon(symbol: Icons.learning?.pngData() ?? Data(), circleColor: .blue.opacity(0.4))
    public static let roadTrip      = Icon(symbol: Icons.roadTrip?.pngData() ?? Data(), circleColor: .red.opacity(0.3))
    public static let running       = Icon(symbol: Icons.running?.pngData() ?? Data(), circleColor: .red.opacity(0.4))
    public static let shopping      = Icon(symbol: Icons.shopping?.pngData() ?? Data(), circleColor: .yellow.opacity(0.5))
    public static let vacation      = Icon(symbol: Icons.vacation?.pngData() ?? Data(), circleColor: .orange.opacity(0.6))
    
    public static let allIcons: [Icon] = [
        dishIcon,
        clipboardIcon,
        birthdayIcon,
        catIcon,
        cinemaIcon,
        dentist,
        dog,
        football,
        learning,
        roadTrip,
        running,
        shopping,
        vacation
    ]
}

and IconDAO:

public final class IconDAO: RealmSwift.Object, LocalDAOInterface {
    @Persisted(primaryKey: true) public var id: String
    @Persisted public var symbol: Data
    @Persisted public var circleColor: String
    
    override public init() {
        super.init()
        self.symbol = Data()
        self.circleColor = ""
    }
    
    public init(from icon: Icon) {
        super.init()
        self.id = icon.id
        self.symbol = icon.symbol
        self.circleColor = icon.circleColor.toHex() ?? ""
    }
}

and also SubToDo:

public struct SubToDo: LocalStorable {
    public let id: String
    public var title: String
    public var isCompleted: Bool
    
    public init(
        id: String = UUID().uuidString,
        title: String,
        isCompleted: Bool = false
    ) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
    }
    
    public init(from dao: SubToDoDAO) {
        self.id = dao.id
        self.title = dao.title
        self.isCompleted = dao.isCompleted
    }
    
    public enum CodingKeys: String, CodingKey {
        case id
        case title
        case isCompleted
    }
}

and DAO:

public final class SubToDoDAO: RealmSwift.Object, LocalDAOInterface {
    @Persisted(primaryKey: true) public var id: String
    @Persisted public var title: String
    @Persisted public var isCompleted: Bool
    
    override public init() {
        super.init()
        self.title = ""
        self.isCompleted = false
    }
    
    public init(from subtodo: SubToDo) {
        super.init()
        self.id = subtodo.id
        self.title = subtodo.title
        self.isCompleted = subtodo.isCompleted
    }
}

i try to update it using func below but it didn't work, for example updating ICON: ToDo gets deafult value which is .clipboardIcon and in Realm Studio it's display nil. enter image description here Update ToDo



    func updateToDo(toDo: ToDo) async throws {
            state = .loading
            
            var updatedToDo = ToDo(
                id: toDo.id,
                name: newTaskTitle,
                symbol: newTaskSymbol,
                image: selectedTaskImage?.jpegData(compressionQuality: 0.9),
                executedDate: taskDay,
                executedTime: taskTime,
                remindTime: remindTime,
                reminderRepetition: repetition,
                tag: tag.rawValue,
                subtasks: subtasks
            )
            
            print("DEBUG - todo: ", toDo.symbol)
            print("DEBUG - updatedToDo: ", updatedToDo.symbol)
            
            let updates = compare(old: toDo, updated: updatedToDo)
            
            try await toDoManager.updateToDo(todo: toDo, data: updates)
            
            state = .loaded
        }

this is also compare func:


    public func compare(old: any LocalStorable, updated: any LocalStorable) -> [String: Any] {
        var differences: [String: Any] = [:]
    
        let oldMirror = Mirror(reflecting: old)
        let newMirror = Mirror(reflecting: updated)
    
        for (propertyName, oldValue) in oldMirror.children {
            guard let propertyName else { continue }
    
            if let newValue = newMirror.descendant(propertyName) {
                if let oldEnumValue = oldValue as? (any RawRepresentable),
                   let newEnumValue = newValue as? (any RawRepresentable),
                   "\(oldEnumValue.rawValue)" != "\(newEnumValue.rawValue)" {
                    differences[propertyName] = newEnumValue.rawValue
                } else if "\(oldValue)" != "\(newValue)" {
                    differences[propertyName] = newValue
                }
            }
        }
    
        return differences
    }

the fun fact is that when i create a ToDo everything works fine. Create ToDo

func createToDo() async throws {
        state = .loading
        
        let newToDo = ToDo(
            name: newTaskTitle,
            symbol: newTaskSymbol,
            image: selectedTaskImage?.jpegData(compressionQuality: 0.9),
            executedDate: taskDay,
            executedTime: taskTime,
            remindTime: remindTime,
            reminderRepetition: repetition,
            tag: tag.rawValue,
            subtasks: subtasks
        )
        
        try await toDoManager.createToDo(todo: newToDo)
        
        state = .loaded
    }

this is also viewModel where i have functions: View Model

@MainActor
final class AddTaskViewModel: ObservableObject {
    enum State: Equatable {
        case loading
        case loaded
        case error(String)
    }
    
    @Published private(set) var state: State = .loaded
    @Published private(set) var showDivider: Bool = true
    @Published private(set) var selectedPicker: Picker?
    @Published private(set) var selectedTaskImage: UIImage? = nil
    @Published var newTaskTitle: String = ""
    @Published var isScrolled: Bool = false
    @Published var newTaskSymbol: Icon = .clipboardIcon
    @Published var taskDay: Date = .now
    @Published var taskTime: Date = .now
    @Published var remindTime: Date?
    @Published var repetition: Repetition = .noRepeat
    @Published var tag: Tag = .all
    @Published var newSubtaskTitle: String = ""
    @Published var editSubtaskTextFieldText: String = ""
    @Published var editingSubtaskIndex: Int? = nil
    @Published var subtasks: [SubToDo] = []
    @Published var defaultScrollAnchor: UnitPoint? = .top
    @Published var taskImageSelection: PhotosPickerItem? = nil {
        didSet {
            Task {
                do {
                    try await setImage(from: taskImageSelection)
                } catch {
                    handleError(error: error)
                }
            }
        }
    }
    //TODO: add validation to newTaskTitle
   
    @Inject private var toDoManager: ToDoManagerInterface
    
    private var existingToDo: ToDo?
    
    private var isEditing: Binding<Bool?>
    
    var buttonTitle: LocalizedStringKey {
        existingToDo == nil ? "Create Task" : "Update Task"
    }
    
    var isPickerSelected: Binding<Bool> {
        Binding<Bool>(
            get: {
                self.selectedPicker != nil },
            set: {
                if !$0 {
                    self.selectedPicker = nil
                }
            }
        )
    }
    
    var selectedDate: String { taskDay.isToday ? "Today" : dateFormatter(dateFormat: .dateWithDots).string(from: taskDay) }
    
    var selectedTime: String { "Time: \(dateFormatter(dateFormat: .time).string(from: taskTime))" }
    
    var selectedReminder: String { remindTime != nil ? dateFormatter(dateFormat: .timeWithPeriods).string(from: remindTime ?? Date()) : "No reminder" }
    
    init(isEditing: Binding<Bool?> = .constant(nil)) {
        if let toDo = existingToDo {
            self.existingToDo = toDo
            self.newTaskTitle = toDo.name
            self.newTaskSymbol = toDo.symbol
            self.taskDay = toDo.executedDate
            self.taskTime = toDo.executedTime
            self.remindTime = toDo.remindTime
            self.repetition = toDo.reminderRepetition
            if let tag = Tag(rawValue: toDo.tag) {
                self.tag = tag
            }
            self.subtasks = toDo.subtasks
            self.selectedTaskImage = toDo.image.flatMap { UIImage(data: $0) }
        }
        
        self.isEditing = isEditing
    }
    
    func handleScrollActions(position: Double) {
        showDivider = position >= -80.0
        isScrolled = position < -10
    }
    
     func onPickerSelected(picker: Picker?) {
         if let index = editingSubtaskIndex, selectedPicker == .editSubtask {
             subtasks[index].title = editSubtaskTextFieldText
         }
         
         if picker == .subtask {
            createSubtask()
             print("DEBUG - created Subtask:", subtasks)
        }
        
        onShowOptionToggle()
    }
    
    private func onShowOptionToggle() {
        withAnimation(.smooth(duration: 0.1)) {
            selectedPicker = nil
        }
    }
    
    func handlePickerSelection(_ picker: Picker, subtaskIndex: Int? = nil) {
        withAnimation(.smooth(duration: 0.7)) {
            selectedPicker = picker
            
            if let index = subtaskIndex {
                editingSubtaskIndex = index
                editSubtaskTextFieldText = subtasks[index].title
            }
        }
    }
    
    typealias Picker = ToDoInterface.Picker
}

extension AddTaskViewModel {
    private func setImage(from selection: PhotosPickerItem?) async throws {
        guard let selection else { return }
        
        let data = try await selection.loadTransferable(type: Data.self)
        
        guard let data, let uiImage = UIImage(data: data) else {
            throw AppError.unableToLoadImage
        }
        
        selectedTaskImage = uiImage
    }
    
    func createToDo() async throws {
        state = .loading
        
        let newToDo = ToDo(
            name: newTaskTitle,
            symbol: newTaskSymbol,
            image: selectedTaskImage?.jpegData(compressionQuality: 0.9),
            executedDate: taskDay,
            executedTime: taskTime,
            remindTime: remindTime,
            reminderRepetition: repetition,
            tag: tag.rawValue,
            subtasks: subtasks
        )
        
        try await toDoManager.createToDo(todo: newToDo)
        
        state = .loaded
    }
    
    func handleError(error: Error) {
        withAnimation {
            if let localizedError = error as? LocalizedError {
                state = .error(localizedError.localizedDescription)
            } else {
                state = .error(AppError.unexpectedError(error.localizedDescription).errorDescription ?? error.localizedDescription)
            }
        }
    }
    
    func handleDismissErrorView() {
        withAnimation {
            state = .loaded
        }
    }
    
    private func createSubtask() {
        guard !newSubtaskTitle.isEmpty else { return }
        
        let newSubtask = SubToDo(title: newSubtaskTitle)
        
        subtasks.append(newSubtask)
        
        newSubtaskTitle = ""
    }
}

//MARK: Task Management
extension AddTaskViewModel {
    func manageToDo() async throws {
        if let existingToDo = existingToDo {
            try await updateToDo(toDo: existingToDo)
            isEditing.wrappedValue = false
        } else {
            try await createToDo()
        }
    }
    
    func updateToDo(toDo: ToDo) async throws {
        state = .loading
        
        var updatedToDo = ToDo(
            id: toDo.id,
            name: newTaskTitle,
            symbol: newTaskSymbol,
            image: selectedTaskImage?.jpegData(compressionQuality: 0.9),
            executedDate: taskDay,
            executedTime: taskTime,
            remindTime: remindTime,
            reminderRepetition: repetition,
            tag: tag.rawValue,
            subtasks: subtasks
        )
        
        print("DEBUG - todo: ", toDo.symbol)
        print("DEBUG - updatedToDo: ", updatedToDo.symbol)
        
        let updates = compare(old: toDo, updated: updatedToDo)
        
        try await toDoManager.updateToDo(todo: toDo, data: updates)
        
        state = .loaded
    }
    
    func readActiveToDo(primaryKey: String) async throws {
        self.existingToDo = try await toDoManager.readToDo(primaryKey: primaryKey)
        print("DEBUG - read todo:", existingToDo)
    }
}

If anyone have a idea why is this not working please help me... Maybe I created the objects incorrectly and that's why updating doesn't work, but the question is why adding ToDo works.

Upvotes: 0

Views: 29

Answers (0)

Related Questions