Manipulating binding variables in SwiftUI - swift

Suppose I have a string that's binding:
#Binding var string: String
But, I want to manipulate that string, and then pass it to a child view.
struct ViewOne: View {
#State var string: String = "Hello"
var body: some View {
ViewTwo(string: string + " World") // this is invalid, as ViewTwo requires Binding<String>
}
}
struct ViewTwo: View {
#Binding var string: String
var body: some View {
Text(string)
}
}
How should I go about manipulating that string, such that it will update the UI when the state changes? Let's assume that I want to re-use ViewTwo for different components, and so I would want to manipulate the string before it is passed to the view.
A computed variable doesn't work, as it isn't Binding
private var fixedString: String {
return string + " World"
}
And I don't want to create a binding variable, because the setter makes no sense in this context
private var fixedString: Binding<String> {
Binding<String> (
get: {
string + " World"
}, set: {
// this function doesn't make sense here - how would it update the original variable?
}
)
}
Am I just using #State and #Binding wrong?

Just remove the #Binding inside ViewTwo:
struct ViewTwo: View {
var string: String /// no need for the `#Binding`!
var body: some View {
Text(string)
}
}
SwiftUI will update ViewTwo even if you don't have the #Binding (it basically re-renders the entire body whenever a #State or #Published var gets changed).
You only need Bindings when you need to update/set the original #State or #Published property. In that case you'd add something like the var fixedString: Binding<String> in your question.

Related

SwiftUI: how to properly use init()?

I am trying to make use of init to call the fetchProducts function in my ViewModel class. When I add init though, I am getting the following 2 errors:
Variable 'self.countries' used before being initialized
and
Return from initializer without initializing all stored properties
The variable countries is binding though so there shouldn't need to be an initialized value in this view. Am I using init incorrectly?
struct ContentView: View {
#Namespace var namespace;
#Binding var countries: [Country];
#Binding var favLists: [Int];
#State var searchText: String = "";
#AppStorage("numTimeUsed") var numTimeUsed = 0;
#Environment(\.requestReview) var requestReview
#StateObject var viewModel = ViewModel();
init() {
viewModel.fetchProducts()
}
var body: some View {
}
}
Look at the initialiser that autocomplete gives you when you use ContentView…
ContentView(countries: Binding<[Country]>, favLists: Binding<[Int]>)
If you're creating your own initialiser, it will need to take those same parameters, e.g.
init(countries: Binding<[Country]>, favLists: Binding<[Int]>) {
_countries = countries
_favLists = favLists
viewModel.fetchProducts()
}
Alternatively, use the default initialiser, and instead…
onAppear {
viewModel.fetchProducts()
}

How do I change the variable "url" which is inside of a class by using a struct? in swift

