#Published Var not updating subview - ios

My problem is that my #Published variable isn't updating my swiftui subview. It changes from false to true, however the view itself isn't updating. Below I pasted a very simplified version of my problem. Also, I passed the api variable that was created within the FirstView to the SubView, however the view still didn't change. If this isn't enough information and you are okay with looking at the full project I can share it down below.
import Foundation
import SwiftUI
struct FirstView: View {
#ObservedObject var Api = API()
var body: some View {
VStack{
SubView()
Button(action:{
Api.apicall()
}){
Text("search")
}
}
}
}
struct SubView:View{
#ObservedObject var daApi = API()
var array = [1,2,3]
var body: some View{
ForEach(0..<array.count){ number in
Text("\(number)")
}
if daApi.viewPresent == true{
Text("Swag")
}
}
}
class API:ObservableObject{
#Published var viewPresent:Bool = false
func apicall(){
viewPresent = true
//The Long ApiCall
viewPresent = false
}
}

Right now, you have 2 different instances of API. You need to share the same instance between both Views:
struct FirstView: View {
#ObservedObject var Api = API()
var body: some View {
VStack{
SubView(daApi: Api)
Button(action:{
Api.apicall()
}){
Text("search")
}
}
}
}
struct SubView:View{
#ObservedObject var daApi : API
var array = [1,2,3]
var body: some View{
ForEach(0..<array.count){ number in
Text("\(number)")
}
if daApi.viewPresent == true {
Text("Swag")
}
}
}
class API:ObservableObject{
#Published var viewPresent:Bool = false
func apicall(){
viewPresent = true
}
}
Also, in your apiCall, you set viewPresent to false and then true immediately again, so I removed one of those.
In Swift, generally variable names start with a lowercase letter -- Api should probably be changed to api.

Related

Why does SwiftUI not update a view here?

