WKWebView not rendering correctly in iOS 10 - wkwebview

I have WKWebView inside the UITableViewCell. The web view load request and then after finished loading, I'll resize the web view height to be equal to its content height, then adjust table view cell height to fit accordingly.
What happened was the web view only displays the area that fit the screen size. When I scroll down, everything is white. But I see that the web view rendered with correct height, tap and hold on the white area on the web view still see selection. Zooming with pinch makes the web view area that displaying on the screen visible, but other areas sometimes become white.
It works fine on iOS 8 and 9. I have created a sample project to demonstrate this behaviour here:
https://github.com/pawin/strange-wkwebview
Open Radar:
https://openradar.appspot.com/radar?id=4944718286815232
Update: This issue is resolved in iOS11

You need to force the WKWebView to layout while your UITableView scrolls.
// in the UITableViewDelegate
func scrollViewDidScroll(scrollView: UIScrollView) {
if let tableView = scrollView as? UITableView {
for cell in tableView.visibleCells {
guard let cell = cell as? MyCustomCellClass else { continue }
cell.webView?.setNeedsLayout()
}
}
}

func reloadWKWebViewIfNeeded() {
for cell in self.tableView.visibleCells {
guard let webviewCell = cell as? WebviewCell else { continue }
// guard cell height > screen height
webviewCell.webview.reload()
}
}
override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard !decelerate else { return }
self.reloadWKWebViewIfNeeded()
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
self.reloadWKWebViewIfNeeded()
}
Not the best solution though, but at least user can see the rest of the content

I have the same problem - add a WKWebView to a UITableViewCell, and I solved this problem by these steps:
1.Create a UITextView instance and add it to UITableView's superview(UIViewControllew.view)
2.implement codes in scrollViewDidScroll like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self.xxtextView becomeFirstResponder];
[self.xxtextView resignFirstResponder];
}
These codes may cause increased cpu performance overhead, you can fix it by some way such us use a temp variable as threshold value.
I don't think this is a perfect solve method, but it works for me.
Eventually I realized that textview becomeFirstResponder just led the webview layout again, so you can just fix it like this:
CGFloat tempOffset = 0;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!tempOffset || ABS(scrollView.contentOffset.y - tempOffset) > ScreenHeight/2)
{
[self.wkWebView setNeedsLayout];
tempOffset = scrollView.contentOffset.y;
}
}

In objective-C this is how I solve the problem.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray * visibleCell = [self.tableView visibleCells];
for (CustomUITableViewCell * cell in visibleCell) {
if ([cell isKindOfClass:[CustomUITableViewCell class]]) {
[cell.wkWebView setNeedsLayout];
}
}
}
That code will collect all visible cell and do the setNeedsLayout in fast enumeration during user scroll.

Im also have uitableview with cell with wkWebView. And i stack with same problem. But timely you can fix this with this code. By performance do not worry. I tested this solution on iphone 5s and it was taken 10-15% CPU only when you scrolling uitableView with visible web cell.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//TODO: Remove this fix when WKWebView will fixed
if let cell = tableView.cellForRow(at: IndexPath(row: 1, section: 0)) as? WKWebViewCell {
// Here we take our cell
cell.wkWebView?.setNeedsLayout()
// here is "magic" (where wkWebView it is WKWebView, which was
// previously added on cell)
}
}

Related

Change swipe to delete button height in UITableview in Xamarin IOS

