Setting action for back button in navigation controller - ios

I'm trying to overwrite the default action of the back button in a navigation controller. I've provided a target an action on the custom button. The odd thing is when assigning it though the backbutton attribute it doesn't pay attention to them and it just pops the current view and goes back to the root:
UIBarButtonItem *backButton = [[UIBarButtonItem alloc]
initWithTitle: #"Servers"
style:UIBarButtonItemStylePlain
target:self
action:#selector(home)];
self.navigationItem.backBarButtonItem = backButton;
As soon as I set it through the leftBarButtonItem on the navigationItem it calls my action, however then the button looks like a plain round one instead of the arrowed back one:
self.navigationItem.leftBarButtonItem = backButton;
How can I get it to call my custom action before going back to the root view? Is there a way to overwrite the default back action, or is there a method that is always called when leaving a view (viewDidUnload doesn't do that)?

Try putting this into the view controller where you want to detect the press:
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
// back button was pressed. We know this is true because self is no longer
// in the navigation stack.
}
[super viewWillDisappear:animated];
}

I've implemented UIViewController-BackButtonHandler extension. It does not need to subclass anything, just put it into your project and override navigationShouldPopOnBackButton method in UIViewController class:
-(BOOL) navigationShouldPopOnBackButton {
if(needsShowConfirmation) {
// Show confirmation alert
// ...
return NO; // Ignore 'Back' button this time
}
return YES; // Process 'Back' button click and pop view controller
}
Download sample app.

Unlike Amagrammer said, it's possible. You have to subclass your navigationController. I explained everything here (including example code).

Swift Version:
(of https://stackoverflow.com/a/19132881/826435)
In your view controller you just conform to a protocol and perform whatever action you need:
extension MyViewController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool {
performSomeActionOnThePressOfABackButton()
return false
}
}
Then create a class, say NavigationController+BackButton, and just copy-paste the code below:
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool
}
extension UINavigationController {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
// Prevents from a synchronization issue of popping too many navigation items
// and not enough view controllers or viceversa from unusual tapping
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
var shouldPop = true
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
shouldPop = viewController.shouldPopOnBackButtonPress()
}
if (shouldPop) {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
// Prevent the back button from staying in an disabled state
for view in navigationBar.subviews {
if view.alpha < 1.0 {
UIView.animate(withDuration: 0.25, animations: {
view.alpha = 1.0
})
}
}
}
return false
}
}

It isn't possible to do directly. There are a couple alternatives:
Create your own custom UIBarButtonItem that validates on tap and pops if the test passes
Validate the form field contents using a UITextField delegate method, such as -textFieldShouldReturn:, which is called after the Return or Done button is pressed on the keyboard
The downside of the first option is that the left-pointing-arrow style of the back button cannot be accessed from a custom bar button. So you have to use an image or go with a regular style button.
The second option is nice because you get the text field back in the delegate method, so you can target your validation logic to the specific text field sent to the delegate call-back method.

For some threading reasons, the solution mentionned by #HansPinckaers wasn't right for me, but I found a very easier way to catch a touch on the back button, and I wanna pin this down here in case this could avoid hours of deceptions for someone else.
The trick is really easy : just add a transparent UIButton as a subview to your UINavigationBar, and set your selectors for him as if it was the real button!
Here's an example using Monotouch and C#, but the translation to objective-c shouldn't be too hard to find.
public class Test : UIViewController {
public override void ViewDidLoad() {
UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
b.BackgroundColor = UIColor.Clear; //making the background invisible
b.Title = string.Empty; // and no need to write anything
b.TouchDown += delegate {
Console.WriteLine("caught!");
if (true) // check what you want here
NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
};
NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
}
}
Fun fact : for testing purposes and to find good dimensions for my fake button, I set its background color to blue... And it shows behind the back button! Anyway, it still catches any touch targetting the original button.

