#AppStorage not updating view - swift

I have the following reproduced example in Swift/SwiftUI.
Intended Functionality: List of numbers, where upon deletion of any number, the numbers update and reorganize so that they are in consecutive order.
Example: Suppose we have numbers 1, 2, 3, 4 in a list. When we delete number 2, the list should become 1, 2, 3 instead of staying like 1, 3, 4.
Problem: When #State is used to hold the array of numbers, the code works as expected. However, when an array in #AppStorage is used to hold the array, the view does not seem to update/change the numbers.
So, why can't I use the #AppStorage approach, and how can I fix this?
I know the code looks like a lot, but you can mostly ignore it if you just read the comments. It's just a body with a couple of functions, nothing crazy.
class MyObj: Codable, Identifiable { //SIMPLE OBJECT TO HOLD A NUMBER
init(num: Int) {
self.num = num
}
var id: UUID = UUID()
var num: Int
}
struct test2: View {
#State var array: [MyObj] = [] //USING THIS (#State) WORKS
//AppStorage("array") var array: [MyObj] = [] //USING THIS (#AppStorage) DOESN’T UPDATE NUMBERS
func minimizeNums() {
for i in 0..<array.count { //MAKES NUMBERS IN ORDER/CONSECUTIVE
array[i].num = i
}
}
var body: some View {
VStack {
Button("add number object") {
array.append(MyObj(num: array.count))
}
List {
ForEach(array) { obj in
Text(String(obj.num))
}
.onDelete(perform: { index in
array.remove(atOffsets: index) //REMOVES NUMBER OBJECT FROM LIST
minimizeNums() //MAKES THE REMAINING OBJECT'S NUMBERS CONSECUTIVE
})
}
}
}
}
Important: I used the extension from this accepted answer in order to store arrays in #AppStorage. I assume this extension may be contributing to the problem, but I'm not sure how!

This is failing, because you are using a class for your model MyObj. There are multiple reasons for using structs with SwiftUI. Please read Blog entry or any other tutorial or documentation.
[TLDR]:
Don´t use classes use structs.
Changing MyObj to:
struct MyObj: Codable, Identifiable {
init(num: Int) {
self.num = num
}
var id: UUID = UUID()
var num: Int
}
should work.

Related

Binding to subscript doesn't update TextField (macOS)

I have this Store struct, which is a wrapper for all my data. It has a subscript operator which takes in a UUID, and returns the associated object.
This way, I can have a List bind to a selection variable, which has type UUID, and then in another view I can access the selected object from that UUID.
However, I'm experiencing an issue where my TextField which binds to the Store doesn't update. It does update if I wrap it in another Binding, or if I instead just use Text.
Here is an minimal reproducible example:
struct Person: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct Store {
var data: [Person]
subscript(id: Person.ID) -> Person {
get {
data.first(where: { $0.id == id })!
}
set {
data[data.firstIndex(where: { $0.id == id })!] = newValue
}
}
}
struct ContentView: View {
#State var store = Store(data: [
Person(name: "Joe"),
Person(name: "Eva"),
Person(name: "Sam"),
Person(name: "Mary")
])
#State var selection: Person.ID?
var body: some View {
NavigationView {
List(store.data, selection: $selection) {
Text($0.name)
}
if let selection = selection {
// Creating a new Binding which simply wraps $store[selection].name
// fixes this issue. Or just using Text also works.
TextField("Placeholder", text: $store[selection].name)
}
else {
Text("No Selection")
}
}
}
}
To reproduce this issue, just click different names on the Sidebar. For some reason the detail view's TextField doesn't update!
This issue can also be resolved if we simply move the Store to a ObservableObject class with #Published.
Also, making the Store conform to Hashable doesn't help this issue.
I feel like I'm missing something very basic with SwiftUI. Is there any way to fix this?
EDIT:
I've changed out Store for an [Person], and I made an extension with the same subscript operator that is in Store. However, the problem still remains!
try this:
TextField("Placeholder", text: $store[selection].name)
.id(selection) // <-- here

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

Not possible to loop over PHFetchResult with ForEach

