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

时间:2019-09-21 10:14:14

标签: swift mkmapview swiftui

我希望能够将对UIViewRespresentable(或者也许是Coordinator)上方法的引用传递给父级View。我能想到的唯一方法是在父View结构上创建一个带有一个类的字段,然后将该类传递给子对象,该子对象充当此行为的委托。但这看起来很冗长。

这里的用例是能够从标准SwiftUI Button调用一个方法,该方法将缩放MKMapView中当前位置,而UIViewRepresentable埋在其他地方的Binding中那个树。我不希望当前位置为UIViewRepresentable,因为我希望此操作为一次性操作且不会在用户界面中不断反映。

TL; DR 是否有一种标准的方法,至少在ctrl+c s中,使父母在SwiftUI中获得对孩子的引用? (我知道这在大多数情况下可能是不希望的,并且很大程度上违反了SwiftUI模式。)

3 个答案:

答案 0 :(得分:1)

我自己也为此苦苦挣扎,以下是使用 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() {
    // ...
  }
}

答案 1 :(得分:0)

我确信还有更好的方法,包括使用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视图或视图控制器之间。

答案 2 :(得分:0)

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

尽管如此,它仍然很冗长,并且感觉不太像是最惯用的解决方案,因此可能与问题的作者所寻找的不完全相同。但是,它确实避免了污染全局名称空间,并允许同步(和重复)执行并返回值,这与之前发布的基于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 {  /*...*/ }