The background color and text in ReadyDashboardView below don't update when isConnected is updated. Obviously I want to update the view when the connection is made. I was expecting that publishing everything in the chain to that variable would make it update instantly in swiftui. Instead, it's always rendering using the value provided when the variable is instantiated. Here's a very simplified look at the situation:
Is there another swiftui feature i should be using or am I going to have to make some sweeping changes to my codebase?
import SwiftUI
#main
struct TestApp: App {
#StateObject private var env = PrinterEnv()
var body: some Scene {
WindowGroup {
ReadyDashboardView()
.environmentObject(env)
}
}
}
struct ReadyDashboardView: View {
#EnvironmentObject var env: PrinterEnv
var body: some View {
VStack {
HStack {
Spacer()
VStack {
Text(env.selectedPrinter?.isConnected ?? false ? "Printer Ready" : "Not Connected")
.padding(.bottom)
}
Spacer()
}
.background(env.selectedPrinter?.isConnected ?? false ? .green : .red)
Button("Connect") { env.selectedPrinter?.isConnected = true }
Button("Disconnect") { env.selectedPrinter?.isConnected = false }
}
}
}
class PrinterEnv: ObservableObject {
#Published var configuredPrinters: [Printer] = []
#Published var selectedPrinter: Printer?
init() {
configuredPrinters.append(contentsOf: [Printer()])
selectedPrinter = configuredPrinters.first
}
}
class Printer: ObservableObject {
#Published var isConnected = false
}
I suggest you do not nest ObservableObject, it does not work very well.
Try a Printer struct for example, such as:
struct Printer: Identifiable {
let id = UUID()
var isConnected = false
}
I do not think the problem is specifically related to nested observable objects. Nesting them is working fine and is even recommended by community members as the best way to manage app wide states and ensure performance is acceptable.
See this: https://www.fivestars.blog/articles/app-state/
That said, I believe it's probably related to the wrapping of the observable object with #Published property wrapper.
Try this in a playground:
import SwiftUI
class AppState: ObservableObject {
let fooState = FooState()
let barState = BarState()
}
class FooState: ObservableObject {
#Published var foo: Int = 42
}
class BarState: ObservableObject {
#Published var bar: String = Date().debugDescription
}
struct FooView: View {
#EnvironmentObject var fooState: FooState
var body: some View {
Text("foo: \(fooState.foo)")
}
}
struct BarView: View {
#EnvironmentObject var barState: BarState
var body: some View {
Text("bar: \(barState.bar)")
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Spacer()
FooView()
.environmentObject(appState.fooState)
Button("update foo") {
appState.fooState.foo = Int.random(in: 1...100)
}
Spacer()
BarView()
.environmentObject(appState.barState)
Button("update bar") {
appState.barState.bar = Date().debugDescription
}
Spacer()
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(
rootView: ContentView()
.frame(width: 320, height: 414)
.environmentObject(AppState())
)

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

Importing the data typed by the user to a view?

In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.

Using #State resolves into 'self' used before all stored properties are initialized

I ran into an issue when using the #State property.
My ContentView.swift looks like this:
import SwiftUI
struct ContentView: View {
#State var showText: Bool = true
var Mod: Modifier
init() {
Mod = Modifier(showText: $showText) // Throws error -> 'self' used before all stored properties are initialized ('self.Mod' not initialized)
}
var body: some View {
VStack {
if showText == true {
Text("Hello, World!")
}
Mod
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And my Modifier.swift from which the Modifier view is called has following code:
import SwiftUI
struct Modifier: View {
#Binding var showText: Bool
var body: some View {
VStack {
Button("Hide Text") {
self.showText.toggle()
}
}
}
}
I created this simplified code from my actual project that my problem is easier to understand.
Problem
The problem is that the code in the init function results into an error and I don't know how to resolve it.
What I tried and what I would need
Because this is just a simplified version of my actual code there are some requirements I need to my code:
Mod can't be a computed variable
I somehow need the Modifier view as a variable called Mod in my ContentView
When I remove the #State property and the #Binding property and the $ the code works and results with 0 errors. But I need to use the #State property (which unfortunately results into errors with my code)
Also the button to hide and show the text should work
I would be very thankful if anyone could give me a hint. I really appreciate your help!
I did actually find a way to do this. I'm not sure whether it'll be suitable but here are the details.
The problem was that SwiftUI didn't seem to allow setting the Binding outside of body. So this solution returns a new instance of Modifier
struct Modifier: View {
#Binding var showText: Bool
var body: some View {
VStack {
Button("Hide Text") {
self.showText.toggle()
}
}
}
// this function returns a new instance with the binding
func bind(to binding: Binding<Bool>) -> Self {
return Modifier(showText: binding)
}
}
And the code for ContentView, where we can call this function from within body:
struct ContentView: View {
#State var showText: Bool = true
var Mod: Modifier
init() {
Mod = Modifier(showText: .constant(true)) // .constant() gives a placeholder Binding
}
var body: some View {
return VStack {
if showText == true {
Text("Hello, World!")
}
Mod.bind(to: $showText)
}
}
}
Tested and the text can be hidden/shown. Hope this can help.
Mod = Modifier(showText: _showText.projectedValue)
You can make it let instead of var if you'd like.
Use views inside body context
struct ContentView: View {
#State var showText: Bool = true
var body: some View {
VStack {
if showText == true {
Text("Hello, World!")
}
Modifier(showText: $showText)
}
}
}

ObservableObject doesn't update view

I'm pretty new to SwiftUI (and Swift I haven't touch for a while either) so bear with me:
I have this view:
import SwiftUI
import Combine
var settings = UserSettings()
struct Promotion: View {
#State var isModal: Bool = true
#State private var selectedNamespace = 2
#State private var namespaces = settings.namespaces
var body: some View {
VStack {
Picker(selection: $selectedNamespace, label: Text("Namespaces")) {
ForEach(0 ..< namespaces.count) {
Text(settings.namespaces[$0])
}
}
}.sheet(isPresented: $isModal, content: {
Login()
})
}
}
What I do here, is to call a Login view upon launch, login, and when successful, I set the
var settings
as such in the LoginView
settings.namespaces = ["just", "some", "values"]
my UserSettings class is defined as such
class UserSettings: ObservableObject {
#Published var namespaces = [String]()
}
According to my recently obtained knowledge, my Login view is setting the namespaces property of my UserSettings class. Since this class is an ObservableObject, any view using that class should update to reflect the changes.
However, my Picker remains empty.
Is that because of a fundamental misunderstanding, or am I just missing a comma or so?
You have to pair ObservableObject with ObservedObject in view, so view is notified about changes and refreshed.
Try the following
struct Promotion: View {
#ObservedObject var settings = UserSettings() // << move here
#State var isModal: Bool = true
#State private var selectedNamespace = 2
// #State private var namespaces = settings.namespaces // << not needed
var body: some View {
VStack {
Picker(selection: $selectedNamespace, label: Text("Namespaces")) {
ForEach(namespaces.indices, id: \.self) {
Text(settings.namespaces[$0])
}
}
}.sheet(isPresented: $isModal, content: {
Login(settings: self.settings) // inject settings
})
}
}
struct Login: View {
#ObservedObject var settings: UserSettings // << declare only !!
// ... other code
}

Resources