ForEach onDelete Mac - swift

I am trying to figure out how to delete from an array in swiftui for Mac. All the articles that I can find show how to do it the UIKit way with the .ondelete method added to the foreach. This is does not work for the Mac because I do not have the red delete button like iOS does. So how do I delete items from an array in ForEach on Mac.
here is the code that I have tried, which gives me the error
Fatal error: Index out of range: file
import SwiftUI
struct ContentView: View {
#State var splitItems:[SplitItem] = [
SplitItem(id: 0, name: "Item#1"),
SplitItem(id: 1, name: "Item#2")
]
var body: some View {
VStack {
Text("Value: \(self.splitItems[0].name)")
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func removeItem(index:Int) {
self.splitItems.remove(at: index)
}
}
struct SplitItem:Identifiable {
var id:Int
var name:String
}

Your ForEach code part is completely correct.
The possible error source could be the line
Text("Value: \(self.splitItems[0].name)") - after deleting all items this will definitely crash.
The next assumption is the VStack. After Drawing a view hierarchy it will not allow to change it.
If you replace the VStack with Group and the next code will work:
var body: some View {
Group {
if !self.splitItems.isEmpty {
Text("Value: \(self.splitItems[0].name)")
} else {
Text("List is empty")
}
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

so I was able to find the answer in this article,
instead of calling array.remove(at) by passing the index from the for loop call it using array.FirstIndex(where).

Related

Forcing a view to unload or don't be displayed when a list has no items

I have a list that builds up with a NavigationLink based on the members of an array. When you click on the list item, the next view loads up to the right displaying the data for that particular item.
When you delete the item, the next one gets displayed. However, when you delete all the items in the list, the last item's data remains left behind on the loaded view. I tried to look at the selected item being nil, or some other form of checking whether something is selected, or the list has at least one item, but I can't figure out how check all this and unload the view.
Here's the code:
#State var selectedNoteId: UUID?
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!).font(.body).fontWeight(.bold)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 10)
}
}
.listStyle(InsetListStyle())
.frame(minWidth: 250, maxWidth: .infinity)
Text("Select a note or create a new one...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
How do I unload NoteView and go back to displaying the Text("Select a note or create a new one...")
Thanks for the help!
EDITED:
this is how I remove data:
func removeNote() {
if let selection = selectedNoteId,
let selectionIndex = data.notes.firstIndex(where: { $0.id == selection }) {
//print("DEBUG: delete item: \(selectionIndex)")
data.notes.remove(at: selectionIndex)
}
}
You can wrap whole list with if
NavigationView {
if !data.notes.isEmpty {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!).font(.body).fontWeight(.bold)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 10)
}
}
.frame(minWidth: 250, maxWidth: .infinity)
} else {
List(data.notes) { _ in EmptyView() }
}
Text("Select a note or create a new one...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.listStyle(InsetListStyle())
.navigationTitle("A title")
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
data.notes.removeAll()
}
}
When notes is empty, content of list isn't being called. Looks like macOS is pretty buggy by not dismissing old navigation link
Would something like this work? Im not sure how you are actually removing data when the user deletes it.
List(data.notes) { note in
if data.notes.isEmpty {
navlink
} else {
Text("Select a note or create a new one...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
So, I found a cleaner way. Add an extension:
extension View {
#ViewBuilder func hidden(_ shouldHide: Bool) -> some View {
switch shouldHide {
case true: self.hidden()
case false: self
}
}
}
Then in NoteView call:
.hidden(data.notes.isEmpty)
And when data is empty the view will go away.
It's an ugly hack, I guess, but somehow the last view is left behind when there are not items in the list, and I can't find any other way to force it to unload.

How do I handle selection in NavigationView on macOS correctly?

I want to build a trivial macOS application with a sidebar and some contents according to the selection in the sidebar.
I have a MainView which contains a NavigationView with a SidebarListStyle. It contains a List with some NavigationLinks. These have a binding for a selection.
I would expect the following things to work:
When I start my application the value of the selection is ignored. Neither is there a highlight for the item in the sidebar nor a content in the detail pane.
When I manually select an item in the sidebar it should be possible to navigate via up/down arrow keys between the items. This does not work as the selection / highlight disappears.
When I update the value of the selection-binding it should highlight the item in the list which doesn't happen.
Here is my example implementation:
enum DetailContent: Int, CaseIterable {
case first, second, third
}
extension DetailContent: Identifiable {
var id: Int { rawValue }
}
class NavigationRouter: ObservableObject {
#Published var selection: DetailContent?
}
struct DetailView: View {
#State var content: DetailContent
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content.rawValue)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent? = .first
var body: some View {
NavigationView {
List {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
NavigationLink(
destination: DetailView(content: item),
tag: item,
selection: self.$detailContent,
label: { Text("\(item.rawValue)") }
)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
}
.environmentObject(navigationRouter)
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
self.detailContent = output
}
}
}
The EnvironmentObject is used to propagate the change from inside the DetailView. If there's a better solution I'm very happy to hear about it.
So the question remains:
What am I doing wrong that this happens?
I had some hope that with Xcode 11.5 Beta 1 this would go away but that's not the case.
After finding the tutorial from Apple it became clear that you don't use NavigiationLink on macOS. Instead you bind the list and add two views to NavigationView.
With these updates to MainView and DetailView my example works perfectly:
struct DetailView: View {
#Binding var content: DetailContent?
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content?.rawValue ?? -1)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent?
var body: some View {
NavigationView {
List(selection: $detailContent) {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
Text("\(item.rawValue)")
.tag(item)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
.listStyle(SidebarListStyle())
DetailView(content: $detailContent)
}
.environmentObject(navigationRouter)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
self.detailContent = output
}
}
}
}