I have a table view with cells having some padding around them. I implemented swipe to delete feature and by default the delete button occupies the cell height.
I used below code for IOS 10 to align it with the visible cell height and it is screwing up the button height in a wierd way. Note : I have got it working fine in IOS 11 using a different set of code as the way to handle this is different between IOS 10 and IOS 11.
But below code for IOS 10 screws up button height. Looks like the layout subview gets called multiple times when user swipes and that is causing the button height to vary a lot. Any ideas of how to solve this.
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Convert.ToInt16(UIDevice.CurrentDevice.SystemVersion.Split('.')[0]) < 11)
{
foreach (var view in this.Subviews)
{
if (view.Class.Name.ToString() == "UITableViewCellDeleteConfirmationView")
{
CGRect newFrame = view.Frame;
newFrame.Y = newFrame.Y + 6;
newFrame.Height = newFrame.Height - 12;
view.Frame = newFrame;
}
}
}
}
Refer to this post.
The view hierarchy before iOS11 demonstrates as below
UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView -> _UITableViewCellActionButton
I see you get the UITableViewCellDeleteConfirmationView not the Button .
Modify your code :
foreach (UIView subview in this.Subviews)
{
if (subview.Class.Name.ToString() == "UITableViewCellDeleteConfirmationView")
{
foreach (var view in subview.Subviews)
{
if (view.Class.Name.ToString() == "_UITableViewCellActionButton")
{
CGRect newFrame = view.Frame;
newFrame.Y = newFrame.Y + 6;
newFrame.Height = newFrame.Height - 12;
view.Frame = newFrame;
}
}
}
}

Show UIAlertController over keyboard

