For UIWebView, we can get content height by this:
[webView stringByEvaluatingJavaScriptFromString:#"document.body.offsetHeight"]
But WKWebView doesn't have this method and webView.scrollView.contentSize.height is not right.
(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
}
Thank you for your help!
I solved this problem with KVO.
First, addObserver for WKWebView's scrollView's contentSize like this:
addObserver(self, forKeyPath: "webView.scrollView.contentSize", options: .New, context: nil)
And next, receive the change like this:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "webView.scrollView.contentSize" {
if let nsSize = change[NSKeyValueChangeNewKey] as? NSValue {
let height = nsSize.CGSizeValue().height
// Do something here using content height.
}
}
}
It's easy to get webview's content height.
Don't forget to remove observer:
removeObserver(self, forKeyPath: "webView.scrollView.contentSize", context: nil)
I worte this line in deinit.
EDIT:
To get the best possible height calculation, in the end I did a few things to get the best height:
I set the size of the webview to be really large and then load the content.
Once the content is loaded (KVO notification) I resize the view using the method below.
I then run the size function again and I get a new size and resize my view once again.
The above got the most accurate results for me.
ORIGINAL:
I've tried the scroll view KVO and I've tried evaluating javascript on the document, using clientHeight, offsetHeight, etc...
What worked for me eventually is: document.body.scrollHeight. Or use the scrollHeight of your top most element, e.g. a container div.
I listen to the loading WKWebview property changes using KVO:
[webview addObserver: self forKeyPath: NSStringFromSelector(#selector(loading)) options: NSKeyValueObservingOptionNew context: nil];
And then:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if(object == self.webview && [keyPath isEqualToString: NSStringFromSelector(#selector(loading))]) {
NSNumber *newValue = change[NSKeyValueChangeNewKey];
if(![newValue boolValue]) {
[self updateWebviewFrame];
}
}
}
The updateWebviewFrame implementation:
[self.webview evaluateJavaScript: #"document.body.scrollHeight" completionHandler: ^(id response, NSError *error) {
CGRect frame = self.webview.frame;
frame.size.height = [response floatValue];
self.webview.frame = frame;
}];
You can get it like this:
self.webview.navigationDelegate = self;
- (void)webView:(WKWebView *)webview didFinishNavigation:(WKNavigation *)navigation{
// webview.scrollView.contentSize will equal {0,0} at this point so wait
[self checkIfWKWebViewReallyDidFinishLoading];
}
- (void)checkIfWKWebViewReallyDidFinishLoading{
_contentSize = _webview.scrollView.contentSize;
if (_contentSize.height == 0){
[self performSelector:#selector(WKWebViewDidFinishLoading) withObject:nil afterDelay:0.01];
}
}
WKWebView didn't finish loading, when didFinishNavigation is called - Bug in WKWebView?
WKWebView doesn't use delegation to let you know when content loading is complete.
You can create a new thread to check the status of WKWebView.
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), {
while(self.webView?.loading == true){
sleep(1)
}
dispatch_async(dispatch_get_main_queue(), {
print(self.webView!.scrollView.contentSize.height)
})
})
}
Related
I want to add another menu option to the default image attachment menu options (Copy Image, Save to Camera Roll). Note that these options are shown when you long press on an image embedded in the UITextView if the textView is not in editing mode.
I have tried adding a custom menu to the uimenucontroller and using -(void)canPerformAction to enable or disable the option, however this seems to add the menu item to the uitextView's edit menu and has no affect on the attachments popup menu.
-(void)canPerformAction never seems to get called when long pressing on the image attachment.
Well according to Apple there is no public API for doing this, however as it turns out its relatively straight forward to replace the default menu with one that looks and behaves the same.
In the viewController that contains the UITextView add the following or similar and set it up as the textView's delegate.
- (BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange {
// save in ivar so we can access once action sheet option is selected
_attachment = textAttachment;
[self attachmentActionSheet:(UITextView *)textView range:characterRange];
return NO;
}
- (void)attachmentActionSheet:(UITextView *)textView range:(NSRange)range {
// get the rect for the selected attachment (if its a big image with top not visible the action sheet
// will be positioned above the top limit of the UITextView
// Need to add code to adjust for this.
CGRect attachmentRect = [self frameOfTextRange:range inTextView:textView];
_attachmentMenuSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:#"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:#"Copy Image", #"Save to Camera Roll", #"Open in Viewer", nil];
// Show the sheet
[_attachmentMenuSheet showFromRect:attachmentRect inView:textView animated:YES];
}
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
CGRect rect = [textView.layoutManager boundingRectForGlyphRange:range inTextContainer:textView.textContainer];
// Now convert to textView coordinates
CGRect rectRange = [textView convertRect:rect fromView:textView.textInputView];
// Now convert to contentView coordinates
CGRect rectRangeSuper = [self.contentView convertRect:rectRange fromView:textView];
// Get the textView frame
CGRect rectView = textView.frame;
// Find the intersection of the two (in the same coordinate space)
CGRect rectIntersect = CGRectIntersection(rectRangeSuper, rectView);
// If no intersection then that's weird !!
if (CGRectIsNull(rectIntersect)) {
return rectRange;
}
// Now convert the intersection rect back to textView coordinates
CGRect rectRangeInt = [textView convertRect:rectIntersect fromView:self.contentView];
return rectRangeInt;
}
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (actionSheet == _attachmentMenuSheet) {
switch (buttonIndex) {
case 0:
[self copyImageToPasteBoard:[_attachment image]];
break;
case 1:
[self saveToCameraRoll:[_attachment image]];
break;
case 2:
[self browseImage:[_attachment image]];
break;
default:
break;
}
}
}
- (void)saveToCameraRoll:(UIImage*)image {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}
- (void)copyImageToPasteBoard:(UIImage*)image {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
NSData *data = UIImagePNGRepresentation(image);
[pasteboard setData:data forPasteboardType:#"public.png"];
}
-(void)browseImage:(UIImage*)image
{
OSImageViewController *_imageViewerController = [[OSImageViewController alloc] init];
UIImage *img = [[UIImage alloc] initWithData:UIImagePNGRepresentation(image)];
_imageViewerController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
_imageViewerController.modalPresentationStyle = UIModalPresentationFullScreen;
_imageViewerController.delegate = self;
[self presentViewController:_imageViewerController animated:YES completion:^(void){
[_imageViewerController setImage:img];
}];
}
I have a requirement were in i have to create a view to display webpages with back, forward and refresh button in iOS app.
how to implement this functionality?
Use UIWebView.
Set the delegate to the view controller.
self.webView.delegate = self;
Other remarkable properties,
self.webView.scalesPageToFit = YES;
To load a URLRequest,
[self.webView loadRequest:theRequest];
or for only strings, use
[self.webView loadHTMLString:htmlString baseURL:nil];
And some delegates here. web view has its own history, you can use it by calling its back and forward methods.
#pragma mark - UIWebView delegate
// You can handle your own url scheme or let the web view handle it.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"url=%#, %#, %#",
request.URL, request.URL.query, request.URL.host);
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
if ([request.URL.scheme compare:#"customescheme"] == NSOrderedSame) {
if ([request.URL.host compare:kSomethingDotCom] == NSOrderedSame) {
[self mymethod];
} else if ([request.URL.host compare:kAnotherDotCom] == NSOrderedSame) {
[self method2];
} else {
NSLog(#"Unsupported service.");
}
return NO;
}
}
return YES;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[_activityIndicator stopAnimating];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
[_activityIndicator stopAnimating];
[Resources showAlert:#"Could not load." withTitle:#"Error!"];
NSLog(#"%#", [error localizedDescription]);
}
#pragma mark - Actions
- (IBAction)didPressBackButton:(id)sender
{
[_webView goBack];
}
- (IBAction)didPressForwardButton:(id)sender
{
[_webView goForward];
}
Similarly you can have the stop method. To refresh reload the request again. Before going back or forward you can check the methods canGoBack or canGoForward.
See docs at, http://developer.apple.com/library/ios/#documentation/uikit/reference/UIWebView_Class/Reference/Reference.html
It is a piece of cake, all you need is UIWebview component and its delegates and you need to create a custom navigation bar that is it.
I try to open wiki mobile version webpage by a UIWebView within a UIPopoverController. the problem is, not matter how I set my contentSizeForViewInPopover, or just UIWebView frame, or simply set UIWebView.scalesPageToFit = YES. the Wiki mobile version page content size seem to larger than my UIWebView. But if I use it on iPhone, there's no such problem. here's my code for popover controller:
//create a UIWebView UIViewController first
WikiViewController *addView = [[WikiViewController alloc] init];
addView.contentSizeForViewInPopover = CGSizeMake(320.0, 480.0f);
//then create my UIPopoverController
popover = [[UIPopoverController alloc] initWithContentViewController:addView];
popover.delegate = self;
[addView release];
//then get the popover rect
CGPoint pointforPop = [self.mapView convertCoordinate:selectAnnotationCord
toPointToView:self.mapView];
CGRect askRect = CGRectMake((int)pointforPop.x, (int)pointforPop.y+10, 1.0, 1.0);
[popover presentPopoverFromRect:askRect
inView:self.mapView
permittedArrowDirections:UIPopoverArrowDirectionRight animated:YES];
[self.mapView deselectAnnotation:annotation animated:YES];
and this is my code on creating UIWebView:
- (void)viewDidLoad
{
wikiWebView = [[UIWebView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 480.0f)];
wikiWebView.scalesPageToFit = YES;
//or No, doesn't matter, it all get larger than this
wikiWebView.delegate = self;
self.view = wikiWebView;
}
all code seem to be typical...
I wonder if anyone can shed me some light, thank you so much.
This is an enhanced version of auco answer, where if the viewport meta tag is not present it will be added:
- (void)webViewDidFinishLoad:(UIWebView*)webView
{
int webviewWidth = (NSUInteger)webView.frame.size.width;
if (!webView.loading) {
NSString *jsCmd = [NSString stringWithFormat:#"try {var viewport = document.querySelector('meta[name=viewport]');if (viewport != null) {viewport.setAttribute('content','width=%ipx, initial-scale=1.0, user-scalable=1');} else {var viewPortTag=document.createElement('meta');viewPortTag.id='viewport';viewPortTag.name = 'viewport';viewPortTag.content = 'width=%ipx, initial-scale=1.0, user-scalable=1';document.getElementsByTagName('head')[0].appendChild(viewPortTag);}} catch (e) {/*alert(e);*/}", webviewWidth, webviewWidth];
[webView stringByEvaluatingJavaScriptFromString:jsCmd];
}
}
Here is the Javascript pretty formatted code we are injecting in the WebView with a width of 320px
try {
var viewport = document.querySelector('meta[name=viewport]');
if (viewport != null) {
viewport.setAttribute('content',
'width=320px, initial-scale=1.0, user-scalable=1');
} else {
var viewPortTag = document.createElement('meta');
viewPortTag.id = 'viewport';
viewPortTag.name = 'viewport';
viewPortTag.content = 'width=320px,initial-scale=1.0, user-scalable=1';
document.getElementsByTagName('head')[0].appendChild(viewPortTag);
}
} catch (e) {
/*alert(e);*/
}
you can remove the try/catch if you want.
oh, i found in another QA that sometimes if html got a line "width=device-width", and you load a webview from popover controller, this popover controller will automatically send out device-width, not the view width you specified, and make your view ugly and funky. in that post it is a jQuery issue, and it solved with a jQuery way. In my problem, it is just a html issue in wiki mobile version. so I try another way, but similar.
I simple add a code in webViewdidload delegate method, first get URL html into a NSString, then use NSString instance method to search for "device-width" in loaded html, and replace it with my view width to make it a new NSString, then load this page with this new NSString. that's it.
- (void) webViewDidFinishLoad:(UIWebView *)webView
{
if (!alreadyReload)
{
NSString *webHTML = [NSString stringWithContentsOfURL:webView.request.URL encoding:NSUTF8StringEncoding error:NULL];
NSRange range = [webHTML rangeOfString:#"device-width"];
if ((range.location!=NSNotFound)&&(range.length != 0))
{
webHTML = [webHTML stringByReplacingOccurrencesOfString:#"device-width" withString:#"whatever width you need" options:0 range:range];
[webView loadHTMLString:webHTML baseURL:wikiWebView.request.URL];
alreadyReload = YES;
}
}
}
something like this.
by the way, since I only use this on wiki mobile version, the html is simple and this kind of compare and replace is pretty easy. if you wanna use it in a more general case, you might use other way.
It would be much more efficient to manipulate the device-width via JavaScript rather than altering the html after it has fully loaded and then reloading the full page with modified html again.
This should work (and also consider if it's even necessary to change the viewport width):
- (void)webViewDidFinishLoad:(UIWebView *)aWebView {
if(aWebView.frame.size.width < aWebView.window.frame.size.width) {
// width=device-width results in a wrong viewport dimension for webpages displayed in a popover
NSString *jsCmd = #"var viewport = document.querySelector('meta[name=viewport]');";
jsCmd = [jsCmd stringByAppendingFormat:#"viewport.setAttribute('content', 'width=%i, initial-scale=1.0, user-scalable=1');", (NSUInteger)aWebView.frame.size.width];
[aWebView stringByEvaluatingJavaScriptFromString:jsCmd];
}
// stop network indicator
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
I have the following code but can't compile it because I have a "Type name requires a specifier or qualifier" error" for (self).
How to fix this error? I have compared it with the original code and there are no differences, so I don't know what's going on.
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
Here is the code from the mirror file HynososViewController.h, and which I cut, pasted and modified:
#import "HypnososViewController.h"
#implementation HypnososViewController
- (id) init
{
// Call the superclass's designated initializer
self = [super initWithNibName:nil
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Hypnosis"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for HypnosisViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor orangeColor]];
}
#end
Here is the complete code for CurrentTimeViewController.h:
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)Bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for CurrentTimeViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor greenColor]];
}
#end
Is the code above a cut-and-paste of EXACTLY what you are trying to compile? If so, I think you are missing something very important that would make that code block a method implementation:
-(id)init // This, or something like it, is missing???
{
...
}
Check your code, here, and in your project. :-)
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
}
}