Is there a way to update only certain elements in an EnvironmentObject - swift

For example, I would like each list element to be independent of the other's data.
Such that, changing the name of one will not affect the other. But I also want the list to be updated if they are altered from another view.
struct ContentView: View {
var listView = People()
let tempPerson = Person()
var body: some View {
VStack {
Button(action: {
self.listView.people.append(self.tempPerson)
}){
Text("Add person")
}
ListView().environmentObject(listView)
}.onAppear{self.listView.people.append(self.tempPerson)}
}
}
struct ListView : View {
#EnvironmentObject var listView : People
var body: some View{
List(listView.people.indices, id: \.self) { index in
TextField("StringProtocol", text: self.$listView.people[index].name)
}
}
}
class Person {
var name = "abc"
}
class People : ObservableObject {
#Published var people : [Person]
init() {
self.people = []
}
}

Person is a class. Which means if you create:
let tempPerson = Person()
then in every place inside your ContentView you'll refer to the same Person instance - and you'll append the same person in your button action:
Button(action: {
self.listView.people.append(self.tempPerson)
}) {
Text("Add person")
}
I recommend you change your Person to be a struct instead of a class:
struct Person {
var name = "abc"
}
As structs are copied, every time you append a new item:
self.listView.people.append(self.tempPerson)
it will be a copy of the original tempPerson and you'll be able to change your items independently.
You can read more here: Structures and Classes

Related

Keeping data in SwiftUI MVVM hierarchy without persistence

