Declare array property with #published var inside same ObservableObject - swift

I'm trying to declare Array with a #Published var property. However, the value is never updated (Always the default -> 0).
How can I do that ? What am I doing wrong ?
Thank you very much for your help ;)
Have a nice day
struct MyCategory: Identifiable, Hashable {
let id = UUID()
let name: String
let amount: Double
}
class MyViewModel: ObservableObject {
#Published var amount1: Double = 0
#Published var amount2: Double = 0
#Published var categories = [MyCategory]()
init() {
self.categories = [
MyCategory(name: "Test 1", amount: self.amount1),
MyCategory(name: "Test 2", amount: self.amount2)
]
}
func updateAmounts(value1: Double, value2: Double) {
self.amount1 += value1
self.amount2 += value2
}
}
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ForEach(myViewModel.categories, id: \.self) { category in
HStack() {
Text("\(category.name)")
Text("\(category.amount)") // Value not refresh! 0
}
}
}
}
}

Related

SwiftUI: Missing argument for parameter 'x' in call

My content view has a StateObject to a data model "Pilot." The complier generates an error of "Missing argument for parameter 'pilot' in call. Xcode offers the following fix: var pilot = Pilot(pilot: [Employee])... however, the complier generates a new error on that fix: "Cannot convert value of type '[Employee].Type' to expected argument type '[Employee]'"
Here is my Content View:
struct ContentView: View {
#StateObject var pilot = Pilot(pilot: [Employee])
var body: some View {
NavigationView {
ZStack {
Color.gray.ignoresSafeArea()
.navigationBarHidden(true)
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
EmployeeView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.padding()
}
.environmentObject(pilot)
}
}
}
}
Here is my data model:
class Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct Data {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var data: Data {
Data(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
}
class Data: Employee {
static let sampleData: [Employee] = [
Employee(age: 35, yearGroup: 1, category: "B717")
]
}
I am also getting a similar compiler error in my Content view for the "Employee View()" which states: "Missing argument for parameter 'data' in call"
Here is my EmployeeView code:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let data: [Employee]
var body: some View {
ZStack {
List {
ForEach(data) {line in
EmployeeCardView(employee: line)
}
}
}
}
}
/// UPDATE1 ///
I tried to pass an instance of Employee to Pilot but I've hit a new wall. Here is my new code.
Here is my data model:
struct Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct UserInfo {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var userInfo: UserInfo {
UserInfo(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
let pilotInfo = Employee(age: 35, yearGroup: 1, category: "B717")
}
And here is my Content view:
struct ContentView: View {
#StateObject var pilot = Pilot(pilotInfo) //<error here>
Now getting an error in the Content view: "Cannot find 'pilotInfo' in scope"
/// UPDATE 2 ///
I removed the UserInfo section of the model data and followed the guidance to take one Employee (not an array) and change the Content view variable. That fixed those associated errors.
In an effort to comply with principles outlined in developer.apple.com/tutorials/app-dev-training/displaying-data-in-a-list, I've tried to match the following from Apple's tutorial:
struct ScrumsView: View {
let scrums: [DailyScrum]
var body: some View {
List {
ForEach(scrums, id: \.title) { scrum in
CardView(scrum: scrum)
.listRowBackground(scrum.theme.mainColor)
}
}
}
}
That's why my Employee view looks like this:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let userInfo: [Pilot]
var body: some View {
ZStack {
List {
ForEach(userInfo, id: \.age) {line in //ERRORs
EmployeeCardView(employee: line)
}
}
}
}
}
The complier errors are:
Cannot convert value of type '[Pilot]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
Key path value type '' cannot be converted to contextual type ''
///UPDATE 3///
struct ProfileFormView: View {
#EnvironmentObject var pilot: Pilot
#StateObject var vm: EmployeeViewModel = EmployeeViewModel()
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {
DatePicker("Birthdate", selection: $vm.birthdate, displayedComponents: .date)
DatePicker("New Hire Date", selection: $vm.newHireDate, displayedComponents: .date)
Picker("Your Current Aircraft", selection: $vm.chosenAircraft) {
ForEach(vm.currentAircraft, id: \.self) {
Text ($0)
}
}
}
}
}
}
}
As you can see, my first attempt was much more complex but since I could not get my previous version views to take user input updates, I decided to start over with a more basic app to better learn the fundamentals of data management.

Binding #Binding / #State to the #Publisher to decouple VM and View Layer

