Отправить действие 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
}
Три преимущества такого подхода:
Мы избегаем исходной проблемы, которую @ada10086 идентифицировал с решением @m-reza; создание вида только внутри
makeUIView
в соответствии с руководством Apple Docs , в котором говорится, что мы «должны реализовать этот метод и использовать его для создания вашего объекта представления».Мы избегаем проблемы, которую @orschaef идентифицировал с альтернативным решением @ada10086; мы не изменяем состояние во время обновления представления.
Используя
ObservableObject
для модели мы можем добавлять свойства в модель и сообщать об изменениях состояния изUIView
объект. Например, еслиSomeView
использует KVO для некоторых своих свойств, мы можем создать наблюдатель, который будет обновлять некоторые@Published
свойства, которые будут распространяться на любые заинтересованные представления SwiftUI.