I am trying to show a scrollview in which every cell has 3 picker text field.
However, only the last cell has a working picker text field that is tappable.
When I check BasketListCell preview it is just like I wanted. All the pickers are working well with placeholders. But in scrollview it does not.
I have 3 different listData in BasketList but ShoppingCartView repeats the first one 3 times.
so this is my cell struct Basket that has 3 Int for each picker text field.
struct Basket: Identifiable {
var id: Int {
return elementID
}
var elementID: Int
var image: String
var title: String
var brand: String
var price: String
var selectionImage: String
var discountedPrice: String
var sizeSelectedIndex: Int?
var colorSelectedIndex: Int?
var itemSelectedIndex : Int?
}
That is my demo BasketList with 3 elements.
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 0, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}
That is my BasketCell
struct BasketListCell: View {
#State var isSelected: Bool = false
#State private var itemSelectedIndex : Int?
#State private var sizeSelectedIndex : Int?
#State private var colorSelectedIndex: Int?
var colors = ["Black", "Green", "Red", "Blue"]
var sizes = ["XS", "S", "M", "L", "XL", "XXL"]
var items = ["1", "2", "3", "4", "5", "6"]
var item: Basket
var body: some View {
HStack(spacing: 20) {
ZStack {
PickerTextField(data: sizes, placeHolder: "Size", lastSelectedIndex: self.$sizeSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
ZStack{
PickerTextField(data: colors, placeHolder: "Color", lastSelectedIndex: self.$colorSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
ZStack {
PickerTextField(data: items, placeHolder: "Item", lastSelectedIndex: self.$itemSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
}
}
and finally, this is my ShoppingCartView
struct ShoppingCartView: View {
#State var selectedProduct = Basket(elementID: 0, image: "", title: "", brand: "", price: "", selectionImage: "", discountedPrice: "", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
#State var shown: Bool = false
var body: some View {
ZStack {
Color(.white)
.edgesIgnoringSafeArea(.all)
VStack {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) {
ForEach(BasketList.listData) { item in
BasketListCell(item: item)
.onTapGesture {
self.shown = true
self.selectedProduct = item
}
}
}
}
You can understand clearly my problem if you check those images better.
This is BasketListCell preview
That is my ShoppingCartView
elementID attribute of your Model class Basket needs to be unique. You currently have 3 Basket objects all with duplicate identifier, causing swiftUI to read first object every time. Changing it to some unique values should fix the problem.
Current-:
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 0, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}
Fix-:
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 1, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 2, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 3, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}
Related
I am making an application for macos using the SwiftUI framework, I am trying to create a List or a Table, with the ability to use MagnificationGesture, but unfortunately the dragging of the list/table does not work. It also does not work, or better to say, the recognition of buttons inside the list is shifted. Any ideas how this can be implemented?
short video about the problem
import SwiftUI
struct ContentView: View {
struct User: Identifiable, Hashable {
let id: Int
var name: String
var score: Int
}
#State private var users = [
User(id: 1, name: "Taylor Swift", score: 90),
User(id: 2, name: "Justin Bieber", score: 80),
User(id: 3, name: "Adele Adkins", score: 85)
]
#State private var sortOrder = [KeyPathComparator(\User.name)]
#State private var selection: User.ID?
#State var mainFrame = CGSize(width: 400, height: 400)
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
var body: some View {
let magnificationGesture = MagnificationGesture()
.onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
var newScale = self.scale * delta
if newScale < 1.0 {
newScale = 1.0
}
scale = newScale
}.onEnded { val in
lastScaleValue = 1
}
ScrollView([.vertical, .horizontal], showsIndicators: false) {
ZStack {
Rectangle()
.foregroundColor(.clear)
.frame(width: mainFrame.width * scale, height: mainFrame.height * scale, alignment: .center)
.padding(40)
VStack {
// doesn't work
List {
ForEach(users, id: \.self) { item in
Text(item.name)
}
.onMove(perform: move)
}.border(.red)
// doesn't work
List {
Button("test - 1", action: {print(1)})
Button("test - 2", action: {print(2)})
Button("test - 3", action: {print(3)})
}.border(.red)
// doesn't work
Table(users, selection: $selection, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Score", value: \.score) { user in
Text(String(user.score))
}
}.border(.red)
.onChange(of: sortOrder) { newOrder in
users.sort(using: newOrder)
}
}
.frame(width: mainFrame.width, height: mainFrame.height)
.background(Rectangle().fill(Color.white).shadow(radius: 3))
.scaleEffect(scale)
.gesture(magnificationGesture)
}
}
.background(Color.gray.edgesIgnoringSafeArea(.all))
}
func move(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}
}
I have been experimenting with LazyVGrid to produce a table with grid lines. I'm using XCode 13.4 & iOS15. Everything seemed to be going well until I parameterized all the column sizes, line widths, etc. The content consists of an array of row data with 5 pieces of text in each row. Here is the code:
struct StatisticsView: View {
private let columnMinWidths: [CGFloat] = [120, 50, 50, 50, 50]
private let rowHeight: CGFloat = 40
private let thinGridlineSize: CGFloat = 0.5
private let thickGridlineSize: CGFloat = 2
private let lineColour = Color.accentColor
private let columns: [GridItem]
private let rowContents = [
["Player's name", "Graham", "John", "Archie", ""],
["Round 1", "2", "3", "1", ""],
["Round 2", "3", "-", "2", ""],
["Round 3", "1", "2", "4", ""],
["Round 4", "2", "1", "1", ""]
]
init() {
self.columns = [
GridItem(.fixed(thickGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[0]), spacing: 0),
GridItem(.fixed(thickGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[1]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[2]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[3]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[4]), spacing: 0),
GridItem(.fixed(thickGridlineSize), spacing: 0),
]
}
var body: some View {
VStack(spacing: 0) {
// top horizontal gridline
lineColour.frame(height: thickGridlineSize)
LazyVGrid(columns: columns, alignment: .leading, spacing: 0) {
ForEach(0..<rowContents.count, id: \.self) { row in
lineColour.frame(height: rowHeight)
ForEach(0..<rowContents.first!.count, id: \.self) { col in
// cell contents with bottom gridline
VStack(spacing: 0) {
Text(rowContents[row][col])
.font(.caption2)
.frame(height: rowHeight - thinGridlineSize)
// tried using a colour fill but same result
// Color.green
// .frame(height: rowHeight - thinGridlineSize)
lineColour.frame(height: thinGridlineSize)
}
// vertical gridline between cells
lineColour.frame(height: rowHeight)
}
}
}
// bottom horizontal gridline
lineColour.frame(height: thickGridlineSize)
}
.padding(.horizontal, 8)
}
}
and here is what is being displayed on the simulator screen:
Can anyone see where I'm going wrong here? Any help would be much appreciated. Thanks.
You have to remove id: \.self from both loops. View Id become duplicated so it's not drawing more than one line.
I have a simple horizontal list view that displays the list of colours and the problem is that I don't know how to apply border to only one selected view, for now it's adding the white border to all views in the list on didTap, please help if you know how to achieve it, thanks 🙌
Here's the code:
struct Colour: Identifiable {
var id: Int
var color: Color
}
struct ContentView: View {
#State private var didTap = false
#State private var colors: [Colour] = [
Colour(id: 1, color: .black),
Colour(id: 2, color: .yellow),
Colour(id: 3, color: .orange),
Colour(id: 4, color: .green),
Colour(id: 5, color: .red),
Colour(id: 6, color: .blue),
Colour(id: 7, color: .pink),
Colour(id: 8, color: .purple),
]
var body: some View {
ZStack {
Color.gray.opacity(0.2)
.ignoresSafeArea()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors) { color in
Rectangle()
.fill(color.color)
.frame(width: 100, height: 150)
.overlay(
Rectangle()
.stroke(didTap ? Color.white : .clear, lineWidth: 5)
.animation(.spring())
)
.padding(5)
.onTapGesture {
didTap.toggle()
print(color.id)
}
}
}
.padding(.leading, 8)
}
}
}
}
You need to store id of selected color instead of bool.
Here is simple demo (deselect on tap on same element is your excise). Tested with Xcode 12.5 / iOS 14.5
struct ContentView: View {
#State private var selected = -1 // << here !!
#State private var colors: [Colour] = [
Colour(id: 1, color: .black),
Colour(id: 2, color: .yellow),
Colour(id: 3, color: .orange),
Colour(id: 4, color: .green),
Colour(id: 5, color: .red),
Colour(id: 6, color: .blue),
Colour(id: 7, color: .pink),
Colour(id: 8, color: .purple),
]
var body: some View {
ZStack {
Color.gray.opacity(0.2)
.ignoresSafeArea()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors) { color in
Rectangle()
.fill(color.color)
.frame(width: 100, height: 150)
.overlay(
Rectangle()
.stroke(selected == color.id ? Color.white : .clear, lineWidth: 5)
.animation(.spring())
)
.padding(5)
.onTapGesture {
selected = color.id // << here !!
print(color.id)
}
}
}
.padding(.leading, 8)
}
}
}
}
I want to make a "card" but the height depends a list of items, I don't how to set a different value according to my sizes list... Example:
ContentView Struct
struct ContentView: View {
#State private var orderList : [Order] = [
Order(id: 0, productList: [Product(id: 0, name: "itemA", quantity: "24", isEvaluated: true), Product(id: 0, name: "itemB", quantity: "2", isEvaluated: false)]),
Order(id: 1, productList: [Product(id: 0, name: "itemC", quantity: "4", isEvaluated: true), Product(id: 0, name: "itemD", quantity: "12", isEvaluated: false),Product(id: 0, name: "itemE", quantity: "6", isEvaluated: false), Product(id: 0, name: "itemF", quantity: "5", isEvaluated: false)]),
Order(id: 2, productList: [Product(id: 0, name: "itemG", quantity: "24", isEvaluated: true)]),
Order(id: 3, productList: [Product(id: 0, name: "itemH", quantity: "5", isEvaluated: true), Product(id: 0, name: "itemI", quantity: "2", isEvaluated: false),Product(id: 0, name: "itemJ", quantity: "16", isEvaluated: false), Product(id: 0, name: "itemK", quantity: "4", isEvaluated: false), Product(id: 0, name: "itemL", quantity: "2", isEvaluated: false)]),
Order(id: 4, productList: [Product(id: 0, name: "itemM", quantity: "8", isEvaluated: true)])
]
var body: some View {
VStack{
ForEach(orderList, id: \.self){order in
ScrollView(showsIndicators: false){
VStack(alignment: .leading){
Group{
HStack{
Text("#Order " + "\(order.id)").multilineTextAlignment(.center)
Spacer()
Text("In Progress").multilineTextAlignment(.center)
}.padding([.bottom],5)
}
Group{
VStack{
ForEach(order.productList.indices) { currentIndex in
ItemRow(getProduct(productList: order.productList, index: currentIndex))
.padding(.bottom, 5)
}
}
}.padding([.bottom], 10)
HStack{
Text("Products")
Spacer()
Text("$00.00")
}
HStack{
Text("Shipping Expenses")
Spacer()
Text("$00.00")
}
HStack{
Text("Total")
Spacer()
Text("$0.00")
}
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.padding(10)
}
.background(
RoundedRectangle(cornerRadius: 2)
.fill(Color.white)
.shadow(color: .gray, radius: 2, x: 0, y: 2)
)
.padding(.bottom, 10)
}
}
.padding(16)
}
func getProduct(productList: [Product], index: Int)-> Product{
return productList[index]
}
}
ItemRow Struct
struct ItemRow: View {
let currentProduct: Product
init(_ currentProduct: Product) {
self.currentProduct = currentProduct
}
var body: some View {
HStack{
HStack{
Text("• \(currentProduct.name)")
Spacer()
}
HStack{
Text("quantity \(currentProduct.quantity)")
Spacer()
}
if !currentProduct.isEvaluated{
HStack{
Spacer()
Button(action:{
// do sth
}){
Text("rate now!")
}
}
}
}
}
}
PS. in itemsList you must create a struct called Order like this: (Don't mind on hardcode values, I made it to make the example easier)
struct Order: Hashable {
var id: Int
var productList: [Product]
}
PS2. in productList you must create a struct called Product like this:
struct Product: Hashable {
var id: Int
var name: String
var quantity : String
var isEvaluated : Bool
}
If I understand you correctly you want the cards (red frame) to take as much space as they need to display the content they have without the need of scrolling on the card itself?
Short answer. You don't need to.
SwiftUI does it for you. You only have one small mistake in your code in ContentView. Move the ScrollView up to the level where you have the VStack and remove the VStack. You ContentView body should now look like this:
var body: some View {
ScrollView {
ForEach(orderList, id: \.self){order in
VStack(alignment: .leading){
Group{
HStack{
Text("#Order " + "\(order.id)").multilineTextAlignment(.center)
Spacer()
Text("In Progress").multilineTextAlignment(.center)
}.padding([.bottom],5)
}
Group{
VStack{
ForEach(order.productList.indices) { currentIndex in
ItemRow(getProduct(productList: order.productList, index: currentIndex))
.padding(.bottom, 5)
}
}
}.padding([.bottom], 10)
HStack{
Text("Products")
Spacer()
Text("$00.00")
}
HStack{
Text("Shipping Expenses")
Spacer()
Text("$00.00")
}
HStack{
Text("Total")
Spacer()
Text("$0.00")
}
Spacer()
}.background(
RoundedRectangle(cornerRadius: 2)
.fill(Color.white)
.shadow(color: .gray, radius: 2, x: 0, y: 2)
)
.padding(.bottom, 10)
}
}
.padding(16)
}
This gives you the following result:
I have a LazyVGrid, every item with is favorite button. and use combine to debounce user input($isFavoriteI), when isFavoriteO changed, then modify the items.
it works fine, but when i scroll the list, log will print: "X, isFavorite changed as false/true)", what cause isFavoriteO changed and why? because of item reusing in list? how to avoid it?
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
index 7, isFavorite changed as true
import SwiftUI
import Combine
struct Item {
var index: Int
var favorite: Bool
}
var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
// Item(index: 8, favorite: true),
// Item(index: 9, favorite: false),
// Item(index: 10, favorite: true),
// Item(index: 11, favorite: false),
// Item(index: 12, favorite: true),
// Item(index: 13, favorite: false),
// Item(index: 14, favorite: true),
// Item(index: 15, favorite: false),
// Item(index: 16, favorite: true),
// Item(index: 17, favorite: false),
// Item(index: 18, favorite: true),
// Item(index: 19, favorite: false),
]
struct ViewModelInListTestView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(items, id: \.index) { item in
ItemView(item: item)
}
}
}.navigationTitle("ViewModel In List")
}
}
struct ItemView: View {
let item: Item
#ObservedObject var viewModel: ViewModel
init(item: Item) {
print("ItemView.init, \(item.index)")
self.item = item
self.viewModel = ViewModel(item: item)
}
var body: some View {
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: viewModel.isFavoriteI ? "heart.fill" : "heart")
.foregroundColor(viewModel.isFavoriteI ? .red : .white)
.padding()
.onTapGesture { onFavoriteTapped() }
.onChange(of: viewModel.isFavoriteO) { isFavorite in
setFavorite(isFavorite)
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
func onFavoriteTapped() {
viewModel.isFavoriteI.toggle()
}
func setFavorite(_ isFavorite: Bool) {
print("index \(item.index), isFavorite changed as \(isFavorite)")
items[item.index].favorite = isFavorite
}
class ViewModel: ObservableObject {
#Published var isFavoriteI: Bool = false
#Published var isFavoriteO: Bool = false
private var subscriptions: Set<AnyCancellable> = []
init(item: Item) {
print("ViewModel.init, \(item.index)")
let isFavorite = item.favorite
isFavoriteI = isFavorite; isFavoriteO = isFavorite
$isFavoriteI
.print("index \(item.index) isFavoriteI:")
.dropFirst()
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.eraseToAnyPublisher()
.print("index \(item.index) isFavoriteO:")
.receive(on: DispatchQueue.main)
.assign(to: \.isFavoriteO, on: self)
.store(in: &subscriptions)
}
}
}
update # 4.15
according to #Cenk Bilgen, i re-write the code, but strange thing happened. print("set favorite as (favorite)") will not present if adding removeDuplicates. why?
import SwiftUI
import Combine
struct Item: Identifiable {
var index: Int
var favorite: Bool
var id: Int { index }
}
class Model: ObservableObject {
#Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
}
struct ViewModelInListTestView: View {
#StateObject var model = Model()
var body: some View {
print("ViewModelInListTestView refreshing"); return
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(model.items.indices) { index in
ItemView(item: model.items[index])
.environmentObject(model)
}
}
}.navigationTitle("ViewModel In List")
}
struct ItemView: View {
#EnvironmentObject var model: Model
let item: Item
#State private var updateFavourite = PassthroughSubject<Bool, Never>()
#State private var favorite: Bool = false
init(item: Item) {
self.item = item
self._favorite = State(initialValue: item.favorite)
}
var body: some View {
print("ItemView \(item.index) refreshing"); return
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(favorite ? .red : .white)
.padding()
.onTapGesture {
favorite.toggle()
updateFavourite.send(favorite)
}
.onReceive(
updateFavourite
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
// .removeDuplicates() <------ HERE
// .eraseToAnyPublisher()
) { favorite in
print("set favorite as \(favorite)")
model.items[item.index].favorite = favorite
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
}
}
struct Item: Identifiable {
var index: Int
var favorite: Bool
var id: Int { index }
}
class Model: ObservableObject {
#Published var items = [
Item(index: 0, favorite: true),
Item(index: 1, favorite: false),
Item(index: 2, favorite: true),
Item(index: 3, favorite: false),
Item(index: 4, favorite: true),
Item(index: 5, favorite: false),
Item(index: 6, favorite: true),
Item(index: 7, favorite: false),
]
}
struct SimplerView: View {
#StateObject var model = Model()
var body: some View {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(items.indices) { index in
ItemView(item: $model.items[index])
.environmentObject(model)
}
}
}.navigationTitle("ViewModel In List")
}
struct ItemView: View {
#EnvironmentObject var model: Model
#Binding var item: Item
#State private var updateFavourite = PassthroughSubject<Bool, Never>()
var body: some View {
HStack {
Text("index \(item.index)")
Spacer()
Image(systemName: item.favorite ? "heart.fill" : "heart")
.foregroundColor(item.favorite ? .red : .white)
.padding()
.onTapGesture {
updateFavourite.send(item.favorite)
}
.onReceive(updateFavourite
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)) { value in
item.favorite = !value
}
}
.frame(width: 200, height: 150)
.background(Color.gray)
}
}
}
I don't really understand how isFavoriteI and isFavoriteO work, but you
could try this:
in ItemView remove the
.onChange(of: viewModel.isFavoriteO) { isFavorite in
setFavorite(isFavorite)
}
and change:
func onFavoriteTapped() {
viewModel.isFavoriteI.toggle()
print("\(item.index), isFavorite changed as \(viewModel.isFavoriteI)")
items[item.index].favorite = viewModel.isFavoriteI
}
The best of doing this is in this way in down code, SwiftUI would stop unnecessary render, and it will render if it needs!
You had some issue that you should use id for your items, plus combine does not work well in this case, so use better and easier way in down:
import SwiftUI
struct ContentView: View {
#StateObject var itemModel: ItemModel = sharedItemModel
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200), spacing: 4, alignment: .center)], spacing: 4) {
ForEach(Array(itemModel.items.enumerated()), id:\.element.id) { (offset, element) in
ItemView(index: offset, favorite: element.favorite)
}
}
}
Button("append new random element") { itemModel.items.append(Item(favorite: Bool.random())) }
.padding()
}
}
struct ItemView: View {
let index: Int
let favorite: Bool
init(index: Int, favorite: Bool) {
self.index = index
self.favorite = favorite
}
var body: some View {
print("rendering item: " + index.description)
return HStack {
Text("index " + index.description)
.bold()
.padding()
Spacer()
Image(systemName: favorite ? "heart.fill" : "heart")
.foregroundColor(Color.red)
.padding()
.onTapGesture { sharedItemModel.items[index].favorite.toggle() }
}
.frame(width: 200, height: 150)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct Item: Identifiable {
let id: UUID = UUID()
var favorite: Bool
}
class ItemModel: ObservableObject {
#Published var items: [Item] = [Item]()
}
let sharedItemModel: ItemModel = ItemModel()