SwiftUI Combine Why can't bind data in init? - swiftui

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.

Related

Pass in default text in TextView while keeping state changes with SwiftUI

I am trying to set a default text on a TextView when the view appears, while being able to still keep track of changes to the TextView that I can then pass on to my ViewModel.
Here is a little example that looks like what I am trying to do. This does however not work, it does not update the state as I would have expected. Am I doing something wrong?
struct NoteView: View {
#State var note = ""
var noteFromOutside: String?
var body: some View {
VStack {
TextField("Write a note...", text: $note)
.onSubmit {
//Do something with the note.
}
}
.onAppear {
if let newNote = noteFromOutside {
note = newNote
}
}
}
}
struct ParentView: View {
var note = "Note"
var body: some View {
VStack {
NoteView(noteFromOutside: note)
}
}
}
Found this answer to another post which solved my problem. The key was in the #Binding and init().
https://stackoverflow.com/a/64526620/12764203

SwiftUI #State variables aren't updated

I basically have the same code as in this question. The problem I have is that when the tapGesture event happens, the sheet shows (the sheet code is called) but debug shows that showUserEditor is false (in that case, how is the sheet showing...?) and that selectedUserId is still nil (and therefore crashes on unwrapping it...)
The view:
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
#State private var selectedUserId : NSManagedObjectID? = nil
#State private var showUserEditor = false
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUserId = user.objectID
self.showUserEditor = true
}
}
}
}.sheet(isPresented: $showUserEditor) {
UserEditorView(userId: self.selectedUserId!)
}
}
}
If you want, I can publish the editor and the row but they seem irrelevant to the question as the magic should happen in the view.
So, I still haven't figured out WHY the code posted in the question didn't work, with a pointer from #loremipsum I got a working code by using another .sheet() method, one that takes an optional Item and not a boolean flag. The code now looks like this and works, but still if anyone can explain why the posted code didn't work I'd appreciate it.
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
#State private var selectedUser : User? = nil
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUser = user
}
}.onDelete(perform: deleteItems)
}
}.sheet(item: $selectedUser, onDismiss: nil) { user in
UserEditorView(user: user)
}
}
}
struct == immutable and SwiftUI decides when the struct gets init and reloaded
Working with code that depends on SwiftUI updating non-wrapped variables at a very specific time is not recommended. You have no control over this process.
To make your first setup work you need to use SwiftUI wrappers for the variables
.sheet(isPresented: $showUserEditor) {
//struct == immutable SwiftUI wrappers load the entire struct when there are changes
//With your original setup this variable gets created/set when the body is loaded so the orginal value of nil is what is seen in the next View
UserEditorView1(userId: $selectedUserId)
}
struct UserEditorView1: View {
//This is what you orginal View likely looks like it won't work because of the struct being immutable and SwiftUI controlling when the struct is reloaded
//let userId: NSManagedObjectID? <---- Depends on specific reload steps
//To make it work you would use a SwiftUI wrapper so the variable gets updated when SwiftUI descides to update it which is invisible to the user
#Binding var userId: NSManagedObjectID?
//This setup though now requres you to go fetch the object somehow and put it into the View so you can edit it.
//It is unnecessary though because SwiftUI provides the .sheet init with item where the item that is set gets passed directly vs waiting for the SwiftUi update no optionals
var body: some View {
Text(userId?.description ?? "nil userId")
}
}
Your answer code doesn't work because your parameter is optional and Binding does not like optionals
struct UserEditorView2: View {
//This is the setup that you posted in the Answer code and it doesn't work becaue of the ? Bindings do not like nil. You have to create wrappers to compensate for this
//But unecessary because all CoreData objects are ObservableObjects so you dont need Binding here the Binding is built-in the object for editing the variables
#Binding var user: User?
var body: some View {
TextField("nickname", text: $user.nickname)
}
}
Now for working code with an easily editable CoreData Object
struct UsersView: View {
#Environment(\.managedObjectContext)
private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.nickname, ascending: true)],
animation: .default)
private var users: FetchedResults<User>
//Your list view would use the CoreData object to trigger a sheet when the new value is available. When nil there will not be a sheet available for showing
#State private var selectedUser : User? = nil
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user: user)
.onTapGesture {
self.selectedUser = user
}
}
}
}.sheet(item: $selectedUser, onDismiss: nil) { user in //This gives you a non-optional user so you don't have to compensate for nil in the next View
UserEditorView3(user: user)
}
}
}
Then the View in the sheet would look like this
struct UserEditorView3: View {
//I mentioned the ObservedObject in my comment
#ObservedObject var user: User
var body: some View {
//If your nickname is a String? you have to compensate for that optional but it is much simpler to do it from here
TextField("nickname", text: $user.nickname.bound)
}
}
//This comes from another very popular SO question (couldn't find it to quote it) that I could not find and is necessary when CoreData does not let you define a variable as non-optional and you want to use Binding for editing
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
//This just give you an empty String when the variable is nil
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}

How to initialize a #Binding Array

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 { }

SwiftUI - How to pass data then initialise and edit data

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
}
}
}
}

SwiftUI - Updating #State when Global changes

I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.
OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}