Problems with layout of some rows in SwiftUI list - ios

I have tried to find a similar problem asked before, but failed.
I have a simple view with list. I am using a ForEach to show 10 iterations of the some list item to create a layout before I will add real data to this list. I have a problem with last 2 rows not rendering correctly. But sometimes it’s other row. I have tested on an iPhone too and sometimes it’s one row, sometimes another. The code for the view with list is this:
import SwiftUI
struct LocksView: View {
#State private var locksPaid = 0
var body: some View {
NavigationView {
List {
DateView()
.listRowInsets(EdgeInsets())
Picker(selection: $locksPaid, label: Text("Picker")) {
Text("All").tag(0)
Text("Not paid (2)").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(10)
ForEach(0 ..< 10) {item in
LocksItemView()
}
}
.navigationBarTitle(Text("Locks"))
.navigationBarItems(trailing: EditButton())
}
}
}
The code for list items is this:
import SwiftUI
struct LocksItemView: View {
#State private var paid : Bool = false
var body: some View {
HStack {
Text("L15")
.font(.title)
.fontWeight(.heavy)
.multilineTextAlignment(.center)
.frame(width: 80)
VStack(alignment: .leading) {
Text("nickname")
.fontWeight(.bold)
Text("category")
Text("4 000 THB")
.fontWeight(.bold)
}
Spacer()
Toggle(isOn: self.$paid) {
Text("Paid")
}
.labelsHidden()
}
}
}
Why is toggle broken in some rows on my list? Why it moves to the left side?

It is not last items. If you set in your ForEach 20 instead of 10 and scroll up & down you'll see much more interesting issue.
I assume the reason, actually List bug, is the same as in this topic.
Workaround If it is not critical to you then use ScrollView instead of List, as tested there is no bug for it. Otherwise, file a Radar and wait for fix.

I tried your code at simulators first and had same issue too. But then I remembered, that there are some problems with 13.2 iOS and tried to run it on my device (iPhone 7, iOS 13.1.1) and everything works fine! I think that is the problem in 13.2 iOS, not in the List. There is sample, how I changed code for demonstration that everything is ok:
import SwiftUI
struct LocksView: View {
#State private var locksPaid = 0
var body: some View {
NavigationView {
List {
Picker(selection: $locksPaid, label: Text("Picker")) {
Text("All").tag(0)
Text("Not paid (2)").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(10)
ForEach(0 ..< 200) {item in
LocksItemView(number: item)
}
}
.navigationBarTitle(Text("Locks"))
.navigationBarItems(trailing: EditButton())
}
}
}
struct LocksItemView: View {
#State private var paid : Bool = false
var number: Int
var body: some View {
HStack {
Text("L\(self.number)")
.font(.title)
.fontWeight(.heavy)
.multilineTextAlignment(.center)
.frame(width: 80)
VStack(alignment: .leading) {
Text("nickname")
.fontWeight(.bold)
Text("category")
Text("4 000 THB")
.fontWeight(.bold)
}
Spacer()
Toggle(isOn: self.$paid) {
Text("Paid")
}
.labelsHidden()
}
}
}
and on my phone the result is:
so there are bugs in 13.2 version and I hope Apple will fix them all

Related

Changing icon causes a hitch in SwiftUI animation?

I have a list of tags where when you select them a chip will appear.
When you click on the 'X' on each chip, the tag should slide out with an animation, and the tag in the list will be marked as unselected.
The problem I'm having is when I remove the last chip, the circle/circle-check to the left of the tag does not flow smoothly with the animation.
I believe that is because the icon is changing when selected vs. unselected, since if I keep the icon the same this is not a problem. It's also not a problem if I remove the slide animation on the chips, however I like this animation and would like to keep it.
I'm actually having this issue in a few places in my app that involve animations + changing icons and was wondering if there's a workaround for this?
I've attached a reproducible example below.
import SwiftUI
struct ContentView: View {
var body: some View {
Icon_Animation()
}
}
struct Icon_Animation: View {
//All tags
var testTags: [Tag] =
[Tag("tag1"),
Tag("tag2"),
Tag("tag3")]
//Only tags that have been selected
#State var selectedTags = [Tag]()
var body: some View {
ScrollView{
//Hstack of the tags that have been selected
HStack{
ForEach(selectedTags){ tag in
HStack(spacing: 0){
Button{
//Clicking on the X will remove from selectedTags array, and then make that tag's isSelected = false
withAnimation(.easeOut) {
if let index = selectedTags.firstIndex(where: {$0.name == tag.name}){
selectedTags.remove(at: index)
}
}
//PROBLEM: even though this statemnt isn't in the withAnimation block, it causes a weird behavior with the circle/check-circle icon
//If I remove the withAnimation statement from the above block, it works fine. However, I would like to keep the slide animation on the chips.
tag.isSelected = false
}label:{
Image(systemName: "x.circle.fill")
.font(.subheadline)
.padding(.horizontal, 6)
}
Image(systemName: "number")
.font(.footnote.weight(.bold))
.padding(.trailing, 2)
Text("\(tag.name)")
.font(.footnote)
}
.padding(.trailing, 20)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.6), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
.transition(.slide)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
//List of tags where you can select each tag to create a chip
ForEach(testTags){ tag in
TagView(tag: tag)
.onTapGesture {
tag.isSelected.toggle()
if(tag.isSelected == true){
selectedTags.append(tag)
}
}
}
.padding()
}
.padding()
}
}
class Tag: Identifiable, ObservableObject {
var id = UUID()
#Published var name: String
#Published var isSelected = false
init(_ name: String){
self.name = name
}
}
struct TagView: View {
#ObservedObject var tag: Tag = Tag("test")
var body: some View {
ZStack{
//Overlay for when tag is selected
Rectangle()
.fill(Color.purple.opacity(0.6))
.edgesIgnoringSafeArea(.all)
.cornerRadius(5)
.opacity(tag.isSelected ? 1 : 0)
HStack(spacing: 8){
//PROBLEM!!: I want to use a different icon based on whether tag isSelected, but it's causing a hitch in the animation when switching
if(tag.isSelected){
Image(systemName: "checkmark.circle.fill")
.font(.title2.weight(.light))
}else{
Image(systemName: "circle")
.font(.title2.weight(.light))
}
Image(systemName: "number")
.font(.body.weight(.bold))
Text(tag.name)
.font(.headline)
.fontWeight(.bold)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
I think this is a common problem that comes down to the a parent view receiving two different child views from an if statement, and as they are two different views, with different ids, SwiftUI doesn't know how to animate between them.
The trick is is to use just one child view with variable content. You should be able to achieve this by replacing the if...else... in your TagView that generates the image with a ternary inside of the initialiser:
Image(systemName: tag.isSelected ? "checkmark.circle.fill" : "circle" )
.font(.title2.weight(.light))

Removing top and bottom lines on a list on iPad

I have an app for iPad that uses a navigation list, and another list to display items. The items list always displays a top and bottom lines, even when no items are there.
I have tried .listStyle(.plain) and .listRowSeparator(.hidden) but nothing helps.
Anyone has any idea how to remove those lines? I can't find anything on Apple's documentation or here at SO.
Thanks.
Code:
struct ContentView: View {
#State var selection: Set<Int> = [0]
var body: some View {
NavigationView {
List(selection: self.$selection) {
NavigationLink(destination: ItemsList(wantStarred: 0)) {
Label("All Items", systemImage: "chart.bar.doc.horizontal").tag(0)
}
NavigationLink(destination: ItemsList(wantStarred: 1)) {
Label("Favorites", systemImage: "star.fill")
}
}
.listStyle(SidebarListStyle())
.frame(minWidth: 150, idealWidth: 150, maxHeight: .infinity)
ItemsList(wantStarred: 0)
}
}
}
struct ItemsList: View {
#State var wantStarred: Int
var body: some View {
List {
//...
}
.listStyle(.plain)
.listRowSeparator(.hidden)
.overlay {
if items.count == 0 {
Text("Create a new item.").fontWeight(.light)
}
}
}
}
The listRowSeparator modifier should be inside list
List {
Text("")
.listRowSeparator(.hidden) // << like here !!
}
.listStyle(.plain)
Tested with Xcode 13.4 / iOS 15.5

EmptyView is not showing in iOS 15, Xcode 13

I have serious problem. My Xcode version is 13, iOS version is 15.
import SwiftUI
struct ContentView: View {
#State var isGo: Bool = false
var body: some View {
ZStack {
Button(action: {
self.isGo = true
}, label: {
Text("Go EmptyView")
})
EmptyView()
.background(Color.green)
.frame(width: 100, height: 100)
.sheet(isPresented: $isGo, onDismiss: nil, content: {
PopupView()
})
}
}
}
struct PopupView: View {
var body: some View {
Rectangle()
.fill(Color.green)
}
}
Above code is not working. But, Previous Xcode version or Previous iOS version is that code is working. Is that iOS bug? Is there anything solution?
You're very close, not sure why nobody has offered help. Your code does work but in theory isn't correct, you needed to move the where you called .sheet to outside your ZStack and it works. But here's a better approach without all the useless code.
struct ContentView: View {
#State var showemptyview: Bool = false
var body: some View {
Button("Go EmptyView") {
showemptyview.toggle()
}
.sheet(isPresented: $showemptyview) {
EmptyView()
.background(Color.green)
}
}
}
Finally in a last version you can use several sheets directly for main view and it works. You don't need to create separate EmptyView() with sheet
YourMainView()
.sheet(item: $viewModel) { item in
// some logic here
}
.sheet(isPresented: $onther_viewModel.showView, content: {
SomeAnotherView(viewModel: viewModel.getVM())
})
.sheet(isPresented: $onther_viewModel2.showView, content: {
SomeView(viewModel: viewModel.getVM())
})
Replace "EmptyView()" with "Spacer().frame(height:0)" did work for me on iOS15 and iOS14

Why SwiftUI context menu show all row view in preview?

I have a complex view in List row:
var body: some View {
VStack {
VStack {
FullWidthImageView(ad)
HStack {
Text("\(self.price) \(self.ad.currency!)")
.font(.headline)
Spacer()
SwiftUI.Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
Where FullWidthImageView is view with defined contexMenu modifier.
But when I long-press on an image I see not the only image in preview, but all row view.
There is no other contextMenu on any element.
How to make a preview in context with image only?
UPD. Here is a simple code illustrating the problem
We don't have any idea why in your case it doesn't work, until we see your FullWidthImageView and how you construct the context menu. Asperi's answer is working example, and it is correctly done! But did it really explain your trouble?
The trouble is that while applying .contextMenu modifier to only some part of your View (as in your example) we have to be careful.
Let see some example.
import SwiftUI
struct FullWidthImageView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
Image(systemName: model.toggle ? "pencil.and.outline" : "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
}.contextMenu(ContextMenu {
Button(action: {
self.model.toggle.toggle()
}) {
HStack {
Text("toggle image to?")
Image(systemName: model.toggle ? "trash" : "pencil.and.outline")
}
}
Button("No") {}
})
}
}
class Model:ObservableObject {
#Published var toggle = false
}
let modelStore = Model()
struct ContentView: View {
#ObservedObject var model = modelStore
var body: some View {
VStack {
FullWidthImageView()
Text("Long press the image to change it").bold()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
while running, the "context menu" modified View seems to be "static"!
Yes, on long press, you see the trash image, even though it is updated properly while you dismiss the context view. On every long press you see trash only!
How to make it dynamic? I need that the image will be the same, as on my "main View!
Here we have .id modifier. Let see the difference!
First we have to update our model
class Model:ObservableObject {
#Published var toggle = false
var id: UUID {
UUID()
}
}
and next our View
FullWidthImageView().id(model.id)
Now it works as we expected.
For another example, where "standard" state / binding simply doesn't work check SwiftUI hierarchical Picker with dynamic data crashes
UPDATE
As a temporary workaround you can mimic List by ScrollView
import SwiftUI
struct Row: View {
let i:Int
var body: some View {
VStack {
Image(systemName: "trash")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu {
Button("A") {}
Button("B") {}
})
Text("I don’t want to show in preview because I don’t have context menu modifire").bold()
}.padding()
}
}
struct ContentView: View {
var body: some View {
VStack {
ScrollView {
ForEach(0 ..< 20) { (i) in
VStack {
Divider()
Row(i: i)
}
}
}
}
}
}
It is not optimal, but in your case it should work
Here is a code (simulated possible your scenario) that works, ie. only image is shown for context menu preview (tested with Xcode 11.3+).
struct FullWidthImageView: View {
var body: some View {
Image("auto")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200)
.contextMenu(ContextMenu() {
Button("Ok") {}
})
}
}
struct TestContextMenu: View {
var body: some View {
VStack {
VStack {
FullWidthImageView()
HStack {
Text("100 $")
.font(.headline)
Spacer()
Image(systemName: "heart")
}
.padding([.top, .leading, .trailing], 10.0)
}
}
}
}
It's buried in the replies here, but the key discovery is that List is changing the behavior of .contextMenu -- it creates "blocks" that pop up with the menu instead of attaching the menu to the element specified. Switching out List for ScrollView fixes the issue.

NavigationLink inside List applies to HStack instead of each element

I'm trying to follow Composing Complex Interfaces guide on SwiftUI and having issues getting NavigationLink to work properly on iOS 13 beta 3 and now beta 4.
If you just download the project files and try running it, click on any of the Lake images - nothing will happen. However if you click on the header "Lakes" it'll start opening every single lake one after another which is not a behaviour anyone would expect.
Seems like NavigationLink is broken in "complex" interfaces. Is there a workaround?
I've tried making it less complex and removing HStack of List helps to get NavigationLinks somewhat to work but then I can't build the full interface like in example.
Relevant parts of the code:
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
NavigationLink(destination: LandmarkList()) {
Text("See All")
}
}
.navigationBarTitle(Text("Featured"))
.navigationBarItems(trailing: profileButton)
.sheet(isPresented: $showingProfile) {
ProfileHost()
}
}
}
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items, id: \.name) { landmark in
NavigationLink(
destination: LandmarkDetail(
landmark: landmark
)
) {
CategoryItem(landmark: landmark)
}
}
}
}
.frame(height: 185)
}
}
}
struct CategoryItem: View {
var landmark: Landmark
var body: some View {
VStack(alignment: .leading) {
landmark
.image(forSize: 155)
.renderingMode(.original)
.cornerRadius(5)
Text(landmark.name)
.foregroundColor(.primary)
.font(.caption)
}
.padding(.leading, 15)
}
}
It looks like there is a bug with NavigationLink instances that aren't directly contained in a List. If you replace the outermost List with a ScrollView and a VStack then the inner NavigationLinks work correctly:
i.e.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: true) {
VStack {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
NavigationLink(destination: LandmarkList()) {
Text("See All")
}
}
}
.navigationBarTitle(Text("Featured"))
}
}

Resources