Hiding the master view controller with UISplitViewController in iOS8 - objective-c

I have an iOS7 application, which was based on the Xcode master-detail template, that I am porting to iOS8. One area that has changed a lot is the UISplitViewController.
When in portrait mode, if the user taps on the detail view controller, the master view controller is dismissed:
I would also like to be able to programmatically hide the master view controller if the user taps on a row.
In iOS 7, the master view controller was displayed as a pop-over, and could be hidden as follows:
[self.masterPopoverController dismissPopoverAnimated:YES];
With iOS 8, the master is no longer a popover, so the above technique will not work.
I've tried to dismiss the master view controller:
self.dismissViewControllerAnimated(true, completion: nil)
Or tell the split view controller to display the details view controller:
self.splitViewController?.showDetailViewController(bookViewController!, sender: self)
But nothing has worked so far. Any ideas?

Extend the UISplitViewController as follows:
extension UISplitViewController {
func toggleMasterView() {
let barButtonItem = self.displayModeButtonItem()
UIApplication.sharedApplication().sendAction(barButtonItem.action, to: barButtonItem.target, from: nil, forEvent: nil)
}
}
In didSelectRowAtIndexPath or prepareForSegue, do the following:
self.splitViewController?.toggleMasterView()
This will smoothly slide the master view out of the way.
I got the idea of using the displayModeButtonItem() from this post and I am simulating a tap on it per this post.
I am not really happy with this solution, since it seems like a hack. But it works well and there seems to be no alternative yet.

Use preferredDisplayMode. In didSelectRowAtIndexPath or prepareForSegue:
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
self.splitViewController?.preferredDisplayMode = .Automatic
Unfortunately the master view abruptly disappears instead of sliding away, despite the documentation stating:
If changing the value of this property leads to an actual change in
the current display mode, the split view controller animates the
resulting change.
Hopefully there is a better way to do this that actually animates the change.

The code below hides the master view with animation
UIView.animateWithDuration(0.5) { () -> Void in
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}

I was able to have the desired behavior in a Xcode 6.3 Master-Detail Application (universal) project by adding the following code in the MasterViewController's - prepareForSegue:sender: method:
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
The complete - prepareForSegue:sender: implementation should look like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
}
}
}
Using traitCollection may also be an alternative/supplement to displayMode in some projects. For example, the following code also works for a Xcode 6.3 Master-Detail Application (universal) project:
let traits = view.traitCollection
if traits.userInterfaceIdiom == .Pad && traits.horizontalSizeClass == .Regular {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}

Swift 4 update:
Insert it into prepare(for segue: ...
if splitViewController?.displayMode == .primaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .primaryHidden
}
let completion: (Bool) -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .automatic
}
UIView.animate(withDuration: 0.3, animations: animations, completion: completion)
}

Modifying the answers above this is all I needed in a method of my detail view controller that configured the view:
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
Of course it lacks the grace of animation.

try
let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden

My solution in the Swift 1.2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
var screen = UIScreen.mainScreen().currentMode?.size.height
if (UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad) || screen >= 2000 && UIDevice.currentDevice().orientation.isLandscape == true && (UIDevice.currentDevice().userInterfaceIdiom == .Phone){
performSegueWithIdentifier("showDetailParse", sender: nil)
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else if (UIDevice.currentDevice().userInterfaceIdiom == .Phone) {
performSegueWithIdentifier("showParse", sender: nil)
}
}

for iPad add Menu button like this
UIBarButtonItem *menuButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:#"burger_menu"]
style:UIBarButtonItemStylePlain
target:self.splitViewController.displayModeButtonItem.target
action:self.splitViewController.displayModeButtonItem.action];
[self.navigationItem setLeftBarButtonItem:menuButtonItem];
This work great with both landscape and portrait mode.
To programmatically close the popover vc you just need to force the button action like this
[self.splitViewController.displayModeButtonItem.target performSelector:appDelegate.splitViewController.displayModeButtonItem.action];

Very similar to the method by phatmann, but a bit simpler in Swift 5. And it's not technically a 'hack', as it is what the iOS doc suggested.
In your prepareForSegue or other methods that handle touches, in
let barButton = self.splitViewController?.displayModeButtonItem
_ = barButton?.target?.perform(barButton?.action)
According to Apple, the splitViewController's displayModeButtonItem is set up for you to display the master view controller in a way that suits your device orientation. That is, .preferHidden in portrait mode.
All there's to do is to press the button, programatically. Or you can put it in an extension to UISplitViewController, like phatmann did.

