ARSCNView freezes when presenting and dismissing UIViewController over it [duplicate] - ios

In my ARKit app I am presenting a modal window. When I close the modal and go back to the ARSCNView then I find out that the session is paused due to this code:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
When I close the modal and go back to the ARKit camera view screen this code gets fired:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
But this code never resumes the session. The screen is completely frozen on the last image it read. Any ideas?
I update the viewDidAppear code to be the following. It is still stuck on the camera screen with image frozen.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
sceneView.session.delegate = self
if self.isPaused {
sceneView.session.run(sceneView.session.configuration!)
} else {
// Run the view's session
sceneView.session.run(configuration)
}
}

Not sure why your session isn't resuming, but... this generally isn't a situation you want to be in anyway.
Notice in the readme that ships with Apple's ARKit sample code (attached to the WWDC17 session on ARKit):
Avoid interrupting the AR experience. If the user transitions to another fullscreen UI in your app, the AR view might not be an expected state when coming back.
Use the popover presentation (even on iPhone) for auxiliary view controllers to keep the user in the AR experience while adjusting settings or making a modal selection. In this example, the SettingsViewController and VirtualObjectSelectionViewController classes use popover presentation.
To go into a bit more detail: if you pause the session, it won't be tracking the world while your user is away in a different fullscreen view controller. That means that when you resume, any virtual content placed in the scene won't be in the positions (relative to the camera) where you left it.

I don't know if the iOS 11 GM Seed or XCode 9 GM Seed versions fixed this today however I can successfully resume a paused ARSCNview with code as in the original question.
sceneView.session.run(sceneView.session.configuration!)

I get that you have chosen an answer, and that answer is what is recommended by apple, you can restart the AR Session. You can't unpause/resume the Session though, because the device stops it's tracking once you're out of your controller presenting the ARSceneView and will stop keeping track of the position of your device relative to the objects you've placed in the scene.
Anyway, I've managed to restart the session essentially by destroying all aspects of my session and rebuilding them them when my view reappears, or through a button press.
I'll post some sample code here. It's in Objective-C cause my project was written in that, but it might help future people with the same question.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]
[self setupScene];
[self setupSession];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self destroySession];
[self destroyScene];
}
- (void)setupScene {
// Setup the ARSCNViewDelegate - this gives us callbacks to handle new
// geometry creation
self.sceneView.delegate = self;
// A dictionary of all the current planes being rendered in the scene
self.planes = [NSMutableDictionary new];
// Contains a list of all the boxes rendered in the scene
self.boxes = [NSMutableArray new];
// Show statistics such as fps and timing information
self.sceneView.showsStatistics = YES;
self.sceneView.autoenablesDefaultLighting = YES;
SCNScene *scene = [SCNScene new];
[self.sceneView setScene:scene];
self.sceneView.scene.physicsWorld.contactDelegate = self;
}
- (void)setupSession {
// Create a session configuration
ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfiguration new];
//ARWorldTrackingSessionConfiguration *configuration = [ARWorldTrackingSessionConfiguration new]; This has been deprecated in favor of the previous line in XCode 9 beta 5.
// Specify that we do want to track horizontal planes. Setting this will cause the ARSCNViewDelegate
// methods to be called when scenes are detected
//configuration.planeDetection = ARPlaneDetectionHorizontal;
// Run the view's session
[self.sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking];
}
-(void)destroyScene {
bottomPlane = nil;
[self.sceneView setScene:nil];
[self.sceneView setDebugOptions:nil];
self.boxes = nil;
self.planes = nil;
self.sceneView.delegate = nil;
}
-(void)destroySession {
[self.sceneView.session pause];
[self.sceneView setSession:nil];
}
These destroy methods are used when the view disappears. I am also restarting the AR Session on a button press, but it is not through these methods. It is as follows:
-(void)resetPressed{
NSLog(#"Reset Pressed");
[_sceneView.session pause];
SCNScene *scene = [[SCNScene alloc] init];
[_sceneView setScene:scene];
[_sceneView.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) {
[child removeFromParentNode];
}];
ARWorldTrackingConfiguration *configuration = [[ARWorldTrackingSessionConfiguration ARWorldTrackingConfiguration] init];
[_sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking | ARSessionRunOptionRemoveExistingAnchors];
}
Hope it helps.

Here's an answer working with Swift 4.2 and iOS 12.
To present UI defined in another view controller over your AR scene, create your view controller instance and set it's modalPresentationStyle property to .overCurrentContext:
EXAMPLE:
func showMaterialPicker(completion: (Texture?) -> Void) {
// create an instance of your view controller, I have convenience functions
// setup to do this via an extension on UIViewController
guard let materialPicker = MaterialCategoriesViewController.instance(from: .product) else {
print("Unable to instantiate MaterialCategoriesViewController, bailing")
return
}
// set presentation style and transition style
materialPicker.modalPresentationStyle = .overCurrentContext
materialPicker.modalTransitionStyle = .crossDissolve
// present the controller
present(materialPicker, animated: true, completion: nil)
}
Bonus tip:
To make your overlay appear to slide up from the bottom like a drawer, set
materialPicker.modalTransitionStyle = .coverVertical
then constrain your views in your overlay view controller a comfortable height from the bottom and set the background color of the view controllers view to UIColor.clear.
If you want to darken the AR view while your overlay is displayed you can set the background color to a black color with an opacity/alpha value of approximately 0.75.
Something like this:
self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.75)
or in the storyboard:
In the screenshot above I have a tableview pinned to the bottom and sides of the overlay view controllers view, and a height constraint of 300.
When done this way you can still see the AR view behind the overlay view controller and the scene continues to render.

Inside viewDidLoad: create a tap event.
arKitView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
Inside viewWillAppear:
arView.session.run(configuration)
And then define the tap event. In which first tap will pause the session and second tap will resume the session and so on.

Related

Safari View Controller Swipe Left to dismiss goes black

I have an iOS app which holds a wkWebView. This wkWebView has links which can pop open an instance of SafariViewController. When SafariViewController is launched and you swipe right to dismiss sometimes it works but sometimes it goes black.
I've tried multiple variations of setting interactivePopGestureRecognizer.enabled to false. Also setting its delegate to nil.
I have the delegate methods which have break points and none of them get hit.
I want to disable this feature entirely.
It seem likes a bug in iOS. You can try to workaround i.e
Add SafariViewController into the rootController of NavigationController
Then present the NavigationController instead of SafariViewController.
https://forums.developer.apple.com/thread/29048
https://www.cocoanetics.com/2015/10/swiping-away-sfsafariviewcontroller/
Here's a temporary workaround I'm using to disable the edge swipe gesture. It doesn't seem to be a problem as long as the Done button is used to dismiss.
let viewController = SFSafariViewController(URL: url)
presentViewController(viewController, animated: true) {
for view in viewController.view.subviews {
if let recognisers = view.gestureRecognizers {
for gestureRecogniser in recognisers where gestureRecogniser is UIScreenEdgePanGestureRecognizer {
gestureRecogniser.enabled = false
}
}
}
}
OC:
for (UIView * view in safari.view.subviews) {
NSArray<__kindof UIGestureRecognizer *> * array = view.gestureRecognizers;
if (array.count) {
for (UIScreenEdgePanGestureRecognizer * sepgr in array) {
sepgr.enabled = NO;
}
}
}
SFSafariViewController in iOS 9.2 | Apple Developer Forums

forceTouchCapability returning nil

