SwiftUI: How to select multi items(image) with ForEach? - 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.

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: Running into issues with Drag and Drop inside a LazyVGrid

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

State is nil when showing sheet

For some reason, my selectedTask State is Empty when presenting the Sheet,
even if I set it on the onTapGesture.
What I'm I missing?
struct TasksTabView: View {
#State private var showComputedTaskSheet: Bool = false
#State var selectedTask: OrderTaskCheck?
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(Array(tasks.enumerated()), id:\.1.title) { (index, task) in
VStack(alignment: .leading, spacing: 40) {
HStack(spacing: 20) {
PillForRow(index: index, task: task)
}.padding(.bottom, 30)
}.onTapGesture {
// Where I'm setting selectedTask
self.selectedTask = task
self.showComputedTaskSheet.toggle()
}
}
}
}.listStyle(SidebarListStyle())
}
.sheet(isPresented: $showComputedTaskSheet) {
// self.selectedTask is returns nil
showScreen(task: self.selectedTask!)
}
.onAppear {
UITableView.appearance().backgroundColor = .white
}
}
Since I have no access to your full project this example can help you to get the idea, you can use .sheet() with item initializer like aheze said.
The advantage is here you pass optional to input item and you receive unwrapped safe value to work!
struct ContentView: View {
#State private var customValue: CustomValue?
var body: some View {
Button("Show the Sheet View") { customValue = CustomValue(description: "Hello, World!") }
.sheet(item: $customValue){ item in sheetView(item: item) }
}
func sheetView(item: CustomValue) -> some View {
return VStack {
Text(item.description)
Button("Close the Sheet View") { customValue = nil }.padding()
}
}
}
struct CustomValue: Identifiable {
let id: UUID = UUID()
var description: String
}

SwiftUI - List that does not display

I don't know how to explain it, I think a video will be more explicit ..
https://www.youtube.com/watch?v=UyuGaNA6NCo
I only want to display the information according to my choices, and I don't know where I was wrong, I am in the problem since some hours
My code :
HOME VIEW
struct HomeView: View {
#ObservedObject var travelLibrary: TravelLibrary
#State private var isShowingSurveyCreation = false
var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
NavigationView {
ScrollView {
VStack {
ForEach(travelLibrary.testTravels) { travel in
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color.white)
.shadow(color: /*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/, radius: 7)
TravelCellView(travel: travel)
}
}
}.padding(12)
}.navigationTitle(Text("Mes Voyages"))
}
PlusButtonView(action: {
isShowingSurveyCreation.toggle()
}).sheet(isPresented: $isShowingSurveyCreation, content: {
TravelCreationView(travelLibrary: travelLibrary)
})
.padding()
}
}
}
struct HomeView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
HomeView(travelLibrary: travelLibrary)
}
}
TRAVEL CREATION VIEW
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}
//
// TravelCreationView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}//
// TravelCreationView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}
TRAVELCELLVIEW
//
// TravelCellView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCellView: View {
let travel : Travel
var body: some View {
HStack {
Image(travel.flagCountry)
.resizable()
.aspectRatio(contentMode: /*#START_MENU_TOKEN#*/.fill/*#END_MENU_TOKEN#*/)
.frame(width: 80, height: 80)
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.padding(.trailing, 8)
VStack(alignment : .leading) {
Text(travel.nameCountry)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(Color.black)
.padding(.bottom, -4)
Text("Durée du voyage: \(travel.day) jours")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(Color.black)
.padding(.bottom, -4)
Text(travel.typeOfTravel)
.font(.subheadline)
.foregroundColor(Color.black)
}
Spacer()
FavouriteButtonView()
}.padding()
}
}
struct TravelCellView_Previews: PreviewProvider {
private static let testTravel = Travel(flagCountry: "USAflag", nameCountry: "États-Unis", typeOfTravel: "Tourisme", day: 0, isFavourite: false)
static var previews: some View {
TravelCellView(travel: testTravel)
.previewLayout(.sizeThatFits)
}
}
DATA VIEW
import Foundation
struct Travel: Identifiable {
let id = UUID().uuidString
let flagCountry: String
let nameCountry: String
let typeOfTravel: String
let day: Int
var isFavourite: Bool
init(flagCountry: String, nameCountry: String, typeOfTravel: String, day: Int, isFavourite: Bool) {
self.flagCountry = flagCountry
self.nameCountry = nameCountry
self.typeOfTravel = typeOfTravel
self.day = day
self.isFavourite = isFavourite
}
init(config: NewTravelConfig) {
flagCountry = ""
nameCountry = ""
typeOfTravel = ""
day = 0
isFavourite = false
}
}
class TravelLibrary: ObservableObject {
#Published var testTravels = [
Travel(flagCountry: "USAflag", nameCountry: "États-Unis", typeOfTravel: "Tourisme", day: 0, isFavourite: false),
Travel(flagCountry: "Japanflag", nameCountry: "Japon", typeOfTravel: "Tourisme", day: 0, isFavourite: true),
Travel(flagCountry: "Germanflag", nameCountry: "Allemagne", typeOfTravel: "Tourisme", day: 0, isFavourite: false)
]
}
class NewTravelConfig: ObservableObject {
#Published var nameCountry = ["États-Unis", "Japon", "Allemagne"]
#Published var typeOfTravel = ["Tourisme", "Business"]
#Published var day = 0
#Published var index1 = 0
#Published var index2 = 0
}
Thanks!
Your issue is inside the Travel struct. You have two init() methods, and the issue is in the one where you pass your config.
You are passing the config to it, but not using any of these values from the config.
Here is the correct one:
init(config: NewTravelConfig) {
flagCountry = ""
nameCountry = config.nameCountry[config.index1]
typeOfTravel = config.typeOfTravel[config.index2]
day = config.day
isFavourite = false
}

SwiftUI => AttributeGraph: cycle detected through attribute after updating one model in list

Context:
I've got a list of custom views. The array is stored a #ObservableObject as #Published.
My custom view has a function which detects when the View is touched (I did it because it's triggered only after an animation). This event activates, through my #ObservableObject, an event that shows a View which is in ZStack with my list. There I could update my passed object through a TextField, and when I come back I have everything updated.
However, when I try to re-show one of every element in my list, my debug shows this error:
AttributeGraph: cycle detected through attribute.
Instead, if I show the detail without updating my model's data, I have not any leak.
Any suggestion?
EDIT:
here's the code:
struct ProcedureList: View {
#ObservedObject var procedureManager = ProcedureManager()
#State private var showModal = false
var isEmpty:Bool {
return procedureManager.procedures.isEmpty
}
init() {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
}
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
VStack{
if !self.isEmpty {
List {
ForEach(self.procedureManager.procedures.indices, id: \.self) { index in
ProcedureCell(procedure: self.$procedureManager.procedures[index]){ procedure, position, size in
self.procedureManager.selectedProcedure = procedure
self.procedureManager.cardSize = size
self.procedureManager.cardPosition = position
self.procedureManager.size = size
self.procedureManager.position = position
self.procedureManager.isPressed = true
withAnimation(Animation.default.delay(0.1)) {
self.procedureManager.size.width = geometry.frame(in: .local).width
self.procedureManager.size.height = geometry.frame(in: .local).size.height
self.procedureManager.position.x = geometry.frame(in: .global).origin.x
self.procedureManager.position.y = geometry.frame(in: .global).origin.y
}
print(
"""
pressed procedure: \(procedure.title)
at position: \(position)
and with size: \(size)
"""
)
}
// .tag(self.procedureManager.procedures[index])
.tag(index)
}
.onDelete(perform: self.onDelete)
}
.environment(\.defaultMinListRowHeight, 120)
.animation(.easeInOut)
}else {
VStack{
Text("Non hai ancora creato una procedura!")
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding(.bottom, 30)
Button(action: {
self.showModal.toggle()
}){
Text("Creane una nuova!")
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
}
}.padding(20)
}
}
Rectangle()
.edgesIgnoringSafeArea(.all)
.zIndex(self.procedureManager.isPressed ? 0 : -1)
.opacity(self.procedureManager.isPressed ? 0.7 : 0)
.animation(Animation.easeInOut(duration: 0.5))
ProcedureDetail(action: { procedure in
self.procedureManager.update(procedure: procedure)
}, procedure: self.$procedureManager.selectedProcedure, isShowingDetail: self.$procedureManager.isPressed)
.frame(width: self.procedureManager.correctSize.width, height: self.procedureManager.correctSize.height)
.position(x: self.procedureManager.correctPosition.x, y: self.procedureManager.correctPosition.y - (geometry.frame(in: .global).origin.y))
.offset(x: self.procedureManager.correctSize.width / 2, y: self.procedureManager.correctSize.height / 2)
.animation(.easeInOut)
.opacity(self.procedureManager.correctOpacity)
.animation(Animation.easeInOut.delay(self.procedureManager.isPressed ? 0 : 0.2))
}
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.onDisappear() {
UITableView.appearance().separatorStyle = .singleLine
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(trailing:
!self.isEmpty && !self.procedureManager.isPressed ?
Button(action: {
self.showModal.toggle()
}){
Image(systemName: "plus.circle.fill")
.font(Font.system(size: 40))
.foregroundColor(Color.red)
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
} : nil
)
}
}
}
private func onDelete(offsets: IndexSet) {
self.procedureManager.procedures.remove(atOffsets: offsets)
}
}
struct ProcedureCell: View {
#Binding var procedure: Procedure
#State var position:CGPoint = .zero
#State var size:CGSize = .zero
var action:(_ procedure:Procedure, _ position: CGPoint, _ size:CGSize)->Void
var body: some View {
return
GeometryReader { geometry in
Button(action: {
let position = geometry.frame(in: .global).origin
let size = geometry.frame(in: .global).size
self.action(self.procedure, position, size)
}){
HStack {
VStack(alignment: .leading) {
Text(self.procedure.title)
.font(.largeTitle)
Text(self.procedure.subtitle)
.font(.title)
}
.padding(10)
Spacer()
}
}
.buttonStyle(MyButtonStyle())
.padding([.top, .bottom])
.edgesIgnoringSafeArea(.all)
}
}
}
struct MyButtonStyle:ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(
Rectangle()
.fill(configuration.isPressed ? Color.red : Color.orange)
.cornerRadius(20)
.shadow(radius: configuration.isPressed ? 5 : 0)
)
.scaleEffect(configuration.isPressed ? 1.1 : 1)
.animation(.easeInOut)
}
}
struct Procedure: Identifiable {
var title: String
var subtitle: String
var id: String
static var empty:Procedure {
return Procedure(title: "", subtitle: "")
}
init (title:String, subtitle:String) {
self.id = UUID().uuidString
self.title = title
self.subtitle = subtitle
}
}
class ProcedureManager: ObservableObject {
#Published var procedures: [Procedure]
#Published var newProcedure = Procedure.empty
#Published var selectedProcedure = Procedure.empty
#Published var cardSize:CGSize = .zero
#Published var cardPosition:CGPoint = .zero
#Published var size:CGSize = .zero
#Published var position:CGPoint = .zero
#Published var isPressed:Bool = false
var correctSize:CGSize {
if isPressed {
return size
}
else{
return cardSize
}
}
var correctPosition:CGPoint {
if isPressed {
return position
}
else{
return cardPosition
}
}
var correctOpacity: Double {
return isPressed ? 1 : 0
}
func update(procedure:Procedure) {
if let index = procedures.compactMap({$0.id}).firstIndex(of: procedure.id) {
procedures[index].title = procedure.title
procedures[index].subtitle = procedure.subtitle
objectWillChange.send()
}
}
func createProcedure(){
procedures.append(newProcedure)
newProcedure = .empty
}
func createProcedure(with title:String, andSubtitle subtitle:String) {
let procedure = Procedure(title: title, subtitle: subtitle)
procedures.append(procedure)
}
init(){
procedures = [
Procedure(title: "test1", subtitle: "subtitletest1"),
Procedure(title: "test2", subtitle: "subtitletest2"),
Procedure(title: "test3", subtitle: "subtitletest3"),
Procedure(title: "test4", subtitle: "subtitletest4"),
Procedure(title: "test5", subtitle: "subtitletest5"),
]
}
}

Resources