Save / restore array of booleans to core date - swift

Building my first SwiftUI app, and have some basic knowledge of Swift. So a bit much to chew but I am enjoying learning.
I have a Form with many toggles saving/restoring from core data in my swift app. Works well but the interface is cumbersome with all the toggles.
Instead I want to make an HStack of tappable labels that will be selected / unselected instead. Then when you submit it will map the selected Text objects to the existing State variables I have OR? save an array of selected strings to core data (for restoring later?).
In either case my code for this has been cobbled from a todo list tutorial plus some nice HStack examples I have put in my form. They select/deselect nicely but I do not know how to save their state like I did the toggle switches.
I will paste what I think is relevant code and remove the rest.
#State var selectedItems: [String] = []
#State private var hadSugar = false
#State private var hadGluten = false
#State private var hadDairy = false
let dayvariablesText = [
"Sugar",
"Gluten",
"Dairy"
]
// section 1 works fine
Section {
VStack {
Section(header: Text("Actions")) {
Toggle("Sugar", isOn: $hadSugar)
Toggle("Gluten", isOn: $hadGluten)
Toggle("Dairy", isOn: $hadDairy)
}
}
}
// section 2 trying this
ScrollView(.horizontal) {
LazyHGrid(rows: rows) {
ForEach(0..<dayvariablesText.count, id: \.self) { item in
GridColumn(item: dayvariablesText[item], items: $selectedItems)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
// save
Button("Submit") {
DataController().addMood(sugar: hadSugar, gluten: hadGluten, dairy: hadDairy, context: managedObjContext)
dismiss()
}
This works fine with the toggles shown above - how to do this when selecting gridItems in the next section for example?

I think you need to remodel your code. Having multiple sources of truth like in your example (with the vars and the array for the naming) is a bad practice and will hurt you in the long run.
Consider this solution. As there is a lot missing in your question it´s more general. So you need to implement it to fit your needs. But it should get you in the right direction.
//Create an enum to define your items
// naming needs some improvement :)
enum MakroType: String, CaseIterable{
case sugar = "Sugar", gluten = "Gluten", dairy = "Dairy"
}
//This struct will hold types you defined earlier
// including the bool indicating if hasEaten
struct Makro: Identifiable{
var id: MakroType {
makro
}
var makro: MakroType
var hasEaten: Bool
}
// The viewmodel will help you store and load the data
class Viewmodel: ObservableObject{
//define and create the array to hold the Makro structs
#Published var makros: [Makro] = []
init(){
// load the data either here or in the view
// when it appears
loadCoreData()
}
func loadCoreData(){
//load items
// ..... code here
// if no items assign default ones
if makros.isEmpty {
makros = MakroType.allCases.map{
Makro(makro: $0, hasEaten: false)
}
}
}
// needs to be implemented
func saveCoreData(){
print(makros)
}
}
struct ContentView: View {
// Create an instance of the Viewmodel here
#StateObject private var viewmodel: Viewmodel = Viewmodel()
var body: some View {
VStack{
ScrollView(.horizontal) {
LazyHStack {
// Iterate over the items themselves and not over the indices
// with the $ in front you can pass a binding on to the ChildView
ForEach($viewmodel.makros) { $makro in
SubView(makro: $makro)
}
}
}.frame(width: 400, height: 100, alignment: .topLeading)
Spacer()
Button("Save"){
viewmodel.saveCoreData()
}.padding()
}
.padding()
}
}
struct SubView: View{
// Hold the binding to the Makro here
#Binding var makro: Makro
var body: some View{
//Toggle to change the hasEaten Bool
//this will reflect through the Binding into the Viewmodel
Toggle(makro.makro.rawValue, isOn: $makro.hasEaten)
}
}

Related

SwiftUI passing an observed object into a new view and getting updates

I am very new to swift working on my first app and having trouble having a view update. I am passing an object into a new view, however the new view does not update when there is change in the Firebase Database. Is there a way to get updates on the Gridview? I though by passing the observed object from the StyleboardView it would update the GridView however Gridview does not update. I am having trouble finding a way for the new Gridview to update and reload the images.
struct StyleBoardView: View {
#State private var showingSheet = false
#ObservedObject var model = ApiModel()
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List (model.list) {item in
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: item)
}
}
struct GridView: View {
var item: Todo
#ObservedObject var model = ApiModel()
#State var newImage = ""
#State var loc = ""
#State var shouldShowImagePicker = false
#State var image: UIImage?
var body: some View {
NavigationView {
var posts = item.styleboardimages
VStack(alignment: .leading){
Text(item.styleboardname)
GeometryReader{ geo in
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 3 ){
ForEach(posts.sorted(by: <), id: \.key) { key, value in
if #available(iOS 15.0, *) {
AsyncImage(url: URL(string: value), transaction: Transaction(animation: .spring())) { phase in
switch phase {
case .empty:
Color.purple.opacity(0.1)
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure(_):
Image(systemName: "exclamationmark.icloud")
.resizable()
.scaledToFit()
#unknown default:
Image(systemName: "exclamationmark.icloud")
}
}
.frame(width: 100, height: 100)
.cornerRadius(20)
You have a few problems with the code. First of all, the original view that creates the view model, or has created for it originally, should own the object. Therefore you declare it as a #StateObject.
struct StyleBoardView: View {
#State private var showingSheet = false
#StateObject var model = ApiModel() // #StateObject here
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List ($model.list) { $item in // Change this to pass a Binding
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: $item, model: model)
}
}
}
}
}
}
Since you are passing to a .sheet, that will not automatically be re-rendered when StyleBoardView's model changes, so you have to use a #Binding to cause GridView to re-render. Lastly, once you have your #StateObject, you pass that to your next view. Otherwise, you continually make new models, so updates to one will not update the other.
struct GridView: View {
#Binding var item: Todo // Make this a #Binding so it reacts to the changes.
#ObservedObject var model: ApiModel // Pass the originally created view model in.
...
var body: some View {
NavigationView {
...
}
}
}
Lastly, you did not post a Minimal, Reproducible Example (MRE). You also did not post the complete GridView struct. You may not even need your view model in that view as you do not use it in what you have posted.
The problem is that you're initializing the model in an ObservedObject, and passing it down to another initialized Observed Object.
What you actually wanna do is use an #StateObject for where you initialize the model. And then use #ObservedObject with the type of the model you're passing down so that:
struct StyleBoardView: View {
#StateObject var model = ApiModel()
/** Code **/
struct GridView: View {
#ObservedObject var model: ApiModel
Notice the difference, an #ObservedObject should never initialize the model, it should only "inherit" (#ObservedObject var model: ApiModel) a model from a parent View, in this case, ApiModel.

Updating SwiftUI view from DB

I am trying to update a view fro a database. As the user types into a field, on each keypress an SQL query is fired that returns a list of items to display.
The problem I have is that the key pres causes the view to redraw and then the DB query changes the data in the view.
I have looked at future and promise but dont understand the swift versions (OK with Java and Scala concurrency).
I have tried using an Observalble instead of a state but get all kinds of problems with that also.
I think I am trying to do this the wrong way but can t think of a better way to try.
It would be nice if I could call the DB method asynchronously and get it to update the data array sometime in the future but just cant work out how to do it or an alternative method.
It might be that I am triggering the DB query from the wrong place, triggers at the start of the view body. The keypress updates an #State variable which triggers the redraw, a List is used to display each field of the DB records held in an array in Text objects. it all works perfectly with static data.
As you can see below not exactly the most complex thing.
struct CompanySearchView: View {
#ObservedObject var viewRouter: ViewRouter
#State private var name: String = ""
#ObservedObject var companys: Companys = nil
var body: some View {
incSearch()
return VStack(spacing: 12){
Text("Company Search")
.font(.headline)
HStack(spacing: 12){
Text("Company name")
.font(.headline)
TextField("Company name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
List(companys.data){item in
Text(item.name)
}
}
.frame(minWidth: 480, minHeight: 300)
}
func incSearch(){
let connection = Connection(logger: self.viewRouter.logger!)
viewRouter.logger!.info("Search string \(name)");
companys.data = connection.search(n: name)
}
}
Many Thanks
struct CompanySearchView: View {
#ObservedObject var viewRouter: ViewRouter
#State private var name: String = ""
#ObservedObject private var companys: Companys
init(viewRouter: ViewRouter){
self.viewRouter = viewRouter
companys = Companys(router: viewRouter)
}
var body: some View {
companys.changeName(newName: name)
return VStack(spacing: 12){
Text("Company Search")
.font(.headline)
HStack(spacing: 12){
Text("Company name")
.font(.headline)
TextField("Company name", text: $name, onEditingChanged: { (changed) in
self.companys.changing = changed
})
.textFieldStyle(RoundedBorderTextFieldStyle())
}
List(companys.data){item in
Text(item.name)
}
}
.frame(minWidth: 480, minHeight: 300)
}
}
struct CompanySearchView_Previews: PreviewProvider {
static var previews: some View {
CompanySearchView(viewRouter: ViewRouter())
}
}
class Companys: ObservableObject {
private var name: String = ""
private let router: ViewRouter
var changing: Bool = false
#Published var data: [Company] = []
init(router: ViewRouter){
self.router = router
}
func changeName(newName: String){
if changing && name != newName {
name = newName;
let connection = Connection(logger: self.router.logger!)
router.logger!.info("Search string \(name)");
connection.search(n: name).then {
result in
self.data = result
print(result)
}
}
}
}
The main problem this solves (there may be better ways) is that when the original code run the first thing it did was run the DB request then render the view, no real problem there except it did a query that returned all companies and but the display remained blank. This is soled using the flag in the companys class set when the editing starts.
Now unless the field is being edited no search is done.
Using a Future the search is now performed asynchronously and updates the Companys class data which is published so that causes the view to be rendered.
Its not perfect as if the query executes very fast it is possible for the observed object to be updated before the current render g=has completed and back to square one.

SwiftUI ObservedObject causes undesirable visible view updates

I am working on an app that applies a filter to an image. The filter has a number of parameters that the user can modify. I have created an ObservableObject that contain said parameters. Whenever one of the parameters changes, there is a visible update for views, even if the view displays the same value as before. This does not happen when I model the parameters as individual #State variables.
If this is to be expected (after all the observed object does change, so each view depending on it will update), is an ObservedObject the right tool for the job? On the other hand it seems to be very inconvenient to model the parameters as individual #State/#Binding variables, especially if a large number of parameters (e.g. 10+) need to be passed to multiple subviews!
Hence my question:
Am I using ObservedObject correctly here? Are the visible updates unintended, but acceptable, or is there a better solution to handle this in swiftUI?
Example using #ObservedObject:
import SwiftUI
class Parameters: ObservableObject {
#Published var pill: String = "red"
#Published var hand: String = "left"
}
struct ContentView: View {
#ObservedObject var parameters = Parameters()
var body: some View {
VStack {
// Using the other Picker causes a visual effect here...
Picker(selection: self.$parameters.pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
// Using the other Picker causes a visual effect here...
Picker(selection: self.$parameters.hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
Example using #State variables:
import SwiftUI
struct ContentView: View {
#State var pill: String = "red"
#State var hand: String = "left"
var body: some View {
VStack {
Picker(selection: self.$pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
Picker(selection: self.$hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
Warning: This answer is less than ideal. If the properties of parameters will be updated in another view (e.g. an extra picker), the picker view will not be updated.
The ContentView should not 'observe' parameters; a change in parameters will cause it to update its content (which is visible in case of the Pickers). To prevent the need for the observed property wrapper, we can provide explicit bindings for parameter's properties instead. It is OK for a subview of ContentView to use #Observed on parameters.
import SwiftUI
class Parameters: ObservableObject {
#Published var pill: String = "red"
#Published var hand: String = "left"
}
struct ContentView: View {
var parameters = Parameters()
var handBinding: Binding<String> {
Binding<String>(
get: { self.parameters.hand },
set: { self.parameters.hand = $0 }
)
}
var pillBinding: Binding<String> {
Binding<String>(
get: { self.parameters.pill },
set: { self.parameters.pill = $0 }
)
}
var body: some View {
VStack {
InfoDisplay(parameters: parameters)
Picker(selection: self.pillBinding, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
Picker(selection: self.handBinding, label: Text("Which hand?")) {
Text("left" ).tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct InfoDisplay: View {
#ObservedObject var parameters: Parameters
var body: some View {
Text("I took the \(parameters.pill) pill from your \(parameters.hand) hand!")
}
}
Second attempt
ContentView should not observe parameters (this causes the undesired visible update). The properties of parameters should be ObservableObjects as well to make sure views can update when a specific property changes.
Since Strings are structs they cannot conform to ObservableObject; a small wrapper 'ObservableValue' is necessary.
MyPicker is a small wrapper around Picker to make the view update on changes. The default Picker accepts a binding and thus relies on a view up the hierarchy to perform updates.
This approach feels scalable:
There is a single source of truth (parameters in ContentView)
Views only update when necessary (no undesired visual effects)
Disadvantages:
Seems a lot of boilerplate code for something that feels so trivial it should be provided by the platform (I feel I am missing something)
If you add a second MyPicker for the same property, the updates are not instantaneous.
import SwiftUI
import Combine
class ObservableValue<Value: Hashable>: ObservableObject {
#Published var value: Value
init(initialValue: Value) {
value = initialValue
}
}
struct MyPicker<Value: Hashable, Label: View, Content : View>: View {
#ObservedObject var object: ObservableValue<Value>
let content: () -> Content
let label: Label
init(object: ObservableValue<Value>,
label: Label,
#ViewBuilder _ content: #escaping () -> Content) {
self.object = object
self.label = label
self.content = content
}
var body: some View {
Picker(selection: $object.value, label: label, content: content)
.pickerStyle(SegmentedPickerStyle())
}
}
class Parameters: ObservableObject {
var pill = ObservableValue(initialValue: "red" )
var hand = ObservableValue(initialValue: "left")
private var subscriber: Any?
init() {
subscriber = pill.$value
.combineLatest(hand.$value)
.sink { _ in
self.objectWillChange.send()
}
}
}
struct ContentView: View {
var parameters = Parameters()
var body: some View {
VStack {
InfoDisplay(parameters: parameters)
MyPicker(object: parameters.pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}
MyPicker(object: parameters.hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}
}
}
}
struct InfoDisplay: View {
#ObservedObject var parameters: Parameters
var body: some View {
Text("I took the \(parameters.pill.value) pill from your \(parameters.hand.value) hand!")
}
}

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

SwiftUI and MVVM - Communication between model and view model

I've been experimenting with the MVVM model that's used in SwiftUI and there are some things I don't quite get yet.
SwiftUI uses #ObservableObject/#ObservedObject to detect changes in a view model that trigger a recalculation of the body property to update the view.
In the MVVM model, that's the communication between the view and the view model. What I don't quite understand is how the model and the view model communicate.
When the model changes, how is the view model supposed to know that? I thought about manually using the new Combine framework to create publishers inside the model that the view model can subscribe to.
However, I created a simple example that makes this approach pretty tedious, I think. There's a model called Game that holds an array of Game.Character objects. A character has a strength property that can change.
So what if a view model changes that strength property of a character? To detect that change, the model would have to subscribe to every single character that the game has (among possibly many other things). Isn't that a little too much? Or is it normal to have many publishers and subscribers?
Or is my example not properly following MVVM? Should my view model not have the actual model game as property? If so, what would be a better way?
// My Model
class Game {
class Character {
let name: String
var strength: Int
init(name: String, strength: Int) {
self.name = name
self.strength = strength
}
}
var characters: [Character]
init(characters: [Character]) {
self.characters = characters
}
}
// ...
// My view model
class ViewModel: ObservableObject {
let objectWillChange = PassthroughSubject<ViewModel, Never>()
let game: Game
init(game: Game) {
self.game = game
}
public func changeCharacter() {
self.game.characters[0].strength += 20
}
}
// Now I create a demo instance of the model Game.
let bob = Game.Character(name: "Bob", strength: 10)
let alice = Game.Character(name: "Alice", strength: 42)
let game = Game(characters: [bob, alice])
// ..
// Then for one of my views, I initialize its view model like this:
MyView(viewModel: ViewModel(game: game))
// When I now make changes to a character, e.g. by calling the ViewModel's method "changeCharacter()", how do I trigger the view (and every other active view that displays the character) to redraw?
I hope it's clear what I mean. It's difficult to explain because it is confusing
Thanks!
I've spent the few last hours playing around with the code and I think I've come up with a pretty good way of doing this. I don't know if that's the intended way or if it's proper MVVM but it seems to work and it's actually quite convenient.
I will post an entire working example below for anyone to try out. It should work out of the box.
Here are some thoughts (which might be complete garbage, I don't know anything about that stuff yet. Please correct me if I'm wrong :))
I think that view models probably shouldn't contain or save any actual data from the model. Doing this would effectively create a copy of what's already saved in the model layer. Having data stored in multiple places causes all kinds of synchronization and update problems you have to consider when changing anything. Everything I tried ended up being a huge, unreadable chunk of ugly code.
Using classes for the data structures inside the model doesn't really work well because it makes detecting changes more cumbersome (changing a property doesn't change the object). Thus, I made the Character class a struct instead.
I spent hours trying to figure out how to communicate changes between the model layer and the view model. I tried setting up custom publishers, custom subscribers that track any changes and update the view model accordingly, I considered having the model subscribe to the view model as well to establish two-way communication, etc. Nothing worked out. It felt unnatural. But here's the thing: The model doesn't have to communicate with the view model. In fact, I think it shouldn't at all. That's probably what MVVM is about. The visualisation shown in an MVVM tutorial on raywenderlich.com shows this as well:
(Source: https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios)
That's a one-way connection. The view model reads from the model and maybe makes changes to the data but that's it.
So instead of having the model tell the view model about any changes, I simply let the view detect changes to the model by making the model an ObservableObject. Every time it changes, the view is being recalculated which calls methods and properties on the view model. The view model, however, simply grabs the current data from the model (as it only accesses and never saves them) and provides it to the view. The view model simply doesn't have to know whether or not the model has been updated. It doesn't matter.
With that in mind, it wasn't hard to make the example work.
Here's the example app to demonstrate everything. It simply shows a list of all characters while simultaneously displaying a second view that shows a single character.
Both views are synched when making changes.
import SwiftUI
import Combine
/// The model layer.
/// It's also an Observable object so that swiftUI can easily detect changes to it that trigger any active views to redraw.
class MyGame: ObservableObject {
/// A data object. It should be a struct so that changes can be detected very easily.
struct Character: Equatable, Identifiable {
var id: String { return name }
let name: String
var strength: Int
static func ==(lhs: Character, rhs: Character) -> Bool {
lhs.name == rhs.name && lhs.strength == rhs.strength
}
/// Placeholder character used when some data is not available for some reason.
public static var placeholder: Character {
return Character(name: "Placeholder", strength: 301)
}
}
/// Array containing all the game's characters.
/// Private setter to prevent uncontrolled changes from outside.
#Published public private(set) var characters: [Character]
init(characters: [Character]) {
self.characters = characters
}
public func update(_ character: Character) {
characters = characters.map { $0.name == character.name ? character : $0 }
}
}
/// A View that lists all characters in the game.
struct CharacterList: View {
/// The view model for CharacterList.
class ViewModel: ObservableObject {
/// The Publisher that SwiftUI uses to track changes to the view model.
/// In this example app, you don't need that but in general, you probably have stuff in the view model that can change.
let objectWillChange = PassthroughSubject<Void, Never>()
/// Reference to the game (the model).
private var game: MyGame
/// The characters that the CharacterList view should display.
/// Important is that the view model should not save any actual data. The model is the "source of truth" and the view model
/// simply accesses the data and prepares it for the view if necessary.
public var characters: [MyGame.Character] {
return game.characters
}
init(game: MyGame) {
self.game = game
}
}
#ObservedObject var viewModel: ViewModel
// Tracks what character has been selected by the user. Not important,
// just a mechanism to demonstrate updating the model via tapping on a button
#Binding var selectedCharacter: MyGame.Character?
var body: some View {
List {
ForEach(viewModel.characters) { character in
Button(action: {
self.selectedCharacter = character
}) {
HStack {
ZStack(alignment: .center) {
Circle()
.frame(width: 60, height: 40)
.foregroundColor(Color(UIColor.secondarySystemBackground))
Text("\(character.strength)")
}
VStack(alignment: .leading) {
Text("Character").font(.caption)
Text(character.name).bold()
}
Spacer()
}
}
.foregroundColor(Color.primary)
}
}
}
}
/// Detail view.
struct CharacterDetail: View {
/// The view model for CharacterDetail.
/// This is intentionally only slightly different to the view model of CharacterList to justify a separate view model class.
class ViewModel: ObservableObject {
/// The Publisher that SwiftUI uses to track changes to the view model.
/// In this example app, you don't need that but in general, you probably have stuff in the view model that can change.
let objectWillChange = PassthroughSubject<Void, Never>()
/// Reference to the game (the model).
private var game: MyGame
/// The id of a character (the name, in this case)
private var characterId: String
/// The characters that the CharacterList view should display.
/// This does not have a `didSet { objectWillChange.send() }` observer.
public var character: MyGame.Character {
game.characters.first(where: { $0.name == characterId }) ?? MyGame.Character.placeholder
}
init(game: MyGame, characterId: String) {
self.game = game
self.characterId = characterId
}
/// Increases the character's strength by one and updates the game accordingly.
/// - **Important**: If the view model saved its own copy of the model's data, this would be the point
/// where everything goes out of sync. Thus, we're using the methods provided by the model to let it modify its own data.
public func increaseCharacterStrength() {
// Grab current character and change it
var character = self.character
character.strength += 1
// Tell the model to update the character
game.update(character)
}
}
#ObservedObject var viewModel: ViewModel
var body: some View {
ZStack(alignment: .center) {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.padding()
.foregroundColor(Color(UIColor.secondarySystemBackground))
VStack {
Text(viewModel.character.name)
.font(.headline)
Button(action: {
self.viewModel.increaseCharacterStrength()
}) {
ZStack(alignment: .center) {
Circle()
.frame(width: 80, height: 80)
.foregroundColor(Color(UIColor.tertiarySystemBackground))
Text("\(viewModel.character.strength)").font(.largeTitle).bold()
}.padding()
}
Text("Tap on circle\nto increase number")
.font(.caption)
.lineLimit(2)
.multilineTextAlignment(.center)
}
}
}
}
struct WrapperView: View {
/// Treat the model layer as an observable object and inject it into the view.
/// In this case, I used #EnvironmentObject but you can also use #ObservedObject. Doesn't really matter.
/// I just wanted to separate this model layer from everything else, so why not have it be an environment object?
#EnvironmentObject var game: MyGame
/// The character that the detail view should display. Is nil if no character is selected.
#State var showDetailCharacter: MyGame.Character? = nil
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Tap on a character to increase its number")
.padding(.horizontal, nil)
.font(.caption)
.lineLimit(2)
CharacterList(viewModel: CharacterList.ViewModel(game: game), selectedCharacter: $showDetailCharacter)
if showDetailCharacter != nil {
CharacterDetail(viewModel: CharacterDetail.ViewModel(game: game, characterId: showDetailCharacter!.name))
.frame(height: 300)
}
}
.navigationBarTitle("Testing MVVM")
}
}
}
struct WrapperView_Previews: PreviewProvider {
static var previews: some View {
WrapperView()
.environmentObject(MyGame(characters: previewCharacters()))
.previewDevice(PreviewDevice(rawValue: "iPhone XS"))
}
static func previewCharacters() -> [MyGame.Character] {
let character1 = MyGame.Character(name: "Bob", strength: 1)
let character2 = MyGame.Character(name: "Alice", strength: 42)
let character3 = MyGame.Character(name: "Leonie", strength: 58)
let character4 = MyGame.Character(name: "Jeff", strength: 95)
return [character1, character2, character3, character4]
}
}
Thanks Quantm for posting an example code above. I followed your example, but simplified a bit. The changes I made:
No need to use Combine
The only connection between view model and view is the binding SwiftUI provides. eg: use #Published (in view model) and #ObservedObject (in view) pair. We could also use #Published and #EnvironmentObject pair if we want to build bindings across multiple views with the view model.
With these changes, the MVVM setup is pretty straightforward and the two-way communication between the view model and view is all provided by the SwiftUI framework, there is no need to add any additional calls to trigger any update, it all happens automatically. Hope this also helps answer your original question.
Here is the working code that does about the same as your sample code above:
// Character.swift
import Foundation
class Character: Decodable, Identifiable{
let id: Int
let name: String
var strength: Int
init(id: Int, name: String, strength: Int) {
self.id = id
self.name = name
self.strength = strength
}
}
// GameModel.swift
import Foundation
struct GameModel {
var characters: [Character]
init() {
// Now let's add some characters to the game model
// Note we could change the GameModel to add/create characters dymanically,
// but we want to focus on the communication between view and viewmodel by updating the strength.
let bob = Character(id: 1000, name: "Bob", strength: 10)
let alice = Character(id: 1001, name: "Alice", strength: 42)
let leonie = Character(id: 1002, name: "Leonie", strength: 58)
let jeff = Character(id: 1003, name: "Jeff", strength: 95)
self.characters = [bob, alice, leonie, jeff]
}
func increaseCharacterStrength(id: Int) {
let character = characters.first(where: { $0.id == id })!
character.strength += 10
}
func selectedCharacter(id: Int) -> Character {
return characters.first(where: { $0.id == id })!
}
}
// GameViewModel
import Foundation
class GameViewModel: ObservableObject {
#Published var gameModel: GameModel
#Published var selectedCharacterId: Int
init() {
self.gameModel = GameModel()
self.selectedCharacterId = 1000
}
func increaseCharacterStrength() {
self.gameModel.increaseCharacterStrength(id: self.selectedCharacterId)
}
func selectedCharacter() -> Character {
return self.gameModel.selectedCharacter(id: self.selectedCharacterId)
}
}
// GameView.swift
import SwiftUI
struct GameView: View {
#ObservedObject var gameViewModel: GameViewModel
var body: some View {
NavigationView {
VStack {
Text("Tap on a character to increase its number")
.padding(.horizontal, nil)
.font(.caption)
.lineLimit(2)
CharacterList(gameViewModel: self.gameViewModel)
CharacterDetail(gameViewModel: self.gameViewModel)
.frame(height: 300)
}
.navigationBarTitle("Testing MVVM")
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView(gameViewModel: GameViewModel())
.previewDevice(PreviewDevice(rawValue: "iPhone XS"))
}
}
//CharacterDetail.swift
import SwiftUI
struct CharacterDetail: View {
#ObservedObject var gameViewModel: GameViewModel
var body: some View {
ZStack(alignment: .center) {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.padding()
.foregroundColor(Color(UIColor.secondarySystemBackground))
VStack {
Text(self.gameViewModel.selectedCharacter().name)
.font(.headline)
Button(action: {
self.gameViewModel.increaseCharacterStrength()
self.gameViewModel.objectWillChange.send()
}) {
ZStack(alignment: .center) {
Circle()
.frame(width: 80, height: 80)
.foregroundColor(Color(UIColor.tertiarySystemBackground))
Text("\(self.gameViewModel.selectedCharacter().strength)").font(.largeTitle).bold()
}.padding()
}
Text("Tap on circle\nto increase number")
.font(.caption)
.lineLimit(2)
.multilineTextAlignment(.center)
}
}
}
}
struct CharacterDetail_Previews: PreviewProvider {
static var previews: some View {
CharacterDetail(gameViewModel: GameViewModel())
}
}
// CharacterList.swift
import SwiftUI
struct CharacterList: View {
#ObservedObject var gameViewModel: GameViewModel
var body: some View {
List {
ForEach(gameViewModel.gameModel.characters) { character in
Button(action: {
self.gameViewModel.selectedCharacterId = character.id
}) {
HStack {
ZStack(alignment: .center) {
Circle()
.frame(width: 60, height: 40)
.foregroundColor(Color(UIColor.secondarySystemBackground))
Text("\(character.strength)")
}
VStack(alignment: .leading) {
Text("Character").font(.caption)
Text(character.name).bold()
}
Spacer()
}
}
.foregroundColor(Color.primary)
}
}
}
}
struct CharacterList_Previews: PreviewProvider {
static var previews: some View {
CharacterList(gameViewModel: GameViewModel())
}
}
// SceneDelegate.swift (only scene func is provided)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let gameViewModel = GameViewModel()
window.rootViewController = UIHostingController(rootView: GameView(gameViewModel: gameViewModel))
self.window = window
window.makeKeyAndVisible()
}
}
Short answer is to use #State, whenever state property changes, view is rebuilt.
Long answer is to update MVVM paradigm per SwiftUI.
Typically for something to be a "view model", some binding mechanism needs to be associated with it. In your case there's nothing special about it, it is just another object.
The binding provided by SwiftUI comes from value type conforming to View protocol. This set it apart from Android where there's no value type.
MVVM is not about having an object called view model. It's about having model-view binding.
So instead of model -> view model -> view hierarchy, it's now struct Model: View with #State inside.
All in one instead of nested 3 level hierarchy. It may go against everything you thought you knew about MVVM. In fact I'd say it's an enhanced MVC architecture.
But binding is there. Whatever benefit you can get from MVVM binding, SwiftUI has it out-of-box. It just presents in an unique form.
As you stated, it would be tedious to do manual binding around view model even with Combine, because SDK deems it not necessary to provide such binding as of yet. (I doubt it ever will, since it's a major improvement over traditional MVVM in its current form)
Semi-pseudo code to illustrate above points:
struct GameModel {
// build your model
}
struct Game: View {
#State var m = GameModel()
var body: some View {
// access m
}
// actions
func changeCharacter() { // mutate m }
}
Note how simple this is. Nothing beats simplicity. Not even "MVVM".
To alert the #Observed variable in your View, change objectWillChange to
PassthroughSubject<Void, Never>()
Also, call
objectWillChange.send()
in your changeCharacter() function.