Home > Software design >  Swiftui listRowInsets makes unexpected changes when adding to an empty array
Swiftui listRowInsets makes unexpected changes when adding to an empty array

Time:09-21

I have recreated a minimal reproducible example here. I am using Xcode 13 beta 5.

In my project, I am rendering a List of scorecards with a List and ForEach loop. I am using swipeactions to enable deleting of the scorecards. After I delete the scorecards, then add a scorecard, the list gets rendered with extra padding or listRowInset on the leading edge.

Also, in the ContentView, I use if/else statements to render a message when the scorecards array is empty. I'm thinking this is where the bug occurs but don't really know how to work around it.

Here is what the list should look like ...

Proper rendering of list

and below is the unexpected, buggy rendering...

Improper rendering after deleting the scorecard and adding a new one

To recreate the bug, first delete the scorecard by swiping from the trailing edge, then click add scorecard at the top and you will see the extra white space on the leading edge of the scorecard. I will attach the code below ... Thank you in advance for the help!

import SwiftUI

struct ContentView: View {

@State var scorecards = [
    Scorecard(date: Date()),
    
]

var sortedScorecards: [Scorecard] {
    get {
        scorecards.sorted {
            $0.date > $1.date
        }
    }
    set {
        scorecards = newValue
    }
}
var body: some View {
    VStack {
        
        Button {
            scorecards = [
                Scorecard(date: Date()),
                
            ]
            
        } label: {
            Text("Add Scorecard")
        }
        
        
        if scorecards.count < 1 {
            
            Spacer()
            
            HStack{
                Spacer()
                Text("No previous scorecards. Start a scorecard!")
                    .font(.headline)
                    .padding(20)
                    .multilineTextAlignment(.center)
                Spacer()
            }
            Spacer()
        } else {
            List {
                ForEach(sortedScorecards, id:\.self) { scorecard in
                    if #available(iOS 15.0, *) {
                        
                        Button {
                            
                        } label: {
                            Card(scorecard: scorecard)
                            
                        }.buttonStyle(.plain)
                            .listRowInsets(.init(top:7.5,
                                                 leading:0,
                                                 bottom:7.5,
                                                 trailing:0))
                            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                                Button("Delete Scorecard", role: .destructive) {
                                    deleteScorecard(scorecard: scorecard)
                                }
                            }
                        
                        
                    } else {
                        // Fallback on earlier versions
                    }
                }
            }
            .onAppear {
                UITableView.appearance().backgroundColor = .black
            }
        }
    }
    
}

    func deleteScorecard(scorecard:Scorecard) {
        scorecards = scorecards.filter{$0.id != scorecard.id}
    }
}



struct Card: View {
    
    var scorecard:Scorecard
    
    func scorecardDate(date:Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter.string(from: date)
    }
    
    var body:some View {
        
        ZStack {
            Rectangle()
                .foregroundColor(Color.blue)
                .frame(height: 25)
            
            Text(scorecardDate(date:scorecard.date))
            
        }
    }
}
    
    
struct Scorecard:Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    var id = UUID().uuidString
    var date:Date
}

CodePudding user response:

It does seem to be related to the if/else statement. When the List is dropped from the view hierarchy and then added back in, the insets seem to get messed up.

The inset amount looked familiar to me -- like the amount it should be inset if there were a delete button on the leading side. So, on a hunch I added an explicit call to set the edit mode to .inactive. That seems to work.

At the top of your view, add this:

@Environment(\.editMode) private var editMode

Then, in your deleteScorecard(scorecard:Scorecard):

func deleteScorecard(scorecard:Scorecard) {
    scorecards = scorecards.filter{$0.id != scorecard.id}
    editMode?.wrappedValue = .inactive //<-- here
}

This seems to explicitly turn off the editing mode and then avoids the bug when the List is added back into the view hierarchy.


Update -- secondary solution.

Here's another solution I came up with, since my code works with your sample code, but not with your app. In this version, the List is included in the view hierarchy the whole time, but the frame is set to a height of 0 if there aren't any items. There may be side effects because of your background color that you're setting, which is one of the reasons I didn't include this initially, but since the original solution wasn't ideal, now it's worth including:

var body: some View {
    VStack {
        
        Button {
            scorecards = [
                Scorecard(date: Date()),
                
            ]
            
        } label: {
            Text("Add Scorecard")
        }
        
        
        if scorecards.count < 1 {
            
            VStack {
                Spacer()
                
                HStack{
                    Spacer()
                    Text("No previous scorecards. Start a scorecard!")
                        .font(.headline)
                        .padding(20)
                        .multilineTextAlignment(.center)
                    Spacer()
                }
                Spacer()
            }
            
        }
        
        List {
            ForEach(sortedScorecards, id:\.self) { scorecard in
                if #available(iOS 15.0, *) {
                    
                    Button {
                        
                    } label: {
                        Card(scorecard: scorecard)
                        
                    }.buttonStyle(.plain)
                        .listRowInsets(.init(top:7.5,
                                             leading:0,
                                             bottom:7.5,
                                             trailing:0))
                        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                            Button("Delete Scorecard", role: .destructive) {
                                deleteScorecard(scorecard: scorecard)
                            }
                        }
                    
                    
                } else {
                    // Fallback on earlier versions
                }
            }
        }
        .frame(maxHeight: scorecards.count == 0 ? 0 : .infinity)
        .onAppear {
            UITableView.appearance().backgroundColor = .black
        }
    }
    
}
  • Related