SwiftUI - Binding in ObservableObject - swiftui

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!

The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

Related

SwiftUI Control Flow and ViewBuilder

I want to have a mother view that displays a different child view based on a #State bool.
However I get two errors when doing that.
On the start of the body closure:
Struct 'ViewBuilder' requires that 'EmptyCommands' conform to 'View'
Return type of property 'body' requires that 'EmptyCommands' conform
to 'View'
And inside the control flow statement:
Closure containing control flow statement cannot be used with result
builder 'CommandsBuilder'
struct ResultView: View {
#State var resultViewSuccess = false
let resultViewModel: ResultViewModel
var body: some View {
Group {
if let showresultView = resultViewSuccess {
ViewOne(viewModel: resultViewModel)
} else {
ViewTwo(
resultViewSuccess: $resultViewSuccess,
viewModel: resultViewModel,
)
}
}
}
}
struct ViewTwo: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
NavigationView {
ButtonResult(
resultViewSuccess: $resultViewSuccess,
viewModel: viewModel)
}
}
struct ButtonResult: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
Button(action: {
self.resultViewSuccess = true
}) {
Text("View Results")
}
}
}

How to use #FocusState with view models?

I'm using view models for my SwiftUI app and would like to have the focus state also in the view model as the form is quite complex.
This implementation using #FocusState in the view is working as expected, but not want I want:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
Button("Set Focus") {
hasFocus = true
}
}
}
}
class ViewModel: ObservableObject {
#Published var textField: String = ""
}
How can I put the #FocusState into the view model?
Assuming you have in ViewModel as well
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
...
}
you can use it like
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0 // << write !!
}
.onAppear {
self.hasFocus = viewModel.hasFocus // << read !!
}
}
}
as well as the same from Button if any needed.
I faced the same problem and ended up writing an extension that can be reused to sync both values. This way the focus can also be set from the view model side if needed.
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.sync($viewModel.hasFocus, with: _hasFocus)
}
}
extension View {
func sync<T: Equatable>(_ binding: Binding<T>, with focusState: FocusState<T>) -> some View {
self
.onChange(of: binding.wrappedValue) {
focusState.wrappedValue = $0
}
.onChange(of: focusState.wrappedValue) {
binding.wrappedValue = $0
}
}
}

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

SwiftUI Childview not not refreshing on #Binding update

I have a conditional view based on 2 binding vars in MyBootomSheet.
Idea is, show detail view if an item is selected in parent view, else show list of items.
ParentView has the logic to select/unselect item.
The code works as expected for the first time. But once an item is selected in the parent view the view never gets updated ever again, even after another item selected or item is unselected.
Any idea how this can be resolved ?
TIA!
struct ParentView: View {
#StateObject var dataSource = MapViewSource()
#State var selectedItem:SomeModel? = nil
var body: some View {
//
}.bottomSheet() {
MyBottomSheet(items:self.$dataSource.items, selectedItem:self.$selectedItem)
}
}
struct MyBottomSheet: View {
#Binding var items:[SomeModel]
#Binding var selectedItem:SomeModel?
var body: some View {
if self.selectedItem != nil {
ItemDetail(item: self.selectedItem!)
}
else {
List(self.items id: \.itemId) { item in
ItemRow(item: item)
}
}
}
}
State uses Binding for childviews but StateObject needs an ObservedObject:
class SomeObservableObject: ObservableObject {
// if this wasn't published, then the view wouldn't update when only changing the value
#Published var value: String
init(_ value: String) {
self.value = value
}
}
struct ParentView: View {
#State var state: String = "This is a State"
#StateObject var stateObject: SomeObservableObject = .init("This is an ObservableObject")
var body: some View {
ChildView(state: $state, stateObject: stateObject)
}
}
struct ChildView: View {
#Binding var state: String
#ObservedObject var stateObject: SomeObservableObject
var body: some View {
VStack {
Text("State: \(state)")
Text("StateObject: \(stateObject.value)")
}
}
}
``

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.