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