Closure with typed throws stored as a SwiftUI View property crashes on iOS 17

Not sure if this is a SwiftUI bug or a Swift bug so I'm posting this also into the Swift forum.

I've encountered an issue where storing a throws(PermissionError) closure as a property inside a SwiftUI View causes a runtime crash with EXC_BAD_ACCESS on iOS 17, while it works correctly on iOS 18.

Here’s an example of the affected code:

enum PermissionError: Error {
  case denied
}

struct PermissionCheckedView<AllowedContent: View, DeniedContent: View>: View {
  var protectedView: () throws(PermissionError) -> AllowedContent
  var deniedView: (PermissionError) -> DeniedContent

  init(
    @ViewBuilder protectedView: @escaping () throws(PermissionError) -> AllowedContent,
    @ViewBuilder deniedView: @escaping (PermissionError) -> DeniedContent
  ) {
    self.protectedView = protectedView
    self.deniedView = deniedView
  }

  public var body: some View {
    switch Result(catching: protectedView) {
    case .success(let content): content
    case .failure(let error): deniedView(error)
    }
  }
}

@main
struct TestApp: App {
  var body: some Scene {
    WindowGroup {
      PermissionCheckedView {
        
      } deniedView: { _ in
        
      }
    }
  }
}

Here's the stack trace (not sure how to get the txt version so posting image).
Since I noticed some func like swift_getTypeByMangledNameInContextImpl in the stack trace I thought that this could be a Swift bug

If I use var protectedView: () throws -> AllowedContent without typed throws it works.

1 Like

Typed throws is new in the Swift 6 compiler and iOS 18, so I'm guessing the demangling used by SwiftUI on iOS 17 doesn't know how to handle it. I think typed throws is back deployable, but only if you don't rely on implementation details like SwiftUI. I'm not sure if the language can fix this, but hopefully there's something Apple can do.

In general I can use typed throws on iOS 17 as long as I don't store closures in views. I suppose there's nothing we can do if Apple doesn't tackle this

1 Like

Never and any Error get special treatment here. They work, but anything else will trap.

import SwiftUI
@main struct App: SwiftUI.App { // Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
  let closure: () throws(TypedError) -> Void = { }
  var body: some Scene { WindowGroup { } }
}
struct TypedError: Error { }

They cannot be used generically, though. This also traps, and is a reduction of the original post.

import SwiftUI
@main struct App: SwiftUI.App {
  var body: some Scene {
    WindowGroup(makeContent: ContentView<Never>.init)
  }
}
struct ContentView<TypedError: Error>: View {
  let closure: () throws(TypedError) -> Void = { }
  var body: some View { EmptyView() }
}

Let me know if the following doesn't work for your situation. Until you can require iOS 18+, you'll just need a function instead of a real initializer.

ErrorRecoveringView.`init` { () throws(PermissionError) in
  throw .denied
} recovery: { error in
  Text("\(error)")
}

ErrorRecoveringView.`init` { } recovery: { _ in }
import SwiftUI

public struct ErrorRecoveringView<
  Success: View, Error: Swift.Error, Recovery: View
>: ErrorRecoveringViewProtocol {
  public var success: () throws(Error) -> Success
  public var recovery: (Error) -> Recovery

  @available(iOS 18, *) init(
    @ViewBuilder success: @escaping () throws(Error) -> Success,
    @ViewBuilder recovery: @escaping (Error) -> Recovery
  ) {
    self.success = success
    self.recovery = recovery
  }
}

@available(iOS, deprecated: 18, message: "No longer necessary.")
public protocol ErrorRecoveringViewProtocol: View {
  associatedtype Success: View
  associatedtype Error: Swift.Error
  associatedtype Recovery: View

  var success: () throws(Error) -> Success { get }
  var recovery: (Error) -> Recovery { get }
}

@available(iOS, deprecated: 18, message: "Add this to `ErrorRecoveringView`.")
public extension ErrorRecoveringViewProtocol {
  @ViewBuilder var body: some View {
    switch Result(catching: success) {
    case .success(let success): success
    case .failure(let error): recovery(error)
    }
  }
}

public extension ErrorRecoveringView {
  @available(
    iOS, deprecated: 18,
    message: "Switch to the real initializer."
  )
  @ViewBuilder static func `init`(
    @ViewBuilder success: @escaping () throws(Error) -> Success,
    @ViewBuilder recovery: @escaping (Error) -> Recovery
  ) -> some View {
    if #available(iOS 18, *) {
      Self(success: success, recovery: recovery)
    } else {
      _iOS17(success: success) { recovery($0 as! Error) }
    }
  }

  private struct _iOS17: ErrorRecoveringViewProtocol {
    var success: () throws -> Success
    var recovery: (any Swift.Error) -> Recovery
  }
}

(Alternatively, if you need to pass along the error type, you can just use a single view type, without the problematic signature.)

