Update Textfield dynamically in swiftui - arrays

In the following screen, there is a TextField which can be edited and can be updated after receiving value from API
struct VerifyCard: View {
#State var cartSample : CartSampleDetail
#EnvironmentObject var bookMod : BookTestModel
var body: some View {
VStack(alignment:.leading){
HStack{
TextField("enter Barcode", text: Binding(get: {cartSample.Barcode ?? ""}, set: {cartSample.Barcode = $0}))
.textFieldStyle(RoundedBorderTextFieldStyle())
.environmentObject(bookMod)
Button(action: {}, label: {
Image(systemName: "barcode.viewfinder")
})
}
HStack{
GenerateBarcode(bookMod: _bookMod, cartBar:$cartSample, showBar: false)
}
}
}.padding()
}}}
whenever, I am clicking generate barcode in the alert button it is calling the API, response received and barcode too but the textfield is not showing the barcode. I tried DispatchQueue also still same result. Ideally, when button is clicked the textfield should also be filled with the barcode. In adition, I tried to updated the textfield using a published var in the generateBarcode() than it successfully updating the textfield but than it is filling all textfields of the list but it should only fill the coressponding textfield of which button is clicked. Cannot able to sort out this.
This is the GenerateBarcode screen
#EnvironmentObject var bookMod : BookTestModel
#Binding var cartBar : CartSampleDetail
#State var showBar = Bool()
var body: some View {
HStack{
// Text(barcode)
Button(action: {
self.showBar.toggle()
}, label: {
Text("Generate Barcode")
.padding(8)
.foregroundColor(.black)
.background(RoundedRectangle(cornerRadius: 6,style: .continuous).fill(Color.yellow))
}).alert("Barcode Generate", isPresented: $showBar, actions: {
Button("Cancel",role:.cancel ,action: {})
Button(action: {
bookMod.generateBarcode()
// DispatchQueue.main.asyncAfter(deadline:.now()){
// if bookMod.genBarcode.Success{
cartBar.Barcode = bookMod.genBarcode.Value
// }
// }
}, label: {
Text("Generate Barcode")
})
},message: {
Text("If you do not have barcode you can generate it here”)
})
The VerifyCard is used in the list as below:
struct VerifySample: View {
#ObservedObject var bookMod : BookTestModel
#ObservedObject var vial : VialImageModel
var body: some View {
VStack{
List(bookMod.cartSample,id:\.self){sample in
VerifyCard(cartSample: sample, vial: vial.link)
.environmentObject(BookTestModel())
}
}.onAppear{
Task{
try await bookMod.getSampleDetails()
try await vial.getVialLink()
}
}
The above is producing a list of samples represented by VerifyCard struct as explained above.
The following is the CartSampleDetail struct
struct CartSampleData:Codable{
let Message : String
let CartSample : [CartSampleDetail]
enum CodingKeys:String, CodingKey{
case Message
case CartSample = "ResponseData"
}}
struct CartSampleDetail:Codable,Hashable{
var SampleName : String
var PackageId : Int
var TestSampleTypeId : String
var Barcode : String?
var SampleBarcodeDigit : Int
var ColourCode : String
}
I am using this struct to receive API response and also trying to update its Barcode field using the above GenerateBarcode button.
How to display the barcode/text received from generateBarcode() in the textfield after clicking the button.

Related

How to bind a list of textfields that edit a variable within an a core data array?

Swift ui requires a Binding<String> to link to the value you are updating in a text field. Much like the native iPhone Reminders app, I am looking to permit inline editing a list that will persist.
The attached code works only but gives the same name for each item due to them all being bound to the same variable. How can I bind this to the [FruitEntity] array?
class CoreDataViewModel: ObservableObject {
//static let instance = CoreDataViewModel()
let container: NSPersistentContainer
let context: NSManagedObjectContext
#Published var savedEntities: [FruitEntity] = []
}
struct Screen: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: entity.name) { text in
entity.name = questionVariable
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}
}
}
You can just move the TextField to a separate view, with its own #State var for the field and another var for the entity.
Create a view like the following one:
struct ChangeName: View {
// Will change the entity
let entity: FruitEntity
// Will update the field
#State private var questionVariable = ""
var body: some View {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: questionVariable) { text in
entity.name = text
// Remember to save the persistent container/ managed-object-context
}
}
}
Call it in your main view:
struct Screen: View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
ChangeName(entity: entity)
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}

How to update variables in ContentView with SheetView function call?

