I have a variable within my @StateObject
that is used to show new message notifications. It is updated periodically from my backend (push noti).
The problem is that once this variable updates the badge, it will pull the user out of other tabs unexpectedly (i.e. reloads the entire TabView
and takes them to the first tab). I want the notification badge to update without interfering with what the user is doing. I've tried using equatable()
, but to no avail. It seems that TabView
reloads everything on state object changes. Is there a way to only reload the tab labels? any help will be appreciated.
ContentView.swift
:
struct ContentView: View {
@State var tab: UInt = 0
@StateObject var messenger: MessagingViewModel = MessagingViewModel()
@State var firstAppear = true
init() {
UITabBar.appearance().backgroundColor = UIColor(Color.white)
}
var body: some View {
TabView(selection: $tab) {
HomeMarket(tab: $tab)
.tabItem {
Label("Market", systemImage: "house.fill")
}.tag(0)
AddItems(tab: $tab)
.tabItem {
Label("Add Item", systemImage: "plus.circle")
}.tag(1)
MessageList(tab: $tab)
.tabItem {
Label("Messages", systemImage: "envelope.fill")
}
.tag(2)
.badge(messenger.newMsgCount)
}
.accentColor(Color("AccentColor"))
.onAppear() {
if firstAppear {
messenger.getAllMessages()
firstAppear = false
}
}
.environmentObject(messenger)
}
}
MessengerViewModel.swift
: (shortened for SO. Even this small sample causes the same issue)
class MessagingViewModel: ObservableObject {
@Published var newMsgCount = 0
init(){// connect socket...}
func setUpChatListener() {
DispatchQueue.main.async {
self.manager.defaultSocket.on("Private Message") { data, ack in
do {
self.newMsgCount = 1
//do what ever else I need to do with the chat...
} catch let err {
print(err)
}
}
}
}
}
I've really hit a roadblock with this one. Am I going to have to use Apple's push system? I find it odd that I can't just reload the little icon to show the new message count rather than reloading the entire thing.
CodePudding user response:
This is caused by a mismatch of your tab
type (which you've defined as UInt
) and the .tag
modifier that you've added to each tab item, which Swift is interpreting as an Int
, when you write .tag(0)
.
You can fix this two ways. Unless you really need it to be a UInt
, you could change to Int
(including all of the bindings in the views that you're sending it to):
struct HomeMarket : View {
@Binding var tab: Int
var body: some View {
Text("HomeMarket")
}
}
struct AddItems : View {
@Binding var tab: Int
var body: some View {
Text("Add")
}
}
struct MessageList : View {
@Binding var tab: Int
var body: some View {
Text("Messages")
}
}
struct ContentView: View {
@State var tab: Int = 0
@StateObject var messenger: MessagingViewModel = MessagingViewModel()
@State var firstAppear = true
var body: some View {
TabView(selection: $tab) {
HomeMarket(tab: $tab)
.tabItem {
Label("Market", systemImage: "house.fill")
}.tag(0)
AddItems(tab: $tab)
.tabItem {
Label("Add Item", systemImage: "plus.circle")
}.tag(1)
MessageList(tab: $tab)
.tabItem {
Label("Messages", systemImage: "envelope.fill")
}
.tag(2)
.badge(messenger.newMsgCount)
}
.onAppear() {
if firstAppear {
messenger.getAllMessages()
firstAppear = false
}
}
.onChange(of: tab, perform: { newValue in
print("Tab changed: ", newValue)
})
.environmentObject(messenger)
}
}
class MessagingViewModel: ObservableObject {
@Published var newMsgCount = 0
func getAllMessages() {
DispatchQueue.main.asyncAfter(deadline: .now() 4) {
self.newMsgCount = 1
}
}
}
Or, you could explicitly use UInt
in your .tag
modifiers:
TabView(selection: $tab) {
HomeMarket(tab: $tab)
.tabItem {
Label("Market", systemImage: "house.fill")
}.tag(0 as UInt)
AddItems(tab: $tab)
.tabItem {
Label("Add Item", systemImage: "plus.circle")
}.tag(1 as UInt)
MessageList(tab: $tab)
.tabItem {
Label("Messages", systemImage: "envelope.fill")
}
.tag(2 as UInt)
.badge(messenger.newMsgCount)
}
Explanation:
tag()
is a relatively common source of issues since the compiler doesn't warn you if the type doesn't match what you provide in selection:
(it would be nice if there were a compiler or linter warning about it).
The reloading happens because when you tap to load another tab, it doesn't actually end up setting the tab
variable (since the types don't match). Then, upon another state change (like an update coming from your StateObject
) the system uses the @State
variable to determine what tab it should be on. Since it was never updated to anything other than 0, it looks like it 'resets' it's state, but in actuality, it thinks it's going to the place that you want (as represented by your @State
variable).