Xcode error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions - swift

When using Xcode 13.2.1 and SwiftUI to implement a simple slideshow, I am hitting a compile-time error in which Xcode takes about 5 minutes on my M1 to decide it cannot parse my code, and eventually gives me the error:
The compiler is unable to type-check this expression in reasonable
time; try breaking up the expression into distinct sub-expressions
I narrowed it down to the NavigationLink line near the bottom. If I comment that out, it compiles quickly with only a warning.
The following is my Minimal, Reproducible Example:
import SwiftUI
import Foundation
enum MarkerType: Double {
case unlabeled = -99
case end = -4
case start = -3
case stop = -2
case blank = -1
case image = 1
}
class LabeledImage {
let image: Image
let marker: Double
var appeared = false
init(image: Image, marker: Double) {
self.image = image
self.marker = marker
}
}
struct SlideShow {
private let maxImages: Int = 10000
var images = [LabeledImage]()
var labels = [String]()
var totalImages: Int { return self.images.count }
private var fromFolder: URL
init(fromURL: URL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/DefaultImages")) {
self.fromFolder = fromURL
}
}
class AppState: ObservableObject {
static var docDir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
#Published var isMainMenuActive = false
#Published var loadFolder: URL = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/DefaultImages")
#Published var intervalSeconds: Double = 0.6
var saveFolder = URL(fileURLWithPath: "BCILab", relativeTo: docDir)
var labels = [String]()
var totalImages: Int = 0
var saveIndex: Int = 0
}
struct minsample: View {
#StateObject private var appState = AppState()
#State private var slideshow = SlideShow()
#State private var selection: Int = 0
private func insertAppears(_ marker: Double) {
let nmarker = marker + 100.0
}
var body: some View {
NavigationView {
ForEach(0..<slideshow.images.count-1, id: \.self) { i in
let thisImage = slideshow.images[i].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[i].marker) })
let nextImage = slideshow.images[i+1].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[i+1].marker) })
NavigationLink(destination: nextImage, tag: i, selection: self.$selection) { thisImage }
}
}
}
}

Generally, using an index-based solution in ForEach is a bad idea. It breaks SwiftUI's system for diffing the views and tends to lead to compile-time issues as well.
I'd start by making LabeledImage Identifiable:
class LabeledImage : Identifiable {
var id = UUID()
let image: Image
let marker: Double
var appeared = false
init(image: Image, marker: Double) {
self.image = image
self.marker = marker
}
}
(I'd also make it a struct -- more on that later)
Then, since you do need indexes to achieve your nextImage functionality, you can use .enumerated on the collection:
struct MinSample: View {
#StateObject private var appState = AppState()
#State private var slideshow = SlideShow()
#State private var selection: Int? = 0
private func insertAppears(_ marker: Double) {
let nmarker = marker + 100.0
}
var body: some View {
NavigationView {
ForEach(Array(slideshow.images.enumerated()), id: \.1.id) { (index,imageModel) in
if index < slideshow.images.count - 1 {
let thisImage = imageModel.image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(imageModel.marker) })
let nextImage = slideshow.images[index + 1].image
.resizable()
.aspectRatio(contentMode: .fit)
.onAppear(perform: { insertAppears(slideshow.images[index+1].marker) })
NavigationLink(destination: nextImage, tag: index, selection: self.$selection) { thisImage }
} else {
EmptyView()
}
}
}
}
}
The above compiles quickly on my M1 with no issues.
Now, not directly-related to your issue, but there are some other things I would change:
Make your models structs, which SwiftUI generally deals with much better when doing state comparisons
Don't store references to SwiftUI Images -- instead, store a reference to a path or some other way to recreate that image. That will make the transition for LabeledImage to a struct easier anyway. So, your model might look like this:
struct LabeledImage : Identifiable {
var id = UUID()
let imageName: String
let marker: Double
var appeared = false
}
Consider whether or not you need the tag and selection parameters in your NavigationLink -- perhaps it's just not clear in the minimal example why they're used.

Related

Dynamically sized #State var

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}

Can't get an array of objects to update the view

