Problem with .disable modifier in ContextMenu SwiftUI - swiftui

I found interesting action in my program:
struct Test: View {
#State private var redButton: Bool = false
var body: some View {
List {
ForEach(1...10, id: \.self) { numbers in
Button {
redButton = false
} label: {
Text("Button \(numbers)")
}.contextMenu {
Button {
//action code
redButton = true
} label: {
Text("Deactivate")
}.disabled(redButton)
}
}
}
}
}
If u run this code and press "Deactivate" in contexMenu, contextMenu will be disabled only for 6..10 buttons, this code switching off/on contextMenu element randomly (try increase or decrease Lists elements and press "Deactivate" on random List element).
If U remove List all working correctly with one Button.
Maybe I need work with dispatchQueue.main.async when change redButton status?
What I doing wrong?

Correct code:
struct Test: View {
#State var redButton: Bool = false
var body: some View {
List {
ForEach(1...3, id: \.self) { numbers in
Menu("Actions \(numbers)") {
Button("Deactivate", action: {
redButton = true
})
Button("Activate", action: {
redButton = false
})
Button(action: {}) {
Label("Delete", systemImage: "trash")
}.disabled(redButton)
Button(action: {}) {
Label("Call", systemImage: "phone")
}.disabled(redButton)
}
}
}
}
}

Related

How to open PhotosPicker from confirmationDialog in SwiftUI?

I would like to make an ActionSheet, where user could choose if he want to select image from Gallery or take a picture.
However, implementing this I was not able to find a way how PhotosPicker could be called in such context. The code below doesn't work: the item "Choose from gallery" displayed, but no reaction on tapping.
I will highly appreciate any advises.
Thank you!
Button {
displayImageActionSheet = true
} label: {
Image("user_photo_placeholder")
}
.confirmationDialog("Profile picture",
isPresented: $displayImageActionSheet,
titleVisibility: .visible) {
PhotosPicker("Choose from gallery",
selection: $selectedPhotoImage,
matching: .any(of: [.videos, .not(.images)]))
Button("Take picture") {
}
Button("Remove", role: .destructive) {
}
}
You can present it by using photosPicker(isPresented:)
import SwiftUI
import PhotosUI
struct ConfimShowPhotos: View {
#State var showPicker: Bool = false
#State var showDialog: Bool = false
#State var showCamera: Bool = false
#State var selectedItem: PhotosPickerItem? = nil
var body: some View {
Button {
showDialog.toggle()
} label: {
Image(systemName: "photo")
}
.confirmationDialog("Profile Picture", isPresented: $showDialog) {
Button {
showCamera.toggle()
} label: {
Label("Camera", systemImage: "camera")
}
Button {
showPicker.toggle()
} label: {
Label("Gallery", systemImage: "photo.artframe")
}
}
.photosPicker(isPresented: $showPicker, selection: $selectedItem)
.alert("Say Cheese!!", isPresented: $showCamera) {
Text("Mimicking showing camera")
}
}
}

Undesired interplay between tapable, movable items and scrolling in SwiftUI List

I'm working on a SwiftUI list that shows tapable and long-pressable full-width items, which are movable, and allow for detail navigation.
I've noticed that .onLongPressGesture isn't detected when the list allows for moving of items, because the List switches to drag-moving the long-pressed item instead.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentView())
I've experimented further and found that using simultaneous gesture via simultaneousGesture() fixes the missing notification on long presses, but instead removes scrolling ability from the List.
import SwiftUI
import PlaygroundSupport
struct ContentViewSimultaneous: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.blue)
.simultaneousGesture(TapGesture().onEnded { print("tapped", item) })
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed", item) })
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentViewSimultaneous())
I'm now looking for a way to make this work and would appreciate any insights! I'm new to SwiftUI and might miss something important.
I think I was able to get this working as you describe. It works with no issues on iOS 15, but there seems to be an animation bug in iOS 16 that causes the rearrange icon not to animate in for some/all List rows. Once you drag an item in edit mode, the icon will display.
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State var disableMove: Bool = true
var body: some View {
let data = Array(0..<20)
NavigationView {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}
.onMove(perform: disableMove ? nil : moveItems)
}
.toolbar {
ToolbarItem {
Button {
withAnimation {
self.disableMove.toggle()
}
} label: {
Text(editMode == .active ? "Done" : "Edit")
}
}
}
.environment(\.editMode, $editMode)
}
.onChange(of: disableMove) { disableMove in
withAnimation {
self.editMode = disableMove ? .inactive : .active
}
}
.navigationViewStyle(.stack)
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
Not sure if this helps
enum Status {
case notPressed
case pressed
case longPressed
}
struct ContentView: View {
#State private var status = Status.notPressed
var body: some View {
Rectangle()
.foregroundColor(color)
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed")
status = .longPressed
})
.simultaneousGesture(TapGesture().onEnded { _ in
print("pressed")
status = .pressed
})
}
var color: Color {
switch status {
case .notPressed:
return .mint
case .pressed:
return .yellow
case .longPressed:
return .orange
}
}
}

