I in the process of converting my application to SwiftUI but have encountered a problem. I know what the root cause is but can’t seem to figure out how to work around it.
I am using Parse live queries to update my ViewModel which contains an array of custom objects. Everything works as expected when adding/removing objects from the server the UI updates as expected however when I am trying to update an object parameter the UI doesn’t reflect the change although the object updates successfully in the ViewModel.
The code below is a simplified version of my code but shows the issue.
Hopefully someone can shed some light or point me to the right direction.
struct Candidate : ParseObject, Equatable {
static func == (lhs: ParseCandidate, rhs: ParseCandidate) -> Bool {
return lhs.objectId == rhs.objectId
} // <- I believe this is causing the issue
var objectId : String?
var name : String?
var status : String?
}
class CandidatesViewModel : ObservableObject {
#Published candidatesList = [Candidates]()
init() {
//1. get objects from server
//2. subscribe to live queries
}
// this method get called when an update is received from the server
func updateCandiate(_ candidate : Candidate) {
if let index = candidatesList.first(where: {$0 == candidate}) {
candidatesList[index] = candidate
}
}
}
struct CandidateListView : View {
#StateObject var viewModel = CandidatesViewModel()
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 180,maximum: 180),spacing: 20) ], alignment: .leading, spacing: 30, pinnedViews: [.sectionHeaders]) {
Section {
ForEach(viewModel.candidatesList, id: \.self){ candidate in
CandidateCard(candidate: candidate)
}
}header: {
Text("Header")
}.padding(.horizontal)
}
}
}
struct CandidateCardView : View {
let candidate : Candidate
var body : some View {
VStack{
Text(candidate.name!)
Text(candidate.status!)
}
}
}
I have noticed if I removed the equatable a new instance gets added to the array as swift assumes its a new object because all properties have to match.
the only way to reflect the update so far is if I make the candidate inside the CandidateCardView #Binding then the update is reflected but seems unnecessary and complicates things.
Any guidance is highly appreciated.
SwiftUI uses Equatable conformance to determine whether an object has changed. In your case, you're using the objectId to determine equality, so it won't register as updated when an different field changes. To update when another property changes, include that in the ==, or just use the synthesised version, which includes all properties. You should also make it Identifiable
struct Candidate : ParseObject, Equatable, Identifiable {
var id: String { objectId ?? UUID().uuidString }
var objectId : String? // Can this _really_ be nil?
var name : String?
var status : String?
}
In that case, in updateCandiate you'll need to update to {$0.objectId == candidate.objectId}
class CandidatesViewModel : ObservableObject {
#Published candidatesList = [Candidates]()
// this method get called when an update is received from the server
func updateCandiate(_ candidate : Candidate) {
if let index = candidatesList.first(where: {$0.objectId == candidate.objectId}) {
candidatesList[index] = candidate
}
}
}
and now Candidate is Identifiable
struct CandidateListView : View {
#StateObject var viewModel = CandidatesViewModel()
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 180,maximum: 180),spacing: 20) ], alignment: .leading, spacing: 30, pinnedViews: [.sectionHeaders]) {
Section {
ForEach(viewModel.candidatesList){ candidate in
CandidateCard(candidate: candidate)
}
}header: {
Text("Header")
}.padding(.horizontal)
}
}
}
Related
I faced the problem when NavTestChildView called more one times. I don't understand what going wrong. I tested on a real device with iOS 16.0.3 and emulator Xcode 14.0.1
I replaced original code to give more info about the architecture why I create NavTestService into navigationDestination.
enum NavTestRoute: Hashable {
case child(Int)
}
class NavTestService: ObservableObject {
let num: Int
init(num: Int) {
self.num = num
print("[init][NavTestService]")
}
deinit {
print("[deinit][NavTestService]")
}
}
struct NavTestChildView: View {
#EnvironmentObject var service: NavTestService
init() {
print("[init][NavTestChildView]")
}
var body: some View {
Text("NavTestChildView \(service.num)")
}
}
struct NavTestMainView2: View {
var body: some View {
VStack {
ForEach(1..<10, id: \.self) { num in
NavigationLink(value: NavTestRoute.child(num)) {
Text("Open child \(num)")
}
}
}
}
}
struct NavTestMainView: View {
var body: some View {
NavigationStack {
NavTestMainView2()
.navigationDestination(for: NavTestRoute.self) { route in
switch route {
case let .child(num):
NavTestChildView().environmentObject(NavTestService(num: num))
}
}
}
}
}
logs:
[init][NavTestChildView]
[init][NavTestService]
[deinit][NavTestService]
[init][NavTestChildView]
[init][NavTestService]
Looks like there is a period when instance of NavTestService is not held by anyone and it leaves the heap. In practice this would hardly ever happen because .environmentObject vars are usually held somewhere up the hierarchy.
If you change NavTestMainView accordingly:
struct NavTestMainView: View {
let navTestService = NavTestService()
var body: some View {
NavigationStack {
NavigationLink(value: NavTestRoute.child) {
Text("Open child")
}
.navigationDestination(for: NavTestRoute.self) { route in
switch route {
case .child:
NavTestChildView().environmentObject(navTestService)
}
}
}
}
}
... you get no deinits and no extra init as well. The console will output:
[init()][NavTestService]
[init()][NavTestChildView]
[init()][NavTestChildView]
Also note that if you comment out let navTestService = NavTestService() and wrap NavTestChildView().environmentObject(NavTestService()) in LazyView you'll get the following output:
[init()][NavTestChildView]
[init()][NavTestService]
Where LazyView is:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
It's not "firing" it's just initing the View struct multiple times which is perfectly normal and practically zero overhead because View structs are value types. It tends to happen because UIKit's event driven design doesn't align well with SwiftUI's state driven design.
You can simplify your code by replacing the router enum / case statement with multiple navigationDestination for each model type.
I everyone! I spent hours looking for something that I guess very simple but I can not managed to find the best way...
I have my body view :
var body: some View {
VStack {
// The CircularMenu
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}
}
Which contains a circular menu. Each click on a menu item calls :
func buttonClickHandler(_ index: Int) {
/// Your actions here
switch index {
//Thermometer
case 0:
print("0")
//Light
case 1:
print("1")
//Video
case 2:
print("2")
//Alarm
case 3:
print("3")
//Car
case 4:
self.destinationViewType = .car
self.nextView(destination: .car)
default:
print("not found")
}
}
I want to perform a simple view transition to another view called Car. nextView function looks like this :
func nextView(destination: DestinationViewType) {
switch destination {
case .car: Car()
}
}
I thought that was simple like this but I get : Result of 'Car' initializer is unused on the case line.
So someone knows how to achieve that ? Thanks a lot in advance!
Here's one way to do it:
Create a struct called IdentifiableView which contains an AnyView and an id:
struct IdentifiableView: Identifiable {
let view: AnyView
let id = UUID()
}
Create a #State var to hold the nextView. Use .fullScreenCover(item:) to display the nextView
#State private var nextView: IdentifiableView? = nil
var body: some View {
VStack {
// The CircularMenu
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}.fullScreenCover(item: self.$nextView, onDismiss: { nextView = nil}) { view in
view.view
}
}
Then, assign self.nextView the IdentifiableView:
case .car: self.nextView = IdentifiableView(view: AnyView(Car()))
When it's time to return to the MenuView use self.presentationMode.wrappedValue.dismiss() to dismiss the view. Here is an example of a minimal Car view:
struct Car: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Text("Car View").onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
}
}
If you want to completely replace the body content with the new view, you need some condition about that. Let's say you have a Container with a body and if there is a Car view created we will display it:
struct Container: View {
#State var car: Car? // Storage for optional Car view
var body: some View {
if let car = car { // if the car view exists
car // returning the car view
} else { // otherwise returning the circular menu
VStack {
CircularMenu(menuItems: homeMenuItems, menuRadius: 55, menuButtonSize: 55, menuButtonColor: .black, buttonClickCompletion: buttonClickHandler(_:))
.buttonStyle(PlainButtonStyle())
}
}
}
...
And then we only need to assign the newly created instance of the car view on click:
...
func buttonClickHandler(_ index: Int) {
switch index {
....
//Car
case 4:
car = Car() // assigning the newly created instance
...
}
}
}
I see that you have mentioning of destinationViewTyp and some other cases. So your code will be slightly more complex than this, but the idea keeps the same. We store either a view or some information that helps us create a view when necessary and then returning either a picker or a view depending on condition.
I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.
This question is a bit of a follow on from SwiftUI: How to get continuous updates from Slider
Basically I have a slider which is one of a number of sliders. Each on changes a parameter on a model class so I'm passing in a binding which represents a specific property on the model class. This works in that the model gets the new value each time the slider moves.
struct AspectSlider: View {
private var value: Binding<Double>
init(value: Binding<Double>, hintKey: String) {
self.value = value
}
var body: some View {
VStack(alignment: .trailing) {
Text("\(self.value.value)")
Slider(value: Binding<Double>(getValue: { self.value.value }, setValue: { self.value.value = $0 }),
from: 0.0, through: 4.0, by: 0.5)
}
}
}
What isn't working correctly is the Text("\(self.value.value)") display which is meant to show the current value of the slider. It's not updating when the Binding<Double> value changes.
Instead it only updates when something else on the display triggers a display refresh. In my case the label that represents the result of the calculating performed by the model (which doesn't necessarily change when a slider changes it's value).
I've confirmed that the model is getting changes so the binding is updating. My question is why is the Text label not updating immediately.
Ok, I've worked out why my code wasn't updating as it should. It came down to my model which looks like this (Simple version):
final class Calculator: BindableObject {
let didChange = PassthroughSubject<Int, Never>()
var total: Int = 0
var clarity: Double = 0.0 { didSet { calculate() }}
private func calculate() {
if newValue.rounded() != Double(total) {
total = Int(newValue)
didChange.send(self.total)
}
}
}
What was happening was that the value on the sider was only updating when this model executed the didChange.send(self.total) line. I think, if I've got this right, that because the label is watching a binding, only when the binding updates does the label update. Makes sense. Still working this out.
I guess it part of learning about Combine and how it works :-)
Your code, if called like this, works fine:
struct ContentView: View {
#State private var value: Double = 0
var body: some View {
AspectSlider(value: $value, hintKey: "hint")
}
}
struct AspectSlider: View {
private var value: Binding<Double>
init(value: Binding<Double>, hintKey: String) {
self.value = value
}
var body: some View {
VStack(alignment: .trailing) {
Text("\(self.value.value)")
Slider(value: Binding<Double>(getValue: { self.value.value }, setValue: { self.value.value = $0 }),
from: 0.0, through: 4.0, by: 0.5)
}
}
}
Note that you can also take advantage of the property wrapper #Binding, to avoid using self.value.value. Your implementation should change slightly:
struct AspectSlider: View {
#Binding private var value: Double
init(value: Binding<Double>, hintKey: String) {
self.$value = value
}
var body: some View {
VStack(alignment: .trailing) {
Text("\(self.value)")
Slider(value: Binding<Double>(getValue: { self.value }, setValue: { self.value = $0 }),
from: 0.0, through: 4.0, by: 0.5)
}
}
}
I have a dictionary that contains various values I want to "filter" by. So I'm doing something like this
struct ExampleView : View {
#EnvironmentObject var externalData : ExternalData
var body: some View {
VStack {
ForEach(externalData.filters) { (v : (String, Bool)) in
Toggle(isOn: $externalData.filters[v.0], label: {
Text("\(v.0)")
})
}
}
}
}
final class ExternalData : BindableObject {
let didChange = PassthroughSubject<ExternalData, Never>()
init() {
filters["Juniper"] = true
filters["Beans"] = false
}
var filters : Dictionary<String, Bool> = [:] {
didSet {
didChange.send(self)
}
}
}
This question seems related, but putting dynamic didn't seem to help and I wasn't able to figure out how to do that NSObject inheritance thing in this case. Right now, this code as is gives me this error:
Cannot subscript a value of type 'Binding<[String : Bool]>' with an argument of type 'String'
But trying to move the $ around or use paren in various ways doesn't seem to help. How can I bind the toggles to the values in my dictionary? I could just make manual toggles for each value, but that makes fragile code since (among other reasons) the potential filter values are based on a dataset that might have new values at some point.
I'm aware that I should really sort the keys (in some way) before iterating over them so the ordering is consistent, but that clutters this example so I left that code out.
I managed to make is work by using a custom binding for each filter.
final class ExternalData: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var filters: Dictionary<String, Bool> = [:] {
didSet {
didChange.send(())
}
}
init() {
filters["Juniper"] = true
filters["Beans"] = false
}
var keys: [String] {
return Array(filters.keys)
}
func binding(for key: String) -> Binding<Bool> {
return Binding(getValue: {
return self.filters[key] ?? false
}, setValue: {
self.filters[key] = $0
})
}
}
The keys property list the filters keys as String so that it can be displayed (using ForEach(externalData.keys))
The binding(for:) method, create a custom Binding for the given key. This binding is given to the Toggle to read/write the current value in the wrapped dictionary.
The view code:
struct ExampleView : View {
#EnvironmentObject var externalData : ExternalData
var body: some View {
VStack {
ForEach(externalData.keys) { key in
Toggle(isOn: self.externalData.binding(for: key)) {
Text(key)
}
}
}
}
}