onDelete is attached to ForEach but there's no swipe gestures - swiftui

With .onDelete() attached to the ForEach, nothing happens - cannot swipe to delete nor does tapping Edit show anything. Not sure what I've done wrong. It compiles fine in simulator and on my iPhone (iOS 14.5) Here's my actual code.
NavigationView {
List {
ForEach(medicines) { medicine in
HStack {
VStack(alignment: .leading) {
Text("\(medicine.name!) \(medicine.strength) \(medicine.unit!)" as String)
.font(.headline)
Text("\(medicine.quantity) x \(medicine.form!) \(medicine.direction!)" as String)
.font(.subheadline)
}
}
.frame(height: 50)
}
.onDelete(perform: deleteMedicines)
.listStyle(PlainListStyle())
.navigationBarTitle("Medicines")
.navigationBarItems(leading: EditButton(), trailing: Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $showingAddScreen) {
AddMedicineView().environment(\.managedObjectContext, self.viewContext)
}
}
}
Then here's the deleteMedicines() function:
private func deleteMedicines(offsets: IndexSet) {
withAnimation {
offsets.map { medicines[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
I've seen a few different ways of doing this and I've tried a few. I had it working when I was first playing around with List. The deleteMedicines code is borrowed from Xcode template for Core Data.

Provided code is not testable, but try to keep only onDelete modifier on ForEach, everything else move below onto List (placement of modifiers are important and can be a reason of your issue), like
NavigationView {
List {
ForEach(medicines) { medicine in
// content here
}
.onDelete(perform: deleteMedicines) // << here !!
}
.listStyle(PlainListStyle())
.navigationBarTitle("Medicines")
.navigationBarItems(leading: EditButton(), trailing: Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $showingAddScreen) {
AddMedicineView().environment(\.managedObjectContext, self.viewContext)
}

Related

How to add conditional ToolbarItems to NavigationView Toolbar with SwiftUI?

I am trying to add an icon button to the leading edge of a NavigationView's toolbar - but I want that button to only be visible when the device is in landscape mode. (Kinda like how the default Notes app works)
I have the following code:
var body: some View {
VStack {
// ... content display
}
.toolbar {
if UIDevice.current.orientation.isLandscape {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: toggleFullscreen) {
if isFullscreen {
Image(systemName: "arrow.down.right.and.arrow.up.left")
} else {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: createNewNote) {
Image(systemName: "square.and.pencil")
}
}
})
}
If I remove the conditional if clause within the toolbar content, it works fine, but it seems to not recognize the if statement at all?
However, the if statement in the VStack works fine.
The specific error I get is:
Closure containing control flow statement cannot be used with result builder 'ToolbarContentBuilder'
Any idea how to fix this?
P.S. Consider me a complete SwiftUI noob. Although, I am proficient in React - so any React analogies to help me understand would be much appreciated.
You're detecting the device orientation in a wrong way SwiftUI One way you can achieve that is by using a custom modifier to detect device rotation:
Cfr: https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation
The code would look like this:
1. Create custom modifier: Detecting Device Rotation
// Our custom view modifier to track rotation and call our action
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
// A View wrapper to make the modifier easier to use
extension View {
func onRotate(perform action: #escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}
2. Use the created modifier:
struct ContentView: View {
#State private var isFullscreen = false
#State private var orientation = UIDeviceOrientation.unknown
var body: some View {
NavigationView {
VStack {
// ... content display
}
.onRotate { orientation = $0 }
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if orientation.isLandscape {
Button(action: {}) {
if isFullscreen {
Image(systemName: "arrow.down.right.and.arrow.up.left")
} else {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
}

SwiftUI iOS 15 Keyboard toolbar doesn't show for textfield

I'm pretty new to SwiftUI, trying to teach myself a few things here and there. But there's this one issue that's been eating at me for a while... and I can't figure out why the toolbar doesn't work/show for me.
The sample code is below, but the button doesn't show nor is there an actual bar. I have iOS 15.2, with XCode 13.2 beta.
TextField("placeholder", text: $text)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
hideKeyboard()
}) {
Text("Done")
}
}
}
}
EDIT:
Figured out the reason why... it just wouldn't work in a scroll view for some reason. Anyone know why?
Under iOS 15.2 - Xcode 13.2 (not beta).
struct ContentView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
Text(text)
TextField("Enter your name", text: $text)
}
.padding()
.navigationTitle("SwiftUI")
.toolbar {
// ToolbarItem(placement: .keyboard) {
// Button("Ok") {
// print("ok")
// }
// }
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button("Press Me") {
print("Pressed")
}
Spacer()
Button(action: {
print("done")
}) { Text("Done") }
}
}
}
}
}
}
Make sure you put everything in a VStack or similar.
Xcode 13.3 (release version), in order for the toolbar to show up, the TextField and the .toolbar need to be inside of a NavigationView.
In my particular case I didn't want a NavigationBar, so I ended up with something like this to make it work:
var body: some View {
NavigationView {
VStack {
TextField("Enter your name", text: $text)
// Additional text fields go here, all text fields will get the toolbar.
}
.navigationTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Dismiss") {
print("Bismiss keyboard...")
}
}
}
}
}

