Binding not working in view when view is generated in loop - binding

Problem:
i tried to make this example https://swiftui-lab.com/communicating-with-the-view-tree-part-1/ a bit swiftier by not using the same MonthView() 12 times but in a loop.
Unfortunately when tapping the label/month the variable activeIdx won't be updated and I have no idea why...
and my question is: how do i have to change the code that the binding works?
Expected behaviour:
When you tap on the month names the red border should mark the label you tapped.
import SwiftUI
let months : [String] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
struct ContentView : View {
#State private var activeIdx: Int = 0
var groupedGroupedViews : [[MonthView]] = []
init() {
var groupedViews : [MonthView] = []
for idx in 0..<months.count {
print(idx)
groupedViews.append(MonthView(activeMonth: self.$activeIdx, label: months[idx], idx: idx))
print(months[idx])
if (idx + 1) % 4 == 0 {
groupedGroupedViews.append(groupedViews)
groupedViews = []
}
}
groupedGroupedViews.append(groupedViews)
}
var body: some View {
VStack {
ForEach (groupedGroupedViews, id: \.self) { groupedViews in
HStack {
ForEach (groupedViews, id: \.idx) { view in
view
}
}
}
}
}
}
struct MonthView: View, Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(idx)
}
static func == (lhs: MonthView, rhs: MonthView) -> Bool {
return lhs.idx == rhs.idx
}
#Binding var activeMonth: Int
let label: String
var idx: Int
var body: some View {
Text(label)
.padding(10)
.onTapGesture {
print(self.idx, " tapped")
self.activeMonth = self.idx
print("active = ", self.activeMonth)
}
.background(MonthBorder(show: activeMonth == idx))
}
}
struct MonthBorder: View {
let show: Bool
var body: some View {
RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 3.0).foregroundColor(show ? Color.red : Color.clear)
.animation(.easeInOut(duration: 0.6))
}
}

The reason is you cannot init a binding View like that.
var myView : MonthView!
init() {
myView = MonthView(activeMonth: self.$activeIdx, label: months[0], idx: 1)
}
var body: some View {
myView}
If you run above code, you still can see activeMonth is not changing.
I think the init() mainly bridges some UIKit features and should be not used as a major way in SwiftUI. Cross platform is necessary but try to avoid if possible. Please appreciate the design merit of swiftUI not the overhead from traditional implementations.
One simple answer to this is moving the init to View. That reaches the same function you need.
var body: some View {
let groupedGroupedViews : [[MonthView]] = {
var groupedGroupedViews : [[MonthView]] = []
var groupedViews : [MonthView] = []
for idx in 0..<months.count {
print(idx)
groupedViews.append( MonthView(activeMonth: self.$activeIdx, label: months[idx], idx: idx))
print(months[idx])
if (idx + 1) % 4 == 0 {
groupedGroupedViews += [groupedViews]
groupedViews.removeAll()
}
}
groupedGroupedViews += [groupedViews]
return groupedGroupedViews
}()
return VStack {
ForEach (groupedGroupedViews, id: \.self) { groupedView in
HStack {
ForEach (groupedView, id: \.idx) { view in
view
}
}
}
}
}

I found it, thanks to E.Coms, i have changed my code and now it works.
Here my solution (but i am pretty sure you could do better)
struct ViewValue : Hashable {
var idx: Int
var text: String
}
struct ContentView : View {
#State private var activeIdx: Int = 0
var groupedGroupedValues : [[ViewValue]] = []
init() {
var groupedValues : [ViewValue] = []
for idx in 0..<months.count {
print(idx)
groupedValues.append(ViewValue(idx: idx, text: months[idx]))
print(months[idx])
if (idx + 1) % 4 == 0 {
groupedGroupedValues.append(groupedValues)
groupedValues = []
}
}
groupedGroupedValues.append(groupedValues)
}
var body: some View {
VStack {
ForEach (groupedGroupedValues, id: \.self) { groupedValue in
HStack {
ForEach (groupedValue, id: \.idx) { viewValue in
MonthView(activeMonth: self.$activeIdx, label: viewValue.text, idx: viewValue.idx)
}
}
}
}
}
}

Related

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

Observable object model not changing View values when model is updated

