I am running my app on iOS 17 and have added swipe actions on list row. I wish to show the image of the action and text below it, but I noticed that only image gets displayed.
Code:
import SwiftUI
import Foundation
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
List {
Section {
HStack {
Text("Hello World").font(.body)
Spacer()
Divider().frame(width: 3.0)
.overlay(Color.blue).padding(.trailing, -50.0).padding([.top, .bottom], -1.5)
}
.padding([.top, .bottom], 1.5)
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
print("Deleting row")
} label: {
/// Note: Want to show Image and text below it.
VStack(spacing: 2.0) {
Image(systemName: "trash")
Text("Delete").font(.caption2)
}
/// Note: Using label as well doesn't show both text and icon. It only shows the icon.
// Label("Delete", systemImage: "trash")
}
// TODO: Add more swipe action buttons
}
}
}
.listStyle(.insetGrouped)
}
}
}
}
How do I show image and text for swipe action?
One way to force an image to be shown together with a label is to build an image yourself that combines the two. This can be done by creating an Image
with drawing instructions, see init(size:label:opaque:colorMode:renderer:)
For example:
private var deleteIcon: Image {
Image(
size: CGSize(width: 60, height: 40),
label: Text("Delete")
) { ctx in
ctx.draw(
Image(systemName: "trash"),
at: CGPoint(x: 30, y: 0),
anchor: .top
)
ctx.draw(
Text("Delete"),
at: CGPoint(x: 30, y: 20),
anchor: .top
)
}
}
You can then use this as the label for the swipe action:
Button(role: .destructive) {
print("Deleting row")
} label: {
deleteIcon
.foregroundStyle(.white)
}
EDIT Following from your comment, longer labels could be addressed in various ways:
.font
modifier to the result:deleteIcon
.foregroundStyle(.white)
.font(.caption)
However, this also impacts the size of the symbol. So it works better to set the font when rendering the label:
ctx.draw(
Text("Check in").font(.caption),
// ...
in
a rectangle of fixed size:ctx.draw(
Text("A longer label"),
in: CGRect(x: 0, y: 20, width: 60, height: 20)
)
.multilineTextAlignment
to the result:advancedSettingsIcon
.foregroundStyle(.white)
.multilineTextAlignment(.center)
.lineLimit
and .minimumScaleFactor
also work. So this opens the way for...Ideally, the generation of this type of composite icon should be able to handle longer labels automatically. This is possible by resolving the text and the image inside the function and then examining their sizes.
So here is a more general-purpose solution for generating a swipe icon. It includes the following logic:
.body
as default, but it is scaled automatically to fit.I found that if the result has square proportions, it is able to fill the full height of the list row. So the function below generates an image of size 60x60. This gets scaled-to-fit, which probably means it gets shrunk a bit when actually used:
private func swipeIcon(label: String, symbolName: String) -> some View {
let w: CGFloat = 60
let h = w
let size = CGSize(width: w, height: h)
let text = Text(LocalizedStringKey(label))
let symbol = Image(systemName: symbolName)
return Image(size: size, label: text) { ctx in
let resolvedText = ctx.resolve(text)
let textSize = resolvedText.measure(in: CGSize(width: w, height: h * 0.6))
let resolvedSymbol = ctx.resolve(symbol)
let symbolSize = resolvedSymbol.size
let heightForSymbol: CGFloat = min(h * 0.35, (h * 0.9) - textSize.height)
let widthForSymbol = (heightForSymbol / symbolSize.height) * symbolSize.width
let xSymbol = (w - widthForSymbol) / 2
let ySymbol = max(h * 0.05, heightForSymbol - (textSize.height * 0.6))
let yText = ySymbol + heightForSymbol + max(0, ((h * 0.8) - heightForSymbol - textSize.height) / 2)
let xText = (w - textSize.width) / 2
ctx.draw(
resolvedSymbol,
in: CGRect(x: xSymbol, y: ySymbol, width: widthForSymbol, height: heightForSymbol)
)
ctx.draw(
resolvedText,
in: CGRect(x: xText, y: yText, width: textSize.width, height: textSize.height)
)
}
.foregroundStyle(.white)
.font(.body)
.lineLimit(2)
.lineSpacing(-2)
.minimumScaleFactor(0.7)
.multilineTextAlignment(.center)
}
Here's how it can be used:
Button(role: .destructive) {
print("Deleting row")
} label: {
swipeIcon(label: "Delete", symbolName: "trash")
}
Button(role: .none) {
print("Advanced settings")
} label: {
swipeIcon(label: "Advanced settings", symbolName: "gearshape")
}
.tint(.orange)