TLDR: Move Indicators did not appear in List EditMode even after the moveDisabled modifier value is false(dynamically updated).
Hello guys, I’m here to ask about the editMode in listView that made me cry for over 2 days. I know that editMode in ListView is still flakey, but I want some help here.
The problem is: When we move items inside listView using the onMove Modifier it works fine until we use the moveDisabled modifier for items inside ForEach.
The real problem begins when we tryin’ to add a new entry to the array while editMode is enabled. The move indicators for new items did not appear even if the value inside moveDisabled is false, unless you have to refresh the whole listview using identifier.
I’ll drop some code down here and an attachment with the problem. Please help me fix this. Sorry for the typos, Thanks in advance!!
//
// AttributesReorderView.swift
// OneApp
//
// Created by Ligen Raj on 06/09/24.
// Created for StackOverflow Questioning.
//
import SwiftUI
fileprivate struct ItemView: View {
// MARK: - PROPERTIES
@Binding var item: IdentifiableAttribute
@FocusState.Binding var focused: Int?
var index: Int?
var disabled: Bool
init(item: Binding<IdentifiableAttribute>, focused: FocusState<Int?>.Binding, index: Int?, disabled: Bool) {
self._item = item
self._focused = focused
self.index = index
self.disabled = disabled
} //: INIT
// MARK: - BODY
var body: some View {
HStack(alignment: .center, spacing: 16) {
TextField("Add new item", text: $item.attribute.title)
.focused($focused, equals: index)
.textInputAutocapitalization(.words)
.font(.medium)
ColorPickerView
.transition(.opacity)
.active(if: !disabled)
DividerView(color: .themeSecondary, width: 1)
.padding(.vertical, 4)
.hidden(disabled)
} //: HSTACK
.padding(8)
.padding(.leading, 2)
.background(alignment: .leading, content: BackgroundView)
.overlay(alignment: .leading, content: {
DividerView(color: item.color, width: 2)
})
.transition(.opacity)
}
@ViewBuilder private var ColorPickerView: some View {
ZStack(alignment: .center) {
Circle()
.fill(item.color)
.aspectRatio(contentMode: .fit)
.allowsHitTesting(false)
.background {
Circle()
.stroke(lineWidth: 1)
.foregroundStyle(.themeSecondary)
}
ColorPicker(selection: $item.color, label: emptyView)
.labelsHidden()
.blendMode(.destinationOut)
.compositingGroup()
} //: ZSTACK
.scaleEffect(0.8)
.frame(width: 20, height: 20)
} //: VIEW_VAR
@ViewBuilder private func DividerView(color: Color, width: CGFloat) -> some View {
Capsule()
.fill(color)
.frame(width: width)
} //: VIEW_FUNC
@ViewBuilder private func BackgroundView() -> some View {
RoundedCorner(radius: 6, corners: [.topRight, .bottomRight])
.fill(.themeDarkBlue)
.frame(width: disabled ? nil : Helper.screenWidth - 16)
} //: VIEW_FUNC
}
struct AttributesReorderView: View {
// MARK: - PROPERTIES
@StateObject private var viewModel: AttributeReorderViewModel = .init()
@State private var editMode: EditMode = .inactive
@FocusState private var focused: Int?
var items: [Attribute]
var title: String
init(items: [Attribute], title: String, type: String) {
self.items = items
self.title = title
viewModel.type = type
} //: INIT
// MARK: - BODY
var body: some View {
VStack(alignment: .center, spacing: 8) {
HeaderView
List {
ForEach($viewModel.items, id: .id) { $item in
let isDisabled: Bool = viewModel.isTitleEmpty(item)
let index: Int? = viewModel.getIndex(item)
ItemView(item: $item, focused: $focused, index: index, disabled: isDisabled)
// Not updating properly => .moveDisabled(isDisabled)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive, action: {
viewModel.onDelete(item)
}) {
Image(systemName: "trash")
} //: BUTTON
.active(if: !isDisabled)
}
} //: LOOP
.onMove(perform: viewModel.onMove)
} //: LIST
.listStyle(.plain)
.environment(.editMode, $editMode)
.emptyListBackground()
.hideScrollContentIfAvailable()
.animation(.easeIn, value: viewModel.items)
.animation(.easeOut, value: editMode.isEditing)
MinimalButtonView(title: "Done", action: {})
.padding([.horizontal, .bottom])
} //: VSTACK
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundStyle(.white)
.customSheetBG(color: .themeBlack)
.environment(.colorScheme, .dark)
.onAppear(perform: onAppear)
.onReceive(viewModel.itemRemovedPublisher, perform: onReceiveItemRemovedAtIndex)
.onSubmit(onSubmit)
}
@ViewBuilder private var HeaderView: some View {
CommonNavBar(title: title, centerAligned: true, foregroundColor: .white)
.overlay(alignment: .trailing, content: {
HStack(alignment: .center, spacing: 0) {
PaddingButton(systemName: "plus", padding: 16, renderingMode: .template(.white), action: {
// The problem is here => viewModel.appendAttribute()
})
.symbolRenderingMode(editMode.isEditing ? .palette : .hierarchical)
PaddingButton(systemName: "square.and.pencil", padding: 16, renderingMode: .template(.white), action: {
editMode = editMode.isEditing ? .inactive : .active
})
.symbolRenderingMode(editMode.isEditing ? .palette : .hierarchical)
}
})
} //: VIEW_VAR
private func onAppear() {
viewModel.items = viewModel.cast(items)
guard !items.contains(where: .title.isEmpty) else { return }
viewModel.appendAttribute()
} //: FUNC
private func onSubmit() {
guard focused != viewModel.items.indices.last else { return }
focused = (focused ?? 0) + 1
} //: FUNC
func onReceiveItemRemovedAtIndex(_ index: Int?) {
guard index != .zero else { return }
focused = (index ?? 0) - 1
} //: FUNC
}
// MARK: - PREVIEW
struct AttributeReorderPreviewView: View {
@State private var items: [Attribute] = [.status1, .status2, .status3]
var body: some View {
AttributesReorderView(items: items, title: "Project Status", type: "status")
}
}
#Preview {
AttributeReorderPreviewView()
}
Feel free to ask any doubt about the code..
Any help regarding this will be appreciated.