#IBSegueAction with condition - swift

In my app I want to perform a segue based on a UITapGestureRecognizer. In case the tap is in the top area of the screen, a segue to the SettingsView should be performed.
In UIKit this was quite simple. I've triggered the performSegue in the UITapGestureRecognizer by wrapping it in an if-statement.
Now I would like to write the SettingsView() in SwiftUI. The SettingsView() will be embedded in a UIHostingController.
My question is:
How can I perform the segue to this UIHostingController (while telling the UIHostingController which View to display)?
I tried to use the new #IBSegueAction. The reason to use this #IBSegueAction is that I can use it to tell the UIHostingController which View to display. The problem is that I can't insert a condition now. Wherever the tap is on the screen, the segue is performed. I haven't found a way to cancel the segue in #IBSegueAction.
My code currently looks like this:
#IBSegueAction func showSettingsHostingControler(_ coder: NSCoder, sender: UITapGestureRecognizer, segueIdentifier: String?) -> UIViewController? {
let location = sender.location(in: self.tableView)
if let _ = self.tableView.indexPathForRow(at: location) {
return nil
} else {
if location.y < 32 {
if location.x < view.bounds.midX {
direction = .left
} else {
direction = .right
}
return UIHostingController(coder: coder, rootView: SettingsView())
}
}
return nil
}
The result currently is that the app segues to a non-existing Nil-view when the tap is in the wrong area.

based on a UITapGestureRecognizer. In case the tap is in the top area of the screen
It sounds to me like your tap gesture recognizer is attached to the wrong view. There should not be any decision to make here. Position a view in the top area of the screen and attach the tap gesture recognizer to that. That way, if this view gets a tap, there is no decision to be made: the tap is in the right place.

Related

SwiftUI: how to block swipe to dismiss behavior when presenting [duplicate]

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

Swift: Segue to another view when Map View is tapped

I would like to perform a segue to another view once a map view is tapped like when you check a location and tap on map on instagram, or like in foursquare.
I could not connect map view itself to another view directly like we do in buttons, and I don't want to perform segue when callout button is tapped.
I found a workaround by placing an invisible button into mapView, and performing segue when the button is tapped(user thinks mapView is tapped)
But I would like to know if there is a better solution for it, without a button.
You can use UITapGestureRecognizer
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapHandler(_:)))
mapView.addGestureRecognizer(gestureRecognizer)
#objc func tapHandler(_ gestureReconizer: UIGestureRecognizer) {
// let location = gestureReconizer.location(in: mapView)
// coordinatte of taplocation
// let coordinate = mapView.convert(location,toCoordinateFrom: mapView)
// **you can navigate or perform Your Actions there**
}

check if user clicked outside view controller