SwiftUI macOS NavigationView Cannot Highlight First Entry

I'm attempting to create a master/detail view on macOS with SwiftUI. When the master/detail view first renders, I'd like it to immediately "highlight" / "navigate to" its first entry.
In other words, I'd like to immediately render the following: master/detail first row highlighted
I'm using NavigationView and NavigationLink on macOS to render the master/detail view:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("detail-1").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Text("link-1")
}
NavigationLink(destination: Text("detail-2").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Text("link-2")
}
}
}
}
}
I've tried using both the isActive and the tag / selection options provided by NavigationLink with no luck. What might I be missing here? Is there a way to force focus on the first master/detail element using SwiftUI?
I came across this problem recently and after being stuck at the same point I found Apple's tutorial which shows that you don't use NavigationLink on macOS.
Instead you just create a NavigationView with a List and a DetailView. Then you can bind the List's selection and it works properly.
There still seems to be a bug with the highlight. The workaround is setting the selection in the next run loop after the NavigationView has appeared. :/
Here's a complete example:
enum DetailContent: Int, CaseIterable, Hashable {
case first, second, third
}
extension DetailContent: Identifiable {
var id: Int { rawValue }
}
struct DetailView: View {
#Binding var content: DetailContent?
var body: some View {
VStack {
Text("\(content?.rawValue ?? -1)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#State var detailContent: DetailContent?
var body: some View {
NavigationView {
List(selection: $detailContent) {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
Text("\(item.rawValue)")
.tag(item)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
.listStyle(SidebarListStyle())
if detailContent != nil {
DetailView(content: $detailContent)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
DispatchQueue.main.async {
self.detailContent = DetailContent.allCases.randomElement()
}
}
}
}

SwiftUI List disable cell press

I am using xCode 11 beta 7 with SwiftUI.
I have a simple list which each list element has several buttons. Currently when the user presses the cell(not the buttons) it is highlighting the back of the list cell(probably not the correct terminology for SwiftUI).
How do i disable this behaviour? I could not locate an obvious api to disable it.
List {
HStack {
Group {
Button(action: {}) {
Text("Read").padding(5)
}.onTapGesture {
print("1")
}
.padding()
.background(Color.blue)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Notify").padding(5)
}.onTapGesture {
print("2")
}
.padding()
.background(Color.purple)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Write").padding(5)
}.onTapGesture {
print("3")
}
.padding()
.background(Color.yellow)
.cornerRadius(5)
}
}
}
Same answer as in How to remove highlight on tap of List with SwiftUI?
I know I'm a bit late, but it's for those of you who are searching (like me 😇)
What I found
I guess you should take a look at the short article How to disable the overlay color for images inside Button and NavigationLink from #TwoStraws
Just add the .buttonStyle(PlainButtonStyle()) modifier to your item in the List and you'll have what you wanted. It also makes the Buttons work again in the List, which is another problem I encountered.
A working example for Swift 5.1 :
import Combine
import SwiftUI
struct YourItem: Identifiable {
let id = UUID()
let text: String
}
class YourDataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var items = [YourItem]()
init() {
items = [
YourItem(text: "Some text"),
YourItem(text: "Some other text")
]
}
}
struct YourItemView: View {
var item: YourItem
var body: some View {
VStack(alignment: .leading) {
Text(item.text)
HStack {
Button(action: {
print("Like")
}) {
Image(systemName: "heart.fill")
}
Button(action: {
print("Star")
}) {
Image(systemName: "star.fill")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
struct YourListView: View {
#ObservedObject var dataSource = YourDataSource()
var body: some View {
List(dataSource.items) { item in
YourItemView(item: item)
}
.navigationBarTitle("List example", displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
}
}
#if DEBUG
struct YourListView_Previews: PreviewProvider {
static var previews: some View {
YourListView()
}
}
#endif
As said in the article, it also works with NavigationLinks. I hope it helped some of you 🤞🏻
This is my simplest solution that is working for me (lo and behold, I'm building my first app in Swift and SwiftUI as well as this being my first post on SO):
Wherever you have buttons, add .buttonStyle(BorderlessButtonStyle())
Button(action:{}){
Text("Hello")
}.buttonStyle(BorderlessButtonStyle())
Then on your list, add .onTapGesture {return}
List{
Text("Hello")
}.onTapGesture {return}
Instead of using a button try it with a gesture instead
Group {
Text("Notify").padding(5).gesture(TapGesture().onEnded() {
print("action2")
})
.padding()
.background(Color.purple)
.cornerRadius(5)
}

Add animations to ForEach loop elements (SwiftUI)

Is there any way I can add animation when elements of a ForEach loop appears or disappears?
I have tried using withAnimation{} and .animation() in many ways but they didn't seem to work
Here is some code (Xcode 11 beta 5):
import SwiftUI
struct test: View {
#State var ContentArray = ["A","B","C"]
var body: some View {
ScrollView{
VStack{
ForEach(ContentArray.indices, id: \.self){index in
ZStack{
// Object
Text(self.ContentArray[index])
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
self.ContentArray.remove(at: index)
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}
}
}
}
}
}
#if DEBUG
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
#endif
As can be seen below, without animations everything feels super abrupt. Any solution is really appreciated
Important notice: The layout should change in the same way List does when the number of elements changes. For example, every object automatically moves top when a top object is deleted
It looks like this problem is still up to day (Xcode 11.4), because by just copy-pasting the observed effect is the same. So, there are a couple of problems here: first, it needs to setup combination of animation and transition correctly; and, second, the ForEach container have to know which exactly item is removed, so items must be identified, instead of indices, which are anonymous.
As a result we have the following effect (transition/animation can be others):
struct TestAnimationInStack: View {
#State var ContentArray = ["A","B","C", "D", "E", "F", "G", "I", "J"]
var body: some View {
ScrollView{
VStack{
ForEach(Array(ContentArray.enumerated()), id: \.element){ (i, item) in // << 1) !
ZStack{
// Object
Text(item)
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
withAnimation { () -> () in // << 2) !!
self.ContentArray.remove(at: i)
}
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}.transition(AnyTransition.scale) // << 3) !!!
}
}
}
}
}