I have a fairly simple piece of code that I am trying to write containing a displayed array and a draggable object inside that array. The problem though is that it compiles and displays the array, but does not update the view when I tap and drag the object, despite the terminal showing that the tOffset variable is getting constantly updated.
I am 90% sure I am just not getting the property wrapper right, maybe #ObservedObject doesn't work for arrays? And feedback is appreciated
.
Here is my class declaration:
class Token: Identifiable, ObservableObject{
var id: UUID
var name: String
var scale: CGFloat
#Published var sOffset: CGSize
#Published var cOffset: CGSize
#Published var tOffset: CGSize
init(n: String) {
id = UUID()
name = n
scale = 2.0
sOffset = .zero
cOffset = .zero
tOffset = .zero
updateTOffset()
}
public func updateTOffset() {
tOffset.height = sOffset.height + cOffset.height
tOffset.width = sOffset.width + cOffset.width
print("Updated T Offset , x: \(tOffset.width), y:\(tOffset.height) ")
}
}
class varToken: Token {
var exp: expToken?
init(name: String, exp: expToken?) {
super.init(n: name)
self.exp = exp
}
}
class expToken: Token {
#Published var bOffset: CGSize = .zero
init(name: String) {
super.init(n: name)
sOffset.height = -10
scale = 1.0
updateTOffset()
}
}
class tokenArray: Identifiable, ObservableObject {
var id: UUID = UUID()
#Published var tokens: [varToken] = [varToken(name: "1 +", exp: nil), varToken(name: "x", exp: expToken(name: "2"))]
}
I created it intending for it to conform to Observable Object. The view is very simple, with all elements in one view struct:
struct ViewDemo2: View {
#ObservedObject var t: tokenArray = tokenArray()
var body: some View {
HStack(alignment: .bottom) {
ForEach(t.tokens) { token in
Text(token.name)
.scaleEffect(token.scale, anchor: .center)
.offset(token.tOffset)
.padding(.leading, 10)
if token.exp != nil {
Text(token.exp!.name)
.scaleEffect(token.exp!.scale)
.offset(token.exp!.tOffset)
.gesture(
DragGesture()
.onChanged() { value in
withAnimation(.spring()) {
token.exp!.cOffset = value.translation
token.exp!.updateTOffset()
}
}
.onEnded() { value in
withAnimation(.spring()) {
token.exp!.cOffset = .zero
token.exp!.updateTOffset()
}
}
)
}
}
}
}
}

I create an instance of 3 different view models and assign each to a state object. How do I place this into a dispatch queue?