SwiftUI : How I can use confirmationDialog for each row of a list?

import SwiftUI
struct MemoListView: View {
let folder : FolderModel
#State private var showActionSheet : Bool = false
#EnvironmentObject var vm : FolderListViewModel
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
vm.deleteMemo(folder: folder, memo: memo)
}, label: {
Text("Delete")
.foregroundColor(.red)
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Hi!
Please check my code above.
I want to apply actionSheet(confirmationDialog) to each of row in list but I think the list can't recognize which row is selected.
If I tried to delete row3, it just delete only row1
I don't know how I can handle this situation.
Thanks!
As per your comments to the original post, .confirmationDialog only has an isPresented binding available, rather than an object-based one as with sheet, fullScreenCover, etc.
One workaround I've used quite successfully is to move the confirmationDialog, and the boolean state flag that drives it, into a child view. This could be MemoRowView itself, or you could keep that view as the presentation component only and wrap it in an interactive view that called MemoRowView and added the interactive elements, e.g.
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder)
}
.listStyle(.plain)
This does mean injecting quite a lot of domain knowledge into the list item, so it'd need its own #EnvironmentObject reference, etc., but that could be lived with.
Alternatively, you could keep the code that deletes the object in the parent view, and just keep the confirmation in the child view:
struct MemoListItem: View {
var memo: MemoModel
var folder: FolderModel
var onDelete: () -> Void
#State private var showDeleteConfirmation: Bool = false
init(memo: MemoModel, folder: FolderModel, onDelete: #escaping () -> Void) {
self.memo = memo
self.folder = folder
self.onDelete = onDelete
}
var body: some View {
// rest of row setup omitted for brevity
.confirmationDialog(Text("Option"), isPresented: $showDeleteConfirmation) {
Button("Delete", role: .destructive, action: onDelete)
}
}
}
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder, onDelete: {
vm.deleteMemo(folder: folder, memo: memo)
})
}
}
An explanation of what I wrote in my comment :
struct MemoListView: View {
#EnvironmentObject var vm : FolderListViewModel
// Updated folder so as it seems it is extracted from your
// view model
var folder : FolderModel {
vm.folder
}
#State private var showActionSheet : Bool = false
// This is the way to know which memo to delete
#State var memoToDelete: Memo?
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
// Save the memo on the current row
memoToDelete = memo
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
// Delete the saved memo
if let memo = memoToDelete {
vm.deleteMemo(folder: folder, memo: memo)
}
}, label: {
// Here to show the memo id before delete
if let memo = memoToDelete {
Text("Delete \(memo.id)")
.foregroundColor(.red)
}
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Note : I changed from the comment and kept the dialog on each row.
It is not the best way to do it.

State variable not updated when changed in another View

I want to better understand binding data across view, so I made this demo app
First View - if isShowing is true, navigating to SecondView (binding value)
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
Text("Go to second view")
}
}
}
}
}
Second view - updating ParentView value
struct SecondView: View {
#Binding var value: Int
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack {
Button {
value = 5
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 5")
}
Button {
value = 1
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 1")
}
}
}
}
ThirdView - showing in FirstView in case value is not 5
struct ThirdView: View {
#Binding var isShowing: Bool
var body: some View {
ZStack {
Button {
isShowing.toggle()
} label: {
Text("Its a problem... Go to second view")
}
}
}
}
I tried to toggle isShowing in ThirdView so it can open SecondView to update value again.
But when button is clicked in ThirdView, it doesnt do anything.
The way you have things set up, it won't change. When value != 5, your `NavigationLink does not exist in the view. Instead, you want to trigger it programmatically like this:
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
VStack {
Text(value.description)
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
// Change out the NavigationLink for a button that sets isShowing.
Button {
isShowing = true
} label: {
Text("Go to second view")
}
}
}
// By placing it in the background, it is always available to be triggered.
.background(
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
EmptyView()
}
)
}
}
}
Lastly, you don't need to toggle isShowing in ThirdView. You are better off either dismissing the view or setting the value to false. Otherwise, you can get confused what it is doing when you are in your various views.