Overriding navigationBar(_ navigationBar:shouldPop): This is not a good idea, even if it works. for me it generated random crashes on navigating back. I advise you to just override the back button by removing the default backButton from navigationItem and creating a custom back button like below:
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction)
...
}
========================================
Building on previous responses with UIAlert in Swift5 in a Asynchronous way
protocol NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ())
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < navigationBar.items!.count {
return true
}
// Check if we have a view controller that wants to respond to being popped
if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
viewController.shouldPopOnBackButtonPress { shouldPop in
if (shouldPop) {
/// on confirm => pop
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
/// on cancel => do nothing
}
}
/// return false => so navigator will cancel the popBack
/// until user confirm or cancel
return false
}else{
DispatchQueue.main.async {
self.popViewController(animated: true)
}
}
return true
}
}
On your controller
extension MyController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress(_ completion: #escaping (Bool) -> ()) {
let msg = "message"
/// show UIAlert
alertAttention(msg: msg, actions: [
.init(title: "Continuer", style: .destructive, handler: { _ in
completion(true)
}),
.init(title: "Annuler", style: .cancel, handler: { _ in
completion(false)
})
])
}
}

This technique allows you to change the text of the "back" button without affecting the title of any of the view controllers or seeing the back button text change during the animation.
Add this to the init method in the calling view controller:
UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];
temporaryBarButtonItem.title = #"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];

Easiest way
You can use the UINavigationController's delegate methods. The method willShowViewController is called when the back button of your VC is pressed.do whatever you want when back btn pressed
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;

Here's my Swift solution. In your subclass of UIViewController, override the navigationShouldPopOnBackButton method.
extension UIViewController {
func navigationShouldPopOnBackButton() -> Bool {
return true
}
}
extension UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
if let vc = self.topViewController {
if vc.navigationShouldPopOnBackButton() {
self.popViewControllerAnimated(true)
} else {
for it in navigationBar.subviews {
let view = it as! UIView
if view.alpha < 1.0 {
[UIView .animateWithDuration(0.25, animations: { () -> Void in
view.alpha = 1.0
})]
}
}
return false
}
}
return true
}
}

Found a solution which retains the back button style as well.
Add the following method to your view controller.
-(void) overrideBack{
UIButton *transparentButton = [[UIButton alloc] init];
[transparentButton setFrame:CGRectMake(0,0, 50, 40)];
[transparentButton setBackgroundColor:[UIColor clearColor]];
[transparentButton addTarget:self action:#selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
[self.navigationController.navigationBar addSubview:transparentButton];
}
Now provide a functionality as needed in the following method:
-(void)backAction:(UIBarButtonItem *)sender {
//Your functionality
}
All it does is to cover the back button with a transparent button ;)

I don't believe this is possible, easily. The only way I believe to get around this is to make your own back button arrow image to place up there. It was frustrating for me at first but I see why, for consistency's sake, it was left out.
You can get close (without the arrow) by creating a regular button and hiding the default back button:
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;

There's an easier way by just subclassing the delegate method of the UINavigationBar and override the ShouldPopItemmethod.

This approach worked for me (but the "Back" button will not have the "<" sign):
- (void)viewDidLoad
{
[super viewDidLoad];
UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:#"Back"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(backButtonClicked)];
self.navigationItem.leftBarButtonItem = backNavButton;
}
-(void)backButtonClicked
{
// Do something...
AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[delegate.navController popViewControllerAnimated:YES];
}

onegray's solution is not safe.According to the official documents by Apple,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html,
we should avoid doing that.
"If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes."

Using Swift:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}

