Row separators not showing when section separators hidden - swiftui

When using a combination of listRowSeparator and listSectionSeparator being hidden, list row separators don't show up when appending items to the end of the list.
Here is a simple sample that reproduces the problem:
import SwiftUI
struct Item: Identifiable {
let id: Int
let text: String
}
struct ContentView: View {
#State var items: [Item] = []
var body: some View {
VStack {
Button {
items.append(Item(id: items.count, text: "\(items.count)"))
} label: {
Text("Append")
}
List {
Section {
ForEach(items) { item in
Text(item.text)
.listRowSeparator(.visible)
.listRowSeparatorTint(Color.red)
}
}
// Comment out this for row separators to work
.listSectionSeparator(.hidden, edges: .all)
}
.listStyle(.plain)
}
}
}
This is happening on iOS 15, only when using plain list style, and only when appending to the end of the list.
Am I doing something wrong, or is there a workaround for this problem?

Looks like a SwiftUI bug, the below combination can be considered as workaround
Section {
ForEach(items) { item in
Text(item.text)
.listRowSeparatorTint(Color.red)
.listRowSeparator(.visible, edges: .bottom)
}
}
.listSectionSeparator(.hidden, edges: .top)
, which gives

Related

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)
}
}

listRowBackground removes selection style

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:

Picker scroll through one element horizontally

I have a SwiftUI Picker in which an item is selected. The text of one element can be large, so I used UIKit UIPickerView and set the manual height to 100, but at some point it became not enough. Is it possible to make scrolling horizontal for each element?
I want to get something like this:
Picker("Items", select: self._selectItem) {
ForEach(self.items, id: \.self) { item in
ScrollView(.horizontal, showsIndicators: false) {
Text(item.description)
}
.tag(item)
}
}
That should work fine. If you only want to scroll one item, you would have to insert a check of the item length.
let items = [
"A long item text.",
"And a even longer item text which is really going further.",
"Another item text which is really going further."
]
struct ContentView: View {
#State private var select = ""
var body: some View {
VStack {
Text("Make your selection!")
List(items, id: \.self) { item in
ScrollView(.horizontal) {
Text(item)
}
.listRowBackground(item == select ? Color.red : Color.white)
.onTapGesture {
select = item
}
}
}
}
}
I would strongly suggest to separate the picking from the text display and scrolling, e.g. like this:
struct ContentView: View {
#State private var select = items[0]
var body: some View {
VStack {
Text("Make your selection!")
Picker("Items", selection: $select) {
ForEach(items) { item in
Text(item.title)
.tag(item)
}
}
ScrollView {
Text(select.text)
}
.padding()
.frame(height: 200)
}
}
}

SwiftUI List messed up after delete action on iOS 15

It seems that there is a problem in SwiftUI with List and deleting items. The items in the list and data get out of sync.
This is the code sample that reproduces the problem:
import SwiftUI
struct ContentView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
}
}
.animation(.easeInOut, value: popupShown)
}
}
struct MainListView: View {
#State var texts = (0...10).map(String.init)
func delete(at positions: IndexSet) {
positions.forEach { texts.remove(at: $0) }
}
var body: some View {
List {
ForEach(texts, id: \.self) { Text($0) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
If you perform a delete action on the first row and scroll to the last row, the data and list contents are not in sync anymore.
This is only happening when animation is attached to it. Removing .animation(.easeInOut, value: popupShown) workarounds the issue.
This code sample works as expected on iOS 14 and doesn't work on iOS 15.
Is there a workaround for this problem other then removing animation?
It isn't the animation(). The clue was seeing It appears that having the .animation outside of the conditional causes the problem. Moving it to the view itself corrected it to some extent. However, there is a problem with this ForEach construct: ForEach(texts, id: \.self). As soon as you start deleting elements of your array, the UI gets confused as to what to show where. You should ALWAYS use an Identifiable element in a ForEach. See the example code below:
struct ListDeleteView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
.animation(.easeInOut, value: popupShown)
}
}
}
}
struct MainListView: View {
#State var texts = (0...10).map({ TextMessage(message: $0.description) })
func delete(at positions: IndexSet) {
texts.remove(atOffsets: positions)
}
var body: some View {
List {
ForEach(texts) { Text($0.message) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
struct TextMessage: Identifiable {
let id = UUID()
let message: String
}

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.