I am trying to incorporate some 3D touch into an application and I've run into a weird issue where the forceTouchCapability check is returning nil on viewDidLoad but not in viewWillAppear/viewDidAppear.
I'm aware that this is only available on iOS 9+ so I've added checks to verify that the traitCollection property on the view controller responds to forceTouchCapability as in the following:
- (void)loadView {
self.view = [[MyView alloc] init];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Checking the force touch availability here
if ([self.traitCollection respondsToSelector:#selector(forceTouchCapability)] &&
self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// This won't get called because forceTouchCapability is returning nil
// which corresponds to UIForceTouchCapabilityUnknown
[self registerForPreviewingWithDelegate:self sourceView:self.view];
}
}
In LLDB with a breakpoint at the if statement, entering po [self.traitCollection forceTouchCapability] returns nil which corresponds to UIForceTouchCapabilityUnknown. However, the traitCollection itself is not nil.
According to the documentation for UIForceTouchCapabilityUnknown:
UIForceTouchCapabilityUnknown: The availability of 3D Touch is unknown. For example, if you create a view but have not yet added it to your app’s view hierarchy, the view’s trait collection has this value.
Has the view not been added to the hierarchy by this point?
I'm curious if anyone has run into this issue before and how to work around this? I would like to avoid adding this in the viewDidAppear as this can get called quite a bit.
If it helps, I'm running this on a 6S on iOS 9.1 with Xcode 7.2
The view hasn't been added to the View Hierarchy yet. You can see this easily by checking for a superview in the debug console
(lldb) po self.view.superview
nil
If that's what you're seeing, the view hasn't been added to a hierarchy yet: so you have to put your check elsewhere.
This is kind of confusing because in Apple's ViewControllerPreview sample app it's in viewDidLoad. But it really should in traitCollectionDidChange:, because then you're sure that the view has been added to the app's hierarchy.
This is the code I use (works on iOS 8, if you don't need to support that feel free to move the outer conditional).
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection respondsToSelector:#selector(forceTouchCapability)]) {
if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// retain the context to avoid registering more than once
if (!self.previewingContext) {
self.previewingContext = [self registerForPreviewingWithDelegate:self sourceView:self.view];
}
} else {
[self unregisterForPreviewingWithContext:self.previewingContext];
self.previewingContext = nil;
}
}
}
The added benefit to this is that your view will be registered/unregistered if the user changes their 3D Touch settings while the app is running.
I've also seen this issue, and found that the easiest way to check whether the device can support force touch or not is doing it via the screen instance. This kinda makes sense because the capability is a property of the screen. Doing it this way means you don't have to worry about the lifecycle of a viewcontroller or a view.
func canForceTouch() -> Bool
{
if iOS9OrHigher // pseudocode, a function that makes sure u only do this check on ios9 or higher
{
return UIScreen.mainScreen().traitCollection.forceTouchCapability == .Available
}
return false
}
Like what #bpapa said, Your view hasn't added to view hierarchy yet, But my solution is different little bit:
var token:dispatch_once_t = 0
override func viewDidAppear(animated: Bool) {
dispatch_once(&token) {
// Force Touch Checking
if #available(iOS 9.0, *) {
if self.traitCollection.forceTouchCapability == .Available {
self.registerForPreviewingWithDelegate(self, sourceView: self.view)
}
}
}
}

How to make a AVPlayerViewController go to fullscreen programmatically?