I'm new to swift and I cannot figure out how to change the url variable by inputting the Binding var url from the struct. I keep getting errors regardless of how I try it. Any help would v vvv appreciated
struct SearchView : View {
#State var showSearchView = true
#State var color = Color.black.opacity(0.7)
**#Binding var url: String**
#ObservedObject var Books = getData()
var body: some View{
if self.showSearchView
{
NavigationView{
List(Books.data) {i in
....}
class getData : ObservableObject{
#Published var data = [Book]()
**var url** = "https://www.googleapis.com/books/v1/volumes?q=harry+potter"
init() {....}
First of all if the current view owns the model object use #StateObject.
Second of all please name classes with starting uppercase and functions and variables with starting lowercase letter.
#StateObject var books = GetData()
...
class GetData : ObservableObject {
You don't need a Binding just address the property directly
books.url = "https://apple.com"
and delete
#Binding var url: String
And if you need to display the changed value immediately use a #Published property and bind the it directly
class GetData : ObservableObject {
#Published var url = "https://www.googleapis.com/books/v1/volumes?q=harry+potter"
...
struct SearchView : View {
#StateObject var books = GetData()
var body: some View {
VStack{
Text(books.url)
TextField("URL", text: $books.url)
}
}
}
Change the *var url** = "https://www.googleapis.com/books/v1/volumes?q=harry+potter" in the second view with: #State var url = "https://www.googleapis.com/books/v1/volumes?q=harry+potter" so it can be mutable

Bridging Optional Binding to Non-Optional Child (SwiftUI)

I have a parent state that might exist:
class Model: ObservableObject {
#Published var name: String? = nil
}
If that state exists, I want to show a child view. In this example, showing name.
If name is visible, I'd like it to be shown and editable. I'd like this to be two-way editable, that means if Model.name changes, I'd like it to push to the ChildUI, if the ChildUI edits this, I'd like it to reflect back to Model.name.
However, if Model.name becomes nil, I'd like ChildUI to hide.
When I do this, via unwrapping of the Model.name, then only the first value is captured by the Child who is now in control of that state. Subsequent changes will not push upstream because it is not a Binding.
Question
Can I have a non-optional upstream bind to an optional when it exists? (are these the right words?)
Complete Example
import SwiftUI
struct Child: View {
// within Child, I'd like the value to be NonOptional
#State var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
class Model: ObservableObject {
// within the parent, value is Optional
#Published var name: String? = nil
}
struct Parent: View {
#ObservedObject var model: Model = .init()
var body: some View {
VStack(spacing: 12) {
Text("Demo..")
// whatever Child loads the first time will retain
// even on change of model.name
if let text = model.name {
Child(text: text)
}
// proof that model.name changes are in fact updating other state
Text("\(model.name ?? "<waiting>")")
}
.onAppear {
model.name = "first change of optionality works"
loop()
}
}
#State var count = 0
func loop() {
async(after: 1) {
count += 1
model.name = "updated: \(count)"
loop()
}
}
}
func async(_ queue: DispatchQueue = .main,
after: TimeInterval,
run work: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + after, execute: work)
}
struct OptionalEditingPreview: PreviewProvider {
static var previews: some View {
Parent()
}
}
Child should take a Binding to the non-optional string, rather than using #State, because you want it to share state with its parent:
struct Child: View {
// within Child, I'd like the value to be NonOptional
#Binding var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
Binding has an initializer that converts a Binding<V?> to Binding<V>?, which you can use like this:
if let binding = Binding<String>($model.name) {
Child(text: binding)
}
If you're getting crashes from that, it's a bug in SwiftUI, but you can work around it like this:
if let text = model.name {
Child(text: Binding(
get: { model.name ?? text },
set: { model.name = $0 }
))
}
Bind your var like this. Using custom binding and make your child view var #Binding.
struct Child: View {
#Binding var text: String //<-== Here
// Other Code
if model.name != nil {
Child(text: Binding($model.name)!)
}

Conditional reaction to update a Binding value in onChange in SwiftUI?

I am updating State value in 2 Views via State and Binding, my goal is that onChange would fire to work only if the update happens from ContentView, I already using this down code for this job, it works but I have to use an extra Variable for this job, I want to know if there is any better way of doing this work with more advanced and better way, also I am not interested to using combine for this.
struct ContentView: View {
#State private var stringValue: String = "Hello"
var body: some View {
Text(stringValue).padding()
Button("update stringValue from ContentView") {
stringValue += " updated from ContentView!"
}
TextView(stringValue: $stringValue)
}
}
struct TextView: View {
#Binding var stringValue: String
#State private var internalStringValue: String = String()
var body: some View {
Text(stringValue).padding()
.onChange(of: stringValue) { newValue in
if newValue != internalStringValue {
print("stringValue updated!")
}
}
Button("update stringValue from TextView") {
internalStringValue = stringValue + " updated from TextView!"
stringValue = internalStringValue
}
}
}

Init for a SwiftUI class with a #Binding var

I have a class which I want to initialize with a Binding var that is set in another View.
View ->
struct CoverPageView: View {
#State var numberOfNumbers:Int
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 0){
TextField("Multiplication Upto:", value: self.$numberOfNumbers, formatter: NumberFormatter())
}
}
}
}
CLASS WHICH NEEDS TO BE INITIALIZED USING THE #Binding var $numberofNumbers -
import SwiftUI
class MultiplicationPractice:ObservableObject {
#Binding var numberOfNumbers:Int
var classNumofNumbers:Int
init() {
self.classNumofNumbers = self.$numberOfNumbers
}
}
The init statement obviously gives the error that self is not initialized and the instance var is being used to initialize which is not allowed.
How do I circumvent this? The class needs to be initialized with the number the user enters on the first view. I have written approx. code here so ignore any typos please.
Typically you'd initialize MultiplicationPractice in CoverPageView with a starting value:
#ObservedObject var someVar = MultiplicationPractice(NoN:123)
And of course, add a supporting init statement:
class MultiplicationPractice:ObservableObject {
init(NoN: Int) {
self.numberOfNumbers = val
}
and you wouldn't want to wrap your var with #Binding, instead wrap it with #Published:
class MultiplicationPractice:ObservableObject {
#Published var numberOfNumbers:Int
...
In your particular case I would even drop the numberOfNumbers var in your CoverPageView, and instead use the direct variable of the above someVar:
struct CoverPageView: View {
//removed #State var numberOfNumbers:Int
#ObservedObject var someVar = MultiplicationPractice(123)
...
TextField("Multiplication Upto:", value: self.$someVar.numberOfNumbers, formatter: NumberFormatter())
You'll notice that I passed in the sub-var of the #ObservedObject as a binding. We can do this with ObservableObjects.
Edit
I see now what you're trying to do, you want to pass a binding along across your ViewModel, and establish an indirect connection between your view and model. While this may not be the way I'd personally do it, I can still provide a working example.
Here is a simple example using your struct names:
struct MultiplicationGame {
#Binding var maxNumber:String
init(maxNumber: Binding<String>) {
self._maxNumber = maxNumber
print(self.maxNumber)
}
}
class MultiplicationPractice:ObservableObject {
var numberOfNumbers: Binding<String>
#Published var MulGame:MultiplicationGame
init(numberOfNumbers: Binding<String> ) {
self.numberOfNumbers = numberOfNumbers
self.MulGame = MultiplicationGame(maxNumber: numberOfNumbers)
}
}
struct ContentView: View {
#State var someText: String
#ObservedObject var mulPractice: MultiplicationPractice
init() {
let state = State(initialValue: "")
self._someText = state
self.mulPractice = MultiplicationPractice(numberOfNumbers: state.projectedValue)
}
var body: some View {
TextField("put your text here", text: $someText)
}
}
Okay, I don't really understand your question so I'm just going to list a few examples and hopefully one of them will be what you're looking for.
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.$value)
}
}
struct SubView: View {
#Binding var value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Binding<Int>) {
self._value = value
}
var body: some View {
Text("\(value)")
}
}
If I misunderstood and you're just trying to get the current value, do this
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.value)
}
}
struct SubView: View {
let value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Int) {
self.value = value
}
var body: some View {
Text("\(value)")
}
}