.sheet: Shows only once and then never again

Working with Beta4, it seems that the bug is still existing. The following sequence of views (a list, where a tap on a list entry opens another list) allows to present the ListView exactly once; the onDisappear is never called, so the showModal flag changes, but does not triggers the redisplay of ListView when tapped again. So, for each GridCellBodyEntry, the .sheet presentation works exactly once, and then never again.
I tried around with several suggestions and workarounds, but none worked (e.g., encapsulating with a NavigationViewModel). I even tried to remove the List, because there was an assumption that the List causes that behaviour, but even this did not change anything.
Are there any ideas around?
The setup:
A GridCellBody with this view:
var body: some View {
GeometryReader { geometry in
VStack {
List {
Section(footer: self.footerView) {
ForEach(self.rawEntries) { rawEntry in
GridCellBodyEntry(entityType: rawEntry)
}
}
}
.background(Color.white)
}
}
}
A GridCellBodyEntry with this definition:
struct GridCellBodyEntry: View {
let entityType: EntityType
let viewModel: BaseViewModel
init(entityType: EntityType) {
self.entityType = entityType
self.viewModel = BaseViewModel(entityType: self.entityType)
}
#State var showModal = false {
didSet {
print("showModal: \(showModal)")
}
}
var body: some View {
Group {
Button(action: {
self.showModal.toggle()
},
label: {
Text(entityType.localizedPlural ?? "")
.foregroundColor(Color.black)
})
.sheet(isPresented: $showModal, content: {
ListView(showModal: self.$showModal,
viewModel: self.viewModel)
})
}.onAppear{
print("Profile appeared")
}.onDisappear{
print("Profile disappeared")
}
}
}
A ListView with this definition:
struct ListView: View {
// MARK: - Private properties
// MARK: - Public interface
#Binding var showModal: Bool
#ObjectBinding var viewModel: BaseViewModel
// MARK: - Main view
var body: some View {
NavigationView {
VStack {
List {
Section(footer: Text("\(viewModel.list.count) entries")) {
ForEach(viewModel.list, id: \.objectID) { item in
NavigationLink(destination: ItemView(),
label: {
Text("\(item.objectID)")
})
}
}
}
}
.navigationBarItems(leading:
Button(action: {
self.showModal = false
}, label: {
Text("Close")
}))
.navigationBarTitle(Text(viewModel.entityType.localizedPlural ?? ""))
}
}
}
The BaseViewModel (excerpt):
class BaseViewModel: BindableObject {
/// The binding support.
var willChange = PassthroughSubject<Void, Never>()
/// The context.
var context: NSManagedObjectContext
/// The current list of typed items.
var list: [NSManagedObject] = []
// ... other stuff ...
}
where willChange.send() is called whenever something changes (create, modify, delete operations).
This is a variant of swiftUI PresentaionLink does not work second time
The following simplified code exhibits the behavior you're experiencing (the sheet only displays once):
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}.sheet(isPresented: $isPresented, content: {
Text("Destination View \(self.whichPresented)") })
}
}
}
}
There appears to be a bug in SwiftUI when you put the .sheet inside a List or a ForEach. If you move the .sheet outside of the List, you should be able to get the correct behavior.
import SwiftUI
struct ContentView: View {
#State var isPresented = false
#State var whichPresented = -1
var body: some View {
NavigationView {
List {
ForEach(0 ..< 10) { i in
Button(action: {
self.whichPresented = i
self.isPresented.toggle()
})
{ Text("Button \(i)") }
}
}
}.sheet(isPresented: $isPresented, content: { Text("Destination View \(self.whichPresented)") })
}
}