Realm Sync Not Updating SwiftUI View After Observe is Fired - ios

I am having issues with Realm, which is not causing my View to update. I have a checkbox in a List and when I click on the checkbox, I want to update the isCompleted to true. This part is working and it updates the isCompleted property in Realm Sync (MongoDB Atlas). The problem is that the view never re-renders again.
Here is code for my TaskCellView which updates the isCompleted.
struct TaskCellView: View {
var task: Task
let realmManager = RealmManager.shared
var body: some View {
let _ = print(Self._printChanges())
HStack {
Image(systemName: task.isCompleted ? "checkmark.square": "square")
.onTapGesture {
try! realmManager.realm.write {
task.isCompleted.toggle()
}
}
Text(task.title)
}
}
}
In my RealmManager I have setupObservers which fires and assigns the new tasks to the tasks property.
private func setupObservers() {
let observableTasks = realm.objects(Task.self)
notificationToken = observableTasks.observe { [weak self] _ in
DispatchQueue.main.async {
print(observableTasks)
self?.tasks = observableTasks
}
}
}
Then in my ContentView I use the following code to send the tasks to the TodoListView.
if let tasks = realmManager.tasksArray {
let _ = print("INSIDE IF")
let _ = print(tasks.count)
TodoListView(tasks: tasks)
}
I have checked and the print lines are getting printed but the body of the TodoListView is never executed.
TodoListView
struct TodoListView: View {
let tasks: [Task]
var pendingTasks: [Task] {
// tasks.where { $0.isCompleted == false }
tasks.filter { !$0.isCompleted }
}
var completedTasks: [Task] {
tasks.filter { $0.isCompleted }
}
var body: some View {
let _ = print(Self._printChanges())
List {
ForEach(Sections.allCases, id: \.self) { section in
Section {
ForEach(section == .pending ? pendingTasks: completedTasks, id: \._id) { task in
TaskCellView(task: task)
}
} header: {
Text(section.rawValue)
}
}
}.listStyle(.plain)
}
}
I know it is a lot of code and but maybe someone can see the problem. So even though the checkbox is checked and the Realm database is updated and observe is fired and the tasks are updated to reflect the isCompleted new states. The TodoListView is never re-rendered.

Related

UI won't change in real time w/ CoreData

I have a SwiftUI + CoreData simple Todo app, and everything works properly, but my updateTodo function which is supposed to handle the click on a todo and turn in from done to undone and vice versa, isn't working properly.
When I click on a todo nothing happens in the UI, but when I go a screen back and come back to the todos screen, I can see the UI change, also it does persist so when I close and open the app the change is being reflected in the app.
So my problem is that the 'isDone' property is not being toggled in the UI in real-time, and only when the view reappears it actually shows the change that has been made.
ViewModel (CoreData) :
class TodoViewModel:ObservableObject {
let container: NSPersistentContainer
#Published var categories = [CategoryTodo]()
#Published var todos = [Todo]()
init() {
container = NSPersistentContainer(name: "UniversityProject")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
//MARK: - Todos
func getTodos() {
let request = NSFetchRequest<Todo>(entityName: "Todo")
let sort = NSSortDescriptor(key: #keyPath(Todo.dateCreated), ascending: false)
request.sortDescriptors = [sort]
do {
try todos = container.viewContext.fetch(request)
} catch {
print("Error getting data. \(error)")
}
}
func addTodo(todo text: String, category categoryName:String) {
let newTodo = Todo(context: container.viewContext)
newTodo.todo = text
newTodo.category = categoryName
newTodo.id = UUID().uuidString
newTodo.isDone = false
newTodo.dateCreated = Date()
saveTodo()
}
func saveTodo() {
do {
try container.viewContext.save()
getTodos()
} catch let error {
print("Error: \(error)")
}
}
func deleteTodo(indexSet: IndexSet) {
let todoIndex = indexSet[indexSet.startIndex]
let object = todos[todoIndex]
container.viewContext.delete(object)
saveTodo()
}
func updateTodo(item: Todo) {
item.setValue(!item.isDone, forKey: "isDone")
saveTodo()
}
}
TodosView:
struct TodosView: View {
#EnvironmentObject var viewModel: TodoViewModel
let categoryName:String
var body: some View {
List {
ForEach(viewModel.todos.filter{$0.category == categoryName}) { item in
TodoItem(item: item)
.onTapGesture {
withAnimation(.linear) {
viewModel.updateTodo(item: item)
}
}
}
.onDelete(perform: viewModel.deleteTodo)
}.onAppear { viewModel.getTodos() }
.navigationBarTitle(categoryName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
EditButton()
NavigationLink(destination: AddTodoView(category: categoryName)) {
Image(systemName: "plus.circle")
.resizable()
.foregroundColor(.blue)
.frame(width: 25, height: 25)
}
}
}
}
}
}
without all relevent code, I can only guess and suggest you try something like these:
func updateTodo(item: Todo) {
if let ndx = todos.firstIndex(where: {$0.id == item.id}) {
todos[ndx].isDone = !item.isDone
saveTodo()
}
}
or
func updateTodo(item: Todo) {
objectWillChange.send()
item.setValue(!item.isDone, forKey: "isDone")
saveTodo()
}