In iOS 8 and lower show a UIActionSheet when keyboard is presented will present the action sheet over the keyboard. With iOS 9 this is no longer the case.
In my app we have a chat functionality and want the show a action over the keyboard. We used to use UIActionSheet which worked fine until iOS 8. In iOS 9 the action sheet is present behind the keyboard. I've tried both UIActionSheet and UIAlertController.
What we want is a action sheet like in messages.app
I've tried placing the action sheet in it own window and overriding canBecomeFirstResponder which just made the keyboard disappear.
I have implemented exactly this in our app. The trick is to have the alert controller appear on a different window. This is how the UIActionSheet implementation does it, and works great on iOS 8, but on 9, Apple has moved the keyboard implementation to a window which has a very high window level (10000000). The fix is to give your alert window an even higher window level (as a custom double value, not using the provided constants).
When using a custom window which will have transparency, make sure to read my answer here, regarding background color, to prevent window becoming black during rotation transitions.
_alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
_alertWindow.rootViewController = [UIViewController new];
_alertWindow.windowLevel = 10000001;
_alertWindow.hidden = NO;
_alertWindow.tintColor = [[UIWindow valueForKey:#"keyWindow"] tintColor];
__weak __typeof(self) weakSelf = self;
UIAlertController* alert = [UIAlertController alertControllerWithTitle:#"Test" message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:#"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
weakSelf.alertWindow.hidden = YES;
weakSelf.alertWindow = nil;
}]];
[alert addAction:[UIAlertAction actionWithTitle:#"Test" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
weakSelf.alertWindow.hidden = YES;
weakSelf.alertWindow = nil;
}]];
[_alertWindow.rootViewController presentViewController:alert animated:YES completion:nil];
The answer supplied by Leo is broken as of iOS 11, because Apple now prevents you from setting a windowLevel above 10000000. A fix is to implement a custom UIWindow and override the windowLevel receiver:
#interface TopWindow : UIWindow #end
#implementation TopWindow
- (UIWindowLevel) windowLevel {
return 20000000.000;
}
#end
// usage:
UIWindow* w = [[TopWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
w.rootViewController = [UIViewController new];
w.hidden = NO;
[w.rootViewController presentViewController:yourActionSheetController animated:YES completion:nil];
This approach should be backwards compatible, but haven't tested all known versions. Happy hacking!
Based on Leo Natan's answer, I've created a Swift extension for presenting an alert sheet over the keyboard.
In my brief testing, the alertWindow is deallocated after the alert is dismissed, I believe because there's no strong reference to it outside of the alert. This means there's no need to hide or deallocate it in your UIAlertActions.
extension UIAlertController {
func presentOverKeyboard(animated: Bool, completion: (() -> Void)?) {
let alertWindow = UIWindow(frame: UIScreen.mainScreen().bounds)
// If you need a white/hidden/other status bar, use an appropriate VC.
// You may not need a custom class, and you can just use UIViewController()
alertWindow.rootViewController = whiteStatusBarVC()
alertWindow.windowLevel = 10000001
alertWindow.hidden = false
// Set to a tint if you'd like
alertWindow.tintColor = UIColor.greenColor()
alertWindow.rootViewController?.presentViewController(self, animated: animated, completion: completion)
}
}
private class whiteStatusBarVC: UIViewController {
private override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
}
use UIAlertController instead of UIActionSheet

Cancel button in searchBar ios 8

tell me how to change the text on the Cancel button in searchBar?
Image: http://i.stack.imgur.com/8G1ZM.png
you can do that in this delegate method of UISearchBar
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
[theSearchBar setShowsCancelButton:YES animated:NO];
for (UIView *subView in theSearchBar.subviews){
if([subView isKindOfClass:[UIButton class]]){
[(UIButton*)subViewsetTitle:#"Button Title"forState:UIControlStateNormal];
}
}
}
UPDATE
after a long way searching the only way i got working in swift
is to set a custom UIBarButtonItem but you will need to show the search bar on the navigation
in ViewDidLoad()
self.searchDisplayController?.displaysSearchBarInNavigationBar = true
and in Delegate Method
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
var barButton = UIBarButtonItem(title: "Button Title", style: UIBarButtonItemStyle.Done, target: self, action: "here")
self.searchDisplayController?.navigationItem.rightBarButtonItem = barButton
}
i hope that works with you
«Another UPDATE
as you said in comments you will need to localize your application, in your case you will only need to localize the storyBoard
first navigate to your project settings then info, under localizations click the + button and add your own languages then check only StoryBoard
and now you have localized your app but you might don't see the changes until you remove the app and install it again or if the device language is set to english you will need to write 2 lines of code to change the language manually here is it
var str:NSString = "ar" // ar stands for arabic you put here you own language small character
var myArray:NSArray = [str]
NSUserDefaults.standardUserDefaults().setObject(myArray, forKey: "AppleLanguages")
and your button will looks like this
if you want to know more about localization see this Internationalization Tutorial for iOS [2014 Edition]
if you still need help till me :)
There are 2 solutions.
set key/value
[self.searchController.searchBar setValue:#"취소" forKey:#"_cancelButtonText"];
This solution works well, but you have to know that _cancelButtonText property is not public property.(You can not find this property in the documentation page.)
And It's not sure that this solution can pass the apple review process. So, use solution 2 please.
use a delegate.
You can change cancel button title(iOS8) within willPresentSearchController method.
(Assume that the searchBar is in tableView headerView)
- (void)willPresentSearchController:(UISearchController *)searchController{
// You have to set YES showsCancelButton.
// If not, you can not change your button title when this method called
// first time.
self.searchController.searchBar.showsCancelButton = YES;
UIView* view=self.searchController.searchBar.subviews[0];
for (UIView *subView in view.subviews) {
if ([subView isKindOfClass:[UIButton class]]) {
// solution 1
UIButton *cancelButton = (UIButton*)subView;
[cancelButton setTitle:NSLocalizedString(#"취소", nil) forState:UIControlStateNormal];
}
}
}
Also, You can get infomation about the searchBar through a view debugging tools in Xcode. Set a breakPoint at the willPresentSearchController method.
Good luck.
Actually, the cancel button is not a top view in the Search bar. You should search for it recursively.
-(UIButton*)findButtonInView:(UIView*)v{
for (UIView *subView in v.subviews){
if([subView isKindOfClass:[UIButton class]]){
return (UIButton*)subView;
}else if([subView isKindOfClass:[UIView class]]){
UIButton* btn = [self findButtonInView:subView];
if(btn){
return btn;
}
}
}
return nil;
}
//...
UIButton* searchButton = [self findButtonInView:self.searchBar];

How to keep a drag able object inside the frame of a view

I have a button that can be dragged around on the screen. I was wondering if there is a way to keep the button inside the frame of the view.
I have used this code to make the button drag able:
UIPanGestureRecognizer *buttonPanRecognizer;
buttonPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(setObjectLocation:)];
[NewButton addGestureRecognizer:buttonPanRecognizer];
- (void)setObjectLocation:(UIPanGestureRecognizer *)recognizer {
CGPoint location = [recognizer locationInView:self.view];
if (CGRectContainsPoint([NewButton frame], location)) { // NewButton
NewButton.center = location;
}
else if (CGRectContainsPoint([NewLabel frame], location)) { // NewLabel
NewLabel.center = location;
} }
I also want to be able to keep other kinds of objects inside.
Thansk in advance :)
The problem is that it is possible to drag parts of the UIButton outside the screen.
I have to agree that your problem is not quite clear. What is all the stuff with the label supposed to to. Generally, to keep a button inside a frame, you have everything you need already there:
UIPanGestureRecognizer* buttonPanRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(setObjectLocation:)];
[NewButton addGestureRecognizer:buttonPanRecognizer];
- (void)setObjectLocation:(UIPanGestureRecognizer *)recognizer {
CGPoint location = [recognizer translationInView:self.view];
if (CGRectContainsPoint(self.view.frame, location)) {
NewButton.center = location;
}
}
What's the problem? Just keep the button's origin within a rect (frame.x,frame.y,frame.width - button.width,frame.height - button.height).

UISearchBar disable auto disable of cancel button

I have implemented a UISearchBar into a table view and almost everything is working except one small thing: When I enter text and then press the search button on the keyboard, the keyboard goes away, the search results are the only items shown in the table, the text stays in the UISearchBar, but the cancel button gets disabled.
I have been trying to get my list as close to the functionality of the Apple contacts app and when you press search in that app, it doesn't disable the cancel button.
When I looked in the UISearchBar header file, I noticed a flag for autoDisableCancelButton under the _searchBarFlags struct but it is private.
Is there something that I am missing when I setup the UISearchBar?
I found a solution. You can use this for-loop to loop over the subviews of the search bar and enable it when the search button is pressed on the keyboard.
for (UIView *possibleButton in searchBar.subviews)
{
if ([possibleButton isKindOfClass:[UIButton class]])
{
UIButton *cancelButton = (UIButton*)possibleButton;
cancelButton.enabled = YES;
break;
}
}
I had to tweak this a bit to get it work for me in iOS7
- (void)enableCancelButton:(UISearchBar *)searchBar
{
for (UIView *view in searchBar.subviews)
{
for (id subview in view.subviews)
{
if ( [subview isKindOfClass:[UIButton class]] )
{
[subview setEnabled:YES];
NSLog(#"enableCancelButton");
return;
}
}
}
}
There is two way to achieve this easily
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{
// The small and dirty
[(UIButton*)[searchBar valueForKey:#"_cancelButton"] setEnabled:YES];
// The long and safe
UIButton *cancelButton = [searchBar valueForKey:#"_cancelButton"];
if ([cancelButton respondsToSelector:#selector(setEnabled:)]) {
cancelButton.enabled = YES;
}
}
You should go with the second one, it will not crash your application if Apple will change it in the background.
BTW i tested it from iOS 4.0 to 8.2 and no changes, also i use it in my Store approved application without any issues.
This is what made it to work on iOS 6 for me:
searchBar.showsCancelButton = YES;
searchBar.showsScopeBar = YES;
[searchBar sizeToFit];
[searchBar setShowsCancelButton:YES animated:YES];
Here's my solution, which works for all situations in all versions of iOS.
IE, other solutions don't handle when the keyboard gets dismissed because the user dragged a scroll view.
- (void)enableCancelButton:(UIView *)view {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton *)view setEnabled:YES];
} else {
for (UIView *subview in view.subviews) {
[self enableCancelButton:subview];
}
}
}
// This will handle whenever the text field is resigned non-programatically
// (IE, because it's set to resign when the scroll view is dragged in your storyboard.)
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar {
[self performSelector:#selector(enableCancelButton:) withObject:searchBar afterDelay:0.001];
}
// Also follow up every [searchBar resignFirstResponder];
// with [self enableCancelButton:searchBar];
As per my answer here, place this in your searchBar delegate:
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
dispatch_async(dispatch_get_main_queue(), ^{
__block __weak void (^weakEnsureCancelButtonRemainsEnabled)(UIView *);
void (^ensureCancelButtonRemainsEnabled)(UIView *);
weakEnsureCancelButtonRemainsEnabled = ensureCancelButtonRemainsEnabled = ^(UIView *view) {
for (UIView *subview in view.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
[(UIControl *)subview setEnabled:YES];
}
weakEnsureCancelButtonRemainsEnabled(subview);
}
};
ensureCancelButtonRemainsEnabled(searchBar);
});
}
None of the answers worked for me at all. I'm targeting iOS 7. But I found an answer.
What I'm trying is something like the Twitter iOS app. If you click on the magnifying glass in the Timelines tab, the UISearchBar appears with the Cancel button activated, the keyboard showing, and the recent searches screen. Scroll the recent searches screen and it hides the keyboard but it keeps the Cancel button activated.
This is my working code:
UIView *searchBarSubview = self.searchBar.subviews[0];
NSArray *subviewCache = [searchBarSubview valueForKeyPath:#"subviewCache"];
if ([subviewCache[2] respondsToSelector:#selector(setEnabled:)]) {
[subviewCache[2] setValue:#YES forKeyPath:#"enabled"];
}
I arrived at this solution by setting a breakpoint at my table view's scrollViewWillBeginDragging:. I looked into my UISearchBar and bared its subviews. It always has just one, which is of type UIView (my variable searchBarSubview).
Then, that UIView holds an NSArray called subviewCache and I noticed that the last element, which is the third, is of type UINavigationButton, not in the public API. So I set out to use key-value coding instead. I checked if the UINavigationButton responds to setEnabled:, and luckily, it does. So I set the property to #YES. Turns out that that UINavigationButton is the Cancel button.
This is bound to break if Apple decides to change the implementation of a UISearchBar's innards, but what the hell. It works for now.
Here's a Swift 3 solution that makes use of extensions to get the cancel button easily:
extension UISearchBar {
var cancelButton: UIButton? {
for subView1 in subviews {
for subView2 in subView1.subviews {
if let cancelButton = subView2 as? UIButton {
return cancelButton
}
}
}
return nil
}
}
Now for the usage:
class MyTableViewController : UITableViewController, UISearchBarDelegate {
var searchBar = UISearchBar()
func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
tableView.tableHeaderView = searchBar
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
if let cancelButton = searchBar.cancelButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
}
}
}
}
For Monotouch or Xamarin iOS I have the following C# solution working for iOS 7 and iOS 8:
foreach(UIView view in searchBar.Subviews)
{
foreach(var subview in view.Subviews)
{
//Console.WriteLine(subview.GetType());
if(subview.GetType() == typeof(UIButton))
{
if(subview.RespondsToSelector(new Selector("setEnabled:")))
{
UIButton cancelButton = (UIButton)subview;
cancelButton.Enabled = true;
Console.WriteLine("enabledCancelButton");
return;
}
}
}
}
This answer is based on David Douglas solution.
A more complete answer:
since iOS 7, there is an additional level of subviews under the searchBar
a good place to enable the cancel button is in searchBarTextDidEndEditing
.
extension MyController: UISearchBarDelegate {
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
// you need that since the disabling will
// happen after searchBarTextDidEndEditing is called
searchBar.subviews.forEach({ view in
view.subviews.forEach({ subview in
// ios 7+
if let cancelButton = subview as? UIButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
return
}
})
// ios 7-
if let cancelButton = subview as? UIButton {
cancelButton.isEnabled = true
cancelButton.isUserInteractionEnabled = true
return
}
})
}
}
}
Time passes, but the problem is still there...
Elegant Swift 5/iOS 13 solution:
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
for case let cancelButton as UIButton in searchBar.subviews {
cancelButton.isEnabled = true
}
}

Resources