segmentedControl and TextView refresh/reload - xcode - objective-c

I've got the following code which is a segmentedControl for a language selection option. Case 0 is for English and case 1 is for Greek which is then passed to a UITextView. The problem is that every time I press the button for case 1 or 0 I have to go back to the previous view controller which is a table view controller and come back to the "detailViewController" for the correct language to be displayed. Is there a way for this to be done without switching back and forth between the view controllers? Something like a small animation or a refresh or a reload method for UITextView.
int a;
- (IBAction)languageSeg:(id)sender {
switch (((UISegmentedControl *)sender).selectedSegmentIndex) {
case 0:
a=0;
break;
case 1:
a=1;
break;
default:
break;
}
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
UIImage *saladImage = [UIImage imageNamed:#"salad.jpg"];
UIImage *fishImage = [UIImage imageNamed:#"fish.jpg"];
[textDetail setScrollEnabled:YES];
[textDetail setContentSize:CGSizeMake(320, 190)];
//Switch the UIImages and UILabel based on item
switch (itemNumber) {
case 0:
itemName.text = #"Salad";
itemPic.image = saladImage;
self.title = #"Salads";
if (a==0){
textDetail.text = #"description in english";
}
if (a==1){
textDetail.text = #"description in greek";
}
break;
case 1:
// etc.

You can update the text for the textView in the languageSeg: method the same way you set it initially in viewWillAppear:
I would create a separate method that handles the language switch, then call that method from both viewWillAppear and languageSeg and anywhere else you need it.
For example, your language setter method would include the code currently at the bottom of viewWillAppear:
- (void) languageSetter {
switch (itemNumber) {
case 0:
itemName.text = #"Salad";
itemPic.image = saladImage;
self.title = #"Salads";
if (a==0){
textDetail.text = #"description in english";
}
if (a==1){
textDetail.text = #"description in greek";
}
break;
case 1:
// etc.
break;
default:
// default case
break;
}
}
Then your viewWillAppear: method would look like this:
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
UIImage *saladImage = [UIImage imageNamed:#"salad.jpg"];
UIImage *fishImage = [UIImage imageNamed:#"fish.jpg"];
[textDetail setScrollEnabled:YES];
[textDetail setContentSize:CGSizeMake(320, 190)];
[self languageSetter];
}
And your languageSeg: method would look like this:
- (IBAction)languageSeg:(id)sender {
switch (((UISegmentedControl *)sender).selectedSegmentIndex) {
case 0:
a=0;
break;
case 1:
a=1;
break;
default:
break;
}
[self languageSetter];
}
A lot of implementation would be dependent on how the rest of your code is set up and your individual needs, but this is the general idea.

I have an app with a language selector, and I use NSUserDefaults. Works like a charm.
In the view controller with the picker:
- (IBAction)languageSeg:(id)sender {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
switch (((UISegmentedControl *)sender).selectedSegmentIndex) {
case 0:
[defaults setInteger:0 forKey:#"language"];
break;
case 1:
[defaults setInteger:1 forKey:#"language"];
break;
default:
break;
}
}
And In the vieWillAppear in the viewController displaying the language:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults integerForKey:#"language"] == 0) {
textDetail.text = #"description in english";
}else{
textDetail.text = #"description in greek";
}
Now, in the viewDidLoad method, you need to give it a default value if there is no value set by the user yet, like this: (again in your view controller displaying the setting, not the picker view.)
if (![defaults integerForKey:#"language"]) {
[defaults setInteger:0 forKey:#"language"];
}
textDetail.text = #"description in english";

Where is the switch (itemNumber) got called?
If it's in methods like viewDidLoad or viewWillAppear etc, it will only be called when this view shows up.
You may need to extract this part as a method, and also put it in (IBAction)languageSeg:(id)sender. For example:
- (void)updateView
{
switch (itemNumber) {
case 0:
itemName.text = #"Salad";
itemPic.image = saladImage;
self.title = #"Salads";
if (a==0){
textDetail.text = #"description in english";
}
if (a==1){
textDetail.text = #"description in greek";
}
break;
case 1:
// etc.
}
Then call this method in viewDidLoad and
languageSeg: after switch block.

Related

How to add 'cancel' button to camera acquisition code?

I have this code which captures a barcode; it works fine, except on an iPad, if you can't/don't hold the iPad steady, it "tries until the cows come home" if you get my drift. I want to add a "cancel" button or figure out some way to cancel the method so I don't have to kill the app and start it again. Here is my code:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
CGRect highlightViewRect = CGRectZero;
AVMetadataMachineReadableCodeObject *barCodeObject;
NSString *detectionString = nil;
NSArray *barCodeTypes = #[AVMetadataObjectTypeEAN13Code];
for (AVMetadataObject *metadata in metadataObjects) {
for (NSString *type in barCodeTypes) {
if ([metadata.type isEqualToString:type])
{
barCodeObject = (AVMetadataMachineReadableCodeObject *)[_prevLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metadata];
highlightViewRect = barCodeObject.bounds;
detectionString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue];
break;
}
}
if (detectionString != nil) {
_label.text = detectionString;
oISBNField.text = detectionString; // move detectionString to ISBN textbox
[_session stopRunning];
[_prevLayer removeFromSuperlayer];
[_label removeFromSuperview];
break;
}
else
_label.text = #"(none)";
}
}
Can someone please give me some help on this? I would really, really appreciate it! :D
Just create a cancel button and add it to the UIViewController's view.
When the button is pressed, stop the capture session and dismiss the presented view controller.
- (void)cancelButtonPressed:(id)sender {
[self.captureSession stopRunning]; //stop the capture session
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; // dismiss the current view controller
}

Objective-C: Why is my if statement not working?

Here i have some code that checks if the iphone has a camera flash. And then check if my switch is on by using this code:
if(switch1.on){
}
The light turns on fine without this code and i have also tried this code as well:
if(!(switch1.on)){
}
and that successfully turns my camera flash on but also turns it on even when the switch is set to off
Here is my full code:
-(IBAction)torchon:(id)sender{
AVCaptureDevice *flashlight = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([flashlight isTorchAvailable] & [flashlight isTorchModeSupported:AVCaptureTorchModeOn]) {
BOOL success = [flashlight lockForConfiguration:Nil];
if(success){
if(switch1.on){ //// or (!(switch1.on))
on.hidden = YES;
[UIScreen mainScreen].brightness = 1.0;
[flashlight setTorchMode:AVCaptureTorchModeOn];
[flashlight unlockForConfiguration];
}
}
}
}
any help would be appreciated
**EDIT:**
Here is the code where i set the switch to no using ibactions when the value of the switch has changed. This code works successfully:
-(IBAction)switch1{
if (switch1.on) {
switch1.on = YES;
switch2.on = NO;
} else{
switch2.on = YES;
switch1.on = NO;
}
}
-(IBAction)switch2{
if (switch2.on) {
switch2.on = YES;
switch1.on = NO;
} else{
switch1.on = YES;
switch2.on = NO;
}
}
Answer
First add a bool:
bool yes;
then set that bool in the viewdidload
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
yes = YES;
}
After that set it to YES when the ibaction is called and set it to NO once the switch is turned off:
-(IBAction)switch1{
if (swich1.on) {
swich1.on = YES;
swich2.on = NO;
yes = YES;
}
}
-(IBAction)switch2{
if (swich2.on) {
swich2.on = YES;
swich1.on = NO;
yes = NO;
}
}
lastly where it checks if(switch1.on){} you need to change it to if(yes==YES){} so together that if statement looks like this:
-(IBAction)torchon:(id)sender{
AVCaptureDevice *flashlight = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([flashlight isTorchAvailable] & [flashlight isTorchModeSupported:AVCaptureTorchModeOn]) {
BOOL success = [flashlight lockForConfiguration:Nil];
if(success){
if(yes==YES){
on.hidden = YES;
[UIScreen mainScreen].brightness = 1.0;
[flashlight setTorchMode:AVCaptureTorchModeOn];
[flashlight unlockForConfiguration];
}
}
}
}
The UISwitch has another getter for the on property.
#property(nonatomic, getter=isOn) BOOL on;
It may be better to use .isOn instead of .on for getting the value.
Also, some of your code can be simplified:
-(IBAction)switch1{
[switch2 setOn:!switch1.isOn];
}
-(IBAction)switch2{
[switch1 setOn:!switch2.isOn];
}
It can get even better if you use the sender of the action in your methods.
It seems that your IBAction methods have a similar form as the getter for the switch. It is possible that this could lead to undesired side effects. Refactor your methods to a single method, more in line with good programming practice:
-(IBAction)switchDidChange:(UISwitch*)aSwitch {
UISwitch* otherSwitch = (aSwitch == switch1)? switch2 : switch1;
[otherSwitch setOn:!aSwitch.on animated:YES];
}
Also, in your code the on property does not really seem to influence what is switched on or off. Include this conditionality.

Is there any notification that gets sent when the Keyboard changes (like from NumberPad to Default)

I am trying to figure out how I can get notified when the keyboard changes. What I am trying to do is add a DONE button to keyboard of type 4 & 5 (NumberPad and PhonePad), everything is working fine, except when I transition from a TextField using a Default KB type, The notification that the KeyboardDidAppear isn't being fired.
Here is what I got:
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
}
Then I added a Property for the current KB type and the current TextField being edited:
#pragma mark - Delegate Methods
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
self.currentKBType = textField.keyboardType;
self.curTextField = textField;
return YES;
}
I then make a decision on whether or not to add that DONE button based on the current KB type:
- (void)keyboardDidShow:(NSNotification *)note {
if (self.currentKBType == 4 || self.currentKBType == 5) {
[self addButtonToKeyboard];
}
}
Problem is that the notification fires when the keyboard is displayed, but not when it changes (transitions from one TextField to another that specifies a different KB type.
Any suggestions? Am I missing something?
Got this figured out. Took a little logic, but it works flawlessly. Here is what I did:
Added private properties for:
#property (nonatomic) UIKeyboardType currentKBType;
#property(nonatomic,strong) UITextField *curTextField;
#property(nonatomic,strong) UIButton *doneButton;
#property(nonatomic) BOOL doneButtonDisplayed;
Then added the following logic in the both the TextField delegate method:
-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
self.currentKBType = textField.keyboardType;
if (textField.keyboardType == 4 || textField.keyboardType == 5) {
if (!doneButtonDisplayed) {
[self addButtonToKeyboard];
}
} else {
if (doneButtonDisplayed) {
[self removeButtonFromKeyboard];
}
}
self.curTextField = textField;
return YES;
}
And in the KeyboardDidShowNotification that I signed the VC up for in the viewDidLoad:
- (void)keyboardDidShow:(NSNotification *)note {
if (self.currentKBType == 4 || self.currentKBType == 5) {
if (!doneButtonDisplayed) {
[self addButtonToKeyboard];
}
} else {
if (doneButtonDisplayed) {
[self removeButtonFromKeyboard];
}
}
}
And the two methods referenced in these methods:
- (void)addButtonToKeyboard {
// create custom button
doneButton = [UIButton buttonWithType:UIButtonTypeCustom];
doneButton.frame = CGRectMake(0, 163, 106, 53);
doneButton.adjustsImageWhenHighlighted = NO;
[doneButton setImage:[UIImage imageNamed:#"DoneNormal.png"] forState:UIControlStateNormal];
[doneButton setImage:[UIImage imageNamed:#"DoneHL.png"] forState:UIControlStateHighlighted];
[doneButton addTarget:self action:#selector(resignKeyboard) forControlEvents:UIControlEventTouchUpInside];
// locate keyboard view
if ([[[UIApplication sharedApplication] windows] count] > 1) {
UIWindow* tempWindow = [[[UIApplication sharedApplication] windows] objectAtIndex:1];
UIView* keyboard;
for(int i=0; i<[tempWindow.subviews count]; i++) {
keyboard = [tempWindow.subviews objectAtIndex:i];
// keyboard found, add the button
if([[keyboard description] hasPrefix:#"<UIPeripheralHost"] == YES) {
[keyboard addSubview:doneButton];
self.doneButtonDisplayed = YES;
}
}
}
}
- (void)removeButtonFromKeyboard {
[doneButton removeFromSuperview];
self.doneButtonDisplayed = NO;
}
And finally, the resignKeyboard method that is called whenever the Done button is touched:
-(void)resignKeyboard {
[self.curTextField resignFirstResponder];
self.doneButtonDisplayed = NO;
}
This will add that Done button whenever a NumberPad or PhonePad type keyboard is displayed and remove it (only when it has been added) from the other keyboard types.
Tested and works great.

Dismiss modal view form sheet controller on outside tap

I am presenting a modal view controller as a form sheet and dismissing it when the cancel button, which is a bar button item, is clicked. I need to dismiss it when I tap on outside of that view. Please help me with a reference. Note: my modal view controller is presented with a navigation controller.
#cli_hlt, #Bill Brasky thanks for your answer. I need to dismiss it when tap occurs outside of the modal view which is a form sheet. I am pasting my code below.
-(void)gridView:(AQGridView *)gridView didSelectItemAtIndex:(NSUInteger)index
{
if(adminMode)
{
CHEditEmployeeViewController *editVC = [[CHEditEmployeeViewController alloc] initWithNibName:#"CHEditEmployeeViewController" bundle:nil];
editVC.delegate = self;
editVC.pickedEmployee = employee;
editVC.edit = TRUE;
editVC.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc]initWithRootViewController:editVC];
navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:navigationController animated:YES];
return;
} //the above code is from the view controller which presents the modal view. Please look at the below code too which is from my modal view controller. Please guide me in a proper way. -(void)tapGestureRecognizer {
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view addGestureRecognizer:recognizer];
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
[self dismissModalViewControllerAnimated:YES];
[self.view.window removeGestureRecognizer:sender];
}
}
}
I know this is an old question but this IS possible, despite of what the "right" answer says. Since this was the first result when I was looking for this I decided to elaborate:
This is how you do it:
You need to add a property to the View Controller from where you want to present modally, in my case "tapBehindGesture".
then in viewDidAppear
if(!tapBehindGesture) {
tapBehindGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapBehindDetected:)];
[tapBehindGesture setNumberOfTapsRequired:1];
[tapBehindGesture setCancelsTouchesInView:NO]; //So the user can still interact with controls in the modal view
}
[self.view.window addGestureRecognizer:tapBehindGesture];
And Here is the implementation for tapBehindDetected
- (void)tapBehindDetected:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
//(edited) not working for ios8 above
//CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
CGPoint location = [sender locationInView: self.presentingViewController.view];
//Convert tap location into the local view's coordinate system. If outside, dismiss the view.
if (![self.presentedViewController.view pointInside:[self.presentedViewController.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
if(self.presentedViewController) {
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
}
Just remember to remove tapBehindGesture from view.window on viewWillDisappear to avoid triggering handleTapBehind in an unallocated object.
I solved iOS 8 issue by adding delegate to gesture recognizer
[taprecognizer setDelegate:self];
with these responses
#pragma mark - UIGestureRecognizer Delegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return YES;
}
that works for me with iOS 8 GM
Swift 4 version that works in both portrait and landscape - no swapping of x, y required.
class TapBehindModalViewController: UIViewController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if (self.tapOutsideRecognizer == nil) {
self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapBehind))
self.tapOutsideRecognizer.numberOfTapsRequired = 1
self.tapOutsideRecognizer.cancelsTouchesInView = false
self.tapOutsideRecognizer.delegate = self
self.view.window?.addGestureRecognizer(self.tapOutsideRecognizer)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if(self.tapOutsideRecognizer != nil) {
self.view.window?.removeGestureRecognizer(self.tapOutsideRecognizer)
self.tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
// MARK: - Gesture methods to dismiss this with tap outside
#objc func handleTapBehind(sender: UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.ended) {
let location: CGPoint = sender.location(in: self.view)
if (!self.view.point(inside: location, with: nil)) {
self.view.window?.removeGestureRecognizer(sender)
self.close(sender: sender)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
As far as I can tell none of the answer seem to be working right away in every condition.
My solution (either inherit from it or paste it in):
#interface MyViewController () <UIGestureRecognizerDelegate>
#property (strong, nonatomic) UITapGestureRecognizer *tapOutsideRecognizer;
#end
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (!self.tapOutsideRecognizer) {
self.tapOutsideRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
self.tapOutsideRecognizer.numberOfTapsRequired = 1;
self.tapOutsideRecognizer.cancelsTouchesInView = NO;
self.tapOutsideRecognizer.delegate = self;
[self.view.window addGestureRecognizer:self.tapOutsideRecognizer];
}
}
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// to avoid nasty crashes
if (self.tapOutsideRecognizer) {
[self.view.window removeGestureRecognizer:self.tapOutsideRecognizer];
self.tapOutsideRecognizer = nil;
}
}
#pragma mark - Actions
- (IBAction)close:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self close:sender];
}
}
}
#pragma mark - Gesture Recognizer
// because of iOS8
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
Here is my version that works for iOS 7 and iOS 8 and does not require conditional swapping of coordinates:
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:self.view];
if (![self.view pointInside:location withEvent:nil]) {
[self.view.window removeGestureRecognizer:self.recognizer];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
For iOS 8, you must both implement the UIGestureRecognizer per Martino's answer, and swap the (x,y) coordinates of the tapped location when in landscape orientation. Not sure if this is due to an iOS 8 bug.
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// add gesture recognizer to window
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
recognizer.delegate = self;
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded) {
// passing nil gives us coordinates in the window
CGPoint location = [sender locationInView:nil];
// swap (x,y) on iOS 8 in landscape
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")) {
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
location = CGPointMake(location.y, location.x);
}
}
// convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil]) {
// remove the recognizer first so it's view.window is valid
[self.view.window removeGestureRecognizer:sender];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
#pragma mark - UIGestureRecognizer Delegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return YES;
}
Based on Bart van Kuik's answer and NavAutoDismiss and other great snippets here.
class DismissableNavigationController: UINavigationController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if tapOutsideRecognizer == nil {
tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(DismissableNavigationController.handleTapBehind))
tapOutsideRecognizer.numberOfTapsRequired = 1
tapOutsideRecognizer.cancelsTouchesInView = false
tapOutsideRecognizer.delegate = self
view.window?.addGestureRecognizer(tapOutsideRecognizer)
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if tapOutsideRecognizer != nil {
view.window?.removeGestureRecognizer(tapOutsideRecognizer)
tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
}
func handleTapBehind(sender: UITapGestureRecognizer) {
if sender.state == UIGestureRecognizerState.Ended {
var location: CGPoint = sender.locationInView(nil)
if UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation) {
location = CGPoint(x: location.y, y: location.x)
}
if !view.pointInside(view.convertPoint(location, fromView: view.window), withEvent: nil) {
view.window?.removeGestureRecognizer(sender)
close(sender)
}
}
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Usage:
let vc = MyViewController()
let nc = DismissableNavigationController(rootViewController: vc)
nc.modalPresentationStyle = UIModalPresentationStyle.FormSheet
presentViewController(nc, animated: true, completion: nil)
#yershuachu's answer, in Swift 2:
class ModalParentViewController: UIViewController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if(self.tapOutsideRecognizer == nil) {
self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: "handleTapBehind:")
self.tapOutsideRecognizer.numberOfTapsRequired = 1
self.tapOutsideRecognizer.cancelsTouchesInView = false
self.tapOutsideRecognizer.delegate = self
self.view.window?.addGestureRecognizer(self.tapOutsideRecognizer)
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if(self.tapOutsideRecognizer != nil) {
self.view.window?.removeGestureRecognizer(self.tapOutsideRecognizer)
self.tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
func handleTapBehind(sender: UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.Ended) {
let location: CGPoint = sender.locationInView(nil)
if (!self.view.pointInside(self.view.convertPoint(location, fromView: self.view.window), withEvent: nil)) {
self.view.window?.removeGestureRecognizer(sender)
self.close(sender)
}
}
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Swift 3
class ModalParentViewController: UIViewController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if(self.tapOutsideRecognizer == nil) {
self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapBehind))
self.tapOutsideRecognizer.numberOfTapsRequired = 1
self.tapOutsideRecognizer.cancelsTouchesInView = false
self.tapOutsideRecognizer.delegate = self
appDelegate.window?.addGestureRecognizer(self.tapOutsideRecognizer)
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if(self.tapOutsideRecognizer != nil) {
appDelegate.window?.removeGestureRecognizer(self.tapOutsideRecognizer)
self.tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
// MARK: - Gesture methods to dismiss this with tap outside
func handleTapBehind(sender: UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.ended) {
let location: CGPoint = sender.location(in: nil)
if (!self.view.point(inside: self.view.convert(location, from: self.view.window), with: nil)) {
self.view.window?.removeGestureRecognizer(sender)
self.close(sender: sender)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Ah ok. So I'm afraid thats not quite possible using the presentModalViewController: method. The whole idea of a "modal" view/window/message box/etc. pp. is that the user cannot do anything else than processing whatever the view/window/message box/etc. pp. wants him/her to do.
What you want to do instead is not present a modal view controller, but rather load and show your form view controller the regular way. Note in your master controller that the form is just showing e.g. with a BOOL variable and then handle there any taps that might occur. If your form is showing, dismiss it.
I use it in this form without any problems neither on iOS 7.1 nor iOS 8.3.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.view.window addGestureRecognizer:self.tapBehindGesture];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.view.window removeGestureRecognizer:self.tapBehindGesture];
}
- (UITapGestureRecognizer*)tapBehindGesture
{
if (_tapBehindGesture == nil)
{
_tapBehindGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapBehindRecognized:)];
_tapBehindGesture.numberOfTapsRequired = 1;
_tapBehindGesture.cancelsTouchesInView = NO;
_tapBehindGesture.delegate = self;
}
return _tapBehindGesture;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
FYI, on iOS 13 form sheets now have a standard dismiss gesture - they go away when you touch anywhere inside the modal and swipe it down (set the new isModalInPresentation property to false if you want to prevent that).
I made a navigationController that auto dismiss for iPad
https://github.com/curciosobrinho/NavAutoDismiss
It is just the same code as above, working on iOS 8.
So, all the credits go to the people above.
I just did it to be more generic and easier to use.
You just need to copy BOTH files to your project
Import the header file (.h)
And use your viewcontroller (that you want to show) as the rootViewController.
How to use it:
//Import the .h file
#import "NavDismissViewController.h"
//Instanciate your view controller (the view you want to present)
YourViewController * yourVC = [YourViewController new];
//Instanciate the NavDismissViewController with your view controller as the rootViewController
NavDismissViewController *nav = [[NavDismissViewController alloc] initWithRootViewController:yourVC];
//if you want to change the navigationBar translucent behaviour
[nav.navigationBar setTranslucent:NO];
//Choose the Modal style
nav.modalPresentationStyle=UIModalPresentationFormSheet;
//present your controller
[self presentViewController:nav animated:YES completion:nil];
//Done
Based on some of the questions and with some improvements and updates:
- (void)tapOutsideDetected:(UITapGestureRecognizer *)sender {
if (#available(iOS 13, *)) {} else {
UIView *aView = self.navigationController ? self.navigationController.view : self.view;
CGPoint location = [sender locationInView: aView];
if (![aView pointInside:location withEvent:nil]) {
[self removeGestureRecognizer:sender];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
Where:
- (void) removeGestureRecognizer:(UIGestureRecognizer *) gesture {
if (self.recognizer) {
UIView *aView = self.recognizer.view;
if (aView) {
[aView removeGestureRecognizer:self.recognizer];
}
}
}
In this case, the UITapGestureRecognizer has been applied to the UIViewController

TTModel load method not being called

Issue
Description
The following code results in load method of TTModel not being called. I've stepped it through the debugger, as well as stepping through the TTCatalog application. The only difference between the two that I can see, is that when the catalog sets it's DataSource in the createModel method of the controller, TTModel's load method gets called, whereas mine does not.
About the Code
I've commented the specific areas of code, to tell what they should be doing and what problem is happening, but I included all of it for completion's sake.
You should look at specifically
PositionsController's createModel method
PositionsList's load method
Those are the areas where the issue is, and would be the best place to start.
Code
PositionsController : TTTableViewController
- (id)initWIthNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
self.title = #"Positions";
self.variableHeightRows = NO;
self.navigationBarTintColor = [UIColor colorWithHexString:#"1F455E"];
}
return self;
}
//This method here should result in a call to the PositionsList load method
- (void)createModel
{
PositionsDataSource *ds = [[PositionsDataSource alloc] init];
self.dataSource = ds;
[ds release];
}
- (void)loadView
{
[super loadView];
self.view = [[[UIView alloc] initWithFrame:TTApplicationFrame()] autorelease];
self.tableView = [[[UITableView alloc] initWithFrame:TTApplicationFrame() style:UITableViewStylePlain] autorelease];
self.tableView.backgroundColor = [UIColor colorWithHexString:#"E2E7ED"];
self.tableView.separatorColor = [UIColor whiteColor];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//self.tableView.delegate = self;
[self.view addSubview:self.tableView];
}
//Override UITableViewDelegate creation method, so we can add drag to refresh
- (id<TTTableViewDelegate>) createDelegate {
TTTableViewDragRefreshDelegate *delegate = [[TTTableViewDragRefreshDelegate alloc] initWithController:self];
return [delegate autorelease];
}
PositionsDataSource : TTListDataSource
#implementation PositionsDataSource
#synthesize positionsList = _positionsList;
-(id)init
{
if (self = [super init])
{
_positionsList = [[PositionsList alloc] init];
self.model = _positionsList;
}
return self;
}
-(void)dealloc
{
TT_RELEASE_SAFELY(_positionsList);
[super dealloc];
}
-(void)tableViewDidLoadModel:(UITableView*)tableView
{
self.items = [NSMutableArray array];
}
-(id<TTModel>)model
{
return _positionsList;
}
#end
PositionsList : NSObject <TTModel>
#implementation PositionsList
//============================================================
//NSObject
- (id)init
{
if (self = [super init])
{
_delegates = nil;
loaded = NO;
client = [[Client alloc] init];
}
return self;
}
- (void)dealloc
{
TT_RELEASE_SAFELY(_delegates);
[client dealloc];
[super dealloc];
}
//==============================================================
//TTModel
- (NSMutableArray*)delegates
{
if (!_delegates)
{
_delegates = TTCreateNonRetainingArray();
}
return _delegates;
}
-(BOOL)isLoadingMore
{
return NO;
}
-(BOOL)isOutdated
{
return NO;
}
-(BOOL)isLoaded
{
return loaded;
}
-(BOOL)isEmpty
{
//return !_positions.count;
return NO;
}
-(BOOL)isLoading
{
return YES;
}
-(void)cancel
{
}
//This method is never called, why is that?
-(void)load:(TTURLRequestCachePolicy)cachePolicy more:(BOOL)more
{
//This method is not getting called
//When the PositionsController calls self.datasource, load should be called,
//however it isn't.
[_delegates perform:#selector(modelDidStartLoad:) withObject:self];
[client writeToServer:self dataToSend:#""];
}
-(void)invalidate:(BOOL)erase
{
}
#end
Short answer: return NO instead of YES for isLoading in your PositionList.
For a longer explanation:
If you dig through the Three20 source, you'll find that setting the dataSource on a view controller sets the model, refreshing the model and potentially calling load. Here is the code called when the TTModelViewController refreshes:
- (void)refresh {
_flags.isViewInvalid = YES;
_flags.isModelDidRefreshInvalid = YES;
BOOL loading = self.model.isLoading;
BOOL loaded = self.model.isLoaded;
if (!loading && !loaded && [self shouldLoad]) {
[self.model load:TTURLRequestCachePolicyDefault more:NO];
} else if (!loading && loaded && [self shouldReload]) {
[self.model load:TTURLRequestCachePolicyNetwork more:NO];
} else if (!loading && [self shouldLoadMore]) {
[self.model load:TTURLRequestCachePolicyDefault more:YES];
} else {
_flags.isModelDidLoadInvalid = YES;
if (_isViewAppearing) {
[self updateView];
}
}
}
Your PositionList object is returning YES for isLoading and NO for isLoaded. This means that Three20 thinks your model is loading when it isn't. You may also need to implement shouldLoad if it doesn't return YES by default.