How to use #FocusedValue with optionals?

This question is similar to this unanswered question from the Apple Developer Forums, but with a slightly different scenario:
I have a view with a #FetchRequest property of type FetchedResults<Metric>
Within the view, I display the list of those objects
I can tap on one item to select it, storing that selection in a #State var selection: Metric? = nil property.
Here's the properties I defined for my #FocusedValue:
struct FocusedMetricValue: FocusedValueKey {
typealias Value = Metric?
}
extension FocusedValues {
var metricValue: FocusedMetricValue.Value? {
get { self[FocusedMetricValue.self] }
set { self[FocusedMetricValue.self] = newValue }
}
}
Here's how I set the focusedValue from my list view:
.focusedValue(\.metricValue, selection)
And here's how I'm using the #FocusedValue on my Commands struct:
struct MacOSCommands: Commands {
#FocusedValue(\.metricValue) var metric
var body: some Commands {
CommandMenu("Metric") {
Button("Test") {
print(metric??.name ?? "-")
}
.disabled(metric == nil)
}
}
}
The code builds successfully, but when I run the app and select a Metric from the list, the app freezes. If I pause the program execution in Xcode, this is the stack trace I get:
So, how can I make #FocusedValue work in this scenario, with an optional object from a list?
I ran into the same issue. Below is a View extension and ViewModifier that present a version of focusedValue which accepts an Binding to an optional. Not sure why this wasn't included in the framework as it corresponds more naturally to a selection situation in which there can be none...
extension View{
func focusedValue<T>(_ keypath: WritableKeyPath<FocusedValues, Binding<T>?>, selection: Binding<T?>) -> some View{
self.modifier(FocusModifier(keypath: keypath, optionalBinding: selection))
}
}
struct FocusModifier<T>: ViewModifier{
var keypath: WritableKeyPath<FocusedValues, Binding<T>?>
var optionalBinding: Binding<T?>
func body(content: Content) -> some View{
Group{
if optionalBinding.wrappedValue == nil{
content
}
else if let binding = Binding(optionalBinding){
content
.focusedValue(keypath, binding)
}
else{
content
}
}
}
}
In your car usage would look like:
.focusedValue(\.metricValue, selection: $selection)
I have also found that the placement of this statement is finicky. I can only make things work when I place this on the NavigationView itself as opposed to one of its descendants (eg List).
// 1 CoreData optional managedObject in #State var selection
#State var selection: Metric?
// 2 modifiers on View who own the list with the selection
.focusedValue(\.metricValue, $selection)
.focusedValue(\.deleteMetricAction) {
if let metric = selection {
print("Delete \(metric.name ?? "unknown metric")?")
}
}
// 3
extension FocusedValues {
private struct SelectedMetricKey: FocusedValueKey {
typealias Value = Binding<Metric?>
}
private struct DeleteMetricActionKey: FocusedValueKey {
typealias Value = () -> Void
}
var metricValue: Binding<Metric?>? {
get { self[SelectedMetricKey.self] }
set { self[SelectedMetricKey.self] = newValue}
}
var deleteMetricAction: (() -> Void)? {
get { self[DeleteMetricActionKey.self] }
set { self[DeleteMetricActionKey.self] = newValue }
}
}
// 4 Use in macOS Monterey MenuCommands
struct MetricsCommands: Commands {
#FocusedValue(\.metricValue) var selectedMetric
#FocusedValue(\.deleteMetricAction) var deleteMetricAction
var body: some Commands {
CommandMenu("Type") {
Button { deleteMetricAction?() } label: { Text("Delete \(selectedMetric.name ?? "unknown")") }.disabled(selectedMetric?.wrappedValue == nil || deleteMetricAction == nil )
}
}
}
// 5 In macOS #main App struct
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.commands {
SidebarCommands()
MetricsCommands()
}
}
Use of #FocusedValues in Apple WWDC21 example
Use of FocusedValues in SwiftUI with Majid page
Use of #FocusedSceneValue with example of action value passed, in Apple documentation
For SwiftUI 3 macOS Table who support multiple selections
// 1 Properties
#Environment(\.managedObjectContext) var context
var type: Type
var fetchRequest: FetchRequest<Propriete>
var proprietes: FetchedResults<Propriete> { fetchRequest.wrappedValue }
#State private var selectedProprietes = Set<Propriete.ID>()
// 2 Init from Type managedObject who own Propriete managedObjects
// #FecthRequest required to have updates in Table (when delete for example)
init(type: Type) {
self.type = type
fetchRequest = FetchRequest<Propriete>(entity: Propriete.entity(),
sortDescriptors: [ NSSortDescriptor(key: "nom",
ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:))) ],
predicate: NSPredicate(format: "type == %#", type))
}
// 3 Table view
VStack {
Table(proprietes, selection: $selectedProprietes) {
TableColumn("Propriétés :", value: \.wrappedNom)
}
.tableStyle(BorderedTableStyle())
.focusedSceneValue(\.selectedProprietes, $selectedProprietes)
.focusedSceneValue(\.deleteProprietesAction) {
deleteProprietes(selectedProprietes)
}
}
// 4 FocusedValues
extension FocusedValues {
private struct FocusedProprietesSelectionKey: FocusedValueKey {
typealias Value = Binding<Set<Propriete.ID>>
}
var selectedProprietes: Binding<Set<Propriete.ID>>? {
get { self[FocusedProprietesSelectionKey.self] }
set { self[FocusedProprietesSelectionKey.self] = newValue }
}
}
// 5 Delete (for example) in Table View
private func deleteProprietes(_ proprietesToDelete: Set<Propriete.ID>) {
var arrayToDelete = [Propriete]()
for (index, propriete) in proprietes.enumerated() {
if proprietesToDelete.contains(propriete.id) {
let propriete = proprietes[index]
arrayToDelete.append(propriete)
}
}
if arrayToDelete.count > 0 {
print("array to delete: \(arrayToDelete)")
for item in arrayToDelete {
context.delete(item)
print("\(item.wrappedNom) deleted!")
}
try? context.save()
}
}
How to manage selection in Table