I'm trying to make a AVPlayerViewController go to full screen mode programmatically, coming from "embedded" mode, however this does not appear to be possible with the published API.
Is there a workaround that I'm missing? I'm interested in obtaining the same animation to the one that you get when the user presses the full screen button on the bottom right of the controls.
Using MPMoviePlayerController is not a viable alternative since I might have more than one video playing at a time.
Thanks.
AVPlayerViewController is a subclass of UIViewController, so it is presentable like any other view controller subclass. Are you able to use presentViewController:animated:completion?
self.avPlayerController.modalPresentationStyle = UIModalPresentationOverFullScreen;
[self presentViewController:self.avPlayerController animated:YES completion:nil];
This then shows the "Done" button in the top left-hand corner.
Updated for iOS 11
There is no supported way to programmatically go fullscreen with AVPlayerViewController (a bit of an oversight in my opinion).
However, AVPlayerViewController does contain a private method that does exactly that. You'll have to decide for yourself whether you'd want to use it or not given you're not supposed to call private methods.
AVPlayerViewController+Fullscreen.h
#import <AVKit/AVKit.h>
#interface AVPlayerViewController (Fullscreen)
-(void)goFullscreen;
#end
AVPlayerViewController+Fullscreen.m
#import "AVPlayerViewController+Fullscreen.h"
#implementation AVPlayerViewController (Fullscreen)
-(void)goFullscreen {
NSString *selectorForFullscreen = #"transitionToFullScreenViewControllerAnimated:completionHandler:";
if (#available(iOS 11.3, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:interactive:completionHandler:";
} else if (#available(iOS 11.0, *)) {
selectorForFullscreen = #"transitionToFullScreenAnimated:completionHandler:";
}
SEL fsSelector = NSSelectorFromString([#"_" stringByAppendingString:selectorForFullscreen]);
if ([self respondsToSelector:fsSelector]) {
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:fsSelector]];
[inv setSelector:fsSelector];
[inv setTarget:self];
NSInteger index = 2; //arguments 0 and 1 are self and _cmd respectively, automatically set
BOOL animated = YES;
[inv setArgument:&(animated) atIndex:index];
index++;
if (#available(iOS 11.3, *)) {
BOOL interactive = YES;
[inv setArgument:&(interactive) atIndex:index]; //arguments 0 and 1 are self and _cmd respectively, automatically set by NSInvocation
index++;
}
id completionBlock = nil;
[inv setArgument:&(completionBlock) atIndex:index];
[inv invoke];
}
}
#end
UPDATE: Swift 4 version of ToddH's answer:
private func enterFullscreen(playerViewController: AVPlayerViewController) {
let selectorName: String = {
if #available(iOS 11.3, *) {
return "_transitionToFullScreenAnimated:interactive:completionHandler:"
} else if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
In iOS11 there are 2 new properties for AVPlayerViewController: entersFullScreenWhenPlaybackBegins and exitsFullScreenWhenPlaybackEnds. You can enable full screen mode right after playback begins and disable it when playback ends with these properties. If you need to enable fullscreen mode after some delay you can use private API methods as ToddH mentioned in his answer. However in iOS11 _transitionToFullScreenViewControllerAnimated:completionHandler: method is not available anymore, there is the same method called _transitionToFullScreenAnimated:completionHandler:. The second method accepts the same arguments as the first one.
I can show an example how to use it. First of all you need to create AVPlayerViewController instance in your UIViewController:
private let playerController : AVPlayerViewController = {
if let urlForPlayer = URL(string: "your_video_url") {
$0.player = AVPlayer(url: urlForPlayer)
}
return $0
} (AVPlayerViewController())
Then you need to setup view for AVPlayerViewController and add it to your current controller view. Function setupAVplayerController can do it for you:
private func setupAVplayerController() {
self.addChildViewController(self.playerController)
self.playerController.view.frame = CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0)
self.view.addSubview(self.playerController.view)
self.playerController.didMove(toParentViewController: self)
}
Function enterFullscreen forces full screen mode for AVPlayerViewController:
private func enterFullscreen(playerViewController:AVPlayerViewController) {
let selectorName : String = {
if #available(iOS 11, *) {
return "_transitionToFullScreenAnimated:completionHandler:"
} else {
return "_transitionToFullScreenViewControllerAnimated:completionHandler:"
}
}()
let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
if playerViewController.responds(to: selectorToForceFullScreenMode) {
playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
}
}
And now you need to call all these functions where you need it, for example in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//Your code
self.setupAVplayerController()
self.playerController.player?.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.enterFullscreen(playerViewController:self.playerController)
}
}
Don't forget that this solution based on private API calls that is not recommended to use.
As a little iOS 14 update to ToddH's answer: the private API to call is enterFullScreenAnimated:completionHandler:. So here's an extension on AVPlayerViewController to enter full screen.
extension AVPlayerViewController {
func enterFullScreen(animated: Bool) {
perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
func exitFullScreen(animated: Bool) {
perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
}
This implementation doesn't offer a completion callback. If you pass a Swift closure to the completionHandler parameter, it crashes in the underlying Obj-C API. I haven't investigated how to pass the closure to make it work.
Swift 3 version for the answer of ToddH:
extension AVPlayerViewController {
func goFullScreen() {
let selector = NSSelectorFromString("_transitionToFullScreenViewControllerAnimated:completionHandler:")
if self.responds(to: selector) {
// first argument is animated (true for me), second is completion handler (nil in my case)
self.perform(selector, with: true, with: nil)
}
}
}
You can just set the videoGravity property of AVPlayerViewController.
if(fullscreen)
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspectFill];
}
else
{
[self.avPlayerController
setVideoGravity:AVLayerVideoGravityResizeAspect];
}
For an 'embedded' AVPlayerViewController instance, it is quite easy to programmatically have it start playback in full screen mode, and without hacking anything (calling private methods). You just need to set its entersFullScreenWhenPlaybackBegins property to true.
You need to add the controller as a child VC to the main VC, and that's basically it. In viewDidAppear(_:) you need to call play() method on the controller's player property - playback will be automatically started in fullscreen.
It's often best to check Apple sample code for these kind of tricky APIs; I think this one might be useful for a lot of AVPlayer use cases: Using AVKit in iOS.
I did not have the need to use any restricted code.
For this, I am assuming that you have added the AVPlayerViewController as a child view controller.
Then for that you will first have to remove the child view controller and then present it again as a fullscreen controller as well attach the AVPlayer view properly to it's parent view.
Here is how I did it. Please note that I am using a library called Easy Peasy for restoring the playerVC.view constraints - one can do that with proper constraints as well.
#objc func fullscreenButtonClicked() {
playerVC.willMove(toParentViewController: nil)
playerVC.view.removeFromSuperview()
playerVC.removeFromParentViewController()
self.present(self.playerVC, animated: false, completion: {
self.playerVC.view.easy.layout(Top(), Right(), Left(), Bottom())
})
}
Its pretty simple, just set
playerViewController.videoGravity = .resizeAspectFill
and it goes full screen:)

