-
-
Save janodev/7e7806b8544e280c6adc820656e60595 to your computer and use it in GitHub Desktop.
Reproduction of a bug with Observable + ForEach + Equatable + child views.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Observation | |
@Observable | |
final class Folder: Equatable { | |
var name: String | |
var isExpanded: Bool | |
init(name: String, isExpanded: Bool) { | |
self.name = name | |
self.isExpanded = isExpanded | |
} | |
func copy(name: String? = nil, isExpanded: Bool? = nil) -> Folder { | |
Folder( | |
name: name ?? self.name, | |
isExpanded: isExpanded ?? self.isExpanded | |
) | |
} | |
static func == (lhs: Folder, rhs: Folder) -> Bool { | |
lhs.name == rhs.name && lhs.isExpanded == rhs.isExpanded | |
} | |
} | |
@Observable | |
final class Category: Equatable { | |
var name: String | |
var folders: [Folder] | |
init(name: String, folders: [Folder]) { | |
self.name = name | |
self.folders = folders | |
} | |
func copy(name: String? = nil, folders: [Folder]? = nil) -> Category { | |
Category( | |
name: name ?? self.name, | |
folders: folders ?? self.folders | |
) | |
} | |
static func == (lhs: Category, rhs: Category) -> Bool { | |
// Uncommenting this line also fixes the issue | |
// ObjectIdentifier(lhs) == ObjectIdentifier(rhs) && | |
lhs.name == rhs.name && | |
lhs.folders == rhs.folders | |
} | |
} | |
@Observable | |
final class Library { | |
var categories: [Category] = [ | |
Category(name: "Work", folders: [ | |
Folder(name: "Meetings", isExpanded: false) | |
]) | |
] | |
/* | |
Given a structure: Library --♢ Categories --♢ Folder, | |
the following three change the 'isExpanded' property in different ways: | |
- folder.isExpanded.toggle() | |
- folder.copy(isExpanded:) | |
- category.copy(folders:) <-- this one causes the other to stop working | |
*/ | |
func toggleFolderExpanded(categoryIndex: Int, folderIndex: Int) { | |
categories[categoryIndex].folders[folderIndex].isExpanded.toggle() | |
print("Direct: toggled isExpanded at [\(categoryIndex)][\(folderIndex)]: \(categories[categoryIndex].folders[folderIndex].isExpanded)") | |
} | |
func toggleFolderWithCopy(categoryIndex: Int, folderIndex: Int) { | |
let oldFolder = categories[categoryIndex].folders[folderIndex] | |
let newFolder = oldFolder.copy(isExpanded: !oldFolder.isExpanded) | |
categories[categoryIndex].folders[folderIndex] = newFolder | |
// Uncommenting this line fixes the issue: | |
// categories[categoryIndex] = categories[categoryIndex] | |
print("Copy Folder: toggled isExpanded at [\(categoryIndex)][\(folderIndex)]: \(categories[categoryIndex].folders[folderIndex].isExpanded)") | |
} | |
func toggleFolderWithCategoryReplacement(categoryIndex: Int, folderIndex: Int) { | |
let foldersCopy = categories[categoryIndex].folders | |
foldersCopy[folderIndex].isExpanded.toggle() | |
let newCategory = categories[categoryIndex].copy(folders: foldersCopy) | |
categories[categoryIndex] = newCategory | |
print("Copy Category: toggled isExpanded at [\(categoryIndex)][\(folderIndex)]: \(categories[categoryIndex].folders[folderIndex].isExpanded)") | |
} | |
} | |
struct CategoryView: View { | |
let category: Category | |
var body: some View { | |
ForEach(category.folders, id: \.name) { folder in | |
Circle() | |
.frame(width: 44) | |
.foregroundColor(folder.isExpanded ? .green : .yellow) | |
} | |
} | |
} | |
struct NestedBugTestView: View { | |
@State private var library = Library() | |
var body: some View { | |
VStack(spacing: 20) { | |
ForEach(library.categories, id: \.name) { category in | |
CategoryView(category: category) | |
} | |
HStack { | |
Button("Direct") { | |
library.toggleFolderExpanded(categoryIndex: 0, folderIndex: 0) | |
} | |
Button("Copy Folder") { | |
library.toggleFolderWithCopy(categoryIndex: 0, folderIndex: 0) | |
} | |
.tint(.green) | |
Button("Copy Category") { | |
library.toggleFolderWithCategoryReplacement(categoryIndex: 0, folderIndex: 0) | |
} | |
.tint(.orange) | |
} | |
.buttonStyle(.borderedProminent) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment