Hanging (Freezing) issue UISemanticContentAttribute = .ForceRightToLeft - ios

I am internally switching the App Language (LTR-RTL) and then re-initializing the storyboard.
here is the piece of code:
let semanticContentAttribute: UISemanticContentAttribute = language == .Arabic ? .ForceRightToLeft : .ForceLeftToRight
UIView.appearance().semanticContentAttribute = semanticContentAttribute
UINavigationBar.appearance().semanticContentAttribute = semanticContentAttribute
The issue is, all the presented view controllers freezes for 3-6 seconds while dismissing it.
What is causing this?

Setting semanticContentAttribute on the appearance() proxy is not supported. You're going to run into many other issues and bugs since the app still believes it's running in a language that isn't the one you're overriding.
Adding a language switcher to your app is only going to make it more confusing; users expect their apps to follow the language that their device is set to.

I found the solution for it after searching long time, if someone seeks.
The reason that the UI freezes / hangs is because the UINavigationController is missing a check for the root view controller when the gesture is executed on the root view. There a few ways to fix that, the following is what I did.
You should subclass UINavigationController, this is the right way to go and add implement as followed:
class RTLNavController: UINavigationController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Adding swipe to pop viewController
self.interactivePopGestureRecognizer?.isEnabled = true
self.interactivePopGestureRecognizer?.delegate = self
// UINavigationControllerDelegate
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
navigationController.view.semanticContentAttribute = UIView.isRightToLeft() ? .forceRightToLeft : .forceLeftToRight
navigationController.navigationBar.semanticContentAttribute = UIView.isRightToLeft() ? .forceRightToLeft : .forceLeftToRight
}
// Checking if the viewController is last, if not disable the gesture
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if self.viewControllers.count > 1 {
return true
}
return false
}
}
extension UIView {
static func isRightToLeft() -> Bool {
return UIView.appearance().semanticContentAttribute == .forceRightToLeft
}
}
Resources:
Original question:
Slide gesture with custom back button freezes the root view controller
The answer is used for the solution:
https://stackoverflow.com/a/27683249/1138498
Other solution which may work better (But it's in Objective-C):
https://stackoverflow.com/a/28364126/1138498

Related

iOS UINavigationBar button remains faded after segue back

In my app I have multiple view controllers, and most have a right-hand-side UIBarButtonItem with direct "show" segue actions attached.
Having segued to another view and then pressed the '< Back' button, the original button item remains faded out, although still otherwise usable.
This only appears to happen under iOS 11.2.
I can't see any setting that could be doing this, and in at least one of the cases where this happens there's no specific segue unwinding nor viewDidAppear handling. I'd post some code, but AFAICS it's all just default UINavigationBar behaviour.
This is a bug in iOS 11.2 and happens because the UIBarButtonItem stays highlighted after navigation and does not return to its normal state after the other view controller pops.
To avoid this behavior, either
use a UIBarButtonItem with a UIButton as a custom view
disable and re-enable the bar button item in viewWillDisappear(_:) (although this causes the button to appear immediately, use matt's solution to avoid this):
barButtonItem.isEnabled = false
barButtonItem.isEnabled = true
What I do is work around this bug, in the view controller's viewWillAppear, as follows:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.tintAdjustmentMode = .normal
self.navigationController?.navigationBar.tintAdjustmentMode = .automatic
}
That seems to wake up the button without visual artifacts.
Another work around is to implement the fix on the parent navigationController - so that each of its child viewController's gets the fix as follows
NOTE: This requires the receiving class to be setup as the UINavigationController delegate
Swift
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if #available(iOS 11.2, *) {
navigationBar.tintAdjustmentMode = .normal
navigationBar.tintAdjustmentMode = .automatic
}
}
Objective-C
-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (#available(iOS 11.2, *)) {
self.navigationBar.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
self.navigationBar.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
}
}
I solved it like this:
override func viewWillDisappear(_ animated: Bool) {
navigationController?.navigationBar.tintAdjustmentMode = .normal
navigationController?.navigationBar.tintAdjustmentMode = .automatic
}
so it will restore the color before the other view appear

Change interactivePopGestureRecognizer direction

My app supports both English and Arabic. interactivePopGestureRecognizer works properly when using English, ie on swiping from left to right, it pops viewController. But when i am using arabic, I have changed the semanticContentAttribute from right to left.
if([[[NSUserDefaults standardUserDefaults] objectForKey:#"LanguageCode"] isEqualToString:#"en"])
{
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceLeftToRight]; //View for English language
}
else
{
[[UIView appearance] setSemanticContentAttribute:UISemanticContentAttributeForceRightToLeft]; //mirror view for Arabic language
}
But the interactivePopGestureRecogniser is still from left to right. How can I change the direction of interactivePopGestureRecogniser such that it supports Arabic? I want to swipe from right to left to pop view controller on using Arabic language.
I found the solution for it after searching long time, if someone seeks.
The previous answer may cause the UI to hang / freeze.
The reason that the UI freezes / hangs is because the UINavigationController is missing a check for the root view controller when the gesture is executed on the root view. There a few ways to fix that, the following is what I did.
You should subclass UINavigationController, this is the right way to go and add implement as followed:
class RTLNavController: UINavigationController, UINavigationControllerDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Adding swipe to pop viewController
self.interactivePopGestureRecognizer?.isEnabled = true
self.interactivePopGestureRecognizer?.delegate = self
// UINavigationControllerDelegate
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
navigationController.view.semanticContentAttribute = UIView.isRightToLeft() ? .forceRightToLeft : .forceLeftToRight
navigationController.navigationBar.semanticContentAttribute = UIView.isRightToLeft() ? .forceRightToLeft : .forceLeftToRight
}
// Checking if the viewController is last, if not disable the gesture
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if self.viewControllers.count > 1 {
return true
}
return false
}
}
extension UIView {
static func isRightToLeft() -> Bool {
return UIView.appearance().semanticContentAttribute == .forceRightToLeft
}
}
Resources:
Original question:
Slide gesture with custom back button freezes the root view controller
The answer is used for the solution:
https://stackoverflow.com/a/27683249/1138498
Other solution which may work better (But it's in Objective-C):
https://stackoverflow.com/a/28364126/1138498
After a lot of trials, the only solution worked for me was this:
Swift 3:
extension UIViewController {
open override func awakeFromNib() {
super.awakeFromNib()
navigationController?.view.semanticContentAttribute = .forceRightToLeft
navigationController?.navigationBar.semanticContentAttribute = .forceRightToLeft
}
}
You can exclude the semantic attribute for certain types like:
UIView.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).semanticContentAttribute = .forceLeftToRight

An alternative to "Storyboard Reference" for iOS 8 when handling Relationship Segue?

My App has a TabBarViewController containing 4 tabs. One of the tabs is Settings which I want to move to a separate storyboard. If I am only consider iOS 9 and above as my deployment target, then I can just refactor the SettingsTab using Storyboard Reference. However I want to target iOS 8 as well. Since Storyboard Reference doesn't support Relationship Segue, I can't rely on it in this case.
So in the main storyboard which contains the TabBarViewController, I keep a dummy SettingsTabViewController as an empty placeholder. And in the function "viewWillAppear" in its class file, I push the view to the real SettingsTabViewController in the Settings.storyboard. This works fine. But the problem is if I keep tabbing the Settings tab, the empty placeholder view controller will show up for a short time and then goes back to the real Settings view.
I tried to implement this delegate to lock the Settings tab:
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
return viewController != tabBarController.selectedViewController
}
However, the other three tabs were locked too after I implemented this delegate.
Is it possible to just lock the Settings tab without locking other three tabs? And in which view controller exactly should I implement this delegate?
Yes, it's possible. You need to check the index;
with the following code not only you can prevent locking other tabs, but also you still have tap on tab goto root view controller feature.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
let tappedTabIndex = viewControllers?.indexOf(viewController)
let settingsTabIndex = 3 //change the index
if tappedTabIndex == settingsTabIndex && selectedIndex == settingsTabIndex {
guard let navVC = viewController as? UINavigationController else { return true }
guard navVC.viewControllers.count > 1 else { return true }
let firstRealVC = navVC.viewControllers[1]
navVC.popToViewController(firstRealVC, animated: true)
return false
}
return true
}
.
This answers your question, but still you would have the settingsVC showing up for a moment. To avoid this you simply need to turn off the animation while you're pushing it. so you need to override viewWillAppear in the following way.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let theVC = storyboard?.instantiateViewControllerWithIdentifier("theVC") {
navigationController?.pushViewController(theVC, animated: false)
}
}
after adding above code you still would see a back button in your real first viewController. You can hide it:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true
}

Extend default interactivePopGestureRecognizer beyond screen edge?

I have a UIViewController that gets pushed onto a navigation stack. I'd like to extend the standard iOS7 interactive pan gesture to pop this view controller beyond the default UIRectEdgeLeft boundaries, so that users can initiate an interactive back action by panning from anywhere on the view.
I've tried rolling my own interactive view controller transition, but it's a lot of hassle to fully replicate the nice parallax handling of the default interactivePopGestureRecognizer. For example, the fromViewController hides the navigation bar, while the toViewController shows it—something that is not easy to handle in a custom interactive transition, but is seamless in the default action.
As a result, I want to extend the default action to a larger area of pan gesture, but the API doesn't seem to support simply replacing the gesture.
Any creative suggestions?
Check out my library SloppySwiper, which achieves this by using UIPanGestureRecognizer and by recreating the default animation. You can also see my ideas in https://github.com/fastred/SloppySwiper/issues/1.
It's actually quite easy to do on the UINavigationController subclass without any intervention into every UIViewController subclass pushed. Also respecting built-in swipe-from-edge state (so when it's disabled for some reason, the new gesture is disabled as well):
import UIKit
class NavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
setupFullWidthBackGesture()
}
private lazy var fullWidthBackGestureRecognizer = UIPanGestureRecognizer()
private func setupFullWidthBackGesture() {
// The trick here is to wire up our full-width `fullWidthBackGestureRecognizer` to execute the same handler as
// the system `interactivePopGestureRecognizer`. That's done by assigning the same "targets" (effectively
// object and selector) of the system one to our gesture recognizer.
guard
let interactivePopGestureRecognizer = interactivePopGestureRecognizer,
let targets = interactivePopGestureRecognizer.value(forKey: "targets")
else {
return
}
fullWidthBackGestureRecognizer.setValue(targets, forKey: "targets")
fullWidthBackGestureRecognizer.delegate = self
view.addGestureRecognizer(fullWidthBackGestureRecognizer)
}
}
extension NavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let isSystemSwipeToBackEnabled = interactivePopGestureRecognizer?.isEnabled == true
let isThereStackedViewControllers = viewControllers.count > 1
return isSystemSwipeToBackEnabled && isThereStackedViewControllers
}
}