Prevent or detect events passed on from iOS 8 keyboard

In our iOS 8 app, the search screen, which is similar to the search screen of the App Store app, no longer works reliably. When a user taps a key, the keyboard is sometimes closed or even an action executed.
Part of the reason is that the tap event is passed on to lower layers, which is close the keyboard (smoke screen), navigate to a search result (UITableView with search result) or execute the search (UITableView with search term suggestions).
For some unknown reason, it properly works as long as the user stays in the app. However, if he/she goes to a different app and then returns, the events are passed on. This behavior affects all iOS 8 version (8.0.x, 8.1).
How can we prevent the keyboard from passing on tap events or how can we detect such an event (e.g. from tableView:didSelectRowAtIndexPath:)?
The question "Keyboard intermittently disappears when editing using IOS 8" seems to refer to the same problem though I can't figure out how to apply that ugly hack to my situation.
I've just found a similar post in Apple's developer forum. Unfortunately, it has no answers and has been archived in the mean time:
I have overriden -hitTest:withEvent: on a view on my view hierarchy
where I check if it was touched and will forward the touch to its
subviews and fire a selector to dismiss the keyboard.
On iOS 7 (and, more strangely, when the app is launched on iOS 8) this
works perfectly and -hitTest:withEvent: will never be called if the
view is behind the keyboard and the user taps on the keyboard.
But on iOS 8, if the user sends the app to the background and brings
it back to the foreground, tapping anything on the keyboard will
trigger -hitTest:withEvent: as if the view was above the keyboard on
the view hierarchy. I've used Reveal.app to verify that it is not
above the keyboard, it is behind as expected.
Anyone got any ideas of what could be happening? I've created a sample
project and attached it to a radar for Apple as this looks like a bug
on iOS 8 for not working the same way consistently.
Update
My search screen contains two views (on top of each other): a background view visible when no search results are available and a table view visible if search results are available. On top of these, I dynamically add two additional views if the search bar becomes active: a smoke class view that can be tapped to end the search text entry and a table view that displays search text suggestions. All four views are directly contained in the view controller's main view and cover the full area.
The interesting thing now is that the keyboard forwards event the two dynamically added views but not to the two lower views that are always there.
I believe it's a bug that tap events are passed on to views underneath the keyboard. As I've figured out in the mean time that only the dynamically added views are affected and not the ones that are part of the Interface Builder file, I've now come up with a work around: if the keyboard appears, I shrink these two view so they do not extend underneath the keyboard. And I grow them again when the keyboard disappears.
So this is the code. The upper part up to (and excluding) [[NSNotificationCenter defaultCenter] has existed before. The rest is the workaround.
- (BOOL) searchBarShouldBeginEditing: (UISearchBar*) searchBar {
// calculate from for entire area
CGRect frame = ... // omitted
// add smoke screen
_smokeScreen = [UIButton buttonWithType: UIButtonTypeCustom];
_smokeScreen.frame = frame;
_smokeScreen.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_smokeScreen.backgroundColor = [UIColor colorWithWhite: 0.0f alpha: 0.5f];
[_smokeScreen addTarget: self action: #selector(smokeScreenPressed:) forControlEvents: UIControlEventTouchDown];
[self.view addSubview: _smokeScreen];
// add table view for search term suggestions
_suggestionTableView = [[SearchControllerTableView alloc] initWithFrame:frame style:UITableViewStylePlain];
_suggestionTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_suggestionTableView.dataSource = self;
_suggestionTableView.delegate = self;
_suggestionTableView.searchController = self;
_suggestionTableView.hidden = YES;
[self.view addSubview:_suggestionTableView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification object:nil];
return YES;
}
- (void) keyboardDidShow:(NSNotification *) notification
{
// shrink the smoke screen area and the table view because otherwise they'll receive tap events from the keyboard
CGRect screenRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect windowRect = [self.view.window convertRect:screenRect fromWindow:nil];
CGRect viewRect = [self.view convertRect:windowRect fromView:nil];
[self setBottom: viewRect.origin.y];
}
- (void) keyboardWillHide:(NSNotification *) notification
{
// grow the views again
[self setBottom: self.view.frame.size.height];
}
- (void) setBottom: (CGFloat)y
{
CGRect frame = _suggestionTableView.frame;
frame.size.height = y - frame.origin.y;
_suggestionTableView.frame = frame;
frame = _smokeScreen.frame;
frame.size.height = y - frame.origin.y;
_smokeScreen.frame = frame;
}
You can try to play with view property userInteractionEnabled or
exclusiveTouch when search view called.
I come across this problem with Xamarin.iOS, this is the solution for Xamarin.iOS use C#.
In WillMoveToSuperview method, observe keyboard show/hide event and store the keyboard frame in variable KeyboardEndFrame:
if (newsuper != null)
{
var wrThis = new WeakReference<XInterceptTouchView> (this);
// add notification observe
keyboardDidShow = UIKeyboard.Notifications.ObserveDidShow ((sender, e) =>
{
XInterceptTouchView interceptTouchView;
if (wrThis.TryGetTarget (out interceptTouchView))
{
interceptTouchView.KeyboardEndFrame = e.FrameEnd;
}
});
keyboardDidHide = UIKeyboard.Notifications.ObserveDidHide ((sender, e) =>
{
XInterceptTouchView interceptTouchView;
if (wrThis.TryGetTarget (out interceptTouchView))
{
interceptTouchView.KeyboardEndFrame = e.FrameEnd;
}
});
}
else
{
// remove notification observe
keyboardDidShow?.Dispose ();
keyboardDidHide?.Dispose ();
}
In HitTest method, get the keyboard window and process touch event with the window:
if (KeyboardEndFrame.Contains (point))
{
IntPtr handle = ObjCRuntime.Class.GetHandle ("UITextEffectsWindow");
if (handle != IntPtr.Zero)
{
var keyboardWindow = UIApplication.SharedApplication.Windows.FirstOrDefault (w => w.IsKindOfClass (new ObjCRuntime.Class ("UITextEffectsWindow")));
if (keyboardWindow != null)
return keyboardWindow.HitTest (point, uievent);
}
}
var hitTestView = base.HitTest (point, uievent);
this.EndEditing (true);
return hitTestView;
I found that keyboard touches trigger hitTest if I show the keyboard, send the application to the background, and come back. This shouldn’t happen so it looks like an iOS bug. I see two solutions:
#1 Send the touch away
Solution #1: hitTest shouldn’t be called for touches on the keyboard, so check against the keyboard frame and send the touch away.
var keyboardFrame: CGRect?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
if let keyboardFrame = keyboardFrame {
if keyboardFrame.contains(point){
return super.hitTest(CGPoint(x:-1,y:-1), with: event)
}
}
return super.hitTest(point, with: event)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func subscribeKeyboard(){
NotificationCenter.default.addObserver(self, selector: #selector(MyView.keyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(MyView.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardDidShow(_ notification: NSNotification) {
keyboardFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
}
func keyboardWillHide(_ notification: NSNotification) {
keyboardFrame = nil
}
#2 Pull the keyboard before resigning
Solution 2#: Pull the keyboard before resigning active. Two flaws: you are touching the delegate to fix something elsewhere, and the user is surprised to find the keyboard down when the app comes back.
func applicationWillResignActive(_ application: UIApplication) {
UIApplication.shared.sendAction(#selector(self.resignFirstResponder), to: nil, from: nil, for: nil)
}
Or if you want this done for a particular view controller only:
if let controller = UIApplication.shared.keyWindow?.topmostViewController {
if controller is MyViewController {
NSLog("Pulling the keyboard to prevent touches from falling through after coming back from the background.")
UIApplication.shared.sendAction(#selector(self.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
where topmostViewController is:
extension UIViewController {
static var topmostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.topmostViewController
}
var topmostViewController: UIViewController? {
return presentedViewController?.topmostViewController ?? self
}
}
extension UINavigationController {
override var topmostViewController: UIViewController? {
return visibleViewController?.topmostViewController
}
}
extension UITabBarController {
override var topmostViewController: UIViewController? {
return selectedViewController?.topmostViewController
}
}
extension UIWindow {
var topmostViewController: UIViewController? {
return rootViewController?.topmostViewController
}
}
Leonardo Cardoso wrote the extensions above.

UIActivityViewController freezes when email or text but not fb or Twitter

I have a very standard implementation of UIActivityViewController. When I use Twitter or Facebook, the view controller is dismissed, and the app continues working. However, when I email or text the same content, the view controller is dismissed but the app freezes (not crashes). Everything is still on screen but frozen - no input etc.
Perhaps the Mail or Message apps have not released control back to my app? Is there a way using Instruments to analyze what's going on?
Thanks!
I am getting a leak from this part from NSArray as the offenders
- (void)postToFacebook:(UITapGestureRecognizer *)sender
{
NSString *postText = #"Testing";
UIImage *imageToPost = [self captureTheScreenImage];
NSArray *postItems = #[postText, imageToPost];
UIActivityViewController *activityPostVC = [[UIActivityViewController alloc]initWithActivityItems:postItems applicationActivities:nil];
NSArray *excludedItems = #[UIActivityTypePostToWeibo,UIActivityTypePrint,UIActivityTypeCopyToPasteboard,UIActivityTypeAssignToContact,UIActivityTypeSaveToCameraRoll, UIActivityTypeMail, UIActivityTypeMessage];
[activityPostVC setExcludedActivityTypes:excludedItems];
[self presentViewController:activityPostVC animated:YES completion:nil];
}
This issue happened to me too when using multiple UIWindow objects at the same time.
Upon dismissal of the UIActivityViewController, the presenting windows contents are not restored correctly. Specifically, the window's first subview (UILayoutContainerView) is missing its constraints to the superview. This causes the window the width of the UILayoutContainerView to be zero causing the window appear transparent and reveal the window underneath it but not allowing user interaction.
The fix can be to place an empty transparent window on top of the current window and present the UIActivityViewController from an empty view controller associated with that new window. When the UIActivityViewController is dismissed, we can dispose of the empty window that it was presented from.
import Foundation
private var previousWindow: UIWindow?
private var activityViewControllerWindow: UIWindow?
extension UIViewController {
fileprivate var isActivityViewControllerWindowPresented: Bool {
return activityViewControllerWindow?.isKeyWindow ?? false
}
func presentActivityViewController(_ activityViewController: UIActivityViewController, animated: Bool = true, completion: (() -> Void)? = nil) {
if isActivityViewControllerWindowPresented {
return
}
let window = UIWindow(frame: view.window!.frame)
previousWindow = UIApplication.shared.keyWindow
activityViewControllerWindow = window
window.rootViewController = UIViewController()
window.makeKeyAndVisible()
let activityCompletionClosure = activityViewController.completionWithItemsHandler
activityViewController.completionWithItemsHandler = { [weak self] (activityType, completed, returnedItems, activityError) in
self?.cleanUpActivityViewControllerWindow()
activityCompletionClosure?(activityType, completed, returnedItems, activityError)
}
window.rootViewController?.present(activityViewController, animated: animated, completion: completion)
}
fileprivate func cleanUpActivityViewControllerWindow() {
previousWindow?.makeKeyAndVisible()
activityViewControllerWindow?.rootViewController = nil
activityViewControllerWindow = nil
}
}
Yes, there is a way, like you mentioned, using Instruments. But If I were to foreshadow your results, I'd say you might want to do network calls on a non-UI thread, somewhere in the background so that your UI thread can do its thang while your app talks to Twitter or Facebook.

Resources