How do I tell when a UIWebView is done rendering (not loading)? - objective-c

I know when its done loading... (webViewDidFinishLoad), but I want to use
[webView.layer renderInContext:UIGraphicsGetCurrentContext()];
to create an image from the UIWebView. Occasionally I get the image prior to the webView finishing its rendering. I can use performSelector to delay the get of the image, but the amount of wait is arbitrary and brittle.

This may depend upon the kind of graphics context you need the view rendered into, but you can call
- (void)drawRect:(CGRect)area forViewPrintFormatter:(UIViewPrintFormatter *)formatter
which apparently tricks the UIWebView into thinking that it's being printed. This may help if your ultimate goal is to capture the complete page. We've had the problem recently in which even if the page was fully loaded, calling plain old -drawRect: didn't render the entire page if some of it was offscreen.

I had a similar problem in an application where I fully control the content. This won't work if you load arbitrary pages from the web but it may work if you know your high-level DOM structure and it doesn't change much.
What worked for me was the following.
I had to recursively find the first UIWebView's subview of type UIWebOverflowScrollView with a frame of (0, 0, 1024, 768). If I couldn't find one, that was a sure sign the content hasn't been rendered yet.
Having found it, I check this view's layer.sublayers.count. When the rendering finishes, I would always end up with one or three sublayers. If the rendering hasn't finished, however, the content view always had at most one sublayer.
Now, that's specific to my DOM structure—but it may be possible that you make up a similar “test” if you compare sublayer tree before and after rendering. For me, the rule of thumb was “the first recursively found subview of type UIWebOverflowScrollView will have at least three sublayers”, for you it will likely be different.
Anyway, take great care if you decide to use this approach, for even though you won't get rejected for looking into UIWebView's view and layer hierarchy, this kind of behaviour is unreliable and is very likely to change in future versions of iOS. It may very well be inconsistent between iOS 5 and iOS 6 as well.
Finally, code snippets for MonoTouch which should be straightforward to translate to Objective C:
bool IsContentScrollView (UIScrollView scrollView)
{
return scrollView.Frame.Equals (new RectangleF (0, 0, 1024, 768));
}
[DllImport ("/usr/lib/libobjc.dylib")]
private static extern IntPtr object_getClassName (IntPtr obj);
public static string GetClassName (this UIView view) {
return Marshal.PtrToStringAuto (object_getClassName (view.Handle));
}
bool IsWebOverflowScrollView (UIScrollView scrollView)
{
return scrollView.GetClassName () == "UIWebOverflowScrollView";
}
IEnumerable<UIScrollView> ScrollViewsInside (UIView view)
{
foreach (var subview in view.Subviews) {
foreach (var scrollView in ScrollViewsInside (subview).ToList())
yield return scrollView;
if (subview is UIScrollView)
yield return (UIScrollView)subview;
}
}
bool CanMakeThumbnail {
get {
var scrollViews = ScrollViewsInside (this).Where (IsWebOverflowScrollView).ToList ();
var contentView = scrollViews.FirstOrDefault (IsContentScrollView);
// I don't know why, but this seems to be a good enough heuristic.
// When the screen is black, either contentView is null or it has just 1 sublayer.
// This may *break* on any iOS updates or DOM changes--be extra careful!
if (contentView == null)
return false;
var contentLayer = contentView.Layer;
if (contentLayer == null || contentLayer.Sublayers == null || contentLayer.Sublayers.Length < 3)
return false;
return true;
}
}

What about using the window.onload or jQuerys $(document).ready event to trigger the shouldStartLoad callback?
Something like:
$(document).ready(function(){
location.href = "app://do.something"
})
and in your UIWebViewDelegate do something like:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = request.URL;
if([url.host isEqualToString:#"app"]){
//maybe check for "do.something"
//at this point you know, when the DOM is finished
}
}
With this method you can forward every possible event from the JS code to your obj-c code.
Hope that helps. The code sample is written in the browser, and therefore not tested! ;-)

- (void)webViewDidFinishLoad:(UIWebView *)webView {
if (webView.isLoading)
return;
else
{
[self hideProgress];
}
}