I'm currently working on a photos app for iOS / macOS and i'm struggeling with PhotoKit.
I did create a class where i manage all my PhotoKit requests.
class PhotosAPI: ObservableObject {
#Published var all = PHFetchResult<PHAsset>()
#Published var allAlbums = PHFetchResult<PHAssetCollection>()
#Published var allSmartAlbums = PHFetchResult<PHAssetCollection>()
// Functions to get the Collections / Assets
}
This part is working so far but now i'm struggeling with showing those data in my View.
In my View i would like to present all Assets in a List / Grid
struct ShowImages: View {
#ObservedObject var photos = PhotosAPI()
var body: some View {
List(photos.all, id: \.self) { item in
Text("\(item)")
}
}
}
But i do get an error "Initializer 'init(_:id:rowContent:)' requires that 'PHFetchResult' conform to 'RandomAccessCollection'" and i did try all day today to fix this but i wasn't successful and i couldn't find anything useful in google.
Does anyone have an idea how i can get PHFetchResults to loop over them?
At the end i was able to show the pictures with below code. But this looks like very bad code to me. I would prefer to loop directly over the PHFetchResult. Does anyone know how i can get this done?
ForEach(0..<photos.all.count) { index in
Text("\(photos.all.object(at: index).localIdentifier)")
}
You can implement RandomAccessCollection for PHFetchResult or create wrapper that conforms RandomAccessCollection.
struct PHFetchResultCollection: RandomAccessCollection, Equatable {
typealias Element = PHAsset
typealias Index = Int
let fetchResult: PHFetchResult<PHAsset>
var endIndex: Int { fetchResult.count }
var startIndex: Int { 0 }
subscript(position: Int) -> PHAsset {
fetchResult.object(at: fetchResult.count - position - 1)
}
}
Then you will be able to use PHFetchResultCollection with ForEach
let collection = PHFetchResultCollection(fetchResult: fetchResult)
var body: some View {
ForEach(collection, id: \.localIdentifier) {
...
}
}
You can enumerate the items of the fetched results object with the enumerate methods, such as this one:
https://developer.apple.com/documentation/photokit/phfetchresult/1620999-enumerateobjects

Change the options in a Picker dynamically using distinct arrays

