how can I refresh another cell when change observedObject - ios

When I select a cell, I want to deselect it from the other cell. After making the selection, I cannot remove the selection from the previous cell. What is the reason of this ? Sorry. I know little English.
Enum
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
ViewModel
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
}
Category Model
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
Basic Category
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
Cell
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color
var page: Page
#ObservedObject var macMainVM = MainViewModel()
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.macMainVM.selectedTab = self.page
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM.selectedTab == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
ForEach
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}

Updated my answer with the working answer.
The problem was that you were using #ObservedObject instead of a #StateObject
Always use #StateObject for the parent view and #ObservedObject for the children view.
import SwiftUI
struct stackT: View {
#StateObject var macMainVM = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(macMainVM: macMainVM, title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}
}
}
struct stackT_Previews: PreviewProvider {
static var previews: some View {
stackT()
}
}
struct TabView: View {
#ObservedObject var macMainVM: MainViewModel
var title: String = ""
var icon: String = ""
var color: Color
let page: Page
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
DispatchQueue.main.async {
self.macMainVM.globalTab = page
print(macMainVM.globalTab)
}
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM.globalTab == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : RoundedRectangle(cornerRadius: 10).stroke(.clear))
}
.buttonStyle(PlainButtonStyle())
}
}
class MainViewModel: ObservableObject {
#Published var globalTab = Page.newest
}
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
case none
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
Category(title: "Another", icon: "book", color: .blue, page: .mac)
]

Below are the approaches for single selection and multiple selection.
For single selection you can pass Page as Bindings to subView.
For multiple selection you can maintain a boolean state array, that can tell if current cell was already selected or not.
Single Selection
import SwiftUI
enum Page:String {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
}
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color = .red
var page: Page
#Binding var macMainVM :Page
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.macMainVM = self.page
}
}) {
VStack {
Text("Tap")
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
struct mainView:View{
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
#ObservedObject var macMainVM = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory.indices) { index in
TabView(page: basicCategory[index].page,macMainVM: $macMainVM.selectedTab)
}
}
}
}
Multiple Selection
import SwiftUI
enum Page:String {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
var stateArray:[Bool] = []
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color = Color.red
var page: Page
var count = 0
var onIndex:Int
#ObservedObject var macMainVM = MainViewModel()
init(totalPage:Int,page:Page,onIndex:Int) {
self.count = totalPage
self.page = page
self.onIndex = onIndex
macMainVM.stateArray = [Bool](repeating: false, count: totalPage)
}
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
if macMainVM.stateArray[onIndex] == false{
macMainVM.stateArray[onIndex] = true
}else{
macMainVM.stateArray[onIndex] = false
}
macMainVM.selectedTab = self.page
}
}) {
VStack {
Text("Tap")
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(macMainVM.stateArray[onIndex] ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
struct mainView:View{
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
//var model = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory.indices) { index in
TabView(totalPage: basicCategory.count, page: basicCategory[index].page, onIndex: index)
}
}
}
}
NOTE-: I haven’t explained what I did in deep, because it will be more understandable by looking into code.

