listRowBackground removes selection style - swiftui

When using listRowBackground on a SwiftUI List there is no longer any highlighting of the selected item. Using a ButtonStyle for the NavigationLink does not work either.
Are there any sane workaround for this?
Example code:
struct ContentView: View {
struct ContentSection: Identifiable {
let id = UUID()
let title: String
let items: [String]
}
var sections = [
ContentSection(title: "Lorem", items: ["Dolor", "Sit", "Amed"]),
ContentSection(title: "Ipsum", items: ["Consectetur", "Adipiscing", "Elit"])
]
var body: some View {
NavigationView {
List {
ForEach(sections) { section in
Section {
ForEach(section.items, id: \.self) { item in
NavigationLink(destination: Text(item)) {
Text(item)
}
.listRowBackground(Color.orange.ignoresSafeArea())
}
} header: {
Text(section.title)
}
}
}
.listStyle(GroupedListStyle())
}
}
}

Although it is not documented in Apple's documentation, setting a .listRowBackground will wisely remove selection behaviour. What should happen if you set a background of Color.grey which matches the default selection color? Should Apple pick a different color now? How can they be sure the contrast is high enough for the user to tell if the selection is active?
Fortunately you can implement your own selection behaviour using List(selection:, content:) and then comparing the item being rendered in ForEach with the current selected item and changing the background yourself:
struct ContentView: View {
#State var selection: Int?
var body: some View {
List(selection: $selection) {
ForEach(1...5, id: \.self) { i in
Text(i, format: .number)
.listRowBackground(i == selection ? Color.red.opacity(0.5) : .white)
.tag(i)
}
}
}
}
Here it is in action:

Related

LazyVStack with animation causing menu to disappear

If an action in the menu rendered in LazyVStack causes the item wrapping it to disappear, when the item appears again via some other data change, the menu won’t display. This only happens if 1) the views are displayed in an LazyVStack and 2) the visibility change happens with some animation.
Here’s a small toy example:
import SwiftUI
import CoreMotion
struct Item: Equatable {
var id: String
var archived = false
}
struct ItemView: View {
let item: Item
let onChange: () -> Void
var body: some View {
HStack {
Text("item \(item.id)")
Menu {
Button {
onChange()
} label: {
Text("Switch")
}
} label: {
Text("menu")
}
}
}
}
class GroupOfItem: ObservableObject {
#Published var items: [Item] = [
Item(id: "a"),
Item(id: "b")
]
}
struct ContentView: View {
#State private var toggle = false
#StateObject private var groupOfItem = GroupOfItem()
var body: some View {
Toggle(isOn: $toggle) {
Text("toggle")
}
ScrollView {
LazyVStack {
ForEach(groupOfItem.items.filter { $0.archived == toggle }, id: \.id) { item in
ItemView(item: item) {
withAnimation {
groupOfItem.items[groupOfItem.items.firstIndex(of: item)!] = Item(id: item.id, archived: !item.archived)
}
}
}
}
}
}
}
Tapping on “switch” in the menu will cause the item to disappear with some animation, and switching the toggle will make it appear. However, notice that the menu is gone.
Is there a way to workaround this? VStack is not performant enough, List has its own quirks and without the animation the experience feels very jarring.
Could you work around this with a contextMenu? While not identical, it seems to work in OK in a LazyVStack
struct ItemView: View {
let item: Item
let onChange: () -> Void
var body: some View {
HStack {
Text("item \(item.id)")
Text("menu").foregroundColor(.accentColor)
.padding()
.contextMenu {
Button("Switch") {
onChange()
}
}
}
}
}

iOS 16 NavigationLinks in nested Lists unclickable

