Deinit before UITapGestureRecognizer function is called - ios

I'm building a custom dialog for an app in which i would want to use closures that are called when the user taps on something, kinda like this:
var modal = ModalDialog(title: "modal title", buttonClick: { () -> Void in
println("clicked")
})
modal.show()
So I made a class called ModalDialog in which I have an UITapGestureRecognizer like so:
var modalTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("buttonClickAction:"))
modalView.addGestureRecognizer(modalTap)
My problem is that due to the ARC, when the tap is actually recognized and it tries to call buttonClickAction: the class is already deinitialised and the app crashes.
How would i go about keeping the ARC from deinitialising the modal until i explicitly tells it do to so?

Keep a strong reference of your modal dialog in view controller.
var dialog : ModalDialog
func show {
// Keep strong reference dialog by property of your view controller
self.dialog = ModalDialog(title: "modal title", buttonClick: { () -> Void in
println("clicked")
})
self.dialog.show()
}

Related

Get an event when UIBarButtonItem menu is displayed

We all know how to make a simple tap on a bar button item present a menu (introduced on iOS 14):
let act = UIAction(title: "Howdy") { act in
print("Howdy")
}
let menu = UIMenu(title: "", children: [act])
self.bbi.menu = menu // self.bbi is the bar button item
So far, so good. But presenting the menu isn't the only thing I want to do when the bar button item is tapped. As long as the menu is showing, I need to pause my game timers, and so on. So I need to get an event telling me that the button has been tapped.
I don't want this tap event to be different from the producing of the menu; for example, I don't want to attach a target and action to my button, because if I do that, then the menu production is a different thing that happens only when the user long presses on the button. I want the menu to appear on the tap, and receive an event telling me that this is happening.
This must be a common issue, so how are people solving it?
The only way I could find was to use UIDeferredMenuElement to perform something on a tap of the menu. However, the problem is that you have to recreate the entire menu and assign it to the bar button item again inside the deferred menu element's elementProvider block in order to get future tap events, as you can see in this toy example here:
class YourViewController: UIViewController {
func menu(for barButtonItem: UIBarButtonItem) -> UIMenu {
UIMenu(title: "Some Menu", children: [UIDeferredMenuElement { [weak self, weak barButtonItem] completion in
guard let self = self, let barButtonItem = barButtonItem else { return }
print("Menu shown - pause your game timers and such here")
// Create your menu's real items here:
let realMenuElements = [UIAction(title: "Some Action") { _ in
print("Menu action fired")
}]
// Hand your real menu elements back to the deferred menu element
completion(realMenuElements)
// Recreate the menu. This is necessary in order to get this block to
// fire again on future taps of the bar button item.
barButtonItem.menu = self.menu(for: barButtonItem)
}])
}
override func viewDidLoad() {
super.viewDidLoad()
let someBarButtonItem = UIBarButtonItem(systemItem: .done)
someBarButtonItem.menu = menu(for: someBarButtonItem)
navigationItem.rightBarButtonItem = someBarButtonItem
}
}
Also, it looks like starting in iOS 15 there's a class method on UIDeferredMenuElement called uncached(_:) that creates a deferred menu element that fires its elementProvider block every time the bar button item is tapped instead of just the first time, which would mean you would not have to recreate the menu as in the example above.

How to prevent timer reset using pushViewController method?