// ErrorRecoveringView<EmptyView, PermissionError, EmptyView>
ErrorRecoveringView { () throws(PermissionError) in } recovery: { _ in }
ErrorRecoveringView<_, PermissionError, _> { } recovery: { _ in }

// ErrorRecoveringView<EmptyView, any Error, EmptyView>
ErrorRecoveringView { throw PermissionError.denied } recovery: { _ in }
struct ErrorRecoveringView<
  Success: View, Error: Swift.Error, Recovery: View
>: View {
  @available(iOS, deprecated: 18, message: "Store `success` directly instead.")
  var _success: OldSchoolTypedGetThrows<Error, Success>

  var success: () throws(Error) -> Success { _success.callAsFunction }
  // Cannot use a `set`. Using typed throws generally crashes the compiler.

  var recovery: (Error) -> Recovery

  init(
    @ViewBuilder success: @escaping () throws(Error) -> Success,
    @ViewBuilder recovery: @escaping (Error) -> Recovery
  ) {
    _success = .init(success)
    self.recovery = recovery
  }

  var body: some View {
    switch Result(catching: success) {
    case .success(let success): success
    case .failure(let error): recovery(error)
    }
  }
}
struct OldSchoolTypedGetThrows<
  // `repeat each Input` should be here but parameter packs generally crash the compiler.
  Error: Swift.Error, Output
> {
  init(_ get: @escaping () throws(Error) -> Output) { self.get = get }
  let get: () throws -> Output
  func callAsFunction() throws(Error) -> Output {
    do { return try get() }
    catch { throw error as! Error }
  }
}

@Danny I can actually change my code to something like this and perform the do catch in the init to avoid storing it as a closure:

struct PermissionCheckedView<AllowedContent: View, DeniedContent: View>: View {
  private enum Content {
    case allowed(AllowedContent)
    case denied(DeniedContent)
  }

  private var content: Content

  init(
    @ViewBuilder protectedView: () throws(PermissionError) -> AllowedContent,
    @ViewBuilder deniedView: (PermissionError) -> DeniedContent
  ) {
    do throws(PermissionError) {
      content = try .allowed(protectedView())
    } catch {
      content = .denied(deniedView(error))
    }
  }

  public var body: some View {
    switch content {
    case .allowed(let content): content
    case .denied(let content): content
    }
  }
}

But my question was more generic as I couldn't understand the reason of the crash

1 Like

SwiftUI (am grouping SwiftUI, SwiftUICore and AttributeGraph together here) relies heavily on runtime reflection internally.

My guess is that SwiftUI's internal runtime reflection code was written relative to the Swift runtime ABI interface before typed throws was implemented. Typed throws the language feature backdeploys, yes, and implementation is (I could be wrong) purely additive from what I understand.

That being said, if you write code that relies on an exhaustive understanding of all possible runtime structures (I imagine SwiftUI's internals do this), you're bound to either make assumptions or implement code-paths that exit-early with respect to new, 'foreign' metadata.

I'm guessing that somewhere in the implementation there are areas that trip up on the runtime changes (even if they're purely additive in the technical sense) introduced with the implementation of typed throws as a new language feature.

1 Like

If you're not going to defer the error handling, then you might as well just store body directly.

struct NondeferringErrorRecoveringView<
  Success: View, Error: Swift.Error, Recovery: View
>: NondeferringErrorRecoveringViewProtocol {
  init(
    @ViewBuilder success: () throws(Error) -> Success,
    @ViewBuilder recovery: (Error) -> Recovery
  ) {
    body = Self.either(success: success, recovery: recovery)
  }

  var body: Body
}

/// Necessary to be able to refer to the return type of `either`.
private protocol NondeferringErrorRecoveringViewProtocol: View where Body == Either {
  associatedtype Success: View
  associatedtype Error: Swift.Error
  associatedtype Recovery: View
  associatedtype Either: View

  static func either(
    success: () throws(Error) -> Success,
    recovery: (Error) -> Recovery
  ) -> Either
}

extension NondeferringErrorRecoveringViewProtocol {
  @ViewBuilder static func either(
    @ViewBuilder success: () throws(Error) -> Success,
    @ViewBuilder recovery: (Error) -> Recovery
  ) -> some View {
    switch Result(catching: success) {
    case .success(let success): success
    case .failure(let error): recovery(error)
    }
  }
}

You can still change the naming from the generic form if you think that's worth bothering with:

struct PermissionCheckedView<AllowedContent: View, DeniedContent: View>: View {
  var body: NondeferringErrorRecoveringView<AllowedContent, PermissionError, DeniedContent>

  init(
    @ViewBuilder protectedView: () throws(PermissionError) -> AllowedContent,
    @ViewBuilder deniedView: (PermissionError) -> DeniedContent
  ) {
    body = .init(success: protectedView, recovery: deniedView)
  }
}