I am trying to have a picker list all of a type, called Course and then let the user select the appropriate course when adding a new Assignment to the managed object context. The picker selection binding (courseIndex) isn't updated when the user taps a row in the picker view. I'm not entirely sure how to fix the issue, nor do I know what is causing it. Any help is appreciated!
Here is the affected code:
struct NewAssignmentView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Course.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Course.name, ascending: true)]) var courses: FetchedResults<Course>
#State var name = ""
#State var hasDueDate = false
#State var dueDate = Date()
#State var courseIndex = 0
var body: some View {
NavigationView {
Form {
TextField("Assignment Name", text: $name)
Section {
Picker(selection: $courseIndex, label:
HStack {
Text("Course: ")
Spacer()
Text(self.courses[self.courseIndex].name ?? "").foregroundColor(self.courses[self.courseIndex].color).bold()
})
{
ForEach(self.courses, id: \.self) { course in
Text("\(course.name ?? "")").foregroundColor(course.color).tag(course)
}
}
}
Section {
Toggle(isOn: $hasDueDate.animation()) {
Text("Due Date")
}
if hasDueDate {
DatePicker(selection: $dueDate, displayedComponents: .date, label: { Text("Set Date:") })
}
}
}
[...]
When using optional binding values it's important that you explicitly provide the optional wrapping for the tag values because Swift doesn't automatically unwrap it for you and fails to equate a non-optional value with an optional one.
#Binding var optional: String?
Picker("Field", selection: $optional) {
// None option.
Text("None").tag(String?.none)
// Other fields.
ForEach(options) { option in
Text(option).tag(String?.some(option))
}
}
I cannot make your snapshot compilable, so just changed, here... I assume that as your selection is index, you have to use ranged ForEach instead, like
ForEach(0 ..< self.courses.count) { i in
Text("\(self.courses[i].name ?? "")").foregroundColor(self.courses[i].color).tag(i)
}
PS. not sure about tag usage, probably it might be not needed.
Related
The problem
TL;DR: A String I'm trying to bind to inside TextField is nested in an Optional type, therefore I cannot do that in a straightforward manner. I've tried various fixes listed below.
I'm a simple man and my use case is rather simple - I want to be able to use TextField to edit my object's name.
The difficulty arises due to the fact that the object might not exist.
The code
Stripping the code bare, the code looks like this.
Please note that that the example View does not take Optional into account
model
struct Foo {
var name: String
}
extension Foo {
var sampleData: [Foo] = [
Foo(name: "Bar")
]
}
view
again, in the perfect world without Optionals it would look like this
struct Ashwagandha: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
var body: some View {
TextField("", text: $ashwagandhaVM.currentFoo.name)
}
}
view model
I'm purposely not unwrapping the optional, making the currentFoo: Foo?
class AshwagandhaVM: ObservableObject {
#Published var currentFoo: Foo?
init() {
self.currentFoo = Foo.sampleData.first
}
}
The trial and error
Below are the futile undertakings to make the TextField and Foo.name friends, with associated errors.
Optional chaining
The 'Xcode fix' way
TextField("", text: $ashwagandhaVM.currentFoo?.name)
gets into the cycle of fixes on adding/removing "?"/"!"
The desperate way
TextField("Change chatBot's name", text: $(ashwagandhaVM.currentFoo!.name) "'$' is not an identifier; use backticks to escape it"
Forced unwrapping
The dumb way
TextField("", text: $ashwagandhaVM.currentFoo!.name)
"Cannot force unwrap value of non-optional type 'Binding<Foo?>'"
The smarter way
if let asparagus = ashwagandhaVM.currentFoo.name {
TextField("", text: $asparagus.name)
}
"Cannot find $asparagus in scope"
Workarounds
My new favorite quote's way
No luck, as the String is nested inside an Optional; I just don't think there should be so much hassle with editing a String.
The rationale behind it all
i.e. why this question might be irrelevant
I'm re-learning about the usage of MVVM, especially how to work with nested data types. I want to check how far I can get without writing an extra CRUD layer for every property in every ViewModel in my app. If you know any better way to achieve this, hit me up.
Folks in the question comments are giving good advice. Don't do this: change your view model to provide a non-optional property to bind instead.
But... maybe you're stuck with an optional property, and for some reason you just need to bind to it. In that case, you can create a Binding and unwrap by hand:
class MyModel: ObservableObject {
#Published var name: String? = nil
var nameBinding: Binding<String> {
Binding {
self.name ?? "some default value"
} set: {
self.name = $0
}
}
}
struct AnOptionalBindingView: View {
#StateObject var model = MyModel()
var body: some View {
TextField("Name", text: model.nameBinding)
}
}
That will let you bind to the text field. If the backing property is nil it will supply a default value. If the backing property changes, the view will re-render (as long as it's a #Published property of your #StateObject or #ObservedObject).
I think you should change approach, the control of saving should remain inside the model, in the view you should catch just the new name and intercept the save button coming from the user:
class AshwagandhaVM: ObservableObject {
#Published var currentFoo: Foo?
init() {
self.currentFoo = Foo.sampleData.first
}
func saveCurrentName(_ name: String) {
if currentFoo == nil {
Foo.sampleData.append(Foo(name: name))
self.currentFoo = Foo.sampleData.first(where: {$0.name == name})
}
else {
self.currentFoo?.name = name
}
}
}
struct ContentView: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
#State private var textInput = ""
#State private var showingConfirmation = false
var body: some View {
VStack {
TextField("", text: $textInput)
.padding()
.textFieldStyle(.roundedBorder)
Button("save") {
showingConfirmation = true
}
.padding()
.buttonStyle(.bordered)
.controlSize(.large)
.tint(.green)
.confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
Button("Yes") {
confirmAndSave()
}
Button("No", role: .cancel) { }
}
//just to check
if let name = ashwagandhaVM.currentFoo?.name {
Text("in model: \(name)")
.font(.largeTitle)
}
}
.onAppear() {
textInput = ashwagandhaVM.currentFoo?.name ?? "default"
}
}
func confirmAndSave() {
ashwagandhaVM.saveCurrentName(textInput)
}
}
UPDATE
do it with whole struct
struct ContentView: View {
#StateObject var ashwagandhaVM = AshwagandhaVM()
#State private var modelInput = Foo(name: "input")
#State private var showingConfirmation = false
var body: some View {
VStack {
TextField("", text: $modelInput.name)
.padding()
.textFieldStyle(.roundedBorder)
Button("save") {
showingConfirmation = true
}
.padding()
.buttonStyle(.bordered)
.controlSize(.large)
.tint(.green)
.confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
Button("Yes") {
confirmAndSave()
}
Button("No", role: .cancel) { }
}
//just to check
if let name = ashwagandhaVM.currentFoo?.name {
Text("in model: \(name)")
.font(.largeTitle)
}
}
.onAppear() {
modelInput = ashwagandhaVM.currentFoo ?? Foo(name: "input")
}
}
func confirmAndSave() {
ashwagandhaVM.saveCurrentName(modelInput.name)
}
}
There is a handy Binding constructor that converts an optional binding to non-optional, use as follows:
struct ContentView: View {
#StateObject var store = Store()
var body: some View {
if let nonOptionalStructBinding = Binding($store.optionalStruct) {
TextField("Name", text: nonOptionalStructBinding.name)
}
else {
Text("optionalStruct is nil")
}
}
}
Also, MVVM in SwiftUI is a bad idea because the View data struct is better than a view model object.
I've written a couple generic optional Binding helpers to address cases like this. See this thread.
It lets you do if let unwrappedBinding = $optional.withUnwrappedValue { or TestView(optional: $optional.defaulting(to: someNonOptional).
I have a CoreData plants entity, a model with a text value, a masterView and a simple detailView
Model:
class TextItem: Identifiable {
var id: String
var text: String = ""
init() {
id = UUID().uuidString
}
}
class RecognizedContent: ObservableObject {
#Published var items = [TextItem]()
}
List in MasterView:
List(recognizedContent.items, id: \.id) { textItem in
NavigationLink(destination: TextPreviewView(text: textItem.text)
.environment(\.managedObjectContext, viewContext)) {
if let item = plants.first(where: {$0.wrappedID == textItem.text}) {
PlantRow(item: item)
}
Text(String(textItem.text))
}
struct TextPreviewView: View {
var text: String
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Plant.timestamp, ascending: true)],
animation: .default)
private var plants: FetchedResults<Plant>
var body: some View {
VStack {
Text(text)
.font(.body)
.padding()
ForEach(plants.filter({$0.wrappedID.range(of: String(text), options: .caseInsensitive) != nil}), id: \.self) { plant in
PlantRow(item: plant)
Text(plant.visualId ?? "")
}
}
}
}
plants has a wrappedID (String) with a certain value ("Benjamin" in this case). When I manually replace String(text) in the ForEach filter with "Benjamin" it filters out the correct value and shows the row. Whenever I let text be filled in from the Model, it doesn't find anything. When I, on ButtonClick, cast the text in recognizedContent.items.text to a #State variable and feed that into the filter, it immediately shows the PlantRows.
I've also changed the ForEaches to all possible ways. Whenever I feed in a 'normal' string, the filter works. However, Text(String(textItem.text)) also always renders and shows correct string.
Is it a problem in the ObservedObject?
I'm trying to update a view in Swift and I can't figure out how to make it work. My app has questions, which are loaded from Core data. From there, a random question should be displayed at the top. After saving the answer (by pressing the Button with action: save), a new random question should be displayed.
struct RecordView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Question.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Question.question, ascending: false)])
var questions: FetchedResults<Question>
var currentQuestion: String { return questions.randomElement()!.question! }
#State private var newEntryText = ""
var body: some View {
VStack {
Section(header: Text(currentQuestion)){
TextField("New entry", text: self.$newEntryText)
.padding(100)
HStack {
SwiftSpeech.RecordButton().scaleEffect(0.8).swiftSpeechToggleRecordingOnTap(locale: Locale(identifier: "de"), animation: .spring(response: 0.3, dampingFraction: 0.5, blendDuration: 0))
.onRecognize(update: self.$newEntryText)
Button(action: save)
{
Image(systemName: "plus.circle.fill").foregroundColor(.green).imageScale(.large).scaleEffect(2.0)
}
}
}.automaticEnvironmentForSpeechRecognition()
}
}
func save() {
let newEntry = Entry(context: self.moc)
newEntry.text = self.newEntryText
newEntry.createdAt = Date()
do {
try self.moc.save()
}catch{
print(error)
}
self.newEntryText = ""
print(currentQuestion)
}
What I tried:
1) #State var currentQuestion: String = questions.randomElement()!.question!-> Cannot use instance member 'questions' within property initializer; property initializers run before 'self' is available. Here the problems seems to be that the questions array has to be loaded first.
2) var currentQuestion: String { return questions.randomElement()!.question! } -> Here the currentQuestion is recomputed every time it is accessed, but the View does not update. Same thing if I move the questions.randomElement()!.question! to the Text() component.
3) lazy var currentQuestion = questions.randomElement()!.question!-> Cannot use mutating getter on immutable value: 'self' is immutable (at the Text() component). The lazy part should have solved the problem I have at the 1) solution, but then I cannot use it at the Text() component.
... and some other minor variations. I'm a Swift/Swift UI Beginner, and I am running out of ideas how to update the displayed current question everytime the button is pressed. Does anyone has an idea for this?
Many thanks!
Try the following (scratchy)
#State var currentQuestion: String = "" // as state !!
var body: some View {
VStack {
Section(header: Text(currentQuestion)){
// ... other your code here
}.automaticEnvironmentForSpeechRecognition()
}.onAppear {
self.nextQuestion() // << here !!
}
}
...
func save() {
// ... other your code here
self.nextQuestion() // << here !!
}
private func nextQuestion() {
self.currentQuestion = questions.randomElement()?.question ?? ""
}
I’m trying to select the entity from CoreData storage, but the Picker is not functional — it does not show the selected entity.
My code:
import SwiftUI
struct SampleView: View {
#FetchRequest(entity: Aircraft.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Aircraft.model, ascending: true)]) var aircrafts: FetchedResults<Aircraft>
#State private var selection: UUID? = UUID()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selection, label: Text("Picker")) {
ForEach(aircrafts, id: \.self) { aircraft in
Text(aircraft.model ?? "Unknown")
}
}
}
}
}
}
}
Result:
Type of selection should be the same to what is selected or to tag. In your case I assume it can be as follows
#State private var selection: Aircraft? = nil
try with .tag as below
ForEach(aircrafts, id: \.self) { aircraft in
Text(aircraft.model ?? "Unknown").tag(aircraft as Aircraft?)
}
Updated (optional specific): according to comment of #user3687284
I can't undertand how to use #Binding in combination with ForEach in SwiftUI. Let's say I want to create a list of Toggles from an array of booleans.
struct ContentView: View {
#State private var boolArr = [false, false, true, true, false]
var body: some View {
List {
ForEach(boolArr, id: \.self) { boolVal in
Toggle(isOn: $boolVal) {
Text("Is \(boolVal ? "On":"Off")")
}
}
}
}
}
I don't know how to pass a binding to the bools inside the array to each Toggle. The code here above gives this error:
Use of unresolved identifier '$boolVal'
And ok, this is fine to me (of course). I tried:
struct ContentView: View {
#State private var boolArr = [false, false, true, true, false]
var body: some View {
List {
ForEach($boolArr, id: \.self) { boolVal in
Toggle(isOn: boolVal) {
Text("Is \(boolVal ? "On":"Off")")
}
}
}
}
}
This time the error is:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires
that 'Binding' conform to 'Hashable'
Is there a way to solve this issue?
⛔️ Don't use a Bad practice!
Most of the answers (including the #kontiki accepted answer) method cause the engine to rerender the entire UI on each change and Apple mentioned this as a bad practice at wwdc2021 (around time 7:40)
✅ Swift 5.5
From this version of swift, you can use binding array elements directly by passing in the bindable item like:
⚠️ Note that Swift 5.5 is not supported on iOS 14 and below but at least check for the os version and don't continue the bad practice!
You can use something like the code below. Note that you will get a deprecated warning, but to address that, check this other answer: https://stackoverflow.com/a/57333200/7786555
import SwiftUI
struct ContentView: View {
#State private var boolArr = [false, false, true, true, false]
var body: some View {
List {
ForEach(boolArr.indices) { idx in
Toggle(isOn: self.$boolArr[idx]) {
Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
}
}
}
}
}
Update for Swift 5.5
struct ContentView: View {
struct BoolItem: Identifiable {
let id = UUID()
var value: Bool = false
}
#State private var boolArr = [BoolItem(), BoolItem(), BoolItem(value: true), BoolItem(value: true), BoolItem()]
var body: some View {
NavigationView {
VStack {
List($boolArr) { $bi in
Toggle(isOn: $bi.value) {
Text(bi.id.description.prefix(5))
.badge(bi.value ? "ON":"OFF")
}
}
Text(boolArr.map(\.value).description)
}
.navigationBarItems(leading:
Button(action: { self.boolArr.append(BoolItem(value: .random())) })
{ Text("Add") }
, trailing:
Button(action: { self.boolArr.removeAll() })
{ Text("Remove All") })
}
}
}
Previous version, which allowed to change the number of Toggles (not only their values).
struct ContentView: View {
#State var boolArr = [false, false, true, true, false]
var body: some View {
NavigationView {
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) { idx in
Toggle(isOn: self.$boolArr[idx]) {
Text(self.boolArr[idx] ? "ON":"OFF")
}
}
.navigationBarItems(leading:
Button(action: { self.boolArr.append(true) })
{ Text("Add") }
, trailing:
Button(action: { self.boolArr.removeAll() })
{ Text("Remove All") })
}
}
}
In SwiftUI, just use Identifiable structs instead of Bools
struct ContentView: View {
#State private var boolArr = [BoolSelect(isSelected: true), BoolSelect(isSelected: false), BoolSelect(isSelected: true)]
var body: some View {
List {
ForEach(boolArr.indices) { index in
Toggle(isOn: self.$boolArr[index].isSelected) {
Text(self.boolArr[index].isSelected ? "ON":"OFF")
}
}
}
}
}
struct BoolSelect: Identifiable {
var id = UUID()
var isSelected: Bool
}
In WWDC21 videos Apple clearly stated that using .indices in the ForEach loop is a bad practice. Besides that, we need a way to uniquely identify every item in the array, so you can't use ForEach(boolArr, id:\.self) because there are repeated values in the array.
As #Mojtaba Hosseini stated, new to Swift 5.5 you can now use binding array elements directly passing the bindable item. But if you still need to use a previous version of Swift, this is how I accomplished it:
struct ContentView: View {
#State private var boolArr: [BoolItem] = [.init(false), .init(false), .init(true), .init(true), .init(false)]
var body: some View {
List {
ForEach(boolArr) { boolItem in
makeBoolItemBinding(boolItem).map {
Toggle(isOn: $0.value) {
Text("Is \(boolItem.value ? "On":"Off")")
}
}
}
}
}
struct BoolItem: Identifiable {
let id = UUID()
var value: Bool
init(_ value: Bool) {
self.value = value
}
}
func makeBoolItemBinding(_ item: BoolItem) -> Binding<BoolItem>? {
guard let index = boolArr.firstIndex(where: { $0.id == item.id }) else { return nil }
return .init(get: { self.boolArr[index] },
set: { self.boolArr[index] = $0 })
}
}
First we make every item in the array identifiable by creating a simple struct conforming to Identifiable. Then we make a function to create a custom binding. I could have used force unwrapping to avoid returning an optional from the makeBoolItemBinding function but I always try to avoid it. Returning an optional binding from the function requires the map method to unwrap it.
I have tested this method in my projects and it works faultlessly so far.