skip to Main Content

I’d like to know if there is a good existing way to wrap a UIKit inside SwiftUI when the initializer of the UIKit component can throw an error.

As a context, I’m using this article to create a generic wrapper like this:

struct Wrap<Wrapped: UIView>: UIViewRepresentable {
    typealias Updater = (Wrapped, Context) -> Void

    var makeView: () -> Wrapped
    var update: (Wrapped, Context) -> Void

    init(_ makeView: @escaping @autoclosure () -> Wrapped,
         updater update: @escaping Updater) {
        self.makeView = makeView
        self.update = update
    }

    func makeUIView(context: Context) -> Wrapped {
        makeView()
    }

    func updateUIView(_ view: Wrapped, context: Context) {
        update(view, context)
    }
}

extension Wrap {
    init(_ makeView: @escaping @autoclosure () -> Wrapped,
         updater update: @escaping (Wrapped) -> Void) {
        self.makeView = makeView
        self.update = { view, _ in update(view) }
    }

    init(_ makeView: @escaping @autoclosure () -> Wrapped) {
        self.makeView = makeView
        self.update = { _, _ in }
    }
}

Now, the problem is that, if the UIKit class has an initializer that can throw an error, attempting to use it in a SwiftUI screen will not compile:

final class MyView: UIView {

    init(text: String) throws {
        throw NSError(domain: NSOSStatusErrorDomain, code: 20)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Compile error on Xcode

My understanding is that SwiftUI can’t handle this kind of situation normally, so what would be the best way to handle this kind of situation in a "SwiftUI" way?

2

Answers


  1. Chosen as BEST ANSWER

    As pointed out in some comments, probably the best way to handle such a thing is to have the wrapper handle the failure with a "fallback" view. I changed the wrapper struct so that it can accept a Result and it works as expected:

    struct Wrap<Wrapped: UIView>: UIViewRepresentable {
        typealias Updater = (Wrapped, Context) -> Void
    
        var makeView: () -> Result<Wrapped, Error>
        var update: (Wrapped, Context) -> Void
        var fallbackView: Wrapped
    
        init(
            _ makeView: @escaping @autoclosure () -> Result<Wrapped, Error>,
            updater update: @escaping Updater,
            fallbackView: Wrapped
        ) {
            self.makeView = makeView
            self.update = update
            self.fallbackView = fallbackView
        }
    
        func makeUIView(context: Context) -> Wrapped {
            switch makeView() {
            case .success(let view):
                return view
            case.failure:
                return fallbackView
            }
        }
    
        func updateUIView(_ view: Wrapped, context: Context) {
            update(view, context)
        }
    
    }
    

    This allows me to create the view with a normal try:

    var body: some View {
      ZStack {
        Wrap(
          Result {
            try MyView(text: "test")
          },
          fallbackView: ErrorView(text: "Render failed")
        )
      }
    }
    

  2. Throwing SwiftUI views are not supported.

    The question is, what is supposed to happen if the initializer throws an error?

    If you want to show an empty view if the initializer fails, you could just return EmptyView.

    func getMyViewWrapped() -> some View {
        do {
            let myView = try MyView(text: "test")
            return AnyView(Wrap(myView))
        } catch {
            return AnyView(EmptyView())
        }
    }
    
    // use in View
    self.getMyViewWrapped() // not throwing.
    

    (Wrapped in AnyView to avoid the return type mismatch error)

    Or if you want a one-liner for your view:

    Wrap((try? MyView(text: "test")) ?? EmptyView())
    

    should work as well (perhaps you also have to wrap it in AnyView though).

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