how is possible deleting complex items in a dynamic list in SwiftUI?

I'm trying to implement a delete feature for a list of elements but the onDelete event doesn't work as expected.
Here's the code:
#ObservedObject var dm: DataManager = DataManager.shared
#State var isAddShown = false
var body: some View {
NavigationView {
VStack {
List($dm.TTDItemList) { $item in
VStack(alignment: .leading) {
TextEditor(text: $item.itemDesc)
.onTapGesture {}
Text(item.ItemTagsToText())
.font(.caption)
.foregroundColor(Color.red)
.multilineTextAlignment(.leading)
}
}
.onDelete(perform: cancella)
}
.onTapGesture { hideKeyboardAndSave() }
.navigationBarTitle(Text("Board"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: { withAnimation { self.isAddShown.toggle() } }) {
Image(systemName: !isAddShown ? "plus.circle.fill" : "minus.circle.fill").imageScale(.large)
}
)
}
}
private func hideKeyboardAndSave() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
dm.salva()
}
func cancella(at offset: IndexSet) {
guard let intindex = Array(offset).first else { return }
dm.TTDItemList.remove(at: intindex)
dm.salva()
}
}
The error I receive is:
Value of type 'List<Never, ForEach<LazyMapSequence<Array.Indices, (Array.Index, UUID)>, UUID, VStack<TupleView<(some View, some View)>>>>' (aka 'List<Never, ForEach<LazyMapSequence<Range, (Int, UUID)>, UUID, VStack<TupleView<(some View, some View)>>>>') has no member 'onDelete'
Implementing the List was tricky, but the code seems working allowing saving the modifications in the data manager.
The onDelete modifier is a method of ForEach.. It is not defined for other Views like List. You need to modify your code to use a ForEach inside your List.
List {
ForEach($dm.TTDItemList) { $item in
VStack(alignment: .leading) {
TextEditor(text: $item.itemDesc)
.onTapGesture {}
Text(item.ItemTagsToText())
.font(.caption)
.foregroundColor(Color.red)
.multilineTextAlignment(.leading)
}
}
.onDelete(perform: cancella)
}
Technically, onDelete is an extension method of the DynamicViewContent protocol, but the only types that conform to DynamicViewContent are ForEach and modified derivatives of ForEach.

Nesting ForEach loop and LazyVStack error

