Отправить действие tapAction из SwiftUI для кнопки в функцию UIView

Я пытаюсь найти способ вызвать действие, которое вызовет функцию в моем UIView когда кнопка нажата внутри swiftUI,

Вот мои настройки:

foo()(UIView) нужно бежать, когда Button(SwiftUI) получает постукивать

Мой пользовательский класс UIView, использующий фреймворки AVFoundation

class SomeView: UIView {

    func foo() {}
}

Чтобы использовать мой UIView внутри swiftUI, я должен обернуть его в UIViewRepresentable

struct SomeViewRepresentable: UIViewRepresentable {

    func makeUIView(context: Context) -> CaptureView {
        SomeView()
    }

    func updateUIView(_ uiView: CaptureView, context: Context) {        
    }
}

SwiftUI Посмотреть, где находится мой UIView()

struct ContentView : View {

    var body: some View {
        VStack(alignment: .center, spacing: 24) {
            SomeViewRepresentable()
                .background(Color.gray)
            HStack {
                Button(action: {
                    print("SwiftUI: Button tapped")
                   // Call func in SomeView()
                }) {
                    Text("Tap Here")
                }
            }
        }
    }
}

6 ответов

Решение

Вы можете хранить экземпляр вашего кастома UIView в вашей представительной структуре (SomeViewRepresentable здесь) и вызвать его методы на действиях крана:

struct SomeViewRepresentable: UIViewRepresentable {

  let someView = SomeView() // add this instance

  func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
    someView
  }

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

  }

  func callFoo() {
    someView.foo()
  }
}

И ваше тело будет выглядеть так:

  let someView = SomeViewRepresentable()

  var body: some View {
    VStack(alignment: .center, spacing: 24) {
      someView
        .background(Color.gray)
      HStack {
        Button(action: {
          print("SwiftUI: Button tapped")
          // Call func in SomeView()
          self.someView.callFoo()
        }) {
          Text("Tap Here")
        }
      }
    }
  }

Чтобы проверить это, я добавил принт в foo() метод:

class SomeView: UIView {

  func foo() {
    print("foo called!")
  }
}

Теперь нажатие на вашу кнопку сработает foo() и заявление печати будет показано.

Решение M Reza работает для простых ситуаций, однако, если ваше родительское представление SwiftUI имеет изменения состояния, каждый раз, когда оно обновляется, это приведет к тому, что ваш UIViewRepresentable будет создавать новый экземпляр UIView из-за этого: let someView = SomeView() // add this instance. Следовательно someView.foo() вызывает действие в предыдущем экземпляре SomeViewвы создали, который уже устарел на момент обновления, поэтому вы можете не видеть никаких обновлений вашего UIViewRepresentable в родительском представлении. См. https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678

Лучше всего избегать создания этого экземпляра UIView и обращения к нему при вызове его функции.

Моя адаптация к решению M Reza будет вызывать функцию косвенно через изменение состояния родительского представления, которое запускает updateUIView:

  var body: some View {
    @State var buttonPressed: Bool = false
    VStack(alignment: .center, spacing: 24) {

      //pass in the @State variable which triggers actions in updateUIVIew
      SomeViewRepresentable(buttonPressed: $buttonPressed)
        .background(Color.gray)
      HStack {
        Button(action: {
          buttonPressed = true
        }) {
          Text("Tap Here")
        }
      }
    }
  }

struct SomeViewRepresentable: UIViewRepresentable {
  @Binding var buttonPressed: Bool 

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

  //called every time buttonPressed is updated
  func updateUIView(_ uiView: SomeView, context: Context) {
    if buttonPressed {
        //called on that instance of SomeView that you see in the parent view
        uiView.foo()
        buttonPressed = false
    }
  }
}

Вот еще одно решение! Связь между супервизором и использованием замыкания:

      struct ContentView: View {
    
    /// This closure will be initialized in our subview
    @State var closure: (() -> Void)?
    
    var body: some View {
        SomeViewRepresentable(closure: $closure)
        Button("Tap here!") {
            closure?()
        }
    }
}