Here is Swift 3 version of #oneway's answer for catching navigation bar back button event before it gets fired. As UINavigationBarDelegate cannot be used for UIViewController, you need to create a delegate that will be triggered when navigationBar shouldPop is called.
#objc public protocol BackButtonDelegate {
#objc optional func navigationShouldPopOnBackButton() -> Bool
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if viewControllers.count < (navigationBar.items?.count)! {
return true
}
var shouldPop = true
let vc = self.topViewController
if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
shouldPop = vc.navigationShouldPopOnBackButton()
}
if shouldPop {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
} else {
for subView in navigationBar.subviews {
if(0 < subView.alpha && subView.alpha < 1) {
UIView.animate(withDuration: 0.25, animations: {
subView.alpha = 1
})
}
}
}
return false
}
}
And then, in your view controller add the delegate function:
class BaseVC: UIViewController, BackButtonDelegate {
func navigationShouldPopOnBackButton() -> Bool {
if ... {
return true
} else {
return false
}
}
}
I've realised that we often want to add an alert controller for users to decide whether they wanna go back. If so, you can always return false in navigationShouldPopOnBackButton() function and close your view controller by doing something like this:
func navigationShouldPopOnBackButton() -> Bool {
let alert = UIAlertController(title: "Warning",
message: "Do you want to quit?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
present(alert, animated: true, completion: nil)
return false
}
func yes() {
print("yes")
DispatchQueue.main.async {
_ = self.navigationController?.popViewController(animated: true)
}
}
func no() {
print("no")
}

Swift 4 iOS 11.3 Version:
This builds on the answer from kgaidis from https://stackoverflow.com/a/34343418/4316579
I am not sure when the extension stopped working, but at the time of this writing (Swift 4), it appears that the extension will no longer be executed unless you declare UINavigationBarDelegate conformity as described below.
Hope this helps people that are wondering why their extension no longer works.
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
}
}

By using the target and action variables that you are currently leaving 'nil', you should be able to wire your save-dialogs in so that they are called when the button is "selected". Watch out, this may get triggered at strange moments.
I agree mostly with Amagrammer, but I don't think it would be that hard to make the button with the arrow custom. I would just rename the back button, take a screen shot, photoshop the button size needed, and have that be the image on the top of your button.

You can try accessing the NavigationBars Right Button item and set its selector property...heres a reference UIBarButtonItem reference, another thing if this doenst work that will def work is, set the right button item of the nav bar to a custom UIBarButtonItem that you create and set its selector...hope this helps

For a form that requires user input like this, I would recommend invoking it as a "modal" instead of part of your navigation stack. That way they have to take care of business on the form, then you can validate it and dismiss it using a custom button. You can even design a nav bar that looks the same as the rest of your app but gives you more control.

To intercept the Back button, simply cover it with a transparent UIControl and intercept the touches.
#interface MyViewController : UIViewController
{
UIControl *backCover;
BOOL inhibitBackButtonBOOL;
}
#end
#implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Cover the back button (cannot do this in viewWillAppear -- too soon)
if ( backCover == nil ) {
backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
// show the cover for testing
backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
[backCover addTarget:self action:#selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
UINavigationBar *navBar = self.navigationController.navigationBar;
[navBar addSubview:backCover];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[backCover removeFromSuperview];
backCover = nil;
}
- (void)backCoverAction
{
if ( inhibitBackButtonBOOL ) {
NSLog(#"Back button aborted");
// notify the user why...
} else {
[self.navigationController popViewControllerAnimated:YES]; // "Back"
}
}
#end

At least in Xcode 5, there is a simple and pretty good (not perfect) solution. In IB, drag a Bar Button Item off the Utilities pane and drop it on the left side of the Navigation Bar where the Back button would be. Set the label to "Back." You will have a functioning button that you can tie to your IBAction and close your viewController. I'm doing some work and then triggering an unwind segue and it works perfectly.
What isn't ideal is that this button does not get the < arrow and does not carry forward the previous VCs title, but I think this can be managed. For my purposes, I set the new Back button to be a "Done" button so it's purpose is clear.
You also end up with two Back buttons in the IB navigator, but it is easy enough to label it for clarity.

Swift
override func viewWillDisappear(animated: Bool) {
let viewControllers = self.navigationController?.viewControllers!
if indexOfArray(viewControllers!, searchObject: self) == nil {
// do something
}
super.viewWillDisappear(animated)
}
func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
for (index, value) in enumerate(array) {
if value as UIViewController == searchObject as UIViewController {
return index
}
}
return nil
}

Found new way to do it :
Objective-C
- (void)didMoveToParentViewController:(UIViewController *)parent{
if (parent == NULL) {
NSLog(#"Back Pressed");
}
}
Swift
override func didMoveToParentViewController(parent: UIViewController?) {
if parent == nil {
println("Back Pressed")
}
}

Swift version of #onegray's answer
protocol RequestsNavigationPopVerification {
var confirmationTitle: String { get }
var confirmationMessage: String { get }
}
extension RequestsNavigationPopVerification where Self: UIViewController {
var confirmationTitle: String {
return "Go back?"
}
var confirmationMessage: String {
return "Are you sure?"
}
}
final class NavigationController: UINavigationController {
func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
popViewControllerAnimated(true)
return true
}
let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
dispatch_async(dispatch_get_main_queue(), {
let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
UIView.animateWithDuration(0.25) {
dimmed.forEach { $0.alpha = 1 }
}
})
return
})
alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
dispatch_async(dispatch_get_main_queue(), {
self.popViewControllerAnimated(true)
})
})
presentViewController(alertController, animated: true, completion: nil)
return false
}
}
Now in any controller, just conform to RequestsNavigationPopVerification and this behaviour is adopted by default.