Related

Dismissing all presented ViewControlelrs in a UINavigationController hierarchically

I was looking for a way to dismiss all the modally presented viewControllers in a UINavigationController hierarchically without knowing the name of them. so I ended up to the while loop as follow:
Swift
while(navigationController.topViewController != navigationController.presentedViewController) {
navigationController.presentedViewController?.dismiss(animated: true, completion: nil)
}
Objective-c
while(![self.navigationController.topViewController isEqual:self.navigationController.presentedViewController]) {
[self.navigationController.presentedViewController dismissViewControllerAnimated:YES completion:nil];
}
I want to dismiss all the presentedControllers one by one till the presentedViewController and topViewcontroller become equal.
the problem is that the navVC.presentedViewController doesn't changed even after dismissing.
It remains still the same even after dismissing and I end up to an infiniteLoop.
Does anyone knows where is the problem?
In my case nothing works but:
func dismissToSelf(completion: (() -> Void)?) {
// Collecting presented
var presentedVCs: [UIViewController] = []
var vc: UIViewController? = presentedViewController
while vc != nil {
presentedVCs.append(vc!)
vc = vc?.presentedViewController
}
// Dismissing all but first
while presentedVCs.count > 1 {
presentedVCs.last?.dismiss(animated: false, completion: nil)
presentedVCs.removeLast()
}
// Dismissing first with animation and completion
presentedVCs.first?.dismiss(animated: true, completion: completion)
}
I've found the answer. I can dismiss all presentedViewControllers on a navigationController by:
navigationController.dismiss(animated: true, completion: nil)
It keeps the topViewController and dismiss all other modals.
Form your question I understood that you want to dismiss all view controllers above the root view controller. For that you can do it like this:
self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)
Not need to used self.navigationController.presentedViewController.
Might be help! my code is as follows:
Objective-c
[self dismissViewControllerAnimated:YES completion:^{
}];
// Or using this
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
Please check this code
-(void)dismissModalStack {
UIViewController *vc = self.window.rootViewController;
while (vc.presentedViewController) {
vc = vc.presentedViewController;
[vc dismissViewControllerAnimated:false completion:nil];
}
}
Glad to see you have found the answer, and I've done this by another way.
You can create a BaseViewController(actually lots of app do that), and defined a property like 'presentingController' in appdelegate that indicate the presenting ViewController, then in the viewWillAppear method, set the property so that it always indicate the top view controller.
-(void)viewWillAppear:(BOOL)animated{
AppDelegate *delegate=(AppDelegate *)[[UIApplication sharedApplication]delegate];
delegate.presentingController = self;
}
All the class inherited from BaseViewController will call it. When you want to dismiss all the controller, just loop as follow:
- (void)clickButton:(id)sender {
AppDelegate *delegate=(AppDelegate *)[[UIApplicationsharedApplication]delegate];
if (delegate.presentingController)
{
UIViewController *vc =self.presentingViewController;
if ( !vc.presentingViewController ) return;
while (vc.presentingViewController)
{
vc = vc.presentingViewController;
}
[vc dismissViewControllerAnimated:YEScompletion:^{
}];
}
}
Hope this will help you :)
I had a similar issue of deleting/dismissing existing/previous push notification when a new push notification arrives where different pictures are sent as a push notification.
In my situation, using Swift 5, I wanted to delete/dismiss previous push notification and display a new push notification all by itself regardless whether the user acknowledged the previous notification or not (i.e. without user's acknowledgement).
I tried Kadian's recommendation with a minor change and it worked flawlessly.
Here is my NotificationDelegate.swift
import UIKit
import UserNotifications
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound, .badge])
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
defer { completionHandler() }
guard response.actionIdentifier == UNNotificationDefaultActionIdentifier else {return}
let payload = response.notification.request.content
let pn = payload.body
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: pn)
//***Below cmd will erase previous push alert***
self.window!.rootViewController?.dismiss(animated: false, completion: nil)
//Below cmd will display a newly received push notification
self.window!.rootViewController!.present(vc, animated: false)
}
}

Search bar overlaps with status bar on iOS 11

I am using a UISearchController and a UISearchResultsController to implement search functionality.
MySearchResultsController implements UISearchResultsUpdating and UISearchBarDelegate:
override open func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = [];
self.automaticallyAdjustsScrollViewInsets = false;
}
I display the searchbar in the tableHeader like this in MyTableViewController:
- (void)viewDidLoad {
[super viewDidLoad];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.searchResultsController];
self.searchController.searchResultsUpdater = self.searchResultsController;
self.searchController.searchBar.delegate = self.searchResultsController;
self.searchController.searchBar.scopeButtonTitles = #[NSLocalizedString(#"SEARCH_SCOPE_TEMPERATURES", nil), NSLocalizedString(#"SEARCH_SCOPE_KNOWHOW", nil)];
self.tableView.tableHeaderView = self.searchController.searchBar;
self.definesPresentationContext = YES;
}
This worked perfectly before, but under iOS 11 the search bar overlaps with the status bar as soon as I tap into it (see screenshots). I tried lots of different things to get it to display correctly but haven't found a solution yet.
I found that the problem was that the presenting view Controller also sets
override open func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = [];
self.automaticallyAdjustsScrollViewInsets = false;
}
I have to do this because the table view does not actually extend all the way to the top.
I solved this like that in my presenting view Controller:
override open func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false;
if (#available(iOS 11.0, *)) {
//NSLog(#"iOS 11.0");
} else {
self.edgesForExtendedLayout = UIRectEdgeNone;
//NSLog(#"iOS < 11.0");
}
}
Seems to be an iOS 11 bug, or at least an odd behavior…
This is what Worked for me:
override func viewDidLoad() {
// to fix the Status Bar Issue:
if #available(iOS 11.0, *) {
definesPresentationContext = true
}
// You'll also need this properties on your Search Bar:
searchController = UISearchController.init(searchResultsController: nil)
searchController?.searchResultsUpdater = self
searchController?.hidesNavigationBarDuringPresentation = false
}
I managed to solve this by subclassing UISearchController. My answer is in Swift but maybe the principles works with ojective-c as well. Please see my answer here: https://stackoverflow.com/a/46339336/8639272

SWRevealViewController closes when opened beyond 'snappoint'

I'm trying to make the SWRevealViewController work in my application.
What I did is I didn't want to be able to open the menu from everywhere on the screen so I changed all the UIPanGestureRecognizer with UIScreenEdgePanGestureRecognizer so it would only be triggered from the side of the screen.
to achieve this I also altered thepanGestureRecognizer method. This looks as follows right now
- (UIScreenEdgePanGestureRecognizer*)panGestureRecognizer
{
if ( _panGestureRecognizer == nil )
{
_panGestureRecognizer = [[SWRevealViewControllerPanGestureRecognizer alloc] initWithTarget:self action:#selector(_handleRevealGesture:)];
_panGestureRecognizer.delegate = self;
_panGestureRecognizer.edges = UIRectEdgeLeft;
[_contentView.frontView addGestureRecognizer:_panGestureRecognizer];
}
return _panGestureRecognizer;
}
However (not sure if this change is causing my problem) when I start to open the menu from the left side and expand it over the point where it will normally snap to when opened up it will collapse back in.
so let's say it opens to 500px of the screen. When I drag it beyond those 500px it will automatically close the menu.
Also it's currently not possible to close the menu again by swiping it. What I did for now is add a gesturerecognizer which will trigger the revealViewController.revealToggle(animated: true) method.
Does anyone happen to know how to fix this?
import #import "SWRevealViewController.h"
These two gestures helps you to open and close sw menu . Drag and click for open and close SW
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
[self.view addGestureRecognizer:self.revealViewController.tapGestureRecognizer];
Open sw menu normally
[self.revealViewController revealToggleAnimated:YES]
I ended up fixing it by using the delegate methods that come with SWRevealViewController my entire code looks like this
import UIKit
import SWRevealViewController
import UIKit.UIGestureRecognizerSubclass
class ViewController: UIViewController, SWRevealViewControllerDelegate {
#IBOutlet var openMenu: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
openMenu.target = self.revealViewController()
openMenu.action = #selector(SWRevealViewController.revealToggle(_:))
revealViewController().delegate = self
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.hideMenuWhenTappedAround()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func revealControllerPanGestureShouldBegin(_ revealController: SWRevealViewController!) -> Bool {
let point = revealController.panGestureRecognizer().location(in: self.view)
if revealController.frontViewPosition == FrontViewPosition.left && point.x < 50.0 {
return true
}
else if revealController.frontViewPosition == FrontViewPosition.right {
return true
}
return false
}
func revealController(_ revealController: SWRevealViewController!, panGestureMovedToLocation location: CGFloat, progress: CGFloat) {
if location >= revealController.rearViewRevealWidth {
revealController.panGestureRecognizer().state = UIGestureRecognizerState.ended
}
}
}
The self.hideMenuWhentappedAround() makes sure that the menu will close when you tap anywhere on the uiview. This method is created in an extension of the UIViewController and it looks as follows.
import UIKit
import SWRevealViewController
extension UIViewController {
func hideMenuWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(SWRevealViewController.dismissMenu))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
func dismissMenu() {
self.revealViewController().revealToggle(animated: true)
}
}