I want to decouple my ViewModel and View layer to increase testability of my Views.
Therefore, I want to keep my property states inside view and only init them as I needed.
But I cannot initialize my #Binding or #States with #Published properties. Is there a way to couple them inside init function?
I just add example code below to
instead of
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#ObservedObject vM = ViewModel()
var body: some View {
Button(action: { vM.int += 1; print(int) }, label: {
Text("Button")
})
}
}
I want to achieve this without using #ObservedObject inside my view.
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ObservedObject<ViewModel> = ObservedObject(wrappedValue: ViewModel())) {
// str: Binding<String> and viewModel.str: Published<String>.publisher
// type so that I cannot bind my bindings to viewModel. I must accomplish
// this by using #ObservedObject but this time my view couples with ViewModel
_str = viewModel.wrappedValue.$str
_int = viewModel.wrappedValue.$int
print("ViewCreated")
}
}
// Testing Init
ContentView(str: Binding<String>, int: Binding<Int>)
// ViewModel Init
ContentView(viewModel: ViewModel)
This way I can't bind them each other, I just want to bind my binding or state properties to published properties.
I have realized that by Binding(get:{}, set{}), I can accomplish that. if anyone want to separate their ViewModel and View layer, they can use this approach:
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ViewModel = ViewModel()) {
_str = Binding ( get: { viewModel.str }, set: { viewModel.str = $0 } )
_int = Binding ( get: { viewModel.int }, set: { viewModel.int = $0 } )
print("ViewCreated")
}
}

How to change #Published var affects another #Published var like Binding

Is there a way to make #Published var in a way that changes it will affect another #Published var like a Binding var?
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ForEach(viewModel.items, id: \.self){ item in
Button("Select \(item.title)"){
viewModel.selectedItem = item
viewModel.selectedItem.title = "grape"
print(viewModel.items) // <-- This will print [Test.Item(title: "Apple"), Test.Item(title: "Banana"), Test.Item(title: "Orange")]
}
}
}
}
struct Item: Hashable {
var title: String
}
class ViewModel: ObservableObject {
#Published var items: [Item] = [Item(title: "Apple"), Item(title: "Banana"), Item(title: "Orange")]
#Published var selectedItem: Item = Item(title: "default") //<-- I want this to be a binding of an item in the bucket above, so what I modify to selectedItem will affect item in the bucket.
}
Three possible solutions -- the last one might be closest to what you're asking for in terms of one #Published reacting to another.
Here's a version using didSet:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ForEach(viewModel.items, id: \.self){ item in
Button("Select \(item.title)"){
viewModel.selectedItem = item
viewModel.selectedItem?.title = "grape"
print(viewModel.items)
}
}
}
}
struct Item: Hashable {
var id = UUID()
var title: String
}
class ViewModel: ObservableObject {
#Published var selectedItem : Item? {
didSet { //when there's a new value, see if it should be mapped into the original item set
self.items = self.items.map {
if let selectedItem = selectedItem, selectedItem.id == $0.id {
return selectedItem
}
return $0
}
}
}
#Published var items: [Item] = [Item(title: "Apple"), Item(title: "Banana"), Item(title: "Orange")]
}
Here's one possibility, using a custom Binding:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ForEach(viewModel.items, id: \.self){ item in
Button("Select \(item.title)"){
viewModel.selectedItemID = item.id
viewModel.selectedItem?.wrappedValue.title = "grape"
print(viewModel.items)
}
}
}
}
struct Item: Hashable {
var id = UUID()
var title: String
}
class ViewModel: ObservableObject {
#Published var selectedItemID : UUID?
#Published var items: [Item] = [Item(title: "Apple"), Item(title: "Banana"), Item(title: "Orange")]
var selectedItem : Binding<Item>? {
guard let selectedItemID = selectedItemID else {
return nil
}
return .init { () -> Item in
self.items.first(where: {$0.id == selectedItemID}) ?? Item(title: "")
} set: { (item) in
self.items = self.items.map {
if $0.id == selectedItemID { return item }
return $0
}
}
}
}
And, lastly, a version using Combine:
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ForEach(viewModel.items, id: \.self){ item in
Button("Select \(item.title)"){
viewModel.selectedItem = item
viewModel.selectedItem?.title = "grape"
print(viewModel.items)
}
}
}
}
struct Item: Hashable {
var id = UUID()
var title: String
}
class ViewModel: ObservableObject {
#Published var selectedItem : Item?
#Published var items: [Item] = [Item(title: "Apple"), Item(title: "Banana"), Item(title: "Orange")]
private var cancellable : AnyCancellable?
init() {
cancellable = $selectedItem
.compactMap { $0 } //remove nil values
.sink(receiveValue: { (newValue) in
self.items = self.items.map {
newValue.id == $0.id ? newValue : $0 //if the ids match, return the new value -- if not, return the old one
}
})
}
}
All three have the same functionality -- it really just depends on what style of coding you prefer. I might personally go for the last option, as Combine and SwiftUI tend to work really well together. It's also easy to extend with filtering, etc.