Use isMovingFromParentViewController
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
// current viewController is removed from parent
// do some work
}
}

The answer from #William is correct however, if the user starts a swipe-to-go-back gesture the viewWillDisappear method is called and even self won't be in the navigation stack (that is, self.navigationController.viewControllers won't contain self), even if the swipe is not completed and the view controller is not actually popped. Thus, the solution would be to:
Disable the swipe-to-go-back gesture in viewDidAppear and only allow using the back button, by using:
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)])
{
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Or simply use viewDidDisappear instead, as follows:
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if (![self.navigationController.viewControllers containsObject:self])
{
// back button was pressed or the the swipe-to-go-back gesture was
// completed. We know this is true because self is no longer
// in the navigation stack.
}
}

The solution I have found so far is not very nice, but it works for me. Taking this answer, I also check whether I'm popping programmatically or not:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ((self.isMovingFromParentViewController || self.isBeingDismissed)
&& !self.isPoppingProgrammatically) {
// Do your stuff here
}
}
You have to add that property to your controller and set it to YES before popping programmatically:
self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

Related

VoiceOver Z gesture won't trigger when UIAlertController is active

I'm trying to use the Z gesture to dismiss a UIAlertController. I have a very simple app. It has a single view with 1 button. Tapping the button presents an alert. I have implemented
- (BOOL)accessibilityPerformEscape {
NSLog(#"Z gesture");
return YES;
}
With VoiceOver on, scrubbing the screen prints out "Z gesture," but when I press the button and the alert is visible, scrubbing the screen does nothing, the method is not called and nothing is printed. What do I have to do to get this to function while the alert is on screen?
Thanks...
To get the desired result on your alert view thanks to the scrub gesture, override accessibilityPerformEscape() in the alert view itself.
A solution could be to implement this override in an UIView extension as follows :
extension UIView {
override open func accessibilityPerformEscape() -> Bool {
if let myViewController = self.findMyViewController() as? UIAlertController {
myViewController.dismiss(animated: true,
completion: nil)
}
return true
}
private func findMyViewController() -> UIViewController? {
if let nextResponder = self.next as? UIViewController {
return nextResponder
} else if let nextResponder = self.next as? UIView {
return nextResponder.findMyViewController()
} else {
return nil
}
}
}
The code is short enough to be understood without further explanation. If it's not clear, don't hesitate to ask.
The function findMyViewController has been found here.

UIControl blocking all my views on iPhone

I got a iPad-designed app using a SplitViewController showing two views, one with a contacts list and another with details of this contact. The SplitView works well on iPad but has some problems on iPhones.
There is a UIControl that take all the size of the Master View, that check if there is any .touchDown interaction by the user and some methods called to enable or disable this UIControl depending if we are on editing contact mode or not allowing user to interact with the screen or not :
private var disableInteractionClosure: (()->())?
private lazy var interactionOverlay: UIControl = {
let c = UIControl()
c.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
c.addTarget(self, action: "interactionOverlayAction", forControlEvents: .TouchDown)
return c
}()
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
navigationController!.view.addSubview(interactionOverlay)
}
}
func interactionOverlayAction() {
disableInteractionClosure!()
}
func enableInteraction() {
disableInteractionClosure = nil
interactionOverlay.removeFromSuperview()
}
Basically the UIControl is used to block user from switching between contact while user is editing another contact/ creating a new one by blocking interaction with the contact list and if changes have been made on the editing/creating view it fires a method that shows a pop up saying "modifications have been made do you want to continue without saving ? cancel or continue " :
func cancel() {
self.view.endEditing(true)
let closure: ()->() = {
self.layoutView.willResign()
self.delegate?.tabDetailsControllerDidCancel(self)
}
if layoutView.hasChanges() {
MKAlertViewController().instantaneousAlert(title: "Modification apportées", message: "Êtes-vous sur de vouloir continuer sans sauvegarder les modifications ?", confirmButtonTitle: "Oui", cancelButtonTitle: "Annuler", confirmed: { () -> Void in
closure()
}, canceled: { () -> Void in
})
} else {
closure()
}
}
It works fine on iPad because the UIControl is only above the Master View and is enabled when in editing mode on the Detail View (iPad 3D Debugging view), so the pop up shows only when manually cancelling the editing/creating or when trying to change contact while editing/creating, but as the splitView don't work the same on iPads and iPhones and it appears that on iPhone the Master View is placed above the Detail View, the UIControl is also above (iPhone 3D Debugging view), and it causes to block interactions on all the screen and wherever I click the cancel pop-up is shown.
Can you guys explain me a way to enable/show this UIControl only when the MasterView is showing and not everywhere. Thanks.
I ended up using the viewWillDisappear on the detail view :
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParentViewController() || self.isBeingDismissed() {
if editingMode {
shared.contactsListController.disableInteractionWhileEditingWithClosure({ (_) in
self.tabDetailsController.cancel()
})
shared.contactsListController.disableToolbar()
} else {
shared.contactsListController.enableInteraction()
shared.contactsListController.enableToolbar()
}
self.navigationController?.toolbar.alpha = 1
}
}
and modifying the disableInteractionWhileEditingWithClosure method on the master view:
func disableInteractionWhileEditingWithClosure(callback: ()->()) {
if disableInteractionClosure == nil {
disableInteractionClosure = callback
/* display control overlay */
interactionOverlay.frame = navigationController!.view.bounds
view.addSubview(interactionOverlay) // this line
}
}
and it works ! :)