I have encountered two issues below my codes. They should be a simple fix but I couldn't figure them out by myself.
The first issue is I cannot pass my nested ForEach loop to change the system images. The reason I used for nested ForEach loop is the Xcode always throw the error "Expression was too complex to be solved in reasonable time". Here is the original link for this problem.
Nested If Statement in ForEach loop
import SwiftUI
struct HymnLyrics: View {
let verseImages: [String] = ["2.circle.fill", "3.circle.fill", "4.circle.fill", "5.circle.fill"]
var lyrics: [Lyric] = LyricList.hymnLa
#AppStorage("fontSizeIndex") var fontSizeIndex = Int("Medium") ?? 18
#AppStorage("fontIndex") var fontIndex: String = ""
#AppStorage("showHVNumbers") var showHVNumbers: Bool = true
#AppStorage("scrollToIndex") var scrollToIndex: Int?
var body: some View {
ScrollView {
ScrollViewReader { proxy in
LazyVStack(spacing: 10) {//LazyVStack is crash
ForEach (lyrics, id: \.id) { lyric in
Group {
HStack {
if showHVNumbers {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
}
Text(lyric.verse1)
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
ForEach ([lyric.verse2, lyric.verse3, lyric.verse4, lyric.verse5], id: \.self) { verseCount in
if (verseCount != nil) {
HStack {
if showHVNumbers {
Image(systemName: verseImages[verseCount])// Here is the error, it simply not works.
.font(.title2)
.foregroundColor(.red)
}
Text(verseCount ?? "")
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
}
}
}
.foregroundColor(.primary)
.padding(.horizontal, 10.0)
.padding(.bottom)
Divider()
}
.onChange(of: scrollToIndex, perform: { value in
proxy.scrollTo(value, anchor: .top)
})
}
}
}
}
}
The second issue is the app crashed when I scroll down to the bottom. When I replace LazyVStack with regular VStact, it works. But I want to use LazyVStack in this case. Please guide me for both of the issues. Thanks you in advance.

Buttons inside ToolbarItem cannot dismiss sheet

In Xcode 12 Beta 6, dismissing a sheet doesn't work inside a button's action inside a ToolbarItem.
My sheet view looks like:
NavigationView {
Form {
Section {
TextField("Name", text: $name)
}
}
.navigationTitle("New Thing")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: {
self.presentation.wrappedValue.dismiss()
}, label: {
Text("Cancel")
})
}
ToolbarItem(placement: .confirmationAction) {
Button(action: {
do {
// some saving logic
try managedObjectContext.save()
self.presentation.wrappedValue.dismiss()
} catch {
print("didn't save due to \(error.localizedDescription)")
}
}, label: {
Text("Save")
})
}
}
}
EDIT: here's how I constructed the sheet
var body: some View {
List {
ForEach(results) { result in
HStack {
NavigationLink(destination: SingleResultView(result: result)) {
SingleResultRowView(result: result)
}
}
}
.onDelete(perform: deleteResult)
}
.navigationTitle("All Results")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
self.isNewResultSheetPresented.toggle()
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
.sheet(isPresented: $isNewResultSheetPresented) {
NewResultView()
// ^ this contains the code above
.environment(\.managedObjectContext, self.managedObjectContext)
}
}
}
}
When the sheet is first presented, immediately a console log appears:
2020-09-13 20:52:02.333679-0700 MyApp[2710:89263]
[Presentation] Attempt to present <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x1027b7890> on
<_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x10270d620>
(from <_TtGC7SwiftUIP10$194f39bd428DestinationHostingControllerVS_7AnyView_: 0x103605930>)
which is already presenting <_TtGC7SwiftUI22SheetHostingControllerVS_7AnyView_: 0x103606d60>.
I can dismiss the sheet only by swiping down.
For reference, I went back to an older commit where I used NavigationBarItems and it worked perfectly. But from what I understand, this is a situation where I'm supposed to be using ToolbarItem.
Does anybody know why the good old self.presentation.wrappedValue.dismiss() doesn't work here or why is the sheet being presented twice?
Move sheet out of toolbar, as
var body: some View {
List {
ForEach(results) { result in
HStack {
NavigationLink(destination: SingleResultView(result: result)) {
SingleResultRowView(result: result)
}
}
}
.onDelete(perform: deleteResult)
}
.navigationTitle("All Results")
.sheet(isPresented: $isNewResultSheetPresented) { // << here !!
NewResultView()
// ^ this contains the code above
.environment(\.managedObjectContext, self.managedObjectContext)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
self.isNewResultSheetPresented.toggle()
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
})
}
}
}