UserDefaults to store a button selection from an array

Struggling to figure out how to store the selection of a button here and hoping somebody could help. I am first making a db query to return a list of results, each item has a button that when clicked, it will always show that they clicked it. The way it is currently working, while I'm on the page, I can click to add, which works and writes to the db, and when clicked it changes to the Undo button, which also works the way I want it to, but as soon as I leave this view and come back, it reverts back to the original with the "Add" button. If a user presses Add, i would like it to persist and always show the Undo button when the user comes back to this screen. Any help would be amazing as I'm quite new to this.
Code below:
Main View
struct AdditemView: View {
let movie: Movie
#State var movieitems = [MovieitemsModel]()
#State var movieID = ""
#State var selection = ""
var body: some View {
VStack {
ForEach(movieitems.indices, id: \.self) { i in
HStack {
Text(movieitems[i].name)
Spacer()
if movieitems[i].selected == false {
Button(action: {
self.selection = movieitems[i].name
self.movieitems[i].selected = true
self.yesitem()
}) {
Text("Add")
}
} else {
HStack {
Text("Added!")
Button(action: {
self.selection = movieitems[i].name
self.movieitems[i].selected = false
self.undoitem()
}) {
Text("Undo")
}
}
}
}
Divider()
}
}
.onAppear {
self.movieID = "\(movie.id)"
fetchitems()
}
}
}
func fetchitems() {
let db = Firestore.firestore()
db.collection("movies").document("\(self.movieID)").collection("items").getDocuments { NewQuerySnapshot, error in
guard let documents = NewQuerySnapshot?.documents else {
print("No Documents")
return
}
movieitems = documents.map { (NewQueryDocumentSnapshot) -> MovieitemsModel in
let data = NewQueryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let yes = data["yes"] as? Int?
let no = data["no"] as? Int?
return MovieitemsModel(name: name, yes: yes! ?? 0, no: no! ?? 0)
}
}
}
func yesitem() {
let db = Firestore.firestore()
db.collection("movies").document(movieID).collection("items").document("\(self.selection)").updateData([AnyHashable("yes"): FieldValue.increment(Int64(1))])
}
func undoitem() {
let db = Firestore.firestore()
db.collection("movies").document(movieID).collection("items").document("\(self.selection)").updateData([AnyHashable("yes"): FieldValue.increment(Int64(-1))])
}
Items Model
struct MovieitemsModel: Identifiable {
var id: String = UUID().uuidString
var name: String
var yes: Int
var no: Int
var selected:Bool = false
}
One way of doing this is writing another function that saves the movies data back to the database and call it in each button’s action handler.
// Example button
Button(action: {
self.selection = movieitems[i].name
self.movieitems[i].selected = true
self.undoitem()
self.saveItems()
}) {
Text("Undo")
}
// The save method
func saveItems():
// Your Firebase saving code here