I am new to SwiftUI, coming from Java. I know MVC pattern and try to understand MVVM in SwiftUI. It kindly works, but I don't know how to keep data between View hierarchy... also the View should not know anything about models, so I tried using the ViewModels passing around the views. Each ViewModel than should manage their Model.
First Question: Is this a good way to implement MVVM in SwiftUI like I did?
Second: Anyone here who could help me out to divide strictly Model from View and getting this code working?
For now I can add a Garage, go into GarageView and add certain Car models. After switching back to GarageListView and adding another Garage model all Car models from first Garage model are gone... what am I doing wrong? ;(
import SwiftUI
// Models
struct Car: Identifiable, Codable {
var id = UUID()
var name: String
}
struct Garage: Identifiable, Codable {
var id = UUID()
var name: String
var cars: [Car] = []
}
// ViewModels
class GarageListViewModel: ObservableObject {
#Published var garages: [Garage] = []
}
class GarageViewModel: ObservableObject {
#Published var garage: Garage
init(garage: Garage) {
self.garage = garage
}
}
class CarViewModel: ObservableObject {
#Published var car: Car
init(car: Car) {
self.car = car
}
}
// Views
struct GarageListView: View {
#ObservedObject var viewModel: GarageListViewModel
var body: some View {
List {
ForEach(viewModel.garages) { garage in
NavigationLink {
GarageView(viewModel: GarageViewModel(garage: garage))
} label: {
Text(garage.name)
}
}
}
.toolbar {
Button {
viewModel.garages.append(Garage(name: "My Garage"))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct GarageView: View {
#ObservedObject var viewModel: GarageViewModel
var body: some View {
List {
ForEach(viewModel.garage.cars) { car in
NavigationLink {
CarDetailView(viewModel: CarViewModel(car: car))
} label: {
Text(car.name)
}
}
}
.toolbar {
Button {
viewModel.garage.cars.append(Car(name: "My Car"))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct CarDetailView: View {
#ObservedObject var viewModel: CarViewModel
var body: some View {
Text(viewModel.car.name)
}
}
struct ContentView: View {
#StateObject var viewModel = GarageListViewModel()
var body: some View {
NavigationView {
GarageListView(viewModel: viewModel)
}
}
}
I would switch from using struct to class. Structs are passed by value, so you'd never get the exact same struct but a copy of it. By adding a new GarageViewModel, SwiftUI will rerender the List, that's why you lose your garages' cars when you add a new garage.
On a side note, you also forgot to add a NavigationView around your GarageListView and the List in the GarageView, rendering the NavigationLinks useless.
It can be a bit overwhelming to get into MVVC. I would definitely go through Apple's SwiftUI Tutorials as they subtly go through how to set up an MVVC.
As #loremipsum said, you should never init your ViewModel in a view struct because structs are immutable value types, and SwiftUI simply discards the view and recreates it, recreating EVERYTHING inside of it. Also, as you said yourself, your views shouldn't know about the inner workings of the model, so the model should change itself. Therefore, adding garages and cars should be handled in the ViewModel.
Another thing with the ViewModel; you do not need one for EACH view. In this case, one GaragesViewModel can handle all your data. And it should. Apple talks about having a "single source of truth'. That means that there is ONE place your views can go and get the data. That is your ViewModel. Unless you have a wholly unrelated set of data, keep it in one model.
Lastly, before some example code, not every view needs to have the model, or even a mutable parameter. Remember, views are disposable, and with them their parameters are disposed of. If you mutate the model, you will get a new view, so let constants are fine to use. I left your models alone and removed the CarViewModel. It is not needed in this example.
// ViewModel
class GaragesViewModel: ObservableObject {
// This initializes the model IN the model and provides a Singleton.
// You can refer to it anywhere you need to in code.
static var shared = GaragesViewModel()
#Published var garages: [Garage] = []
// Data manipulation in the model
public func addNewGarage() {
garages.append(Garage(name: "My Garage"))
}
public func add(car: Car, to garage:Garage) {
// The guard will stop the func if garage is not in garages.
guard let index = garages.firstIndex(where: { $0.id == garage.id }) else { return }
garages[index].cars.append(car)
}
}
// Views
struct ContentView: View {
var body: some View {
NavigationView {
GarageListView()
}
}
}
struct GarageListView: View {
// Since this is the only view that needs the model, it is called here.
// There is no need to pass it in.
#ObservedObject var viewModel = GaragesViewModel.shared
var body: some View {
List {
ForEach(viewModel.garages) { garage in
NavigationLink {
GarageView(garage: garage)
} label: {
Text(garage.name)
}
}
}
.toolbar {
Button {
GaragesViewModel.shared.addNewGarage()
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct GarageView: View {
// GarageView doesn't mutate garage, so it can be a let constant. KISS principal.
let garage: Garage
var body: some View {
List {
ForEach(garage.cars) { car in
NavigationLink {
CarDetailView(car: car)
} label: {
Text(car.name)
}
}
}
.toolbar {
Button {
// if you wanted, you could add a func to car to return a new "My Car"
// and further separate the model.
GaragesViewModel.shared.add(car: Car(name: "My Car"), to: garage)
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct CarDetailView: View {
// CarDetailView doesn't mutate car, so it can be a let constant.
let car: Car
var body: some View {
Text(car.name)
}
}
Welcome to StackOverflow! You just got yourself too far into the weeds trying to implement this, but it was a good first shot.
edit:
You can add something like:
public func changeName(of car: Car, in garage: Garage, to name: String) {
guard let garageIndex = garages.firstIndex(where: { $0.id == garage.id }),
let carIndex = garage.cars.firstIndex(where: { $0.id == car.id }) else { return }
garages[garageIndex].cars[carIndex].name = name
}
to your model, and then use it like this:
struct CarDetailView: View {
// Make car an #State variable so you can change it, and then pass the change to the func in the model.
#State var car: Car
let garage: Garage
var body: some View {
VStack {
TextField("", text: $car.name)
.padding()
Button {
GaragesViewModel.shared.changeName(of: car, in: garage)
} label: {
Text("Save")
}
}
}
}
Now, this is for learning purposes only. I would not use this in a shipping app as you have no persistence of your data. If you need to create a database of cars and garages, etc. I would use CoreData to track it, and it works a little differently than just using the structs and class we have here.
Also, if you have any more questions, you really need to make a new question. The purpose of StackOverflow is to get discrete answers to discrete questions, and so follow on questions are discouraged.

How do I instantiate a view with multiple EnvironmentObjects?

I was learning about how EnvironmentObject works for a school project, and I was confused about how to instantiate a view with multiple EnvironmentObjects. For example, the following code:
import SwiftUI
class names: ObservableObject {
#Published var myName = ""
}
struct FirstView: View {
#StateObject var FirstName = names()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $FirstName.myName)
NavigationLink(destination: SecondView()) {
Text("Second View")
}
}
}.environmentObject(FirstName)
}
}
struct SecondView: View {
#StateObject var LastName = names()
var body: some View {
VStack {
TextField("Type", text: $LastName.myName)
NavigationLink(destination: ThirdView().environmentObject(FirstName).environmentObject(LastName)) {
Text("Third View")
}
}.environmentObject(LastName)
}
}
struct ThirdView: View {
#EnvironmentObject var FirstName: names
#EnvironmentObject var LastName: names
var body: some View {
Text("Full name: \(FirstName.myName) \(LastName.myName)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
I need ThirdView to receive FirstName from FirstView and LastName from SecondView, but I can't instantiate ThirdView from SecondView with the required Environment Objects; this code above crashes with the error "Cannot find FirstName in scope".
Alternatively, If I try to instantiate ThirdView with only LastName as an environment object, the code will present something like "Smith Smith" if I entered "John" in the text field on FirstView and "Smith" in the text field on SecondView.
Can someone tell me what I'm doing wrong? Thank you! :)
Since they are of the same type you can’t have two. SwiftUI can’t tell the difference
//Names for classes and structs should start with an uppercase letter
class PersonModel: ObservableObject {
#Published var firstName = ""
#Published var lastName = ""
}
struct FirstNameView: View {
//variables start with lowercase
#StateObject var person: PersonModel = PersonModel()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $person.firstName)
NavigationLink(destination: LastNameView()) {
Text("Second View")
}
}
}.environmentObject(person)
}
}
struct LastNameView: View {
#EnvironmentObject var person: PersonModel
var body: some View {
VStack {
TextField("Type", text: $person.lastName)
NavigationLink(destination: FullNameView()) {
Text("Third View")
}
}
}
}
struct FullNameView: View {
#EnvironmentObject var person: PersonModel
var body: some View {
Text("Full name: \(person.firstName) \(person.lastName)")
}
}
environmentObject(_:) modifier method takes an ObservableObject and passes it down the view tree. It works without specifying an environment key because the type of the object is automatically used as the key.
So to resume your last name instance is somehow invalidating your first name instance.
I'd then suggest either to create a model that contains both first and last name or simply use #Environment with a key (as it's suggested by Damiaan Dufaux) if it’s possible to get away with passing a value type, because it’s the safer mechanism.
You are probably looking for EnvironmentKeys.
Use them like this:
private struct FirstNameKey: EnvironmentKey {
static let defaultValue = "No first name"
}
private struct LastNameKey: EnvironmentKey {
static let defaultValue = "No last name"
}
And add them to your EnvironmentValues:
extension EnvironmentValues {
var firstName: String {
get { self[FirstNameKey.self] }
set { self[FirstNameKey.self] = newValue }
}
var lastName: String {
get { self[LastNameKey.self] }
set { self[LastNameKey.self] = newValue }
}
}
They values can then be bound to the environment like this:
var body: some View {
MyCustomView()
.environment(\.firstName, "John")
.environment(\.lastName, "Doe")
}
And retrieved like this:
struct ThirdView: View {
#Environment(\.firstName) var firstName
#Environment(\.lastName) var lastName
var body: some View {
Text("Full name: \(firstName) \(lastName)")
}
}
Side note on conventions
To understand code more easily the Swift.org community asks to
Give types UpperCamelCase names (such as SomeStructure and SomeClass here) to match the capitalization of standard Swift types (such as String, Int, and Bool). Give properties and methods lowerCamelCase names (such as frameRate and incrementCount) to differentiate them from type names.
So it would be better to write your class names as class Names as it greatly improves readability for Swift users.

SwiftUI List selection always nil

In my macOS app project, I have a SwiftUI List view of NavigationLinks build with a foreach loop from an array of items:
struct MenuView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List(selection: $settings.selectedWeek) {
ForEach(settings.weeks) { week in
NavigationLink(
destination: WeekView(week: week)
.environmentObject(settings)
tag: week,
selection: $settings.selectedWeek)
{
Image(systemName: "circle")
Text("\(week.name)")
}
}
.onDelete { set in
settings.weeks.remove(atOffsets: set)
}
.onMove { set, i in
settings.weeks.move(fromOffsets: set, toOffset: i)
}
}
.navigationTitle("Weekplans")
.listStyle(SidebarListStyle())
}
}
This view creates the sidebar menu for a overall NavigationView.
In this List view, I would like to use the selection mechanic together with tag from NavigationLink. Week is a custom model class:
struct Week: Identifiable, Hashable, Equatable {
var id = UUID()
var days: [Day] = []
var name: String
}
And UserSettings looks like this:
class UserSettings: ObservableObject {
#Published var weeks: [Week] = [
Week(name: "test week 1"),
Week(name: "foobar"),
Week(name: "hello world")
]
#Published var selectedWeek: Week? = UserDefaults.standard.object(forKey: "week.selected") as? Week {
didSet {
var a = oldValue
var b = selectedWeek
UserDefaults.standard.set(selectedWeek, forKey: "week.selected")
}
}
}
My goal is to directly store the value from List selection in UserDefaults. The didSet property gets executed, but the variable is always nil. For some reason the selected List value can't be stored in the published / bindable variable.
Why is $settings.selectedWeek always nil?
A couple of suggestions:
SwiftUI (specifically on macOS) is unreliable/unpredictable with certain List behaviors. One of them is selection -- there are a number of things that either completely don't work or at best are slightly broken that work fine with the equivalent iOS code. The good news is that NavigationLink and isActive works like a selection in a list -- I'll use that in my example.
#Published didSet may work in certain situations, but that's another thing that you shouldn't rely on. The property wrapper aspect makes it behave differently than one might except (search SO for "#Published didSet" to see a reasonable number of issues dealing with it). The good news is that you can use Combine to recreate the behavior and do it in a safer/more-reliable way.
A logic error in the code:
You are storing a Week in your user defaults with a certain UUID. However, you regenerate the array of weeks dynamically on every launch, guaranteeing that their UUIDs will be different. You need to store your week's along with your selection if you want to maintain them from launch to launch.
Here's a working example which I'll point out a few things about below:
import SwiftUI
import Combine
struct ContentView : View {
var body: some View {
NavigationView {
MenuView().environmentObject(UserSettings())
}
}
}
class UserSettings: ObservableObject {
#Published var weeks: [Week] = []
#Published var selectedWeek: UUID? = nil
private var cancellable : AnyCancellable?
private var initialItems = [
Week(name: "test week 1"),
Week(name: "foobar"),
Week(name: "hello world")
]
init() {
let decoder = PropertyListDecoder()
if let data = UserDefaults.standard.data(forKey: "weeks") {
weeks = (try? decoder.decode([Week].self, from: data)) ?? initialItems
} else {
weeks = initialItems
}
if let prevValue = UserDefaults.standard.string(forKey: "week.selected.id") {
selectedWeek = UUID(uuidString: prevValue)
print("Set selection to: \(prevValue)")
}
cancellable = $selectedWeek.sink {
if let id = $0?.uuidString {
UserDefaults.standard.set(id, forKey: "week.selected.id")
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(self.weeks) {
UserDefaults.standard.set(encoded, forKey: "weeks")
}
}
}
}
func selectionBindingForId(id: UUID) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.selectedWeek == id
} set: { (newValue) in
if newValue {
self.selectedWeek = id
}
}
}
}
//Unknown what you have in here
struct Day : Equatable, Hashable, Codable {
}
struct Week: Identifiable, Hashable, Equatable, Codable {
var id = UUID()
var days: [Day] = []
var name: String
}
struct WeekView : View {
var week : Week
var body: some View {
Text("Week: \(week.name)")
}
}
struct MenuView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List {
ForEach(settings.weeks) { week in
NavigationLink(
destination: WeekView(week: week)
.environmentObject(settings),
isActive: settings.selectionBindingForId(id: week.id)
)
{
Image(systemName: "circle")
Text("\(week.name)")
}
}
.onDelete { set in
settings.weeks.remove(atOffsets: set)
}
.onMove { set, i in
settings.weeks.move(fromOffsets: set, toOffset: i)
}
}
.navigationTitle("Weekplans")
.listStyle(SidebarListStyle())
}
}
In UserSettings.init the weeks are loaded if they've been saved before (guaranteeing the same IDs)
Use Combine on $selectedWeek instead of didSet. I only store the ID, since it seems a little pointless to store the whole Week struct, but you could alter that
I create a dynamic binding for the NavigationLinks isActive property -- the link is active if the stored selectedWeek is the same as the NavigationLink's week ID.
Beyond those things, it's mostly the same as your code. I don't use selection on List, just isActive on the NavigationLink
I didn't implement storing the Week again if you did the onMove or onDelete, so you would have to implement that.
Bumped into a situation like this where multiple item selection didn't work on macOS. Here's what I think is happening and how to workaround it and get it working
Background
So on macOS NavigationLinks embedded in a List render their Destination in a detail view (by default anyway). e.g.
struct ContentView: View {
let beatles = ["John", "Paul", "Ringo", "George", "Pete"]
#State var listSelection = Set<String>()
var body: some View {
NavigationView {
List(beatles, id: \.self, selection: $listSelection) { name in
NavigationLink(name) {
Text("Some details about \(name)")
}
}
}
}
}
Renders like so
Problem
When NavigationLinks are used it is impossible to select multiple items in the sidebar (at least as of Xcode 13 beta4).
... but it works fine if just Text elements are used without any NavigationLink embedding.
What's happening
The detail view can only show one NavigationLink View at a time and somewhere in the code (possibly NavigationView) there is piece of code that is enforcing that compliance by stomping on multiple selection and setting it to nil, e.g.
let selectionBinding = Binding {
backingVal
} set: { newVal in
guard newVal <= 1 else {
backingVal = nil
return
}
backingVal = newVal
}
What happens in these case is to the best of my knowledge not defined. With some Views such as TextField it goes out of sync with it's original Source of Truth (for more), while with others, as here it respects it.
Workaround/Fix
Previously I suggested using a ZStack to get around the problem, which works, but is over complicated.
Instead the idiomatic option for macOS, as spotted on the Lost Moa blog post is to not use NaviationLink at all.
It turns out that just placing sidebar and detail Views adjacent to each other and using binding is enough for NavigationView to understand how to render and stops it stomping on multiple item selections. Example shown below:
struct ContentView: View {
let beatles = ["John", "Paul", "Ringo", "George", "Pete"]
#State var listSelection: Set<String> = []
var body: some View {
NavigationView {
SideBar(items: beatles, selection: $listSelection)
Detail(ids: listSelection)
}
}
struct SideBar: View {
let items: Array<String>
#Binding var selection: Set<String>
var body: some View {
List(items, id: \.self, selection: $selection) { name in
Text(name)
}
}
}
struct Detail: View {
let ids: Set<String>
var detailsMsg: String {
ids.count == 1 ? "Would show details for \(ids.first)"
: ids.count > 1 ? "Too many items selected"
: "Nothing selected"
}
var body: some View {
Text(detailsMsg)
}
}
}
Have fun.

How to add something to a favorite list in SwiftUI?

I'm trying to create a Favorite list where I can add different items but it doesn't work. I made a simple code to show you what's going on.
// BookData gets data from Json
struct BookData: Codable {
var titolo: String
var descrizione: String
}
class FavoriteItems: ObservableObject {
#Published var favItems: [String] = []
}
struct ContentView: View {
#ObservedObject var bookData = BookDataLoader()
#ObservedObject var favoriteItems = FavoriteItems()
var body: some View {
NavigationView {
List {
NavigationLink(destination: FavoriteView()) {
Text("Go to favorites")
}
ForEach(0 ..< bookData.booksData.count) { num in
HStack {
Text("\(self.bookData.booksData[num].titolo)")
Button(action: {
self.favoriteItems.favItems.append(self.bookData.booksData[num].titolo)
}) {
Image(systemName: "heart")
}
}
}
}
}
}
}
struct FavoriteView: View {
#ObservedObject var favoriteItems = FavoriteItems()
var body: some View {
List {
ForEach (0 ..< favoriteItems.favItems.count) { num in
Text("\(self.favoriteItems.favItems[num])")
}
}
}
}
When I launch the app I can go to the Favorite View but after adding an Item I cannot.
My aim is to add an Item to Favorites and be able to save it once I close the app
The view model favoriteItems inside ContentView needs to be passed into FavoriteView because you need a reference of favoriteItems to reload FavoriteView when you add a new data.
Change to
NavigationView(destination: FavoriteView(favoriteItems: favoriteItems)) #ObservedObject var favoriteItems: FavoriteItems
It will be fine.
Thanks, X_X

SwiftUI store View as a variable while passing it some bindings

I want to store a View as a variable for later use, while passing that View some Bindings.
Here's what I've tried:
struct Parent: View {
#State var title: String? = ""
var child: Child!
init() {
self.child = Child(title: self.$title)
}
var body: some View {
VStack {
child
//...
Button(action: {
self.child.f()
}) {
//...
}
}
}
}
struct Child: View {
#Binding var title: String?
func f() {
// complex work from which results a string
self.title = <that string>
}
var body: some View {
// ...
}
}
It compiles correctly and the View shows as expected, however when updating from the child the passed Binding from the parent, the variable never gets updated. You can even do something like this (from the child):
self.title = "something"
print(self.title) // prints the previous value, in this case nil
I don't know if this is a bug or not, but directly initializing the child in the body property does the trick. However, I need that child as a property to access its methods.
If you want to change something from Parent for the child, binding is the right way. If that's complicated, you have to use DataModel.
struct Parent: View {
#State var title: String? = ""
var body: some View {
VStack {
Child(title: $title)
Button(action: {
self.title = "something"
}) {
Text("click me")
}
}
}
}
struct Child: View {
#Binding var title: String?
var body: some View {
Text(title ?? "")
}
}
This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
Encapsulate your data in an ObservableObject model, and implement any methods you need to call on that model.
Update
It is fine to have such a function defined in Child, but you should only be calling it from within the Child struct definition. For instance, if your child view contains a button, that button can call the child's instance methods. For example,
struct Parent: View {
#State private var number = 1
var body: some View {
VStack {
Text("\(number)")
Child(number: $number)
}
}
}
struct Child: View {
#Binding var number: Int
func double() {
number *= 2
}
var body: some View {
HStack {
Button(action: {
self.double()
}) {
Text("Double")
}
}
}
}
But you wouldn't try to call double() from outside the child struct. If you wanted a function that can be called globally, put it in a data model. This is especially true if the function call is making network requests, as the model will stick around outside your child view, even if it is recreated due to layout changing.
class NumberModel: ObservableObject {
#Published var number = 1
func double() {
number *= 2
}
}
struct Parent: View {
#ObservedObject var model = NumberModel()
var body: some View {
VStack {
Text("\(model.number)")
Button(action: {
self.model.double()
}) {
Text("Double from Parent")
}
Child(model: model)
}
}
}
struct Child: View {
#ObservedObject var model: NumberModel
var body: some View {
HStack {
Button(action: {
self.model.double()
}) {
Text("Double from Child")
}
}
}
}