问题
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
enumis intended to be used both inside and outside of the class, with a generic type being required in order to use it. - The
enumdoes not make use of, and therefore has no dependency on, the genericContenttype. - With a good enough name, the intended use of the
enumwould 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
contentmember to be ofAnyViewtype, - Make your
initfunctions 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