Затем инициализируйте замыкание вUIViewRepresentable:

      struct SomeViewRepresentable: UIViewRepresentable {
    
    // This is the same closure that our superview will call
    @Binding var closure: (() -> Void)?
    
    func makeUIView(context: Context) -> UIView {
        
        let uiView = UIView()
        
        // Since `closure` is part of our state, we can only set it on the main thread
        DispatchQueue.main.async {
            closure = {
                // Perform some action on our UIView
            }
        }
        return uiView
    }
}

@ada10086 дает отличный ответ. Просто подумал, что я предоставлю альтернативное решение, которое было бы более удобным, если вы хотите отправить много разных действий на свойUIView.

Ключ в том, чтобы использоватьотCombineдля отправки сообщений из супервизора вUIViewRepresentable.

      struct ContentView: View {
    
    /// This will act as a messenger to our subview
    private var messenger = PassthroughSubject<String, Never>()
    
    var body: some View {
        SomeViewRepresentable(messenger: messenger)  // Pass the messenger to our subview
        Button("Tap here!") {
            // Send a message
            messenger.send("button-tapped")
        }
    }
}

Затем мы следим заPassthroughSubjectв нашем подвиде:

      struct SomeViewRepresentable: UIViewRepresentable {
    
    let messenger = PassthroughSubject<String, Never>()
    @State private var subscriptions: Set<AnyCancellable> = []
    
    func makeUIView(context: Context) -> UIView {
        
        let uiView = UIView()
        
        // This must be run on the main thread
        DispatchQueue.main.async {
            // Subscribe to messages
            messenger.sink { message in
                switch message {
                    // Call funcs in `uiView` depending on which message we received
                }
            }
            .store(in: &subscriptions)
        }
        return uiView
    }
}

Этот подход удобен тем, что вы можете отправить любую строку в подпредставление, чтобы вы могли разработать целую схему обмена сообщениями.

Вот еще один способ сделать это с помощью связующего класса.

      //SwiftUI
struct SomeView: View{
  var bridge: BridgeStuff?

  var body: some View{
    Button("Click Me"){
      bridge?.yo()
    }
  }
}

//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
  var yo:() -> Void = {}
}

class YourViewController: UIViewController{

  override func viewDidLoad(){
    let bridge = BridgeStuff()
    let view = UIHostingController(rootView: SomeView(bridge: bridge))
    bridge.yo = { [weak self] in
      print("Yo")
      self?.howdy()
    }
  }

  func howdy(){
    print("Howdy")
  }
}

Мое решение - создать посредникаSomeViewModelобъект. Объект хранит необязательное замыкание, которому при создании назначается действие.

      struct ContentView: View {
    // parent view holds the state object
    @StateObject var someViewModel = SomeViewModel()

    var body: some View {
        VStack(alignment: .center, spacing: 24) {
            SomeViewRepresentable(model: someViewModel)
                .background(Color.gray)
            HStack {
                Button {
                    someViewModel.foo?()
                } label: {
                    Text("Tap Here")
                }
            }
        }
    }
}

struct SomeViewRepresentable: UIViewRepresentable {
    @ObservedObject var model: SomeViewModel

    func makeUIView(context: Context) -> SomeView {
        let someView = SomeView()
        // we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
        model.foo = { [weak someView] in
            someView?.foo()
        }
        return someView
    }

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

    }
}

class SomeViewModel: ObservableObject {
    var foo: (() -> Void)? = nil
}

Три преимущества такого подхода:

  1. Мы избегаем исходной проблемы, которую @ada10086 идентифицировал с решением @m-reza; создание вида только внутриmakeUIViewв соответствии с руководством Apple Docs , в котором говорится, что мы «должны реализовать этот метод и использовать его для создания вашего объекта представления».

  2. Мы избегаем проблемы, которую @orschaef идентифицировал с альтернативным решением @ada10086; мы не изменяем состояние во время обновления представления.

  3. ИспользуяObservableObjectдля модели мы можем добавлять свойства в модель и сообщать об изменениях состояния изUIViewобъект. Например, еслиSomeViewиспользует KVO для некоторых своих свойств, мы можем создать наблюдатель, который будет обновлять некоторые@Publishedсвойства, которые будут распространяться на любые заинтересованные представления SwiftUI.

Другие вопросы по тегам