SwiftUI why doesn't my view update once this object property changes? - ios

#ObservedObject var vm: PlayerViewModel
var body : some View {
Button("Test"){
Task{
await vm.getPlayer(username: username)
switch vm.state{
case .success(var player):
let docRef = vm.db.collection("players").document(player.displayname)
docRef.getDocument {document, error in
if let document = document, document.exists {
print("document exists")
player.todayFinalKills = document["todayFinalKills"] as? Int
print(player.todayFinalKills ?? 0)
}
}
}
}
So I just have some firestore database here(not really relevant to the question I don't think) I'm calling some function, I check the state of some enum and then check if the document exists, if it exists I change one property in the associated data of the enum. This is what this prints:
So it does seem that the property is getting updated in the player object for sure but then I have another view with a List
struct DailyView: View {
#ObservedObject var vm: PlayerViewModel
var body: some View {
List{
Text("\(player.todayFinalKills ?? 0)")
}
}
}
But this view always has a zero, implying that this still thinks todayFinalKills is nil? If it literally prints it out as 8291 why is this happening? I was thinking maybe if I switch on a enum like this and extract the associated data maybe it creates a copy but I wasn't able to find much information about this online. Can anyone help me figure out why this is happening? I am passing the same ViewModel into all these views.
Edit: PlayerViewModel.swift
import FirebaseFirestore
import Foundation
#MainActor
class PlayerViewModel: ObservableObject{
enum PlayerState {
case na
case loading
case success(data: Player)
case failed(error : Error)
}
var db = Firestore.firestore()
#Published private(set) var state: PlayerState = .na
private let service: PlayerService
init(service: PlayerService)
{
self.service = service
}
func getPlayer(username: String) async {
state = .loading
do{
let uuid = try await service.fetchUUID(username: username)
let player = try await service.fetchPlayer(uuid: uuid)
state = .success(data: player)
}
catch{
print(error)
state = .failed(error: error)
}
}
}
DailyView.swift
import SwiftUI
struct DailyView: View {
#ObservedObject var vm: PlayerViewModel
var body: some View {
switch vm.state{
case .success(let player):
List{
Text("\(player.todayFinalKills ?? 0)")
}
default:
EmptyView()
}
}
}
struct DailyView_Previews: PreviewProvider {
static var previews: some View {
DailyView(vm: PlayerViewModel(service: PlayerService()))
}
}
LoginView.swift
struct LoginView: View {
#ObservedObject var vm : PlayerViewModel
#State private var username = ""
#State private var profileLoaded = false
private var searchAllowed : Bool{
if(username.count>2)
{
return false
}
return true
}
var body: some View {
NavigationView{
ZStack{
VStack{
Button{
Task{
await vm.getPlayer(username: username)
switch vm.state{
case .success(var player):
let docRef = vm.db.collection("players").document(player.displayname)
docRef.getDocument {document, error in
if let document = document, document.exists {
player.todayFinalKills = document["todayFinalKills"] as? Int
print(player.todayFinalKills ?? 0)
}
else{
let stats : [String : Int] = [
"todayFinalKills": player.stats.Bedwars?.final_kills_bedwars ?? 0,
"todayFinalDeaths" : player.stats.Bedwars?.final_deaths_bedwars ?? 0,
"todayBedwarsWins" : player.stats.Bedwars?.wins_bedwars ?? 0,
"todayBedwarsLosses" : player.stats.Bedwars?.losses_bedwars ?? 0,
"todayTntRunLosses" : player.stats.TNTGames?.deaths_tntrun ?? 0,
"todayTntRunWins" : player.achievements.tntgames_tnt_run_wins ?? 0,
]
vm.db.newPlayerDocument(data: stats, username: player.displayname, uuid: player.uuid)
}
}
profileLoaded = true
default:
print("default")
}
}
} label:
{
ZStack{
RoundedRectangle(cornerRadius: 5)
.fill(.gray)
.frame(width: 200, height: 50)
Text("Search")
.foregroundColor(.white)
}
}
Model.swift
struct Player : Codable, Hashable, Equatable{
static func == (lhs: Player, rhs: Player) -> Bool {
lhs.uuid == rhs.uuid
}
// so a player contains many objects, like stats, achievements, etc
var stats: Stats
var achievements: Achievements
var displayname: String
var uuid : String
var networkExp: Int
var knownAliases : [String]
var karma: Int
var newPackageRank : String? //this could be MVP_PLUS
var rankPlusColor : String? //this could be LIGHT_PURPLE
//daily statistics for the player.
**var todayFinalKills : Int?**
var todayBedwarsLosses : Int?
var todayBedwarsWins: Int?
var todayFinalDeaths: Int?
var todayTntRunWins: Int?
var TodayTntRunLosses: Int?
//

Related

Firebase is not saving the data - swiftui

I have a problem that Firebase not saving the data and it shows like that :
enter image description here
First I created a model :
struct Box: Identifiable, Hashable, Codable {
#DocumentID var id: String?
var boxName: String
var boxSize: String
enum CodingKeys: String, CodingKey {
case id
case boxName
case boxSize
}
var dictionary: [String: Any] {
let data = (try? JSONEncoder().encode(self)) ?? Data()
return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:]
}
}
Then I created ViewModel:
class BoxViewModel: ObservableObject {
#Published var box: Box
private var db = Firestore.firestore()
init(box: Box = CashBox(boxName: "", boxSize: "")) {
self.box = box
}
private func addNewBox(_ box: Box){
do {
print("the name \(box.boxName)")
let _ = db.collection("Box")
.addDocument(data: box.dictionary)
}
}
func addBox() {
self.addNewBox(box)
}
func setBoxData(boxName: String, boxSize: String){
self.box.boxName = boxName
self.box.boxSize = boxSize
}
}
Finally here is the view:
struct CashBoxView: View {
#State var boxName = ""
#State var boxSize = ""
#EnvironmentObject var boxViewModel: BoxViewModel
var body: some View {
Text("Enter box name")
TextField("", text: $boxName)
Text("Enter box size")
TextField("", text: $boxSize)
Button( action: {
boxViewModel.setBoxData(boxName: boxName, boxSize: boxSize)
boxViewModel.addBox()
}) {
Text("Done")
}
}
First I thought that the problem is the box is empty but when I tried to print the box name print("the name \(box.boxName)") and printed it
is the problem the the viewModel is #EnvironmentObject ? or what is the problem ?
Thank you,

Update Details on a List SwiftUI

I'm fairly new to SwiftUI and I'm trying to update the details on a list and then save it.
I am able to get the details to update, but every time I try saving I'm not able to do it.
I have marked the area where I need help. Thanks in advance.
// This is the Array to store the items:
struct ExpenseItem : Identifiable, Codable {
var id = UUID()
let name: String
let amount: Int
}
// This is the UserDefault Array
class Expenses: ObservableObject {
#Published var items = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Items") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
items = decodedItems
return
}
}
items = []
}
}
// View to add details :
struct AddView: View {
#State private var name = ""
#State private var amount = 0
#StateObject var expenses: Expenses
#Environment(\.dismiss) var dismiss
var body: some View {
Form {
TextField("Name", text: $name)
Text("\(amount)")
Button("Tap Me") {
amount += 1
}
}
.navigationTitle("Add New Count")
.toolbar {
if name != "" {
Button("Save") {
let item = ExpenseItem(name: name, amount: amount)
expenses.items.append(item)
dismiss()
}
}
}
}
}
// This is the file to update the details:
struct UpdateDhikr: View {
#EnvironmentObject var expenses : Expenses
#State var name : String
#State var amount : Int
var body: some View {
Form {
TextField("Name", text: $name)
Text("\(amount)")
Button("Tap Me") {
amount += 1
}
}
.navigationTitle("Update Count")
.toolbar {
if name != "" {
Button("Save") {
// This is where I'm having problems.
}
}
}
}
}

ObservableObject not updating view in nested loop SWIFTUI

Regarding the following project :
You have an amountSum of 100
When you click on one user "plus" button, this specific user have to pay this amount but if you click on multiple user "plus" button, the amount to pay is divided between them equally.
Any idea how I can update the entire Model2.MustPayM2 prop when I click on the "plus" button please ?
import SwiftUI
struct Model1: Identifiable, Codable {
var id: String = UUID().uuidString
var nameM1: String
var amountM1: Double
var amountSumM1: Double = 100
var arrayM2: [Model2]
var isVisible: Bool = false
}
struct Model2: Identifiable, Codable {
var id: String = UUID().uuidString
var nameM2: String
var amountM2: Double = 0
var mustPayM2: Bool = false
}
class ViewModel1: ObservableObject {
#Published var Publi1: Model1
#Published var Publi1s: [Model1] = []
#Published var Publi2: Model2
#Published var Publi2s: [Model2] = []
init() {
let pub2 = Model2(nameM2: "init")
let pub1 = Model1(nameM1: "init", amountM1: 0, arrayM2: [pub2])
self.Publi2 = pub2
self.Publi1 = pub1
var newPub1s: [Model1] = []
for i in (0..<5) {
let newNameM1 = "name\(i+1)"
let newAmountM1 = Double(i+1)
var newModel1 = Model1(nameM1: newNameM1, amountM1: newAmountM1, arrayM2: [pub2])
var newPub2s: [Model2] = []
for i in (0..<5) {
let newNameM2 = "\(newNameM1)-user\(i+1)"
let newModel2 = Model2(nameM2: newNameM2)
newPub2s.append(newModel2)
}
newModel1.arrayM2 = newPub2s
newPub1s.append(newModel1)
}
Publi1s = newPub1s
Publi1 = newPub1s[0]
Publi2s = newPub1s[0].arrayM2
Publi2 = newPub1s[0].arrayM2[0]
}
}
struct View1: View {
#EnvironmentObject var VM1: ViewModel1
#State private var tt: String = ""
private let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
VStack {
ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
Text("\(VM1.Publi2s[i].nameM2)")
Text(tt)
Button {
VM1.Publi2s[i].mustPayM2.toggle()
var a = VM1.Publi2s.filter { $0.mustPayM2 == true }
let b = VM1.Publi1.amountM1 / Double(a.count)
// How can I update the new props between all users ??
// for j in 0..<a.count {
// a[j].amountM2 = b
// }
} label: {
Image(systemName: "plus")
}
}
Spacer()
Button {
VM1.Publi1.isVisible.toggle()
} label: {
Text("SHOW ME")
}
Spacer()
}
View2()
.offset(y: VM1.Publi1.isVisible ? 0 : screenHeight)
}
}
}
struct View2: View {
#EnvironmentObject var VM1: ViewModel1
var body: some View {
VStack {
Spacer()
ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
Text("\(VM1.Publi2s[i].amountM2)")
}
}
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
Group {
View1()
}
.environmentObject(ViewModel1())
}
}
You implementation seems overly complicated and error prone. I´ve practically rewritten the code for this. I´ve added comments to make it clear what and why I have done certain things. If you don´t understand why, don´t hesitate to ask a question. But please read and try to understand the code first.
//Create one Model containing the individuals
struct Person: Identifiable, Codable{
var id = UUID()
var name: String
var amountToPay: Double = 0.0
var shouldPay: Bool = false
}
//Create one Viewmodel
class Viewmodel:ObservableObject{
//Entities being observed by the View
#Published var persons: [Person] = []
init(){
//Create data
persons = (0...4).map { index in
Person(name: "name \(index)")
}
}
//Function that can be called by the View to toggle the state
func togglePersonPay(with id: UUID){
let index = persons.firstIndex { $0.id == id}
guard let index = index else {
return
}
//Assign new value. This will trigger the UI to update
persons[index].shouldPay.toggle()
}
//Function to calculate the individual amount that should be paid and assign it
func calculatePayment(for amount: Double){
//Get all persons wich should pay
let personsToPay = persons.filter { $0.shouldPay }
//Calcualte the individual amount
let individualAmount = amount / Double(personsToPay.count)
//and assign it. This implementation will trigger the UI only once to update
persons = persons.map { person in
var person = person
person.amountToPay = person.shouldPay ? individualAmount : 0
return person
}
}
}
struct PersonView: View{
//pull the viewmodel from the environment
#EnvironmentObject private var viewmodel: Viewmodel
//The Entity that holds the individual data
var person: Person
var body: some View{
VStack{
HStack{
Text(person.name)
Text("\(person.amountToPay, specifier: "%.2f")$")
}
Button{
//toggle the state
viewmodel.togglePersonPay(with: person.id)
} label: {
//Assign label depending on person state
Image(systemName: "\(person.shouldPay ? "minus" : "plus")")
}
}
}
}
struct ContentView: View{
//Create and observe the viewmodel
#StateObject private var viewmodel = Viewmodel()
var body: some View{
VStack{
//Create loop to display person.
//Dont´t itterate over the indices this is bad practice
// itterate over the items themselves
ForEach(viewmodel.persons){ person in
PersonView(person: person )
.environmentObject(viewmodel)
.padding(10)
}
Button{
//call the func to calculate the result
viewmodel.calculatePayment(for: 100)
}label: {
Text("SHOW ME")
}
}
}
}

NavigationLink doesn't work in nested ScrollView after EnvironmentObject update from network request

I have two tabs for navigationView, Dashboard and Userscreen.
Userscreen is used for fetching user information through network request, and dashboard is showing information through network request based on user information, hense user information is the environment object here.
After update user information I can view dashboard properties but click on navigation link is not working.
After doing some testing, I found that the navigation link(showing based on user info) under ScrollView -> ScrollView(.horizontal) -> NavigationLink is not working, but navigation link(showing based on user info) under VStack -> ScrollView(.horizontal) -> NavigationLink performs well.
I wonder if I was missing something on the implementation or may I ask is it an existing bug for XCode?
Edit: Updated code
Edit again: Please refer to #workingdog solution below, the whole problem is just caused by a simple silly mistake, which is not declare the right state/environment object.
import SwiftUI
import Kingfisher
import Alamofire
struct ExampleView: View {
#StateObject var userInfoManager = UserInfoManagerExample()
var body: some View {
NavigationView {
TabView (){
DashboardScreenExample()
.environmentObject(userInfoManager)
.tabItem{
Text("Dashboard")
}
LoginScreenExample()
.environmentObject(userInfoManager)
.tabItem{
Text("UpdateInfo")
}
}
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DashboardScreenExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#State var please : Bool = false
init(){
Theme.navigationBarColors(background: UIColor(viewProperties.themered), titleColor: .white)
}
var viewProperties = ViewProperties()
var body: some View {
if(userInfoManager.shouldShowDashboard){
ScrollView{//Switch this ScrollView to VStack make Navigation Link work again
//I had other screens too, but screen below is the one causing problem
DashboardContentExample()
}
}else{
ProgressView().onAppear{userInfoManager.shouldShowDashboard = true}
}
}
}
struct DashboardContentExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#ObservedObject var dashboardList = DashboardConfigRequestExample()
func loadRequest(){
dashboardList.loadData(passedLanguage: userInfoManager.preferenceLangauge)
}
//View start from here
var body: some View {
if(dashboardList.dashboardConfig.count == 0){
ProgressView()
.onAppear{
loadRequest()
}
}else{
ScrollView(.horizontal){
LazyHStack{
//Here is the NavigationLink not working
NavigationLink(destination: EmptyView()){
Text("Hello")
}
}
}
}
}
}
struct LoginScreenExample: View {
let usernameLoginRequest = UsernameLoginRequestExample()
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#State var loginClicked : Bool = false
var body: some View {
VStack(alignment: .leading){
if(userInfoManager.isLogin){
Text("You have logged in.")
.onAppear{
loginClicked = false
}
}
else{
ZStack{
VStack{
Spacer()
//the username and password had to be entered
Button(action:{clickLogin(forUsername: "iamusername", andPassword: "iampassword")}){
Text("Sign In")
}
Spacer()
}
if(loginClicked){
ProgressView()
}
}
}
}
}
func clickLogin(forUsername: String, andPassword: String){
loginClicked.toggle()
usernameLoginRequest.loadData(forUsername: forUsername, andPassword: andPassword, andUserInfo: userInfoManager)
}
}
class UserInfoManagerExample : ObservableObject{
#Published var isLogin: Bool = false
#Published var username: String = ""
#Published var password: String = ""
#Published var preferenceLangauge: String = "en"
#Published var shouldShowDashboard = true
init(){
}
func updateCredential(forUsername: String, andPassword: String){
username = forUsername
password = andPassword
}
}
struct UsernameLoginRequestExample{
func loadData(forUsername: String, andPassword: String, andUserInfo: UserInfoManagerExample){
let request_url = "This is the request url"
let request_parameters = getUsernameLoginParameter(withUsername: forUsername, andPassword: andPassword)
AF
.request(request_url, method: .post, parameters: request_parameters, encoding: JSONEncoding.default ).responseJSON{
responses
in
debugPrint(responses)
switch responses.result{
case .success:
do {
andUserInfo.shouldShowDashboard = false
andUserInfo.updateCredential(forUsername: forUsername, andPassword: andPassword)
andUserInfo.isLogin = true
}
case let .failure(error):
print(error)
}
}
}
func getUsernameLoginParameter(withUsername : String, andPassword : String) -> [String : String]{
var parameters : [String : String] = [String : String]()
parameters.updateValue(withUsername, forKey: "username")
parameters.updateValue(andPassword, forKey: "password")
var device_id : String
if(UIDevice.current.identifierForVendor != nil){
device_id = UIDevice.current.identifierForVendor!.uuidString
}else{
device_id = ""
}
parameters.updateValue(device_id, forKey:"device_id")
return parameters
}
}
class DashboardConfigRequestExample : ObservableObject{
#Published var dashboardConfig = [DashboardConfigExample]()
func loadData(passedLanguage : String){
let request_url = "This is request url"
let request_parameters = getDashboardConfigParameter(passedLanguage: passedLanguage)
AF
.request(request_url, method:.post, parameters: request_parameters)
.responseJSON{
responses in
switch responses.result {
case .success:
guard
let data = responses.data
else { return }
do {
let configResponse = try JSONDecoder().decode(DashboardConfigDataExample.self, from: data)
self.dashboardConfig = configResponse.list ?? []
} catch {
print(error)
}
case let .failure(error):
print(error)
}
}
}
func getDashboardConfigParameter(passedLanguage: String) -> [String:String]{
var parameters : [String:String] = [String:String]()
parameters.updateValue(passedLanguage, forKey: "language")
return parameters
}
}
struct DashboardConfigDataExample : Decodable{
public var status: String?
public var list: [DashboardConfigExample]?
}
struct DashboardConfigExample: Encodable & Codable {
let background_image: String?
let background_color: String?
}
Try replacing
#ObservedObject var infoRequest = InfoRequest()
in Dashboard with
#StateObject var infoRequest = InfoRequest()
EDIT1: in light of the new code.
Try moving #ObservedObject var dashboardList = DashboardConfigRequestExample() of DashboardContentExample
into ExampleView as #StateObject. Something like the following example code:
struct ExampleView: View {
#StateObject var userInfoManager = UserInfoManagerExample()
#StateObject var dashboardList = DashboardConfigRequestExample() // <-- here
var body: some View {
NavigationView {
TabView (){
DashboardScreenExample().tabItem{ Text("Dashboard")}
LoginScreenExample().tabItem{ Text("UpdateInfo")}
}.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(userInfoManager) // <-- here
.environmentObject(dashboardList) // <-- here
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DashboardContentExample: View {
#EnvironmentObject var userInfoManager : UserInfoManagerExample
#EnvironmentObject var dashboardList: DashboardConfigRequestExample // <-- here
func loadRequest(){
dashboardList.loadData(passedLanguage: userInfoManager.preferenceLangauge)
}
//View start from here
var body: some View {
if(dashboardList.dashboardConfig.count == 0){
ProgressView()
.onAppear{
loadRequest()
}
}else{
ScrollView(.horizontal){
LazyHStack{
//Here is the NavigationLink not working
NavigationLink(destination: Text("Hello destination")){ // <-- here for testing
Text("Hello")
}
}
}
}
}
}

pass values dynamically for network request

I have to pass the value of movie.id which is received from a View which is called ReviewView.
I need to pass the movie.id value received in this view to ReviewFetcher and then make a network request using that movie.id. As of now I have hard coded the movie id in ReviewFetcher but I require this to be received from ReviewView and then make a request and then update the list in ReviewView.
Below is the Code:-
ReviewFetcher.swift
import Foundation
import Alamofire
import SwiftUI
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
// #State var movieID:Int
init() {
// self.movieID = movieID
getReviews(movieID : 181812)
}
func getReviews(movieID:Int) {
//self.review.results.removeAll()
let reviewURL = "https://api.themoviedb.org/3/movie/"+String(movieID)+"/reviews?api_key=a18f578d774935ef9f0453d7d5fa11ae&language=en-US&page=1"
Alamofire.request(reviewURL)
.responseJSON { response in
if let json = response.result.value {
if (json as? [String : AnyObject]) != nil {
if let dictionaryArray = json as? Dictionary<String, AnyObject?> {
let json = dictionaryArray
if let id = json["id"] as? Int,
let page = json["page"] as? Int,
let results = json["results"] as? Array<Dictionary<String, AnyObject?>> {
for i in 0..<results.count {
if let author = results[i]["author"] as? String,
let content = results[i]["content"] as? String,
let url = results[i]["url"] as? String {
let newReview = ReviewModel(author: author,
content: content,
url: url)
self.review.results.append(newReview)
}
}
}
}
}
}
}
}
}
ReviewView.swift
import SwiftUI
struct ReviewsView: View {
#State var movie: MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading) {
Text("Review")
.font(.largeTitle)
.bold()
.foregroundColor(Color.steam_rust)
.padding(.leading)
Divider()
// Text(String(fetcher.movieID))
List(fetcher.review.results) { item in
VStack(alignment:.leading) {
Text("Written by : "+item.author)
.font(.body)
.bold()
.padding(.bottom)
Text(item.content)
.font(.body)
.lineLimit(.max)
}
}
}
}
}
MovieModel.swift
import Foundation
import SwiftUI
import Combine
struct MovieArray: Codable {
var page: Int = 0
var total_results: Int = 0
var total_pages: Int = 0
var results: [MovieModel] = []
}
struct MovieModel: Codable, Identifiable {
var id : Int
var original_title: String
var title: String
var original_language:String
var overview: String
var poster_path: String?
var backdrop_path: String?
var popularity: Double
var vote_average: Double
var vote_count: Int
var video: Bool
var adult: Bool
var release_date: String?
}
Remove the init() of your ReviewObserver class. and then call getReviews method in .onAppear modifier of your VStack. The idea of what you need:
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
func getReviews(movieID:Int) {
//you block,, anything you wanna do with movieID.
//Assume you are going to change 'review' variable
}
}
struct ReviewsView: View {
#State var movie:MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading){
Text("Review")
Divider()
Text(String(fetcher.movieID))
List(fetcher.review.results)
{
item in
VStack(alignment:.leading){
Text("Written by : "+item.author)
}
}.onAppear {
self.fetcher.getReviews(movieID: movie.id)
}
}
}

Resources