How to initialize a #Binding Array - swiftui

So I'm doing some refactoring and I ran across this line of code that I wanted to refactor:
struct MyView: View {
#State private var myArrayOfCustomObjects = [CustomObject]
let text: String
var body: some View {
Text(text)
}
}
Then when I wanted to refactor the view as so..
struct ExtractedView: View {
#Binding var customObjects: [CustomObject]
let text: String
init(customObjects: Binding<Array<CustomObject>>, text: String) {
self.customObjects = customObjects // Error: 'self' used before all stored properties are initialized
// Also tried _customObjects = customObjects
self.text = text
}
var body: some View {
Text(text)
}
}
This code is simplified of course but I fear I may be getting that error due to some complexity I'm not exposing in the example. Any feedback is welcome
What am I doing wrong??
( I also have an Environment instance (managedObjectContext) and a coreData class - which has some logic inside of the init that are being initialized too but didn't think it was relevant for this code example )

This will work! also try clean your build folder and build your project first.
struct ExtractedView: View {
#Binding var customObjects: [CustomObject]
let text: String
init(customObjects: Binding<Array<CustomObject>>, text: String) {
self._customObjects = customObjects
self.text = text
}
var body: some View {
Text(text)
}
}
struct CustomObject { }

Related

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

Passing a map location variable to other view in SwiftUI

I was able to get the location using CLLocationManager() and the city information is able to display in the Text(config.text). Can someone help how to pass that "config.text" to other view (mainView())
struct CityLocationView2 : View {
let location: Location
#State var config = CityLocationView2Config()
var body: some View {
Text(config.text)
mainView(). // How to pass the city ?????
.task(id: location.id) {
await config.reverseGecode(location: location)
}
}
}
Thanks,
try this:
struct CityLocationView2 : View {
let location: Location
#State var config = CityLocationView2Config()
var body: some View {
Text(config.text)
MainView(text: config.text) // <-- here
.task(id: location.id) {
await config.reverseGecode(location: location)
}
}
}
struct MainView: View {
#State var text: String // <-- here
var body: some View {
Text(text)
}
}
You can of course also use let text: String or var text: String, etc...
I recommend you read and do the tutorial at: https://developer.apple.com/tutorials/swiftui/
Knowing how to pass values to other Views is essential to use SwiftUI.

SwiftUI Combine Why can't bind data in init?

I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.

Why does a SwiftUI TextField inside a navigation bar only accept input one character at a time

I want to allow the user to filter data in a long list to more easily find matching titles.
I have placed a TextView inside my navigation bar:
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: TextField("search", text: $modelData.searchString)
I have an observable object which responds to changes in the search string:
class DataModel: ObservableObject {
#Published var modelData: [PDFSummary]
#Published var searchString = "" {
didSet {
if searchString == "" {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name })
} else {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name }).filter({ $0.name.lowercased().contains(searchString.lowercased()) })
}
}
}
Everything works fine, except I have to tap on the field after entering each letter. For some reason the focus is taken away from the field after each letter is entered (unless I tap on a suggested autocorrect - the whole string is correctly added to the string at once)
The problem is in rebuilt NavigationView completely that result in dropped text field focus.
Here is working approach. Tested with Xcode 11.4 / iOS 13.4
The idea is to avoid rebuild NavigationView based on knowledge that SwiftUI engine updates only modified views, so using decomposition we make modifications local and transfer desired values only between subviews directly not affecting top NavigationView, as a result the last kept stand.
class QueryModel: ObservableObject {
#Published var query: String = ""
}
struct ContentView: View {
// No QueryModel environment object here -
// implicitly passed down. !!! MUST !!!
var body: some View {
NavigationView {
ResultsView()
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: SearchItem())
}
}
}
struct ResultsView: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
var body: some View {
VStack {
Text("Search: \(qm.query)") // receive query string
}
}
}
struct SearchItem: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
#State private var query = "" // updates only local view
var body: some View {
let text = Binding(get: { self.query }, set: {
self.query = $0; self.qm.query = $0; // transfer query string
})
return TextField("search", text: text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(QueryModel())
}
}

Use protocol to define property of swiftui view

I have multiple classes that I want to use with a budget picker view. They all have this budgetable protocol defined.
import SwiftUI
struct BudgetPickerView: View {
#EnvironmentObject var userData: UserData
#State var budgetable: Budgetable
...
}
import Foundation
protocol Budgetable
{
var budgetId: String { get set }
}
For example this Allocation class
import Foundation
import Combine
class Allocation: ObservableObject, Identifiable, Budgetable {
let objectWillChange = ObservableObjectPublisher()
let id: String?
var amount: String { willSet { self.objectWillChange.send() } }
var budgetId: String { willSet { self.objectWillChange.send() } }
init(id: String? = nil, amount: String, budgetId: String) {
self.id = id
self.amount = amount.removePrefix("-")
self.budgetId = budgetId
}
}
However, when I try to pass an allocation into my budget picker view I get an error
NavigationLink(destination: BudgetPickerView(budgetable: allocation))...
Cannot convert return expression of type 'NavigationLink>, BudgetPickerView>' to return type 'some View'
Expression type 'BudgetPickerView' is ambiguous without more context
Change as bellow code
struct BudgetPickerView: View {
#EnvironmentObject var userData: UserData
var budgetable: Budgetable
var body: some View {
...
}
}
and
NavigationLink(destination: BudgetPickerView(budgetable: allocation).EnvironmentObject(UserData()))
By SwiftUI concept you are not allowed to work with #State outside of View, but the following works well (having other your parts unchanged)
struct BudgetPickerView: View {
#State private var budgetable: Budgetable
init(budgetable: Budgetable) {
_budgetable = State<Budgetable>(initialValue: budgetable)
}
var body: some View {
Text("Hello, World!")
}
}
struct TestBudgetPickerView: View {
var body: some View {
NavigationView {
NavigationLink(destination:
BudgetPickerView(budgetable: Allocation(amount: "10", budgetId: "1")))
{ Text("Item") }
}
}
}
BTW, just incase, again by design #State is intended to hold temporary-view-state-only data, not a model. For model is more preferable to use ObservableObject. In your case Budgetable looks like a model.