skip to Main Content

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?

enter image description here

2

Answers


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

    Screenshot


    EDIT Following from your comment, longer labels could be addressed in various ways:

    • The font can be made smaller by applying a .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),
        // ...
    
    • The label can be made to truncate by drawing the text in a rectangle of fixed size:
    ctx.draw(
        Text("A longer label"),
        in: CGRect(x: 0, y: 20, width: 60, height: 20)
    )
    
    • If the rectangle for the text has sufficient height, labels will automatically wrap onto multiple lines, if necessary. Line alignment can be controlled by applying .multilineTextAlignment to the result:
    advancedSettingsIcon
        .foregroundStyle(.white)
        .multilineTextAlignment(.center)
    
    • As it turns out, other modifiers like .lineLimit and .minimumScaleFactor also work. So this opens the way for…

    A more generic solution

    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:

    • The font size for the label uses .body as default, but it is scaled automatically to fit.
    • A line limit of 2 is applied, with a minimum scale factor of 0.7.
    • If the label wraps to multiple lines, the symbol is made smaller.

    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 60×60. 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)
    

    Screenshot

    Login or Signup to reply.
  2. For each item add a vertical padding of 5.
    .padding(.vertical, 5)

    This will show the image and label, just like in Mail app.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search