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