I'm losing my mind over this, please help
I'm following the standford's iOS tutorial, I'm trying to finish an assignment of creating a card games, I have 3 models, Game, Card, Theme and Themes:
Game and Card are in charge of the main game logic
import Foundation
struct Game {
var cards: [Card]
var score = 0
var isGameOver = false
var theme: Theme
var choosenCardIndex: Int?
init(theme: Theme) {
cards = []
self.theme = theme
startTheme()
}
mutating func startTheme() {
cards = []
var contentItems: [String] = []
while contentItems.count != theme.numberOfPairs {
let randomElement = theme.emojis.randomElement()!
if !contentItems.contains(randomElement) {
contentItems.append(randomElement)
}
}
let secondContentItems: [String] = contentItems.shuffled()
for index in 0..<theme.numberOfPairs {
cards.append(Card(id: index*2, content: contentItems[index]))
cards.append(Card(id: index*2+1, content: secondContentItems[index]))
}
}
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
mutating func endGame() {
isGameOver = true
}
mutating func penalizePoints() {
score -= 1
}
mutating func awardPoints () {
score += 2
}
struct Card: Identifiable, Equatable {
static func == (lhs: Game.Card, rhs: Game.Card) -> Bool {
return lhs.content == rhs.content
}
var id: Int
var isFaceUp: Bool = false
var content: String
var isMatchedUp: Bool = false
var isPreviouslySeen = false
}
}
Theme is for modeling different kind of content, Themes is for keeping track which one is currently in use and for fetching a new one
import Foundation
import SwiftUI
struct Theme: Equatable {
static func == (lhs: Theme, rhs: Theme) -> Bool {
return lhs.name == rhs.name
}
internal init(name: String, emojis: [String], numberOfPairs: Int, cardsColor: Color) {
self.name = name
self.emojis = Array(Set(emojis))
if(numberOfPairs > emojis.count || numberOfPairs < 1) {
self.numberOfPairs = emojis.count
} else {
self.numberOfPairs = numberOfPairs
}
self.cardsColor = cardsColor
}
var name: String
var emojis: [String]
var numberOfPairs: Int
var cardsColor: Color
}
import Foundation
struct Themes {
private let themes: [Theme]
public var currentTheme: Theme?
init(_ themes: [Theme]) {
self.themes = themes
self.currentTheme = getNewTheme()
}
private func getNewTheme() -> Theme {
let themesIndexes: [Int] = Array(0..<themes.count)
var visitedIndexes: [Int] = []
while(visitedIndexes.count < themesIndexes.count) {
let randomIndex = Int.random(in: 0..<themes.count)
let newTheme = themes[randomIndex]
if newTheme == currentTheme {
visitedIndexes.append(randomIndex)
} else {
return newTheme
}
}
return themes.randomElement()!
}
mutating func changeCurrentTheme() -> Theme {
self.currentTheme = getNewTheme()
return self.currentTheme!
}
}
This is my VM:
class GameViewModel: ObservableObject {
static let numbersTheme = Theme(name: "WeirdNumbers", emojis: ["1", "2", "4", "9", "20", "30"], numberOfPairs: 6, cardsColor: .pink)
static let emojisTheme = Theme(name: "Faces", emojis: ["ðŸĨ°", "😄", "😜", "ðŸĨģ", "ðŸĪ“", "😎", "😋", "ðŸĪĐ"], numberOfPairs: 8, cardsColor: .blue)
static let carsTheme = Theme(name: "Cars", emojis: ["🚓", "🏎ïļ", "🚗", "🚎", "🚒", "🚙", "🚑", "🚌"], numberOfPairs: 20, cardsColor: .yellow)
static let activitiesTheme = Theme(name: "Activities", emojis: ["ðŸĪš", "🏌ïļ", "🏄‍♂ïļ", "ðŸšĢ", "🏊‍♂ïļ", "🏋ïļ", "ðŸšī‍♂ïļ"], numberOfPairs: -10, cardsColor: .green)
static let fruitsTheme = Theme(name: "Fruits", emojis: ["🍇", "🍉", "🍈", "🍊", "🍋", "🍎", "🍏", "ðŸĨ­"], numberOfPairs: 5, cardsColor: .purple)
static var themes = Themes([numbersTheme, emojisTheme, carsTheme, fruitsTheme])
static func createMemoryGame() -> Game {
Game(theme: themes.currentTheme!)
}
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
func createNewGame() {
gameController.theme = GameViewModel.themes.changeCurrentTheme()
gameController.startTheme()
}
func choose(_ card: Game.Card) {
objectWillChange.send()
gameController.chooseCard(card)
}
var cards: [Game.Card] {
return gameController.cards
}
var title: String {
return gameController.theme.name
}
var color: Color {
return gameController.theme.cardsColor
}
}
And this is my view:
struct ContentView: View {
var columns: [GridItem] = [GridItem(.adaptive(minimum: 90, maximum: 400))]
#ObservedObject var ViewModel: GameViewModel
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
.padding(.trailing)
}
Section {
VStack {
Text(ViewModel.title)
.foregroundColor(/*#START_MENU_TOKEN#*/.blue/*#END_MENU_TOKEN#*/)
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
}
}
ScrollView {
LazyVGrid(columns: columns ) {
ForEach(ViewModel.cards, id: \.id) { card in
Card(card: card, color: ViewModel.color)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
ViewModel.choose(card)
}
}
}
.font(.largeTitle)
}
.padding()
Text("Score")
.frame(maxWidth: .infinity, minHeight: 30)
.background(Color.blue)
.foregroundColor(/*#START_MENU_TOKEN#*/.white/*#END_MENU_TOKEN#*/)
Spacer()
HStack {
Spacer()
Text("0")
.font(.title2)
.bold()
Spacer()
}
}
}
}
struct Card: View {
let card: Game.Card
let color: Color
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(color)
.foregroundColor(color)
}
else {
shape
.fill(color)
}
}
}
}
Basically the problem lies with the
.onTapGesture {
ViewModel.choose(card)
}
Of the View, when someone taps a card, the isFaceUp property of the Card is changed to true, but this doesn't get reflected in the UI.
If I generate a new view by changing the theme and adding new cards, this works.
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
But when I'm trying to flip a card it doesn't work, the value changes in the Game model but it's not updated on the view
After the tap the ViewModel calls the choose method
func choose(_ card: Game.Card) {
gameController.chooseCard(card)
}
And this changed the value of the Model in the Game.swift file by calling the chooseCard method
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
The values changes but the view does not, the gameController variable of the GameViewModel has the #Published state, which points to an instance of the Game model struct
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
And the view it's accesing this GameViewModel with the #ObservedObject property
#ObservedObject var ViewModel: GameViewModel
I thought I was doing everything right, but I guess not lol, what the heck am I doing wrong? Why can't update my view if I'm using published and observable object on my ViewModel? lol
The main reason the card view doesn't see changes is because in your card view you did put an equatable conformance protocol where you specify an equality check == function that just checks for content and not other variable changes
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
// && lhs.isFaceUp && rhs.isFaceUp //<- you can still add this
}
if you remove the equatable protocol and leave swift to check for equality it should be the minimal change from your base solution.
I would still use the solution where you change the state of the class card so the view can react to changes as an ObservableObject, and the #Published for changes that the view need to track, like this:
class Card: Identifiable, Equatable, ObservableObject {
var id: Int
#Published var isFaceUp: Bool = false
var content: String
#Published var isMatchedUp: Bool = false
var isPreviouslySeen = false
internal init(id: Int, content: String) {
self.id = id
self.content = content
}
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
}
}
and in the Card view the card variable will become
struct Card: View {
#ObservedObject var card: Game.Card
...
}
btw you don't need to notify the view of changes with
objectWillChange.send() if you are already using the #Published notation. every set to the variable will trigger an update.
you could try this instead of declaring Card a class:
Card(card: card, color: ViewModel.color, isFaceUp: card.isFaceUp)
and add this to the Card view:
let isFaceUp: Bool
My understanding is that the Card view does not see any changes to the card (not sure why, maybe because it is in an if),
but if you give it something that has really changed then it is re-rendered. And as mentioned before no need for objectWillChange.send()
EDIT1:
you could also do this in "ContentView":
Card(viewModel: ViewModel, card: card)
and then
struct Card: View {
#ObservedObject var viewModel: GameViewModel
let card: Game.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(viewModel.color)
.foregroundColor(viewModel.color)
}
else {
shape.fill(viewModel.color)
}
}
}
}

SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?

I was wondering if it is possible to use the View.onDrag and View.onDrop to add drag and drop reordering within one LazyGrid manually?
Though I was able to make every Item draggable using onDrag, I have no idea how to implement the dropping part.
Here is the code I was experimenting with:
import SwiftUI
//MARK: - Data
struct Data: Identifiable {
let id: Int
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [Data]
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
init() {
data = Array<Data>(repeating: Data(id: 0), count: 100)
for i in 0..<data.count {
data[i] = Data(id: i)
}
}
}
//MARK: - Grid
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
ItemView(d: d)
.id(d.id)
.frame(width: 160, height: 240)
.background(Color.green)
.onDrag { return NSItemProvider(object: String(d.id) as NSString) }
}
}
}
}
}
//MARK: - GridItem
struct ItemView: View {
var d: Data
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
}
}
Thank you!
SwiftUI 2.0
Here is completed simple demo of possible approach (did not tune it much, `cause code growing fast as for demo).
Important points are: a) reordering does not suppose waiting for drop, so should be tracked on the fly; b) to avoid dances with coordinates it is more simple to handle drop by grid item views; c) find what to where move and do this in data model, so SwiftUI animate views by itself.
Tested with Xcode 12b3 / iOS 14
import SwiftUI
import UniformTypeIdentifiers
struct GridData: Identifiable, Equatable {
let id: Int
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [GridData]
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
init() {
data = Array(repeating: GridData(id: 0), count: 100)
for i in 0..<data.count {
data[i] = GridData(id: i)
}
}
}
//MARK: - Grid
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: GridData?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
}
}.animation(.default, value: model.data)
}
}
}
struct DragRelocateDelegate: DropDelegate {
let item: GridData
#Binding var listData: [GridData]
#Binding var current: GridData?
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
}
}
//MARK: - GridItem
struct GridItemView: View {
var d: GridData
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
.frame(width: 160, height: 240)
.background(Color.green)
}
}
Edit
Here is how to fix the never disappearing drag item when dropped outside of any grid item:
struct DropOutsideDelegate: DropDelegate {
#Binding var current: GridData?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DemoDragRelocateView: View {
...
var body: some View {
ScrollView {
...
}
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}
Here's my solution (based on Asperi's answer) for those who seek for a generic approach for ForEach where I abstracted the view away:
struct ReorderableForEach<Content: View, Item: Identifiable & Equatable>: View {
let items: [Item]
let content: (Item) -> Content
let moveAction: (IndexSet, Int) -> Void
// A little hack that is needed in order to make view back opaque
// if the drag and drop hasn't ever changed the position
// Without this hack the item remains semi-transparent
#State private var hasChangedLocation: Bool = false
init(
items: [Item],
#ViewBuilder content: #escaping (Item) -> Content,
moveAction: #escaping (IndexSet, Int) -> Void
) {
self.items = items
self.content = content
self.moveAction = moveAction
}
#State private var draggingItem: Item?
var body: some View {
ForEach(items) { item in
content(item)
.overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
draggingItem = item
return NSItemProvider(object: "\(item.id)" as NSString)
}
.onDrop(
of: [UTType.text],
delegate: DragRelocateDelegate(
item: item,
listData: items,
current: $draggingItem,
hasChangedLocation: $hasChangedLocation
) { from, to in
withAnimation {
moveAction(from, to)
}
}
)
}
}
}
The DragRelocateDelegate basically stayed the same, although I made it a bit more generic and safer:
struct DragRelocateDelegate<Item: Equatable>: DropDelegate {
let item: Item
var listData: [Item]
#Binding var current: Item?
#Binding var hasChangedLocation: Bool
var moveAction: (IndexSet, Int) -> Void
func dropEntered(info: DropInfo) {
guard item != current, let current = current else { return }
guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return }
hasChangedLocation = true
if listData[to] != current {
moveAction(IndexSet(integer: from), to > from ? to + 1 : to)
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
current = nil
return true
}
}
And finally here is the actual usage:
ReorderableForEach(items: itemsArr) { item in
SomeFancyView(for: item)
} moveAction: { from, to in
itemsArr.move(fromOffsets: from, toOffset: to)
}
There was a few additional issues raised to the excellent solutions above, so here's what I could come up with on Jan 1st with a hangover (i.e. apologies for being less than eloquent):
If you pick a griditem and release it (to cancel), then the view is not reset
I added a bool that checks if the view had been dragged yet, and if it hasn't then it doesn't hide the view in the first place. It's a bit of a hack, because it doesn't really reset, it just postpones hiding the view until it knows that you want to drag it. I.e. if you drag really fast, you can see the view briefly before it's hidden.
If you drop a griditem outside the view, then the view is not reset
This one was partially addressed already, by adding the dropOutside delegate, but SwiftUI doesn't trigger it unless you have a background view (like a color), which I think caused some confusion. I therefore added a background in grey to illustrate how to properly trigger it.
Hope this helps anyone:
import SwiftUI
import UniformTypeIdentifiers
struct GridData: Identifiable, Equatable {
let id: String
}
//MARK: - Model
class Model: ObservableObject {
#Published var data: [GridData]
let columns = [
GridItem(.flexible(minimum: 60, maximum: 60))
]
init() {
data = Array(repeating: GridData(id: "0"), count: 50)
for i in 0..<data.count {
data[i] = GridData(id: String("\(i)"))
}
}
}
//MARK: - Grid
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: GridData? // I can't reset this when user drops view ins ame location as drag started
#State private var changedView: Bool = false
var body: some View {
VStack {
ScrollView(.vertical) {
LazyVGrid(columns: model.columns, spacing: 5) {
ForEach(model.data) { d in
GridItemView(d: d)
.opacity(dragging?.id == d.id && changedView ? 0 : 1)
.onDrag {
self.dragging = d
changedView = false
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, changedView: $changedView))
}
}.animation(.default, value: model.data)
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
.background(Color.gray.edgesIgnoringSafeArea(.all))
.onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging, changedView: $changedView))
}
}
struct DragRelocateDelegate: DropDelegate {
let item: GridData
#Binding var listData: [GridData]
#Binding var current: GridData?
#Binding var changedView: Bool
func dropEntered(info: DropInfo) {
if current == nil { current = item }
changedView = true
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 {
changedView = false
self.current = nil
return true
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: GridData?
#Binding var changedView: Bool
func dropEntered(info: DropInfo) {
changedView = true
}
func performDrop(info: DropInfo) -> Bool {
changedView = false
current = nil
return true
}
}
//MARK: - GridItem
struct GridItemView: View {
var d: GridData
var body: some View {
VStack {
Text(String(d.id))
.font(.headline)
.foregroundColor(.white)
}
.frame(width: 60, height: 60)
.background(Circle().fill(Color.green))
}
}
Goal: Reordering Items in HStack
I was trying to figure out how to leverage this solution in SwiftUI for macOS when dragging icons to re-order a horizontal set of items. Thanks to #ramzesenok and #Asperi for the overall solution. I added a CGPoint property along with their solution to achieve the desired behavior. See the animation below.
Define the point
#State private var drugItemLocation: CGPoint?
I used in dropEntered, dropExited, and performDrop DropDelegate functions.
func dropEntered(info: DropInfo) {
if current == nil {
current = item
drugItemLocation = info.location
}
guard item != current,
let current = current,
let from = icons.firstIndex(of: current),
let toIndex = icons.firstIndex(of: item) else { return }
hasChangedLocation = true
drugItemLocation = info.location
if icons[toIndex] != current {
icons.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
}
}
func dropExited(info: DropInfo) {
drugItemLocation = nil
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
drugItemLocation = nil
current = nil
return true
}
For a full demo, I created a gist using Playgrounds
Here is how you implement the on drop part. But remember the ondrop can allow content to be dropped in from outside the app if the data conforms to the UTType. More on UTTypes.
Add the onDrop instance to your lazyVGrid.
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
ItemView(d: d)
.id(d.id)
.frame(width: 160, height: 240)
.background(Color.green)
.onDrag { return NSItemProvider(object: String(d.id) as NSString) }
}
}.onDrop(of: ["public.plain-text"], delegate: CardsDropDelegate(listData: $model.data))
Create a DropDelegate to handling dropped content and the drop location with the given view.
struct CardsDropDelegate: DropDelegate {
#Binding var listData: [MyData]
func performDrop(info: DropInfo) -> Bool {
// check if data conforms to UTType
guard info.hasItemsConforming(to: ["public.plain-text"]) else {
return false
}
let items = info.itemProviders(for: ["public.plain-text"])
for item in items {
_ = item.loadObject(ofClass: String.self) { data, _ in
// idea is to reindex data with dropped view
let index = Int(data!)
DispatchQueue.main.async {
// id of dropped view
print("View Id dropped \(index)")
}
}
}
return true
}
}
Also the only real useful parameter of performDrop is info.location a CGPoint of the drop location, Mapping a CGPoint to the view you want to replace seems unreasonable. I would think the OnMove would be a better option and would make moving your data/Views a breeze. I was unsuccessful to get OnMove working within a LazyVGrid.
As LazyVGrid are still in beta and are bound to change. I would abstain from use on more complex tasks.
I came with a bit different approach that works fine on macOS. Instead of using .onDrag and .onDrop Im using .gesture(DragGesture) with a helper class and modifiers.
Here are helper objects (just copy this to the new file):
// Helper class for dragging objects inside LazyVGrid.
// Grid items must be of the same size
final class DraggingManager<Entry: Identifiable>: ObservableObject {
let coordinateSpaceID = UUID()
private var gridDimensions: CGRect = .zero
private var numberOfColumns = 0
private var numberOfRows = 0
private var framesOfEntries = [Int: CGRect]() // Positions of entries views in coordinate space
func setFrameOfEntry(at entryIndex: Int, frame: CGRect) {
guard draggedEntry == nil else { return }
framesOfEntries[entryIndex] = frame
}
var initialEntries: [Entry] = [] {
didSet {
entries = initialEntries
calculateGridDimensions()
}
}
#Published // Currently displayed (while dragging)
var entries: [Entry]?
var draggedEntry: Entry? { // Detected when dragging starts
didSet { draggedEntryInitialIndex = initialEntries.firstIndex(where: { $0.id == draggedEntry?.id }) }
}
var draggedEntryInitialIndex: Int?
var draggedToIndex: Int? { // Last index where device was dragged to
didSet {
guard let draggedToIndex, let draggedEntryInitialIndex, let draggedEntry else { return }
var newArray = initialEntries
newArray.remove(at: draggedEntryInitialIndex)
newArray.insert(draggedEntry, at: draggedToIndex)
withAnimation {
entries = newArray
}
}
}
func indexForPoint(_ point: CGPoint) -> Int {
let x = max(0, min(Int((point.x - gridDimensions.origin.x) / gridDimensions.size.width), numberOfColumns - 1))
let y = max(0, min(Int((point.y - gridDimensions.origin.y) / gridDimensions.size.height), numberOfRows - 1))
return max(0, min(y * numberOfColumns + x, initialEntries.count - 1))
}
private func calculateGridDimensions() {
let allFrames = framesOfEntries.values
let rows = Dictionary(grouping: allFrames) { frame in
frame.origin.y
}
numberOfRows = rows.count
numberOfColumns = rows.values.map(\.count).max() ?? 0
let minX = allFrames.map(\.minX).min() ?? 0
let maxX = allFrames.map(\.maxX).max() ?? 0
let minY = allFrames.map(\.minY).min() ?? 0
let maxY = allFrames.map(\.maxY).max() ?? 0
let width = (maxX - minX) / CGFloat(numberOfColumns)
let height = (maxY - minY) / CGFloat(numberOfRows)
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: width, height: height)
gridDimensions = CGRect(origin: origin, size: size)
}
}
struct Draggable<Entry: Identifiable>: ViewModifier {
#Binding
var originalEntries: [Entry]
let draggingManager: DraggingManager<Entry>
let entry: Entry
#ViewBuilder
func body(content: Content) -> some View {
if let entryIndex = originalEntries.firstIndex(where: { $0.id == entry.id }) {
let isBeingDragged = entryIndex == draggingManager.draggedEntryInitialIndex
let scale: CGFloat = isBeingDragged ? 1.1 : 1.0
content.background(
GeometryReader { geometry -> Color in
draggingManager.setFrameOfEntry(at: entryIndex, frame: geometry.frame(in: .named(draggingManager.coordinateSpaceID)))
return .clear
}
)
.scaleEffect(x: scale, y: scale)
.gesture(
dragGesture(
draggingManager: draggingManager,
entry: entry,
originalEntries: $originalEntries
)
)
}
else {
content
}
}
func dragGesture<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some Gesture {
DragGesture(coordinateSpace: .named(draggingManager.coordinateSpaceID))
.onChanged { value in
// Detect start of dragging
if draggingManager.draggedEntry?.id != entry.id {
withAnimation {
draggingManager.initialEntries = originalEntries.wrappedValue
draggingManager.draggedEntry = entry
}
}
let point = draggingManager.indexForPoint(value.location)
if point != draggingManager.draggedToIndex {
draggingManager.draggedToIndex = point
}
}
.onEnded { value in
withAnimation {
originalEntries.wrappedValue = draggingManager.entries!
draggingManager.entries = nil
draggingManager.draggedEntry = nil
draggingManager.draggedToIndex = nil
}
}
}
}
extension View {
// Allows item in LazyVGrid to be dragged between other items.
func draggable<Entry: Identifiable>(draggingManager: DraggingManager<Entry>, entry: Entry, originalEntries: Binding<[Entry]>) -> some View {
self.modifier(Draggable(originalEntries: originalEntries, draggingManager: draggingManager, entry: entry))
}
}
Now to use it in view you have to do few things:
Create a draggingManager that is a StateObject
Create a var that exposes either real array you are using or temporary array used by draggingManager during dragging.
Apply coordinateSpace from draggingManager to the container (LazyVGrid)
That way draggingManager only modifies its copy of the array during the process, and you can update the original after dragging is done.
struct VirtualMachineSettingsDevicesView: View {
#ObservedObject
var vmEntity: VMEntity
#StateObject
private var devicesDraggingManager = DraggingManager<VMDeviceInfo>()
// Currently displaying devices - different during dragging.
private var displayedDevices: [VMDeviceInfo] { devicesDraggingManager.entries ?? vmEntity.config.devices }
var body: some View {
Section("Devices") {
LazyVGrid(columns: [.init(.adaptive(minimum: 64, maximum: 64))], alignment: .leading, spacing: 20) {
Group {
ForEach(displayedDevices) { device in
Button(action: { configureDevice = device }) {
device.label
.draggable(
draggingManager: devicesDraggingManager,
entry: device,
originalEntries: $vmEntity.config.devices
)
}
}
Button(action: { configureNewDevice = true }, label: { Label("Add device", systemImage: "plus") })
}
.labelStyle(IconLabelStyle())
}
.coordinateSpace(name: devicesDraggingManager.coordinateSpaceID)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.buttonStyle(.plain)
}
}

How to reset child view state variable with SwiftUI?

I'm sure it's something very silly but how should one reset the state value of a child view when another state has changed?
For example, the code below shows 2 folders, which respectively have 2 and 3 items., which can be edited.
If you select the second folder (Work) and its 3rd item (Peter) and then select the first folder (Home), the app crashes since selectedItemIndex is out of bounds.
I tried to "reset" the state value when the view gets initialized but it seems like changing the state like such triggers out a "runtime: SwiftUI: Modifying state during view update, this will cause undefined behavior." warning.
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
What is the proper way to do this? Thanks!
Here's the code:
AppDelegate.swift
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let store = ItemStore()
let contentView = ContentView(store: store)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
ContentView.swift
import SwiftUI
final class ItemStore: ObservableObject {
#Published var data: [Folder] = [Folder(name: "Home",
items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work",
items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
static func == (lhs: Item, rhs: Item) -> Bool {
return true
}
var id = UUID()
var name: String
var content = Date().description
init(name: String) {
self.name = name
}
}
struct ContentView: View {
#ObservedObject var store: ItemStore
#State var selectedFolderIndex: Int?
var body: some View {
HSplitView {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
Text(folder.name).tag(index)
}
}.collapsible(false)
}
.listStyle(SidebarListStyle())
// ITEMS
if selectedFolderIndex != nil {
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.frame(minWidth: 800, maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedItemIndex: Int?
var body: some View {
HSplitView {
List(selection: $selectedItemIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
}
}
.frame(width: 300)
if selectedItemIndex != nil {
DetailView(item: $items[selectedItemIndex!])
.padding()
.frame(minWidth: 200, maxHeight: .infinity)
}
}
}
init(items: Binding<[Item]>) {
self._items = items
self._selectedItemIndex = State(wrappedValue: 0)
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Thanks to #jordanpittman for suggesting a fix:
ItemsView(items: $store.data[selectedFolderIndex!].items).id(selectedRowIndex)
Source: https://swiftui-lab.com/swiftui-id
Fully playable sample draft for ContentView.swift. Play with it in both edit modes (inactive/active row selection) and adopt to your needs.
import SwiftUI
struct ItemStore {
var data: [Folder] = [Folder(name: "Home", items: [Item(name: "Mark"), Item(name: "Vincent")]),
Folder(name: "Work", items:[Item(name: "Joseph"), Item(name: "Phil"), Item(name: "Peter")])]
}
struct Folder: Identifiable {
var id = UUID()
var name: String
var items: [Item]
}
struct Item: Identifiable {
var id = UUID()
var name: String
var content = Date().description
}
struct ContentView: View {
#State var store: ItemStore
#State var selectedFolderIndex: Int? = 0
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
VStack {
// FOLDERS
List(selection: $selectedFolderIndex) {
Section(header: Text("Groups")) {
ForEach(store.data.indexed(), id: \.1.id) { index, folder in
HStack {
Text(folder.name).tag(index)
Spacer()
}
.background(Color.white) //make the whole row tapable, not just the text
.frame(maxWidth: .infinity)
.multilineTextAlignment(.leading)
.onTapGesture {
self.selectedFolderIndex = index
}
}.onDelete(perform: delete)
}
}
.listStyle(GroupedListStyle())
.id(selectedFolderIndex)
// ITEMS
if selectedFolderIndex != nil && (($store.data.wrappedValue.startIndex..<$store.data.wrappedValue.endIndex).contains(selectedFolderIndex!) ){
ItemsView(items: $store.data[selectedFolderIndex!].items)
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing: EditButton())
.environment(\.editMode, $editMode)
}
}
func delete(at offsets: IndexSet) {
$store.wrappedValue.data.remove(atOffsets: offsets) // Note projected value! `store.data.remove() will not modify SwiftUI on changes and it will crash because of invalid index.
}
}
struct ItemsView: View {
#Binding var items: [Item]
#State var selectedDetailIndex: Int?
var body: some View {
HStack {
List(selection: $selectedDetailIndex) {
ForEach(items.indexed(), id: \.1.id) { index, item in
Text(item.name).tag(index)
.onTapGesture {
self.selectedDetailIndex = index
}
}
}
if selectedDetailIndex != nil && (($items.wrappedValue.startIndex..<$items.wrappedValue.endIndex).contains(selectedDetailIndex!) ) {
DetailView(item: $items[selectedDetailIndex!])
.padding()
}
}
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
VStack {
TextField("", text: $item.name)
}
}
}
// Credit: https://swiftwithmajid.com/2019/07/03/managing-data-flow-in-swiftui/
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(store: ItemStore())
}
}

Deleting data from an array according to toggle button clicked in swiftui

I have an array of some integer that represents
some Toggle button. I want to delete the integer from the array according to the toggle button switched off!
struct CleanData: View {
var numberArray = [101, 102, 109, 143]
#State var numToDelete: [Bool] = [true, true, true, true]
var body: some View {
ForEach(0 ..< numberArray.count) {
if !self.numToDelete[$0] {
numberArray.remove(at: $0)
//to save modified array permanently
}
}
return ForEach(0 ..< numberArray.count) {
Toggle("Days: \(self.numberArray[$0])", isOn: self.$numToDelete[$0])
.padding()
}
}
}
Or trying differently but no luck!
struct CleanData: View {
var numberArray = [101, 102, 109, 143]
var body: some View {
return ForEach(0 ..< numberArray.count) { num in
RowView(mID: num, numArray: self.numberArray)
}
}
}
struct RowView: View {
#State var isShow = true
var mID: Int
var numArray: [Int]
var body: some View {
if(!isShow) {
numArray.remove(at: mID)
//to save modified array permanently
}
return Toggle("Days: \(self.numArray[mID])", isOn: self.$isShow)
.padding()
}
}
Here is possible approach
struct CleanData: View {
#State private var numberArray = [101, 102, 109, 143]
#State private var numToDelete = [true, true, true, true]
var body: some View {
VStack {
ForEach(Array(numberArray.enumerated()), id: \.element.self) { (i, number) -> AnyView in
let toDelete = Binding<Bool>(get: { self.numToDelete[i] },
set: {
self.numToDelete[i] = $0
// delayed to give time for toggle animation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.numberArray.remove(at: i)
self.numToDelete.remove(at: i)
}
}
)
return AnyView(Toggle("Days: \(number)", isOn: toDelete))
}
}.padding()
}
}

Resources