How to make shortcut/binding to a struct inside class? (SwiftUI) - class

In my class, I have an array of Item and an optional var selection, which is supposed to store a SHORTCUT to the selected Item.
I need to be able to access the selected Item by referring to selection.
In order for selection to work as SHORTCUT does selection has to be a Binding?
If yes, is it a #Binding like in structs, or maybe Binding<T>?
And does it has to be #Published?
My code:
import SwiftUI
struct Item: Identifiable, Equatable {
var id = UUID().uuidString
var color: Color
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(color: .blue), Item(color: .blue), Item(color: .blue)]
#Published var selection: Item? //this supposed to be not a value, but a SHORTCUT to a selected item inside array
func setSelection (item: Item) {
selection = item
}
func changeColor (color: Color) {
if selection != nil {
selection?.color = color// << PROBLEM is that it only copies object and modifies the copy instead of original
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
//list
VStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SubView(item: $model.items[i], model: model)
}
// change color button
Button {
model.changeColor(color: .red)
} label: {Text("Make Selection Red")}
}.padding()
}
}
struct SubView: View {
#Binding var item: Item
var model: Model
var body: some View {
VStack {
// button which sets selection to an items inside this subview
Button {
model.setSelection(item: item)
} label: {
Text("Select").background(item.color)}.buttonStyle(PlainButtonStyle())
}
}
}
Desired functionality: click on one if items, and then charging its color.

since you want selection to be "....a selected item inside array", then you could
just use the index in the array of items. Something like this:
(although your code logic is a bit strange to me, I assumed this is just a test example)
struct Item: Identifiable, Equatable {
var id = UUID().uuidString
var color: Color
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(color: .blue), Item(color: .blue), Item(color: .blue)]
#Published var selection: Int? // <-- here
func changeColor(color: Color) {
if let ndx = selection { // <-- here
items[ndx].color = color
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
//list
VStack {
ForEach(model.items.indices, id:\.self) { i in
SubView(index: i, model: model) // <-- here
}
// change color button
Button {
model.changeColor(color: .red)
} label: {Text("Make Selection Red")}
}.padding()
}
}
struct SubView: View {
var index: Int // <-- here
#ObservedObject var model: Model // <-- here
var body: some View {
VStack {
// button which sets selection to an items inside this subview
Button {
model.selection = index
} label: {
Text("Select").background(model.items[index].color) // <-- here
}
.buttonStyle(PlainButtonStyle())
}
}
}

Related

SwiftUI iterating through #State or #Published dictionary with ForEach

Here is a minimum reproducible code of my problem. I have a dictionary of categories and against each category I have different item array. I want to pass the item array from dictionary, as binding to ListRow so that I can observer the change in my ContentView. Xcode shows me this error which is very clear
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.
The solution shows in this question Iterating through set with ForEach in SwiftUI not using any #State or #Published variable. They are just using it for showing the data. Any work around for this issue ??
struct Item {
var id = UUID().uuidString
var name: String
}
struct ListRow {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
ForEach(testDictionary[category]) { item in
ListRow(item: item)
}
}
}.onAppear(
addDummyDateIntoDictonary()
)
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}
One problem is that you didn't make ListRow conform to View.
// add this
// ╭─┴──╮
struct ListRow: View {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
Now let's address your main problem.
A Binding is two-way: SwiftUI can use it to get a value, and SwiftUI can use it to modify a value. In your case, you need a Binding that updates an Item stored somewhere in testDictionary.
You can create such a Binding “by hand” using Binding.init(get:set:) inside the inner ForEach.
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
let items = testDictionary[category] ?? []
ForEach(items, id: \.id) { item in
let itemBinding = Binding<Item>(
get: { item },
set: {
if
let items = testDictionary[category],
let i = items.firstIndex(where: { $0.id == item.id })
{
testDictionary[category]?[i] = $0
}
}
)
ListRow(item: itemBinding)
}
}
}.onAppear {
addDummyDateIntoDictonary()
}
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}

Propertly break down and pass data between views