Difficulty Updating String With a TextField in SwiftUI

I am currently having trouble modifying a String value using a TextField. Here is my (simplified) code so far:
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
}
var courses: [Course] {
// initialization code...
}
struct GradeCalculatorView: View {
#State var selectedCourseIndex: Int = 0
var body: some View {
VStack {
// allows user to select a course:
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
}, label: {
Text(courses[i].name)
})
}
CourseView(course: courses[selectedCourseIndex])
}
}
}
struct CourseView: View {
#ObservedObject var course: Course // passed in from GradeCalculatorView
var body: some View {
VStack(alignment: .leading) {
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
}.padding(.leading).frame(alignment: .leading)
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: HorizontalAlignment.leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id:\.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name)
}
}
I cannot seem to modify the GradeItem object's name using the TextField. When the TextField is edited, its text changes temporarily. However, when the GradeItemRow View is reloaded, it displays the GradeItem object's original name, rather than its updated name.
Would somebody please be able to help?
Thanks in advance
UPDATE: As per your requests, I have added more context to this sample code.
I know that this does not work, as when I attempt to modify a GradeItem's name with a TextField, it changes temporarily. However, when I select a different course and then the course I was initially on, the TextField displays the unmodified name value.
The following test works.
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
init() {
self.name = "gradeItem" + UUID().uuidString
self.scoredPoints = 0.0
self.totalPoints = 0.0
self.isUserCreated = false
}
}
class Course: ObservableObject {
#Published var name: String
#Published var categories: [GradeCategory]
init(name: String, categories: [GradeCategory]) {
self.name = name
self.categories = categories
}
init() {
self.name = "course_" + UUID().uuidString
self.categories = [GradeCategory]()
self.categories.append(GradeCategory())
}
}
class GradeCategory: ObservableObject {
#Published var name: String
#Published var items: [GradeItem]
init(name: String, items: [GradeItem]) {
self.name = name
self.items = items
}
init() {
self.name = "category_" + UUID().uuidString
self.items = [GradeItem]()
self.items.append(GradeItem())
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id: \.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct CourseView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var course: Course // passed in from ContentView
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("done")
})
Spacer()
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
Spacer()
}.padding(.leading).frame(alignment: .leading)
}
}
struct ContentView: View {
#State var courses: [Course] = [Course(), Course()]
#State var selectedCourseIndex: Int = 0
#State var showCourse = false
var body: some View {
VStack {
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
self.showCourse = true
}, label: {
Text(self.courses[i].name)
})
}
}.sheet(isPresented: self.$showCourse) {
CourseView(course: self.courses[self.selectedCourseIndex])
}
}
}

How to model data using Combine & SwiftUI

I've been learning Swift & SwiftUI and all has been going well until very recently. I have successfully used #Published properties to keep my data & views in sync. However, I now want to display some data that is a combination of several #Published properties.
The simplified data model class:
import Foundation
final class GameAPI: ObservableObject {
struct PlayerStats: Identifiable, Codable {
var gamesPlayed: Int
var wins: Int
var losses: Int
}
struct Player: Identifiable, Codable {
var id = UUID()
var name: String
var stats: PlayerStats
}
struct Room: Identifiable, Codable {
var id = UUID()
var name: String
var players: [Player]
}
struct ServerStats {
var totalGamesPlayed: Int
var totalPlayers: Int
var totalPlayersOnline: Int
}
#Published var players: [Player] = []
#Published var rooms: [Room] = []
func addPlayer(name: String) {
players.append(Player(
name: name,
stats: PlayerStats(
gamesPlayed: 0,
wins: 0,
losses: 0
)
))
}
func removePlayer(id: UUID) { ... }
func updatePlayerStats(playerId: UUID, stats: PlayerStats) { ... }
func getLeaderboard() -> [Player] {
return players.sorted({ $0.stats.wins > $1.stats.wins }).prefix(10)
}
func getServerStats() -> ServerStats {
return ServerStats(
totalGamesPlayed: ...,
totalPlayers: ...,
totalPlayersOnline: ...,
)
}
}
View:
import SwiftUI
struct LeaderboardTabView: View {
#EnvironmentObject var gameAPI: GameAPI
var body: some View {
VStack {
Text("TOP PLAYERS")
Leaderboard(model: gameAPI.getLeaderboard())
// ^^^ How can I make the view automatically refresh when players are added/removed or any of the player stats change?
}
}
}
How can I wire up my views to leaderboard & server stats data so that the view refreshes whenever the data model changes?