Pausing a Notification publisher in SwiftUI

When I call a backend service (login, value check…) I use a Notification publisher on the concerned Views to manage the update asynchronously.
I want to unsubscribe to the notifications when the view disappear, or « pause » the publisher.
I went first with the simple « assign » option from the WWDC19 Combine and related SwiftUI talks, then I looked at this great post and the onReceive modifier. However the view keeps updating with the published value even when the view is not visible.
My questions are:
Can I « pause » this publisher when the view is not visible ?
Should I really be concerned by this, does it affect resources (the backend update could trigger a big refresh on list and images display) or should I just let SwiftUI manage under the hood ?
The sample code:
Option 1: onReceive
struct ContentView: View {
#State var info:String = "???"
let provider = DataProvider() // Local for demo purpose, use another pattern
let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
.map { notification in
return notification.userInfo?["data"] as! String
}
.receive(on: RunLoop.main)
var body: some View {
TabView {
VStack {
Text("Info: \(info)")
Button(action: {
self.provider.startNotifications()
}) {
Text("Start notifications")
}
}
.onReceive(publisher) { (payload) in
self.info = payload
}
.tabItem {
Image(systemName: "1.circle")
Text("Notifications")
}
VStack {
Text("AnotherView")
}
.tabItem {
Image(systemName: "2.circle")
Text("Nothing")
}
}
}
}
Option 2: onAppear / onDisappear
struct ContentView: View {
#State var info:String = "???"
let provider = DataProvider() // Local for demo purpose, use another pattern
#State var cancel: AnyCancellable? = nil
var body: some View {
TabView {
VStack {
Text("Info: \(info)")
Button(action: {
self.provider.startNotifications()
}) {
Text("Start notifications")
}
}
.onAppear(perform: subscribeToNotifications)
.onDisappear(perform: unsubscribeToNotifications)
.tabItem {
Image(systemName: "1.circle")
Text("Notifications")
}
VStack {
Text("AnotherView")
}
.tabItem {
Image(systemName: "2.circle")
Text("Nothing")
}
}
}
private func subscribeToNotifications() {
// publisher to emit events when the default NotificationCenter broadcasts the notification
let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
.map { notification in
return notification.userInfo?["data"] as! String
}
.receive(on: RunLoop.main)
// keep reference to Cancellable, and assign String value to property
cancel = publisher.assign(to: \.info, on: self)
}
private func unsubscribeToNotifications() {
guard cancel != nil else {
return
}
cancel?.cancel()
}
}
For this test, I use a dummy service:
class DataProvider {
static let updated = Notification.Name("Updated")
var payload = "nothing"
private var running = true
func fetchSomeData() {
payload = Date().description
print("DEBUG new payload : \(payload)")
let dictionary = ["data":payload] // key 'data' provides payload
NotificationCenter.default.post(name: DataProvider.updated, object: self, userInfo: dictionary)
}
func startNotifications() {
running = true
runNotification()
}
private func runNotification() {
if self.running {
self.fetchSomeData()
let soon = DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(3))
DispatchQueue.main.asyncAfter(deadline: soon) {
self.runNotification()
}
} else {
print("DEBUG runNotification will no longer run")
}
}
func stopNotifications() {
running = false
}
}
It seems that there are two publishers name let publisher in your program. Please remove one set of them. Also self.info = payload and publisher.assign(to: \.info, on: self)} are duplicating.
}
.onAppear(perform: subscribeToNotifications)
.onDisappear(perform: unsubscribeToNotifications)
.onReceive(publisher) { (payload) in
// self.info = payload
print(payload)
}
.tabItem {
In the following:
#State var cancel: AnyCancellable? = nil
private func subscribeToNotifications() {
// publisher to emit events when the default NotificationCenter broadcasts the notification
// let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
// .map { notification in
// return notification.userInfo?["data"] as! String
// }
// .receive(on: RunLoop.main)
// keep reference to Cancellable, and assign String value to property
if cancel == nil{
cancel = publisher.assign(to: \.info, on: self)}
}
private func unsubscribeToNotifications() {
guard cancel != nil else {
return
}
cancel?.cancel()
}
Now you can see, cancel?.cancel() does work and the info label no longer update after you come back from tab2. ~~~Publisher pause Here because subscription has been cancelled.~~~
Publisher is not paused as there is another subscriber in the view , so the print(payload) still works.

How to present a view after a request with URLSession in SwiftUI?

I want to present a view after I receive the data from a request, something like this
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(LogonView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}
}
Business logic mixed with UI code is a recipe for trouble.
You can create a model object as a #ObjectBinding as follows.
class Model: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var shouldPresentModal = false {
didSet {
didChange.send(())
}
}
func fetch() {
// Request goes here
// Edit `shouldPresentModel` accordingly
}
}
And the view could be something like...
struct ContentView : View {
#ObjectBinding var model: Model
#State var companyID: String = ""
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
if (model.shouldPresentModal) {
// presentation logic goes here
}
}.onAppear {
self.model.fetch()
}
}
}
The way it works:
When the VStack appears, the model fetch function is called and executed
When the request succeeds shouldPresentModal is set to true, and a message is sent down the PassthroughSubject
The view, which is a subscriber of that subject, knows the model has changed and triggers a redraw.
If shouldPresentModal was set to true, additional UI drawing is executed.
I recommend watching this excellent WWDC 2019 talk:
Data Flow Through Swift UI
It makes all of the above clear.
I think you can do smth like that:
var body: some View {
VStack {
Text("Company ID")
}
.onAppear() {
self.loadContent()
}
}
private func loadContent() {
let url = URL(string: "https://your.url")!
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(ContentView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}

Resources