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

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]()
...

Related

SwiftUI: Set a Published value in an ObservableObject from the UI (Picker, etc.)

Update:
This question is already solved (see responses below). The correct way to do this is to get your Binding by projecting the
ObservableObject For example, $options.refreshRate.
TLDR version:
How do I get a SwiftUI Picker (or other API that relies on a local Binding) to immediately update my ObservedObject/EnvironmentObject. Here is more context...
The scenario:
Here is something I consistently need to do in every SwiftUI app I create...
I always make some class that stores any user preference (let's call this class Options and I make it an ObservableObject.
Any setting that needs to be consumed is marked with #Published
Any view that consumes this brings it in as a #ObservedObject or #EnvironmentObject and subscribes to changes.
This all works quite nicely. The trouble I always face is how to set this from the UI. From the UI, here is usually what I'm doing (and this should all sound quite normal):
I have some SwiftUI view like OptionsPanel that drives the Options class above and allows the user to choose their options.
Let's say we have some option defined by an enum:
enum RefreshRate {
case low, medium, high
}
Naturally, I'd choose a Picker in SwiftUI to set this... and the Picker API requires that my selection param be a Binding. This is where I find the issue...
The issue:
To make the Picker work, I usually have some local Binding that is used for this purpose. But, ultimately, I don't care about that local value. What I care about is immediately and instantaneously broadcasting that new value to the rest of the app. The moment I select a new refresh rate, I'd like immediately know that instant about the change. The ObservableObject (the Options class) object does this quite nicely. But, I'm just updating a local Binding. What I need to figure out is how to immediately translate the Picker's state to the ObservableObject every time it's changed.
I have a solution that works... but I don't like it. Here is my non-ideal solution:
The non-ideal solution:
The first part of the solution is quite actually fine, but runs into a snag...
Within my SwiftUI view, rather than do the simplest way to set a Binding with #State I can use an alternate initializer...
// Rather than this...
#ObservedObject var options: Options
#State var refreshRate: RefreshRate = .medium
// Do this...
#ObservedObject var options: Options
var refreshRate: Binding<RefreshRate>(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
So far, this is great (in theory)! Now, my local Binding is directly linked to the ObservableObject. All changes to the Picker are immediately broadcast to the entire app.
But this doesn't actually work. And this is where I have to do something very messy and non-ideal to get it to work.
The code above produces the following error:
Cannot use instance member 'options' within property initializer; property initializers run before 'self' is available
Here my my (bad) workaround. It works, but it's awful...
The Options class provides a shared instance as a static property. So, in my options panel view, I do this:
#ObservedObject var options: Options = .shared // <-- This is still needed to tell SwiftUI to listen for updates
var refreshRate: Binding<RefreshRate>(
get: { Options.shared.refreshRate },
set: { Options.shared.refreshRate = $0 }
)
In practice, this actually kinda works in this case. I don't really need to have multiple instances... just that one. So, as long as I always reference that shared instance, everything works. But it doesn't feel well architected.
So... does anyone have a better solution? This seems like a scenario EVERY app on the face of the planet has to tackle, so it seems like someone must have a better way.
(I am aware some use an .onDisapear to sync local state to the ObservedObject but this isn't ideal either. This is non-ideal because I value having immediate updates for the rest of the app.)
The good news is you're trying way, way, way too hard.
The ObservedObject property wrapper can create this Binding for you. All you need to say is $options.refreshRate.
Here's a test playground for you to try out:
import SwiftUI
enum RefreshRate {
case low, medium, high
}
class Options: ObservableObject {
#Published var refreshRate = RefreshRate.medium
}
struct RefreshRateEditor: View {
#ObservedObject var options: Options
var body: some View {
// vvvvvvvvvvvvvvvvvvvv
Picker("Refresh Rate", selection: $options.refreshRate) {
// ^^^^^^^^^^^^^^^^^^^^
Text("Low").tag(RefreshRate.low)
Text("Medium").tag(RefreshRate.medium)
Text("High").tag(RefreshRate.high)
}
.pickerStyle(.segmented)
}
}
struct ContentView: View {
#StateObject var options = Options()
var body: some View {
VStack {
RefreshRateEditor(options: options)
Text("Refresh rate: \(options.refreshRate)" as String)
}
.padding()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
It's also worth noting that if you want to create a custom Binding, the code you wrote almost works. Just change it to be a computed property instead of a stored property:
var refreshRate: Binding<RefreshRate> {
.init(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
}
If I understand your question correctly, you want
to Set a Published value in an ObservableObject from the UI (Picker, etc.) in SwiftUI.
There are many ways to do that, I suggest you use a ObservableObject class, and use it directly wherever you need a binding in a view, such as in a Picker.
The following example code shows one way of setting up your code to do that:
import Foundation
import SwiftUI
// declare your ObservableObject class
class Options: ObservableObject {
#Published var name = "Mickey"
}
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $optionModel.name) { // <-- use the model directly as a $binding
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}
struct SheetView: View {
#ObservedObject var optionModel: Options // <-- receive the model
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.green) // <-- show updated value
}
}
}
If you really want to have a "useless" intermediate local variable, then use this approach:
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
#State var localVar = "" // <-- the local var
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $localVar) { // <-- using the localVar
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
.onChange(of: localVar) { newValue in
optionModel.name = newValue // <-- update the model
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}

Making struct conform to Identifiable crashes app

EDITS:
removed id: \.self
removed .onDelete from List (mistake in editing)
removed removeRows function
change the Note struct to only hash the id
I am making an app in swiftUI that returns a list of notes. I have a struct, note, that is defined as follows:
struct Note: Identifiable, Hashable {
let id = UUID()
var title: String
var content: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
It conforms to Identifiable so that it can be selected inside a List, and it conforms to Hashable so that it can be held inside a Set (for selection).
My view that (I think) is being super slow and eventually crashing is as follows:
struct SearchView: View {
#State private var searchValue: String = ""
#State private var searchResults: [Note] = [
Note(title: "Hi", content: "whats up"),
Note(title: "wassup", content: "hi")
]
#State private var selectKeeper = Set<Note>()
var body: some View {
VStack(alignment: .leading) {
SearchInputView(searchValue: $searchValue, searchResults: $searchResults)
List(searchResults, selection: $selectKeeper) { note in
Text(note.title )
}
}
}
}
SearchInputView is basically just a view that uses combine to run this function on the contents of a textfield every keypress:
func findIn(notes: [Note], pattern: String) -> [Note] {
if pattern.isEmpty { return notes }
var matchedNotes: [Note] = []
for note in notes {
let note = Note(title: note.title.uppercased(), content: note.content.uppercased())
let pattern = pattern.uppercased()
if note.title.contains(pattern) || note.content.contains(pattern) {
matchedNotes.append(note)
}
}
return matchedNotes
}
This all used to work fine with a ForEach loop and the Note struct not conforming to Identifiable, but right now for whatever reason, the app crashes as soon as I type anything into the text box. I have no idea why this is happening, and I can't see anything in the profilers that might tell me what's going on. The only thing is that as soon as I type all of the uses goes up to 100% and the app crashes. Any ideas?
Thanks!
I figured out that my problem was due to putting the findIn function outside of the struct that was using it. I am not at all sure why this is, but putting it inside of the struct calling it caused it to work out fine. Thanks so much to everyone who provided helpful suggestions in the comments!

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.

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....