Popover notification view, independent of current view controller in swift

I want to notify users that an action has been completed in the background. Currently, the AppDelegate receives notification of this:
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// do something
}
}
I'd really like to display a pop over notification (e.g. "Awarded points to 10 students") over the currently active view.
I know how to do this with NSNotification, but that means I have to add a listener to each of the views. An alternative to that would be great!
The next part of question is how do I actually get the view to fade in and then fade out again in front of whatever view I have - be that a table view, collection view or whatever else. I've tried the following code (in the viewDidLoad for the sake of testing):
override func viewDidLoad() {
// set up views
let frame = CGRectMake(0, 200, 320, 200)
let notificationView = UIView(frame: frame)
notificationView.backgroundColor = UIColor.blackColor()
let label = UILabel()
label.text = "Hello World"
label.tintColor = UIColor.whiteColor()
// add the label to the notification
notificationView.addSubview(label)
// add the notification to the main view
self.view.addSubview(notificationView)
print("Notification should be showing")
// animate out again
UIView.animateWithDuration(5) { () -> Void in
notificationView.hidden = true
print("Notification should be hidden")
}
}
The view does appear without the hiding animation, but with that code in it hides straight away. I'm also not sure how to stick this to the bottom of the view, although perhaps that's better saved for another question. I assume I'm doing a few things wrong here, so any advice pointing me in the right direction would be great! Thanks!
For your notification issue, maybe UIAlertController suits your needs?
This would also solve your issues with fading in/out a UIView
func didRecieveAPIResults(originalRequest: String, apiResponse: APIResponse) {
if(originalRequest == "actionName") {
// Creates an UIAlertController ready for presentation
let alert = UIAlertController(title: "Score!", message: "Awarded points to 10 students", preferredStyle: UIAlertControllerStyle.Alert)
// Adds the ability to close the alert using the dismissViewControllerAnimated
alert.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.Cancel, handler: { action in alert.dismissViewControllerAnimated(true, completion: nil)}))
// Presents the alert on top of the current rootViewController
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
UIAlertController
When adding a subview you want to be on top of everything else, do this:
self.view.addSubview(notificationView)
self.view.bringSubviewToFront(notificationView)
Fading a UIView by changing the alpha directly:
For testing, you should be calling this in your viewDidAppear so that the fading animation starts after the view actually is shown.
// Hides the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
// Displays the view
UIView.animateWithDuration(5) { () -> Void in
notificationView.alpha = 0
}
This solution takes up unnecessary space in your code, I would recommend extensions for this purpose.
Extensions:
Create a Extensions.swift file and place the following code in it.
Usage: myView.fadeIn(), myView.fadeOut()
import UIKit
extension UIView {
// Sets the alpha to 0 over a time period of 0.15 seconds
func fadeOut(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 0
})
}
// Sets the alpha to 1 over a time period of 0.15 seconds
func fadeIn(){
UIView.animateWithDuration(0.15, animations: {
self.alpha = 1
})
}
}
Swift 2.1 Extensions
Hope this helps! :)

