SwiftUI calling delegate in UIViewController - ios

This is the first time I am working with SwiftUI, and I have searched for this problem but did not find any working solution, so I would appreciate all help I can get.
I have a UIViewController, in which I present a SwiftUI View through UIHostingController. What I want to achieve is that when I press the button in my SwiftUI view, the action is going to trigger the delegate in UIViewController, alternatively, trigger a function in UIViewController and then from that function trigger the delegate.
class MyViewController: UIViewController {
public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())
override func viewWillAppear(_ animated: Bool) {
addChild(facialView)
view.addSubview(facialView.view)
setupConstraints()
}
extension MyViewController {
#objc private func buttonPressed() {
delegate?.buttonPressed()
}
}
}
And in my SwiftUI view FacialTutorial
struct FacialTutorial: View {
var body: some View {
VStack() {
Button {
// I want to call delegate?.buttonPressed() action here
} label: {
Text("Press button")
}
}
}
}
EDIT
Okay to be more clear, my ViewController is configuring the page differently for a number of cases. So in practice, I do not initiate the SwiftUI view from viewWillAppear. Rather this is how I do
public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addChild(facialView)
configureSubviews()
}
private func configureForInstructionMode() {
view.addSubview(facialUIView.view)
setupConstraints()
}
I must have it this way because I need to configure the view differently depending on which mode I am going to configure for. When I declare the facialView inside the viewDidLoad or viewWillAppear, I cannot access the instance in configureForInstructionMode(), or it's value is nil..

You simply need to pass a reference to your MyViewController instance through to MyUIView (Which should probably be MyView since it isn't a subclass of UIView) via its initialiser. You could use a delegation pattern or you could pass a closure and then invoke the delegate method from the closure. The second way is more "Swifty".
(I have moved the code to viewDidLoad since if you have it in viewWillAppear the view may be added multiple times if the view controller appears and disappears and appears again)
class MyViewController: UIViewController {
public var delegate: MyViewControllerDelegate? {
weak var myView: UIHostingController<MyUIView>!
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIHostingController(rootView: MyUIView(buttonHandler: { [weak self] in
self?.delegate?.buttonPressed()
}))
self.myView = myView
view.addSubview(myView.view)
setupConstraints()
}
}
Then, in your SwiftUI view you can declare a property to hold the closure and invoke that closure in your button
struct MyUIView: View {
var buttonHandler: (()->Void)?
var body: some View {
VStack() {
Button {
buttonHandler?()
} label: {
Text("Press button")
}
}
}
}

Related

How do you access UIViewControllerRepresentable NavigationBar property?

In SwiftUI if you are transitioning using a NavigationLink() into a UIViewControllerRepresentable how would you; say, add buttons or change the title property on the navigationbar.
This is what I am doing right now:
import SwiftUI
/// Controls the actual action performed by the button upon taps.
struct CatagoryButton: View {
#State var isPresenting :Bool = false
var company : Company?
var text : String
var body: some View {
NavigationLink(destination: UIKitWrapper(company: self.company, storyboardPointer: self.text)
.navigationBarTitle(self.text)
.edgesIgnoringSafeArea(.all),
isActive: self.$isPresenting,
label: {
Button(action: {
self.isPresenting.toggle()
}){
ZStack {
ButtonShadowLayer(text: text)
GradientBackground()
.mask(ButtonBaseLayer())
CircleAndTextLayer(text: text)
}
}
})
}
}
Here is the struct for my representable.
import SwiftUI
/// Wraps UIKIT instance in a representable that swiftUI can present.
struct UIKitWrapper: UIViewControllerRepresentable {
//Specify what type of controller is being wrapped in an associated type.
typealias UIViewControllerType = UIViewController
//Company property passed from parent view. Represents the company the user selected from main view.
private var company : Company
//Determines which viewcontroller will be presented to user. This string corresponds to the name of the storyboard file in the main bundle.
private var storyboardPointer : String
init(company: Company?, storyboardPointer: String) {
guard let company = company else {fatalError()}
self.company = company
self.storyboardPointer = storyboardPointer
}
func makeUIViewController(context: Context) -> UIViewControllerType {
//Find user defined storyboard in bundle using name.
let storyboard = UIStoryboard(name: storyboardPointer, bundle: .main)
//Downcast returned controller to protocol AccessControllerProtocol. This step is required because we are not sure which storyboard will be accessed. Potential storyboard controllers that can be called all conform to this protocol.
//FIXME: Remove fatalError and create error enum asap.
guard let viewController = storyboard.instantiateInitialViewController() as? AccessControllerProtocol else { fatalError() }
//Assign user selected company object to instance property on incoming viewController.
viewController.company = company
//Return UINavigationController with storyboard instance view controller as root controller.
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Finally, here is one of the classes that use the representable.
import UIKit
class OrdersViewController: UIViewController, AccessControllerProtocol {
var company : Company!
#IBOutlet var companyNameLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
companyNameLabel.text = company.name
self.navigationController?.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.tapRightBarButton))]
}
func setBackgroundColor(){
let backgroundGradient = BackgroundGradientSetter()
let viewWithGradient = backgroundGradient.setGradientToView(with: [DarkBlueHue_DEFAULT,LightBlueHue_DEFAULT], size: view.bounds)
view.addSubview(viewWithGradient)
view.sendSubviewToBack(viewWithGradient)
}
#objc func tapRightBarButton(){
}
}
No matter what I do this button doesn't show up. I'm not sure if I need to put this in a makeCoordinator() or if there is just something I am missing. If anyone has insight please let me know!
If it isn't available in viewDidLoad, try calling your setupNavigation() in viewWillAppear()
In your case navigationController is not available yet on viewDidLoad, try instead as in below demo module
Tested & works with Xcode 11.2 / iOS 13.2
class MyUIController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.onAdd(_:)))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// might be needed to remove injected item here
}
#objc func onAdd(_ sender: Any?) {
print(">> tapped add")
}
}
struct MyInjector: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyInjector>) -> MyUIController {
MyUIController()
}
func updateUIViewController(_ uiViewController: MyUIController, context: UIViewControllerRepresentableContext<MyInjector>) {
}
}
struct DemoNavigationBarUIButton: View {
var body: some View {
NavigationView {
MyInjector()
.navigationBarTitle("Demo")
}
}
}
struct DemoNavigationBarUIButton_Previews: PreviewProvider {
static var previews: some View {
DemoNavigationBarUIButton()
}
}

Delegate not executing after call swift

I have a viewController with another containerView insider set up to appear temporarily (added programmatically). The containerView is a sort of operation bar, which allows you to change values of the viewController. The protocol called from an IBAction of a button however, does not call the protocol set up inside the viewController class.
Here is the code from both classes:
class viewController: UIViewController, updateListDelegate {
let dataSource = containerView()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
func updateList(sender: containerView) {
print("is called") //is not printed
}
}
The code from the containerView:
protocol updateListDelegate {
func updateList(containerView)
}
class containerView: UIViewController {
var delegate: updateListDelegate?
#IBAction func AddSong(_ sender: UIButton) {
self.delegate?.updateList(sender: self)
}
}
If this method is only to be called from one object, then, in my opinion, I would not define a protocol. If multiple objects are to call this method, then I would define a protocol. This is typically how you would call a method backwards, using a basic delegate.
class ViewController: UIViewController {
let container = ContainerView()
override func viewDidLoad() {
super.viewDidLoad()
container.viewControllerDelegate = self
// push to this instance of container at some point
}
func doSomething() {
print("great success")
}
}
class ContainerView: UIViewController {
weak var viewControllerDelegate: ViewController?
#objc func someAction() {
if let viewControllerDelegate = viewControllerDelegate {
viewControllerDelegate.doSomething()
}
}
}
// prints "great success" when someAction() called
One of the most common mistakes people make is not keeping track of instances. For delegates to work, you must be sure you are using the specific instances that you've instantiated and assigned those delegates to.

DismissViewController passing parameter back swift

I present my secondViewController from (attendanceViewController) and in dismiss completion I'm trying to pass parameters and call functions. The AttendanceViewController appears and the function is called. The problem is that all the Objects are nil when dismiss(#IBOutlet weak var tableView: UITableView! , #IBOutlet weak var boxTypeSKU: UIView!....all)
self.presentingViewController!.dismissViewControllerAnimated(true, completion: { _ i
let attView: AttendanceViewController = self.storyboard!.instantiateViewControllerWithIdentifier("AttendanceViewID") as! AttendanceViewController
attView.currAttendance = self.currAttendance
attView.searchProductWithSKU("\(sku)")
})
I solved my problem using Protocols like this tutorial (http://swiftdeveloperblog.com/pass-information-back-to-the-previous-view-controller/) I think it's more elegant and efficient.
There's my updated code:
In second view Controller (BarcodeScannerViewController.swift) I do it:
protocol BarcodeScannerProtocol {
func setSKUScanner(sku: String)
}
class BarcodeScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var delegate:BarcodeScannerProtocol?
func back() {
let sku = (barcode as NSString).substringWithRange(NSMakeRange(6, 8))
delegate?.setSKUScanner(sku)
self.presentingViewController!.dismissViewControllerAnimated(true, completion: { _ in
}
}
In first view controller (AttendanceViewController.swift):
class AttendanceViewController: UIViewController, BarcodeScannerProtocol {
var strSKUScanner : String?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let skuScanned = strSKUScanner {
searchProductWithSKU(skuScanned)
} else {
fetchProducts()
}
}
// MARK: BarcodeScannerProtocol functions
func setSKUScanner(sku: String) {
self.strSKUScanner = sku
}
}
The first thing to be noticed is that a new instance of AttendanceViewController is being instantiated. This means that the properties are not being set on the correct object. There needs to be a reference to the view controller that presented the secondViewController. How that is done is up to you, but I recommend a callback containing the currAttendance variable. This would be a property on the presented view controller. Once the callback is called by the presented view controller, the parent AttendanceViewController can set its own property and dismiss the presented view controller and call the searchProductWithSKU(_:) method.

update value of text when click back on navigation controller

I have 2 controllers
and have got 1 global variable, the problem is if I go to controller 2 and click on button northAmericaClick, it will navigate back to control 1, but the value of global variable won't change!
this is my code
controller 1
class OurViewController: UIViewController {
#IBOutlet weak var menuButton: UIBarButtonItem!
#IBOutlet weak var selectedServer: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
selectedServer.setTitle(selected server, forState: UIControlState.Normal) // selected server this is global variable
}
controller 2
class selectServerController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func northAmericaClick(sender: AnyObject) {
selectedserver = "North America"
self.navigationController?.popViewControllerAnimated(true)
}
From
You need to use a delegate. Here is an example how do use a delegate in Swift.
On your first ViewController, set your delegate when you load the second VC:
For example, if you are using the Storyboard Editor:
var secondViewController = (segue.destinationViewController.visibleViewController as MySecondViewControllerClass)
secondViewController.delegate = self
Write a Protocol and define a func to write you values back
For example, create a file called "Protocol.swift" and write something like that:
protocol writeValueBackDelegate {
func writeValueBack(value: String)
}
Add the function to your FirstViewController
func writeValueBack(value: String) {
// this is my value from my second View Controller
}
And to your ViewControllerClass
class ViewController: UIViewController, writeValueBackDelegate
Go to the Second View Controller, and add the delegate here:
class SecondViewController: ViewController {
// delegate for FirstViewController
var delegate: writeValueBackDelegate?
On your Second View Controller, you can now use this to call the func in the first View Controller an pass data.
delegate?.writeValueBack("That is a value")
You also need to indicate that your first view controller implements the protocol: class ViewController: UIViewController, writeValueBackDelegate {
A part of doing it with delegate you also can create singleton class ViewControllersDataModel class and share the variable using it:
import Foundation
class ViewControllersDataModel {
static let sharedInstance = ViewControllersDataModel()
var selectedserver: String = ""
private init() {
}
}
And call it like this:
ViewControllersDataModel.sharedInstance.selectedserver = "Selected Option";
Ok, I can do this with this code, only check when viewWillDisapear and call the parent of this view controller in the navicationController:
override func viewWillDisappear(animated: Bool) {
if ((self.navigationController!.viewControllers.last?.isKindOfClass(ActivityMyViewController)) == true){
let backView:MyViewController = self.navigationController!.viewControllers.last as! MyDetailViewController
backView // do whatever you want
}
}
I hope this code can help you, good luck
thanks guys for helping ;)
it was very simple
i just use then when it comeback ^^"
override func viewWillAppear(animated: Bool) {
selectedServer.setTitle(selectedserv, forState: UIControlState.Normal)
}

SubView click item to show in parent view label

I would like to make a UI that have label, table view and one button click. When click on the button, we pop up a half screen view that have lots of buttons. I want user can still click on the rest of the screen also.
So i use the approach that suggest in the post
How To Present Half Screen Modal View?
Method 2: to animate a UIView which is of size half of the existing view.
Then you have to simply follow animation of the UIView.
Here as it is just a UIView that will be added as subview to existing view, you will be able to touch the rest of the screen.
As i am newbie to the ios and swift, I would like to get some suggestions.
Now i am successfully add as subview and show in the half of the screen.
How can i implement to let subview click button result show on parent view label text?
I am thinking about parent.xib and subview.xib have the same UIVeiwController.swift. Then i can #IBOutlet and #IBAction to the same controller swift file and update the result. But don't know it is the accpetable way to do?
If not, how can the subViewController send result/event to the parent view and update in the parent view component?
You could use delegation. This keeps your view controllers decoupled, i.e. prevents the child from having a reference to its parent, which allows other view controllers to interact with the modal view controller in the same way.
class ParentViewController : UIViewController, ModalViewControllerDelegate {
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let modalViewContorller = ModalViewController()
modalViewContorller.delegate = self
self.presentViewController( modalViewContorller, animated: true, completion: nil )
}
func modalViewControllerDidProduceResult( modalViewController: ModalViewController, result: String ) {
self.label.text = result
}
}
protocol ModalViewControllerDelegate {
func modalViewControllerDidProduceResult( modalViewController: ModalViewController, result: String )
}
class ModalViewController: UIViewController {
var delegate: ModalViewControllerDelegate?
#IBAction func buttonClicked( sender: AnyObject? ) {
delegate?.modalViewControllerDidProduceResult( self, result: "Hello!" )
}
}
You could also use a closure, which in Swift provides a more concise syntax.
class ParentViewController : UIViewController {
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let modalViewContorller = ModalViewController()
self.presentViewController( modalViewContorller, animated: true, completion: nil )
modalViewContorller.resultBlock = { (result: String) in
self.label.text = result
}
}
}
class ModalViewController: UIViewController {
var resultBlock: ((String) -> ())?
#IBAction func buttonClicked( sender: AnyObject? ) {
self.resultBlock?( "Hello!" )
}
}

Resources