I have a parent view and a modal view with a text box. What I am trying to do is pass whatever is entered into the text box from the modal view and then pass it to a label in the parent view which updates the label to what was entered. I hope that made any sense.
I have been pulling my hair out for a couple of weeks trying to figure this out with no luck. I found many examples and tutorials about segues and passing between views that are being pushed but nothing about modal views and passing back to the parent view.
I have been trying to understand this and need a good example. I kind of understand the prepare for segue concept but for some reason, I just can't figure this one out. Any help on this would be much appreciated and you would be my hero for life lol.
In my project that uses segues, here's how I did it (note that I'm new to iOS, so there's probably "better" ways, and this may be obvious to the iOS veterans):
The short version: define a callback protocol in your modal view controller's .h file. When your modal view controller closes, it checks to see if the presenter implements that protocol and invokes those methods to pass along the data.
So like you said, let's say your modal view controller just gathers a single string value from the user and then they click OK or Cancel. That class might look like this:
#interface MyModalViewController : UIViewController
...
#end
I'm suggesting you add a protocol like this to the same header:
#protocol MyModalViewControllerCallback
-(void) userCancelledMyModalViewController:(MyModalViewController*)vc;
-(void) userAcceptedMyModalViewController:(MyModalViewController*)vc
withInput:(NSString*)s;
#end
Then in MyModalViewController.m, you add a viewDidDisappear with code similar to this:
-(void) viewDidDisappear:(BOOL)animated {
UIViewController* presenter = self.presentingViewController;
// If the presenter is a UINavigationController then we assume that we're
// notifying whichever UIViewController is on the top of the stack.
if ([presenter isKindOfClass:[UINavigationController class]]) {
presenter = [(UINavigationController*)presenter topViewController];
}
if ([presenter conformsToProtocol:#protocol(MyModalViewControllerCallback)]) {
// Assumes the presence of an "accepted" ivar that knows whether they
// accepted or cancelled, and a "data" ivar that has the data that the
// user entered.
if (accepted) {
[presenter userAcceptedMyModalViewController:self withInput:data];
}
else {
[presenter userCancelledMyModalViewController:self];
}
}
[super viewDidDisappear:animated];
}
And finally in the parent view, you implement the new #protocol, e.g. in the .h:
#interface MyParentViewController : UIViewController <MyModalViewControllerCallback>
...
#end
and in the .m:
#implementation MyParentViewController
...
-(void) userCancelledMyModalViewController:(MyModalViewController*)vc {
// Update the text field with something like "They clicked cancel!"
}
-(void) userAcceptedMyModalViewController:(MyModalViewController*)vc
withInput:(NSString*)s {
// Update the text field with the value in s
}
...
#end
Related
I created custom Poster view so it can be reused in multiple collection view cells (just like TVPosterView in TVUIKit). I add it directly to cell content view with all needed constraints.
The problem is when cell is focused this subview doesn't receive focus update (didUpdateFocus..) so I cannot customize it's focused / unfocused constraints etc. It's odd btw that image view inside is getting floating effect.
In case if I specify cell's preferredFocusEnvironments to return [self.posterView] + super. preferredFocusEnvironments, UI behaves as expected, but the collection view delegate method didSelect not called!
Thanks in advance for any help!
Seems didUpdateFocus not called on all subviews for the focused cell and it's system design. From docs:
After the focus is updated to a new view, the focus engine calls this
method on all focus environments that contain either the previously
focused view, the next focused view, or both, in ascending order. You
should override this method to update your app’s state in response to
changes in focus. Use the provided animation coordinator to animate
changes in visual appearance related to the update. For more
information on animation coordinators, see
UIFocusAnimationCoordinator.
Note: So it means didUpdateFocus will be called first on UICollectionViewCell, than on UIViewController subclasses, in ascending order. For subviews you need to manually register customDidUpdateFocus method that will be triggered in notification update. E.g. to update it's appearance we can use notifications (tvOS 11+), please see the example below.
func customDidUpdateFocus(isFocused: Bool, with coordinator: UIFocusAnimationCoordinator) { /* Custom logic to customize appearance */ }
// Register observer
observerToken = NotificationCenter.default.addObserver(forName: UIFocusSystem.didUpdateNotification, object: nil, queue: .main) { [weak self] (note) in
guard let self = self else { return }
guard let context = note.userInfo?[UIFocusSystem.focusUpdateContextUserInfoKey] as? UIFocusUpdateContext else { return }
guard let coordinator = note.userInfo?[UIFocusSystem.animationCoordinatorUserInfoKey] as? UIFocusAnimationCoordinator else { return }
if let prev = context.previouslyFocusedView, self.isDescendant(of: prev) {
self.didUpdateFocus(isFocused: false, with: coordinator)
} else if let next = context.nextFocusedView, self.isDescendant(of: next) {
self.didUpdateFocus(isFocused: true, with: coordinator)
}
}
My UISplitViewController basically works like a charm except that there is an annoying error message displayed when transitioning the first time (first time only!) from the master table view to the detail view.
Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x160015600>.
Both the master and the detail view controller are embedded in a UINavigationController. However, the error only occurs when setting the following (which is necessary for logic behavior on the iPhone):
class MySplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
return true
}
}
It would be great if anyone could provide a solution to this issue, thanks in advance.
BTW: the split view controller was set up in the storyboard
Presenting the detail view controller is done in the tableView:didSelectRowAtIndexPath: method like this:
if let detailViewController = delegate as? DetailViewController {
detailViewController.navigationItem.leftItemsSupplementBackButton = true
detailViewController.navigationItem.leftBarButtonItem = splitViewController!.displayModeButtonItem()
splitViewController!.showDetailViewController(detailViewController.navigationController!, sender: self)
}
Most probably, your first transition from master (UITableView in UIViewController?) to detail (UIViewController) view in your UISplitViewController starts before the active/current view has finished displaying itself.
A possible reason for this is that you are possibly trying to present the first "instance" of the detail view in the viewDidLoad() method of you master UIViewController? In such a case, you app might try to present the detail view prior to master view finished appearing. Note the difference here between view did load a view and view did appear:
override func viewDidLoad()
Description:
Called after the controller's view is loaded into memory.
This method is called after the view controller has loaded its view
hierarchy into memory.
override func viewDidAppear(animated: Bool)
Description:
Notifies the view controller that its view was added to a view
hierarchy. You can override this method to perform additional tasks
associated with presenting the view.
Now, as you question doesn't show how you load your initial detail view, the following advice is maybe already heeded by yourself, but anyway: if your detail view is presented from the viewDidLoad(), try to move this to the viewDidAppear() method:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
// present/load detail view here instead
}
This might be too late an answer, but anyways, I solved this using perform segue instead of showDetailViewController
I have the following files:
Main.storyboard
This is the same as the default storyboard created when creating a new project with a few additions: There is a button in the view which is connected to the button outlet of the view controller (instance of ViewController) and the doTheThing: action on the view controller.
ViewController.m
#import "ViewController.h"
#interface ViewController ()
#property (weak) IBOutlet NSButton *button;
#property (weak) NSViewController *controller;
#end
#implementation ViewController
- (IBAction)doTheThing:(id)sender {
if (self.controller) {
NSLog(#"Removing %#", self.controller);
[self.controller.view removeFromSuperview];
[self.controller removeFromParentViewController];
} else {
self.controller = [[NSStoryboard storyboardWithName:#"Another" bundle:[NSBundle mainBundle]] instantiateInitialController];
[self addChildViewController:self.controller];
[self.view addSubview:self.controller.view];
NSLog(#"Adding %#", self.controller);
}
}
#end
Another.storyboard
A simple storyboard containing a single scene (view controller + view) that is set to the initial controller and is an instance of AnotherViewController. There is a label in the view that is not connected to any outlet.
AnotherViewController.m
#import "AnotherViewController.h"
#interface AnotherViewController ()
#property (weak) IBOutlet NSTextField *label;
#end
#implementation AnotherViewController
- (void)dealloc {
NSLog(#"Deallocing AnotherViewController %#", self);
}
#end
When I run the app and click the button, it adds the view controller and view from Another.storyboard, and when I click the button again they are removed and the instance of AnotherViewController is deallocated.
However, if I connect the label in Another.storyboard to the label outlet on the AnotherViewController, the deallocation never occurs. Why is this and what can I do to fix it?
Edit: I do have a few workarounds, but they aren't very desirable and I would prefer to understand why the recommended way (storyboards and outlets) isn't working properly.
Undesirable workaround 1: Do the same thing but load the view controller and view from a XIB file. This works as expected, but ideally I would be able to do the same thing using storyboards.
Undesirable workaround 2: Bind all my outlets manually in code in the view controller's viewDidLoad method. This is just tedious and ugly as it requires iterating through all the view's subviews and comparing identifiers.
Apple have confirmed (via bug report) that this is a known issue and will be fixed in OS X 10.10.3.
How do I pop up an UIAlertView when the back button of a UINavigationBar (controlled by a UINavigationController) was tapped? Under certain conditions, I want to ask the user an "Are you sure?" type of question so he could either abort the action and stay on the current view or pop the navigation stack and go to the parent view.
The most appealing approach I found was to override ShouldPopItem() on UINavigationBar's Delegate.
Now, there is a quite similar question here: iphone navigationController : wait for uialertview response before to quit the current view
There are also a few other questions of similar nature, for example here:
Checking if a UIViewController is about to get Popped from a navigation stack?
and How to tell when back button is pressed in a UINavigationControllerStack
All of these state "subclass UINavigationController" as possible answers.
Then there is this one that reads like subclassing UINavigationController is generally not a good idea:
Monotouch: UINavigationController, override initWithRootViewController
The apple docs also say that UINavigationController is not intended to be subclassed.
A few others state that overriding ShouldPopItem() is not even possible when using a UINavigationController as that does not allow to assign a custom/subclassed UINavigationBarDelegate to the UINavigationBar.
None of my attempts of subclassing worked, my custom Delegate was not accepted.
I also read somewhere that it might be possible to implement ShouldPopItem() within my custom UINavigationController since it assigns itself as Delegate of its UINavigationBar.
Not much of a surprise, this didn't work. How would a subclass of UINavigationController know of the Methods belonging to UINavigationBarDelegate. It was rejected: "no suitable method found to override". Removing the "override" keyword compiled, but the method is ignored completely (as expected). I think, with Obj-C one could implement several Protocols (similar to Interfaces in C# AFAIK) to achieve that. Unfortunately, UINavigationBarDelegate is not an Interface but a Class in MonoTouch, so that seems impossible.
I'm pretty much lost here. How to override ShouldPopItem() on UINavigationBar's Delegate when it is controlled by a UINavigationController? Or is there any other way to pop up an UIAlertView and wait for it's result before possibly popping the navigation stack?
This post is a bit old, but in case you're still interested in a solution (still involves subclassing though):
This implements a "Are you sure you want to Quit?" alert when the back button is pressed, modified from the code here: http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller/
Turns out if you implement the UINavigationBarDelegate in the CustomNavigationController, you can make use of the shouldPopItem method:
CustomNavigationController.h :
#import <Foundation/Foundation.h>
#interface CustomNavigationController : UINavigationController <UIAlertViewDelegate, UINavigationBarDelegate> {
BOOL alertViewClicked;
BOOL regularPop;
}
#end
CustomNavigationController.m :
#import "CustomNavigationController.h"
#import "SettingsTableController.h"
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if (regularPop) {
regularPop = FALSE;
return YES;
}
if (alertViewClicked) {
alertViewClicked = FALSE;
return YES;
}
if ([self.topViewController isMemberOfClass:[SettingsTableViewController class]]) {
UIAlertView * exitAlert = [[[UIAlertView alloc] initWithTitle:#"Are you sure you want to quit?" message:nil delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Yes", nil] autorelease];
[exitAlert show];
return NO;
}
else {
regularPop = TRUE;
[self popViewControllerAnimated:YES];
return NO;
}
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
//Cancel button
}
else if (buttonIndex == 1) {
//Yes button
alertViewClicked = TRUE;
[self popViewControllerAnimated:YES];
}
}
#end
The weird logic with the "regularPop" bool is because for some reason just returning "YES" on shouldPopItem only pops the navbar, not the view associated with the navBar - for that to happen you have to directly call popViewControllerAnimated (which then calls shouldPopItem as part of its logic.)
For reference, the route I took after giving up on ShouldPopItem() is to replace the back button with a UIBarButtonItem that has a custom UIButton assigned as it's CustomView. The UIButton is crafted to look like the original back button using two images for the normal and the pressed state. Finally, hiding the original back button is required.
Way too much code for what it's supposed to do. So yeah, thanks Apple.
BTW: Another possibility is creating a UIButton with the secret UIButtonType 101 (which is actually the back button) but I avoided this as it may break at any later iOS version.
Override only UINavigationBarDelegate methods in a UINavigationController subclass and it should simply work. Be cautious that the protocol methods are also called when you push or pop a view controller from inside your code and not only when the back button is pressed. This is because them are push/pop notifications not button pressed actions.
Xamarin does provide the IUINavigationBarDelegate interface to allow you to implement the UINavigationBarDelegate as part of your custom UINavigationController class.
The interface however does not require that the ShouldPopItem method be implemented. All the interface does is add the appropriate Protocol attribute to the class so it can be used as a UINavigationBarDelegate.
So in addition you need to add the ShouldPopItem declaration to the class as follows:
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
I've merged this solution with a native Obj-C solution. This is the way I'm currently handling the cancellation of the BACK button in iOS
It seems that it is possible to handle the shouldPopItem method of the NavigationBar in this way:
Subclass a UINavigationController
Mark your custom UINavigationController with the IUINavigationBarDelegate
Add this method with the Export attribute
[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
Now you can handle popping in the ShoulPopItem method. An example to this is to create an interface like this
public interface INavigationBackButton
{
// This method should return TRUE to cancel the "back operation" or "FALSE" to allow normal back
bool BackButtonPressed();
}
Then mark your UIViewController which needs to handle the back button with this interface. Implement something like this
public bool BackButtonPressed()
{
bool needToCancel = // Put your logic here. Remember to return true to CANCEL the back operation (like in Android)
return needToCancel;
}
Then in your ShouldPopItem Implementation have something like this
tanks to: https://github.com/onegray/UIViewController-BackButtonHandler/blob/master/UIViewController%2BBackButtonHandler.m
[Export("navigationBar:shouldPopItem:")]
public bool ShouldPopItem(UINavigationBar navigationBar, UINavigationItem item)
{
if (this.ViewControllers.Length < this.NavigationBar.Items.Length)
return true;
bool shouldPop = true;
UIViewController controller = this.TopViewController;
if (controller is INavigationBackButton)
shouldPop = !((INavigationBackButton)controller).BackButtonPressed();
if (shouldPop)
{
//MonoTouch.CoreFoundation.DispatchQueue.DispatchAsync
CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(
() =>
{
PopViewController(true);
});
}
else
{
// Workaround for iOS7.1. Thanks to #boliva - http://stackoverflow.com/posts/comments/34452906
foreach (UIView subview in this.NavigationBar.Subviews)
{
if(subview.Alpha < 1f)
UIView.Animate(.25f, () => subview.Alpha = 1);
}
}
return false;
}
Having tried a number of different solutions I keep coming back to this. I need a Window.ShowDialog, using the ViewModelLocator class as a factory via a UnityContainer.
Basically I have a View(and ViewModel) which on a button press on the the view needs to create a dialog (taking a couple of parameters in its constructor) that will process some logic and eventally return a result to the caller (along with the results of all the logic it computed).
Maybe I'm wrong for stilll looking at this from a Windows Forms perspective, but I know exactly what I want to do and I want to ideally do it using WPF and MVVM. I'm trying to make this work for a project, and ultimately don't want to have to go back to vanilla WPF in order to make it work.
I break the rules to implement a dialogwindow but tried to reduce it to a minimum. I have a method OpenDialog in my BaseViewModel:
public void OpenDialog(DialogViewModel model)
{
this.MessengerInstance.Send<DialogViewModel, MainWindow>(model);
}
And in my MainWindow:
Messenger.Default.Register<DialogViewModel>(this, model =>
{
// Instantiate the dialog box
var dlg = new DialogWindow();
// Configure the dialog box
dlg.Owner = this;
dlg.Content = model;
// Open the dialog box modally
dlg.ShowDialog();
});
That way i only have a loose coupling between my viewmodel and my MainView.
You can do the same for closing, my BaseDialogViewModel has a method:
public void CloseDialog()
{
this.MessengerInstance.Send<PopUpAction, DialogWindow>(PopUpAction.Close);
}
(PopupAction is just an enum) and my DialogWindow registers for that:
Messenger.Default.Register<PopUpAction>(this, action =>
{
switch (action)
{
case PopUpAction.Close:
this.Close();
break;
}
});
You could also leave the receiver away when sending, to keep the view class out of the viewmodel but either way i think it's a acceptable solution :)
You can do that. Just create an instance of a page/usercontrol/window and call instance.ShowDialog().
Here's my T4 templates to generate a view/viewmodel with the messaging for closing a window and other tricks.