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))
}
}
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
}