I have a Swift program that works as desired. I have 3 view models that each call a separate model. Each model calls a function that reads a separate large CSV file, performs some manipulation and returns a data frame. This takes some time and I would like to speed things up.
Swift offers a DispatchQueue that allows one to place code into an asynchronous global queue with QOS and I believe if I ran the creation of the view models in this fashion, I would display the initial view sooner.
The problem is: I have no idea how to incorporate it. Any help to point me in the right direction will be appreciated.
Below is my content view, one view model, and one model. The test dispatch queue code at the end runs successfully in a playground.
struct ContentView: View {
#StateObject var vooVM: VOOViewModel = VOOViewModel()
#StateObject var vfiaxVM: VFIAXViewModel = VFIAXViewModel()
#StateObject var principalVM: PrincipalViewModel = PrincipalViewModel()
#State private var selectedItemId: Int?
var body: some View {
NavigationView {
VStack (alignment: .leading) {
List {
Spacer()
.frame(height: 20)
Group {
Divider()
NavigationLink(destination: Summary(vooVM: vooVM, vfiaxVM: vfiaxVM, prinVM: principalVM), tag: 1, selection: $selectedItemId, label: {
HStack {
Image(systemName: "house")
.padding(.leading, 10)
.padding(.trailing, 0)
.padding(.bottom, 5)
Text("Summary")
.bold()
.padding(.bottom, 2)
} // end h stack
})
} // end group
NavigationLinks(listText: "VOO", dataFrame1: vooVM.timeSeriesDailyDF1, dataFrame5: vooVM.timeSeriesDailyDF5)
NavigationLinks(listText: "VFIAX", dataFrame1: vfiaxVM.timeSeriesDailyDF1, dataFrame5: vfiaxVM.timeSeriesDailyDF5)
NavigationLinks(listText: "Principal", dataFrame1: principalVM.timeSeriesDailyDF1, dataFrame5: principalVM.timeSeriesDailyDF5)
Divider()
Spacer()
} // end list
} // end v stack
} // end navigation view
.onAppear {self.selectedItemId = 1}
.navigationTitle("Stock Data")
.frame(width: 1200, height: 900, alignment: .center)
} // end body view
} // end content view
View Model
class VOOViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
var timeSeriesDailyDF1: DataFrame {
return vooModel.vooDF.0
}
var timeSeriesDailyDF5: DataFrame {
return vooModel.vooDF.1
}
var symbol: String {
return vooModel.symbol
}
var currentShares: Double {
return vooModel.currentShares
}
var currentSharePrice: Double {
let lastRowIndex: Int = vooModel.vooDF.0.shape.rows - 1
let currentPrice: Double = (vooModel.vooDF.0[row: lastRowIndex])[1] as! Double
return currentPrice
}
var percentGain: Double {
let pastValue: Double = (vooModel.vooDF.0[row: 0])[1] as! Double
let numRows: Int = vooModel.vooDF.0.shape.rows - 1
let curValue: Double = (vooModel.vooDF.0[row: numRows])[1] as! Double
let oneYearGain: Double = (100 * (curValue - pastValue)) / pastValue
return oneYearGain
}
}
Model
struct VOOModel {
var vooDF = GetDF(fileName: "FormattedVOO")
let symbol: String = "VOO"
let currentShares: Double = 1
}
Playground Code
let myQue = DispatchQueue.global()
let myGroup = DispatchGroup()
myQue.async(group: myGroup) {
sleep(5)
print("Task 1 complete")
}
myQue.async(group: myGroup) {
sleep(3)
print("Task 2 complete")
}
myGroup.wait()
print("All tasks completed")
I was able to solve my problem by using only 1 viewmodel instead of 3. The viewmodel calls all three models which were modified such that their function call to read a CSV file and place it into a dataframe is contained in a function. This function is in turn called within a function in the viewmodel which is called in the viewmodels init. Below is the updated code. Note that the ContentView was simplified to make testing easy.
New Content View:
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
let printValue1 = (viewModel.dataFrames.0.0[row: 0])[0]
let tempValue = (viewModel.dataFrames.0.0[row: 0])[1] as! Double
let tempValueFormatted: String = String(format: "$%.2f", tempValue)
Text("\(dateToStringFormatter.string(from: printValue1 as! Date))" + " " + tempValueFormatted )
.frame(width: 1200, height: 900, alignment: .center)
}
}
New ViewModel:
class ViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
#Published private var vfiaxModel: VFIAXModel = VFIAXModel()
#Published private var principalModel: PrincipalModel = PrincipalModel()
var dataFrames = ((DataFrame(), DataFrame()), (DataFrame(), DataFrame()), (DataFrame(), DataFrame()))
init() {
self.dataFrames = GetDataFrames()
}
func GetDataFrames() -> ((DataFrame, DataFrame), (DataFrame, DataFrame), (DataFrame, DataFrame)) {
let myQue: DispatchQueue = DispatchQueue.global()
let myGroup: DispatchGroup = DispatchGroup()
var vooDF = (DataFrame(), DataFrame())
var vfiaxDF = (DataFrame(), DataFrame())
var principalDF = (DataFrame(), DataFrame())
myQue.async(group: myGroup) {
vfiaxDF = self.vfiaxModel.GetData()
}
myQue.async(group: myGroup) {
principalDF = self.principalModel.GetData()
}
myQue.async(group: myGroup) {
vooDF = self.vooModel.GetData()
}
myGroup.wait()
return (vooDF, vfiaxDF, principalDF)
}
}
One of the new models. The other 2 are identical except for the CSV file they read.
struct VOOModel {
let symbol: String = "VOO"
let currentShares: Double = 1
func GetData() -> (DataFrame, DataFrame) {
let vooDF = GetDF(fileName: "FormattedVOO")
return vooDF
}
}

SwiftUI - using variable count within ForEach

