I have multiple view controllers. Something like this:
ViewControllerA
ViewControllerB
ViewControllerC
I use these as my UIPageViewController's view controllers, (as pages).
Each one of them has scrollable UITextViews inside. I cannot disable scrolling because text length should be flexible.
When I scroll the textview, UIPageViewControllerDataSource's viewControllerBefore or viewControllerAfter also is being triggered. How can I prevent this?
Can I disable vertical gestures for UIPageViewController and prevent clashes? Or is there some other way to stop them working at the same time? I want to change page only on horizontal gesture, and scroll the text view only on vertical gesture. But I don't know how to do it. What should I do?
Edit: Thank you #DonMag and #BulatYakupov for trying to help me and warning me about the given information is not enough. I had some restriction on code sharing. Today I found another way. But first here how my screen looks :
I was swiping to turn the page with page curl.
When I scroll the text, it was also triggering viewControllerBefore or viewControllerAfter. It wasn't changing the page, but It was triggering my view controller creating function. This function keep track of current index on an array to supply data to vc creation.
It is something like this:
class myCustomVC: UIViewController {
let textView = UITextView()
}
class PageVCDataService {
var array = ["John","Josh","Joe","Harvy"]
//keeping track of the current index for suppling correct text from array for every page
var currentIndex = 0
func createAfterPage() -> UIViewController {
currentIndex += 1
return createVC(page :array[currentIndex])
}
func createBeforePage() -> UIViewController {
currentIndex -= 1
return createVC(page :array[currentIndex])
}
func createVC(page : String) ->UIViewController {
let vc = myCustomVC()
vc.textView.text = page
return vc
}
}
class PageVC : UIPageViewController {
let dataService = PageVCDataService()
// ...some code
}
extension PageVC: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return dataService.createBeforePage()
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return dataService.createAfterPage()
}
}
It wasn't changing the page but it was changing the index. For example if I scroll the text and if my current index at 10 (page 11), my current index was changing to 9. When I swipe for next page, next page was the same page because index was going 9 to 10, which is the same index at the beginning.
So i implemented UITextViewDelegate and used scrollViewDidScroll method. Every time text view scrolls, it send a notification to my data service class, tell it to check if the text on the screen and the text at the current index is same. If it's not, my function find the index for the text on the screen and corrects current index.
It is an ugly solution but maybe it can help someone else.
In iOS 13 modal presentations using the form and page sheet style can be dismissed with a pan down gesture. This is problematic in one of my form sheets because the user draws into this box which interferes with the gesture. It pulls the screen down instead of drawing a vertical line.
How can you disable the vertical swipe to dismiss gesture in a modal view controller presented as a sheet?
Setting isModalInPresentation = true still allows the sheet to be pulled down, it just won't dismiss.
In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.
If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.
If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:
If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.
If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.
With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.
I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.
This gesture can be found in the modal view controller's presentedView property. As I debugged, the gestureRecognizers array of this property has only one item and printing it resulted in something like this:
UIPanGestureRecognizer: 0x7fd3b8401aa0
(_UISheetInteractionBackgroundDismissRecognizer);
So to disable this gesture you can do like below:
let vc = UIViewController()
self.present(vc, animated: true, completion: {
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
})
To re-enable it simply set isEnabled back to true:
vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true
Note that iOS 13 is still in beta so a simpler approach might be added in an upcoming release.
Although this solution seems to work at the moment, I would not recommend it as it might not work in some situations or might be changed in future iOS releases and possibly affect your app.
Use this in the presented ViewController viewDidLoad:
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
In my case, I have a modal screen with a view that receives touches to capture customer signatures.
Disabling the gesture recognizer in the navigation controller solved the problem, preventing the modal interactive dismissal from being triggered at all.
The following methods are implemented in our modal view controller, and are called via delegate from our custom signature view.
Called from touchesBegan:
private func disableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = false
}
}
Called from touchesEnded:
private func enableDismissalRecognizers() {
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.isEnabled = true
}
}
Here is a GIF showing the behavior:
This question, flagged as duplicate, describes better the issue I had: Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view
you can change the presentation style, if its in full screen the pull down to dismiss would be disabled
navigationCont.modalPresentationStyle = .fullScreen
No need to reinvent the wheel. It is as simple as adopting the UIAdaptivePresentationControllerDelegate protocol on your destinationViewController and then implement the relevant method:
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
For example, let's suppose that your destinationViewController is prepared for segue like below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourIdentifier",
let destinationVC = segue.destination as? DetailViewController
{
//do other stuff
destinationVC.presentationController?.delegate = destinationVC
}
}
Then on the destinationVC (that should adopt the protocol described above), you can implement the described method func presentationControllerShouldDismiss(_ presentationController:) -> Bool or any of the other ones, in order to handle correctly your custom behaviour.
You can use the UIAdaptivePresentationControllerDelegate method presentationControllerDidAttemptToDismiss and disable the gestureRecognizer on the presentedView.
Something like this:
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}
For every body having problems with Jordans solution #3 running.
You have to look for the ROOT viewcontroller which is beeing presented, depending on your viewstack, this is maybe not you current view.
I had to look for my navigation controllers PresentationViewController.
BTW #Jordam: Thanks!
UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
pan.delegate = self;
}
You may first get a reference to the UIPanGestureRecognizer handling the page sheet dismissal in viewDidAppear() method. Notice that this reference is nil in viewWillAppear() or viewDidLoad(). Then you simply disable it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presentationController?.presentedView?.gestureRecognizers?.first.isEnabled = false
}
If you want more customization rather than disabling it completely, for example, when using a navBar within the page sheet, set the delegate of that UIPanGestureRecognizer to your own view controller. That way, you can disable the gesture recognizer exclusively in your contentView while keeping it active in your navBar region by implementing
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
in IOS 13
if #available(iOS 13.0, *) {
obj.isModalInPresentation = true
} else {
// Fallback on earlier versions
}
Me, I use this :
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
for(UIGestureRecognizer *gr in self.presentationController.presentedView.gestureRecognizers) {
if (#available(iOS 11.0, *)) {
if([gr.name isEqualToString:#"_UISheetInteractionBackgroundDismissRecognizer"]) {
gr.enabled = false;
}
}
}
Will try to describe method 2 already suggested by #Jordan H in more details:
1) To be able to catch and make decisions about the modal sheet's pan gesture add this into view controller's viewDidLoad:
navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
$0.delegate = self
}
2) Enable the ability to catch the pan gesture together with your own gestures using gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
3) The actual decision can go in gestureRecognizer(_:shouldBeRequiredToFailBy:)
Example code, which makes the swipe gesture to be preferred over sheet's pan gesture, if both present. It doesn't affect original pan gesture in areas where there is no swipe gesture recognizer and therefore the original "swipe to dismiss" can still work as designed.
extension PeopleViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer === UIPanGestureRecognizer.self && otherGestureRecognizer === UISwipeGestureRecognizer.self {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
In my case I have only a few swipe gesture recognizers, so comparing types is enough for me, but if there more of them it might make sense to compare the gestureRecognizers themselves (either programmatically added ones or as outlets from interface builder) as described in this doc: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another
Here's how the code works in my case. Without it the swipe gesture was mostly ignored and worked only occasionally.
In the case when a UITableView or UICollectionView initiates the page sheet dismiss gesture when the user attempts to scroll past the top end of the scrolling view, this gesture can be disabled by adding an invisible UIRefreshControl that calls endRefreshing immediately.
See also https://stackoverflow.com/a/58676756/2419404
SwiftUI since iOS 15
.interactiveDismissDisabled()
For Example:
.sheet(isPresented: $add) {
AddView()
.interactiveDismissDisabled()
}
For navigation Controller, to avoid swipe interaction for presented view we can use:
if #available(iOS 13.0, *) {navController.isModalInPresentation = true}
In prepare(for:sender:) :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == viewControllerSegueID {
let controller = segue.destination as! YourViewController
controller.modalPresentationStyle = .fullScreen
}
}
or, after you initialize your controller:
let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen
I'd like to use the NSTabViewController for switching through 6 different Tabs with the toolbar style.
All tabs have in common that they show different aspects of a Customer entity.
Now I want to add aditional NSToolbarItems to the toolbar of the NSTabViewController? But I haven't found a way to access the toolbar.
I also would like to add Space between the ToolbarItems.
Is there a way to do so?
Or how can I add my ViewController from the Storyboard to a NSTabView without using NSTabViewController?
Regards
Oliver
In the meantime I've tried another approach that I thought was more promising but lead to another strange behaviour:
I've created a new NSViewController and put a NSTabView inside. In order to load my already existing ViewControllers I used this
override func viewDidLoad() {
super.viewDidLoad()
let customerController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("CustomerVCID")) as! CustomerViewController
let servicesController = self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("ServicesVCID")) as! ServicesController
customerController.customer = self.customer
servicesController.customer = self.customer
self.tabView.tabViewItems[0].view = customerController.view
self.tabView.tabViewItems[1].view = servicesController.view
}
That indeed worked, but now all my NSButtons that have actions will cause my application to crash.
There is only one toolbar per window. So your NSTabViewController shares it.
Select toolbar mode of NSTabViewController
Override NSWindowController and add your items
Example:
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.insertItem(withItemIdentifier: .print, at: 0)
}
You can always access your toolbar via following path view->window->toolbar
Your only issue is that there is one delegate per NSToolbar. Which means you have to create your custom NSToolbarItem inside NSTabViewController delegate.
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == .export {
return ExportToolbarItem.new()
} else {
return super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
}
}
Remember your are required to call super. This is because underlying method wants to create bindings to view controller.
In case you need actionable buttons in toolbar just add them without calling super.
I have a UIViewController with a UILabel that needs to display either "lbs" or "kg". My app has a settings screen (another UIViewController) that is presented modally over the first view controller and the user can select either of the two units and save their preference. If the units are changed and the modal settings screen is dismissed, I of course want the label on the first view controller to be updated with the new units value (but without refreshing the whole view). I thought I knew how to make it work, but evidently I don't.
On my modal settings screen, I have a UISegmentedControl to allow the user to select units. Anytime it's changed, this function updates userDefaults:
func saveUnitsSelection() {
if unitsControl.selectedSegmentIndex == 0 {
UserDefaultsManager.sharedInstance.preferredUnits = Units.pounds.rawValue
} else {
UserDefaultsManager.sharedInstance.preferredUnits = Units.kilograms.rawValue
}
}
Then they would likely dismiss the settings screen. So, I added this to viewDidLoad in my first view controller:
override func viewDidLoad() {
super.viewDidLoad()
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
units.text = preferredUnits
}
That didn't work, so I moved it to viewWillAppear() and that didn't work either. I did some research and some caveman debugging and found out that neither of those functions is called after the view has been loaded/presented the first time. It seems that viewWillAppear will be called a second time if I'm working within a hierarchy of UITableViewControllers managed by a UINavigationController, but isn't called when I dismiss my modal UIViewController to reveal the UIViewController underneath it.
Edit 1:
Here's the view hierarchy I'm working with:
I'm kinda stuck at this point and not sure what to try next.
Edit 2:
The user can tap a 'Done' button in the navigation bar and when they do, the dismissSettings() function dismisses the Settings view:
class SettingsViewController: UITableViewController {
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
// some other variables set here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Plain, target: self, action: #selector(self.dismissSettings(_:)))
if preferredUnits == Units.pounds.rawValue {
unitsControl.selectedSegmentIndex = 0
} else {
unitsControl.selectedSegmentIndex = 1
}
}
func dismissSettings(sender: AnyObject?) {
navigationController?.dismissViewControllerAnimated(true, completion: nil)
}
}
THE REAL PROBLEM
You misspelled viewWillAppear. You called it:
func viewWillAppear()
As far as Cocoa Touch is concerned, this is a random irrelevant function that hooks into nothing. You meant:
override func viewWillAppear(animated: Bool)
The full name of the first function is: "viewWillAppear"
The full name of the second function is: "viewWillAppear:animated"
Once you get used to this, the extreme method "overloading" that Cocoa Touch uses gets easier.
This is very different in other languages where you might at least get a warning.
The other lesson that everyone needs to learn when posting a question is: Include All Related Code!
Useful logging function I use instead of print or NSLog, to help find these things:
class Util {
static func log(message: String, sourceAbsolutePath: String = #file, line: Int = #line, function: String = #function, category: String = "General") {
let threadType = NSThread.currentThread().isMainThread ? "main" : "other"
let baseName = (NSURL(fileURLWithPath: sourceAbsolutePath).lastPathComponent! as NSString).stringByDeletingPathExtension ?? "UNKNOWN_FILE"
print("\(NSDate()) \(threadType) \(baseName) \(function)[\(line)]: \(message)")
}
}
[Remaining previous discussion removed as it was incorrect guesses]
The main ViewController is embedded in a UINavigationController subclass, and the VC has a subview that is loaded from a nib. The subview is called MenuView, and contains UIButtons that will link to other VCs.
To keep my main ViewController less unruly, I have put all these buttons into a subview that loads from a nib that animates the menu opening and closing.
However, I would like to present other view controllers from these, sometimes "Modally", sometimes "Show". What I have done seems to work, but I just want to know if this is alright, or if I have caused some unwanted effects that I'm unaware of (like a strong reference cycle that would cause a memory leak, or something). Or is there a better way to do this?
Some code:
In MenuView.swift
class MenuView: UIView {
var navigationController = CustomNavigationController()
func combinedInit(){
NSBundle.mainBundle().loadNibNamed("MenuViewXib", owner: self, options: nil)
addSubview(mainView)
mainView.frame = self.bounds
}
#IBAction func optionsAction(sender: AnyObject) {
self.navigationController.performSegueWithIdentifier("presentOptions", sender: self)
}
In ViewController.swift
menuView.navigationController = self.navigationController as! CustomNavigationController
Short answer: No, it is not alright to access a view controller from within some view in the hierarchy, because that would break all the MVC rules written.
UIView objects are meant to display UI components in the screen and are responsible for drawing and laying out their child views correctly. That's all there is. Nothing more, nothing less.
You should handle those kind of interactions between views and controllers always in the controller in which the view in question actually belong. If you need to send messages from a view to its view controller, you can make use of either the delegate approach or NSNotificationCenter class.
If I were in your shoes, I would use a delegate when view needs some information from its view controller. It is more understandable than using notification center as it makes it much easier to keep track of what's going on between. If the view controller needs some information from a view (in other words, the other way around), I'd go with the notification center.
protocol MenuViewDelegate: class {
func menuViewDidClick(menuView: MenuView)
}
class MenuView: UIView {
var weak delegate: MenuViewDelegate?
#IBAction func optionsAction(sender: AnyObject) {
delegate?.menuViewDidClick(self)
}
}
Let's look at what's going on at the view controller side:
class MenuViewController: UIViewController, MenuViewDelegate {
override func viewDidLoad() {
...
self.menuView.delegate = self
}
func menuViewDidClick(menuView: MenuView) {
navigationController?.performSegueWithIdentifier("presentOptions", sender: self)
}
}
For more information about communication patterns in iOS, you might want to take a look at this great article in order to comprehend how they work.