Briefly, my problem is: some PDFs loaded into some web views don't show up, while others do, as shown in the following pictures (please note that I edited the images placing fake pdf content, the original cannot be published).
Three PDFs loaded correctly into three UIWebViews
Second UIWebView height is right and comes from the PDF height, but its content is not rendered
My app has to show very short PDFs into a series of web views (assume that this is a mandatory requirement and that I cannot switch to anything else. The PDFs are LaTex generated and contain text which cannot be displayed into UILabel and so on. Also, they are about 4-5 rows length).
The layout is as following: there is a vertical scroll view which has a view (content view) as direct child. A single UIWebView (yellow background) is embedded inside a custom class that inherits from UIView (PDFView, blue background), so I can assign that class as the webview's delegate and perform the webview initialization+constraints settings in just one place. The webview userInteraction is disabled. Into the content view, I dynamically add one PDFView per PDF, setting up the constraints and calling loadData:MIMEType:textEncodingName:baseURL: to load the PDF (which comes as NSData from a file on disk, the white frame). Since PDFView is the delegate of its UIWebView, when it finishes to load its content (the pdf), I change the webview height constraint to match the webview.scrollview.contentsize.height. This allows me to resize the webview so that it displays the entire PDF. Autolayout takes care of setting the scrollview contentsize.height so the user can scroll the entire list of pdf.
Everything seems to work fine but sometimes, on some webviews, the content is not rendered. When the call for webViewDidFinishLoad: triggers, the webView.scrollView.contentSize.height value is correct (in fact, the webview has the right height) and after autolayout finishes his work, the frame of the webview whose content has not been rendered has the correct dimensions. This leads me to think that it must be some sort of rendering problem, since it seems to happen randomly on different webviews and on different pdfs. Sometimes I have to manually push and pop the controller like 10 times before the problem arises, sometimes it appears just on the first push. Memory consumption seems to be fine and there aren't any memory warnings.
I checked every data and view: no one is nil. Also the delegate webView:didFailLoadWithError: is never invoked so the webview always succeeds in loading its content (webViewDidFinishLoad: is called every time, even for the webviews that don't display any content). The PDF data is fine since I loaded it into memory, saved again in another pdf file and opened with a PDF viewer. I'm not using storyboards but xibs. I'm targeting iOS 7+.
Code is as following:
PDFView.h
#interface PDFView : UIView
-(instancetype) initWithData:(NSData *)pdfData at:(NSInteger)position;
#end
PDFView.m
#import "PDFView.h"
#interface PDFView() <UIWebViewDelegate>
#property (strong, nonatomic) UIWebView * contentUIWV;
#property (strong, nonatomic) NSLayoutConstraint * contentHeightNSLC;
#end
#implementation PDFView
-(instancetype) initWithData:(NSData *)pdfData at:(NSInteger)position
{
self = [super init];
if (self)
{
[self initViews];
[self setConstraints];
[self fillViews:pdfData];
self.tag = position;
}
return self;
}
-(void) initViews
{
_contentUIWV = [[UIWebView alloc] init];
[_contentUIWV setContentHuggingPriority:250 forAxis:UILayoutConstraintAxisVertical];
_contentUIWV.userInteractionEnabled = NO;
_contentUIWV.delegate = self;
_contentUIWV.scalesPageToFit = YES;
_contentUIWV.backgroundColor = [UIColor yellowColor];
[self addSubview:_contentUIWV];
}
-(void) setConstraints
{
_contentUIWV.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary * viewsNSD = #{ #"contentUIWV":_contentUIWV };
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-8-[contentUIWV]-8-|" options:0 metrics:nil views:viewsNSD]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-8-[contentUIWV]-8-|" options:0 metrics:nil views:viewsNSD]];
NSArray * constraintsNSA = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[contentUIWV(==1)]" options:0 metrics:nil views:viewsNSD];
self.contentHeightNSLC = constraintsNSA.firstObject;
[self.contentUIWV addConstraints:constraintsNSA];
}
-(void) fillViews:(NSData *)pdfData
{
#ifdef DEBUG
NSLog(#"PDFView::fillViews: %ld %#", (long)self.tag, pdfData?#"NOT null":#"NULL");
#endif
[_contentUIWV loadData:pdfData MIMEType:#"application/pdf" textEncodingName:#"utf-8" baseURL:nil];
}
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
return YES;
}
-(void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
#ifdef DEBUG
NSLog(#"PDFView::webView:didFailLoadWithError: %#", error.localizedDescription);
#endif
}
-(void) webViewDidFinishLoad:(UIWebView *)webView
{
#ifdef DEBUG
NSLog(#"PDFView::webViewDidFinishLoad: %ld new height (%ld)", (long)self.tag, (long)webView.scrollView.contentSize.height);
#endif
self.contentHeightNSLC.constant = webView.scrollView.contentSize.height;
}
#end
Controller.m
#import "Controller.h"
#import "PDFView.h"
#interface Controller ()
// This Mutable Array stores the NSData of each PDF (PDF are loaded form disk and stored as NSData)
#property (strong, nonatomic) NSMutableArray * exerciseDataNSMA;
#property (weak, nonatomic) IBOutlet UIView *mainUIV;
#property (weak, nonatomic) IBOutlet UIScrollView *scrollUISV;
#property (weak, nonatomic) IBOutlet UIView *contentUIV;
#end
#implementation Controller
- (void)viewDidLoad
{
[super viewDidLoad];
// ... omitted code inits self.exerciseDataNSMA and stores pdf content in it
[self loadContentView];
}
#pragma mark - Private Methods
-(void) loadContentView
{
// Subviews by name (used for constraint bindings)
NSMutableDictionary * subviews = [[NSMutableDictionary alloc] init];
// String in Visual Language Format to create vertical scrollview's contentview constraint
NSMutableString *strVerticalConstraint = [[NSMutableString alloc] initWithString:#"V:|"];
for (NSInteger i=0; i<self.exerciseDataNSMA.count; i++)
{
PDFView * pdfView = [[PDFView alloc] initWithData:self.exerciseDataNSMA[i] at:i];
pdfView.delegate = self;
pdfView.backgroundColor = [UIColor blueColor];
pdfView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentUIV addSubview:pdfView];
[self.contentUIV addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[pdfView]|" options:0 metrics:nil views:#{#"pdfView":pdfView}]];
NSString * exerciseUIVName = [NSString stringWithFormat:#"pdfView%ld", (long)i];
[strVerticalConstraint appendString:[NSString stringWithFormat:#"[%#]-8-", exerciseUIVName]];
subviews[exerciseUIVName] = pdfView;
}
[strVerticalConstraint appendString:#"|"];
// Add content view constraints
[self.contentUIV addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:strVerticalConstraint options:0 metrics:nil views:subviews]];
}
#end
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Without the cache disabled, the webviews do not resize their height
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"WebKitDiskImageCacheEnabled"];
[[NSUserDefaults standardUserDefaults] synchronize];
// ... unrelated stuff
return YES;
}
Constraints on Controller are as following:
Note that the height constraint is removed at runtime.
Any idea why this happens? Any idea on how to solve it?
Here what I already tried:
Reproduced the problem on both iOS 7 and iOS8, on iPhone and on iPad. The number of webviews whose content is not rendered is greater on low specs devices (like iPhone 4s) than those with better resources (as the iPad 4);
Executing loadContentView without loading pdfData into the webview. After the controller has appeared, load the pdfData into the webviews;
Load the pdfdata sequentially, meaning that i do the same as B) and then I call loadData:MIMEType:textEncodingName:baseURL: for the first webview, wait for webViewDidFinishLoad: and then repeat this sequence for the second webview and so on;
Force every webview to reload its content (since I don't have a way to detect a webview that doesn't render its content. This results in some webviews rendering their content again, some others not);
Use javascript to resize the webview (it didn't work at all);
Related
I have a UIViewController with many subviews like UILabels, UIImages and a UIWebview. With a defined action by the user, the subviews of the UIViewController animate to different sizes and different locations inside of the UIViewController's view. Is it possible that this can be undone with a different defined action by the user? I want to make all the subviews revert back to their previous locations and sizes that they were before the animation was run. I thought of two possible solutions:
Get the properties of the subviews with the view.subviews() method before the animation is run, and then set the subviews after the animation to the properties in this array, or,
Call a method on the UIViewController to tell it to redraw all the subviews according to the properties set in the storyboard file.
Are these the right way of accomplishing what I would like to do? And if so, how would I go about doing this? (I don't know how to programmatically implement either of my ideas.)
Any help is greatly appreciated. Thanks.
Here is the solution.
#interface ViewController ()
#property (strong, nonatomic) NSMutableArray *frames;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Saving initial frames of all subviews
self.frames = [NSMutableArray new];
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
CGRect frame = view.frame;
NSValue *frameValue = [NSValue valueWithCGRect:frame];
[self.frames addObject:frameValue];
}
}
- (NSMutableArray *)allViewsOfView:(UIView *)view
{
NSMutableArray *result = [NSMutableArray new];
[result addObject:view];
for (UIView *subView in view.subviews) {
[result addObjectsFromArray:[self allViewsOfView:subView]];
}
return result;
}
- (void)resetFrames
{
NSArray *allViews = [self allViewsOfView:self.view];
for (UIView *view in allViews) {
NSValue *frameValue = [self.frames objectAtIndex:[allViews indexOfObject:view]];
CGRect frame = [frameValue CGRectValue];
view.frame = frame;
}
}
#end
Call [self resetFrame]; whenever you want to revert view's frames back to their initial values.
You could cache all your subview's frame before changing it and running the animation, in this way you can even cache more than one action. A stack structure will be perfect for this, but there is no way to achieve this in interface builder, you have to reference outlets from IB to code to get their frame.
In my app, I am trying to make a splash image appear as my UIWebView loads so it is not just a blank screen. However my webViewDidFinishLoad method will not work. This means that the splash image appears but does not disappear from the screen once the UIWebView has loaded.
My code for the method is:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"content loading finished");
[loadingImageView removeFromSuperview];
}
Any help on why the method will not work would be appreciated greatly.
My .h:
#interface ViewController : UIViewController
-(IBAction)makePhoneCall:(id)sender;
#property (nonatomic, strong) IBOutlet UIWebView *webView;
#property(nonatomic, strong) UIImageView *loadingImageView;
#end
My ViewDidLoad and webViewDidFinishLoading:
- (void)viewDidLoad {
UIWebView *mWebView = [[UIWebView alloc] init];
mWebView.delegate = self;
mWebView.scalesPageToFit = YES;
[super viewDidLoad];
}
//**************** Set website URL for UIWebView
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.sleafordpizza.com/food"]]];
//**************** Add Static loading image to prevent white "flash" ****************/
UIImage *loadingImage = [UIImage imageNamed:#"LittleItalyLogo.png"];
loadingImageView = [[UIImageView alloc] initWithImage:loadingImage];
loadingImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"LittleItalyLogo.png"],
nil];
[self.view addSubview:loadingImageView];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"content loading finished");
// Remove loading image from view
[loadingImageView removeFromSuperview];
}
Hi probably you do not set proper delegate.
This is small code tip for you.
-(void)viewDidLoad {
mWebView = [[UIWebView alloc] init];
mWebView.delegate = self;
mWebView.scalesPageToFit = YES;
}
-(void)webViewDidFinishLoad:(UIWebView *)webView {
[loadingImageView removeFromSuperview];
NSLog(#"finish");
}
In you're .h file add.
#interface MyView: UIViewController <UIWebViewDelegate> {
UIWebView *webView;
}
Code fixes.
For .h file
#interface ViewController : UIViewController<UIWebViewDelegate>
-(IBAction)makePhoneCall:(id)sender;
#property (nonatomic, strong) IBOutlet UIWebView *webView;
#property(nonatomic, strong) UIImageView *loadingImageView;
#end
For .m file
- (void)viewDidLoad
{
[super viewDidLoad];
webView.delegate = self;
//**************** Set website URL for UIWebView
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.sleafordpizza.com/food"]]];
//**************** Add Static loading image to prevent white "flash" ****************/
UIImage *loadingImage = [UIImage imageNamed:#"LittleItalyLogo.png"];
loadingImageView = [[UIImageView alloc] initWithImage:loadingImage];
loadingImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"LittleItalyLogo.png"],
nil];
[self.view addSubview:loadingImageView];
}
At certain times, this delegate method actually never gets fired. I have had severe problems with the same thing in some of my projects.
At one occasion, I actually had to solve it with a timer, checking the state of the web view every second or so to see if I could proceed.
In that particular case, I just needed a certain element to be present. Still, the view did not trigger the finish loading event, due to external script errors being injected.
So, I just started a trigger when the web view begun loading, then called a method every now and then to see if the web view contained the element in question.
- (void)methodCalledByTimer {
if (<I still do not have what I need>) {
//The web view has not yet finished loading; keep checking
} else {
//The web view has finished loading; stop the timer, hide spinners and proceed
}
}
You could also check if the web view is actually loading, if that is absolutely necessary:
- (void)methodCalledByTimer {
if (self.webView.isLoading) {
//The web view has not yet finished loading; keep checking
} else {
//The web view has finished loading; stop the timer, hide spinners and proceed
}
}
Then, naturally, I'd check for the finishedLoading event as well, just to be sure. Remember to also implement the webView:didFailLoadWithError: method as well.
When waiting for a web page to finish loading, there are some things to keep in mind.
For instance, do you really need it to stop loading, or is there anything else you can do? In my case, I needed an element. Being able to properly execute a script is another thing that may be required.
Second, is the loading page using any external resources? I once had external script errors causing the webViewDidFinishLoad: method to not being called at all. If I removed the external scripts, it worked.
Third, if the page is using external resources, you are exposed not only to the loading capacity of your own resources, but that of the external resources as well. Tracking scripts, ads etc...if one resource provider is delivering content sloooowly (or not at all), you could page could be stuck in loading state forever.
So, I'd go with checking for something else. :)
I see you aren't handling errors. If there is an error, all subsequent delegate calls will not happen. I was surprised to find that this is true when the webview uses a plugin too. It calls this error method telling you that the webview handed off to the delegate, in my case the movie player.
implement this and see if that is it.
-(void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (error.code == 204) {
//request was handled by a plugin instead of by the webview directly
...
}
else
{
NSLog(#"didFailLoadWithError. ERROR: %#", error);
}
}
I was able to do all the remaining loading work in this method instead of the webviewdidfinishLoad
I have a screen, which contains multiple UIImages (their amount and size are known only at runtime, so i add them programmatically) and some fixed buttons below these UIImages.
How to make buttons display certainly under all Images?
I've tried
1.) Put Buttons and Images into 2 separate views, and then add constraint between them. No result, buttons are hidden behind images.
2.) Put buttons into separate view and set constraint in code, (tried both viewDidLoad and viewDidAppear). Constraint is set between container view and top of the screen, depending on size and amount of images.
Example of code:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSInteger totalImages = [self.object.fullphotos count];
self.labelsTopConstraint.constant = totalImages*(imageHeight + 20) + 10;
}
In case 2 buttons are positioned right, but don't respond to touches.
How should I layout everything correctly?
Thanks in advance!
Take a Tableview for those images and add buttons in a last cell.
The best way is creating a Object with a refresh method that can be called in viewDidAppear
MyObject.h
#interface MyObject : UIViewController
#property (nonatomic,strong) UIImageview *img;
#property (nonatomic,strong) UIButton *btn;
- (void) refresh;
in MyObject.m
- (void)viewDidLoad {
[self.btn addTarget:self action:#selector(myMethod:) forControlEvents:UIControlEventTouchUpInside];
}
- (void) refresh {
//make your settings here
}
-(void)myMethod {
//your button action here
}
Then in your controller if you have your objects in an NSArray:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
for (MyObject *myObj in objectsArray) {
#autoreleasePool {
[myObj refresh];
}
}
}
I have a head(butt)ache last two days. A dodgy memory leak makes me Hulk.
A lot of leaks appear after loading any url request in custom UIWebView when profiling code on the simulator. But if I use a device like iPhone 5 there is just one leak.
The huge trouble is that Instruments does not show any line of my code.
I remember as I saw a great video tutorial to locate these leaks, but googling has no result in two days :(
Here is a simple project for Xcode 5, it leaks when webView property loaded an url request.
Screenshots: one, two.
UPD: Added whole code.
UPD2: Tiny refactoring.
#import "AKViewController.h"
#interface AKViewController ()
#property (nonatomic, strong, readonly) UIWebView *webView;
#end
#implementation AKViewController
#synthesize webView = _webView;
#define MARGIN_WEB_VIEW_X 15.0f
#define MARGIN_WEB_VIEW_TOP 30.0f
#define MARGIN_WEB_VIEW_BOTTOM 25.0f
#pragma mark - Private methods
- (CGRect)makeRectForWebView {
CGRect appFrame = UIScreen.mainScreen.applicationFrame;
CGRect rectWebView = CGRectMake(MARGIN_WEB_VIEW_X,
MARGIN_WEB_VIEW_TOP,
appFrame.size.width - MARGIN_WEB_VIEW_X * 2,
appFrame.size.height - MARGIN_WEB_VIEW_BOTTOM);
return rectWebView;
}
- (void)presentViews {
[self.view.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
self.webView.frame = [self makeRectForWebView];
[self.view addSubview:self.webView];
}
- (NSURLRequest *)makeLoginURLRequest {
NSString *stringUrl = #"http://google.com/";
NSURL *url = [NSURL URLWithString:[stringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
return request;
}
#pragma mark - Properties
- (UIWebView *)webView {
if (!_webView) {
_webView = [[UIWebView alloc] initWithFrame:UIScreen.mainScreen.applicationFrame];
_webView.scalesPageToFit = YES;
}
return _webView;
}
#pragma mark - Lifecycle
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self presentViews];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
[self.webView loadRequest:[self makeLoginURLRequest]];
}
#end
HELP!
I don't know if you're showing all of the code, so I'm going to guess that you may not be releasing the web view's delegate. The documentation for the UIWebView delegate property says:
Important: Before releasing an instance of UIWebView for which you
have set a delegate, you must first set its delegate property to nil.
This can be done, for example, in your dealloc method.
I am trying to reload content from local file of UIWebView in viewDidDisappear of UIViewController. Badaccess is caught. But if I write the same code in viewWillDisappear, it works.
What might be the reason?
Once I've heard that UIWebView can't reload its content when it is not visible (not sure about it).
My code (hope it'll be enough):
#interface WebViewController : UIViewController <UIWebViewDelegate> {
ExtendedWebView * webView;
}
#property (nonatomic, retain) ExtendedWebView * webView;
#end
//WebViewController implementation
- (void)loadView
{
[super loadView];
WebViewCachingSingleton * webViewSingleton = [WebViewCachingSingleton sharedService];
ExtendedWebView * newWebView = [webViewSingleton getAvailableWebViewResource];//here I get ExtendedWebView. it works =)
newWebView.frame = CGRectMake(0, 0, 320, 400);
newWebView.delegate = self;
[self.view addSubview:newWebView];
self.webView = newWebView;
}
- (void)viewDidDisappear:(BOOL)animated
{
[[WebViewCachingSingleton sharedService] makeWebViewUnused:self.webView];
}
//WebViewCachingSingleton:
- (void) makeWebViewUnused : (ExtendedWebView *) aWebView
{
aWebView.isFree = YES;
[aWebView reload];
}
It will not work because viewDidDisappear is called when the view is disappeared, so all the subviews are released. The viewWillDisappear is called just before releasing all the objects associated with that view.
So you are trying to call the reload method of a UIWebView that has been already released. That is basically the reason why it crashes.
Hope it helps
The reload can be performed even if not displayed.
I tried to make an example of code to put in your state and have not had any problems.
Try to debug and enable NSZombieEnabled to see what actually happens to your application.
Try to post on any piece of code that might help us give you more details.