How to refer UICollectionView from UICollectionViewCell?

Can I refer UICollectionView from UICollectionViewCell?
I finally want to refer ViewController from UICollectionViewCell.
Following codes are in my CustomCollectionViewCell Class.
I want to archive tweet support in this class.
#IBAction func tweetBtnTapped(_ sender: Any) {
let cvc = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
if let c = cvc {
c.setInitialText("test tweet from iOS App")
### How can I refer ViewController??
viewController = ???????
if let vc = viewController {
vc.present(c, animated: true, completion: nil)
}
}
}
Please provide the code sample or what you are trying to achieve. What I understand is that you want some action back in your view controller when you perform some action on your cell. Am I correct?
So As per the comments, Custom collection view cell :
class CustomeCell: UICollectionViewCell {
#IBAction func tweetButtonPressed() {
let cvc = SLComposeViewController(forServiceType:SLServiceTypeTwitter)
if let c = cvc {
c.setInitialText("test tweet from iOS App")
self.parentViewController?.presentViewController(c, animated: true, completion: nil)
}
}
}
Create a Extension.Swift file :
import UIKit
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.nextResponder()
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
This is working for me. All the best!

Transitioning between view controller, OS X

I am trying to write a single window timer application, where when the user presses the start button I want it to show another view controller with countdown etc. I'm also using story board in Xcode, where I have got a segue which connects the start button and the second view controller. However, there are only three different styles i.e. modal, sheet, and pop-over. I want to replace the first view controller the second one in the window. I cannot find a way to do that. I tried using a custom style for the segue, and in that use presentViewController: animator: method but I cannot figure out what to send as the argument for the animator:.
What is the simplest/proper way to transition from one view controller to the other in one window and vice versa?
Also in the storyboard when I select a view controller it shows an attribute called "Presentation" which can be multiple and single, what do those represent?
I think the simplest way is that swapping contentViewController of NSWindow.
// in NSViewController's subclass
#IBAction func someAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
if let window = view.window where window.styleMask & NSFullScreenWindowMask > 0 {
// adjust view size to current window
nextViewController.view.frame = CGRectMake(0, 0, window.frame.width, window.frame.height)
}
view.window?.contentViewController = nextViewController
}
This is option #1.
If you want to use segue, create custom one and set it to segue class with identifier in IB.
class ReplaceSegue: NSStoryboardSegue {
override func perform() {
if let fromViewController = sourceController as? NSViewController {
if let toViewController = destinationController as? NSViewController {
// no animation.
fromViewController.view.window?.contentViewController = toViewController
}
}
}
}
This is option #2.
Last option is using presentViewController:animator: of NSViewController. The code below is custom NSViewControllerPresentationAnimator for dissolve animation.
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentationOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissalOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Then present VC like this.
#IBAction func replaceAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
presentViewController(nextViewController, animator: ReplacePresentationAnimator())
}
For dismissal, call presentingViewController's dismissViewController: in the presented VC.
#IBAction func dismissAction(sender: AnyObject) {
presentingViewController?.dismissViewController(self)
}
Swift4 Version
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Hope this help.
If you have one parent view controller, you can assign child view controllers to it, and use the transition method. Example code, to be placed in viewDidLoad of the parent view controller:
if let firstController = self.storyboard?.instantiateController(withIdentifier: "firstController") as? NSViewController {
firstController.view.autoresizingMask = [.width, .height]
firstController.view.frame = self.view.bounds
self.addChild(firstController)
self.view.addSubview(firstController.view)
}
if let secondController = self.storyboard?.instantiateController(withIdentifier: "secondController") as? NSViewController {
self.addChild(secondController)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let firstController = self.children.first, let secondController = self.children.last {
self.transition(from: firstController, to: secondController, options: .crossfade, completionHandler: nil)
}
}
It's essential that the view of the first child controller is being added as sub view to the view of the parent controller, otherwise the transition method doesn't work.
In the example above, a storyboard is used with one main view controller (= self), one child view controller with storyboard ID "firstController', and another child view controller with storyboard ID "secondController'