I am working with swift 4 for macOS and I would like to dismiss a view controller, if I clicked outside of this view controller.
With this code I can check, if the user has clicked into the view controller. but how can I check, if the user has clicked outside the view controller?
override func viewDidAppear() {
let gesture = NSClickGestureRecognizer(target: self, action: #selector(clicked))
gesture.buttonMask = 0x1 // left mouse
gesture.numberOfClicksRequired = 1
self.view.addGestureRecognizer(gesture)
}
#objc func clicked() {
print("Hello world")
}
NSEvent has a method called...
+ (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent * _Nullable (^)(NSEvent *))block;
...that captures events before they are dispatched.
Ask to monitor mouse up or mouse down events and use the block to compare the coordinates to your view's bounds.
Edit:
Except, in Swift, it's called...
class func addLocalMonitorForEvents(matching mask: NSEventMask, handler block: #escaping (NSEvent) -> NSEvent?) -> Any?
I would use touchesEnded: then use the event to grab the locationInWindow. If the location falls outside your bounds of your view then dismiss it. A similar question for iOS can be seen here.
IOS - How to hide a view by touching anywhere outside of it
I always prefer touchedEnded because if a user accidentally clicks outside they can still drag to the view to cancel the dismissal.

view move up in particular textfield delegate

I have to move the UIView in only last UITextField in Swift 3.0 on mentioned below delegate method using tag,
func textFieldDidBeginEditing(_ textField: UITextField) {
if (textField.tag == 4){
//UIView Up
}
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if (textField.tag == 4){
//UIView Down
}
return true
}
I tried many codes but none of them are working like notification,..etc.
You need to add Observers into the NotificationCenter for listening to both when Keyboard goes up and down (i'll assume your textfield outlet is lastTextField for this example to work but this obviously have to be adapted to whatever name you've had provide for it)
IBOutlet weak var passwordTextField: UITextField!
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
(Code above can be added in viewDidLoad())
Then you add methods to be executed when those notifications arrive, like this:
func keyboardWillShow(_ notification:Notification) {
if view.frame.origin.y >= 0 && lastTextField.isFirstResponder {
view.frame.origin.y -= getKeyboardHeight(notification)
}
}
func keyboardWillHide(_ notification:Notification) {
if view.frame.origin.y < 0 {
view.frame.origin.y += getKeyboardHeight(notification)
}
}
Validations within those methods prevent double execution like moving up/down twice when moving between textfields without resigning first responder which is common in cases like your (i assume your doing this for a form hence the clarification you only need it for the fourth textfield). Notice i'm only doing validation in for the specified textfield (with its outlet lastTextField) in the keyboardWillShow method, this in case you move thor another textfield while the keyboard is shown and resign responder from it in which case, even though it isn't the original place where you started, the view will return to its original place when the keyboard is hidden.
You'll also need a method for getting keyboard's height, this one can help with that:
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue // of CGRect
return keyboardSize.cgRectValue.height
}
Let me know how it goes but i just tested this same code on my app and it works so you should be fine.
PS: pay close attention to the storyboard (if you're using it) and that delegate for textfields are set up properly.
The problem you are trying to remedy is rather complicated, because it requires you to:
Find the textField which is firstResponder
Calculate where that TextField is relative to it's superViews
Determine the distance for the animation, so that the containing
superview doesnt jump out of the window, or jumps too
much/repeatedly
Animate the proper superView.
As you can see.. it's quite the algorithm. But luckily, I can help. However, this only works for a hierarchy which has the following layout:
superView (view in the case of UIViewController) > (N)containerSubviews > textFields
where N is an integer
or the following:
superView (view in the case of UIViewController) > textFields
The idea is to animate superView, based on which textField is firstResponser, and to calculate if it's position inside of the SCREEN implies that it either partially/totally obstructed by the Keyboard or that it is not positioned the way you want for editing. The advantage to this, over simply moving up the superView when the keyboard is shown in an arbitrary manner, is that your textField might not be positioned properly (ie; obstructed by the statusbar), and in the case where your textfields are in a ScrollView/TableView or CollectionView, you can simply scroll the texfield into the place you want instead. This allows you to compute that desired location.
First you need a method which will parse through a given superView, and look for which of it's subViews isFirstResponder:
func findActiveTextField(subviews : [UIView], textField : inout UITextField?) {
for view in subviews {
if let tf = view as? UITextField {
guard !tf.isFirstResponder else {
textField = tf; break
return
}
} else if !subviews.isEmpty {
findActiveTextField(subviews: view.subviews, textField: &textField)
}
}
}
Second, to aleviate the notification method, also make a method to manage the actual animation:
func moveFromDisplace(view: UIView, keyboardheight: CGFloat, comp: #escaping (()->())) {
//You check to see if the view passed is a textField.
if let texty = view as? UITextField {
//Ideally, you set some variables to animate with.
//Next step, you determine which textField you're using.
if texty == YourTextFieldA {
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The value needed
})
comp()
return
}
if texty == YourTextFieldB {
// Now, since you know which textField is FirstResponder, you can calculate for each textField, if they will be cropped out by the keyboard or not, and to animate the main view up accordingly, or not if the textField is visible at the time the keyboard is called.
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The Value needed
})
comp()
return
}
}
}
Finally, the method which is tied to the notification for the keyboardWillShow key; in this case, i have a UIViewController, with an optional view called profileFlow containing a bunch of UITextFields
func searchDisplace(notification: NSNotification) {
guard let userInfo:NSDictionary = notification.userInfo as NSDictionary else { return }
guard let keyboardFrame:NSValue = userInfo.value(forKey: UIKeyboardFrameEndUserInfoKey) as? NSValue else { return }
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let keybheight = keyboardHeight
var texty : UITextField? //Here is the potential textfield
var search : UISearchBar? //In my case, i also look out for searchBars.. So ignore this.
guard let logProfile = profileFlow else { return }
findActiveTextField(subviews: [logProfile], textField: &texty)
//Check if the parsing method found anything...
guard let text = texty else {
//Found something.. so determine if it should be animated..
moveFromDisplace(view: searchy, keybheight: keybheight, comp: {
value in
search = nil
})
return
}
//Didn't find anything..
}
Finally, you tie in this whole logic to the notification:
NotificationCenter.default.addObserver(self, selector: #selector(searchDisplace(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
I can't provide more content to the code, since it all depends on your view hierarchy, and how you want things to animate. So it's up to you to figure that out.
On a side note, usually, if you have so many textfields that to lay them out properly means they overstep the length of the screen.. it's probable that you could simplify your layout. A way to make this algorithm better would be to make sure you have all your textfields in one containing view, which again can become heavy for when, say, you use AutoLayout constraints. Odds are if you're in this situation, you can probably afford to add a flow of several views etc.
There is also the fact that i've never really needed to use this for iPhone views, more for iPad views, and even then for large forms only (e-commerce). So perhaps if you're not in that category, it might be worth reviewing your layout.
Another approach to this, is to use my approach, but to instead check for specific textFields right in the findActiveTextField() method if you only have a handful of textfields, and to animate things within findActiveTextField() as well if you know all of the possible positions they can be in.
Either way, i use inout parameters in this case, something worth looking into if you ask me.

Change tab in tab bar controller using finger swipe

I have three tabs in my tabbar controller and I want to switch between these tabs just like tinder switches the tab using finger touch. I have done it using UISwipeGestureRecognizer but its not exactly same as that of Tinder (the dating app ) swiping.
I have added UISwipeGestureRecognizer on one of the Tabbar controller and then added the function to change the tabbar selected index. But the animations is not controlled by finger touch. I want the swiping should be controlled by finger touch.
I think the best way to do it is to put all your tab views in a UIScrollView. You place them next to each other.
Implement the scroll view delegate methods in your tabbarController.
You'll probably need scrollViewDidEndScrollingAnimation and scrollViewDidEndDecelerating to know on which view you are when the user stops scrolling, like this:
let page_width=UIScreen.main.bounds.width
let page=Int(floor((scrollView.contentOffset.x-page_width/2)/page_width)+1)
Here, I assume each of your tab view is the same size as the screen.
I am bit late but I found my ans -
I have created 4 UIviewcontrollers programatically and then
created an array of it.
var views = [CareTeamTableViewController(),VFCChatQViewController(), NewAccountViewController(), ShareViewController()]
Then I added a scrollview in my main UiViewController
private func initMainScroll() {
scrollView = UIScrollView.init()
scrollView?.delegate = self
scrollView?.showsHorizontalScrollIndicator = false
scrollView?.isPagingEnabled = true
self.view.addSubview(scrollView!)
}
and then added the views array like :
func setupScrollView(complete:()->()) {
scrollView?.frame = views.first!.view.frame
scrollView?.contentSize = CGSize(width: CGFloat(views.count) * UIScreen.main.bounds.width, height: 0)
_ = views.map({ addViewToScrollView($0) })
_ = views.map({ $0.view.frame.origin = CGPoint(x: CGFloat(views.index(of: $0)!) * UIScreen.main.bounds.width, y: 0) })
complete()
}
func addViewToScrollView(_ viewController: UIViewController) {
scrollView?.addSubview(viewController.view)
viewController.willMove(toParentViewController: self)
addChildViewController(viewController)
}