So I'm still learning Swift and I wanted to cleanup some code and break down views, but I can't seem to figure out how to pass data between views, so I wanted to reach out and check with others.
So let's say that I have MainView() which previously had this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
HStack(alignment: .center, spacing: 3) {
Text(item.title)
}
}
}
Now I created a SecondView() and changed the MainView() content to this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
SecondView(item: item)
}
}
Inside SecondView(), how can I access the item data so that I can use item.title inside SecondView() now?
In order to pass item to SecondView, declare item as a let property and then when you call it with SecondView(item: item), SecondView can refer to item.title.
Here is a complete example expanding on your code:
import SwiftUI
struct Item {
let title = "Test Title"
}
class MainViewModel: ObservableObject {
#Published var selectedItem: Item? = Item()
}
struct MainView: View {
#ObservedObject var model: MainViewModel
var body: some View {
if let item = model.selectedItem {
SecondView(item: item)
}
}
}
struct SecondView: View {
let item: Item
var body: some View {
Text(item.title)
}
}
struct ContentView: View {
#StateObject private var model = MainViewModel()
var body: some View {
MainView(model: model)
}
}

SwiftUI: How to update element in ForEach without necessity to update all elements?

Imagine that you have some parent view that generate some number of child views:
struct CustomParent: View {
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index)
}
}
}
}
struct CustomChild: View {
#State var index: Int
#State private var text: String = ""
var body: some View {
Button(action: {
// Here should be some update of background/text/opacity or whatever.
// So how can I update background/text/opacity or whatever for button with index for example 3 from button with index for example 1?
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Question is included in the code as comment.
Thanks!
UPDATE:
First of all really thanks for all of your answers, but now imagine that you use mentioned advanced approach.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
If I use let _ = Self._printChanges() method in CustomChildView, to catch UI updates/changes, it'll print that every element in ForEach was updated/changed on button action.
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
let _ = Self._printChanges() // This have been added to code
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}
And now imagine that you have for example 1000 custom elements which have some background, opacity, shadow, texts, fonts and so on. Now I change text in any of the elements.
Based on log from let _ = Self._printChanges() method, it goes through all elements, and all elements are updated/changed what can cause delay.
Q1: Why did update/change all elements, if I change text in only one element?
Q2: How can I prevent update/change all elements, if I change only one?
Q3: How to update element in ForEach without necessity to update all elements?
Simpler Approach:
Although child views cannot access things that the host views have, it's possible to declare the child states in the host view and pass that state as a binding variable to the child view. In the code below, I have passed the childTexts variable to the child view, and (for your convenience) initialized the text so that it binds to the original element in the array (so that your onAppear works properly). Every change performed on the text and childTexts variable inside the child view reflects on the host view.
I strongly suggest not to do this though, as more elegant approaches exist.
struct CustomParent: View {
#State var childTexts = [String](repeating: "", count: 10)
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, childTexts: $childTexts)
}
}
}
}
struct CustomChild: View {
let index: Int
#Binding private var text: String
#Binding private var childTexts: [String]
init(index: Int, childTexts: Binding<[String]>) {
self.index = index
self._childTexts = childTexts
self._text = childTexts[index]
}
var body: some View {
Button(action: {
//button behaviors goes here
//for example
childTexts[index + 1] = "A"
}) {
Text(text)
}
.onAppear {
text = String(index)
}
}
}
Advanced Approach:
By using the Combine framework, all your logics can be moved into an ObservableObject view model. This is much better as the button logic is no longer inside the view. In simplest terms, the #Published variable in the ObservableObject will publish a change when it senses its own mutation, while the #StateObjectand the #ObservedObject will listen and recalculate the view for you.
struct CustomParent: View {
#StateObject var customViewModel = CustomViewModel()
var body: some View {
HStack {
ForEach(0..<10, id: \.self) { index in
CustomChild(index: index, customViewModel: customViewModel)
}
}
}
}
struct CustomChild: View {
let index: Int
#ObservedObject var customViewModel: CustomViewModel
var body: some View {
Button(action: {
customViewModel.buttonPushed(at: index)
}) {
Text(customViewModel.childTexts[index])
}
}
}
class CustomViewModel: ObservableObject {
#Published var childTexts = [String](repeating: "", count: 10)
init() {
for i in 0..<childTexts.count {
childTexts[i] = String(i)
}
}
func buttonPushed(at index: Int) {
//button behaviors goes here
//for example:
childTexts[index + 1] = "A"
}
}

How to set up Binding of computed value inside class (SwiftUI)

