SwiftUI extract Childview with Bindings, #State - ios

I have the following component in a view
HStack {
TextField("New Note", text: $newNoteContent)
.onSubmit(submitNewNote)
.focused($newNoteIsFocused)
if (!newNoteContent.isEmpty) {
Button(action: submitNewNote) {
Image(systemName: "checkmark")
}
}
}
The variables are defined as follows
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
I would like to extract it and make it either a computed variable returning a view or a function that returns a view (I dont know which is better). I want to extract it because I reuse a similar struct.
Full code in case its needed: https://github.com/charelF/RemindMeApp/blob/main/RemindMe/NotesView.swift
I have tried the following:
func editCell(
noteContent: Binding<String>,
submitFunc: #escaping () -> (),
focus: FocusState<Bool>.Binding
) -> some View {
return HStack {
TextField("New Note", text: noteContent)
.onSubmit(submitFunc)
.focused(focus)
if (!noteContent.isEmpty) {
Button(action: submitFunc) {
Image(systemName: "checkmark")
}
}
}
}
But there are some errors and its generally just playing around -- I have no idea what I am really doing here and need some feedback/help.
Update from comment:
So I extracted the view as follows for now
struct ExtractedView: View {
#State private var editNoteContent: String = ""
#FocusState private var editNoteIsFocused: Bool
#State private var editNote: Note? = nil
func editExistingNote(note: Note?) {
guard let note else { return }
note.content = editNoteContent
PersistenceController.shared.save()
editNoteContent = ""
editNoteIsFocused = false
editNote = nil
}
But I dont understand how to call it. If I call it with ExtractedView() then the code compiles and the app runs, but it crashes when I enter this path of the app. And when I call it like this:
ExtractedView(
editNoteContent: editNoteContent,
editNoteIsFocused: editNoteIsFocused,
editNote: editNote
)
Then i get lots of errors ...

Thanks to the comments and this answer here: https://developer.apple.com/forums/thread/682448 I got it to work. I extracted the view as follows:
struct EditNoteView: View {
#Binding var noteContent: String
#FocusState var isNoteFocused: Bool
var onsubmit: () -> ()
var placeholder: String
var body: some View {
HStack {
TextField(placeholder, text: $noteContent)
.onSubmit(onsubmit)
.focused($isNoteFocused)
if (!noteContent.isEmpty) {
Button(action: onsubmit) {
Image(systemName: "checkmark")
}
}
}
}
}
and then I call this child view from my parent view (inside the body) with
EditNoteView(
noteContent: $newNoteContent,
isNoteFocused: _newNoteIsFocused,
onsubmit: submitNewNote,
placeholder: "New Note"
)
Also in my parent views, I have the following definitions for the variables
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
The main takeaways is that all of the #State things map to #Binding in the childview, and the #FocusState maps to another #FocusState, but there is a _ required before the parameter in the call.

Related

Supply any string value to swiftUI form in different for textfield

I have two view file. I have textfield. I want to supply string value to the textfield from another view
File 1 :- Place where form is created
struct ContentView: View {
#State var subjectLine: String = ""
var body: some View {
form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine
}
}
}
}
File 2 :- Place where I want to provide value to the string and that will show in the textfield UI
struct CalenderView: View {
#Binding var subjectLine: String
var body : some View {
Button(action: {
subjectLine = "Assigned default value"
}, label: {
Text("Fill textfield")
}
}
})
}
}
This is not working. Any other way we can supply value to the textfield in other view file.
As i can understand you have binding in CalenderView
that means you want to navigate there when you navigate update there.
struct ContentView: View {
#State private var subjectLine: String = ""
#State private var showingSheet: Bool = false
var body: some View {
NavigationView {
Form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine)
}
}
.navigationBarItems(trailing: nextButton)
.sheet(isPresented: $showingSheet) {
CalenderView(subjectLine: $subjectLine)
}
}
}
var nextButton: some View {
Button("Next") {
showingSheet.toggle()
}
}
}
CalendarView
struct CalenderView: View {
#Binding var subjectLine: String
#Environment(\.dismiss) private var dismiss
var body: some View {
Button {
subjectLine = "Assigned default value"
dismiss()
} label: {
Text("Fill textfield")
}
}
}

SwiftUI TextField onChange not triggered