I'm trying to get a Picker to update dynamically depending on the selection of the prior Picker. In order to achieve this, I'm using a multidimensional array. Unfortunately this seems to confuse my ForEach loop and I noticed the following message in the logs:
ForEach<Range<Int>, Int, Text> count (3) != its initial count (5).ForEach(:content:)should only be used for *constant* data. Instead conform data toIdentifiableor useForEach(:id:content:)and provide an explicitid!
This kinda makes sense, I'm guessing what is happening is that I'm passing it one array and it keeps referring to it, so as far as it is concerned, it keeps changing constantly whenever I pass it another array. I believe the way to resolve this is to use the id parameter that can be passed to ForEach, although I'm not sure this would actually solve it and I'm not sure what I would use. The other solution would be to somehow destroy the Picker and recreate it? Any ideas?
My code follows. If you run it, you'll notice that moving around the first picker can result in an out of bounds exception.
import SwiftUI
struct ContentView: View {
#State private var baseNumber = ""
#State private var dimensionSelection = 1
#State private var baseUnitSelection = 0
#State private var convertedUnitSelection = 0
let temperatureUnits = ["Celsius", "Fahrenheit", "Kelvin"]
let lengthUnits = ["meters", "kilometers", "feet", "yards", "miles"]
let timeUnits = ["seconds", "minutes", "hours", "days"]
let volumeUnits = ["milliliters", "liters", "cups", "pints", "gallons"]
let dimensionChoices = ["Temperature", "Length", "Time", "Volume"]
let dimensions: [[String]]
init () {
dimensions = [temperatureUnits, lengthUnits, timeUnits, volumeUnits]
}
var convertedValue: Double {
var result: Double = 0
let base = Double(baseNumber) ?? 0
if temperatureUnits[baseUnitSelection] == "Celsius" {
if convertedUnitSelection == 0 {
result = base
} else if convertedUnitSelection == 1 {
result = base * 9/5 + 32
} else if convertedUnitSelection == 2 {
result = base + 273.15
}
}
return result
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Enter a number", text: $baseNumber)
.keyboardType(.decimalPad)
}
Section(header: Text("Select the type of conversion")) {
Picker("Dimension", selection: $dimensionSelection) {
ForEach(0 ..< dimensionChoices.count) {
Text(self.dimensionChoices[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
Group {
Section(header: Text("Select the base unit")) {
Picker("Base Unit", selection: $baseUnitSelection) {
ForEach(0 ..< self.dimensions[self.dimensionSelection].count) {
Text(self.dimensions[self.dimensionSelection][$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Select the unit to convert to")) {
Picker("Converted Unit", selection: $convertedUnitSelection) {
ForEach(0 ..< self.dimensions[self.dimensionSelection].count) {
Text(self.dimensions[self.dimensionSelection][$0])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
Section(header: Text("The converted value is")) {
Text("\(convertedValue) \(dimensions[dimensionSelection][convertedUnitSelection])")
}
}.navigationBarTitle("Unit Converter")
}
}
}
I hate to answer my own question, but after spending some time on it, I think it's worth summarizing my findings in case it helps somebody. To summarize, I was trying to set the second Picker depending on what the selection of the first Picker was.
If you run the code that I pasted as is, you will get an out of bounds. This is only the case if I set #State private var dimensionSelection = 1 and the second array is larger than the first array. If you start with smaller array, you will be fine which you can observe by setting #State private var dimensionSelection = 0. There are a few ways to solve this.
Always start with the smallest array (Not great)
Instead of using an array of String, use an array of objects implementing Identifiable. this is the solution proposed by fuzz above. This got past the out of bound array exception. In my case though, I needed to specify the id parameter in the ForEach parameters.
Extend String to implement Identifiable as long as your strings are all different (which works in my trivial example). This is the solution proposed by gujci and his proposed solution looks much more elegant than mine, so I encourage you to take a look. Note that this to work in my own example. I suspect it might be due to how we built the arrays differently.
HOWEVER, once you get past these issues, it will still not work, You will hit an issue that appears be some kind of bug where the Picker keep adding new elements. My impression is that to get around this, one would have to destroy the Picker every time, but since I'm still learning Swift and SwiftUI, I haven't gotten round doing this.
So you'll want to make sure according to Apple's documentation that the array elements are Identifiable as you've mentioned.
Then you'll want to use ForEach like this:
struct Dimension: Identifiable {
let id: Int
let name: String
}
var temperatureUnits = [
Dimension(id: 0, name: "Celsius"),
Dimension(id: 1, name: "Fahrenheit"),
Dimension(id: 2, name: "Kelvin")
]
ForEach(temperatureUnits) { dimension in
Text(dimension.name)
}

Binding an element of an array of an ObservableObject : 'subscript(_:)' is deprecated

I'm using an ObservableObject 'DataStore', which contains an array ('exampleList') of objects ('exampleObject').
#Published exampleList = [exampleObject]()
I'm calling the DataStore via #EnvironmentObject ('dataStore').
#EnvironmentObject var dataStore = DataStore()
Then I iterate the list with
ForEach(0..<dataStore.exampleList.count) { index in ....
To bind element of item to a detail view, I'm doing like this:
DetailView(itemBinding: $dataStore.exampleList[index])
Until Xcode11 beta 4, it worked perfectly. Since XCode11 beta 5, it still works but Xcode gives me this alert:
'subscript(_:)' is deprecated: See Release Notes for a migration path
I tried with simpler stuff, with a simple #State var containing an array of strings, and it's the same issue: when calling an element of this array, and trying to use the value into a TextField:
TextField("test", text: $test[0])
I get the same alert.
I don't understand how to fix it. Does that mean that we no longer can bind values inside an array?
Then, how can we iterate an array and bind a specific item?
This is my first question on Stack Overflow, I apologize if my question is clumsy...
Thanks a lot for your answers, I'm using Stack Overflow for years, it's amazing, I always find existing and helpful answers, but it is the first time I can't find any, that's why I'm asking.
Xcode 11, beta 6 UPDATE:
Good news! Just as I suspected, in beta 6, the Binding conformance to MutableCollection has been been replaced with something else. Instead of conforming to MutableCollection, it now let your access the elements via #dynamicMemberLookup. The result is you now can keep doing $text[3] and no longer get a warning! It seems this question can be closed now.
Xcode 11, beta 5. Old answer:
I finally got some time to investigate this a little. As I mentioned in the comments, I think it would be wise to wait until the Collection conformance is completely removed (or replaced with something else). But just to satisfy our curiosity, I have created an extension on Binding, that I think does what the current Collection conformance does. The only difference is that, instead of accessing through a subscript, I implemented a function called element(_ idx: Int) to get a Binding<T> to the element.
If one day the conformance is completely removed, I may change the implementation, and conform to Collection myself. I cannot do it now, because it would conflict with the existent (and deprecated) implementation. For the time being, I think this demonstrate how to handle the warnings if you absolutely want to get rid of them.
Just to be clear. I am not using this code. As long as I can still access the elements through the subscript, I will still do it and ignore the warnings. This is just for academic purposes.
The extension is:
extension Binding where Value: MutableCollection, Value.Index == Int {
func element(_ idx: Int) -> Binding<Value.Element> {
return Binding<Value.Element>(
get: {
return self.wrappedValue[idx]
}, set: { (value: Value.Element) -> () in
self.wrappedValue[idx] = value
})
}
}
And it can be used like this:
struct MainView: View {
#Binding var text: [String]
var body: some View {
TextField("", text: $text.element(0))
TextField("", text: $text.element(1))
TextField("", text: $text.element(2))
}
}
I had to bind the array of an observable object recently, didn't get any warnings on stable XCode11. I did it like this
struct ScheduleTimer: Identifiable {
var id: Int
var name: String
var start: Date
var end: Date
var isActive: Bool
}
struct ScheduleView: View {
#ObservedObject var scheduleController = ScheduleController()
var body: some View {
NavigationView {
Form {
ForEach(scheduleController.timers) { timer in
ScheduleForm(scheduleController: self.scheduleController, timer: timer)
}
}
}
}
}
struct ScheduleForm: View {
#ObservedObject var scheduleController: ScheduleController
var timer: ScheduleTimer
var scheduleIndex: Int {
scheduleController.timers.firstIndex(where: { $0.id == timer.id })!
}
#State var start = Date()
var body: some View {
Section(header: Text(self.scheduleController.timers[scheduleIndex].name)){
DatePicker("From", selection: self.$scheduleController.timers[scheduleIndex].start, displayedComponents: .hourAndMinute)
DatePicker("To", selection: self.$scheduleController.timers[scheduleIndex].end, displayedComponents: .hourAndMinute)
Toggle(isOn: self.$scheduleController.timers[scheduleIndex].isActive) {
Text("")
}.toggleStyle(DefaultToggleStyle())
}
}
}
class ScheduleController: ObservableObject {
#Published var timers = [ScheduleTimer]()
...