I need to implement a dismissive keyboard (swiping down to dismiss) like the one in the stock messages app on iOS.
I have this code to get the keyboard view:
func keyboardWillShowWithNotification(notification:NSNotification) {
let keyboardView = accessoryView.superview
}
And I connected the UIPanGestureRecognizer of the tableView to detect when I need to start moving the keyboard down.
func handleTableViewPan(gr:UIPanGestureRecognizer) {
let location = panGestureRecognizer.locationInView(self.view)
let offset = ... //calculated correctly
keyboardView.frame.origin.y = originalKeyboardFrame.origin.y + offset
}
The method worked fine with iOS 8 but with iOS 9 it seems like the keyboard is hold in place a little different so I can't move it.
Maybe someone encountered the same problem and can help me.
Thank you.
In iOS 9 there is a new window for keyboard named UIRemoteKeyboardWindow, so when you are using accessoryView.superview you will get a wrong view.
To get correct view try to find it from window hierarchy directly:
(objective-c code)
-(UIView*)getKeyboardInputView {
if([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) {
for(UIWindow* window in [[UIApplication sharedApplication] windows])
if([window isKindOfClass:NSClassFromString(#"UIRemoteKeyboardWindow")])
for(UIView* subView in window.subviews)
if([subView isKindOfClass:NSClassFromString(#"UIInputSetHostView")])
for(UIView* subsubView in subView.subviews)
if([subsubView isKindOfClass:NSClassFromString(#"UIInputSetHostView")])
return subsubView;
} else {
return accessoryView.superview;
}
return nil;
}
P.S. Taken from DAKeyboardControl https://github.com/danielamitay/DAKeyboardControl/pull/98
Related
Long-pressing images or links in a WKWebView on iOS 11 and 12 initiates a Drag & Drop session (the user can drag the image or the link). How can I disable that?
I did find a solution that involves method swizzling but it's also possible to disable drag and drop in a WKWebView without any swizzling.
Note: See special notes for iOS 12.2+ below
WKContentView — a private subview of WKWebView's WKScrollView — has an interactions property, just like any other UIView in iOS 11+. That interactions property contains both a UIDragInteraction and a UIDropInteraction. Simply setting enabled to false on the UIDragInteraction does the trick.
We don't want to access any private APIs and make the code as solid as possible.
Assuming your WKWebView is called webView:
if (#available(iOS 11.0, *)) {
// Step 1: Find the WKScrollView - it's a subclass of UIScrollView
UIView *webScrollView = nil;
for (UIView *subview in webView.subviews) {
if ([subview isKindOfClass:[UIScrollView class]]) {
webScrollView = subview;
break;
}
}
if (webScrollView) {
// Step 2: Find the WKContentView
UIView *contentView = nil;
// We don't want to trigger any private API usage warnings, so instead of checking
// for the subview's type, we simply look for the one that has two "interactions" (drag and drop)
for (UIView *subview in webScrollView.subviews) {
if ([subview.interactions count] > 1) {
contentView = subview;
break;
}
}
if (contentView) {
// Step 3: Find and disable the drag interaction
for (id<UIInteraction> interaction in contentView.interactions) {
if ([interaction isKindOfClass:[UIDragInteraction class]]) {
((UIDragInteraction *) interaction).enabled = NO;
break;
}
}
}
}
}
That's it!
Special note for iOS 12.2+
The above code still works on iOS 12.2, but it is important when to call it. On iOS 12.1 and below you could call this code right after creating the WKWebView. That's not possible anymore. The WKContentView's interactions array is empty when it's first created. It is only populated after the WKWebView is added to a view hierarchy that is attached to a UIWindow - simply adding it to a superview that is not yet part of the visible view hierarchy is not enough. In a view controller viewDidAppear would most likely be a safe place to call it from.
How did I find this out?
I searched through the WebKit source and found this: https://github.com/WebKit/webkit/blob/65619d485251a3ffd87b48ab29b342956f3dcdc7/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L4953
That's the method that creates and adds the UIDragInteraction
It turns out that this method (setupDataInteractionDelegates) actually exists on WKContentView
So I set a symbolic breakpoint on -[WKContentView setupDataInteractionDelegates]
The breakpoint was hit
I used lldb to print the backtrace using the bt command
This was the output:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 50.1
* frame #0: 0x00000001115b726c WebKit`-[WKContentView(WKInteraction) setupDataInteractionDelegates]
frame #1: 0x00000001115a8852 WebKit`-[WKContentView(WKInteraction) setupInteraction] + 1026
frame #2: 0x00000001115a5155 WebKit`-[WKContentView didMoveToWindow] + 79
So clearly the creation and addition of the UIDragInteraction is triggered by the view moving to (being added to) a window.
This works great!
Thanks #basha for the swift version.
I did the same but with some compactMaps to reduce the depth of the if-statements and guards to get rid of the force unwraps.
private func disableDragAndDropInteraction() {
var webScrollView: UIView? = nil
var contentView: UIView? = nil
if #available(iOS 11.0, *) {
guard let noDragWebView = webView else { return }
webScrollView = noDragWebView.subviews.compactMap { $0 as? UIScrollView }.first
contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 })
guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else { return }
contentView?.removeInteraction(dragInteraction)
}
}
Based on Johannes FahrenKrug's Post, with some changes.
private func disableDragAndDropInteraction() {
var webScrollView: UIView? = nil
var contentView: UIView? = nil
if #available(iOS 11.0, *) {
if (webView != nil) {
for subView in webView!.subviews {
if (subView is UIScrollView) {
webScrollView = subView
break
}
}
if (webScrollView != nil) {
for subView in webScrollView!.subviews {
if subView.interactions.count > 1 {
contentView = subView
break
}
}
if (contentView != nil) {
for interaction in contentView!.interactions {
if interaction is UIDragInteraction {
contentView!.removeInteraction(interaction)
}
}
}
}
} else {
// Fallback on earlier versions
}
}
}
Use the CSS property webkit-touch-callout on the img and a elements. Also set their draggable attribute to false by changing the HTML or injecting it using -[WKWebView evaluateJavaScript(...)].
img, a {
-webkit-touch-callout: none;
}
<img draggable="false">
Avoids all the fragile subview diving and manipulation.
(Recap of my answer to the similar Disable link drag on WKWebView .)
My approach was to subclass WKWebView and search the view hierarchy for subviews that contain drag or drop interactions, recursively. Once a view is found, which most likely will be a WKContentView, the interactions are removed. The benefit of this method is to not rely on any subview order / view hierarchy, which could change between OS releases.
override func didMoveToWindow() {
super.didMoveToWindow()
disableDragAndDrop()
}
func disableDragAndDrop() {
func findInteractionView(in subviews: [UIView]) -> UIView? {
for subview in subviews {
for interaction in subview.interactions {
if interaction is UIDragInteraction {
return subview
}
}
return findInteractionView(in: subview.subviews)
}
return nil
}
if let interactionView = findInteractionView(in: subviews) {
for interaction in interactionView.interactions {
if interaction is UIDragInteraction || interaction is UIDropInteraction {
interactionView.removeInteraction(interaction)
}
}
}
}
Hi I am trying to remove the UIWebFormAccesory that appears on top of the keyboard on iOS when entering text to a WKWebView text field. I have a slight hack below based on previous answers to this question, but they are all outdated and no longer work. I've added a picture below, with a red box around the bar I'm talking about:
The code below works if I run it 2x in a row, but then causes weird behavior on the keyboard, which eventually crashes the application.
for (UIWindow* wind in [[UIApplication sharedApplication] windows]) {
for (UIView* currView in wind.subviews) {
if ([[currView description] containsString:#"UIInputSetContainerView"]) {
for (UIView* perView in currView.subviews) {
for (UIView *subView in perView.subviews) {
if ([[subView description] containsString:#"UIWebFormAccessory"]) {
[subView setHidden:true];
[subView removeFromSuperview];
// CGRect f = perView.frame;
// CGRect n = CGRectMake(0, f.origin.y, f.size.width, f.size.height-44.0);
// [perView setFrame:n];
}
}
}
}
}
}
Was wondering if anyone has had any luck with this? I'm using Objective-C but a swift solution would be fine as well, thanks for the help!
Also here are the older threads that I have looked at, but haven't had any luck with:
How to find UIWebView toolbar (to replace them) on iOS 6?
Remove form assistant from keyboard in iPhone standalone web app
I've been looking at the same problem for so long I'm probably missing a simple solution here.
I created a small library to provide a custom UIView that sticks to the keyboard like the one for iMessage does (aka doesn't hide with keyboard): https://github.com/oseparovic/MessageComposerView
Basically the problem I'm experiencing is that when the user init's custom view I want a view with the following default rect initialized:
CGFloat defaultHeight = 44.0;
CGRect frame = CGRectMake(0,
[self currentScreenSize].height-defaultHeight,
[self currentScreenSize].width,
defaultHeight)
This requires that the currentScreenSize is calculated within the UIView. I've tried multiple implementations all of which have their downsides. There doesn't seems to be a good solution due to this breaking principles of MVC.
There are lots of duplicate questions on SO but most assume you have access to the rest of the code base (e.g. the app delegate) which this custom view does not so I'm looking for a self contained solution.
Here are the two leading implementations I'm using:
NextResponder
This solution seems to be fairly successful in a wide variety of scenarios. All it does is get the next responder's frame which very conveniently doesn't include the nav or status bar and can be used to position the UIView at the bottom of the screen.
The main problem is that self.nextResponder within the UIView is nil at the point of initialization, meaning it can't be used (at least not that I know) to set up the initial frame. Once the view has been initialized and added as a subview though this seems to work like a charm for various repositioning uses.
- (CGSize)currentScreenSize {
// return the screen size with respect to the orientation
return ((UIView*)self.nextResponder).frame.size;
}
ApplicationFrame
This was the solution I was using for a long time but it's far more bulky and has several problems. First of all, by using the applicationFrame you have to deal with the nav bar height as it will otherwise offset the position of your view. This means you have to determine if it is visible, get its height and subtract it from your currentSize.
Getting the nav bar unfortunately means you need to access the UINavigationController which is not nearly as simple as accessing the UIViewController. The best solution I've had so far is the below included currentNavigationBarHeight. I recently found an issue though where this will fail to get the nav bar height if a UIAlertView is present as [UIApplication sharedApplication].keyWindow.rootViewController will evaluate to _UIAlertShimPresentingViewController
- (CGSize)currentScreenSize {
// there are a few problems with this implementation. Namely nav bar height
// especially was unreliable. For example when UIAlertView height was present
// we couldn't properly determine the nav bar height. The above method appears to be
// working more consistently. If it doesn't work for you try this method below instead.
return [self currentScreenSizeInInterfaceOrientation:[self currentInterfaceOrientation]];
}
- (CGSize)currentScreenSizeInInterfaceOrientation:(UIInterfaceOrientation)orientation {
// http://stackoverflow.com/a/7905540/740474
// get the size of the application frame (screensize - status bar height)
CGSize size = [UIScreen mainScreen].applicationFrame.size;
// if the orientation at this point is landscape but it hasn't fully rotated yet use landscape size instead.
// handling differs between iOS 7 && 8 so need to check if size is properly configured or not. On
// iOS 7 height will still be greater than width in landscape without this call but on iOS 8
// it won't
if (UIInterfaceOrientationIsLandscape(orientation) && size.height > size.width) {
size = CGSizeMake(size.height, size.width);
}
// subtract the height of the navigation bar from the screen height
size.height -= [self currentNavigationBarHeight];
return size;
}
- (UIInterfaceOrientation)currentInterfaceOrientation {
// Returns the orientation of the Interface NOT the Device. The two do not happen in exact unison so
// this point is important.
return [UIApplication sharedApplication].statusBarOrientation;
}
- (CGFloat)currentNavigationBarHeight {
// TODO this will fail to get the correct height when a UIAlertView is present
id nav = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([nav isKindOfClass:[UINavigationController class]]) {
UINavigationController *navc = (UINavigationController *) nav;
if(navc.navigationBarHidden) {
return 0;
} else {
return navc.navigationBar.frame.size.height;
}
}
return 0;
}
Does anyone have suggestion about how I can best calculate the UIViewController size from within this UIView. I'm totally open to other suggestions on how to stick the UIView to the bottom of the screen upon initialization that I may have overlooked. Thank you!
+ (id) getCurrentUIViewController : (id)res {
if([res isKindOfClass:[UIViewController class]]) {
return res;
}
else if ([res isKindOfClass:[UIView class]]) {
return [Function getCurrentUIViewController:[res nextResponder]];
}
else {
return nil;
}
}
So I just installed Xcode 6GM and fiddled with my iOS7 app on simulator running iOS8.
I have a UITableView that's in editing mode and there's now a circle on the left side of the cell which doesn't appear when running on iOS7.
I glanced at the documentation for iOS8, but I don't see any new constants and I'm using UITableViewCellEditingStyleNone and UITableViewCellSelectionStyleNone.
That circle disappears when tableView.editing = NO, also allowsMultipleSelectionDuringEditing = YES.
If anyone can tell me what's going on that'd be great :)
EDIT: compiling from XCode6GM onto my iPhone running iOS7.1 gives me the circle too. I suspect a bug with XCode6GM?
Here is a screenshot with the circles:
I just had this annoying issue while migrating my app to iOS8.
Here is the workaround I found ... add something like this in your UITableViewCell subclass:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
for( UIView* subview in self.subviews )
if( [NSStringFromClass(subview.class) isEqualToString:#"UITableViewCellEditControl"] )
subview.hidden = YES;
}
I hope this will be documented / fixed soon ...
I think I have a better solution, add this code to your custom uitableviewcell:
- (void)addSubview:(UIView *)view {
[super addSubview:view];
if( [NSStringFromClass(view.class) isEqualToString:#"UITableViewCellEditControl"] ) {
view.hidden = YES
}
}
Here's a Swift solution combining the two answers:
override func addSubview(view: UIView) {
super.addSubview(view)
if view.isKindOfClass(NSClassFromString("UITableViewCellEditControl")!) {
view.hidden = true
}
}
Here is the Swift3 version:
override func addSubview(_ view: UIView) {
super.addSubview(view)
if view.classAsString() == "UITableViewCellEditControl" {
view.isHidden = true
}
}
thanks for turning up :-)
I noticed that on the iPad the Form Sheet view does not have any iOS 7 parallax effects, and I would actually like to incorporate this because I think it kinda looks cool.
Well this is what I have and it doesn't have that, so is there a way to do this? How would one go about doing it? I've googled and googled and google has finally failed me because nothing appropriate is appearing.
Thanks :)
This can be done with some effort.
First off, you need to know how to add parallax to a view in general:
I have the following category method for UIView to make it easy:
- (void)addDepthMotionX:(CGFloat)x y:(CGFloat)y {
Class clazz = NSClassFromString(#"UIInterpolatingMotionEffect");
if (clazz) {
BOOL reverse = NO;
if (CGAffineTransformEqualToTransform(self.transform, CGAffineTransformMakeRotation(-M_PI_2)) || CGAffineTransformEqualToTransform(self.transform, CGAffineTransformMakeRotation(M_PI_2))) {
reverse = YES;
}
UIInterpolatingMotionEffect *eff = [[clazz alloc] initWithKeyPath:#"center.x" type:reverse ? UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis : UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
eff.maximumRelativeValue = #(x);
eff.minimumRelativeValue = #(-x);
[self addMotionEffect:eff];
eff = [[clazz alloc] initWithKeyPath:#"center.y" type:reverse ? UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis : UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
eff.maximumRelativeValue = #(y);
eff.minimumRelativeValue = #(-y);
[self addMotionEffect:eff];
}
}
The code is guarded so it won't crash if called from other than iOS 7 (or later).
Now to add parallax to a view you simply do:
[someView addDepthMotionX:10 y:10]; // pick an appropriate depth value
The final step to your question is to apply this to the root of the view controller you are displaying.
Here's code you can add in the viewWillAppear: method of the view controller. Adjust to meet your needs:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.isBeingPresented || self.isMovingToParentViewController) {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && self.modalPresentationStyle == UIModalPresentationFormSheet) {
[self.navigationController.view.superview addDepthMotionX:15 y:15];
}
}
}
One issue with this code is if the user rotates the iPad 90 degrees after the form sheet is presented, the parallax effect needs to be updated. But this will get you started.