//: [Previous](#previous)
import SwiftUI
import PlaygroundSupport
// class observer and update with a button.
public class ItemClass2 {
public var name: String
let id: UUID
public var number: Int = 0
public init(name: String) {
self.name = name
self.id = .init()
}
}
class ContentViewModel: ObservableObject {
#Published var item = ItemClass2(name: "first")
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("Item Struct")
.font(.title)
Text("Name: \(viewModel.item.name)")
.font(.headline)
Text("Number: \(viewModel.item.number)")
TextField("name", text: $viewModel.item.name)
.textFieldStyle(.roundedBorder)
Button {
viewModel.item.number += 1
// viewModel
// .objectWillChange
// .send()
} label: {
Text("increase number")
}
}
.frame(width: 400, height: 600)
}
}
PlaygroundPage.current.setLiveView(ContentView())
If you put this in a playground, and tap the increment button, you'll notice that the number doesn't increment until the text field is modified.
I want to bind viewModel.item.number to the Button. If I change the property wrapper for the #StateObject var viewModel = ContentViewModel() to #Bindable it isn't allowed because viewModel is a class.
If we uncomment the objectWillChange lines, it solves the problem, but it feels sketchy and counter to the purpose of SwiftUI MVVM to have to manually call send().
How can I update the viewModel from a button tap, in a way that adheres to MVVM?
You must either:
Make ItemClass2 a struct;
Or keep calling objectWillChange.send().
This is happening because Combine doesn’t create a Binding relation with a refrence type(i.e class), so you must tell Combine when the value has changed or simply use a value type(i.e struct).
Related
I am trying to have a value chosen from a picker update a variable in the observable object class. However, the value does not seem to be binding. Does anyone have some example code for how to accomplish this?
This will work to set the EnvironmentObject with the index of the picked object.
final class GlobalClass: ObservableObject {
#Published public var itemIndex: Int
init(){
self.itemIndex = 0
}
}
struct EnvironPicker: View{
#EnvironmentObject private var globalObj: GlobalClass
var body: some View{
VStack{
Picker(selection: $globalObj.itemIndex, label:
Text("Select")) {
Text("No Action").tag(0)
Text("Proposed").tag(1)
Text("Sold").tag(2)
Text("Lost").tag(3)
}
Text("PrintValue").onTapGesture {
print(globalObj.itemIndex)
}
}
}
}
I added a print to show the globalObj.itemIndex is updating with the binding.
I am making a Slider based on a view model, but I am facing this error message Initializer 'init(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)' requires that 'Int.Stride' (aka 'Int') conform to 'BinaryFloatingPoint'
It is strange because converting the integer from view model into Double doesn't quite do the trick.
I found very similar question and read the SO answer (How can I make Int conform to BinaryFloatingPoint or Double/CGFloat conform to BinaryInteger?), but it doesn't seem like I can implementation the solution for my case, probably because I am using ObservedObject for the view model.
If I remove $ in front of setInformationVM.elapsedRestTime, I would see another error message saying Cannot convert value of type 'Int' to expected argument type 'Binding<Int>'
They said "Binding are generally used when there is a need for 2-way communication" - would that mean the Slider needs a way to communicate/update back to the View Model? Why is it that the Slider was accepting #State private var xx: Double for the value in general , but not a simple integer from my view model?
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
#State var showingLog = false
var body: some View {
GeometryReader { geometry in
ZStack() {
(view content removed for readability)
}
.sheet(isPresented: $showingLog) {
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
setLoggingView(setInformationVM: setInformatationVM, restfullness: 3, stepValue: 10)
}
}
}
setLoggingView
struct setLoggingView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var setInformationVM: SetInformationTestClass
#State var restfullness: Int
var stepValue: Int
var body: some View {
GeometryReader { geometry in
let rect = geometry.frame(in: .global)
ScrollView {
VStack(spacing: 5) {
Text("Rested \(Int(setInformationVM.elapsedRestTime)) sec")
Slider(value: $setInformationVM.elapsedRestTime,
in: 0...setInformationVM.totalRestTime,
step: Int.Stride(stepValue),
label: {
Text("Slider")
}, minimumValueLabel: {
Text("-\(stepValue)")
}, maximumValueLabel: {
Text("+\(stepValue)")
})
.tint(Color.white)
.padding(.bottom)
Divider()
Spacer()
Text("Restfullness")
.frame(minWidth: 0, maxWidth: .infinity)
restfullnessStepper(rect: rect, maxRestFullness: 5, minRestFullness: 1, restfullnessIndex: restfullness)
Button(action: {
print("Update Button Pressed")
//TODO
//perform further actions to update restfullness metric and elapsed rest time in the viewmodels before dismissing the view, and also update the iOS app by synching the view model.
dismiss()
}) {
HStack {
Text("Update")
.fontWeight(.medium)
}
}
.cornerRadius(40)
}
.border(Color.yellow)
}
}
}
SetInformationTestClass view model
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
You can create a custom binding variable like :
let elapsedTime = Binding(
get: { Double(self.setInformationVM.elapsedRestTime) },
set: { self.setInformationVM.elapsedRestTime = Int($0) } // Or other custom logic
)
// then you reference it in the slider like:
Slider(elapsedTime, ...)
Given the setup I've outlined below, I'm trying to determine why ChildView's .onChange(of: _) is not receiving updates.
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
#State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
#StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
#Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
Interestingly, if I make the following changes in ChildView, I get the behavior I want.
Change #StateObject to #ObservedObject
Change _viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem)) to viewModel = ViewModel(someItem: someItem)
From what I understand, it is improper for ChildView's viewModel to be #ObservedObject because ChildView owns viewModel but #ObservedObject gives me the behavior I need whereas #StateObject does not.
Here are the differences I'm paying attention to:
When using #ObservedObject, I can tap the black area and see the changes applied to both the white text and red rectangle. I can also tap the red rectangle and see the changes observed in ParentView through the white text.
When using #StateObject, I can tap the black area and see the changes applied to both the white text and red rectangle. The problem lies in that I can tap the red rectangle here and see the changes reflected in ParentView but ChildView doesn't recognize the change (rotation does not change and "Change Detected" is not printed).
Is #ObservedObject actually correct since ViewModel contains a #Binding to a #State created in ParentView?
Normally, I would not write such a convoluted solution to a problem, but it sounds like from your comments on another answer there are certain architectural issues that you are required to conform to.
The general issue with your initial approach is that onChange is only going to run when the view has a render triggered. Generally, that happens because some a passed-in property has changed, #State has changed, or a publisher on an ObservableObject has changed. In this case, none of those are true -- you have a Binding on your ObservableObject, but nothing that triggers the view to re-render. If Bindings provided a publisher, it would be easy to hook into that value, but since they do not, it seems like the logical approach is to store the state in the parent view in a way in which we can watch a #Published value.
Again, this is not necessarily the route I would take, but hopefully it fits your requirements:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
#Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
#StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
#StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}
Actually we don't use view model objects at all in SwiftUI, see [Data Essentials in SwiftUI WWDC 2020]. As shown in the video at 4:33 create a custom struct to hold the item, e.g. ChildViewConfig and init it in an #State in the parent. Set the childViewConfig.item in a handler or add any mutating custom funcs. Pass the binding $childViewConfig or $childViewConfig.item to the to the child View if you need write access. It's all very simple if you stick to structs and value semantics.
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).
Normally, we're restricted from discussing Apple prerelease stuff, but I've already seen plenty of SwiftUI discussions, so I suspect that it's OK; just this once.
I am in the process of driving into the weeds on one of the tutorials (I do that).
I am adding a pair of buttons below the swipeable screens in the "Interfacing With UIKit" tutorial: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
These are "Next" and "Prev" buttons. When at one end or the other, the corresponding button hides. I have that working fine.
The problem that I'm having, is accessing the UIPageViewController instance represented by the PageViewController.
I have the currentPage property changing (by making the PageViewController a delegate of the UIPageViewController), but I need to force the UIPageViewController to change programmatically.
I know that I can "brute force" the display by redrawing the PageView body, reflecting a new currentPage, but I'm not exactly sure how to do that.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
HStack(alignment: .center) {
Spacer()
if 0 < currentPage {
Button(action: {
self.prevPage()
}) {
Text("Prev")
}
Spacer()
}
Text(verbatim: "Page \(currentPage)")
if currentPage < viewControllers.count - 1 {
Spacer()
Button(action: {
self.nextPage()
}) {
Text("Next")
}
}
Spacer()
}
}
}
func nextPage() {
if currentPage < viewControllers.count - 1 {
currentPage += 1
}
}
func prevPage() {
if 0 < currentPage {
currentPage -= 1
}
}
}
I know the answer should be obvious, but I'm having difficulty figuring out how to programmatically refresh the VStack or body.
2021 SWIFT 1 and 2 both:
IMPORTANT THING! If you search for this hack, probably you doing something wrong! Please, read this block before you read hack solution!!!!!!!!!!
Your UI wasn't updated automatically because of you miss something
important.
Your ViewModel must be a class wrapped into ObservableObject/ObservedObject
Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
Must be used modifiers correctly (state, observable/observedObject, published, binding, etc)
If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/Observed object and assign them into View's object !!!!!!!! inside init() of View. !!!!!!!
Sometimes is needed to use hacks. But this is really-really-really exclusive situation! In most cases this wrong way! One more time: Please, use structs instead of classes!
Your UI will be refreshed automatically if all of written above was used correctly.
Sample of correct usage:
struct SomeView : View {
#ObservedObject var model : SomeViewModel
#ObservedObject var someClassValue: MyClass
init(model: SomeViewModel) {
self.model = model
//as this is class we must do it observable and assign into view manually
self.someClassValue = model.someClassValue
}
var body: some View {
//here we can use model.someStructValue directly
// or we can use local someClassValue taken from VIEW, BUT NOT value from model
}
}
class SomeViewModel : ObservableObject {
#Published var someStructValue: Bool = false
var someClassValue: MyClass = MyClass() //myClass : ObservableObject
}
And the answer on topic question.
(hacks solutions - prefer do not use this)
Way 1: declare inside of view:
#State var updater: Bool = false
all you need to do is call updater.toggle()
Way 2: refresh from ViewModel
Works on SwiftUI 2
public class ViewModelSample : ObservableObject
func updateView(){
self.objectWillChange.send()
}
}
Way 3: refresh from ViewModel:
works on SwiftUI 1
import Combine
import SwiftUI
class ViewModelSample: ObservableObject {
private let objectWillChange = ObservableObjectPublisher()
func updateView(){
objectWillChange.send()
}
}
This is another solution what worked for me, using id() identifier. Basically, we are not really refreshing view. We are replacing the view with a new one.
import SwiftUI
struct ManualUpdatedTextField: View {
#State var name: String
var body: some View {
VStack {
TextField("", text: $name)
Text("Hello, \(name)!")
}
}
}
struct MainView: View {
#State private var name: String = "Tim"
#State private var theId = 0
var body: some View {
VStack {
Button {
name += " Cook"
theId += 1
} label: {
Text("update Text")
.padding()
.background(Color.blue)
}
ManualUpdatedTextField(name: name)
.id(theId)
}
}
}
Setting currentPage, as it is a #State, will reload the whole body.