I have a simple View called TextView, I could make this View working without explicitly using some View like this code:
struct TextView: View {
var body = Text("Hello, world!")
}
now I like to control the String also like this code:
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
}
var body = Text(stringOfText)
}
with second code I get this Error:
How could I possibly solve this issue without using Binding or State wrappers?
I have doubts all this is good SwiftUI code style, but following your first part you can use
struct TextView: View {
var body: Text
init(stringOfText: String) {
body = Text(stringOfText)
}
}
Related
In my function, I have a parameter a manager: TextManager, but when I try to bind its text to a Binding<String>, it says "Cannot find '$manager' in scope". Why cannot it find the 'manager', it is right there in the function parameter?
The main UI code is the following:
struct BugView: View {
#ObservedObject var textManager = TextManager(limit: 30)
var body: some View {
VStack {
Text("line 1")
fetchTextView(title:"Name", manager: textManager)
Text("line 3")
}
}
#ViewBuilder
private func fetchTextView(title: String, manager : TextManager) -> some View {
TextField(title, text: $manager.text) // <- Cannot find '$manager' in scope
Text("\(manager.text.count)/\(manager.characterLimit)")
}
}
where the TextManager is the following
class TextManager: ObservableObject {
let characterLimit: Int
#Published var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
}
I started with this code, and it works fine
struct BugView: View {
#ObservedObject var textManager = TextManager(limit: 30)
var body: some View {
VStack {
Text("line 1")
TextField("good case", text: $textManager.text)
Text("line 3")
}
}
}
I just want to wrap the some text view generation logic inside a function because I would have multiple of those views, and I would like to avoid code duplication.
The first problem is TextManager is a class instead of struct. We only need reference types in SwiftUI if we are doing something asynchronous like saving or syncing data (although .task removes that need for most use cases).
struct TextManager {
let characterLimit: Int
var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
mutating func reset() {
text = ""
}
}
In a struct, any change to its properties, like text is detected as a change to the whole struct, which is a feature that SwiftUI takes advantage of in its design. For logic you might want to test, you can use mutating func.
Next, instead of a #ViewBuilder func you need a custom subview, e.g.
struct FetchTextView: View {
let title: String
#Binding var config: TextManager
var body: some View {
TextField(title, text: $config.text)
Text("\(config.text.count)/\(config.characterLimit)")
}
}
#Binding var instead of let gives us write access to the state, but as with let, body is also called when the value changes. The reason is breaking up the View struct hierarchy into small Views makes SwiftUI more efficient and faster, if you use funcs you risk breaking its dependency tracking (which is how SwiftUI decides if body should be called based on any used values changing). Use it like:
#State var config = TextManager()
FetchTextView(title: title, config: $config)
To use ObservableObject in function, you will have to create manual binding instead of the syntactic sugar $. The following code would work
#ViewBuilder
private func fetchTextView(title: String, manager: TextManager) -> some View {
let binding = Binding(
get: { manager.text },
set: { manager.text = $0 }
)
TextField(title, text: binding)
Text("\(manager.text.count)/\(manager.characterLimit)")
}
why can't we use it like this
just pass textManager directly instead pass via fetchTextView Method
struct BugView: View {
#StateObject var textManager = TextManager(limit: 30) // <-- when creating an instance make sure use StateObject
var body: some View {
VStack {
Text("line 1")
fetchTextView(title:"Name")
Text("line 3")
}
}
#ViewBuilder
private func fetchTextView(title: String) -> some View {
TextField(title, text: $textManager.text) // <-- just use textManagerDirectly...
Text("\(textManager.text.count)/\(textManager.characterLimit)")
}
}
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 { }
I have a simple View called TextView, the type of TextView is naturally View, I want make a modification on TextView like .foregroundColor(Color.red) and replace it again as itself! from my understanding my TextView is type View and should just working because the type is not Text But also I understand the complaining from SwiftUI which says cannot assign value of some View to type View. I like make some correction for solving issue, also I am not interested to add another initializer as foregroundColor.
import SwiftUI
struct ContentView: View {
#State private var myTextView: TextView?
var body: some View {
if let unwrappedMyTextView = myTextView {
unwrappedMyTextView
}
Button ("update") {
myTextView = TextView(stringOfText: "Hello, world!")//.foregroundColor(Color.red)
}
.padding()
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
}
var body: some View {
return Text(stringOfText)
}
}
The problem is that foregroundColor doesn't return TextView:
#inlinable public func foregroundColor(_ color: Color?) -> some View
Which means that
myTextView = TextView(stringOfText: "Hello, world!").foregroundColor(Color.red)
is no longer a TextView but some other View which is a result of applying foregroundColor.
Also, you shouldn't hold View properties as #State.
Try this instead:
struct ContentView: View {
var body: some View {
myTextView
}
var myTextView: some View {
TextView(stringOfText: "Hello, world!")
.foregroundColor(Color.red)
}
}
I'm downloading data from Firebase and trying to edit it. It works, but with an issue. I am currently passing data to my EditViewModel with the .onAppear() method of my view. And reading the data from EditViewModel within my view.
class EditViewModel: ObservableObject {
#Published var title: String = ""
}
struct EditView: View {
#State var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I have given you the extremely short-hand version as it's much easier to follow.
However, I push to another view to select options from a list and pop back. As a result, everything is reset due to using the onAppear method. I have spent hours trying to use init() but I am struggling to get my application to even compile, getting errors in the process. I understand it's due to using the .onAppear method, but how can I use init() for this particular view/view-model?
I've search online but I've found the answers to not be useful, or different from what I wish to achieve.
Thank you.
You don't need to use State for input property - it is only for internal view usage. So as far as I understood your scenario, here is a possible solution:
struct EditView: View {
private var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
init(selectedItem: ItemModel) {
selected_item = selectedItem
editViewModel.title = selectedItem.title
}
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I want to know if there is simple or proper way to read a State variable value from a different View, I know the usage of .onChange or Binding or ObservableObject(class) and ..., but I like to know is there any other better way?
For example in this code I have a View called TextView which has a State value, and I am calling this View inside my ContentView, Now I put a Text in my ContentView which I want to read the State Value of TextView. Is there a spacial method for this job?
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView()
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#State var stringOfText: String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
}
}
The entire point of State is that it's internal to a View. If you're trying to read it elsewhere, something has gone wrong in your design. The tool you want in this case is #Binding. ContentView should pass a Binding to TextView. Any changes in TextView will be seen by ContentView (in your example, this doesn't make sense, because stringOfText can't change, but I assume that the rest of your code changes it somehow). In your example, that would look something like this:
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
TextView(stringOfText: $readStringOfTextView)
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
struct TextView: View {
#Binding var stringOfText : String
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.onAppear {
stringOfText = "Hello, world!"
}
}
}
In is possible to directly pass data up the view hierarchy using Preferences, but it's much more complicated, and not the right tool for the problem you've described. Even so, this is what it would look like:
Create a PreferenceKey to pass the data
Set the PreferenceKey in the child view(s) using .preference
Read the PreferenceKey in the parent view using .onPreferenceChange or .overlayPreferenceValue or .backgroundPreferenceValue.
struct TextPreference: PreferenceKey {
static var defaultValue = "default"
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
struct ContentView: View {
#State var readStringOfTextView: String = ""
var body: some View {
VStack {
TextView()
.onPreferenceChange(TextPreference.self) { value in
readStringOfTextView = value
}
Text(readStringOfTextView)
.foregroundColor(Color.blue)
}
}
}
struct TextView: View {
#State var stringOfText : String = "Hello, world!"
var body: some View {
Text(stringOfText)
.padding()
.foregroundColor(Color.red)
.preference(key: TextPreference.self, value: stringOfText)
}
}