#Published struct in SwiftUI does not published - swiftui

See my code below. My problem is if i change accessToken via UserService.shared.currentStore.accessToken = xxx, SwiftUI doesn't publish, and there's no update on StoreBanner at all.
//SceneDelegate
let contentView = ContentView()
.environmentObject(UserService.shared)
//Define
class UserService: ObservableObject {
#Published var currentStore = Store.defaultValues()
static let shared = UserService()
}
struct Store: Codable, Hashable {
var storeName: String = ""
var accessToken: String = ""
}
//Use it
struct StoreBanner: View {
var body: some View {
Group {
if UserService.shared.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}

You're trying to use UserService inside StoreBanner without using a property wrapper to tell the view to respond to updates. Without the #ObservedObject property wrapper, the View doesn't have a mechanism to know that any of the #Published properties have been updated.
Try this:
struct StoreBanner: View {
#ObservedObject private var userService = UserService.shared
var body: some View {
Group {
if userService.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
This should work assuming you set accessToken somewhere in your code on the same instance of UserService.

Related

Pass binding to child view in init SwiftUI

I need to save an instance of a child view into a variable, so I can call a method on it afterward.
However, I need to pass a binding into this child view when its initialized. How do I do that?
struct EditImageView: View {
#State private var currentSelectedText:String
#State private var currentSelectedFilter:Filter
var imageCanvasView: ImageCanvasView
init() {
currentSelectedText = "Hello"
currentSelectedFilter = Filter.noFilter
imageCanvasView = ImageCanvasView(imageText: $currentSelectedText, filter: $currentSelectedFilter)
//Error: 'self' used before all stored properties are initialized
}
var body: some View {
imageCanvasview
Button("Take screenshot") {
imageCanvasview.takeScreenshot()
}
}
}
One way is to declare imageCanvasView in body, like:
struct EditImageView: View {
#State private var currentSelectedText = "Hello"
#State private var currentSelectedFilter = Filter.noFilter
var body: some View {
let imageCanvasView = ImageCanvasView(imageText: $currentSelectedText, filter: $currentSelectedFilter)
VStack {
imageCanvasView
Button("Take screenshot") {
imageCanvasView.takeScreenshot()
}
}
}
}
All you need to do is to change the property wrapper prefix. For example, if you wanted to pass your currentSelectedText you would pass it like so.
var currentSelectedText: Binding<String>
// Effectively is the equivalent of `#State`
The same can be done in your init()
init(someString: Binding<String>) { ....
Probably a better way is to use a view model which both EditImageView and ImageCanvasView use, something like:
class EditImageViewModel: ObservableObject {
#Published var currentSelectedText: String = "Hello"
#Published var currentSelectedFilter = Filter.noFilter
func takeScreenshot() {
}
}
struct ImageCanvasView: View {
#EnvironmentObject var editImage: EditImageViewModel
var body: some View {}
}
struct EditImageView: View {
#StateObject var editImage = EditImageViewModel()
var body: some View {
VStack {
ImageCanvasView()
Button("Take screenshot") {
editImage.takeScreenshot()
}
}
.environmentObject(editImage)
}
}

update View when view model's, publish object's, property is updated

How to update view, when view models publish var's (user's) , name property is updated. I do know why its happening but what is the best way to update the view in this case.
class User {
var id = "123"
#Published var name = "jhon"
}
class ViewModel : ObservableObject {
#Published var user : User = User()
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += "update"
print( viewModel.user.name)
}
}
}
so one way i do it, is by using onReceive like this,
var body: some View {
userNameView
.onReceive(viewModel.user.$name){ output in
let tmp = viewModel.user
viewModel.user = tmp
print("onTapGesture",output)
}
}
but it is not a good approach it will update all view using users properties.
should i make a #state var for the name?
or should i just make a ObservedObject for user as well?
Make you class conform to ObservableObject
class User: ObservableObject {
var id = "123"
#Published var name = "jhon"
}
But he catch with that is that you have to observe it directly you can't chain it in a ViewModel
Use #ObservedObject var user: User in a View
You should use struct:
import SwiftUI
struct User {
var id: String
var name: String
}
class ViewModel : ObservableObject {
#Published var user : User = User(id: "123", name: "Mike")
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += " update"
print( viewModel.user.name)
}
}
}

How do I pass a subset of data to a view that has its own view model?

Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}

UndoManager's canUndo property not updating in SwiftUI

Why does the #Environment UndoManager not update its canUndo property when it has actions in its stack? I have a view that has a child that can utilize the un/redo functionality, but for some reason I can't disable the undo button based on the manager.
struct MyView: View {
#Environment(\.undoManager) var undoManager: UndoManager?
var body: some View {
Button("Undo") { ... }
.disabled(!self.undoManager!.canUndo)
}
}
UndoManager.canUndo is not KVO compliant, so use some notification publisher to track state, like below
struct MyView: View {
#Environment(\.undoManager) var undoManager
#State private var canUndo = false
// consider also other similar notifications
private let undoObserver = NotificationCenter.default.publisher(for: .NSUndoManagerDidCloseUndoGroup)
var body: some View {
Button("Undo") { }
.disabled(!canUndo)
.onReceive(undoObserver) { _ in
self.canUndo = self.undoManager!.canUndo
}
}
}
When it comes to canRedo I tried multiple things, and what I ended up with is this - so observing viewModel (or document or any other undo-supporting data source) and updating canUndo/canRedo in reaction to it's change:
struct MyView: View {
#ObservedObject var viewModel: ViewModel
#Environment(\.undoManager) private var undoManger: UndoManager!
#State private var canUndo = false
#State private var canRedo = false
var body: some View {
RootView()
.onReceive(viewModel.objectWillChange) { _ in
canUndo = undoManger.canUndo
canRedo = undoManger.canRedo
}
if canUndo {
Button(
action: { undoManger?.undo() },
label: { Text("Undo") }
)
}
if canRedo {
Button(
action: { undoManger?.redo() },
label: { Text("Redo") }
)
}
...
I also wrapped it in a standalone button (without overgeneralizing the implementation above my own needs) that eliminates the boilerplate from my view and keeps complexity more private so it ends up like this for me:
struct MyView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
RootView()
UndoManagerActionButton(
.undo,
willChangePublisher: viewModel.objectWillChange
)
UndoManagerActionButton(
.redo,
willChangePublisher: viewModel.objectWillChange
)
...

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.