List clipping issue (bug with SwiftUI?) - swift

I'm having this weird issue with the List in macOS Montery Beta 3, where some of the list's items are being clipped, and you have to scroll up and back down to unclip it.
Example:
https://imgur.com/a/P0TUt85
Reproduction
Create a new blank SwiftUI macOS project
Paste the following code:
struct Bla: Identifiable {
var text: String
var subtext: String
var id = UUID()
}
struct ContentView: View {
#State var data = (0..<100).map { Bla(text: "Text: \($0)", subtext: "Subtext: \($0)")}
var body: some View {
NavigationView {
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}
.toolbar {
Button(action: {
var offset = 0
for i in 0..<data.count {
if i % 2 == 0 {
continue
}
data.remove(at: i - offset)
offset += 1
}
}) {
Image(systemName: "plus")
}
}
}
}
}
Run the app, and scroll the list to the middle.
Press the plus button on the toolbar. This simply removes half of the items.
You should see the clipping issue. Scrolling up and back down fixes it.
Is this something with my code, or with SwiftUI's List?

After adding the .listStyle I dont see the clipping issue anymore, try this:
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}.listStyle(SidebarListStyle())

Related

LazyVGrid code behaves differently in different Xcode versions?

I was just watching Standord's CS193p (2021 editions)
The instructor shows this as an example of LazyVGrid:
As you see the spacing is equal in every direction.
Now here is how I see it with the exact same code in Xcode 14.2 (it's the same with 9 cards too):
I double checked and the code is exactly the same for both of us!
import SwiftUI
struct ContentView: View {
var emojis = ["✈️", "🚀", "🚑", "🚕", "🚁", "🚜", "🚂", "🛻", "🏍️"]
#State var emojiCount = 8
var body: some View {
VStack {
LazyVGrid(columns: [GridItem(),GridItem(),GridItem(),GridItem()]) {
ForEach(emojis[0..<emojiCount], id: \.self) { emoji in
CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)
}
}
.foregroundColor(.red)
Spacer()
HStack {
remove
Spacer()
add
}
.font(.largeTitle)
.padding(.horizontal)
}
.padding(.horizontal)
}
var remove: some View {
Button {
if emojiCount > 1 {
emojiCount -= 1
}
} label: {
Image(systemName: "minus.circle")
}
}
var add: some View {
Button {
if emojiCount < emojis.count {
emojiCount += 1
}
} label: {
Image(systemName: "plus.circle")
}
}
}
struct CardView: View {
var content: String
#State var isFaceUp: Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 25)
if isFaceUp {
shape.fill(.white)
shape.stroke(lineWidth: 3)
Text(content).font(.largeTitle)
} else {
shape.fill(.red)
}
}
.onTapGesture {
isFaceUp = !isFaceUp
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
ContentView()
.preferredColorScheme(.dark)
}
}
How is that possible? if Apple just changes the behavior like that, what happens to old apps when running in newer devices?
Also, how can I set the vertical spacing to be exactly the same as horizonal spacing? (I am aware I can set the vertical spacing like this: LazyVGrid(columns:..., spacing: 10), but that is a fixed amount and not always equal to horizontal spacing depending on the cols)
Timed link to the video on Youtube
Edit:
Funny enough, removing the Text() inside card or adding .padding() to it fixes the issue, but I still don't understand why, it is more confusing when you watch the video:
Link to Video

TabView with .tabViewStyle(.page) does not dynamically update [duplicate]