I have a code with a sheet view and a normal view. When I press a button within my sheet view I make an API call. This API call then updates some variables which I'm trying to display in my regular view using ´ForEach´. However, when I make the call in the sheet view and close it down, the array does not seem to update in my normal view. My view just remains blank (except for displaying the button that says "Show sheet". How do I make the array update so that it isn't blank?
Here is my regular view:
// MARK: - Schedule View
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
var body: some View {
GeometryReader { geo in
VStack {
ForEach(SheetView().vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!) // I want this varible to update, but I doesn't
Text(dict["Destination"]!) // It instead remains blank
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
And here is my sheet view:
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var vm: PlanTripViewModel = PlanTripViewModel()
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
Right now, you're making a new SheetView instance on every single ForEach call -- it's not the same one that you're using in your sheet call.
To solve this, you'll want to store the state in your parent view and give the sheet view a reference to it.
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: PlanTripViewModel //<-- Here
#State var selected = 0
var body: some View {
GeometryReader {geo in
ZStack{
VStack {
TextField("From", text: $vm.origin.input).padding()
TextField("To", text: $vm.dest.input).padding()
TextField("00:00", text: $vm.arrivalTime).padding()
TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
Button("Add trip") {
vm.fetchStatus = .start // This starts the API call in another file of mine
presentationMode.wrappedValue.dismiss() // This closes the sheet view
}.padding()
}.foregroundColor(.blue)
}
}
}
}
struct ScheduleView: View {
#State var selectedTab = 0
#State private var showingSheet = true
#StateObject var vm: PlanTripViewModel //<-- Here
var body: some View {
GeometryReader { geo in
VStack {
ForEach(vm.Trips, id: \.self) { dict in
Text(dict["Origin"]!)
Text(dict["Destination"]!)
}
Button("Show sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView(vm: vm) //<-- Here
}
.frame(width: geo.size.width*0.7, height: geo.size.height*0.06)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(11)
.position(x: geo.size.width/2, y: geo.size.height/2)
// MARK: - Padding funkar inte
}
}.padding()
}
}
(Note: you may know this already, but force unwrapping your dictionary values with ! will cause a crash if the keys don't exist. You may want to use optional binding (if let) or another safe check to make sure they exist.)

How do you bind a list view

I have a CoordinateList view that shows a list of CoordinateRow views that have been entered, and are editable in place (in the list view). To add a new point to the list, the user presses a button at the bottom of the list, and it adds a row (without going to another screen). How do I make it update the view to show this new entry? I have tried wrapping the append function of the list of coordinates with a function so that I can call objectWillChange.send() when adding to the list, but it doesn't seem to do anything.
I guess I don't have enough reputation to upload an image, but here's an image:
import SwiftUI
class LocationTime : ObservableObject {
#Published var lat: String = "0.0"
#Published var lon: String = "0.0"
#Published var timestamp: String = "SomeDateTime"
}
class ModelData: ObservableObject {
#Published var positionCoords = [LocationTime]()
func appendPosition(_ loc: LocationTime) {
objectWillChange.send()
positionCoords.append(loc)
}
}
struct CoordinateRow: View {
#EnvironmentObject var modelData: ModelData
var pointIndex : Int
var body: some View {
HStack {
Text("Lon: ")
TextField("40",text:$modelData.positionCoords[pointIndex].lon)
Text("Lat: ")
TextField("",text:$modelData.positionCoords[pointIndex].lat)
Text("Time: ")
TextField("time",text:$modelData.positionCoords[pointIndex].timestamp)
}.padding()
.overlay(RoundedRectangle(cornerRadius:16)
.stroke(Color.blue,lineWidth:4.0))
}
}
struct CoordinateList: View {
#EnvironmentObject var modelData : ModelData
var body: some View {
VStack{
Text("Location Log")
.font(.largeTitle).padding()
List{
ForEach(modelData.positionCoords.indices){
CoordinateRow(pointIndex: $0).environmentObject(modelData)
}
}
Button(action:{
modelData.appendPosition(LocationTime())
print(modelData.positionCoords.count)
}){
Image(systemName: "plus.circle")
.imageScale(.large)
.scaleEffect(2.0)
.padding()
}
Spacer()
}
}
}
You need identify records in ForEach
ForEach(modelData.positionCoords.indices, id: \.self){ // << here !!
CoordinateRow(pointIndex: $0).environmentObject(modelData)
}
and by the way, remove objectWillChange.send()
func appendPosition(_ loc: LocationTime) {
// objectWillChange.send() // << called automatically for #Published
positionCoords.append(loc)
}

SwiftUI - How do I create a Picker that selects values from a property?

I have these two models, the first one:
import SwiftUI
import Combine
class FolderModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var folderName : String
var values : [ValueModel] = []
init(folderName: String) {
self.folderName = folderName
}
}
And the second one:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
And these storages:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageValues : [ValueModel] = []
typealias StorageValues = [ValueModel]
#Published var storageFolder : [FolderModel] = []
typealias StorageFolder = [FolderModel]
//The rest of the code
}
And then I have a Detail View of the Value, which shows all of his properties. From there, I would like to select the folder that the user wants to put it in (which in code translates to appending that value into the array "values" of the FolderModel).
To do this, I tried to create a Picker that display all the folders (by name) and that can be selected, so that when I press "Save", I can do something like "selectedFolder.append(value)". The Picker I tried to create is this:
import SwiftUI
struct DetailValueView: View {
#ObservedObject var dm : DataManager
#State private var selector = 0
#State var selectedFolder : FolderModel?
var body: some View {
Form {
Section(header: Text("Properties")) {
folderCell
if hasFolder == true {
picker
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: " + "//Here I would like to display the selected value")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(dm.storageFolder) { foldersForEach in
Button(action: {
selectedFolder = foldersForEach
}, label: {
Text(foldersForEach.folderName)
})
}
}.pickerStyle(DefaultPickerStyle())
}
I tried to find a solution online but I don't really understand how the Picker works, I don't understand how to use that "#State private var selector = 0" to get the value that I want.
Thanks to everyone who will help me!
Two things to stress here: First, you need to either wrap your form in a NavigationView or change the picker style to WheelPickerStyle. Otherwise the picker won't work (see here for a detailed explanation). Second, your state selector is of type integer. So make sure to loop through integers as well. Now your state selector holds the index of the selected folder from the list of folders.
Please see my working example below:
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var selector = 0
#State private var hasFolder = false
var body: some View {
NavigationView {
Form {
Section(header: Text("Properties")) {
folderCell
if !dm.storageFolder.isEmpty {
picker
}
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: \(dm.storageFolder[selector].folderName)")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(0 ..< dm.storageFolder.count) {
Text(dm.storageFolder[$0].folderName)
}
}.pickerStyle(DefaultPickerStyle())
}
}

SwiftUI FetchRequest and onReceive cause infinite loop

I have a #FetchRequest for NSManagedObject in swiftUI. I’m trying to change the title of the first item in core data when the toggle is used, by calling onReceive. This does not work if the fetched results are used in MainVC. To demonstrate this, the Navigation Buttons title is how many elements are in the fetched result. This ends up creating an infinite loop in the ContentView onRecieve method. If the Navigation Button has regular text instead of using anything from the FetchedResults then there is no loop and everything works as expected. How is this loop being caused, and is there any better way of toggling a specific core data element?
import SwiftUI
import CoreData
final class ContentVCModel : ObservableObject{
#Published var newToDoItem = String()
#Published var shouldTurnOn = false
func createNewToDoItem(){
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let toDoItem = ToDoItem(context: moc)
toDoItem.title = self.newToDoItem
toDoItem.createdAt = Date()
toDoItem.cost = Cost(main: "$\(Int.random(in: 1...99))", tax: "$\(Int.random(in: 1...9))")
toDoItem.isOn = false
ToDoItem.save()
self.newToDoItem = ""
}
}
struct ContentView: View {
#ObservedObject var model = ContentVCModel()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
List{
Section(header : Text("Whats next?")){
Toggle("Toggle Test", isOn: $model.shouldTurnOn)
.onReceive(model.$shouldTurnOn) { (newValue) in
self.toDoItems.first?.title = "\(Int.random(in: 23...3423))"
//infitine loop
}
HStack{
TextField("New Item", text: $model.newToDoItem)
Button(action: {
self.model.createNewToDoItem()
}){
Image(systemName: "plus.circle.fill")
.foregroundColor(self.model.newToDoItem.isEmpty ? .gray : .green)
.imageScale(.large)
}.disabled(self.model.newToDoItem.isEmpty)
}
}.font(.headline)
Section(header: Text("To Do's")){
ForEach(toDoItems) { toDoItem in
ToDoItemView(title: toDoItem.title, createdAt: toDoItem.createdAt.description, cost: toDoItem.cost, isOn: true)
}.onDelete { (indexSet) in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
ToDoItem.save()
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing: EditButton())
}
}
struct MainVC : View {
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
NavigationView{
NavigationLink(destination: ContentView()) {
Text("\(toDoItems.count)") //Using toDoItems causes this to repeat ex: "\(toDoItems.count)"
}
}
}
}

Resources