If you have access to the HTML file and can edit it - then just add some special variable which will tell the code that rendering is done.
Then use method stringByEvaluatingJavaScriptFromString to know should you start some actions or not.
The example below is pretty dirty, but hope it will help and give you the idea:
(void)webViewDidFinishLoad:(UIWebView *)webView
{
BOOL renderingDone = NO;
while (!(renderingDone == [[webView stringByEvaluatingJavaScriptFromString:#"yourjavascriptvariable"] isEqualToString:#"YES"]))
{
// the app will be "freezed" till the moment when your javascript variable will not get "YES" value
}
// do your stuff, rendering is done
}

if (webView.loading == YES)
{
//Load image
}
Something like that might work.

Related

How can dark mode be detected on macOS 10.14?

In macOS 10.14 users can choose to adopt a system-wide light or dark appearance and I need to adjust some colours manually depend of the current mode.
Since the actual appearance object you usually get via effectiveAppearance is a composite appearance, asking for its name directly probably isn't a reliable solution.
Asking for the currentAppearance usually isn't a good idea, either, as a view may be explicitly set to light mode or you want to know whether a view is light or dark outside of a drawRect: where you might get incorrect results after a mode switch.
The solution I came up with looks like this:
BOOL appearanceIsDark(NSAppearance * appearance)
{
if (#available(macOS 10.14, *)) {
NSAppearanceName basicAppearance = [appearance bestMatchFromAppearancesWithNames:#[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua
]];
return [basicAppearance isEqualToString:NSAppearanceNameDarkAqua];
} else {
return NO;
}
}
You would use it like appearanceIsDark(someView.effectiveAppearance) since the appearance of a specific view may be different than that of another view if you explicitly set someView.appearance.
You could also create a category on NSAppearance and add a - (BOOL)isDark method to get someView.effectiveAppearance.isDark (better chose a name that is unlikely to be used by Apple in the future, e.g. by adding a vendor prefix).
I have used the current appearance checking if the system is 10.14
+ (BOOL)isDarkMode {
NSAppearance *appearance = NSAppearance.currentAppearance;
if (#available(*, macOS 10.14)) {
return appearance.name == NSAppearanceNameDarkAqua;
}
return NO;
}
And to detect the change of mode in a view the methods are:
- (void)updateLayer;
- (void)drawRect:(NSRect)dirtyRect;
- (void)layout;
- (void)updateConstraints;
And to detect the change of mode in a view controller the methods are:
- (void)updateViewConstraints;
- (void)viewWillLayout;
- (void)viewDidLayout;
Using notification:
// Monitor menu/dock theme changes...
[NSDistributedNotificationCenter.defaultCenter addObserver:self selector:#selector(themeChanged:) name:#"AppleInterfaceThemeChangedNotification" object: nil];
-(void)themeChanged:(NSNotification *) notification {
NSLog (#"%#", notification);
}
For more information Dark Mode Documentation
Swift 4
func isDarkMode(view: NSView) -> Bool {
if #available(OSX 10.14, *) {
return view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
return false
}
For me neither of these answers worked, if I wanted a global state, not per view, and I didn't have access to the view, and I wanted to be notified for updates.
The solution was to ask for NSApp.effectiveAppearance in the main thread, or at least after the current callback method has returned to the system.
So, first I have to register, following the directions of Saúl Moreno Abril, with a code like
[NSDistributedNotificationCenter.defaultCenter addObserver:self selector:#selector(themeChanged:) name:#"AppleInterfaceThemeChangedNotification" object: nil];
then on the callback method write something like
-(void)themeChanged:(NSNotification *) notification {
[self performSelectorOnMainThread:#selector(themeChangedOnMainThread) withObject:nil waitUntilDone:false];
}
and then the actual code:
- (void) themeChangedOnMainThread {
NSAppearance* appearance = NSApp.effectiveAppearance;
NSString* name = appearance.name;
BOOL dark = [appearance bestMatchFromAppearancesWithNames:#[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]] == NSAppearanceNameDarkAqua;
}
Also the answer from Borzh helped, but is seemed more fragile than the others.
There are actually 8 possible appearances for a view, and 4 of them are for ordinary use. That is,
NSAppearanceNameAqua the Light Mode,
NSAppearanceNameDarkAqua the Dark Mode,
NSAppearanceNameAccessibilityHighContrastAqua Light Mode with increased contrast (set from Accessibility),
NSAppearanceNameAccessibilityHighContrastDarkAqua Dark Mode with increased contrast.
A direct comparison
appearance.name == NSAppearanceNameDarkAqua;
may fail to detect the dark mode if it is with increased contrast. So, always use bestMatchFromAppearancesWithNames instead.
And it is even better to take account of the high-contrast appearances for better accessibility.
To know if the app appearance is Dark, use next code:
+ (BOOL)isDarkMode {
NSString *interfaceStyle = [NSUserDefaults.standardUserDefaults valueForKey:#"AppleInterfaceStyle"];
return [interfaceStyle isEqualToString:#"Dark"];
}

UICollectionViewLayout supplementary view duplicate on delete

I have been tinkering with a UICollectionViewLayout for a few days now, and seem to be stuck on getting a Supplementary view to delete with animation using the finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
I have finally made some headway and discovered this issue. Initially it looked as if it was not animating, because i was not getting a fading animation. so i added a transform on it to get it to rotate... and discovered a supplementary view is in fact being animated, but another one just sits there. until its finished and the unceremoniously disappears not being affected by being told to disappear at all. So to sum up it looks like an exact copy is made and that is animated out for finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath: and the original just sits there unmoving and animating, then both disappear.
This copy or whatever it is does not disappear either until i scroll the view.
Does anyone have any insight into this issue?
Update
So after some tweaking, experimentation and many hypothesis later as to what was causing the issue, it turns out that the culprit was this method
- (NSArray *)indexPathsToDeleteForSupplementaryViewOfKind:(NSString *)kind
{
NSMutableArray *deletedIndexPaths = [NSMutableArray new];
[self.updatedItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL * _Nonnull stop)
{
switch (updateItem.updateAction)
{
case UICollectionUpdateActionInsert:
{
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
}
break;
case UICollectionUpdateActionDelete:
{
[deletedIndexPaths addObject:updateItem.indexPathBeforeUpdate];
}
break;
default:
break;
}
}];
return deletedIndexPaths;
}
The part to focus on is here
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
Particularly the _unReadHeaderIndexPath = nil;
What this is doing is making a supplementary view disappear which indicates unread messages when the user inserts any text and then setting it to nil so it is not added multiple times, the logic is sound however the method in which is employed seems to be the issue.
indexPathsToDeleteForSupplementaryViewOfKind: is called multiple times and one can assume it needs multiple passes through it to work properly but since we nil _unReadHeaderIndexPath; it seems like it isn't added to the returned array at the appropriate time.
It also seems like the incorrect method in as I'm checking for insertion action in a method name indexPathsToDeleteForSupplementaryViewOfKind: so the solution was to move all of that logic to the method
prepareForCollectionViewUpdates:
Where we originally set the values for self.updatedItems in the first place. Which means i can get rid of that member variable altogether. And it animates and disappears without any odd copy sticking around.

NSOpenPanel (doesn't) validateVisibleColumns

I have an NSOpenPanel with an accessoryView; in this view the user chooses a couple of radio button to change the allowed types. When the panel opens, the right files are enabled, the other disabled. Ok, good.
Now the user changes the radio buttons, the viewController of the accessoryView observe the changes in the radio button matrix and changes consequently the allowedTypes of the NSOpenPanel.
After that, following Apple documentation, it calls -validateVisibleColumns, but nothing visible changes in the panel. That is: the right files seems disabled: I can choose them but they are in grey!
Another wrong effect: I select a file (enabled), change the file type, the (now wrong) file remains selected, with the OK button enabled: but this is the wrong file type! It seems that the change happens but the interface doesn't know!
My code is (selected is bound to the matrix of radio button):
- (void)observeValueForKeyPath.....
{
NSString *extension = (self.selected==0) ? #"txt" : #"xml";
[thePanel setAllowedFileTypes:#[extension, [extension uppercaseString]]];
[thePanel validateVisibleColumns];
}
I first tried to insert a call
[thePanel displayIfNeeded]
then I tried with
[thePanel contentView] setNeedsDisplay]
with no results. I also tried to implement the panel delegate method panel:shouldEnableURL:, that should be called by validateVisibleColumns: I just found that it was called just once, at the opening of NSOpenPanel.
Can someone have an idea why this happens? I tried all this with sandboxed and not-sandboxed applications, no difference. I'm developing on ML with 10.8 sdk.
Edit
By now the only way to avoid the problem is to implement panel:validateURL:error, but this is called after the user clicked 'open' and it's very bad.
I have the exact same problem, under 10.9, non-sandboxed, and have spent the better part of this DAY trying to find a solution!
After A LOT of tinkering and drilling down through the various classes that make up the NSOpenPanel (well NSSavePanel really) I did find a way to force the underlying table to refresh itself:
id table = [[[[[[[[[[[[_openPanel contentView] subviews][4] subviews][0] subviews][0] subviews][0] subviews][7] subviews][0] subviews][1] subviews][0] subviews][0] subviews][0] subviews][2];
[table reloadData];
Of course, the best way to code this hack would be to walk down the subview list ensuring the right classes are found and eventually caching the end table view for the subsequent reloadData calls.
I know, I know, this is a very ugly kludge, however, I can not seem to find any other answer to fix the issue, other than "file a bug report". Which, from what I can see online people have been doing since 1.8! :(
EDIT:
Here is the code I am now using to make my NSOpenPanel behave correctly under 10.9:
- (id) openPanelFindTable: (NSArray*)subviews;
{
id table = nil;
for (id view in subviews) {
if ([[view className] isEqualToString: #"FI_TListView"]) {
table = view;
break;
} else {
table = [self openPanelFindTable: [view subviews]];
if (table != nil) break;
}
}
return table;
}
- (void) refreshOpenPanel
{
if (_openPanelTableHack == nil)
_openPanelTableHack = [self openPanelFindTable: [[_openPanel contentView] subviews]];
[_openPanelTableHack reloadData];
[_openPanel validateVisibleColumns];
}
This code requires two instance variables _openPanel and _openPanelTableHack to be declared in order to work. I declared _openPanel as NSOpenPanel* and _openPanelTableHack is declared as id.
Now, instead of calling [_openPanel validateVisibleColumns] I call [self refreshOpenPanel] to force the panel to update the filenames as expected. I tried caching the table view when the NSOpenPanel was created, however, it seems that once you "run" the panel the table view changes, so I have to cache it on the first update instead.
Again, this is a GIANT hack, however, I do not know how long it will take Apple to fix the issue with accessory views and the file panels, so for now, this works.
If anyone has any other solutions that are not huge kludges please share! ;)
An implementation in swift of Eidola solution.
Biggest difference is that I search for a NSBrowser (sub)class rather than a specific class name. Tested on 10.10 (not sandboxed).
private weak var panelBrowser : NSBrowser? //avoid strong reference cycle
func reloadBrowser()
{
if let assumedBrowser = panelBrowser
{
assumedBrowser.reloadColumn(assumedBrowser.lastColumn)
}
else if let searchResult = self.locateBrowser(self.panel?.contentView as! NSView)
{
searchResult.reloadColumn(searchResult.lastColumn)
self.panelBrowser = searchResult //hang on to result
}
else
{
assertionFailure("browser not found")
}
}
//recursive search function
private func locateBrowser(view: NSView) -> NSBrowser?
{
for subview in view.subviews as! [NSView]
{
if subview is NSBrowser
{
return subview as? NSBrowser
}
else if let result = locateBrowser(subview)
{
return result
}
}
return nil
}
Edit:
Ok, so the code above will not work all the time. If it's not working and a file is selected (you can see the details/preview), then you have to reload the last to one column instead of the last column. Either reload the last two columns (make sure there are at least 2 columns) or reload all columns.
Second problem: if you reload the column, then you lose the selection. Yes, the selected files/directory will still be highlighted, but the panel will not return the correct URL's.
Now I am using this function:
func reloadBrowser()
{
//obtain browser
if self.panelBrowser == nil
{
self.panelBrowser = self.locateBrowser(self.panel?.contentView as! NSView)
}
assert(panelBrowser != nil, "browser not found")
//reload browser
let panelSelectionPatch = panelBrowser.selectionIndexPaths //otherwise the panel return the wrong urls
if panelBrowser.lastColumn > 0
{
panelBrowser.reloadColumn(panelBrowser.lastColumn-1)
}
panelBrowser.reloadColumn(panelBrowser.lastColumn)
panelBrowser.selectionIndexPaths = panelSelectionPatch
}
Just upgrade to xcode 6.3 (on Yosemite 10.10.3) and its ok. Apple fixed the bug (no more need Eidola Hack).

mimic animated iPhone recent calls list on OS X with NSTableView

I'm trying to pretty closely mimic the functionality of the iPhone phone app recent calls TableView, using an NSTableView on OS X. The desired functionality is that you click a button and the TableView switches from showing you all the recent calls to showing the subset of those calls that were missed, AND, I want to have the animation that you see on the iPhone where the appearance and disappearance of the rows is animated.
I'm using a view based NSTableView and I have it wired up very simply with a manual data source and delegate in my view controller (not using cocoa bindings).
Everything is working except when trying to delete rows to show the subset of "missed" calls. If I just reconstruct the NSMutableArray that is the data source for the TableView and then call reloadData, it works perfectly. But that gives me no animation. Here is the unanimated version which results in the TableView and the data source NSMutableArray in perfect sync, the correct content in the TableView, but no animation:
- (IBAction)showMissed:(id)sender {
NSMutableIndexSet *indices = [NSMutableIndexSet indexSet];
Call *call;
for (call in callsArray) {
if (!call.missed) {
[indices addIndex:[callsArray indexOfObject:call]];
}
}
if ([indices count] > 0) {
[callsArray removeObjectsAtIndexes:indices];
[self.recentsTableView reloadData];
}
}
When I try to use the NSTableView removeRowsAtIndexes:indices withAnimation: method to get the animation I want, I am getting some very strange behavior. Here is the animated showMissed method:
- (IBAction)showMissed:(id)sender {
NSMutableIndexSet *indices = [NSMutableIndexSet indexSet];
Call *call;
for (call in callsArray) {
if (!call.missed) {
[indices addIndex:[callsArray indexOfObject:call]];
}
}
if ([indices count] > 0) {
[callsArray removeObjectsAtIndexes:indices];
[self.recentsTableView removeRowsAtIndexes:indices withAnimation:NSTableViewAnimationSlideUp];
}
}
The logic I am using in the animated version of the method is pulled out of Apple's TableViewPlaygound sample code.
When I run this code, I get two strange results. First, the TableView does not show only "missed" calls -- it shows the right number of rows (i.e. the number of rows in the TableView equals the number of Call objects marked as missed), but the actual objects showing in the rows are some missed and some not missed.
Even weirder though is that after the animated version runs, the callsArray has ALL of the Call objects in it, as though the [callsArray removeObjectsAtIndexes:indices]; statement was not there at all. It seems like somehow the removeRowsAtIndexes:indices withAnimation: method is messing with the contents of my data source array and throwing the whole thing out of whack.
FWIW, my animated showAll method which is basically the reverse of the above using the insertRowsAtIndexes:indices withAnimation: seems to work perfectly.
What am I doing wrong?
There must have been some corruption somewhere in the sync between the data source array and the tableView. I fixed it by adding a reloadData call at the top of the method. Here is the fixed showMissed method:
- (IBAction)showMissed:(id)sender {
NSMutableIndexSet *indices = [NSMutableIndexSet indexSet];
Call *call;
[self.recentsTableView reloadData];
for (call in callsArray) {
if (!call.missed) {
[indices addIndex:[callsArray indexOfObject:call]];
}
}
if ([indices count] > 0) {
[callsArray removeObjectsAtIndexes:indices];
[self.recentsTableView removeRowsAtIndexes:indices withAnimation:NSTableViewAnimationSlideUp];
}
}

cocoa webview links disabled

On OSX, I am using XCode to make a desktop app with a webview in it. The webview loads ok, and I can dynamically load content into it - but when I click on links inside the webview, they are not followed. They change color, but no new page is loaded. If I code my links with javascript like this - then they work.
link there
Is there an Objective-C one liner that allows links to be followed inside web-views?
Is there another issue I am not aware of here?
The links at the top of Gmail open in a new window. To make them work you have to implement at least the WebUIDelegate methods webView:createWebViewWithRequest: and webViewShow:. If you simply want to open all links in the same web view, you could return it from webView:createWebViewWithRequest: instead of creating a new one.
Turns out it was because I had code I copied form the web with some custom function to ignore WebPolicyDecisionListener...
Sorry for asking question without giving all the details - all this objective-c is new to me, I don't know which bits do what yet. I do some pointing and clicking, and then some coding - I don't know exactly how that all links up. With other languages, you have the whole program in one place - it takes a bit of a learning curve to get used to... but I digress.
I fixed by adding some comments - see code below...
- (void)webView:(WebView *)aWebView
decidePolicyForNavigationAction:(NSDictionary *)actionInformation
request:(NSURLRequest *)request
frame:(WebFrame *)frame
decisionListener:(id < WebPolicyDecisionListener >)listener
{
if ([self requestIsLinkClick:actionInformation]) {
if ([#"method" isEqual:[[request URL] scheme]]) {
SEL selector = NSSelectorFromString([[request URL] resourceSpecifier]);
if ([prototypeDelegate respondsToSelector:selector]) {
[prototypeDelegate performSelector:selector];
}
}
// [listener ignore];
} // else {
[listener use];
//}
}