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