I'm following every guide I've seen online to bring in the managed object to the SwiftUI scene with environment and then run a #FetchRequest in the scene, all of it works great.
I can use that result to populate the value of a picker
Heres what I have
CoreData Object
public class CD_LookupData: NSManagedObject,Identifiable {}
extension CD_LookupData {
#nonobjc public class func fetchRequest() -> NSFetchRequest<CD_LookupData> {
return NSFetchRequest<CD_LookupData>(entityName: "CD_LookupData")
}
#NSManaged public var lookluptable: String?
#NSManaged public var primarykeyid: String?
#NSManaged public var value: String?
}
SwiftUI View
struct LabledSelectBoxCD: View {
var label: String
var lookupdata: FetchedResults<CD_LookupData>
#Binding var value: Int
var body: some View
{
VStack(alignment: .leading)
{
Picker(selection: $value, label: Text(""))
{
ForEach(lookupdata, id: \.primarykeyid)
{ data in
Text(data.value ?? "Unknown")
}
}
}
}
}
Its populates the picker with the values just fine but my default value never works and no value selected is saved.
If I try the same view with just an array of strings it works perfectly.
Any ideas on how I can get it to use the value of primarykeyid for the value of the picker?
Update:
#Binding var value: String
and
Text(data.value ?? "Unknown").tag(data.primarykeyid)
Don't make any changes
You need to make your value an optional String because that is what the type primaryKeyId is
#Binding var value: String?
And then you need a tag on each element of the picker to set the selection:
Text(data.value ?? "Unknown").tag(data.primaryKeyId)
Related
I am working on a registration screen which has a ViewModel attached to it.
The ViewModel is just a class which extends my registration screen class, where I place all of my business logic.
extension RegistrationScreen {
#MainActor class ViewModel : ObservableObject {
//business logic goes here
}
}
The ViewModel has #Published variables to represent two states for every text field in the screen: text and validationText.
#Published var first: String = ""
#Published var firstValidationMessage: String = ""
within that ViewModel I have a call to a helper function from another class which checks to see if the field's text is empty and if so it can set the fields validation text to an error, that looks like this:
class FieldValidation: Identifiable {
func isFieldEmptyAndSetError(fieldText:String, fieldValidationText:Binding<String>) -> Bool {
if(fieldText.isEmpty){
fieldValidationText.wrappedValue = "Required"
return true
}else{
fieldValidationText.wrappedValue = ""
return false
}
}
}
to call that function from my viewModel I do the following:
FieldValidation().isFieldEmptyAndSetError(fieldText:first,fieldValidationText: firstValidationMessage)
This is throwing runtime errors which are hard to decrypt for me since I am new to Xcode and iOS in general.
My question is, how can I get this passing by reference to work? alternatively if the way I am doing is not possible please explain what is going on.
It's kind of weird having a function that receives a #Published as a parameter, if you want to handle all the validations in the viewModel, this seems as an easy solution for Combine. I would suggest you to create an extension to validate if the string is empty, since .isEmpty does not trim the text.
The class
class FieldValidation: Identifiable {
static func isFieldEmptyAndSetError(fieldText:String) -> String {
return fieldText.isEmpty ? "Required" : ""
}
}
The view Model
import Foundation
import Combine
class viewModel: ObservableObject {
#Published var text1 = ""
#Published var text2 = ""
#Published var text3 = ""
#Published var validationText1 = ""
#Published var validationText2 = ""
#Published var validationText3 = ""
init() {
self.$text1.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText1)
self.$text2.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText2)
}
func validateText(value: String) {
self.validationText3 = FieldValidation.isFieldEmptyAndSetError(fieldText: value)
}
}
The view would look like this:
import SwiftUI
struct ViewDate: View {
#StateObject var myviewModel = viewModel()
var body: some View {
VStack{
Text("Fill the next fields:")
TextField("", text: $myviewModel.text1)
.border(Color.red)
Text(myviewModel.validationText1)
TextField("", text: $myviewModel.text2)
.border(Color.blue)
Text(myviewModel.validationText2)
TextField("", text: $myviewModel.text3)
.border(Color.green)
.onChange(of: myviewModel.text3) { newValue in
if(newValue.isEmpty){
self.myviewModel.validateText(value: newValue)
}
}
Text(myviewModel.validationText3)
}.padding()
}
}
I've created a property wrapper that I want to insert some logic into and the "set" value is doing the right thing, but the textfield isn't updating with all uppercase text. Shouldn't the text field be showing all uppercase text or am I misunderstanding how this is working?
Also this is a contrived example, my end goal is to insert a lot more logic into a property wrapper, I'm just using the uppercase example to get it working. I've searched all over the internet and haven't found a working solution.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var vm = FormDataViewModel()
var body: some View {
Form {
TextField("Name", text: $vm.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
}
#propertyWrapper
public class Capitalized {
#Published var value: String
public var wrappedValue: String {
get { value }
set { value = newValue.uppercased() } //Printing this shows all caps
}
public var projectedValue: AnyPublisher<String, Never> {
return $value
.eraseToAnyPublisher()
}
public init(wrappedValue: String) {
value = wrappedValue
}
}
SwiftUI watches #Published properties in #StateObject or #ObservedObject and triggers UI update on changes of them.
But it does not go deep inside the ObservableObject. Your FormDataViewModel does not have any #Published properties.
One thing you can do is that simulating what #Published will do on value changes.
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
private var nameObserver: AnyCancellable?
init() {
nameObserver = _name.$value.sink {_ in
self.objectWillChange.send()
}
}
}
Please try.
This can be done with standard #Published that seems simpler and more reliable.
Here is a solution. Tested with Xcode 12 / iOS 14.
class FormDataViewModel: ObservableObject {
#Published var name: String = "" {
didSet {
let capitalized = name.uppercased()
if name != capitalized {
name = capitalized
objectWillChange.send()
}
}
}
}
Following this cheat sheet I'm trying to figure out data flow in SwiftUI. So:
Use #Binding when your view needs to mutate a property owned by an ancestor view, or owned by an observable object that an ancestor has a reference to.
And that is exactly what I need so my embedded model is:
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
init(values: [String] = []) {
self.values = values
}
}
and my View has two fields:
struct SimpleModelView: View {
#Binding var model: SimpleModel
#Binding var strings: [String]
var body: some View {
VStack {
HStack {
Text(self.strings[0])
TextField("name", text: self.$strings[0])
}
HStack {
Text(self.model.values[0])
EmbeddedView(strings: self.$model.values)
}
}
}
}
struct EmbeddedView: View {
#Binding var strings: [String]
var body: some View {
VStack {
TextField("name", text: self.$strings[0])
}
}
}
So I expect the view to change Text when change in input field will occur. And it's working for [String] but does not work for embedded #Binding object:
Why it's behaving differently?
Make property published
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
and model observed
struct SimpleModelView: View {
#ObservedObject var model: SimpleModel
Note: this in that direction - if you introduced ObservableObject then corresponding view should have ObservedObject wrapper to observe changes of that observable object's published properties.
In SimpleModelView, try changing:
#Binding var model: SimpleModel
to:
#ObservedObject var model: SimpleModel
#ObservedObjects provide binding values as well, and are required if you want state changes from classes conforming to ObservableObject
I have a DataBase handled by DataCore. I am trying to retrieve any object of "assignment" and insert it to a View List. The assignment class itself is identifiable but I am getting an error while trying to create the List in the View :
Initializer 'init(_:id:rowContent:)' requires that 'Set<NSManagedObject>' conform to 'RandomAccessCollection'
Is the set itself is not identifiable even though the objects are identifiable ? How can I present all the objects in the set inside the View's list ?
The view:
import SwiftUI
struct AssignmentList: View {
#ObservedObject var assignmentViewModel = assignmentViewModel()
var body: some View {
VStack {
NavigationView {
//**The error is in the following line : **
List(assignmentViewModel.allAssignments, id: \.self) { assignment in
AssignmentRow(assignmentName: assignment.assignmentName, notes: assignment.notes) //This view works by itself and just present the data as text under HStack
}
.navigationBarTitle(Text("Assignments"))
}
Button(action: {
self.assignmentViewModel.retrieveAllAssignments()
}) {
Text("Retrieve")
}
}
}
}
This is the assignment class:
import Foundation
import CoreData
extension Assignment: Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Assignment> {
return NSFetchRequest<Assignment>(entityName: "Assignment")
}
#NSManaged public var id: UUID?
#NSManaged public var assignmentName: String?
#NSManaged public var notes: String?
}
This is the ViewModel that connects to the view using binding:
class AssignmentViewModel : ObservableObject
{
private var assignmentModel = AssignmentModel()
/*AssignmentModel is a different class, we assume all methods working correctly and it's not a part of the question.*/
#Published var allAssignments : Set<Assignment>
init()
{
allAssignments=[]
}
func retrieveAllAssignment()
{
allAssignments=assignmentModel.retrieveAllAssignments()
}
}
Try the following
List(Array(assignmentViewModel.allAssignments), id: \.self) { assignment in
My question is probably the result of a misunderstanding but I can't figure it out, so here it is:
When using a component like a TextField or any other component requiring a binding as input
TextField(title: StringProtocol, text: Binding<String>)
And a View with a ViewModel, I naturally thought that I could simply pass my ViewModel #Published properties as binding :
class MyViewModel: ObservableObject {
#Published var title: String
#Published var text: String
}
// Now in my view
var body: some View {
TextField(title: myViewModel.title, text: myViewModel.$text)
}
But I obviously can't since the publisher cannot act as binding. From my understanding, only a #State property can act like that but shouldn't all the #State properties live only in the View and not in the view model? Or could I do something like that :
class MyViewModel: ObservableObject {
#Published var title: String
#State var text: String
}
And if I can't, how can I transfer the information to my ViewModel when my text is updated?
You were almost there. You just have to replace myViewModel.$text with $myViewModel.text.
class MyViewModel: ObservableObject {
var title: String = "SwiftUI"
#Published var text: String = ""
}
struct TextFieldView: View {
#StateObject var myViewModel: MyViewModel = MyViewModel()
var body: some View {
TextField(myViewModel.title, text: $myViewModel.text)
}
}
TextField expects a Binding (for text parameter) and StateObject property wrapper takes care of creating bindings to MyViewModel's properties using dynamic member lookup.