I came across a weird Issue in SwiftUI.
I created a simple View that only holds a Button
and a TabView that uses the PageViewStyle. It seems that the TabView does not update it's content
correctly depending on the State of the Variable.
It seems that the content gets updated somehow but the View wont be updated how I would expect
Here is the Code of my View:
struct ContentView: View {
#State var numberOfPages: Int = 0
#State var selectedIndex = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
self.selectedIndex = 0
})
TabView(selection: $selectedIndex){
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").background(Color.red)
}
}
.frame(height: 300)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
}.background(Color.blue)
}
}
This is how the result looks after tapping the label several Times.
The Initial State is no 0 Pages. After you tap i would expect that the content of the
TabView changes so all Pages will be scrollable and visible but just the page indicator updates it State for some reason.
TabView expects to have container of pages, but you included only one HStack (with own dynamic content), moreover chaining number of pages you have to reset tab view, so here is a fix.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var numberOfPages: Int = 0
var body: some View {
VStack {
Text("Tap Me").onTapGesture(count: 1, perform: {
self.numberOfPages = [2,5,10,15].randomElement()!
})
if self.numberOfPages != 0 {
TabView {
ForEach(0..<numberOfPages, id: \.self) { index in
Text("\(index)").frame(width: 300).background(Color.red)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.frame(height: 300)
.id(numberOfPages) // << here !!
}
}
}
}

NavigationLink inside a List and VStack does not work after the first pop

I am trying to use the new Pull to Refresh feature in the latest version of SWiftUI which requires a List. Enclosing the VStack in a List causes the NavigationLink to work only once. Below is a simple version of the code without the Pull To Refresh part.
There is a question that was asked 68144891 on stackoverflow and there was a refrence to a known issue link which takes you to a page not found (https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-15-beta-release-notes)
Steps o reproduce
Tap "Press Me 1" or one of the items
Tap "Show Details"
Tap Back at the top
Tap "Press Me" again will not navigate to the next screen. A grey screen blocks when you tap
The app works without the VStack
struct ContentView: View {
var body: some View {
NavigationView {
List {
VStack { // commenting VStack works
Text("Options").font(.largeTitle).bold()
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}
struct SubView: View {
var counter: Int
#State private var showDetails = false
var body: some View {
VStack(alignment: .leading) {
Button("Show details") {
showDetails.toggle()
}
if showDetails {
Text("Clicked")
.font(.largeTitle)
}
}
}
}
Any help appreciated
Thanks much!
... follow-up to my comment
I assume you wanted this
struct ContentView: View {
var body: some View {
NavigationView {
List {
Section(Text("Options").font(.largeTitle).bold()) {
ForEach(1..<5, id:\.self) { counter in
NavigationLink(destination: SubView(counter: counter)) {
Text("Press Me \(counter)").font(.headline)
}
.buttonStyle(PlainButtonStyle())
}
}
}.listStyle(.grouped)
}
}
}

SwiftUI: HStack in ForEach in VStack makes multi line text overlap

I have a Text() view inside a HStack inside of a ForEach inside of a VStack. The text can be a string of any length, and I have no control of that is put inside of it. The problem is that when you run the program, the views in the VStack overlap resulting in this jumbled mess
What I want to do is have a view that resizes its height based on the height of the multi line text view, so that the views never overlap, and always displays the entirety of the string.
Here is some code that generates the view in question:
struct ScrollingChatView: View {
#State var model: WatchModel
#State var messages: [DisplayableMessage] = []
var body: some View {
ScrollView {
if (!messages.isEmpty) {
LazyVStack {
ForEach(messages, id: \.sortTimestamp) { message in
CompactChatView(message: message)
}
}.padding()
} else {
Text("Getting Chat...").padding()
}
}.onReceive(model.chatDriver.publisher) { m in
self.messages = m
}
}
}
struct CompactChatView: View {
#State var message: DisplayableMessage
#State var stringMessage: String? = nil
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(message.displayAuthor)
.lineLimit(1)
.layoutPriority(1)
Group {
Text(getEmojiText(message))
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Text(message.displayTimestamp)
.font(.subheadline)
.foregroundColor(Color.gray)
.layoutPriority(1)
}.padding(.all, 6.0)
}
}
func getEmojiText(_ item: DisplayableMessage) -> String {
var fullMessage: String = ""
for m in item.displayMessage {
switch m {
case .text(let s):
fullMessage += s
case .emote(_):
print()
}
}
return fullMessage
}
}
I've tried removing .fixedSize(horizontal: false, vertical: true) from the text view, but it only makes the text cut off after one line, which is not what I want.
If you need more context, the entire project in located at: https://github.com/LiveTL/apple. We're looking at code in the macOS folder.
You may find it useful to instead use a List() with a trailing closure like this...
List(itemList) { item in
Text(item)
}
This should prevent the issue you are running into when trying to display messages. For more information on lists check this out: https://developer.apple.com/documentation/swiftui/list.
You can see an example of this at 25:11 here: https://developer.apple.com/videos/play/wwdc2019/216/

List scroll freeze on catalyst NavigationView

I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.
Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.
The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.
Are there any known workarounds ?
This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.
Here is the code:
ContentView.swift
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
var body: some View {
NavigationView {
List() {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailListView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...4).map({NamedItem(name: "\($0)")})
var body: some View {
VStack {
List {
Text(item.name)
NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
listItem(" LinkedView", "Item")
.foregroundColor(Color.blue)
}
ForEach(sections) { section in
sectionDetails(section)
}
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
TestListApp.swift
import SwiftUI
#main
struct TestListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: NewView()) {
Text("Navigation Link to new view")
}
}
.onAppear {
UITableView.appearance().showsVerticalScrollIndicator = false
}
}
}
}
OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665
The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.
I ran in to a couple of other gotcha's with this arrangement.
Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.
Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.
Here is the code, no more list freezes !
import SwiftUI
import NavigationStack
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
init() {
// Ensure toolbar is allways opaque
UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
var body: some View {
NavigationStackView {
DetailListView(item: item)
}
}
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
var linked = NamedItem(name: "LinkedView")
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
List {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name)
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: {
self.isSelected = linked.id
})
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.
The following code makes use of PR#44 of swiftui-navigation-stack
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let depth: Int
let id = UUID()
init(name:String, depth: Int = 0) {
self.name = name
self.depth = depth
}
var linked: NamedItem {
return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
}
}
// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func listTitle(_ title: String) -> some View {
self.preference(key: ListTitleKey.self, value: title)
}
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
#ObservedObject var navigationStack = NavigationStack()
#State var toolbarTitle: String = ""
var body: some View {
List {
NavigationStackView(noGroup: true, navigationStack: navigationStack) {
DetailListView(item: item, linked: item.linked)
.listTitle(item.name)
}
}
.listStyle(PlainListStyle())
.animation(nil)
// Updated title
.onPreferenceChange(ListTitleKey.self) { value in
toolbarTitle = value
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
.toolbar(content: {
ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
.opacity(self.navigationStack.depth > 0 ? 1.0 : 0.0)
}
})
}
}
struct DetailListView: View {
var item: NamedItem
var linked: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked, linked: linked.linked)
.listTitle(linked.name)
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func buttonAction() {
self.isSelected = linked.id
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: buttonAction)
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}