SwiftUI - Weird behavior in foreach when notification received(fcm) - ios

So, if I have a view opened that contains a ForEach, as soon as a receive the notification the ForEach disapears. I even used a print in .onDisapear to be sure about that. I have multiple situations like that in my app. I am using Firebase Cloud Messaging, and the notification works fine, it receives the data as well. The only problem is only with a view that contains a ForEach, causing it to disapear.
Example of a ForEach in the app:
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 1) {
ForEach(scheduleFuncs.panelsListOne) { panel in
if panel.hasSpeakers {
PanelsSpeakerCard(widthVstack: geometry.size.width, heightVStack: geometry.size.height * 0.40, panelTitle: panel.panelName, startTime: panel.startTime, moderatorId: panel.moderatorId, speakers: panel.speakers)
} else {
PanelsNoSpeakersCard(widthVstack: geometry.size.width, heightVStack: geometry.size.height * 0.20, panelTitle: panel.panelName, startTime: panel.startTime, details: panel.details)
}
}
} ...
The app state that I am using to manage data from the notification:
class AppState: ObservableObject {
static let shared = AppState()
#Published var notification: Bool = false
#Published var inboxId: String = ""
}
Thanks in advance.
Made sure that the ForEach disapeared.

Related

Body of View is not rerendered

I am making QuizApp. Currently I have viewModel in which questions are fetched and stored in #Published array.
class HomeViewModel: ObservableObject {
let repository: QuestionRepository
#Published var questions: [Question] = []
#Published var isLoading: Bool = true
private var cancellables: Set<AnyCancellable> = .init()
init(repository: QuestionRepository){
self.repository = repository
getQuestions()
}
private func getQuestions(){
repository
.getQuestions()
.receive(on: RunLoop.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] questions in
self?.isLoading = false
self?.questions = questions
}
)
.store(in: &cancellables)
}
func updateQuestions(){
questions.removeFirst()
if questions.count < 2 {
getQuestions()
}
}
}
In QuestionContainerView HomeViewModel is created as #StateObject and from it, first data from questions array is used and passed to QuestionView.
#StateObject private var viewModel: HomeViewModel = HomeViewModel(repository: QuestionRepositoryImpl())
var body: some View {
if viewModel.isLoading {
ProgressView()
} else {
VStack(alignment: .leading, spacing: 16) {
if let question = viewModel.questions.first {
QuestionView(question: question){
viewModel.updateQuestions()
}
} else {
Text("No more questions")
.font(.title2)
}
}
.padding()
}
}
QuestionView has two properties, Question and showNextQuestion callback.
let question: Question
let showNextQuestion: () -> Void
And when some button is pressed in that view, callBack is called after 2.5s and after that viewModel function updateQuestions is called.
struct QuestionView: View {
let question: Question
let showNextQuestion: () -> Void
#State private var showCorrectAnswer: Bool = false
#State private var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Text("\(timeRemaining)")
.font(.title2)
Text(question.question)
.font(.title2)
.padding()
ForEach(question.allAnswers, id: \.self){ answer in
Button(
action: {
showCorrectAnswer.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
showNextQuestion()
}
},
label: {
Text(answer)
.font(.title2)
.padding()
.background(getBackgroundColor(answer: answer))
.clipShape(Capsule())
}
)
}
Spacer()
}
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
} else {
showNextQuestion()
}
}
}
My idea was to pass first item from viewModel array to QuestionView and after some Button action in QuestionView I wanted to remove firstItem from array and pass next firstItem.
But problem is that QuestionView is not updated (it is not rerendered) and it contains some data from past item - I added timer in QuestionView which is counting down and when question is changed, timer value is still same as for before question, it is not reseted.
I thought that marking viewModel array property with #Published will trigger whole QuestionContainerView render with new viewModel first item from array, but it is not updated as I wanted.
There are several mistakes in the SwiftUI code, one or all could contribute to the problem, here are the ones I noticed:
We don't use view model objects in SwiftUI for view data, that's the job of the View struct and property wrappers.
When ObservableObject is being used for model data, it's usually a singleton (one for the app and another for previews) and passed in as environmentObject. We don't usually use the reference version of #State, i.e. #StateObject for holding the model since we don't want model lifetime tied to any view on screen, it has to be tied to the app executable's lifetime. Also, #StateObject are disabled for previews since usually those are used for network downloads.
In an ObservableObject we .assign(to: &$propertyName) the end of the pipeline to an #Published var, we don't use sink or need cancellables in this case. This ties the pipeline's lifetime to the object's, if you use sink you need to cancel it yourself when the object de-inits (Not required for singletons but it's good to learn the pattern).
Since your timer is a let it will be lost every time the QuestionView is re-init, to fix it needs to be #State.
ForEach is a View not a for loop. You have either supply Identifiable data or an id param, you can't use id:\.self for dynamic data or it'll crash when it changes.