I have a view created through a ForEach loop which needs to take a variable count within the ForEach itself i.e. I need the app to react to a dynamic count and change the UI accoridngly.
Here is the view I am trying to modify:
struct AnimatedTabSelector: View {
let buttonDimensions: CGFloat
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
HStack {
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
ForEach(1..<tabBarViewModel.activeFormIndex + 1) { _ in
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.green)
}
Circle().frame(
width: buttonDimensions,
height: buttonDimensions)
.foregroundColor(
tabBarViewModel.activeForm.loginFormViewModel.colorScheme
)
ForEach(1..<tabBarViewModel.loginForms.count - tabBarViewModel.activeFormIndex) { _ in
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
}
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.gray)
}
}
}
And the viewModel I am observing:
class TabBarViewModel: ObservableObject, TabBarCompatible {
var loginForms: [LoginForm]
#Published var activeForm: LoginForm
#Published var activeFormIndex = 0
private var cancellables = Set<AnyCancellable>()
init(loginForms: [LoginForm]) {
self.loginForms = loginForms
self.activeForm = loginForms[0] /// First form is always active to begin
setUpPublisher()
}
func setUpPublisher() {
for i in 0..<loginForms.count {
loginForms[i].loginFormViewModel.$isActive.sink { isActive in
if isActive {
self.activeForm = self.loginForms[i]
self.activeFormIndex = i
}
}
.store(in: &cancellables)
}
}
}
And finally the loginFormViewModel:
class LoginFormViewModel: ObservableObject {
#Published var isActive: Bool
let name: String
let icon: Image
let colorScheme: Color
init(isActive: Bool = false, name: String, icon: Image, colorScheme: Color) {
self.isActive = isActive
self.name = name
self.icon = icon
self.colorScheme = colorScheme
}
}
Basically, a button on the login form itself sets its viewModel's isActive property to true. We listen for this in TabBarViewModel and set the activeFormIndex accordingly. This index is then used in the ForEach loop. Essentially, depending on the index selected, I need to generate more or less spacers in the AnimatedTabSelector view.
However, whilst the activeIndex variable is being correctly updated, the ForEach does not seem to react.
Update:
The AnimatedTabSelector is declared as part of this overall view:
struct TabIconsView: View {
struct Constants {
static let buttonDimensions: CGFloat = 50
static let buttonIconSize: CGFloat = 25
static let activeButtonColot = Color.white
static let disabledButtonColor = Color.init(white: 0.8)
struct Animation {
static let stiffness: CGFloat = 330
static let damping: CGFloat = 22
static let velocity: CGFloat = 7
}
}
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
ZStack {
AnimatedTabSelector(
buttonDimensions: Constants.buttonDimensions,
tabBarViewModel: tabBarViewModel)
HStack {
Spacer()
ForEach(tabBarViewModel.loginForms) { loginForm in
Button(action: {
loginForm.loginFormViewModel.isActive = true
}) {
loginForm.loginFormViewModel.icon
.font(.system(size: Constants.buttonIconSize))
.foregroundColor(
tabBarViewModel.activeForm.id == loginForm.id ? Constants.activeButtonColot : Constants.disabledButtonColor
)
}
.frame(width: Constants.buttonDimensions, height: Constants.buttonDimensions)
Spacer()
}
}
}
.animation(Animation.interpolatingSpring(
stiffness: Constants.Animation.stiffness,
damping: Constants.Animation.damping,
initialVelocity: Constants.Animation.velocity)
)
}
}
UPDATE:
I tried another way by adding another published to the AnimatedTabSelector itself to check that values are indeed being updated accordingly. So at the end of the HStack in this view I added:
.onAppear {
tabBarViewModel.$activeFormIndex.sink { index in
self.preCircleSpacers = index + 1
self.postCircleSpacers = tabBarViewModel.loginForms.count - index
}
.store(in: &cancellables)
}
And of course I added the following variables to this view:
#State var preCircleSpacers = 1
#State var postCircleSpacers = 6
#State var cancellables = Set<AnyCancellable>()
Then in the ForEach loops I changed to:
ForEach(1..<preCircleSpacers)
and
ForEach(1..<postCircleSpacers)
respectively.
I added a break point in the new publisher declaration and it is indeed being updated with the expected figures. But the view is still failing to reflect the change in values
OK so I seem to have found a solution - what I am presuming is that ForEach containing a range does not update dynamically in the same way that ForEach containing an array of objects does.
So rather than:
ForEach(0..<tabBarViewModel.loginForms.count)
etc.
I changed it to:
ForEach(tabBarViewModel.loginForms.prefix(tabBarViewModel.activeFormIndex))
This way I still iterate the required number of times but the ForEach updates dynamically with the correct number of items in the array

