I have a simple watchOS 6.2.8 SwiftUI application in which I present a list of messages to the user.
These messages are modelled as classes and have a title, body and category name. I also have a Category object which is a view on these messages that only shows a specific category name.
I specifically mention watchOS 6.2.8 because it seems SwiftUI behaves a bit different there than on other platforms.
class Message: Identifiable {
let identifier: String
let date: Date
let title: String
let body: String
let category: String
var id: String {
identifier
}
init(identifier: String, date: Date, title: String, body: String, category: String) {
self.identifier = identifier
self.date = date
self.title = title
self.body = body
self.category = category
}
}
class Category: ObservableObject, Identifiable {
let name: String
@Published var messages: [Message] = []
var id: String {
name
}
init(name: String, messages: [Message] = []) {
self.name = name
self.messages = messages
}
}
Category itself is an @ObservableObject and publishes messages, so that when a category gets updated (like in the background), the view that is displaying the category message list will also update. (This works great)
To store these messages I have a simple MessageStore, which is an @ObservableObject that looks like this:
class MessageStore: ObservableObject {
@Published var messages: [Message] = []
@Published var categories: [Category] = []
static let sharedInstance = MessageStore()
func insert(message: Message) throws { ... mutage messages and categories ... }
func delete(message: Message) throws { ... mutage messages and categories ... }
}
(For simplicity I use a singleton, because there are problems with environment objects not being passed properly on watchOS)
The story keeps messages and categories in sync. When a new Message is added that has a category name set, it will also create or update a Category object in the categories list.
In my main view I present two things:
- An All Messages
NavigationLinkthat goes to a view to display all messages - For each Category I display a
NavigationLinkthat goes to a view to display messages just in that specific category.
This all works, amazingly. But there is one really odd thing happening that I do not understand. (First SwiftUI project)
When I go into the All Messages list and delete all messages containing to a specific category, something unexpected happen when I navigate back to the main view.
First I observe that the category is properly removed from the list.
But then, the main view automatically quickly navigates to the All Messages list and then back.
The last part is driving me .. crazy .. I don't understand why this is happening. From a data perspective everyting looks good - the messages have been deleted and the category too. The final UI state, after the automagical navigation, also looks good - the message count for All Messages is correct and the category with now zero messages is not shown in the list anymore.
Here is the code for the main ContentView and also for the AllMessagesView. If helpful I can post the complete code here of course.
struct AllMessagesView: View {
@ObservedObject var messageStore = MessageStore.sharedInstance
@ViewBuilder
var body: some View {
if messageStore.messages.count == 0 {
Text("No messages").multilineTextAlignment(.center)
.navigationBarTitle("All Messages")
} else {
List {
ForEach(messageStore.messages) { message in
MessageCellView(message: message)
}.onDelete(perform: deleteMessages)
}
.navigationBarTitle("All Messages")
}
}
func deleteMessages(at offsets: IndexSet) {
for index in offsets {
do {
try messageStore.delete(message: messageStore.messages[index])
} catch {
NSLog("Failed to delete message: \(error.localizedDescription)")
}
}
}
}
//
struct CategoryMessagesView: View {
@ObservedObject var messageStore = MessageStore.sharedInstance
@ObservedObject var category: Category
var body: some View {
Group {
if category.messages.count == 0 {
Text("No messages in category “\(category.name)”").multilineTextAlignment(.center)
} else {
List {
ForEach(category.messages) { message in
MessageCellView(message: message)
}.onDelete(perform: deleteMessages)
}
}
}.navigationBarTitle(category.name)
}
func deleteMessages(at offsets: IndexSet) {
for index in offsets {
do {
try messageStore.delete(message: category.messages[index])
} catch {
NSLog("Cannot delete message: \(error.localizedDescription)")
}
}
}
}
struct ContentView: View {
@ObservedObject var messageStore = MessageStore.sharedInstance
var body: some View {
List {
Section {
NavigationLink(destination: AllMessagesView()) {
HStack {
Image(systemName: "tray.2")
Text("All Messages")
Spacer()
Text("\(messageStore.messages.count)")
.font(messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
}
Section {
Group {
if messageStore.categories.count > 0 {
Section {
ForEach(messageStore.categories) { category in
NavigationLink(destination: CategoryMessagesView(category: category)) {
HStack {
Image(systemName: "tray") // .foregroundColor(.green)
Text("\(category.name)").lineLimit(1).truncationMode(.tail)
Spacer()
Text("\(category.messages.count)")
.font(self.messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
}
}
} else {
EmptyView()
}
}
}
}
}
// TODO This is pretty inefficient
func messageCountFont() -> Font {
let font = UIFont.preferredFont(forTextStyle: .caption1)
return Font(font.withSize(font.pointSize * 0.75))
}
}
Apologies, I know this is a lot of code, but I feel I need to give enough context and visibility to show what is going on here.
Full project at https://github.com/st3fan/LearningSwiftUI/tree/master/MasterDetail - I don't think more code is relevant, but if it is, let me know and I'll move it into the question here.
