Skip to content

Crash: ForEach(0...num-1) produces invalid Range when num == 0 (ConfettiSwiftUI.swift:168) #71

Description

@mikeini

Summary

ConfettiContainer.body crashes with Swift runtime failure: Range requires lowerBound <= upperBound whenever a ConfettiCannon is mounted with num: 0. The crash fires during view-body evaluation, so it's a hard SIGTRAP that kills the app — not catchable.

The offending line:

// Sources/ConfettiSwiftUI.swift, line 168
ForEach(0...confettiConfig.num-1, id:\.self) { _ in
    ConfettiView(confettiConfig: confettiConfig)
}

When num == 0, the closed range expands to 0...-1, which is invalid. Closed ranges require lowerBound <= upperBound.

Why this matters in practice

The natural pattern for honoring @Environment(\.accessibilityReduceMotion) is to set num to 0 (or some small number) when the system flag is on:

@Environment(\.accessibilityReduceMotion) var reduceMotion

SomeView()
    .confettiCannon(
        trigger: $trigger,
        num: reduceMotion ? 0 : 60,
        // ...
    )

This is a documented, common SwiftUI accessibility pattern (Apple's HIG explicitly recommends suppressing decorative motion when Reduce Motion is on). Anyone using ConfettiSwiftUI in a shippable, accessibility-aware app will hit this.

We caught it via a TestFlight crash report on iOS 26.4.2 from a tester who had Reduce Motion enabled. The app crashed every time they performed the action that triggered the cannon. Workaround applied on our side: we pinned num to a non-zero value and gated Reduce Motion at the trigger site instead — but library users shouldn't have to know about this footgun.

Reproduction

Minimum repro (~10 lines):

import SwiftUI
import ConfettiSwiftUI

struct CrashRepro: View {
    @State private var trigger = 0
    var body: some View {
        Button("Fire") { trigger += 1 }
            .confettiCannon(
                trigger: $trigger,
                num: 0   // ← crashes the app on first render
            )
    }
}

Run on simulator or device. App crashes on initial render of the view tree, before trigger is even bumped.

Crash signature

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Termination Reason: SIGNAL 5 Trace/BPT trap: 5

Thread 0 Crashed:
0   MyApp  Swift runtime failure: Range requires lowerBound <= upperBound + 0
1   MyApp  closure #1 in ConfettiContainer.body.getter + 352 (ConfettiSwiftUI.swift:168)
2   MyApp  specialized closure #1 in ZStack.init(alignment:content:) + 8
3   MyApp  ConfettiContainer.body.getter + 44 (ConfettiSwiftUI.swift:166)

Suggested fix

Two equivalent one-line patches — either works.

Option A — half-open range (preferred, no edge-case math):

ForEach(0..<confettiConfig.num, id:\.self) { _ in
    ConfettiView(confettiConfig: confettiConfig)
}

0..<0 is a valid empty range. ForEach with 0 iterations is a no-op.

Option B — guard the existing closed range:

if confettiConfig.num > 0 {
    ForEach(0...confettiConfig.num-1, id:\.self) { _ in
        ConfettiView(confettiConfig: confettiConfig)
    }
}

Option A is cleaner and matches the idiom used elsewhere in SwiftUI for "render N copies".

Environment

  • ConfettiSwiftUI: 3.0.0 (9ae5bc2)
  • Xcode: 26.4.1
  • iOS: 26.4.2 (also reproduces on iOS 18 simulator)
  • Swift: 5/6 strict concurrency

Happy to send a PR if it'd help.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions