“Generic parameter could not be inferred” in SwiftUI UIViewRepresentable

二次信任 提交于 2020-05-13 14:43:25

问题


I've got the following code, which makes it possible to use the UIKit's UIScrollView in my SwiftUI code. It can be pasted in a new SwiftUI project.

struct LegacyScrollView<Content: View>: UIViewRepresentable {
    enum Action {
        case idle
        case offset(x: CGFloat, y: CGFloat, animated: Bool)
    }

    @Binding var action: Action
    let content: Content

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIScrollView {
        let hosting = UIHostingController(rootView: self.content)
        hosting.view.translatesAutoresizingMaskIntoConstraints = false

        let uiScrollView = UIScrollView()
        uiScrollView.addSubview(hosting.view)

        let constraints = [
            hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
            hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
            hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
        ]
        uiScrollView.addConstraints(constraints)

        return uiScrollView
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        switch self.action {
        case .offset(let x, let y, let animated):
            uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
            DispatchQueue.main.async {
                self.action = .idle
            }
        default:
            break
        }
    }

    class Coordinator: NSObject {
        let legacyScrollView: LegacyScrollView

        init(_ legacyScrollView: LegacyScrollView) {
            self.legacyScrollView = legacyScrollView
        }
    }

    init(@ViewBuilder content: () -> Content) {
        self._action = Binding.constant(Action.idle)
        self.content = content()
    }

    init(action: Binding<Action>, @ViewBuilder content: () -> Content) {
        self._action = action
        self.content = content()
    }
}

struct ContentView: View {
    @State private var action = LegacyScrollView.Action.idle

    var body: some View {
        VStack(spacing: 0) {
            LegacyScrollView(action: self.$action) {
                ForEach(0 ..< 40) { _ in
                    Text("Hello, World!")
                }
            }
            .padding(20)
            .background(Color.gray)
            Spacer()
            Button("Set offset") {
                self.action = LegacyScrollView.Action.offset(x: 0, y: 200, animated: true)
            }.padding()
        }
    }
}

The code above will give Generic parameter 'Content' could not be inferred on the first line of the ContentView. I've tried to change the line to:

@State private var action = LegacyScrollView<AnyView>.Action.idle

but that will give another error. It works when I place the enum Action outside the struct LegacyScrollView. But in my opinion, that's a rather inelegant scoping of this enum. How can I solve the error message?


回答1:


Here is possible approach that allows usage of provided ContentView as-is.

Just change the direction of... instead of making entire type generic, which is actually not needed in this case, just make a generic only initialisation, like below.

Also it actually makes clear that Action is Content-independent, that is really correct.

Tested & works with Xcode 11.2 / iOS 13.2 (w/o no changes in ContentView)

struct LegacyScrollView: UIViewRepresentable {
    enum Action {
        case idle
        case offset(x: CGFloat, y: CGFloat, animated: Bool)
    }

    @Binding var action: Action
    private let uiScrollView: UIScrollView

    init<Content: View>(content: Content) {
        let hosting = UIHostingController(rootView: content)
        hosting.view.translatesAutoresizingMaskIntoConstraints = false

        uiScrollView = UIScrollView()
        uiScrollView.addSubview(hosting.view)

        let constraints = [
            hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
            hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
            hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
        ]
        uiScrollView.addConstraints(constraints)

        self._action = Binding.constant(Action.idle)
    }

    init<Content: View>(@ViewBuilder content: () -> Content) {
        self.init(content: content())
    }

    init<Content: View>(action: Binding<Action>, @ViewBuilder content: () -> Content) {
        self.init(content: content())
        self._action = action
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIScrollView {
        return uiScrollView
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        switch self.action {
        case .offset(let x, let y, let animated):
            uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
            DispatchQueue.main.async {
                self.action = .idle
            }
        default:
            break
        }
    }

    class Coordinator: NSObject {
        let legacyScrollView: LegacyScrollView

        init(_ legacyScrollView: LegacyScrollView) {
            self.legacyScrollView = legacyScrollView
        }
    }

}



回答2:


I disagree with your assertion that the enum should be nested inside the class for the following reasons:

  • The enum is intended to be used both inside and outside of the class, with a generic type being required in order to use it.
  • The enum does not make use of, and therefore has no dependency on, the generic Content type.
  • With a good enough name, the intended use of the enum would be obvious.

If you really want to nest the enum definition, I would suggest the following:

  • Drop the generic type requirement on the class definition,
  • Convert your content member to be of AnyView type,
  • Make your init functions generic and store the return values of the given view builders into type-erased views, like so:
init<Content: View>(@ViewBuilder content: () -> Content) {
    self._action = Binding.constant(Action.idle)
    self.content = AnyView(content())
}

init<Content: View>(action: Binding<Action>, @ViewBuilder content: () -> Content) {
    self._action = action
    self.content = AnyView(content())
}

Of course, with this approach, you will:

  • Lose the type information of the underlying content view.
  • Possibly incur a greater runtime cost with type-erased views.

So it depends what you value more in this case... Ahhh, tradeoffs...



来源:https://stackoverflow.com/questions/60165059/generic-parameter-could-not-be-inferred-in-swiftui-uiviewrepresentable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!