Make UILabel focusable and tappable (tvOS)

I'm trying to implement 6 lines high description label and I want it to be focusable. Ideally that would mean extending UILabel class to make a custom component. I tried that by implementing canBecomeFocused and didUpdateFocusInContext but my UILabel doesn't seem to get any focus.
I also tried replacing UILabel with UIButton, but buttons aren't really optimised for this sort of thing. Also that would mean I'd need to change buttonType on focus from custom to plain.. and buttonType seems to be a ready-only property.
In reality I'd like to have exact same text label implemented by Apple in Apple TV Movies app. For movie description they have a text label that displays a few lines of text and a "more". When focused it looks like a button (shadows around) and changed background color. When tapped - it opens up a modal window with entire movie description.
Any suggestions? Or maybe someone has already implemented this custom control for tvOS? Or event better - there is a component from Apple that does this and I'm missing something.
P.S: Swift solution would be welcome :)
Ok, answering my own question :)
So it appears that some some views are "focusable" on tvOS out-of-the-box, and other have to be instructed to do so.
I finally ended up using UITextView, which has a selectable property, but if not one of these focusable views by default. Editing of TextView has to be disabled to make it look like UILabel. Also, currently there is a bug which prevents you from using selectable property from Interface Builder but works from code.
Naturally, canBecomeFocused() and didUpdateFocusInContext had to be implemented too. You'll also need to pass a UIViewController because UITextView is not capable of presenting a modal view controller. Bellow is what I ended up creating.
class FocusableText: UITextView {
var data: String?
var parentView: UIViewController?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: "tapped:")
tap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.addGestureRecognizer(tap)
}
func tapped(gesture: UITapGestureRecognizer) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let descriptionView = storyboard.instantiateViewControllerWithIdentifier("descriptionView") as? DescriptionViewController {
if let view = parentView {
if let show = show {
descriptionView.descriptionText = self.data
view.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
view.presentViewController(descriptionView, animated: true, completion: nil)
}
}
}
}
override func canBecomeFocused() -> Bool {
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
} else if context.previouslyFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
}
}
As for making a UILabel focusable:
class MyLabel: UILabel {
override var canBecomeFocused: Bool {
return true
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator)
backgroundColor = context.nextFocusedView == self ? .blue:.red
}
}
IMPORTANT!!!
As stated on the apple developer portal:
The value of this property is true if the view can become focused; false otherwise.
By default, the value of this property is false. This property informs the focus engine if a view is capable of being focused. Sometimes even if a view returns true, a view may not be focusable for the following reasons:
The view is hidden.
The view has alpha set to 0.
The view has userInteractionEnabled set to false.
The view is not currently in the view hierarchy.
Use a collection view with just one cell and add transform to cell and change cell background color in didUpdateFocusInContext when focus moves to cell.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.focused {
self.transform = CGAffineTransformMakeScale(1.01, 1.01)
self.backgroundColor = UIColor.whiteColor()
self.textLabel.textColor = .blackColor()
}
else {
self.transform = CGAffineTransformMakeScale(1, 1)
self.backgroundColor = UIColor.clearColor()
self.textLabel.textColor = .whiteColor()
}
}, completion: nil)
}
As an additional step you could try to extract the color of the image if you are using the image as background like iTunes and use that for Visual effect view behind the cell.
Also you can apply transform to the collectionView in the video controller to make it look like in focus
You can use system button, and set the background image in storyboard to an image that contains the color you would like

