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).
Related
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.
I was under the impression that when using bindings (been following this tutorial despite being outdated. http://cocoadevcentral.com/articles/000085.php - You can use it to see what I'm doing) the Persistent Store would automagically save the changes you make. In fact, though it was hours ago and I wouldn't be surprised if I'm now going mad, I got it working, and when I made a change it would persist on rebuilding the app.
However, the test app I've built following the tutorial no longer saves and despite showing the changes I make within the app, they disappear once I re-run the app. I've been checking the Core Data debug menu and nothing happens when I press the "+" button which is set up to the "Add" method of my NSArrayController. I know it's accessing my data model too as my textField for the Title (again, see the tutorial so you know what I'm referring to) adopts the default text I put in the DataModel section. The only thing missing therefore is the actual saving.
So my real question is, based on the tutorial, what part of the bindings actually makes the managedObjectContext save? Is there a flag or something that isn't checked?
I don't know if it's important or not, but there were differences between the tutorial and my project, mainly that the NSArrayControllers are bound to "App Delegate"with a Model Key Path of "self.managedObjectContext". Also, I removed all the relationships in an attempt to whittle down the issue.
Any help would be greatly appreciated.
Regards,
Mike
UPDATE: Here are some pictures that show the bindings.
How I set up the NSArrayController:
Here is how is how my Data Model Looks:
Lastly, this is how I set up the TextFields to update the NSArrayControllers:
I hope this helps to get a an ideas as to the set up.
Thanks,
Mike
Could you check to make sure you've copied all the Core Data boiler-plate code from the source code of the tutorial you mentioned.
Specifically this part in the App Delegate:
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
NSError *error;
NSManagedObjectContext *context;
int reply = NSTerminateNow;
context = [self managedObjectContext];
if (context != nil) {
if ([context commitEditing]) {
if (![context save:&error]) {
// This default error handling implementation should be changed to make sure the error presented includes application specific error recovery. For now, simply display 2 panels.
BOOL errorResult = [[NSApplication sharedApplication] presentError:error];
if (errorResult == YES) { // Then the error was handled
reply = NSTerminateCancel;
} else {
// Error handling wasn't implemented. Fall back to displaying a "quit anyway" panel.
int alertReturn = NSRunAlertPanel(nil, #"Could not save changes while quitting. Quit anyway?" , #"Quit anyway", #"Cancel", nil);
if (alertReturn == NSAlertAlternateReturn) {
reply = NSTerminateCancel;
}
}
}
} else {
reply = NSTerminateCancel;
}
}
return reply;
}
If it's there, changes will be saved when the app is terminated normally. Pressing the 'stop' button in Xcode will terminate the app immediately, without going through the method mentioned above.
My guess is that you are not going mad, but first exited the app properly and have been pressing the 'stop' button later ;).
In my app, a graph in one view can be dragged to a second view so that the new graph replaces the second view (like a copy/paste effect with a drag and drop feature). The app works if the delegate protocol is taken out so that the second view handles the change in function itself. When the protocol is added, the app crashes in the main file at
return UIApplicationMain(argc, argv, nil, NSStringFromClass([Load_CreatorAppDelegate class]));.
There isn't any error output other than the standard (lldb). Even when I take out the call to the delegate (keeping in the code), the app crashes. I know that it has to be related to the protocol code, though, because it worked fine before that.
Here is part of the code for the second view (BeamView):
[self drawSupportsAtLeftPoint:self.beamBottomLeft rightPoint:self.beamBottomRight inContext:context :leftPin :rightPin];
BOOL pt = NO;
if (self.tempLoad) {
//self.loadGraph = [self.dataSource changeToTempLoad:self]; NOTE #1
//if (self.tempPtLoad.x != 0 || self.tempPtLoad.y != 0) pt = YES;
pt = [self changeLoad];
[self drawLoadWithFunction:self.loadGraph inContext:context fromPoint:self.beamTopLeft toPoint:self.beamTopRight withAlpha:0.3 isPointLoad:pt inBlack:YES];
}
else {
self.loadGraph = ^(int x) {return x/15;};
[self drawLoadWithFunction:self.loadGraph inContext:context fromPoint:self.beamTopLeft toPoint:self.beamTopRight withAlpha:1 isPointLoad:pt inBlack:NO];
}
self.tempLoad = NO;
NOTE #1: These lines that are commented out are the ones that call on the delegate. Those two methods and their implementation are the only changes I made.
I'm completely confused, any help would be greatly appreciated. What are possible reasons the app will crash in the main file?
Ok! I feel kind of stupid, but it turned out that the crash didn't have anything to do with delegation (well, kind of). I had deleted outlets in the ViewController.m file without disconnecting them in IB, which was causing the crash.
I'd forgotten that I'd done that, and so it took me a while to think of it--it wasn't until I went back to an older saved version that I saw the differences.
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];
}
}
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.