I am fairly new to iOS development. I am trying to update the property "cars" in the "Sire" model using a stepper. Whenever I press on + or - from the stepper controls, it changes the value by a step and then becomes disabled.
If I bind the stepper to the variable cars, it works flawlessly.
struct AddSireView: View {
// #EnvironmentObject var sireVM:SiresViewModel
#State var newSire = Sire (id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
#State var cars = 0
#State var cups = 0
#State private var state = FormState.idle
var createAction: CreateAction
// TODO: Put validation that the added sire is valid and if not show errors to the user
var body: some View {
Form {
VStack (spacing: 18) {
TitledTextView(text: $newSire.name, placeHolder: "الاسم", title: "الاسم")
TiltedTextEditor(text: Binding<String>($newSire.info)!, title: "معلومات البعير")
TiltedTextEditor(text: Binding<String>($newSire.achievments)!, title: "انجازات البعير")
}
Stepper(value: $newSire.cars, in: 0...10,step:1) {
HStack {
Text ("سيارات:")
TextField("Cars", value: $newSire.cars, formatter: NumberFormatter.decimal)
}
}
And this is the "Sire" struct
struct Sire: Hashable, Identifiable, Decodable {
static func == (lhs: Sire, rhs: Sire) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.ownerID == rhs.ownerID
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
hasher.combine(ownerID)
}
var id:String
var name:String
var ownerID:Int
var fatherID:String?
var info:String?
var achievments:String?
var cars:Int = 0
var cups:Int = 0
init (id:String, name:String, ownerID:Int, info:String? = nil, achievments:String? = nil,
fatherID:String? = nil, cars:Int = 0, cups:Int = 0) {
self.id = id
self.name = name
self.ownerID = ownerID
self.cars = cars
self.cups = cups
self.info = info
self.achievments = achievments
}
}
"Sire" was a class and i made it a Struct thinking that that was the problem, but to no avail.
Consider this approach using an ObservableObject to hold your Sire. This allows you to use
both the Stepper and the Textfield at the same time.
struct ContentView: View {
#StateObject var sireModel = SireModel() // <-- here
var body: some View {
Form {
Stepper(value: $sireModel.sire.cars, in: 0...10, step:1) {
HStack {
Text ("سيارات: ")
TextField("", value: $sireModel.sire.cars, formatter: NumberFormatter())
}
}
}
}
}
class SireModel: ObservableObject {
#Published var sire: Sire = Sire(id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
}
Get rid of the custom implementations for Equatable and Hashable (func == and func hash) you don't include cars in it so SwiftUI doesn't know when to reload.
SwiftUI is all about identity if you change how Swift computes the identity (using Hashable, Equatable and Identifiable) you change the behavior.
Check out Demystify SwiftUI
The video above is the "best" place to learn about the concept.
Related
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")
}
}
}
}
I'm losing my mind over this, please help
I'm following the standford's iOS tutorial, I'm trying to finish an assignment of creating a card games, I have 3 models, Game, Card, Theme and Themes:
Game and Card are in charge of the main game logic
import Foundation
struct Game {
var cards: [Card]
var score = 0
var isGameOver = false
var theme: Theme
var choosenCardIndex: Int?
init(theme: Theme) {
cards = []
self.theme = theme
startTheme()
}
mutating func startTheme() {
cards = []
var contentItems: [String] = []
while contentItems.count != theme.numberOfPairs {
let randomElement = theme.emojis.randomElement()!
if !contentItems.contains(randomElement) {
contentItems.append(randomElement)
}
}
let secondContentItems: [String] = contentItems.shuffled()
for index in 0..<theme.numberOfPairs {
cards.append(Card(id: index*2, content: contentItems[index]))
cards.append(Card(id: index*2+1, content: secondContentItems[index]))
}
}
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
mutating func endGame() {
isGameOver = true
}
mutating func penalizePoints() {
score -= 1
}
mutating func awardPoints () {
score += 2
}
struct Card: Identifiable, Equatable {
static func == (lhs: Game.Card, rhs: Game.Card) -> Bool {
return lhs.content == rhs.content
}
var id: Int
var isFaceUp: Bool = false
var content: String
var isMatchedUp: Bool = false
var isPreviouslySeen = false
}
}
Theme is for modeling different kind of content, Themes is for keeping track which one is currently in use and for fetching a new one
import Foundation
import SwiftUI
struct Theme: Equatable {
static func == (lhs: Theme, rhs: Theme) -> Bool {
return lhs.name == rhs.name
}
internal init(name: String, emojis: [String], numberOfPairs: Int, cardsColor: Color) {
self.name = name
self.emojis = Array(Set(emojis))
if(numberOfPairs > emojis.count || numberOfPairs < 1) {
self.numberOfPairs = emojis.count
} else {
self.numberOfPairs = numberOfPairs
}
self.cardsColor = cardsColor
}
var name: String
var emojis: [String]
var numberOfPairs: Int
var cardsColor: Color
}
import Foundation
struct Themes {
private let themes: [Theme]
public var currentTheme: Theme?
init(_ themes: [Theme]) {
self.themes = themes
self.currentTheme = getNewTheme()
}
private func getNewTheme() -> Theme {
let themesIndexes: [Int] = Array(0..<themes.count)
var visitedIndexes: [Int] = []
while(visitedIndexes.count < themesIndexes.count) {
let randomIndex = Int.random(in: 0..<themes.count)
let newTheme = themes[randomIndex]
if newTheme == currentTheme {
visitedIndexes.append(randomIndex)
} else {
return newTheme
}
}
return themes.randomElement()!
}
mutating func changeCurrentTheme() -> Theme {
self.currentTheme = getNewTheme()
return self.currentTheme!
}
}
This is my VM:
class GameViewModel: ObservableObject {
static let numbersTheme = Theme(name: "WeirdNumbers", emojis: ["1", "2", "4", "9", "20", "30"], numberOfPairs: 6, cardsColor: .pink)
static let emojisTheme = Theme(name: "Faces", emojis: ["🥰", "😄", "😜", "🥳", "🤓", "😎", "😋", "🤩"], numberOfPairs: 8, cardsColor: .blue)
static let carsTheme = Theme(name: "Cars", emojis: ["🚓", "🏎️", "🚗", "🚎", "🚒", "🚙", "🚑", "🚌"], numberOfPairs: 20, cardsColor: .yellow)
static let activitiesTheme = Theme(name: "Activities", emojis: ["🤺", "🏌️", "🏄♂️", "🚣", "🏊♂️", "🏋️", "🚴♂️"], numberOfPairs: -10, cardsColor: .green)
static let fruitsTheme = Theme(name: "Fruits", emojis: ["🍇", "🍉", "🍈", "🍊", "🍋", "🍎", "🍏", "🥭"], numberOfPairs: 5, cardsColor: .purple)
static var themes = Themes([numbersTheme, emojisTheme, carsTheme, fruitsTheme])
static func createMemoryGame() -> Game {
Game(theme: themes.currentTheme!)
}
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
func createNewGame() {
gameController.theme = GameViewModel.themes.changeCurrentTheme()
gameController.startTheme()
}
func choose(_ card: Game.Card) {
objectWillChange.send()
gameController.chooseCard(card)
}
var cards: [Game.Card] {
return gameController.cards
}
var title: String {
return gameController.theme.name
}
var color: Color {
return gameController.theme.cardsColor
}
}
And this is my view:
struct ContentView: View {
var columns: [GridItem] = [GridItem(.adaptive(minimum: 90, maximum: 400))]
#ObservedObject var ViewModel: GameViewModel
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
.padding(.trailing)
}
Section {
VStack {
Text(ViewModel.title)
.foregroundColor(/*#START_MENU_TOKEN#*/.blue/*#END_MENU_TOKEN#*/)
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
}
}
ScrollView {
LazyVGrid(columns: columns ) {
ForEach(ViewModel.cards, id: \.id) { card in
Card(card: card, color: ViewModel.color)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
ViewModel.choose(card)
}
}
}
.font(.largeTitle)
}
.padding()
Text("Score")
.frame(maxWidth: .infinity, minHeight: 30)
.background(Color.blue)
.foregroundColor(/*#START_MENU_TOKEN#*/.white/*#END_MENU_TOKEN#*/)
Spacer()
HStack {
Spacer()
Text("0")
.font(.title2)
.bold()
Spacer()
}
}
}
}
struct Card: View {
let card: Game.Card
let color: Color
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(color)
.foregroundColor(color)
}
else {
shape
.fill(color)
}
}
}
}
Basically the problem lies with the
.onTapGesture {
ViewModel.choose(card)
}
Of the View, when someone taps a card, the isFaceUp property of the Card is changed to true, but this doesn't get reflected in the UI.
If I generate a new view by changing the theme and adding new cards, this works.
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
But when I'm trying to flip a card it doesn't work, the value changes in the Game model but it's not updated on the view
After the tap the ViewModel calls the choose method
func choose(_ card: Game.Card) {
gameController.chooseCard(card)
}
And this changed the value of the Model in the Game.swift file by calling the chooseCard method
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
The values changes but the view does not, the gameController variable of the GameViewModel has the #Published state, which points to an instance of the Game model struct
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
And the view it's accesing this GameViewModel with the #ObservedObject property
#ObservedObject var ViewModel: GameViewModel
I thought I was doing everything right, but I guess not lol, what the heck am I doing wrong? Why can't update my view if I'm using published and observable object on my ViewModel? lol
The main reason the card view doesn't see changes is because in your card view you did put an equatable conformance protocol where you specify an equality check == function that just checks for content and not other variable changes
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
// && lhs.isFaceUp && rhs.isFaceUp //<- you can still add this
}
if you remove the equatable protocol and leave swift to check for equality it should be the minimal change from your base solution.
I would still use the solution where you change the state of the class card so the view can react to changes as an ObservableObject, and the #Published for changes that the view need to track, like this:
class Card: Identifiable, Equatable, ObservableObject {
var id: Int
#Published var isFaceUp: Bool = false
var content: String
#Published var isMatchedUp: Bool = false
var isPreviouslySeen = false
internal init(id: Int, content: String) {
self.id = id
self.content = content
}
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
}
}
and in the Card view the card variable will become
struct Card: View {
#ObservedObject var card: Game.Card
...
}
btw you don't need to notify the view of changes with
objectWillChange.send() if you are already using the #Published notation. every set to the variable will trigger an update.
you could try this instead of declaring Card a class:
Card(card: card, color: ViewModel.color, isFaceUp: card.isFaceUp)
and add this to the Card view:
let isFaceUp: Bool
My understanding is that the Card view does not see any changes to the card (not sure why, maybe because it is in an if),
but if you give it something that has really changed then it is re-rendered. And as mentioned before no need for objectWillChange.send()
EDIT1:
you could also do this in "ContentView":
Card(viewModel: ViewModel, card: card)
and then
struct Card: View {
#ObservedObject var viewModel: GameViewModel
let card: Game.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(viewModel.color)
.foregroundColor(viewModel.color)
}
else {
shape.fill(viewModel.color)
}
}
}
}
Why is my view rendering the model update, before the update has completed? Or at least it seems that way. It seems like, it re-draws based on the previous value of my model.
What I'm doing: When you tap a person cell, it marks that person as "selected" and then changes the foreground color to green. If they are not selected, the foreground color is white.
What is happening: It is important to note that this doesn't ALWAYS happen. It seems random. But occasionally, when I tap on a person cell, the color does not change. And I've dug deeper, and that is because it (looks like) it redraws the view before the value has changed inside my PeopleStore model object.
Here is the code:
struct PersonCell: View {
#EnvironmentObject var peopleStore: PeopleStore
var personId: UUID
var person: Person {
return peopleStore.people.filter({ $0.id == personId }).first!
}
var body: some View {
Button(action: {
print("Tapped Cell: " + peopleStore.personName(id: personId))
peopleStore.toggleSelectionOfPerson(with: personId)
}) {
GeometryReader { geo in
ZStack {
Rectangle().foregroundColor(peopleStore.personIsSelected(id: personId) ? .tGreen : .white).cornerRadius(geo.size.width / 2).frame(height: 70)
Here is what happens when I tap a cell:
class PeopleStore: ObservableObject {
#Published var people : [Person]
init(people: [Person]){
self.people = people
}
func personIsSelected(id: UUID) -> Bool {
let index = people.firstIndex(where: { $0.id == id })!
print("ZSTACK: \(people[index].name) is selected: \(people[index].isSelected.description)")
return people[index].isSelected
}
func toggleSelectionOfPerson(with id: UUID) {
let index = people.firstIndex(where: { $0.id == id })!
print("BEFORE \(people[index].name) is selected: \(people[index].isSelected.description)")
people[index].isSelected = !people[index].isSelected
print("AFTER \(people[index].name) is selected: \(people[index].isSelected.description)")
}
And when I look at my console, you can see that after I select a person, the view redraws, but it has the previous value (In this case, I tapped the Alexandra cell):
Tapped Cell: Alexandra R.
BEFORE Alexandra R. is selected: false
ZSTACK: Jose G. is selected: false
ZSTACK: Jimmy T. is selected: false
ZSTACK: Alexandra R. is selected: false
ZSTACK: Dominic R. is selected: false
AFTER Alexandra R. is selected: true
ZSTACK: Franny E. is selected: false
ZSTACK: Jon B. is selected: true
ZStack: is printed whenever the cell is re-drawn. As you can see, Alexandra is selected: false shows up, when I would expect it to be true.
The timing of the re-draw seems to be just a bit off, and it seems random. What am I missing?
EDIT: Person struct
struct Person: Identifiable {
let id = UUID()
var name: String
var photo: Image
var isFavorite: Bool
var dateFavorited = Date()
var isSelected: Bool = false
}
Edit 2: If it helps, the PersonCell is a subview of something called a PersonList that uses ForEach to generate PersonCells.
struct PeopleList: View {
#EnvironmentObject var peopleStore: PeopleStore
var title: String {
return isFavorites ? "Favorites" : "All Employees"
}
var isFavorites: Bool
var body: some View {
LazyVStack(alignment: .leading, spacing: 15) {
Text(title).padding(.top, 18).padding(.horizontal, 5).font(.regular())
if isFavorites {
ForEach(peopleStore.people.sorted(by: { $0.dateFavorited < $1.dateFavorited }).filter({ $0.isFavorite })) { person in
PersonCell(personId: person.id).frame(height: 70.0)
}
I am not sure what exactly is going wrong at your end, i have tested your code at my end and it’s working perfectly fine. The only change I have done is set var isFavorites: Bool = true in PeopleList because i don’t know how you are toggling that property in your code.So, I have provided it a constant value.
Below is the code -:
struct Person: Identifiable {
let id = UUID()
var name: String
var photo: Image
var isFavorite: Bool
var dateFavorited = Date()
var isSelected: Bool = false
}
class PeopleStore: ObservableObject {
#Published var people : [Person]
init(people: [Person]){
self.people = people
}
func personIsSelected(id: UUID) -> Bool {
let index = people.firstIndex(where: { $0.id == id })!
print("ZSTACK: \(people[index].name) is selected: \(people[index].isSelected.description)")
return people[index].isSelected
}
func personName(id: UUID) -> String {
let index = people.firstIndex(where: { $0.id == id })!
return people[index].name
}
func toggleSelectionOfPerson(with id: UUID) {
let index = people.firstIndex(where: { $0.id == id })!
print("BEFORE \(people[index].name) is selected: \(people[index].isSelected.description)")
people[index].isSelected = !people[index].isSelected
print("AFTER \(people[index].name) is selected: \(people[index].isSelected.description)")
}
}
struct PeopleList: View {
#EnvironmentObject var peopleStore:PeopleStore
var title: String {
return isFavorites ? "Favorites" : "All Employees"
}
var isFavorites: Bool = true
var filteredPersons:[Person]{
peopleStore.people.sorted(by: { $0.dateFavorited < $1.dateFavorited }).filter({ $0.isFavorite })
}
var body: some View {
LazyVStack(alignment: .leading, spacing: 15) {
Text(title).padding(.top, 18)
if isFavorites {
ForEach(filteredPersons) { person in
PersonCell(personId: person.id).frame(height: 70.0)
}
}
}
}
}
struct PersonCell: View {
#EnvironmentObject var peopleStore:PeopleStore
var personId: UUID
var body: some View {
Button(action: {
print("Tapped Cell: " + peopleStore.personName(id: personId))
peopleStore.toggleSelectionOfPerson(with: personId)
}) {
GeometryReader { geo in
ZStack {
Rectangle().foregroundColor(peopleStore.personIsSelected(id: personId) ? .green : .gray).cornerRadius(geo.size.width / 2).frame(height: 70)
}
}
}
}
}
Main View-:
import SwiftUI
#main
struct Test: App {
#StateObject var store:PeopleStore
init() {
let person = Person(name: "foo", photo: Image(""), isFavorite: true)
let person1 = Person(name: "foo1", photo: Image(""), isFavorite: true)
let person2 = Person(name: "foo2", photo: Image(""), isFavorite: true)
let obj = PeopleStore(people: [person,person1,person2])
_store = StateObject(wrappedValue: obj)
}
var body: some Scene {
WindowGroup {
PeopleList().environmentObject(store)
}
}
}
Output-:
Tapped Cell: foo
BEFORE foo is selected: false
AFTER foo is selected: true
ZSTACK: foo2 is selected: false
ZSTACK: foo is selected: true
ZSTACK: foo1 is selected: false
Note-: I have tested multiple times, and didn’t found any issue where selected cell isn’t changing color.
I am making a personal project to study SwiftUI. All was going well, the I noticed a bug on my app.
I have the simple view bellow, that saves a description, a value and some tags on my ViewModel. I am having an issue with the $viewModel.value. That variable is not being filled with values from the view.
I supose that my #Published var value: Double? from my ViewModel should be updated whenever the user types some value. Thing is, it is not updating on any iPhone 11 and up, but it works perfectly on the iPhone 8.
public struct AddBillView: View {
#ObservedObject private var viewModel: AddBillViewModel
#Environment(\.presentationMode) var presentationMode
public let onExpenseCreated: ((_ expense: Expense)->Void)
public var body: some View {
Text("Add Expense")
VStack {
TextField("Descrição", text: $viewModel.name)
HStack {
Text("Valor \(NumberFormatter.currency.currencySymbol)")
CurrencyTextField("Value", value: $viewModel.value)
.multilineTextAlignment(TextAlignment.leading)
}
HStack {
Text("Tags")
TextField("car pets home",
text: $viewModel.tags)
}
Picker("Type", selection: $viewModel.type) {
Text("Paid").tag("Paid")
Text("Unpaid").tag("Unpaid")
Text("Credit").tag("Credit")
}
}.navigationTitle("+ Expense")
Button("Adicionar") {
if !viewModel.hasExpense() {
return
}
onExpenseCreated(viewModel.expense())
self.presentationMode.wrappedValue.dismiss()
}
}
public init(viewModel outViewModel: AddBillViewModel,
onExpenseCreated: #escaping ((_ expense: Expense)->Void)) {
self.viewModel = outViewModel
self.onExpenseCreated = onExpenseCreated
}
}
And I have a ViewModel:
public class AddBillViewModel: ObservableObject {
#Published var name: String = ""
#Published var type: String = "Paid"
#Published var tags: String = ""
#Published var value: Double?
init(expense: Expense?=nil) {
self.name = expense?.name ?? ""
self.type = expense?.type.rawValue ?? "Paid"
self.tags = expense?.tags?.map { String($0.name) }.joined(separator: " ") ?? ""
self.value = expense?.value
}
func hasExpense() -> Bool {
if self.name.isEmpty ||
self.value == nil ||
self.value?.isZero == true {
return false
}
return true
}
func expense() -> Expense {
let tags = self.tags.split(separator: " ").map { Tag(name: String($0)) }
return Expense(name: self.name, value: self.value ?? 0.0 ,
type: ExpenseType(rawValue: self.type)!,
id: UUID().uuidString,
tags: tags)
}
}
Then I use my view:
AddBillView(viewModel: AddBillViewModel()) { expense in
viewModel.add(expense: expense)
viewModel.state = .idle
}
I already google it and spend a couple of hours looking for an answer, with no luck. Someone have any ideas?
Edited
Here is the code for the CurrencyTextField. I`m using this component:
https://github.com/youjinp/SwiftUIKit/blob/master/Sources/SwiftUIKit/views/CurrencyTextField.swift
But the component works perfectly fine on iPhone 8 simulator and with a #State property inside my view. It does not work only with my ViewModel
I figured it out! The problem was that my AddBillViewModel is an ObservableObject and I was marking each property with #Published. This was causing some kind of double observable object.
I removed the #Published and it started working again.
import SwiftUI
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects = [Subjects](repeating: Subjects(name: "", grade: ""), count: 10)
var body: some View {
NavigationView {
Group {
Form {
Section(header: Text("Student details")) {
TextField("Name", text: $name)
TextField("Number of subjects", text: $numberOfSubjects)
}
let count = Int(numberOfSubjects) ?? 0
Text("Count: \(count)")
Section(header: Text("Subject grades")) {
if count>0 && count<10 {
ForEach(0 ..< count, id: \.self) { number in
TextField("Subjects", text: $subjects[number].name)
TextField("Grade", text: $subjects[number].grade)
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.id) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.self) { subject in //works fine with this
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
let details = Details(name: name, subjects: subjects)
students.details.append(details)
}, label: {
Text("Save")
})
)
}
}
}
struct TestStudentView_Previews: PreviewProvider {
static var previews: some View {
TestStudentView()
}
}
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
var name: String
var subjects: [Subjects]
}
struct Subjects: Identifiable, Hashable {
let id = UUID()
var name: String
var grade: String
}
When I use - "ForEach(student.subjects, id:.id) { subject in" under normal circumstances it is supposed to work as id = UUID and the incorrect output is as follows:
then as the class conforms to Identifiable I tried - "ForEach(student.subjects) { subject in" it still does not work correctly. However, when I do - "ForEach(student.subjects, id:.self) { subject in" except I had to have the class conform to hashable and gives me the correct expected output. The correct output which is shown:
You need to use a map instead of repeating.
By using Array.init(repeating:) will invoke the Subjects to initialize only one time, and then insert that object into the array multiple times.
So all, in this case, all id is same.
You can check by just print all id in by this .onAppear() { print(subjects.map({ (sub) in print(sub.id) }))
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects: [Subjects] = (0...10).map { _ in
Subjects(name: "", grade: "")
} //<-- Here