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
Related
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)
}
}
I've been working on my own smart home app and have run into some issues when trying to build the grid for the app.
I've been basing this home app on this tutorial. The goal is that one can reorder the individually sized blocks in the grid basically like he or she wants. The blocks(items) represent different gadgets in the smart home application. The issue I'm facing is that I can't seem to get the drag & drop to work. Maybe it's better to put all the item views in one custom view and then run a "ForEach" loop for them so that the .onDrag works? I'm relatively new to SwiftUI so I appreciate every hint I can get this program to work.
Here is my code:
ItemModel1:
struct ItemModel: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
ItemModel2:
struct ItemModel2: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel2 {
return ItemModel2(id: id, title: title)
}
}
It's essentially the same for the two other ItemModels 3 and 4..
ItemViewModel:
class ItemViewModel {
var items: [ItemModel] = []
#Published var currentGrid: ItemModel?
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "Item1"),
]
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()
}
}
}
ContentView:
struct DropViewDelegate: DropDelegate {
var grid: ItemModel
var gridData: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == gridData.currentGrid?.id
} ?? 0
let toIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == self.grid.id
} ?? 0
if fromIndex != toIndex{
withAnimation(.default){
let fromGrid = gridData.items[fromIndex]
gridData.items[fromIndex] = gridData.items[toIndex]
gridData.items[toIndex] = fromGrid
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct ContentView: View {
#State var items: [ItemModel] = []
#State var items2: [ItemModel2] = []
#State var items3: [ItemModel3] = []
#State var items4: [ItemModel4] = []
#State var gridData = ItemViewModel()
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
let columns2 = [
GridItem(.flexible()),
]
var body: some View {
ZStack{
ScrollView{
VStack{
HStack(alignment: .top){
Button(action: saveButtonPressed, label: {
Text("Item1")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed2, label: {
Text("Item2")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed3, label: {
Text("Item3")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed4, label: {
Text("Item4")
.font(.title2)
.foregroundColor(.white)
})
}
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 12
){
ForEach(items) { item in
Item1View (item: item)
if 1 == 1 { Color.clear }
}
ForEach(items4) { item4 in
Item4View (item4: item4)
if 1 == 1 { Color.clear }
}
ForEach(items2) { item2 in
Item2View (item2: item2)
}
LazyVGrid(
columns: columns2,
alignment: .leading,
spacing: 12
){
ForEach(items3) { item3 in
Item3View (item3: item3)
}
}
}
.onDrag({
self.gridData = items
return NSItemProvider(item: nil, typeIdentifier:
self.grid)
})
.onDrop(of: [items], delegate: DropViewDelegate(grid:
items, gridData: gridData))
}
}
}
}
func saveButtonPressed() {
addItem(title: "Hello")
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func saveButtonPressed2() {
addItem2(title: "Hello")
}
func addItem2(title: String) {
let newItem = ItemModel2(title: title)
items2.append(newItem)
}
func saveButtonPressed3() {
addItem3(title: "Hello")
}
func addItem3(title: String) {
let newItem = ItemModel3(title: title)
items3.append(newItem)
}
func saveButtonPressed4() {
addItem4(title: "Hello")
}
func addItem4(title: String) {
let newItem = ItemModel4(title: title)
items4.append(newItem)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Item1:
struct Item1View: View {
#State var item: ItemModel
var body: some View {
HStack {
Text(item.title)
}
.padding( )
.frame(width: 396, height: 56)
.background(.black)
.cornerRadius(12.0)
}
}
Item2:
struct Item2View: View {
#State var item2: ItemModel2
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.black)
.cornerRadius(12.0)
}
}
Item3:
struct Item3View: View {
#State var item3: ItemModel3
var body: some View {
HStack {
Text(item3.title)
}
.padding( )
.frame(width: 182, height: 62)
.background(.black)
.cornerRadius(12.0)
}
}
Item4:
struct Item4View: View {
#State var item4: ItemModel4
var body: some View {
HStack {
Text(item4.title)
}
.padding( )
.frame(width: 396, height: 156)
.background(.black)
.cornerRadius(12.0)
}
}
I tried recreating the grid Asperi linked. However, the .onDrop doesn't seem to work like it should. The drop only occurs after you pressed another item to drag it. Only then will the previous items reorder themselves..
My version:
import SwiftUI
import UniformTypeIdentifiers
struct ItemModel6: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel6 {
return ItemModel6(id: id, title: title)
}
}
class Model: ObservableObject {
var data: [ItemModel6] = []
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
init() {
data = Array(repeating: ItemModel6(title: "title"), count:
100)
for i in 0..<data.count {
data[i] = ItemModel6(title: "Hello")
}
}
}
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: ItemModel6?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns) {
ForEach(model.data) { item2 in GridItemView (item2:
item2)
.overlay(dragging?.id == item2.id ?
Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = item2
return NSItemProvider(object:
String(item2.id) as NSString)
}
.onDrop(of: [UTType.text], delegate:
DragRelocateDelegate(item: item2, listData: $model.data,
current: $dragging))
}
}.animation(.default, value: model.data)
}
.onDrop(of: [UTType.text], delegate:
DropOutsideDelegate(current: $dragging))
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: ItemModel6?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DragRelocateDelegate: DropDelegate {
let item: ItemModel6
#Binding var listData: [ItemModel6]
#Binding var current: ItemModel6?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
struct GridItemView: View {
#State var item2: ItemModel6
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.gray)
.cornerRadius(12.0)
}
}
struct DemoDragRelocateView_Previews: PreviewProvider {
static var previews: some View {
DemoDragRelocateView()
.preferredColorScheme(.dark)
}
}
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.
I am trying to change a value inside a for in loop. The value Is a bool that I declared as a var and not a let but I get the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
So im trying to make it so that when I tap the image inside the button in my list it will change the completed (bool) value to true. And I want it so that when completed == true I get a filled checkmark
import SwiftUI
struct Notat : Identifiable
{
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
struct ContentView: View {
var modelData: [Notat] =
[Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)]
var body: some View {
List(modelData)
{
notat in HStack
{
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/)
{
Image(systemName: checkmarkSymbol(completed: notat.completed)).font(Font.system(size: 25, weight: .light))
.onTapGesture
{
test(notat: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
func checkmarkSymbol(completed: Bool) -> String
{
if(completed)
{
return "checkmark.square.fill"
}else
{
return "checkmark.square"
}
}
func test(notat: Notat)
{
notat.completed.toggle() //Here is the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
}
You could do something like the following:
import SwiftUI
struct Notat : Identifiable, Equatable {
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
//You should change the name to something more descriptive than ViewModel...
class ViewModel: ObservableObject {
#Published var modelData = [
Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)
]
func setCompleted(for notat: Notat) {
guard let index = modelData.firstIndex(of: notat) else { return }
modelData[index].completed.toggle()
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
List(viewModel.modelData) { notat in HStack {
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: {})
{
Image(systemName: self.checkmarkSymbol(completed: notat.completed))
.font(Font.system(size: 25, weight: .light))
.onTapGesture
{
self.viewModel.setCompleted(for: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
func checkmarkSymbol(completed: Bool) -> String {
if (completed) {
return "checkmark.square.fill"
}
else {
return "checkmark.square"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So basically each time you are tapping on an image, the ViewModel will toggle the completed property and since modelData is marked with #Publish these changes will causing the View to reload itself.
Here it's descriped in more detail...
I'm trying to update arrays item with typed new value into Textfield, but List is not updated with edited value.
My Code is:
Model:
struct WalletItem: Identifiable{
let id = UUID()
var name:String
var cardNumber:String
var type:String
var cvc:String
let pin:String
var dateOfExpiry:String
}
ModelView:
class Wallet: ObservableObject{
#Published var wallets = [
WalletItem(name: "BSB", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2016-06-29"),
WalletItem(name: "Alpha bank", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2017-03-12"),
WalletItem(name: "MTБ", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2020-11-12"),
]
}
First View:
struct WalletListView: View {
// Properties
// ==========
#ObservedObject var wallet = Wallet()
#State var isNewItemSheetIsVisible = false
var body: some View {
NavigationView {
List(wallet.wallets) { walletItem in
NavigationLink(destination: EditWalletItem(walletItem: walletItem)){
Text(walletItem.name)
}
}
.navigationBarTitle("Cards", displayMode: .inline)
.navigationBarItems(
leading: Button(action: { self.isNewItemSheetIsVisible = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add item")
}
}
)
}
.sheet(isPresented: $isNewItemSheetIsVisible) {
NewWalletItem(wallet: self.wallet)
}
}
}
and Secondary View:
struct EditWalletItem: View {
#State var walletItem: WalletItem
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form{
Section(header: Text("Card Name")){
TextField("", text: $walletItem.name)
}
}
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Back")
}, trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Save")
})
}
}
P.S: If I use #Binding instead of the #State I've got an error in the first view: Initializer init(_:) requires that Binding<String> conform to StringProtocol
Here are modified parts (tested & works with Xcode 11.2 / iOS 13.2):
Sure over binding
struct EditWalletItem: View {
#Binding var walletItem: WalletItem
Place to pass it
List(Array(wallet.wallets.enumerated()), id: .element.id) { (i, walletItem) in
NavigationLink(destination: EditWalletItem(walletItem: self.$wallet.wallets[i])){
Text(walletItem.name)
}
}
ForEach(Array(list.enumerated())) will only work correctly if the list is an Array but not for an ArraySlice, and it has the downside of copying the list.
A better approach is using a .indexed() helper:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { self.base.startIndex }
var endIndex: Index { self.base.endIndex }
func index(after i: Index) -> Index {
self.base.index(after: i)
}
func index(before i: Index) -> Index {
self.base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
self.base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: self.base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Example:
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Foundation
import SwiftUI
struct Position {
var id = UUID()
var count: Int
var name: String
}
class BookingModel: ObservableObject {
#Published var positions: [Position]
init(positions: [Position] = []) {
self.positions = positions
}
}
struct EditableListExample: View {
#ObservedObject var bookingModel = BookingModel(
positions: [
Position(count: 1, name: "Candy"),
Position(count: 0, name: "Bread"),
]
)
var body: some View {
// >>> Passing a binding into an Array via index:
List(bookingModel.positions.indexed(), id: \.element.id) { i, _ in
PositionRowView(position: self.$bookingModel.positions[i])
}
}
}
struct PositionRowView: View {
#Binding var position: Position
var body: some View {
Stepper(
value: $position.count,
label: {
Text("\(position.count)x \(position.name)")
}
)
}
}
struct EditableListExample_Previews: PreviewProvider {
static var previews: some View {
EditableListExample()
}
}
See also:
How does the Apple-suggested .indexed() property work in a ForEach?