Skip to content

accessibility_label_for_image: False positive for SwiftUI Label icon closures #6420

@samhenrigold

Description

@samhenrigold

New Issue Checklist

  • I've Updated SwiftLint to the latest version.
  • I've searched for existing GitHub issues.

Bug Description

The accessibility_label_for_image rule incorrectly flags images within SwiftUI Label components as violations, even though Label automatically provides accessibility context through its text content.

In SwiftUI, when using Label with the expanded closure syntax, the text block provides the accessibility label for the entire component, including the icon. The image is decorative within this context and doesn't need a separate accessibility label.

Example that triggers false positive:

Label {
    Text("Connected")
} icon: { // Violation here
    Image(systemName: "checkmark.circle.fill")
}

This is semantically equivalent to:

Label("Connected", systemImage: "checkmark.circle.fill")  // No violation

Both produce identical accessibility behavior; VoiceOver reads "Connected" for the entire label. The rule should recognize that images within Label icon closures are inherently labeled.

Current behavior: Violation triggered
Expected behavior: No violation - the image has accessibility context from the Label's text

Environment

  • SwiftLint version: 0.62.2
  • Xcode version: Xcode 26.2; Build version 17C52
  • Installation method: [SPM]
  • Configuration file:
# From https://github.com/Lickability/swift-best-practices/blob/main/.swiftlint.yml
# You can reference documentation on rules here https://realm.github.io/SwiftLint/rule-directory.html

disabled_rules: # rule identifiers to exclude from running
  - attributes
  - closure_end_indentation
  - cyclomatic_complexity
  - discouraged_object_literal
  - discouraged_optional_collection
  - explicit_acl
  - explicit_enum_raw_value
  - explicit_top_level_acl
  - explicit_type_interface
  - extension_access_modifier
  - file_length
  - file_types_order
  - function_body_length
  - function_parameter_count
  - generic_type_name
  - identifier_name
  - implicit_return
  - large_tuple
  - let_var_whitespace
  - line_length
  - literal_expression_end_indentation
  - multiline_arguments
  - multiline_arguments_brackets
  - multiline_literal_brackets
  - multiline_parameters_brackets
  - nesting
  - nimble_operator
  - no_extension_access_modifier
  - no_grouping_extension
  - nslocalizedstring_require_bundle
  - number_separator
  - object_literal
  - prefixed_toplevel_constant
  - quick_discouraged_call
  - quick_discouraged_focused_test
  - quick_discouraged_pending_test
  - raw_value_for_camel_cased_codable_enum
  - reduce_into
  - redundant_self_in_closure
  - redundant_string_enum_value
  - required_deinit
  - sorted_enum_cases
  - sorted_imports
  - strict_fileprivate
  - superfluous_else
  - switch_case_on_newline
  - todo
  - trailing_closure
  - trailing_whitespace
  - type_body_length
  - type_contents_order
  - type_name
  - unneeded_parentheses_in_closure_argument
  - vertical_whitespace_between_cases
  - vertical_whitespace_opening_braces
  - xct_specific_matcher
  
opt_in_rules: # some rules are only opt-in
  - accessibility_label_for_image
  - accessibility_trait_for_button
  - array_init
  - block_based_kvo
  - class_delegate_protocol
  - closing_brace
  - closure_parameter_position
  - closure_spacing
  - collection_alignment
  - colon
  - comma
  - compiler_protocol_init
  - conditional_returns_on_newline
  - contains_over_filter_count
  - contains_over_filter_is_empty
  - contains_over_first_not_nil
  - contains_over_range_nil_comparison
  - control_statement
  - convenience_type
  - custom_todo
  - direct_return
  - discarded_notification_center_observer
  - discouraged_direct_init
  - discouraged_optional_boolean
  - duplicate_enum_cases
  - duplicate_imports
  - dynamic_inline
  - empty_collection_literal
  - empty_count
  - empty_enum_arguments
  - empty_parameters
  - empty_parentheses_with_trailing_closure
  - empty_string
  - empty_xctest_method
  - explicit_init
  - fallthrough
  - fatal_error_message
  - file_header
  - first_where
  - flatmap_over_map_reduce
  - for_where
  - force_cast
  - force_try
  - force_unwrapping
  - function_name_whitespace
  - identical_operands
  - implicit_getter
  - implicit_optional_initialization
  - implicitly_unwrapped_optional
  - is_disjoint
  - joined_default_parameter
  - last_where
  - leading_whitespace
  - legacy_cggeometry_functions
  - legacy_constant
  - legacy_constructor
  - legacy_hashing
  - legacy_multiple
  - legacy_nsgeometry_functions
  - local_doc_comment
  - lower_acl_than_parent
  - mark
  - multiline_parameters
  - multiple_closures_with_trailing_closure
  - no_space_in_method_call
  - non_overridable_class_declaration
  - notification_center_detachment
  - nslocalizedstring_key
  - opening_brace
  - operator_usage_whitespace
  - overridden_super_call
  - override_in_extension
  - pattern_matching_keywords
  - period_spacing
  - prefer_self_in_static_references
  - prefer_self_type_over_type_of_self
  - prefer_zero_over_explicit_init
  - private_action
  - private_outlet
  - private_over_fileprivate
  - private_subject
  - private_swiftui_state
  - private_unit_test
  - prohibited_super_call
  - protocol_property_accessors_order
  - reduce_boolean
  - redundant_discardable_let
  - redundant_nil_coalescing
  - redundant_objc_attribute
  - redundant_void_return
  - required_enum_case
  - return_arrow_whitespace
  - return_value_from_void_function
  - shorthand_operator
  - single_test_class
  - sorted_first_last
  - statement_position
  - superfluous_disable_command
  - switch_case_alignment
  - syntactic_sugar
  - test_case_accessibility
  - trailing_comma
  - trailing_newline
  - trailing_semicolon
  - unavailable_function
  - unneeded_break_in_switch
  - unneeded_override
  - unneeded_synthesized_initializer
  - unused_closure_parameter
  - unused_control_flow_label
  - unused_enumerated
  - unused_optional_binding
  - unused_setter_value
  - valid_ibinspectable
  - vertical_parameter_alignment
  - vertical_parameter_alignment_on_call
  - vertical_whitespace
  - vertical_whitespace_closing_braces
  - void_return
  - weak_delegate
  - xctfail_message
  - yoda_condition

excluded: # paths to ignore during linting. Takes precedence over `included`.
  - Carthage
  - Pods

file_header:
  required_pattern: |
                    \/\/
                    \/\/  .*?
                    \/\/  .*?
                    \/\/
                    \/\/  Created by .*? on \d{1,2}\/\d{1,2}\/(\d{2}|\d{4})\.
                    \/\/  Copyright (©|\(c\)) \d{4} .*?\. All rights reserved\.
                    \/\/

custom_rules:
  force_https:
    name: "Force HTTPS over HTTP"
    regex: "((?i)http(?!s))"
    match_kinds: string
    message: "HTTPS should be favored over HTTP"
    severity: warning
  custom_todo:
    name: "TODO Violation"
    regex: "(TODO).(?!.*(https&)).(?!.*issue)"
    match_kinds: comment
    message: "TODOs must include a link to the issue."
    severity: warning

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementIdeas for improvements of existing features and rules.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions