I am trying to update the Master view in a UISplitView when a user taps a UIBarButtonItem in the detail view, which represents a favourite button. For some reason, the delegate method never gets called. Relevant code below. Any suggestions?
import UIKit
import WebKit
protocol FavouriteCaseDelegate: class {
func updateMasterForFavouriteStatusChange(caseID: Int, favouriteStatus: Bool)
}
class DetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
weak var favouriteDelegate: FavouriteCaseDelegate? = nil
override func viewDidLoad() {
super.viewDidLoad()
//Configure favourite bar button
favouriteButton = UIButton.init(type: .custom)
if caseFavourited == false {
favouriteButton.setImage(UIImage(named: "FavouriteIcon"), for: UIControlState.normal)
} else {
favouriteButton.setImage(UIImage(named: "FavouriteIconSelected"), for: UIControlState.normal)
}
favouriteButton.addTarget(self, action:#selector(didTapFavouriteBarButton), for: UIControlEvents.touchUpInside)
favouriteButton.frame = CGRect.init(x: 0, y: 0, width: 30, height: 30)
let favouriteBarButton = UIBarButtonItem.init(customView: favouriteButton)
self.navigationItem.rightBarButtonItem = favouriteBarButton
}
func didTapFavouriteBarButton() {
caseFavourited = !caseFavourited
CaseManager.caseWorker.changeFavouritedStateForCase(subjectID: subjectID, caseID: caseID, isFavourited: caseFavourited)
favouriteDelegate?.updateMasterForFavouriteStatusChange(caseID: caseID, favouriteStatus: caseFavourited)
}
import UIKit
class MasterTableViewController: UITableViewController, FavouriteCaseDelegate {
func updateMasterForFavouriteStatusChange(caseID: Int, favouriteStatus: Bool) {
print("updateMasterForFavourite called")
self.tableView.reloadData()
}
}
Inside your MasterTableViewController class you need to set DetailViewController-> favouriteDelegate to self for instance:-
//Assuming you have reference of detailViewController inside your MasterTableViewController class
let detailViewController = DetailViewController()
detailViewController.favouriteDelegate = self
Once set the delegate and invoke the method then it should called.
Related
I have an app reader like Apple Books. And I have a problem. I have NavigationViewController next FirstViewController with book button, SecondViewController it is a reader and TableViewController it is settings for text size. But if I open book in FirstViewController go to SecondViewController and in navigation bar tab aA button and open TableViewController all works fine. But after if I want back to FirstViewController I not close TableViewController and do swipe from left to right my app stops. How to fix it?
SecondViewController:
#IBOutlet weak var textSettings: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupGestures()
}
// MARK: - Text Settings View
private func setupGestures() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped))
tapGesture.numberOfTapsRequired = 1
textSettings.addGestureRecognizer(tapGesture)
}
#objc private func tapped(){
guard let popVC = storyboard?.instantiateViewController(withIdentifier: "popVC") else { return }
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = self.textSettings
popOverVC?.sourceRect = CGRect(x: self.textSettings.bounds.midX, y: self.textSettings.bounds.maxY, width: 0, height: 0)
popVC.preferredContentSize = CGSize(width: 250, height: 250)
self.present(popVC, animated: true)
}
}
extension SecondViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
Other controllers have no special code. Video illustrating the problem - https://drive.google.com/file/d/1HUV26H4VFoKglh8FbkNP0y2Y3rYl4Q_f/view?usp=sharing
I have two super view controllers MasterCategoryListViewController and MasterCategoryItemViewController.
I want to use these in several apps.
I inherit from both of these
class CustomListController: MasterCategoryListViewController
class CustomItemController: MasterCategoryItemViewController
Now in the MasterCategoryListViewController
I have a button handler...
#objc open func btnAddTapped(sender: UIBarButtonItem) {
let itemViewController = MasterCategoryItemViewController()
itemViewController.title = "Type"
self.navigationController?.pushViewController(itemViewController, animated: true)
}
I know I can override the method to push to CustomItemController however, I'm just wondering if I can do this in my MasterCategoryListViewController, obviously without it knowing anything about what CustomItemController?
Create a method on the parent called something like detailVCClass(), answering the class that should be instantiated upon button tap. The parent can answer something generic, and the subclasses answer any class that's appropriate for each to know about.
Have the button tap method instantiate an instance of self.detailVCClass(), rather than a class name literal.
however, I'm just wondering if I can do this in my
MasterCategoryListViewController, obviously without it knowing
anything about what CustomItemController ?
Yes. Why not? However, the one that will be pushed is the MasterCategoryItemViewController, not any subclassing classes of it. So like what you've mentioned in your question, you know that btnAddTapped can be overriden, so do it like that.
OR, you could do something a bit more interesting:
In your MasterCategoryListViewController, have an object of MasterCategoryItemViewController. Then in your CustomListController, apply any subclassing MasterCategoryListViewController class. Next, push that MasterCategoryItemViewController object in your btnAddTapped()
Complete sample:
import UIKit
class ListVC: MasterListVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ListVC"
self.itemVCToBePushed = ItemVC2()
}
}
class MasterListVC: UIViewController {
var itemVCToBePushed: MasterItemVC?
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.frame = CGRect(x: 100, y: 100, width: 250.0, height: 44.0)
button.setTitle("Test", for: .normal)
button.addTarget(self, action: #selector(self.pushMe), for: .touchUpInside)
button.backgroundColor = .gray
return button
}()
#objc func pushMe() {
guard let itemVCToBePushed = self.itemVCToBePushed else { return }
self.navigationController?.pushViewController(itemVCToBePushed, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "MasterListVC"
self.view.backgroundColor = .white
self.view.addSubview(self.button)
}
}
/////
class ItemVC: MasterItemVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ItemVC"
}
}
class ItemVC2: MasterItemVC {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ItemVC2"
}
}
class MasterItemVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "MasterItemVC"
self.view.backgroundColor = .white
}
}
The code below compiles fine, but crashes with an unrecognized selector sent to instance error.
I have one class that inherits from UIViewController:
class Controller: UIViewController {
override func viewDidLoad() {
let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
let toolbar = toolbarWrapper.toolbarView
view.addSubview(toolbar)
... Other code ...
}
}
And another class that is just a wrapper for a UIView and contains buttons:
class CustomToolbarWrapper {
var toolbarView: UIView
init(view: UIView, target: Any) {
let height: CGFloat = 80
toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height, width: view.frame.width, height: height))
let button = UIButton()
... Some button layout code ...
button.addTarget(target, action: #selector(CustomToolbar.buttonTapped(_:)), for: .touchUpInside)
toolbarView.addSubview(button)
}
#objc static func buttonTapped(_ sender: Any) {
print("button tapped")
}
}
For the sake of clarity, I left out a large chunk of code and kept what I thought was necessary. I think that my code doesn't work because of my misunderstanding of the how the target works in the addTarget function. Normally, I would just use self as the target of my button's action, so I just tried to pass along self from the view controller to the CustomToolbarWrapper's init function.
What else I have tried:
Changing the button's target from target to self like this:
button.addTarget(self, action: #selector(CustomToolbar.buttonTapped(_:)), for: .touchUpInside)
results in the app not crashing anymore. Instead, however, I believe that line of code fails to do anything (which doesn't throw an error for some reason?) because attempting to print button.allTargets or even button.allTargets.count results in the app crashing at compile time, with an EXC_BREAKPOINT error and no error description in the console or the XCode UI (which just confuses me even more because there are no breakpoints in my code!).
Also, making buttonPressed(_:) non-static does not change any of the previously mentioned observations.
Also, to make sure the button could in fact be interacted with, I added this in the viewDidLoad() of Controller:
for subview in toolbar.subviews? {
if let button = subview as? UIButton {
button.addTarget(self, action: #selector(buttonPressed(_:)), for: .touchUpInside)
}
}
and added a simple testing method to Controller for the button:
#objc func buttonPressed(_ sender: UIButton) {
print("Button Pressed")
}
And running the code did result in "Button Pressed" being printed in the console log, so the button should be able to be interacted with by the user.
Feel free to let me know if you think this is not enough code to figure out the problem, and I will post more details.
Edit
I prefer to keep the implementation of the button's action in the CustomToolbarWrapper class to prevent repeating code in the future, since the action will be the same no matter where an instance of CustomToolbarWrapper is created.
The best option would be to add the target in your controller and then call a method in your toolbarWrapper on button press. But if you really need to keep this design, you should have a strong reference to your toolbarWrapper in your controller class, otherwise your toolbarWrapper is deallocated and nothing gets called. Also, the buttonTapped(_:) method does not need to be static. Thus, in your controller:
class Controller: UIViewController {
var toolbarWrapper: CustomToolbarWrapper?
override func viewDidLoad() {
toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
let toolbar = toolbarWrapper.toolbarView
view.addSubview(toolbar)
... Other code ...
}
}
And in your wrapper:
class CustomToolbarWrapper {
var toolbarView: UIView
init(view: UIView, target: Any) {
let height: CGFloat = 80
toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height,width: view.frame.width, height: height))
let button = UIButton()
... Some button layout code ...
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
toolbarView.addSubview(button)
}
#objc func buttonTapped(_ sender: Any) {
print("button tapped")
}
}
There is another way I would use which is delegation. The target does not necessarily have to be a controller, it can be the CustomToolbarWrapper itself.
First, declare a protocol
protocol CTDelegate: AnyObject {
func didClickButton()
}
Then in CustomToolbarWrapper add a property, weak var delegate: CTDelegate? and a button action:
#objc func buttonTapped(_ sender: UIButton) {
delegate?.didClickButton()
}
So in your case, it becomes:
button.addTarget(self, action: #selector(CustomToolbarWrapper.buttonTapped(_:)), for: .touchUpInside)
Then when you go to any ViewController, conform to CTDelegate and initialize the CustomToolbarWrapper, you can set its delegate to the controller.
e.g
let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
toolbarWrapper.delegate = self
and implement your action inside the method you are conforming to in your controller i.e.
func didClickButton()
Your problem is right here:
let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
You're passing an instance of Controller class which doesn't implement the buttonTapped(_:) selector. It is implemented by your CustomToolbarWrapper class. This is a bad design in general. You should either follow a delegate pattern, or a callback pattern.
Updated Answer:
Delegate pattern solution:
class Controller: UIViewController, CustomToolbarWrapperDelegate {
override func viewDidLoad() {
let toolbarWrapper = CustomToolbarWrapper(view: view, buttonDelegate: self)
let toolbar = toolbarWrapper.toolbarView
view.addSubview(toolbar)
}
// MARK: - CustomToolbarWrapperDelegate
func buttonTapped(inToolbar toolbar: CustomToolbarWrapper) {
print("button tapped")
}
}
protocol CustomToolbarWrapperDelegate: AnyObject {
func buttonTapped(inToolbar toolbar: CustomToolbarWrapper) -> Void
}
class CustomToolbarWrapper {
var toolbarView: UIView
weak var buttonDelegate: CustomToolbarWrapperDelegate?
init(view: UIView, buttonDelegate: CustomToolbarWrapperDelegate?) {
let height: CGFloat = 80
toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height, width: view.frame.width, height: height))
self.buttonDelegate = buttonDelegate
let button = UIButton()
button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
toolbarView.addSubview(button)
}
#objc private func buttonTapped(_ sender: Any) {
// Your button's logic here. Then call the delegate:
self.buttonDelegate?.buttonTapped(inToolbar: self)
}
}
If you'd rather stick to your current design then just implement the following changes:
class Controller: UIViewController {
override func viewDidLoad() {
let toolbarWrapper = CustomToolbarWrapper(view: view, target: self, selector: #selector(self.buttonTapped(_:)), events: .touchUpInside)
let toolbar = toolbarWrapper.toolbarView
view.addSubview(toolbar)
}
#objc private func buttonTapped(_ sender: Any) {
print("button tapped")
}
}
class CustomToolbarWrapper {
var toolbarView: UIView
init(view: UIView, target: Any?, selector: Selector, events: UIControlEvents) {
let height: CGFloat = 80
toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height, width: view.frame.width, height: height))
let button = UIButton()
button.addTarget(target, action: selector, for: events)
toolbarView.addSubview(button)
}
}
Note: I created a Test program so I can better understand how ARC works - I am having trouble implementing it in my real project.
I created a Test program to determine how ARC works - it works great! Here it is.
ViewController1:
import UIKit
class ViewController: UIViewController {
weak var vc2:ViewController2?
override func viewDidLoad() {
super.viewDidLoad()
print("VC1 Initialized")
addButton()
}
func addButton() {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
button.backgroundColor = .red
button.setTitle("Go to VC2", for: .normal)
self.view.addSubview(button)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
}
#objc func buttonAction(sender: UIButton!) {
let vx = ViewController2()
vx.VC1 = self
vc2 = vx
self.present(vc2!, animated: false, completion: nil)
}
}
ViewController2:
import UIKit
class ViewController2: UIViewController {
weak var VC1:ViewController?
weak var VC3:ViewController3?
override func viewDidLoad() {
super.viewDidLoad()
print("VC2 Initialized")
addButton()
}
deinit {
print("VC2 Deinitialized")
}
func addButton() {
for i in 0..<2 {
let button = UIButton(frame: CGRect(x: 0, y: self.view.frame.height*0.5*CGFloat(i), width: self.view.frame.width, height: self.view.frame.height*0.5))
button.backgroundColor = i == 0 ? .red : .blue
button.setTitle("Go to VC\(i*2+1)", for: .normal)
self.view.addSubview(button)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.tag = i
}
}
#objc func buttonAction(sender: UIButton!) {
let tag = sender.tag
if(tag == 0) {
self.dismiss(animated: true, completion: nil)
}
else {
let vc3 = ViewController3()
vc3.ViewController2 = self
VC3 = vc3
self.present(VC3!, animated: true, completion: nil)
}
}
}
ViewController3:
import UIKit
class ViewController3: UIViewController {
var ViewController2:ViewController2?
override func viewDidLoad() {
super.viewDidLoad()
print("VC3 Initialized")
addButton()
}
deinit {
print("VC3 Deinitialized")
}
func addButton() {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
button.backgroundColor = .red
button.setTitle("Go to VC1", for: .normal)
self.view.addSubview(button)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
}
#objc func buttonAction(sender: UIButton!) {
self.ViewController2!.VC1!.dismiss(animated: true, completion: nil)
}
}
What I was looking at was how to remove multiple pages at a time (as indicated by the button push on VC3 and it goes back to the main page) and still remove ARC memory. All goes well. You can see the output below of me doing these operations.
Start program - push to page 2 - push to page. 1 - push to page 2 - push to page 3 - go to page 1
VC1 Initialized
VC2 Initialized
VC2 Deinitialized
VC2 Initialized
VC3 Initialized
VC3 Deinitialized
VC2 Deinitialized
The problem I am incurring is in my actual program - when I use these methods above, it automatically calls deinit on the last object I created when I CREATE my new object. But it doesn't actually delete anything - the memory graph will show x number of items even though the deinit method is called.
import UIKIT
class myClass: UIViewController {
weak var pageController:PageController?
override func viewDidLoad() {
super.viewDidLoad()
print("Initialized PC")
presentThemes()
}
func presentThemes() {
let pg = PageController(x: 1, controller: self)
pageController = pg
self.present(pageController!, animated: true, completion: nil)
}
deinit {
print("Deinit")
}
}
The output I am being provided is:
Initialized PC
On the first instance and is:
Deinit
Initialized PC
On the second instance. Any idea why Deinit is being called but the memory graph shows it there?
I have a class like this,
import UIkit
class One {
let btn = UIButton()
override func viewDidLoad(){
super.viewDidLoad()
btn.frame = CGRectMake(10, 20, 30, 30)
btn.setTitle("Go", forState: UIControlState.Normal)
btn.addTarget(self, action: "goToClassTwo", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(btn)
}
func goToClassTwo(){
if(AppGlobals().getIsFromDiffView()){
let difView = UINavigationController(rootViewController: DiffView())
difView.pushViewController(Two(), animated: true)
}else{
self.navigationController?.pushViewController(Two(), animated: true)
}
}
}
A setter/getter class like this,
class AppGlobals: NSObject {
var isFromDiffView = false
func setIsFromDiffView(val: Bool){
isFromDiffView = val
}
func getIsFromDiffView() -> Bool {
return isFromDiffView
}
}
And I have another class like this,
class DiffView {
let btn = UIButton()
override func viewDidLoad(){
super.viewDidLoad()
btn.frame = CGRectMake(10, 20, 30, 30)
btn.setTitle("Push", forState: UIControlState.Normal)
btn.addTarget(self, action: "btnAction", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(btn)
}
func btnAction(){
AppGlobals().setIsFromDiffView(true)
One().goToClassTwo()
}
}
I am facing a problem here. When the 'Go' button in the class 'One' is tapped, then the 'Two' view controller is shown. But when I tap on the 'Push' button in the class 'DiffView' is tapped, the 'Two' view controller is not being shown.
I have checked setting breakpoints. The control does come to the goToClassTwo function in the class 'One' and the if path is being executed. But the 'Two' view controller is not shown. difView.pushViewController is called. But it is not pushing to the next view.
NOTE: I am not using storyboard
Any help would be appreciated!
This is the updated code.
Code for class 'One':
import UIKit
class One {
let btn = UIButton()
override func viewDidLoad(){
super.viewDidLoad()
btn.frame = CGRectMake(10, 20, 30, 30)
btn.setTitle("Go", forState: UIControlState.Normal)
btn.addTarget(self, action: "goToClassTwo", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(btn)
}
func goToClassTwo(){
if(AppGlobals().getIsFromDiffView()){
//Using the navigation controller of DiffView
AppGlobals().getController().pushViewController(Two(), animated: true)
}else{
self.navigationController?.pushViewController(Two(), animated: true)
}
}
}
setter/getter class:
class AppGlobals: NSObject {
var isFromDiffView = false
var cntrlr: UINavigationController!
func setIsFromDiffView(val: Bool){
isFromDiffView = val
}
func getIsFromDiffView() -> Bool {
return isFromDiffView
}
//Setting and getting DiffView Navigation controller
func setController(cntrl: UINavigationController){
cntrlr = cntrl
}
func getController() -> UINavigationController {
return cntrlr
}
}
DiffView class:
class DiffView {
let btn = UIButton()
override func viewDidLoad(){
super.viewDidLoad()
btn.frame = CGRectMake(10, 20, 30, 30)
btn.setTitle("Push", forState: UIControlState.Normal)
btn.addTarget(self, action: "btnAction", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(btn)
}
func btnAction(){
AppGlobals().setIsFromDiffView(true)
//Setting the navigation controller
AppGlobals().setController(self.navigationController!)
One().goToClassTwo()
}
}
With this updated code, class 'Two' view controller is being displayed.
Thank you #zp_x for your help.