UINavigationController Interactive Pop Gesture Not Working?

So I have a navigation controller in my built for iOS 7 app. The titleView is visible, as well as the back button and navigation bar its self. For some reason, the interactive pop gesture (swipe from the left edge) isn't working. Nothing happens. When I log the gesture, it is not nil. Is there anything special I have to do to enable this functionality? What could cause it not to work?
I have found that when using custom back buttons, the interactive pop gesture stops working (my take is that Apple cannot foresee how your custom back button will behave, so they disable the gesture).
To fix this, as other mentioned before, you can set the interactivePopGestureRecognizer.delegate property to nil.
In Swift, this can easily be done across your entire application by adding an extension for UINavigationController like this:
extension UINavigationController {
override public func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = nil
}
}
Updated answer
Seems like setting the delegate to nil causes the app UI to freeze in some scenarios (eg. when the user swipes left or right on the top view controller of the navigation stack).
Because gestureRecognizerShouldBegin delegate method cannot be handled in an extension, subclassing UINavigationController seems like the best solution:
class NavigationController: UINavigationController, UIGestureRecognizerDelegate {
/// Custom back buttons disable the interactive pop animation
/// To enable it back we set the recognizer to `self`
override func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Eh, looks like I just had to set the gesture delegate and implement the following:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
Look at this response and comments. All you have to do is set your navigation controller's interactive pop gesture recognizer's delegate to nil:
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
Setting it to a casted self to id<UIGestureRecognizerDelegate> also works because all methods in the protocol are optional, but I think setting the delegate to nil is more appropriate in this case.
My answer is based on Eneko's answer but uses only an extension on UINavigationController and works in Swift 5:
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
You can put this line in the viewDidLoad method.
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
If you feel you have tried all solutions and stretching your head then you're at the right place.
Goto simulator > Window > Enable Show Device Bezels
Now tried to simulate swipe to back gesture.
The more worked out answer was both Aaron and lojals
First Customise the Navigation controller and then put this code in the class
In ViewDidload put this line:
self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
And in class write this function
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES;}
Maybe someone may find this helpful.
If you want to hide the navigation bar but use normal swipe gestures to go back and other navigation controller features, you should use: (navigationBar)
self.navigationController?.navigationBar.isHidden = true
If you want to disable navigation bar (hide navigation bar, disable swipe for back) but want to push viewcontroller you should use: (isNavigationBarHidden)
self.navigationController?.isNavigationBarHidden = true
Update 7-DEC-2018:
Recommended way:
In case that your first controller use hidden navigation bar, but next childs use navigation bar, when you come back to base view controller you will see a black bar in transition in place of navigation bar. This will be fixed very easy if you use in first viewcontroller(parent):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
In Swift 4, I have a UITableView inside my view controller, I solved this issue with:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.delegate=nil
}
Generically add interactive pop gesture to the whole app.
XCODE: 9.0, Swift: 4.0
Preferably create UINavigationController in AppDelegate.swift
Create a navigation controller
// I created a global variable, however not necessarily you will be doing this way
var nvc: UINavigationController!
implement UIGestureRecognizerDelegate
class AppDelegate: UIResponder, UIApplicationDelegate, UIGestureRecognizerDelegate {
Instantiat UINavigationController in application didFinishLaunchingWithOptions function
nvc=UINavigationController()
// For interactive pop gesture
nvc.navigationBar.isHidden=true
nvc?.interactivePopGestureRecognizer?.delegate=self
Extra step, add controller to navigation controller in application didFinishLaunchingWithOptions function
window=UIWindow()
window?.rootViewController=nvc
window?.makeKeyAndVisible()
// BaseViewController is sample controller i created with xib
nvc.pushViewController(BaseViewController(), animated: true)
Implement gusture recognizer, add below code to AppDelegate.swift
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Note: See other post in this section for the difference between
self.navigationController?.navigationBar.isHidden=true
And
self.navigationController?.isNavigationBarHidden = true

Resources