Keeping the original datasource in sync with downstream bindings - swiftui

If I have a collection of fruits, and I pass one of them to a detail view, how do I edit that item so that both the item and it's original datasource are updated?
final class Merchant: ObservableObject {
#Published
var selection: Fruit?
#Published
var fruits = [
Fruit(name: "Banana"),
Fruit(name: "Apple")
]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}
.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection, content: {
FruitDetailView(item: $0)
})
}
}
struct FruitDetailView: View {
let item: Fruit
init(item: Fruit) {
self.item = item
}
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon" // error
}
}
}
}
Changing the item on FruitDetailView to a binding doesn't change the original datasource.

There are a number of ways to achieve what you ask. This is one simple way using the model constructs you already have. It uses the Merchant selection to update the Merchant fruits data.
struct ContentView: View {
#StateObject var merchant = Merchant()
var body: some View {
FruitsView().environmentObject(merchant)
}
}
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
final class Merchant: ObservableObject {
#Published var selection: Fruit? = nil {
didSet {
if selection != nil,
let index = fruits.firstIndex(where: {$0.id == selection!.id}) {
fruits[index].name = selection!.name
}
}
}
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection) { _ in
FruitDetailView().environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
Text(merchant.selection?.name ?? "no selection name")
Button("Press Me") {
merchant.selection?.name = "Watermelon"
}
}
}
}
EDIT-1:
This is another way of keeping the model in sync. It uses a function updateFruits in the Merchant ObservableObject class, to update the model's data.
It separates the UI interaction part using a local #State var selection: Fruit? from the main data in the Merchant model.
final class Merchant: ObservableObject {
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
func updateFruits(with item: Fruit) {
if let index = fruits.firstIndex(where: {$0.id == item.id}) {
fruits[index].name = item.name
}
}
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
#State var selection: Fruit?
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $selection) { item in
FruitDetailView(item: item).environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
#State var item: Fruit
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon"
merchant.updateFruits(with: item)
}
}
}
}

Related

Why does the TextField in the List stop updating?