The most Easy way: You should just use shared object, which you are creating new one every time! I did not edit your code! I just used a shared model! that was all I done! Or you can use environmentObject, and that would also solve the issue without shared one! both way is working!
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}
.background(Color.gray)
}
}
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
class MainViewModel: ObservableObject {
static let shared: MainViewModel = MainViewModel() // <<: Here
#Published var selectedTab = Page.newest
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
Category(title: "iPhone", icon: "iphone.homebutton", color: .yellow, page: .iPhone),
Category(title: "ipad", icon: "ipad.homebutton", color: .yellow, page: .iPad),
]
struct TabView: View {
let title: String
let icon: String
let color: Color
let page: Page
#ObservedObject var macMainVM = MainViewModel.shared // <<: Here
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
macMainVM.selectedTab = page
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
.background(macMainVM.selectedTab == page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}

Related

Swiftui items get duplicated in all views when added to a single custom view

I'm struggling with the following issue: I'm trying to build a very simple app that lets you add items in a dedicated view that can be collapsed. I managed to write a simple function that lets me add multiple of these custom collapsable views. It's my first app so I wanted to follow the MVVM protocol. I think I got confused along the way because now every item I add gets automatically added to all the custom collapsable views I made. Is there any way to fix this? I thought using the UUID would solve this issue.. I'm guessing that I have to customise the "saveButtonPressed" function, but I don't know how to tell it to only add the item to the view where I pressed the "plus" button..
Here are the Models for the individual items and the collapsable view:
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
--
import Foundation
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
These are my two ViewModels:
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
--
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
The item view:
struct ListRowView: View {
#EnvironmentObject var listViewModel: ListViewModel
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
The collapsable view:
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
#State private var collapsed: Bool = true
#EnvironmentObject var listViewModel: ListViewModel
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
ZStack{
VStack {
HStack{
Button(
action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" :
"chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: 396, maxWidth: 396, minHeight: 0, maxHeight: collapsed ?
0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
and finally the main view:
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
var body: some View {
ZStack{
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
.padding()
.padding(.leading)
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer() }
.frame(maxWidth: .infinity)
})
}
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
Would love to understand how I could fix this!
There is the anwser for your comment about add item in each CollapsedView2.
Because ListViewModel is not ObservableObject (ListViewModel is difference from each CollapsableItem). You should use "#State var items: [ItemModel]".
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
// #State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
#State var items: [ItemModel] = []
#State var count = 1
var body: some View {
VStack {
HStack{
Text("Hello")
.font(.title2.bold())
Spacer()
Button( action: { self.collapsed.toggle() },
label: {
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
addItem(title: "Hello \(count)")
count += 1
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
There is the anwser. Ask me if you have some questions
struct ListView: View {
#StateObject var collapsedViewModel = CollapsedViewModel()
var body: some View {
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer()
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
VStack {
HStack{
Button( action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
struct ListRowView: View {
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
class ListViewModel {
var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}

SwiftUI clear button for all Toggles in a List

I am new to SwiftUI and here is what I want to achieve:
I have a Form with sections. Each section has a list and each row has a Toggle I can select/deselect. Below the list, I have a clear button. With a click, I would like to reset all the Toggles to a deselected state.
Model (each ListSection represents Section in Form):
struct RowItem: Identifiable {
let id = UUID()
var isSelected: Bool
var rowName: String
var amount: Int
}
struct ListSection: Identifiable, Equatable {
static func == (lhs: ListSection, rhs: ListSection) -> Bool {
lhs.id == rhs.id
}
let id = UUID()
let title: String
let items: [RowItem]
let visible: Int
}
class HomeModel: ObservableObject {
private let items1 = [RowItem(isSelected: false, rowName: "Energy", amount: 24),
RowItem(isSelected: true, rowName: "Automotive", amount: 70),
RowItem(isSelected: true, rowName: "Materials", amount: 56),
RowItem(isSelected: false, rowName: "Industrials", amount: 109),
RowItem(isSelected: true, rowName: "Consumer1", amount: 209),
RowItem(isSelected: true, rowName: "Consumer2", amount: 12),]
private let items2 = [RowItem(isSelected: true, rowName: "Europe", amount: 302),
RowItem(isSelected: true, rowName: "America", amount: 589),
RowItem(isSelected: false, rowName: "Asia", amount: 67),
RowItem(isSelected: false, rowName: "Africa", amount: 207),
RowItem(isSelected: true, rowName: "Oceania", amount: 9)]
#Published var sections: [ListSection]
init() {
self.sections = [ListSection(title: "Sectors", items: items1, visible: 4),
ListSection(title: "Regions", items: items2, visible: 5)]
}
}
struct ExpandableList: View {
private var title: String
private var visibleElements: Int
private var showAllButtonVisible: Bool
#State private var items: [RowItem]
#State private var isExpanded = false
init(title: String, items: [RowItem], visible: Int) {
self.title = title.lowercased()
self.items = items
self.visibleElements = visible
self.showAllButtonVisible = visible < items.count
}
var body: some View {
List {
ForEach(isExpanded ? 0..<items.count : 0..<Array(items[0..<visibleElements]).count, id: \.self) { index in
HStack {
Text(items[index].rowName).fontWeight(.light)
Spacer()
Text("\(items[index].amount)").foregroundColor(.gray).fontWeight(.light)
Toggle(isOn: Binding(
get: { return items[index].isSelected },
set: { items[index].isSelected = $0 }
)){}.toggleStyle(.checklist)
}.listRowSeparator(.hidden)
}
if showAllButtonVisible {
HStack {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
if isExpanded {
Label("Show less \(title)", systemImage: "chevron.up").foregroundColor(Color(red: 55/255, green: 126/255, blue: 83/255, opacity: 1)).labelStyle(ImageToRightSideLabelStyle())
} else {
Label("Show all \(title)", systemImage: "chevron.down").foregroundColor(Color(red: 55/255, green: 126/255, blue: 83/255, opacity: 1)).labelStyle(ImageToRightSideLabelStyle())
}
}
}
}
}
}
}
struct MyForm: View {
#ObservedObject var model = HomeModel()
var body: some View {
NavigationView {
VStack {
Form {
ForEach(model.sections, id: \.id) { section in
Section {
ExpandableList(title: section.title, items: section.items, visible: section.visible)
} header: {
Text(section.title).font(.title3).foregroundColor(.black).textCase(nil)
} footer: {
if section != model.sections.last {
VStack {
Divider().padding(.top, 15)
}
}
}
}
}.navigationTitle(Text("Filter stocks"))
HStack {
Button {
for section in model.sections {
for var item in section.items {
item.isSelected = false
print("Tag1 false")
}
}
} label: {
Text("Clear filters").foregroundColor(.black)
}.padding(.leading, 30)
Spacer()
Button(action: {
print("sign up bin tapped")
}) {
Text("Show 1506 assets")
.font(.system(size: 18))
.padding()
.foregroundColor(.black)
}
.background(Color.green) // If you have this
.cornerRadius(10)
.padding(.trailing, 30)
}
}
}
}
}
I assume I need to use Binding somewhere, but not sure where.
I made a small project to address your issue. The main takeaway is that you need to let SwiftUI know when to redraw the view after the model or model element's properties change. That can be done with #Published and/or objectWillChange which is available to any class conforming to the ObservableObject protocol.
import SwiftUI
struct ContentView: View {
#StateObject var model = Model()
class Model: ObservableObject {
#Published var items = [
Item(zone: "America", count: 589),
Item(zone: "Asia", count: 67),
Item(zone: "Africa", count: 207),
Item(zone: "Oceania", count: 9)
]
class Item: ObservableObject, Hashable {
static func == (lhs: ContentView.Model.Item, rhs: ContentView.Model.Item) -> Bool {
lhs.id == rhs.id
}
let id = UUID()
let zone: String
let count: Int
#Published var selected: Bool = false
init(zone: String, count: Int) {
self.zone = zone
self.count = count
}
func toggle() {
selected.toggle()
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}
var body: some View {
VStack {
ForEach(Array(model.items.enumerated()), id: \.element) { index, item in
HStack {
Text(item.zone)
Spacer()
Text(String(item.count))
Button {
item.toggle()
print(index, item.selected)
} label: {
Image(systemName: item.selected ? "checkmark.square" : "square")
.resizable()
.frame(width: 24, height: 24)
.font(.system(size: 20, weight: .regular, design: .default))
}
}
.padding(10)
.onReceive(item.$selected) { isOn in
model.objectWillChange.send()
}
}
Button {
model.items.forEach { item in
item.selected = false
}
model.objectWillChange.send()
} label: {
Text("Clear")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
https://github.com/aibo-cora/SwiftUI/blob/main/ResetCheckBoxes.md

I can't seem to be able to update the state of my index variable(SwiftUI)

I want depending on which button is pressed to update the index number. Also when I print it, I want it to show me that it is well updated.
struct idontcare { //Named it this way cause I got mad
#State var index: Int = 0
let buttons: [String] = [ "Primary", "Blue", "Green", "Red", "Yellow", "Orange","Gray", "Purple", "Cyan", "Pink", "Teal"]
let buttonColor: [Color] = [Color.primary, Color.blue, Color.green, Color.red, Color.yellow, Color.orange, Color.gray, Color.purple, Color.cyan, Color.pink, Color.teal]
func showMenu() -> some View{
return Menu("Update Color: "){
ForEach(buttons, id: \.self) { button in
Button(action: {
self.index = 5 //DOES NOT WORK!!
print(self.index) //ALWAYS PRINTS 0!
}) {
Label(button, systemImage: "paintbrush.pointed")
}
}
}
}
}
You code is missing the body property.
The struct "Idontcare" has to conform to View, it might got lost while pasting to your question.
However, if I add both to your code, everything works fine for me.
struct Idontcare: View { //Has to conform to View
#State var index: Int = 0
let buttons: [String] = [ "Primary", "Blue", "Green", "Red", "Yellow", "Orange","Gray", "Purple", "Cyan", "Pink", "Teal"]
let buttonColor: [Color] = [Color.primary, Color.blue, Color.green, Color.red, Color.yellow, Color.orange, Color.gray, Color.purple, Color.cyan, Color.pink, Color.teal]
var body: some View { //was missing in your Code
showMenu()
}
func showMenu() -> some View{
return Menu("Update Color: "){
ForEach(buttons, id: \.self) { button in
Button(action: {
self.index = 5 //Works now
print(self.index)
}) {
Label(button, systemImage: "paintbrush.pointed")
}
}
}
}
}
This should work just like what you wanted. I made a Test view, you can follow the same logic.
import SwiftUI
struct Test: View {
#State var index: Int = 0
let buttons: [String] = [ "Primary", "Blue", "Green", "Red", "Yellow", "Orange","Gray", "Purple", "Cyan", "Pink", "Teal"]
let buttonColor: [Color] = [Color.primary, Color.blue, Color.green, Color.red, Color.yellow, Color.orange, Color.gray, Color.purple, Color.cyan, Color.pink, Color.teal]
var body: some View {
ForEach(0..<buttons.count, id: \.self) { index in
Button(action: {
print(index)
}) {
Label(buttons[index], systemImage: "paintbrush.pointed")
}
}
}
}
You can try the below code. it will help you to get selected button index.
struct idontcare : View {
#State var index: Int = 0
let buttons: [String] = ["Primary", "Blue", "Green", "Red", "Yellow", "Orange","Gray", "Purple", "Cyan", "Pink", "Teal"]
let buttonColor: [Color] = [Color.primary, Color.blue, Color.green, Color.red, Color.yellow, Color.orange, Color.gray, Color.purple, Color.gray, Color.pink, Color.black]
var body: some View {
showMenu()
}
func showMenu() -> some View{
return Menu("Update Color: ") {
ForEach(0..<buttons.count, id: \.self) { index in
Button(action: {
self.index = index
debugPrint("Color Selected = \(buttons[index])")
debugPrint("Selected Color Index = \(self.index)")
}) {
Label(buttons[index], systemImage: "paintbrush.pointed")
}
}
}
}
}

SwiftUI Expand Lists One-At-A-Time, Auto Collapse

I'm trying to build a view with several collapsed lists, only the headers showing at first. When you tap on a header, its list should expand. Then, with the first list is expanded, if you tap on the header of another list, the first list should automatically collapse while the second list expands. So on and so forth, so only one list is visible at a time.
The code below works great to show multiple lists at the same time and they all expand and collapse with a tap, but I can't figure out what to do to make the already open lists collapse when I tap to expand a collapsed list.
Here's the code (sorry, kind of long):
import SwiftUI
struct Task: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: Task
#State private var isExpanded = false
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
struct ContentView: View {
//sample data
private let tasks: [Task] = [
Task(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
Task(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task)
.animation(.default)
}
Spacer()
}
}
}
}
Thanks ahead for any help!
EDIT: Here's the collapse functionality to go with the accepted solution below:
Update the onTapGesture in private var header: some View to look like this:
.onTapGesture {
withAnimation {
if task.isExpanded {
viewmodel.collapse(task)
} else {
viewmodel.expand(task)
}
}
}
Then add the collapse function to class Viewmodel
func collapse(_ taks: TaskModel) {
var tasks = self.tasks
tasks = tasks.map {
var tempVar = $0
tempVar.isExpanded = false
return tempVar
}
self.tasks = tasks
}
That's it! Fully working as requested!
I think the best way to achieve this is to move the logic to a viewmodel.
struct TaskModel: Identifiable {
let id: String = UUID().uuidString
let title: String
let subtask: [Subtask]
var isExpanded: Bool = false // moved state variable to the model
}
struct Subtask: Identifiable {
let id: String = UUID().uuidString
let title: String
}
struct SubtaskCell: View {
let task: Subtask
var body: some View {
HStack {
Image(systemName: "circle")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
}
}
struct TaskCell: View {
var task: TaskModel
#EnvironmentObject private var viewmodel: Viewmodel //removed state here and added viewmodel from environment
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if task.isExpanded {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
viewmodel.expand(task) //handle expand / collapse here
}
}
}
}
struct ContentView: View {
#StateObject private var viewmodel: Viewmodel = Viewmodel() //Create viewmodel here
var body: some View {
NavigationView {
VStack(alignment: .leading) {
ForEach(viewmodel.tasks) { task in //use viewmodel tasks here
TaskCell(task: task)
.animation(.default)
.environmentObject(viewmodel)
}
Spacer()
}
}
}
}
class Viewmodel: ObservableObject{
#Published var tasks: [TaskModel] = [
TaskModel(
title: "Create playground",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Write article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Prepare assets",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
TaskModel(
title: "Publish article",
subtask: [
Subtask(title: "Cover image"),
Subtask(title: "Screenshots"),
]
),
]
func expand(_ task: TaskModel){
//copy tasks to local variable to avoid refreshing multiple times
var tasks = self.tasks
//create new task array with isExpanded set
tasks = tasks.map{
var tempVar = $0
tempVar.isExpanded = $0.id == task.id
return tempVar
}
// assign array to update view
self.tasks = tasks
}
}
Notes:
Renamed your task model as it is a very bad idea to name somthing with a name that is allready used by the language
This only handles expanding. But implementing collapsing shouldn´t be to hard :)
Edit:
if you dont want a viewmodel you can use a binding as alternative:
Add to your containerview:
#State private var selectedId: String?
change body to:
NavigationView {
VStack(alignment: .leading) {
ForEach(tasks) { task in
TaskCell(task: task, selectedId: $selectedId)
.animation(.default)
}
Spacer()
}
}
and change your TaskCell to:
struct TaskCell: View {
var task: TaskModel
#Binding var selectedId: String?
var body: some View {
content
.padding(.leading)
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if selectedId == task.id {
Group {
List(task.subtask) { subtask in
SubtaskCell(task: subtask)
}
}
.padding(.leading)
}
Divider()
}
}
private var header: some View {
HStack {
Image(systemName: "square")
.foregroundColor(Color.primary.opacity(0.2))
Text(task.title)
}
.padding(.vertical, 4)
.onTapGesture {
withAnimation {
selectedId = selectedId == task.id ? nil : task.id
}
}
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

Resources