SwiftUI ForEach printing duplicate items

In my application I am creating various arrays of Identifiable structs using my API response. I am iterating over said array to build lists within the Content and Sidebar columns of my Navigation Split View. If I print the array before my ForEach call, the array is normal. When printing each item from within the ForEach (let _ = print(item)) the item prints twice. However the item is only added to the List once. Is this normal behavior? It appears to be happening with all of my ForEach calls. Visually the view looks correct, just want to be sure there isn’t any additional looping or view updated occurring.
Printing each item of array. Resulting in duplicate prints.
//
// TeamView.swift
// myDashboard
//
// Created by nl492k on 10/18/22.
//
import SwiftUI
struct TeamView: View {
var user: loggedInUser
var viewData = apiData()
// viewData is an instance of the apiData struct that includes 2 Arrays of identifieable structs ("gauges" & "trends") and a "team" struct that containts an array of idenfifiable structs "teamMembers" viewData is a singular object that is updated by the completion handler of my API call.
// struct apiData {
// var gauges : Array<gaugeObj>
// var trends : Array<trendObj>
// var team : teamObj
//
// init(gauges : Array<gaugeObj> = Array<gaugeObj>(), trends: Array<trendObj> = Array<trendObj>(), team: teamObj = teamObj()) {
// self.gauges = gauges
// self.trends = trends
// self.team = team
// }
// }
#Binding var uid_selection: String?
var emulation_uid: String
var body: some View {
if viewData.team.attuid == "" {
Label("Not Signed In", systemImage: "person.crop.circle.fill.badge.questionmark")
}
else {
List(selection: $uid_selection){
HStack {
NavigationLink(value: viewData.team.superv) {
AsyncImage(url: URL(string: "\(userImageUrl)\(viewData.team.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(viewData.team.fName) \(viewData.team.lName)")
Text("\(viewData.team.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
Label("", systemImage:"arrow.up.and.person.rectangle.portrait")
}
Divider()
//------ This prints the Array of identifiable structs, as expected, with no issues --------
let _ = print(viewData.team.teamMembers)
ForEach(viewData.team.teamMembers) { employee in
//----- This prints multiple times per employee in array ------.
let _ = print(employee)
NavigationLink(value: employee.attuid) {
AsyncImage(url: URL(string: "\(userImageUrl)\(employee.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(employee.fName) \(employee.lName)")
Text("\(employee.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
}
}
.background(Color("ContentColumn"))
.scrollContentBackground(.hidden)
}
}
}
struct TeamView_Previews: PreviewProvider {
static var previews: some View {
TeamView(user: loggedInUser.shared,
viewData: apiData(gauges:gaugesTest,
trends: trendsTest,
team: teamTest),
uid_selection: .constant(loggedInUser.shared.attuid),
emulation_uid: "")
}
}
From your code snippet, it looks like you're doing a print in the body of the ForEach, and seeing multiple prints per item.
Actually, this is completely normal behaviour because SwiftUI may render a view multiple times (which will cause your print statement to be called each time). There is no need to worry about such rerenders (unless you're debugging performance issues). SwiftUI's rendering heuristics isn't known to the public, and may sometimes choose to make multiple rendering passes even though no state variables have changed.

SwiftUI: How can I add a new component to the list? Idea: Click the "plus"-button, enter a "Text", store it and... it's been added to the List

So here is the declaration of the variable (passwordNewName):
#State var passwordNewName = ""
It is being updated from the input of a TextField which I coded to accept user's data.
So the idea is that the String which is stored in the variable will - eventually - get transmitted into this List():
If I understand it right, the .setname files are stored inside this static func all() database or whatever these [*braces*] are:
Basically, the String-variable passwordNewName should somehow be added to these [braces] automatically... I am really so lost.((
Thank you in advance!!!
According to the question it looks that you want to update the list along with to send/save it permanently. The actual solution for this problem is to use ObservableObject class with #Published property. But the code below will give you more understanding of scenario.
import SwiftUI
struct ContentView: View {
#State private var lernSet_array: [Lernset] = Lernset.get_all()
var body: some View {
VStack(spacing: 0) {
HStack {
Text("Collection")
Button {
lernSet_array.append(Lernset(setname: "Testing", color: "black"))
} label: {
Image(systemName: "plus")
.resizable()
.frame(width: 30, height: 30)
.padding()
}
}
ScrollView {
VStack(alignment: .leading) {
ForEach(lernSet_array) { lernset in
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 80, height: 80)
.foregroundColor(.gray)
Text(lernset.setname)
}
}
}
}
}.onChange(of: lernSet_array) { newValue in
// Update this database, and somehow store the data
// and save it permanently… even after restarting the phone
print("Update this database, and somehow store the data and save it permanently… even after restarting the phone")
print(lernSet_array.count)
}
}
}
struct Lernset: Identifiable, Equatable {
let id = UUID()
let setname: String
let color: String
static func get_all() -> [Lernset] {
return [Lernset(setname: "Biology - Tail", color: "green"),
Lernset(setname: "Math - Tail", color: "blue"),
Lernset(setname: "Phy - Tail", color: "black"),
]
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How can I get UNNotificationRequest array using for loop to populate SwiftUI List view?

I want to simply get the list of local notifications that are scheduled and populate a SwiftUI List using forEach. I believe it should work like I have done below, but the array is always empty as it seems to be used before the for loop is finished. I tried the getNotifications() function with a completion handler, and also as a return function, but both ways still didn't work. How can I wait until the for loop is done to populate my list? Or if there is another way to do this please let me know, thank you.
var notificationArray = [UNNotificationRequest]()
func getNotifications() {
print("getNotifications")
center.getPendingNotificationRequests(completionHandler: { requests in
for request in requests {
print(request.content.title)
notificationArray.append(request)
}
})
}
struct ListView: View {
var body: some View {
NavigationView {
List {
ForEach(notificationArray, id: \.content) { notification in
HStack {
VStack(alignment: .leading, spacing: 10) {
let notif = notification.content
Text(notif.title)
Text(notif.subtitle)
.opacity(0.5)
}
}
}
}
.onAppear() {
getNotifications()
}
}
}
Update:
Here is how I am adding a new notification and calling getNotifications again. I want the list to dynamically update as the new array is made. Printing to console shows that the getNotifications is working correctly and the new array contains the added notiication.
Section {
Button(action: {
print("Adding Notification: ", title, bodyText, timeIntValue[previewIndex])
addNotification(title: title, bodyText: bodyText, timeInt: timeIntValue[previewIndex])
showDetail = false
self.vm.getNotifications()
}) {
Text("Save Notification")
}
}.disabled(title.isEmpty || bodyText.isEmpty)
Your global notificationArray is not observed by view. It should be dynamic property... possible solution is to wrap it into ObservableObject view model.
Here is a demo of solution:
class ViewModel: ObservableObject {
#Published var notificationArray = [UNNotificationRequest]()
func getNotifications() {
print("getNotifications")
center.getPendingNotificationRequests(completionHandler: { requests in
var newArray = [UNNotificationRequest]()
for request in requests {
print(request.content.title)
newArray.append(request)
}
DispatchQueue.main.async {
self.notificationArray = newArray
}
})
}
}
struct ListView: View {
#ObservedObject var vm = ViewModel()
//#StateObject var vm = ViewModel() // << for SwiftUI 2.0
var body: some View {
NavigationView {
List {
ForEach(vm.notificationArray, id: \.content) { notification in
HStack {
VStack(alignment: .leading, spacing: 10) {
let notif = notification.content
Text(notif.title)
Text(notif.subtitle)
.opacity(0.5)
}
}
}
}
.onAppear() {
self.vm.getNotifications()
}
}
}

SwiftUI List with TextField adjust when keyboard appears/disappears

I have written a List with SwiftUI. I also have a TextField object which is used as a search bar. My code looks like this:
import SwiftUI
struct MyListView: View {
#ObservedObject var viewModel: MyViewModel
#State private var query = ""
var body: some View {
NavigationView {
List {
// how to listen for changes here?
// if I add onEditingChange here, Get the value only after the user finish search (by pressing enter on the keyboard)
TextField(String.localizedString(forKey: "search_bar_hint"), text: self.$query) {
self.fetchListing()
}
ForEach(viewModel.myArray, id: \.id) { arrayObject in
NavigationLink(destination: MyDetailView(MyDetailViewModel(arrayObj: arrayObject))) {
MyRow(arrayObj: arrayObject)
}
}
}
.navigationBarTitle(navigationBarTitle())
}
.onAppear(perform: fetchListing)
}
private func fetchListing() {
query.isEmpty ? viewModel.fetchRequest(for: nil) : viewModel.fetchRequest(for: query)
}
private func navigationBarTitle() -> String {
return query.isEmpty ? String.localizedString(forKey: "my_title") : query
}
}
The problem I have now is that the List remains behind the keyboard :(. How can I set the list padding bottom or edge insets (or whatever else works, I am totally open) so that the scrolling of the list ends above the keyboard? The list „size“ should also adjust automatically depending on if keyboard will be opened or closed.
Problem looks like this:
Please help me with any advice on this, I really have no idea how to do this :(. I am a SwiftUI beginner who is trying to learn it :).
You may try the following and add detailed animations by yourself.
#ObservedObject var keyboard = KeyboardResponder()
var body: some View {
NavigationView {
List {
// how to listen for changes here?
// if I add onEditingChange here, Get the value only after the user finish search (by pressing enter on the keyboard)
TextField("search_bar_hint", text: self.$query) {
self.fetchListing()
}
ForEach(self.viewModel, id: \.self) { arrayObject in
Text(arrayObject)
}
}.padding(.bottom, self.keyboard.currentHeight).animation(.easeIn(duration: self.keyboard.keyboardDuration))
.navigationBarTitle(self.navigationBarTitle())
}
.onAppear(perform: fetchListing)
}
class KeyboardResponder: ObservableObject {
#Published var currentHeight: CGFloat = 0
#Published var keyboardDuration: TimeInterval = 0
private var anyCancellable: Set<AnyCancellable> = Set<AnyCancellable>()
init() {
let publisher1 = NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardWillShowNotification).map{ notification -> Just<(CGFloat, TimeInterval)> in
guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {return Just((CGFloat(0.0), 0.0)) }
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return Just((CGFloat(0.0), 0.0)) }
return Just((keyboardSize.height, duration))}
let publisher2 = NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardWillHideNotification) .map{ notification -> Just<(CGFloat, TimeInterval)> in
guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return Just((CGFloat(0.0), 0.0)) }
return Just((0.0, duration))}
Publishers.Merge(publisher1, publisher2).switchToLatest().subscribe(on: RunLoop.main).sink(receiveValue: {
if $0.1 > 1e-6 { self.currentHeight = $0.0 }
self.keyboardDuration = $0.1
}).store(in: &anyCancellable)
}
}
The resolution for the problem with the keyboard padding is like E.coms suggested. Also the class written here by kontiki can be used:
How to make the bottom button follow the keyboard display in SwiftUI
The problems I had was because of state changes in my view hierarchy due to multiple instances of reference types publishing similar state changes.
My view models are reference types, which publish changes to its models, which are value types. However, these view models also contain reference types which handle network requests. For each view I render (each row), I assign a new view model instance, which also creates a new network service instance. Continuing this pattern, each of these network services also create and assign new network managers.

Resources