How to Call a Method on a Uiview from Outside the Uiviewrepresentable in Swiftui

iOS: Make function calls between UIViewRepresentable and View both ways, SwiftUI

You can use computed property and closure for a callback.

Here is the example code.

struct LoginView: View {

// Other code

// Webview var
private var webView: LoginWebview {
LoginWebview(testdo: self.showJSAlert) {
// Here you will get the call back
print("Callback")
}
}

var body: some View {

webView // <-- Here

// Other Code

And for button action

Button(action: {

//Calls login webview and triggers update that calls the js
self.showJSAlert.toggle()
// Access your function
self.webView.printstuff()

}) {
Text("Login")
.padding()
.font(.system(size: 20))

}

And in LoginWebview

struct LoginWebview: UIViewRepresentable {
var testdo = false

var callBack: (() -> Void)? = nil
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
// Other code

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.control.callBack?() // < Example for calling callback
}

// Other code
}

Send tapAction from SwiftUI button action to UIView function

You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:

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()
}
}

And your view body will look like this:

  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")
}
}
}
}

To test it I added a print to the foo() method:

class SomeView: UIView {

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

Now tapping on your button will trigger foo() and the print statement will be shown.

Calling functions from UIViewController in SwiftUI

There are probably multiple solutions to this problem, but one way or another, you'll need to find a way to keep a reference to or communicate with the UIViewController. Because SwiftUI views themselves are pretty transient, you can't just store a reference in the view itself, because it could get recreated at any time.

Tools to use:

  • ObservableObject -- this will let you store data in a class instead of a struct and will make it easier to store references, connect data, etc

  • Coordinator -- in a UIViewRepresentable, you can use a Coordinator pattern which will allow you to store references to the UIViewController and communicate with it

  • Combine Publishers -- these are totally optional, but I've chosen to use them here since they're an easy way to move data around without too much boilerplate code.

import SwiftUI
import Combine

struct ContentView: View {
@ObservedObject var vcLink = VCLink()
var body: some View {
VStack {
VCRepresented(vcLink: vcLink)
Button("Take photo") {
vcLink.takePhoto()
}
}
}
}

enum LinkAction {
case takePhoto
}

class VCLink : ObservableObject {
@Published var action : LinkAction?

func takePhoto() {
action = .takePhoto
}
}

class CustomVC : UIViewController {
func action(_ action : LinkAction) {
print("\(action)")
}
}

struct VCRepresented : UIViewControllerRepresentable {
var vcLink : VCLink

class Coordinator {
var vcLink : VCLink? {
didSet {
cancelable = vcLink?.$action.sink(receiveValue: { (action) in
guard let action = action else {
return
}
self.viewController?.action(action)
})
}
}
var viewController : CustomVC?

private var cancelable : AnyCancellable?
}

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

func makeUIViewController(context: Context) -> CustomVC {
return CustomVC()
}

func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
context.coordinator.viewController = uiViewController
context.coordinator.vcLink = vcLink
}
}

What happens here:

  1. VCLink is an ObservableObject that I'm using as a go-between to communicate between views
  2. The ContentView has a reference to the VCLink -- when the button is pressed, the Publisher on VCLink communicates that to any subscribers
  3. When the VCRepresented is created/updated, I store a reference to the ViewController and the VCLink in its Coordinator
  4. The Coordinator takes the Publisher and in its sink method, performs an action on the stored ViewController. In this demo, I'm just printing the action. In your example, you'd want to trigger the photo itself.

Swiftui - Access UIKit methods/properties from UIViewRepresentable

You can use something like configurator callback pattern, like

struct TextView: UIViewRepresentable {
@ObservedObject var config: ConfigModel = .shared
@Binding var text: String

@State var isEditable: Bool
var borderColor: UIColor
var borderWidth: CGFloat
var configurator: ((UITextView) -> ())? // << here !!

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

func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator

myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}

func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = uiView.font?.withSize(CGFloat(config.textsize))
uiView.text = text

// alternat is to call this function in makeUIView, which is called once,
// and the store externally to send methods directly.
configurator?(myTextView) // << here !!
}

// ... other code
}

and use it in your SwiftUI view like

TextView(...) { uiText in
uiText.isEditing = some
}

Note: depending on your scenarios it might be additional conditions need to avoid update cycling, not sure.

SwiftUI + UIViewRepresentable: how would you have the UIViewRepresentable respond to a binding

Figured it out:

struct VideoPlayerView: UIViewRepresentable {

var urls: [URL]
var playOnLoad: Bool
@EnvironmentObject var appState: AppState


func makeUIView(context: Context) -> UIView {
return PlayerUIViewQueue(urls: urls, playOnLoad: playOnLoad, frame: .zero)
}

// @use: on `appState` context change, `play` `pause` or `resume` the video
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {

if let uiv = uiView as? PlayerUIViewQueue {

switch appState.getTokenPlayCommand() {
case .pause:
uiv.pause()
case .play:
uiv.playFromBeginning()
case .resume:
uiv.resume()
}
}
}

}

class PlayerUIViewQueue: UIView {

private var URLs: [URL]

// player references
private let playerLayer = AVPlayerLayer()
private var current_mutli : AVQueuePlayer? = nil
private var current_single: AVPlayer? = nil


required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}

init(urls: [URL], playOnLoad:Bool, frame: CGRect){

self.URLs = urls

super.init(frame: frame)

if self.URLs.count == 1 {
print("if case")
initSinglePlayer()
} else {
print("else case")
loopAll()
}
}


//MARK:- API

func resume(){
print("RESUMNING")
current_mutli?.play()
current_single?.play()
}

func pause(){
print("PAUSING")
current_mutli?.pause()
current_single?.pause()
}

func playFromBeginning(){
print("playFromBeginning")
current_mutli?.seek(to: .zero)
current_mutli?.play()
current_single?.seek(to: .zero)
current_single?.play()
}


//MARK:- player utils

private func initSinglePlayer(){

if URLs.count == 0 { return }

let player = AVPlayer(url: URLs[0])
player.actionAtItemEnd = .none
self.current_single = player

playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill

layer.addSublayer(playerLayer)

player.play()
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerSingle(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem
)

}


@objc func loopPlayerSingle(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}


// @use: on each `loopAll()` invocation, reinit
// the player with all video `items`
private func loopAll(){

let items = URLs.map{ AVPlayerItem.init(url: $0) }
let player = AVQueuePlayer(items: items)
self.playerLayer.player = player
self.current_mutli = player

player.seek(to: .zero)
player.play()

playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)

NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerMulti(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.items().last
)

}



@objc private func loopPlayerMulti(notification: Notification) {
loopAll()
}

}



Related Topics



Leave a reply



Submit