How do you keep the cancel button in the search bar enabled when the keyboard is dismissed?

I'm trying to achieve the same effect as Apple's Contacts app (left screenshot). The cancel button in UISearchBar is enabled even when the keyboard is dismissed. My app behaves differently (right screenshot). The cancel button automatically becomes disabled when the keyboard is dismissed. The user is forced to tap the cancel button one time to enable it and then another time to actually trigger the dismissal. This is not good user experience. How would I always keep the cancel button enabled like Apple's Contacts app?
Technical Details:
I'm not using UISearchDisplayController due to some design requirements. This is just a UISearchBar with my own custom search controller. The cancel button is shown using [self.searchBar showsCancelButton:YES animated:YES]. The keyboard is dismissed using [self.searchBar resignFirstResponder].
Call to [self.searchBar resignFirstResponder] will make the cancel button disabled. Hence, you should always update cancel button to enable after calling it.
Objective-C
[searchBar resignFirstResponder];
UIButton *cancelButton = (UIButton *)[searchBar valueForKey:#"cancelButton"];
[cancelButton setEnabled:YES];
Swift
searchBar.resignFirstResponder()
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton {
cancelButton.isEnabled = true
}
In my experience, view.endEditing(true) is the problem. Because it's also called .resignFirstResponder if there's a UITextField inside the view, which is contained in UISearchBar.
https://developer.apple.com/reference/uikit/uiview/1619630-endediting
For Swift 4.0
if let cancelButton : UIButton = searchBar.value(forKey: "cancelButton") as? UIButton{
cancelButton.isEnabled = true
}
This is what worked for me to handle any dismissal such as searchBar.resignFirstResponder(), view.endEditing(false), interactive swipe to dismiss, presenting a view controller, etc.
extension ViewController: UISearchBarDelegate {
func searchBarShouldEndEditing(_ searchBar: UISearchBar) -> Bool {
//cancel button becomes disabled when search bar isn't first responder, force it back enabled
DispatchQueue.main.async {
if let cancelButton = searchBar.value(forKey: "cancelButton") as? UIButton {
cancelButton.isEnabled = true
}
}
return true
}
}
Making sure to set searchBar.delegate = self.
You can use the runtime API to access the cancel button.
UIButton *btnCancel = [self.searchBar valueForKey:#"_cancelButton"];
[btnCancel setEnabled:YES];
As far as your question is concerned, there is no way you can enable the cancel button when the keyboard is dismissed, like there is no callback as such.
Since iOS 7 all the subview of UISearchBar are one level deeper. This should work:
for (UIView *subView in searchBar.subviews) {
for (UIView *secondLevelSubview in subView.subviews) {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton *)view setEnabled:YES];
}
}
Still hacky and can easily break in the next iOS version.
You could do this:
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
[self enableCancelButton];
}
- (void)enableCancelButton {
for (UIView *view in _seachBar.subviews) {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton *)view setEnabled:YES];
}
}
}
BUT this is a pretty hackish method and I'm fairly certain it's generally frowned upon by Apple and could potentially lead to the app being rejected. As far as I know, there doesn't seem to be any other way to do what you're trying to do.
Here's a recursive solution that is working for me.
func enableButtons(_ view:UIView) {
for subView in view.subviews {
enableButtons(subView)
}
if let buttonView = view as? UIButton {
buttonView.isEnabled = true
}
}
Try this simple solution, works perfect for me
extension UISearchBar {
func enableCancelButton(in view: UIView) {
view.subviews.forEach {
enableCancelButton(in: $0)
}
if let cancelButton = view as? UIButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
}
}
}
extension ViewController: UISearchBarDelegate {
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
searchBar.enableCancelButton(in: searchBar)
}
}
}
Implement the below searchBarShouldEndEditing delegate method in your code. Hope it will helpful.
(BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar
{
[[searchBar valueForKey:#"_cancelButton"] setEnabled:YES];
return YES;
}
Here's a simple way:
searchBar.resignFirstResponder()
(searchBar.value(forKey: "_cancelButton") as? UIButton)?.isEnabled = true

Resources