New to SwiftUI and trying to figure something out.
I'm implementing a barcode scanner, which consists of a TextField, Button and CameraView.
CameraView will scan a barcode, display the serial number in the TextField, and the Button will use the serial number to pair a bluetooth device.
So now in my SwiftUI class, I have my UI setup like so:
struct BTLEConnectionView: View {
#ObservedObject var bluetoothConnectorViewModel: BluetoothConnectorViewModel
#ObservedObject var barcodeScannerViewModel = BarcodeScannerViewModel()
#Binding var deviceID: String
#State private var serialNumber: String = ""
var body: some View {
VStack {
HStack {
DeviceIDTextField(deviceID: $deviceID, serialNumber: $serialNumber, viewModel: barcodeScannerViewModel)
PairButton(bluetoothConnectorViewModel: bluetoothConnectorViewModel, barcodeScannerViewModel: barcodeScannerViewModel, serialNumber: $serialNumber)
}
.padding()
BarcodeScannerView(barscannerViewModel: barcodeScannerViewModel)
}
}
}
struct DeviceIDTextField: View {
#Binding var deviceID: String
#Binding var serialNumber: String
#ObservedObject var viewModel: BarcodeScannerViewModel
var body: some View {
let serialNumberBinding = Binding<String>(
get: { viewModel.barcodeString.isEmpty ? serialNumber : viewModel.barcodeString },
set: { serialNumber = viewModel.barcodeString.isEmpty ? $0 : viewModel.barcodeString }
)
TextField(NSLocalizedString("textfield.hint.device.id", comment: ""), text: serialNumberBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: serialNumber, perform: { value in
serialNumber = value
})
}
private func textFieldChanged(_ text: String) {
print(text)
}
}
struct PairButton: View {
#ObservedObject var bluetoothConnectorViewModel: BluetoothConnectorViewModel
#ObservedObject var barcodeScannerViewModel: BarcodeScannerViewModel
#Binding var serialNumber: String
var body: some View {
Button(action: {
withAnimation {
bluetoothConnectorViewModel.connectBluetoothLowEnergyDevice(deviceID: serialNumber)
}
}) {
ButtonText(text: NSLocalizedString("button.text.pair", comment: ""))
}
}
}
So basically I want to make it so:
If user scans barcode, the serial is added to the textfield, press pair button to use that serial and call the method and pass in the serial number.
If user deletes the prepopulated serial, enters their own, it should update the textfield, and pressing the button should use the new serial number.
Right now when I scan a barcode, it populates the text field, however the onChange callback isn't picked up until I actually type in the TextField, so the result is never set for the method call.
Any help with this would be great, hope it makes sense.
You can create a binding with a custom closure, like this:
struct ContentView: View {
#State var location: String = ""
var body: some View {
let binding = Binding<String>(get: {
self.location
}, set: {
self.location = $0
// do whatever you want here
})
return VStack {
Text("Current location: \(location)")
TextField("Search Location", text: binding)
}
}
}

Importing the data typed by the user to a view?

In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.

How to initialize derived variables in body methods in SwiftUI (or alternate approach)

I'm trying to figure out the right way to initialized derived variables in the body method for a SwiftUI view. An example would the string value for an editable integer which would then be edited in a TextField. The integer could for example be part of an #ObservedObject. I cannot figure out any remotely clean way to do this.
I've looked into using custom initializers but this does not seem like the right thing to do. I'm not even sure this code would be run at the appropriate time.
I've also tried using the .onAppear method for TextField, but this method does not appear to be re-executed when the view is rebuilt.
simplified example:
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct ContentView : View {
#ObservedObject var values = sharedValues
var body: some View {
VStack {
Button(
action: { self.add() },
label: { Text("Plus")}
)
InnerView()
}
}
func add() { values.count += 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
#State private var text = ""
var body: some View {
// text = String(value.count) - what I want to do
TextField("", text: $text, onEditingChanged: updateCount)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
func updateCount(updated: Bool) { /* this isn't important in this context */}
}
I would hope to be able to update sharedValues externally and see the update in MyView. In this example, I would want pressing the button to update the text field with the updated text value. But I can't figure a way to have the string representation of the count value computed at the appropriate point in the execution of the code.
I've tried multiple approaches to achieving this type of result, but have come up short.
I'm not sure if I'm understanding your question correctly, but if you are just trying to be able to change a number with a button, have the number be displayed in a text field, and then be able to edit it there, you don't need an ObserverableObject or multiple views.
Here is an example of how you can do it:
struct ContentView: View {
#State var count = 0
#State var countStr = ""
var body: some View {
VStack {
Button(action: {
self.count += 1
self.countStr = "\(self.count)"
}) {
Text("Plus")
}
TextField("", text: $countStr, onEditingChanged: updateCount)
}
}
func updateCount(updated: Bool) { /* this isn't important in this context */ }
}
Use value init method of TextField. This take the value as 2 way Binding. So it automatically update count from both text field and buttons.
import SwiftUI
import Combine
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct AndrewVoelkel : View {
#ObservedObject var values = sharedValues
var body: some View {
HStack {
InnerView()
VStack{
Button(
action: { self.add() },
label: { Text("+")}
)
Button(
action: { self.sub() },
label: { Text("-")}
)
}.font(.headline)
}.padding()
}
func add() { values.count += 1 }
func sub() { values.count -= 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
var body: some View {
TextField("", value: $values.count, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}

SwiftUI set a Binding from the String result of a picker

I am tearing out my hair trying to figure out how to bind the picked value in my SwiftUI view:
The picker needs to be bound to the Int returned from the tags. I need to covert this Int to the String and set the Binding. How?
struct ContentView: View {
#Binding var operatorValueString:String
var body: some View {
Picker(selection: queryType, label: Text("Query Type")) {
ForEach(0..<DM.si.operators.count) { index in
Text(DM.si.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
How and where can I set my operatorValueString ?
operatorValueString = DM.si.operators[queryType] //won't compile.
You can achieve the result, using your own custom binding that sets the string, whenever the picker's selection changes:
struct ContentView: View {
#State private var operatorString = ""
var body: some View {
VStack {
Subview(operatorValueString: $operatorString)
Text("Selected: \(operatorString)")
}
}
}
struct Subview: View {
#Binding var operatorValueString: String
#State private var queryType: Int = 0
let operators = ["OR", "AND", "NOT"]
var body: some View {
let binding = Binding<Int>(
get: { self.queryType },
set: {
self.queryType = $0
self.operatorValueString = self.operators[self.queryType]
})
return Picker(selection: binding, label: Text("Query Type")) {
ForEach(operators.indices) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}

Resources