Make view not redraw after updating ObservableObject

I have a situation where I am using LazyVGrid to display Images from a users Photo library. The grid has sections, and each section contains an array of images which are to be displayed, along with some meta data about the section.
The problem I am having is that each time the user clicks on a tile, which toggles markedForDeletion in the corresponding Image, all of the views in the Grid (which are visible) redraw. This isn't ideal, as in the "real" version of the sample code there is a real penalty to redrawing each Tile, as images must be retrieved and rendered.
I have tried to make Tile conform to Equatable, and then use the .equatable() modifier to notify SwiftUI that the Tile shouldn't be redrawn, but this doesn't work.
Placing another .onAppear outside the Section hints that the entire section is being redrawn each time anything changes, but I'm not sure how to structure my code so that the impact of redrawing expensive Tiles is minimised.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
let columns = [
GridItem(.flexible(minimum: 40), spacing: 0),
GridItem(.flexible(minimum: 40), spacing: 0),
]
var body: some View {
GeometryReader { gr in
ScrollView {
LazyVGrid(columns: columns) {
ForEach(viewModel.imageSections.indices, id: \.self) { imageSection in
Section(header: Text("Section!")) {
ForEach(viewModel.imageSections[imageSection].images.indices, id: \.self) { imageIndex in
Tile(image: viewModel.imageSections[imageSection].images[imageIndex])
.equatable()
.onTapGesture {
viewModel.imageSections[imageSection].images[imageIndex].markedForDeletion.toggle()
}
.overlay(Color.blue.opacity(viewModel.imageSections[imageSection].images[imageIndex].markedForDeletion ? 0.2 : 0))
.id(UUID())
}
}
}
}
}
}
}
}
struct Image {
var id: String = UUID().uuidString
var someData: String
var markedForDeletion: Bool = false
}
struct ImageSection {
var id: String = UUID().uuidString
var images: [Image]
}
class ViewModel: ObservableObject {
#Published var imageSections: [ImageSection] = generateFakeData(numSections: 10, numImages: 10)
}
func generateFakeData(numSections: Int, numImages: Int) -> [ImageSection] {
var sectionsToReturn: [ImageSection] = []
for _ in 0 ..< numSections {
var imageArray: [Image] = []
for i in 0 ..< numImages {
imageArray.append(Image(someData: "Data \(i)"))
}
sectionsToReturn.append(ImageSection(images: imageArray))
}
return sectionsToReturn
}
struct Tile: View, Equatable {
static func == (lhs: Tile, rhs: Tile) -> Bool {
return lhs.image.id == rhs.image.id
}
var image: Image
var body: some View {
Text(image.someData)
.background(RoundedRectangle(cornerRadius: 20).fill(Color.blue))
.onAppear(perform: {
NSLog("Appeared - \(image.id)")
})
}
}
Main reason of global redraw is .id(UUID()), because on every call it force view recreation. But to remove it and keep ForEach operable we need to make model identifiable explicitly.
Here is fixed parts of code. Tested with Xcode 12.1 / iOS 14.1
main cycle
ForEach(Array(viewModel.imageSections.enumerated()), id: \.1) { i, imageSection in
Section(header: Text("Section!")) {
ForEach(Array(imageSection.images.enumerated()), id: \.1) { j, image in
Tile(image: image)
.onTapGesture {
viewModel.imageSections[i].images[j].markedForDeletion.toggle()
}
.overlay(Color.red.opacity(image.markedForDeletion ? 0.2 : 0))
}
}
}
models (note - your Image conflicts with standards SwiftUI Image - avoid such conflicts otherwise you might get into very unclear errors. For demo I renamed it everywhere needed to ImageM)
struct ImageM: Identifiable, Hashable {
var id: String = UUID().uuidString
var someData: String
var markedForDeletion: Bool = false
}
struct ImageSection: Identifiable, Hashable {
var id: String = UUID().uuidString
var images: [ImageM]
}