I am running xCode 14.2 on iOS 16.2 simulator and iOS 16.1.2 device.
I have, in my app, NavigationLinks in sublists that are implemented as nested Lists. After updating my xCode, suddenly the NavigationLinks have become unclickable. It looks like something happened to the touch target where the NavigationLink itself cannot be clicked, and only some tiny background sliver is clickable.
Here is sample code reproducing the issue:
import SwiftUI
#available(iOS 16.0, *)
struct ContentView: View {
var body: some View {
NavigationStack {
List {
List {
NavigationLink("Mint") { ColorDetail(color: .mint) }
NavigationLink("Pink") { ColorDetail(color: .pink) }
NavigationLink("Teal") { ColorDetail(color: .teal) }
}.listStyle(.plain)
List {
NavigationLink("Red") { ColorDetail(color: .red) }
NavigationLink("Blue") { ColorDetail(color: .blue) }
NavigationLink("Black") { ColorDetail(color: .black) }
}.listStyle(.plain)
}.listStyle(.plain)
.navigationTitle("Colors")
}
}
}
struct ColorDetail: View {
var color: Color
var body: some View {
color.navigationTitle(color.description)
}
}
Here is a screencast of what it looks like: https://imgur.com/a/SrJ1IbO. Basically, the bulk of the color label is unclickable, but the edges are clickable. But even when they are clicked, they behave funkily, with multiple links being triggered. This happens with both NavigationStack and NavigationView.
Could someone shed some insight into why this is happening and how to fix it? It works great on < iOS 15
EDIT:
I tried going away from nested lists to use sections instead. But it looks to me like as soon as a list item gets a little complicated, the navigation completely breaks. Here is an example where I add a title to each list item, but each navigation link should still go to its own ColorDetail view. However, the navigation doesn't work as you'd expect:
struct ContentView: View {
var body: some View {
NavigationStack {
List {
ForEach(Range(1...3), id: \.self) { num in
Section {
VStack {
Text("Title: \(num)")
NavigationLink("Mint") { ColorDetail(color: .mint) }
NavigationLink("Pink") { ColorDetail(color: .pink) }
NavigationLink("Teal") { ColorDetail(color: .teal) }
}
}
}
}.listStyle(.plain).navigationTitle("Colors")
}
}
}
List is suitable for re-usable row.
One of your NavigationLink is unique. So please switch to use ScrollView.
Apple is still develop SwiftUI and change it frequently, still not stable as my experience.
You can't nest lists. You got lucky in iOS 15 where you got the desired outcome, but that was a side effect. That doesn't work, as you see. Your better option is to have one List, and then use Section in place of your other Lists. Since you use the .plain listStyle, it will all render as one list.
struct ContentView: View {
var body: some View {
NavigationStack {
List {
Section {
NavigationLink("Mint") { ColorDetail(color: .mint) }
NavigationLink("Pink") { ColorDetail(color: .pink) }
NavigationLink("Teal") { ColorDetail(color: .teal) }
}
Section {
NavigationLink("Red") { ColorDetail(color: .red) }
NavigationLink("Blue") { ColorDetail(color: .blue) }
NavigationLink("Black") { ColorDetail(color: .black) }
}
}.listStyle(.plain)
.navigationTitle("Colors")
}
}
}
You are overlapping the lists on the view, the UI is drawing them on top of each other. In you would like to create sections for your colors with in the NavigationStack, I have created this sample for to provide an idea how you can use NavigationStack with list of items.
import SwiftUI
struct MyColor: Hashable, Identifiable {
var id: UUID
var color: Color
}
struct MySection: Hashable, Identifiable {
var id: UUID
var name: String
var myColors: [MyColor]
static let sections = [
MySection(id: UUID(), name: "First Section", myColors: [MyColor(id: UUID(), color: .mint),
MyColor(id: UUID(), color: .pink),
MyColor(id: UUID(), color: .teal)]),
MySection(id: UUID(), name: "Second Section", myColors: [MyColor(id: UUID(), color: .red),
MyColor(id: UUID(), color: .blue),
MyColor(id: UUID(), color: .black)])
]
}
struct ContentView: View {
let sections: [MySection] = MySection.sections
var body: some View {
NavigationStack {
List {
// For each section
ForEach(sections) { section in
// Create a section
Section(section.name) {
// create navigation link for each color in section
ForEach(section.myColors) { item in
// Navigation link, provide a hashable value
NavigationLink(value: item) {
Text(item.color.description)
}
}
}
}
}
// when you see menu item coming in you go to item detail
.navigationDestination(for: MyColor.self) { item in
ColorDetail(color: item.color)
}
.navigationTitle("Colors")
}
}
}
struct ColorDetail: View {
var color: Color
var body: some View {
color.navigationTitle(color.description)
}
}

Get selected item in SwiftUI list without using a navigation link

I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)

SwiftUI selection in lists not working on reused cells

Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}

Conditionally Text in SwiftUI depending on Array value

I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.