Search code examples
iosswiftswiftuirealm

Return Duplicate record with Realm


I am using Relam to store the data locally and working fine but when I try to add the new record with navigation link it returns the duplicate record as well . Another problem is when I click the record , I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work .

Here is the Model .

import SwiftUI
import RealmSwift

struct Task: Identifiable {
    var id: String
    var title: String
    var completed: Bool = false
    var completedAt: Date = Date()

    init(taskObject: TaskObject) {
            self.id = taskObject.id.stringValue
            self.title = taskObject.title
            self.completed = taskObject.completed
            self.completedAt = taskObject.completedAt
        }
}

Here is the Persisted Model...

import Foundation
import RealmSwift

class TaskObject: Object {
    @Persisted(primaryKey: true) var id: ObjectId
    @Persisted var title: String
    @Persisted var completed: Bool = false
    @Persisted var completedAt: Date = Date()
}

Here is the View Model ..

/

/ 2
final class TaskViewModel: ObservableObject {
    // 3
    @Published var tasks: [Task] = []
    // 4
    private var token: NotificationToken?

    init() {
        setupObserver()
    }

    deinit {
        token?.invalidate()
    }
    // 5
    private func setupObserver() {
        do {
            let realm = try Realm()
            let results = realm.objects(TaskObject.self)

            token = results.observe({ [weak self] changes in
                // 6
                self?.tasks = results.map(Task.init)
                    .sorted(by: { $0.completedAt > $1.completedAt })
                    .sorted(by: { !$0.completed && $1.completed })
            })
        } catch let error {
            print(error.localizedDescription)
        }
    }
    // 7
    func addTask(title: String) {
        let taskObject = TaskObject(value: [
            "title": title,
            "completed": false
        ])
        do {
            let realm = try Realm()
            try realm.write {
                realm.add(taskObject)
            }
        } catch let error {
            print(error.localizedDescription)
        }
    }
    // 8
    func markComplete(id: String, completed: Bool) {
        do {
            let realm = try Realm()
            let objectId = try ObjectId(string: id)
            let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
            try realm.write {
                task?.completed = completed
                task?.completedAt = Date()
            }
        } catch let error {
            print(error.localizedDescription)
        }
    }

    func remove(id: String) {
        do {
            let realm = try Realm()
            let objectId = try ObjectId(string: id)
            if let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId) {
                try realm.write {
                    realm.delete(task)
                }
            }
        } catch let error {
            print(error.localizedDescription)
        }
    }

    func updateTitle(id: String, newTitle: String) {
        do {
            let realm = try Realm()
            let objectId = try ObjectId(string: id)
            let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
            try realm.write {
                task?.title = newTitle
            }
        } catch let error {
            print(error.localizedDescription)
        }
    }
}

Here is the code for Content view ...

struct ContentView: View {
    var body: some View {
            NavigationView {
                VStack {
                    AddTaskView()
                    TaskListView()
                }
                .navigationTitle("Todo")
                .navigationBarTitleDisplayMode(.automatic)
            }
        }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here is the code for Add task view ..

import SwiftUI

struct AddTaskView: View {
    @State private var taskTitle: String = ""
    @EnvironmentObject private var viewModel: TaskViewModel

    var body: some View {
        HStack(spacing: 12) {
            TextField("Enter New Task..", text: $taskTitle)
            Button(action: handleSubmit) {
                Image(systemName: "plus")
            }
        }
        .padding(20)
    }

    private func handleSubmit() {
        viewModel.addTask(title: taskTitle)
        taskTitle = ""
    }
}

Here is the Task list View ..

struct TaskListView: View {
    @EnvironmentObject private var viewModel: TaskViewModel
    var body: some View {

        ScrollView {
            
            LazyVStack (alignment: .leading) {
                ForEach(viewModel.tasks, id: \.id) { task in
                    TaskRowView(task: task)
                    Divider().padding(.leading, 20)
                    NavigationLink (destination: TaskView(task: task)) {
                        TaskRowView(task: task)

                }.animation(.default)
                }
            }
        }
    }
}

Here is the code for Row View ..

struct TaskRowView: View {
    let task: Task
    // 1
    @EnvironmentObject private var viewModel: TaskViewModel
    var body: some View {
        HStack(spacing: 12) {
            Button(action: {
                // 2
                viewModel.markComplete(id: task.id, completed: !task.completed)
            }) {
                Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundColor(task.completed ? Color.green : Color.gray)
            }
            VStack(alignment: .leading, spacing: 8) {
                Text(task.title)
                    .foregroundColor(.black)
                if !task.completedAt.formatted().isEmpty {
                    Text(task.completedAt.formatted())
                        .foregroundColor(.gray)
                        .font(.caption)
                }
            }
            Spacer()

        }
        .padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20))
    }
}

Here is the screenshot .. enter image description here


Solution

  • Let's troubleshoot the discrepancies one by one.

    According to your code, each row in the list represents a Task. But, there are two models Task and TaskObject (persistable model) for that.

    struct Task: Identifiable {
        var id: String
        var title: String
        var completed: Bool = false
        var completedAt: Date = Date()
    
        init(taskObject: TaskObject) {
                self.id = taskObject.id.stringValue
                self.title = taskObject.title
                self.completed = taskObject.completed
                self.completedAt = taskObject.completedAt
            }
    }
    
    class TaskObject: Object {
        @Persisted(primaryKey: true) var id: ObjectId
        @Persisted var title: String
        @Persisted var completed: Bool = false
        @Persisted var completedAt: Date = Date()
    }
    

    Instead of using two models, convert them into one.

    class TaskObject: Object, Identifiable {
        @Persisted(primaryKey: true) var id: ObjectId
        @Persisted var title: String
        @Persisted var completed: Bool = false
        @Persisted var completedAt: Date = Date()
        
        var idStr: String {
            id.stringValue
        }
    }
    

    Therefore, there's no need for mapping to another object after retrieving it from the database. The updated setupObserver function should be...

        private func setupObserver() {
            do {
                let realm = try Realm()
                let results = realm.objects(TaskObject.self)
    
                token = results.observe({ [weak self] changes in
                    // 6
                    self?.tasks = results
                        .sorted(by: { $0.completedAt > $1.completedAt })
                        .sorted(by: { !$0.completed && $1.completed })
                })
            } catch let error {
                print(error.localizedDescription)
            }
        }
    

    Let's address your questions now.

    When I try to add the new record with navigation link it returns the duplicate record as well

    It does not produce duplicate data. Instead, the same data is displayed twice in the view. To correct this, remove one of the two instances of TaskRowView(task: task).

    struct TaskListView: View {
        @EnvironmentObject private var viewModel: TaskViewModel
        var body: some View {
            ScrollView {
                LazyVStack (alignment: .leading) {
                    ForEach(viewModel.tasks, id: \.id) { task in
                        TaskRowView(task: task) // first row 📌
                        Divider().padding(.leading, 20)
                        NavigationLink (destination: TaskView(task: task)) {
                            TaskRowView(task: task) // second row 📌
    
                    }.animation(.default)
                    }
                }
            }
        }
    }
    

    Next question,

    I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work.

    Again, the second one changes navigation, and the first one does not, because this is exactly what is written in the code.

    TaskRowView(task: task) // Why would it change navigation?
    Divider().padding(.leading, 20)
    NavigationLink (destination: TaskView(task: task)) {
         TaskRowView(task: task) // changing navigation
    }