Swift UI understanding on observable classes - swift

Ok so my content view is this
import SwiftUI
struct ContentView: View {
#ObservedObject private var user = User()
var body: some View {
VStack {
ShowName()
TextField("First name", text: $user.firstName)
TextField("Last name", text: $user.lastName)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I migrated my class to swift file
import Foundation
class User: ObservableObject {
#Published var firstName = "Bilbo"
#Published var lastName = "Baggins"
}
Then I moved the single view into an external view file
import SwiftUI
struct ShowName: View {
#ObservedObject private var user = User()
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
struct ShowName_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
I have Observed the same class but it does not update, when the class and view were in the
Is there something I need to do special in the ShowName() view to make it observe the class, or am I missing the point entirely.
When the main view bound to the class updates the class, the ShowName view is not updated, I know this is me not understanding it but its so hard to ask Google with something like this, so please be kind, im 47 and learning a new language ;)
Sorry ive only been doing swift a few days migrating from Angular/c++/PHP

In ContentView Change
#StateObject private var user = User()
And
ShowName(user: user)
Then in ShowName remove the initializer
#ObservedObject var user = User
Every time you initialize User() you get a different instance one does not know what the other is doing.
The change to state object is because it is unsafe to use #ObservedObject like that.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

That is because the instance of User is unique in both of those views. To correct it, you would want to pass in the user to the ShowName view instead of create a new one in the view itself. It would look something like this:
struct ShowName: View {
#ObservedObject private var use: User
init(_ user: User) {
self.user = user
}
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
Then in content view you would initialize it like:
ShowName(user)
The important thing to realize is that every time you write User() a new user instance is created. Where this change would pass the same user from content view to the show name view.
I’m writing this from my phone so something may be a little off.

Related

How to get a Value in a Class before the View begins? | SwiftUI

I know wired question, but I dont know how to name the Problem, I am very new to Swift and I am trying to get the #ObservedObject messagesManager(HERE) changed to the ChatId both is coming within a navigation Link, so I have both informations, but I can't merge them before the View starts, and I dont want to have a button to update it, when the View loads, it should change to the new Value. Here the code:
import SwiftUI
struct ChatView: View {
#State var infos : SessionServiceImpl
#State var ChatId : String
#ObservedObject var messagesManager = messagingManager(chatID: "")
var body: some View {
VStack {
VStack {
TitleRow(infos: infos)
ScrollViewReader { proxy in
ScrollView{
ForEach(messagesManager.messages, id: \.id) { message in
MessageBubbleView(infos: infos, message: message)
}
}
When the chatID gets changed it will change the Result in the ForEach. I hope u understand my Problem, because currently it just uses the empty Placeholder.

Passing state between 2 pages SwiftUI

I'm trying to re create an older version of my Onboarding setup with the new SwiftUI and when I try to share the state so the view changes, it simply doesn't know that something has changed, this is what I'm doing:
In the main .swift struct (not ContentView.swift) I defined the pages like this:
#main
struct AnotherAPP: App {
#ObservedObject var onBoardingUserDefaults = OnBoardingUserDefaults()
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
// Onboarding screen
if (onBoardingUserDefaults.isOnBoardingDone == false) {
OnboardingPageView()
} else {
UserLoginView()
}
}
}
}
So on the onBoarding page when I click the button to go to the login, it stores it, but it doesn't actually refreshes the view. There (in the OnboardingPageView.swift) I call the UserDefaults like this:
#ObservedObject private var onBoardingUserDefaults = OnBoardingUserDefaults()
and on the button I change it like this:
self.onBoardingUserDefaults.isOnBoardingDone = true
UserDefaults.standard.synchronize()
So what's going on?
I know for instance if I create a #State on the #main and I bind it to the OnboardingPageView it works, as soon as I hit that button it takes me there.
You can use AppStorage to manage UserDefaults variable in multiple views:
#main
struct TestApp: App {
#AppStorage("isOnBoardingDone") var isOnBoardingDone = false
var body: some Scene {
WindowGroup {
if !isOnBoardingDone {
OnboardingPageView(isOnBoardingDone: $isOnBoardingDone)
} else {
UserLoginView()
}
}
}
}
struct OnboardingPageView: View {
#Binding var isOnBoardingDone: Bool
var body: some View {
Button("Complete") {
isOnBoardingDone = true
}
}
}
If I understood correctly, you are trying to pass the value of a state variable in the Content View to another view in the same app. For simplicity, Let's say your variable is initialised as follows in ContentView:
#State private var countryIndex = 0 //Assuming the name of the variable is countryIndex
Now, to transfer the value write the following in the Content View (or wherever the variable is initially):
//Other code
NavigationLink(destination: NextPage(valueFromContentView: $countryIndex)) {
Text("Moving On")
}//In this case, the variable that will store the value of countryIndex in the other view is called valueFromContentView
//Close your VStacks and your body and content view with a '}'
In your second view or the other view, initialise a Binding variable called valueFromContentView using:
#Binding var valueFromContentView: Int
Then, scroll down to the code that creates your previews. FYI, It is another struct called ViewName_Previews: PreviewProvider { ... }
IF you haven't changed anything, it will be:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
}
}
Remember, my second view is called NextPage.
Inside the previews braces, enter the code:
NextPage(valueFromContentView: .constant(0))
So, the code that creates the preview for your application now looks like:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
NextPage(valueFromContentView: .constant(0)) //This is what you add
}
}
Remember, NextPage is the name of my view and valueFromContentView is teh binding variable that I initialised above
Like this, you now can transfer the value of a variable in one view to another view.

How do you edit the members of a Struct of type View from another Struct of type View? (SwiftUI)

I have the following 2 structs within separate files and displayed in the contentView. What I'm trying to understand is how to maintain the contentView as only displaying and organizing the UI. Placing all of my other views in separate files. My first thought was the correct approach would be to use static variables updated by functions that are called from the button press action. But the buttons text did not update accordingly. As they are dynamically updated according to #State.
update:
I attempted to solve this by using protocols and delegates to no avail. By my understanding this delegate call should be receiving on the other end and updating structcop.ID and the change should be reflected in the content view.
FILE 1
import SwiftUI
struct structdispatch: View {
var radio:RadioDelegate?
func send() {
radio?.update()
self.debug()
}
var body: some View {
Button(action: self.send)
{Text("DISPATCHER")}
}
func debug() {
print("Button is sending?")
}
}
struct structdispatch_Previews: PreviewProvider {
static var previews: some View {
structdispatch()
}
}
**FILE 2:**
import SwiftUI
protocol RadioDelegate {
func update()
}
struct structcop: View, RadioDelegate {
#State public var ID:Int = 3
func update(){
print("message recieved")
self.ID += 1
print(self.ID)
}
var body: some View {
Text(String(self.ID))
}
}
struct structcop_Previews: PreviewProvider {
static var previews: some View {
structcop()
}
}
DEBUG CONSOLE RETURNS:
The Button is working
View is updated on some internal DynamicProperty change, like #State, so here is possible solution
Tested with Xcode 12 / iOS 14
struct structcop: View {
static public var ID = 3
#State private var localID = Self.ID {
didSet {
Self.ID = localID
}
}
var body: some View {
Button(action: printme)
{Text(String(localID))}
}
func printme(){
self.localID = 5
print(structcop.ID)
}
}
Solution:
After some digging I have a working solution but I'm still curious if there is a way to modify properties of other structs while maintaining dynamic view updates.
Solution: store data for display in an observable object which will either read or act as the model which the user is interacting with.
An observable object is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change. -apple
A new model type is declared that conforms to the ObservableObject protocol from the Combine framework. SwiftUI subscribes to the observable object and updates relevant views that need refreshing when the data changes. SceneDelegate.swift needs to have the .environmentObject(_:) modifier added to your root view.
Properties declared within the ObservableObject should be set to #Published so that any changes are picked up by subscribers.
For test code I created an ObservableObject called headquarters
import SwiftUI
import Combine
final class hq: ObservableObject {
#Published var info = headQuarters
}
let headQuarters = hqData(id: 3)
struct hqData {
var id: Int
mutating func bump() {
self.id += 1
}
}
In my struct dispatch I subscribed to the object and called a function that iterated the id in the model whenever the button was pressed. My struct cop also subscribed to the object and thus the model and button text updated accordingly to changes.
struct dispatch: View {
#EnvironmentObject private var hq: headqarters
var body: some View {
Button(action: {self.hq.info.bump()}) {
Text("Button")
}
}
}
struct cop: View {
#EnvironmentObject private var hq: headquarters
var body: some View {
Text(String(self.hq.info.id))
}
}

SwiftUI: ObservableObject does not persist its State over being redrawn

Problem
In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.
A normal ViewModel looks a bit like this:
class SomeViewModel: ObservableObject {
#Published var state = 1
// Logic and calls of Business Logic goes here
}
and is used like so:
struct SomeView: View {
#ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).
In my opinion, the ViewModel should stay, or the State should persist.
If I replace the ViewModel with a #State Property and use the int (in this example) directly it stays persisted and does not get recreated:
struct SomeView: View {
#State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
This does obviously not work for more complex States. And if I set a class for #State (like the ViewModel) more and more Things are not working as expected.
Question
Is there a way of not recreating the ViewModel every time?
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
Why is #State keeping the State over the redraw?
I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.
Duplicate Question
I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.
Edit (adding more detailed Example)
When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the NavigationLink, which would then lead to a recreation of the ViewModel.
I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Finally, there is a Solution provided by Apple: #StateObject.
By replacing #ObservedObject with #StateObject everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
#State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
#Published var title = 0
}
struct TestView2: View {
#StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.
struct MyView: View {
#State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
#ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.
Is there a way of not recreating the ViewModel every time?
Yes, keep ViewModel instance outside of SomeView and inject via constructor
struct SomeView: View {
#ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
No needs. #ObservedObject is-a already DynamicProperty similarly to #State
Why is #State keeping the State over the redraw?
Because it keeps its storage, ie. wrapped value, outside of view. (so, see first above again)
You need to provide custom PassThroughSubject in your ObservableObject class. Look at this code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//#ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
First, I using TextChanger to pass new value of .text to .onReceive(...) in CustomState View. Note, that onReceive in this case gets PassthroughSubject, not the ObservableObjectPublisher. In last case you will have only Publisher.Output in perform: closure, not the NewValue. state.text in that case would have old value.
Second, look at the ComplexState class. I made an objectWillChange property to make text changes send notification to subscribers manually. Its almost the same like #Published wrapper do. But, when the text changing it will send both, and objectWillChange.send() and textChanged.send(newValue). This makes you be able to choose in exact View, how to react on state changing. If you want ordinary behavior, just put the state into #ObservedObject wrapper in CustomStateContainer View. Then, you will have all the views recreated and this section will get updated values too:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove #ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.
update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// #Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
#ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
#State private var text: String = ""
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.
Is that what you needed?
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
.environmentObject(viewModel)
Just init viewModel somewhere it will not be reset(example root view).

SwiftU NavigationView: how to update previous view every time coming back from the secondary view

In ContentView.swift, I have:
List(recipeData) { recipe in NavigationLink(destination: RecipeView(recipe: recipe)){
Text(recipe.name)
}
}
In the RecipeView, user might update the recipeData variable. However, when the RecipeView is closed, ContentView is not updated based on the updated recipeData.
recipeData is not a #State array but a normal one that is declared outside the ContentView struct. I cannot easily make it a #State var because it is used in other parts of the app.
Thanks!
Using #ObservableObject and #Published you can achieve your requirements.
ViewModel
final class RecipeListViewModel: ObservableObject {
#Published var recipeData: [Recipe] = []
....
....
//write code to fetch recipes from the server or local storage and fill the recipeData
....
....
}
View
struct RepositoryListView : View {
#ObservedObject var viewModel: RecipeListViewModel
var body: some View {
NavigationView {
List(viewModel.recipeData) { recipe in
NavigationLink(destination: RecipeView(recipe: recipe)) {
Text(recipe.name)
}
}
}
}