In my Model I have an array of Items, and a computed property proxy which using set{} and get{} to set and return currently selected item inside array and works as shortcut. Setting item's value manually as model.proxy?.value = 10 works, but can't figure out how to Bind this value to a component using $.
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 1), Item(value: 2), Item(value: 3)]
var proxy: Item? {
get {
return items[1]
}
set {
items[1] = newValue!
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
Text("Value: \(model.proxy!.value)")
Button(action: {model.proxy?.value = 123}, label: {Text("123")}) // method 1: this works fine
SubView(value: $model.proxy.value) // method 2: binding won't work
}.padding()
}
}
struct SubView <B:BinaryFloatingPoint> : View {
#Binding var value: B
var body: some View {
Button( action: {value = 100}, label: {Text("1")})
}
}
Is there a way to modify proxy so it would be modifiable and bindable so both methods would be available?
Thanks!
Day 2: Binding
Thanks to George, I have managed to set up Binding, but the desired binding with SubView still won't work. Here is the code:
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 0), Item(value: 0), Item(value: 0)]
var proxy: Binding <Item?> {
Binding <Item?> (
get: { self.items[1] },
set: { self.items[1] = $0! }
)
}
}
struct ContentView: View {
#StateObject var model = Model()
#State var myval: Double = 10
var body: some View {
VStack {
Text("Value: \(model.proxy.wrappedValue!.value)")
Button(action: {model.proxy.wrappedValue?.value = 555}, label: {Text("555")})
SubView(value: model.proxy.value) // this still wont work
}.padding()
}
}
struct SubView <T:BinaryFloatingPoint> : View {
#Binding var value: T
var body: some View {
Button( action: {value = 100}, label: {Text("B 100")})
}
}
Create a Binding instead.
Example:
var proxy: Binding<Item?> {
Binding<Item?>(
get: { items[1] },
set: { items[1] = $0! }
)
}

How to update an element of an array in an Observable Object

Sorry if my question is silly, I am a beginner to programming. I have a Navigation Link to a detail view from a List produced from my view model's array. In the detail view, I want to be able to mutate one of the tapped-on element's properties, but I can't seem to figure out how to do this. I don't think I explained that very well, so here is the code.
// model
struct Activity: Identifiable {
var id = UUID()
var name: String
var completeDescription: String
var completions: Int = 0
}
// view model
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
}
// view
struct ActivityView: View {
#StateObject var viewModel = ActivityViewModel()
#State private var showingAddEditActivityView = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.activities, id: \.id) {
activity in
NavigationLink(destination: ActivityDetailView(activity: activity, viewModel: self.viewModel)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
}
}
.navigationBarItems(trailing: Button("Add new"){
self.showingAddEditActivityView.toggle()
})
.navigationTitle(Text("Activity List"))
}
.sheet(isPresented: $showingAddEditActivityView) {
AddEditActivityView(copyViewModel: self.viewModel)
}
}
}
// detail view
struct ActivityDetailView: View {
#State var activity: Activity
#ObservedObject var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
var tempActivity = viewModel.activities.first{ activity in activity.id == self.activity.id
}!
tempActivity.completions += 1
}
}
// Add new activity view (doesn't have anything to do with question)
struct AddEditActivityView: View {
#ObservedObject var copyViewModel : ActivityViewModel
#State private var activityName: String = ""
#State private var description: String = ""
var body: some View {
VStack {
TextField("Enter an activity", text: $activityName)
TextField("Enter an activity description", text: $description)
Button("Save"){
// I want this to be outside of my view
saveActivity()
}
}
}
func saveActivity() {
copyViewModel.activities.append(Activity(name: self.activityName, completeDescription: self.description))
print(copyViewModel.activities)
}
}
In the detail view, I am trying to update the completion count of that specific activity, and have it update my view model. The method I tried above probably doesn't make sense and obviously doesn't work. I've just left it to show what I tried.
Thanks for any assistance or insight.
The problem is here:
struct ActivityDetailView: View {
#State var activity: Activity
...
This needs to be a #Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the #Binding, you can get rid of it.
// detail view
struct ActivityDetailView: View {
#Binding var activity: Activity /// here!
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}
But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:
/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
And for iOS 14 or below, you'll need to loop over indices instead. But it works.
/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
You are changing and increment the value of tempActivity so it will not affect the main array or data source.
You can add one update function inside the view model and call from view.
The view model is responsible for this updation.
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
func updateCompletionCount(for id: UUID) {
if let index = activities.firstIndex(where: {$0.id == id}) {
self.activities[index].completions += 1
}
}
}
struct ActivityDetailView: View {
var activity: Activity
var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
self.viewModel.updateCompletionCount(for: activity.id)
}
}
Not needed #State or #ObservedObject for details view if don't have further action.