如何在 SwiftUI 中从 UIViewRepresentable 外部调用 UIView 上的方法?



我希望能够将对UIViewRespresentable上方法的引用(或者可能是Coordinator(传递给父View。我能想到的唯一方法是在父View结构上创建一个字段,其中包含一个类,然后将其传递给子级,子级充当此行为的委托。但这似乎很啰嗦。

这里的用例是能够从标准 SwiftUIButton调用方法,该方法将缩放隐藏在树中其他位置UIViewRepresentableMKMapView中的当前位置。我不希望当前位置成为Binding,因为我希望此操作是一次性的,而不是经常反映在 UI 中。

TL;DR是否有一种标准方法可以让父级在 SwiftUI 中获取对子项的引用,至少在UIViewRepresentable秒内是这样?(我知道这在大多数情况下可能是不可取的,并且在很大程度上与 SwiftUI 模式背道而驰(。

我自己也为此苦苦挣扎,以下是使用CombinePassthroughSubject的工作原理:

struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
@State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: (value)")

// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}

我相信有更好的方法,包括使用CombinePassthroughSubject.(但我从来没有把它弄好。也就是说,如果你愿意"反对 SwiftUI 模式",为什么不直接发送一个Notification呢?(这就是我所做的。

在我的模型中:

extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
@Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}

在我的UIKit中表示:

NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)

把它放在你的initviewDidLoad,取决于什么样的可代表。

同样,这不是"纯粹的"SwiftUI 或 Combine,但比我更好的人可能会给你 - 你听起来愿意得到一些有用的东西。相信我,这行得通。

编辑:值得注意的是,您不需要在可表示对象中执行任何额外的操作 - 这仅在模型和UIKit视图或视图控制器之间工作。

我来这里是为了找到一个更好的答案,然后是我自己想出来的答案,但也许这确实对某人有帮助?

尽管如此,它还是相当冗长,并且不太像是最惯用的解决方案,所以可能不完全是问题作者想要的。但它确实避免了污染全局命名空间,并允许同步(和重复(执行和返回值,这与之前发布的基于NotificationCenter的解决方案不同。

考虑的另一种方法是改用@StateObject,但我目前需要支持 iOS 13,而目前尚不可用。

游览:我为什么要这样?我需要处理触摸事件,但我正在与 SwiftUI 世界中定义的另一个手势竞争,该手势将优先于我的UITapGestureRecognizer。(我希望这可以通过为下面的简短示例代码提供一些上下文来提供帮助。

所以我想出的是:

  • 添加可选的闭包作为状态(在FooView上(,
  • 将其作为绑定传递到可表示的视图中(BarViewRepresentable(,
  • makeUIView填写这个,
  • 这样就可以在BazUIView上调用方法。

注意:它会导致不希望/不必要的后续更新BarViewRepresentable,因为设置绑定会更改可表示视图的状态,但这在我的情况下并不是真正的问题。

struct FooView: View {
@State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
@Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView {  /*...*/ }

这就是我成功完成它的方式。我将UIView创建为 SwiftUIView中的常量属性。然后,我将该引用传递到我在makeUI方法中使用的UIViewRepresentable初始值设定项中。然后,我可以从 SwiftUIView调用任何方法(可能是在UIView的扩展中((例如,点击按钮时(。在代码中是这样的:

SwiftUI 视图

struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use

var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}

UIView

struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView

func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}

func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}

Swift 5Combine

调整 KBog 的解决方案,因为它对我不起作用。

import SwiftUI
import MapKit
import Combine
struct MapView: UIViewRepresentable {
enum Action {
case zoomToCurrentLocation
}

let actionPublisher: any Publisher<Action, Never>

func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
context.coordinator.actionSubscriber = actionPublisher.sink { action in
switch action {
case .zoomToCurrentLocation:
// call the required methods for setting current location
}
}
return mapView
}

func updateUIView(_ uiView: UIViewType, context: Context) {

}

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

class Coordinator: NSObject {
var actionSubscriber: any Cancellable?
}
}

然后在其他任何地方使用它,就像这样

struct SomeOtherView: View {
let actionPublisher = PassthroughSubject<MapView.Action, Never>()

var body: some View {
VStack {
MapView(actionPublisher: actionPublisher)
Button("Zoom") {
actionPublisher.send(.zoomToCurrentLocation)
}
}
}
}

最新更新