I started studying SwiftUI and wanted to make a prototype of standard reminders, like in an iPhone. It seems nothing complicated, there is a List, in each cell a TextField.
But I ran into a problem: when we change the text in the TextField using onChange, then we accordingly tell the view model to update our objects.
And when the objects are updated, the entire List is redrawn and the editing of the current TextField is reset (you can neither remove more than one character, nor add). You have to click on the text again to continue editing.
Does anyone know how to treat this?
This is my code:
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
let name: String
func updateName(newName: String) -> Fruit {
return Fruit(name: newName)
}
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
func updateName(newName: String, fruit: Fruit) {
if let index = fruits.firstIndex(where: { $0.id == fruit.id }) {
fruits[index] = fruit.updateName(newName: newName)
}
}
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.fruits) { fruit in
ListViewRow(fruit: fruit)
}
}
.environmentObject(viewModel)
}
}
struct ListViewRow: View {
#EnvironmentObject var viewModel: ViewModel
#State var fruitTextField: String
let fruit: Fruit
init(fruit: Fruit) {
self.fruit = fruit
_fruitTextField = State(initialValue: fruit.name)
}
var body: some View {
TextField("", text: $fruitTextField)
.onChange(of: fruitTextField) { newValue in
viewModel.updateName(newName: newValue, fruit: fruit)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
You can simplify it even more with:
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
That being said, you really need to view the Apple Swift Tutorials that were linked in the comments.
Edit: Full Project Code for Lorem Ipsum:
//
// ContentView.swift
// FruitApp
//
// Created by Developer on 11/27/21.
//
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
struct ContentView: View {
var body: some View {
ListView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Creating an edit detail view sheet that needs confirmation in SwiftUI

On Xcode 13 Beta 3, I am trying to find a good solution for an edit detail view presented in a sheet that needs to explicitly be confirmed.
In the DetailEditView, I initialise a #State property (editingModel) which is initialised from a #Binding (model) that I hand down.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
//...
When I tap/press the confirm button in my sheet, I want to assign the altered editingModel to the passed model.
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
//...
While I do not have any build errors, the code does not work as expected–and I don't understand why. Look out for my #warning: that's where my code does not work as expected.
For all I know this could be a bug in the Xcode 13 Beta–or am I misunderstanding something fundamentally?
Here's all the code:
import SwiftUI
//MARK: - Main
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#SceneStorage("selection") var selection: Model.ID?
var body: some View {
NavigationView {
SidebarView(selection: $selection)
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var selection: Model.ID?
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $selection)
} label: {
Text(modelItem.id)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model.ID?
#State private var isEditing = false
var body: some View {
Form {
Text(modelBinding.wrappedValue.id)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: modelBinding, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
var modelBinding: Binding<Model> {
$modelStore[modelSelection]
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.id)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(id: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
var id: String
static func mockModel() -> Model {
Model(id: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
At first, do not edit id of Model. Instead use a new property and edit it.
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
For the first time you are in DetailView, selected model is not among the $modelStore.models. You need to send the first object of `` to the DetailsView.
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
When you choose a model from SidebarView, the model in DetailView does not get updated. Send $modelItem to DetailView instead.
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
In DetailView, remove modelBinding and send modelSelection to DetailEditView.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
All the code
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#Binding var selection: Model
var body: some View {
NavigationView {
SidebarView()
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model
#State private var isEditing = false
var body: some View {
Form {
Text(modelSelection.content)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: $modelSelection, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(content: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
Now upon confirmation the selected model is edited in all the views.

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

Why .append doesn't reload swiftUI view

I have the following view hierarchy
Nurse List View > Nurse Card > Favorite button
Nurse List View
struct NurseListView: View {
#State var data: [Nurse] = []
var body: some View {
List {
ForEach(data.indices, id: \.self) { index in
NurseCard(data: self.$data[index])
}
}
}
}
Nurse Card
struct NurseCard: View {
#Binding var data: Nurse
var body: some View {
FavoriteActionView(data:
Binding(
get: { self.data },
set: { self.data = $0 as! Nurse }
)
)
}
}
Favorite Action View
struct FavoriteActionView: View {
#Binding var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}
When toggleFavIcon execute, it append/remove the user id from the likes property in data object but I can't see the change unless I go back to previous page and reopen the page. What I am missing here?
As Asperi wrote, using an ObservableObject would work well here. Something like this:
class FavoritableData: ObservableObject {
#Published var likes: [String] = []
#Published var isFavorite = false
}
struct FavoriteActionView: View {
#ObservedObject var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}

How is it possible to work with a struct inside of a struct in SwiftUI in different Views?

I have gut a simple project with a struct inside of a struct and want to add at first the names and hobbies of a single user and than want to add this user to a whole pool of users. The code is the following:
import SwiftUI
struct User: Identifiable, Hashable {
var id = UUID()
var firstName = ""
var lastName = ""
var hobbiesOfUser = [Hobbies]()
}
struct Hobbies: Identifiable, Hashable {
var id = UUID()
var nameOfHobby = ""
var nameClub = ""
}
class UsersStorage: ObservableObject {
#Published var users = [User]()
}
struct ContentView: View {
#EnvironmentObject var userStorage: UsersStorage
#State private var isPresented = false
var body: some View {
NavigationView {
List(userStorage.users) { singleUser in
VStack {
HStack {
Text(singleUser.firstName)
Text(singleUser.lastName)
}
}
}
.navigationBarItems(trailing:
Button(action: {
self.isPresented = true
}) {
Text("New User")
}.sheet(isPresented: $isPresented, onDismiss: {
self.isPresented = false
}) {
AddUserView(isPresented: self.$isPresented, user: User()).environmentObject(self.userStorage)
}
)
}
}
}
struct AddUserView: View {
#EnvironmentObject var userStorage: UsersStorage
#Binding var isPresented: Bool
#State var user: User
#State var hobbiesOfUser = [Hobbies]()
var body: some View {
NavigationView {
VStack {
Text("First Name")
TextField("First Name", text: $user.firstName)
Text("Last Name")
TextField("Last Name", text: $user.lastName)
NavigationLink(destination: AddHobbieView(hobbie: Hobbies())) {
Text("Add New Hobbie")
}
List(user.hobbiesOfUser) { singleHobbie in
VStack {
HStack {
Text(singleHobbie.nameOfHobby)
Text(singleHobbie.nameClub)
}
}
}
HStack {
Button(action: {
self.isPresented = false
}){
Text("Cancel")
}
Button(action: {
self.userStorage.users.append(self.user)
self.isPresented = false
}){
Text("Save")
}
}
}
}
}
}
struct AddHobbieView: View {
#EnvironmentObject var userStorage: UsersStorage
#State var hobbie: Hobbies
var body: some View {
VStack {
Text("Hobby")
TextField("First Name", text: $hobbie.nameOfHobby)
Text("Club")
TextField("Last Name", text: $hobbie.nameClub)
HStack {
Button(action: {
// self.userStorage.users.append(self.hobbie)
}){
Text("Cancel")
}
Button(action: {
}){
Text("Save")
}
}
}
}
}
My question is: how can I add hobbies to the list in the AddUserView and get the buttons in the AddHobbieView let me go back to the AddUserView.
You add #Environment(\.presentationMode) var presentationMode to AddHobbieView and call self.presentationMode.wrappedValue.dismiss() when you want to dismiss the view
I hope this helps!