I'm trying to keep a timer running even if I switch view controllers. I played around with the Singleton architecture, but I don't quite get it. Pushing a new view controller seems a little easier, but when I call the below method, the view controller that is pushed is blank (doesn't look like the view controller that I created in Storyboards). The timer view controller that I'm trying to push is also the second view controller, if that changes anything.
#objc func timerPressed() {
let timerVC = TimerViewController()
navigationController?.pushViewController(timerVC, animated: true)
}
You need to load it from storyboard
let vc = self.storyboard!.instantiateViewController(withIdentifier: "VCName") as! TimerViewController
self.navigationController?.pushViewController(timerVC, animated: true)
Not sure if your problem is that your controller is blank or that the timer resets. Anyway, in case that you want to keep the time in the memory and not deallocate upon navigating somewhere else I recommend you this.
Create some kind of Constants class which will have a shared param inside.
It could look like this:
class AppConstants {
static let shared = AppConstants()
var timer: Timer?
}
And do whatever you were doing with the timer here accessing it via the shared param.
AppConstants.shared.timer ...
There are different parts to your question. Sh_Khan told you what was wrong with the way you were loading your view controller (simply invoking a view controller’s init method does not load it’s view hierarchy. Typically you will define your view controller’s views in a storyboard, so you need to instantiate it from that storyboard.)
That doesn’t answer the question of how to manage a timer however. A singleton is a good way to go if you want your timer to be global instead of being tied to a particular view controller.
Post the code that you used to create your singleton and we can help you with that.
Edit: Updated to give the TimeManager a delegate:
The idea is pretty simple. Something like this:
protocol TimeManagerDelegate {
func timerDidFire()
}
class TimerManager {
static let sharedTimerManager = TimerManager()
weak var delegate: TimeManagerDelegate?
//methods/vars to manage a shared timer.
func handleTimer(timer: Timer) {
//Put your housekeeping code to manage the timer here
//Now tell our delegate (if any) that the timer has updated.
//Note the "optional chaining" syntax with the `?`. That means that
//If `delegate` == nil, it doesn't do anything.
delegate?.timerDidFire() //Send a message to the delegate, if there is one.
}
}
And then in your view controller:
//Declare that the view controller conforms to the TimeManagerDelegate protocol
class SomeViewController: UIViewController, TimeManagerDelegate {
//This is the function that gets called on the current delegate
func timerDidFire() {
//Update my clock label (or whatever I need to do in response to a timer update.)
}
override func viewWillAppear() {
super.viewWillAppear()
//Since this view controller is appearing, make it the TimeManager's delegate.
sharedTimerManager.delegate = self
}

PresentViewController presents compile error in static/class functions

I have a menu bar which I want to use on every page(UIViewController) in an app. So I figured I'd just write it once and use it on all the pages, instead of reproducing the same code on every page. To go about that, I created a new class which extends UIViewController(named it MenuBarViewController), with its own XIB file, created a function within that class which contains the code for generating the menubar, and just make a call to that function in any UIViewController where I want to place the menubar. Seems to work pretty seemlessly, but I run into problems when I tried to create some helper functions within the MenuBarViewController class, which trigger a navigation to another view, when one of the menu icons is tapped. I will include a simplified version of the code I'm working with below which illustrates the entirety of the problem.
class MenuBarViewController: UIViewController {
func menuBar(viewController:UIViewController){
let menuBar = UIView(frame:CGRectMake(0,0,100,20 ))
menuBar.backgroundColor = UIColor.blackColor()
viewController.view.addSubview(menuBar) //Add the newly created view to the main view(self)
//Add an icon view to the menubar
let iconView = UIImageView(frame: CGRectMake(5,0,5,5))
menuBar.addSubview(iconView)
//Add a gesture recognizer for the icon view
iconView.userInteractionEnabled = true
let tapHomeButton = UITapGestureRecognizer(target: self, action:#selector(MenuBarViewController.goToHomeView(_:)))
iconView.addGestureRecognizer(tapHomeButton)
//Add an icon to the icon view
let iconImageName = UIImage(named:"home.png")!
iconView.image = iconImageName
}
//Helper function to handle the gesture recognizer, helps transition to another view
func goToHomeView(sender: UIImageView!) {
appData.lastView = "GameViewController"
let settingsVC = SettingsViewController(nibName: "SettingsViewController", bundle: nil)
presentViewController(settingsVC, animated: true, completion: nil)
}
}
Now when I tap on the icon, there is no response. So I figured I'd declare the class functions(menuBar and goToHomeView) as either "class" or "static" funcs, and then call the menu bar directly with the class name. But then I get this compile error "extra argument 'animated' in call" on the this line:
presentViewController(settingsVC, animated: true, completion: nil)
So something must be wrong with the presentViewController method when it is declared within a static or class function because when I have the goToHomeView function just do something as simple as printing some text, it works fine. So I'm at a total loss at this point. What to do? Am I going about this the completely wrong way?

Swift: Best way to get value from view

I have a custom UIView (called GridView) that I initialize and then add to a ViewController (DetailViewController). GridView contains several UIButtons and I would like to know in DetailViewController when those buttons are touched. I'm new to Swift and am wondering what is the best pattern to use to get those events?
If you want to do this with notifications, use 1:
func postNotificationName(_ notificationName: String,
object notificationSender: AnyObject?)
in the method that is triggered by your button. Then, in your DetailViewController, add a listener when it is initialized with 2:
func addObserver(_ notificationObserver: AnyObject,
selector notificationSelector: Selector,
name notificationName: String?,
object notificationSender: AnyObject?)
Both functions can be called from NSNotificationCenter.defaultCenter().
Another method would be to add callbacks which you connect once you initialize the GridView in your DetailViewController. A callback is essentially a closure:
var callback : (() -> Void)?
which you can instantiate when needed, e.g.
// In DetailViewController initialization
gridView = GridView()
gridView.callback = { self.doSomething() }
In GridView you can trigger the callback like this:
func onButton()
{
callback?()
}
The callback will only execute, if unwrapping succeeds. Please ensure, that you have read Automatic Reference Counting, because these constructs may lead to strong reference cycles.
What's the difference? You can connect the callback only once (at least with the method I've showed here), but when it triggers, the receiver immediately executes its code. For notifications, you can have multiple receivers but there is some delay in event delivery.
Lets assume your GridView implementation is like as follows:
class GridView : UIView {
// Initializing buttons
let button1:UIButton = UIButton(...)
let button2:UIButton = UIButton(...)
// ...
// Adding buttons to view
self.addSubview(button1)
self.addSubview(button2)
// ...
}
Now, we will add selector methods which will be called when a button is touched. Lets assume implementation of your view controller is like as follows:
class DetailViewController : UIViewController {
let myView:GridView = GridView(...)
myView.button1.addTarget(self, action: "actionForButton1:", forControlEvents: UIControlEvents.TouchUpInside)
myView.button2.addTarget(self, action: "actionForButton2:", forControlEvents: UIControlEvents.TouchUpInside)
// ...
func actionForButton1(sender: UIButton!) {
// Your actions when button 1 is pressed
}
// ... Selectors for other buttons
}
I have to say that my example approach is not a good example for encapsulation principles of Object-Oriented Programming, but I have written like this because you are new to Swift and this code is easy to understand. If you want to prevent duplicate codes such as writing different selectors for each button and if you want to set properties of your view as private to prevent access from "outside" like I just did in DetailViewController, there are much much better solutions. I hope it just helps you!
I think you better create a class called GridView that is inherited from the UIView. Then, you can connect all you UI element with you class as IBOutlet or whatever using tag something like that. Later on, you can ask the instance of GridView in DetailViewController so that you can connect as IBAction.
Encapsulation is one of the principles of OOP.

MvvmCross iOS: how to dismiss a view controller when use IMvxModelTouchView

I have used IMvXModelTouchView for a custom popup screen animation. And, I have a close button on this popup view. What is the proper way to switch back to a previous view?
Here is my code look like:
public class PopupView
: MvxViewController, IMvxModalTouchView
{
public PopupView()
{
ModalPresentationStyle = UIModalPresentationStyle.PageSheet;
}
public override void ViewDidLoad()
{
Title = "Map";
base.ViewDidLoad();
var closeButton = new UIButton(new RectangleF(0, 0, 50, 30));
closeButton.TouchUpInside += CloseButtonClicked();
Add(closeButton);
}
private EventHandler CloseButtonClicked()
{
return (sender, args) => NavigationController.DismissViewController(true, null);
}
}
It worked, the first time when I click this close button, but it crash when I tried to pop up this view again.
I suspect you are probably using the standard MvxModalSupportTouchViewPresenter which only expects one modal view/viewModel to be displayed at a time, and which expects that view/viewModel to be cleared using Close(this) from the ViewModel.
See: MvxModalSupportTouchViewPresenter.cs#L29
If you have strong ideas about your UI, then (in my opinion) your best bet is to write your own custom presenter - then you can open/close/hide/show whatever you want. For more on writing custom presenters, see some of the links and videos from: http://slodge.blogspot.com/2013/06/presenter-roundup.html

Resources