Where to get NSTextView dragged images reference - cocoa

I have a NSTextView with setImportGraphics(true), I can drag images there, they show up in the interface but I have no idea how to programmatically get the image (and store it) once it's been dragged.
If I call myNSTextView.string all I get is the text around the image, but the image seems non-existent.
Do I have to implement some methods regarding drag and drop to manage this case?

I have no idea how to programmatically get the image (and store it) once it's been dragged.
Dropped images are added to the NSTextStorage as NSTextAttachment. So, in order to access the dropped images you should iterate over the content of the textStorage and check for attachments that conforms to image files.
Do I have to implement some methods regarding drag and drop to manage this case
You certainly can handle the dropped files by extending NSTextView and overwriting the - (void)performDragOperation:(id<NSDraggingOperation>)sender method, if you want to go this way, I recommend you to read the Drag and Drop Programming Topics document from Apple.
Since I'm no fan of subclassing, my answer to this problem uses a Category of NSAttributedString to return a NSArray of attached images. Which can be solved with the code below:
#import "NSAttributedString+AttachedImages.h"
#implementation NSAttributedString (AttachedImages)
- (NSArray *)images
{
NSMutableArray *images = [NSMutableArray array];
NSRange effectiveRange = NSMakeRange(0, 0);
NSTextAttachment *attachment;
CFStringRef extension;
CFStringRef fileUTI;
while (NSMaxRange(effectiveRange) < self.length) {
attachment = [self attribute:NSAttachmentAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];
if (attachment) {
extension = (__bridge CFStringRef) attachment.fileWrapper.preferredFilename.pathExtension;
fileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extension, NULL);
if (UTTypeConformsTo(fileUTI, kUTTypeImage)) {
NSImage *theImage = [[NSImage alloc] initWithData:attachment.fileWrapper.regularFileContents];
[theImage setName:attachment.fileWrapper.preferredFilename];
[images addObject:theImage];
}
}
}
return images.copy;
}
#end
If you use GIT, you can clone the code from my Github repository.
I hope it helps

Related

Efficiently handling UIImage loading in UICollectionView

In my app the user can take a photo of their pet, or whatever, and that will show in the collection view as a circular picture. Using cellForItemAtIndexPath to check for the existence of a user generated photo (which is stored in the Documents directory as a png file), if not display a default photo.
Issues:
- When Scrolling the UICollectionView the image load halts the
smoothness of the scroll UI as the picture is going to appear on
screen.
- If you scroll the picture off the screen many times the
app crashes due to memory pressure.
QUESTION: Is there an efficient way of loading and keeping the images in memory without having to keep loading the image after the first time it is displayed?
I thought rasterizing the images would help, but it seems the UI scrolling and memory issues persist.
Here is the code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifierPortrait = #"allPetCellsPortrait";
AllPetsCell *cell = (AllPetsCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifierPortrait forIndexPath:indexPath];
CGRect rect = CGRectMake(cell.petView.center.x, cell.petView.center.y, cell.petView.frame.size.width, cell.petView.frame.size.height);
UIImageView *iv = [[UIImageView alloc] initWithFrame:rect];
//set real image here
NSString *petNameImage = [NSString stringWithFormat:#"%#.png", self.petNames[indexPath.section][indexPath.item]];
NSString *filePath = [_ad documentsPathForFileName:petNameImage];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filePath];
iv.image = nil;
if (fileExists){
[_allPetsCollectionView performBatchUpdates:^{
if (iv && fileExists) {
iv.image = [UIImage imageWithContentsOfFile:filePath];
}
} completion:^(BOOL finished) {
//after
}];
//loadedImage = YES;
[collectionView layoutIfNeeded];
}//file exists
else{
iv.image = [UIImage imageNamed:[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]]];
[cell.petNameLabel setTextColor:[UIColor darkGrayColor]];//be aware of color over pictures
shadow.shadowColor = [UIColor whiteColor];
}
cell.petView.clipsToBounds = YES;
UIColor *color = MediumPastelGreenColor;
if ([[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]] isEqualToString:#"addpet.png"]) {
color = MediumLightSkyBlueColor;
}
[cell.petView setBounds:rect forBorderColor:color];
[cell.petView addSubview:iv];
if (![[NSString stringWithFormat:#"%#", self.contents[indexPath.section][indexPath.item]] isEqualToString:#"addpet.png"])
[iv addSubview:cell.petNameLabel];
//TODO: optimize the cell by rasterizing it?
if ([_contents count] > 4) {
cell.layer.shouldRasterize = YES;
}
return cell;
}
Here is a screen shot:
Thanks!
Assigning an image to the collection view cell's imageView doesn't cost you any performance issues at all..
There are 2 problems here.
You are fetching the contents (datasource for the collection view) from within the collection view's delegate method. This is causing the delay.
You are adding the imageView holding the image again and again. Thus, each time the cell is displayed, a new imageView holding a newly fetched data is being added as the subView to the reused cell (As the new imageView covers the old one as both have the same frame, you wouldn't notice. And at the sametime this eats up memory). This is causing the memory pressure crash.
SOLUTION
FOR IMPROVING PERFORMANCE
Fetch the image contents from your model class(if you are using one), or from you viewController's viewDidLoad() (You should never fetch datasource from within the collection view's delegate method). Perform the fetching process in the background queue as it wouldn't affect your UI in any manner.
Store the contents onto an array and fetch the contents only from the array to populate your collection view cells.
FOR AVOIDING MEMORY CRASH
As long as you are not using a custom collection view cell, its better to remove all the subviews from the collectionViewCell's content view each time the cell is dequeued.
I hope this solves your problem :)
I recently discovered NSCache which is a dictionary type object that I can store my *already loaded image*s into and check to see if they are loaded with the -cellForItemAtIndexPath method.
I changed the above code to accommodate for this also by using a custom dispatch queue that would be sent to the main queue. This still causes a slight pause bump in the scrolling when the initial image is loaded but after that it is read from the image cache. Not totally optimal yet but I think I am headed in the right direction.
here is the code:
if (fileExists){
//DONE: implement NSCACHE and dispatch queue
const char *petImageQ = "pet image";
dispatch_queue_t petImageQueue = dispatch_queue_create( petImageQ, NULL);
//check to see if image exsists in cache - if not add it.
if ([_petImagesCache objectForKey:petNameImage] == nil) {
dispatch_async(petImageQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView performBatchUpdates:^{
iv.image = [UIImage imageWithContentsOfFile:filePath];
} completion:^(BOOL finished) {
//add to cache
[_petImagesCache setObject:iv.image forKey:petNameImage];
}];
});
});
}
else{
//load from cache
iv.image = [_petImagesCache objectForKey:petNameImage];
}
I hope this helps. Please add more to this for improving it!

Sprite Kit Animation: Atlas Error

I dragged a texture atlas into my project. The pictures are named correctly ("heliani_1-9")
The animation is running smooth, except for 3 frames, which are displayed as big red cross on a white ground. (See screenshot enclosed)
What is wrong with my code?
Cheers
#import "MRMPlayer.h"
#implementation MRMPlayer
-(instancetype)init
{
self = [super init];
{
[self setupAnimations];
[self runAction:[SKAction repeatActionForever:[SKAction animateWithTextures:self.runFrames timePerFrame:0.5 resize:YES restore:NO]] withKey:#"heli"];
self.name = playerName;
}
return self;
}
-(void) setupAnimations{
self.runFrames = [[NSMutableArray alloc]init];
SKTextureAtlas *heliAtlas = [SKTextureAtlas atlasNamed:#"heli"];
for (int i = 0; i < [heliAtlas.textureNames count]; i++) {
NSString *tempName = [NSString stringWithFormat:#"heliani_%d",i];
SKTexture *tempTexture = [heliAtlas textureNamed:tempName];
if(tempTexture) {
[self.runFrames addObject:tempTexture];
}
}
}
#end
Go to the product menu, and you will see the Clean option.
Now, hold down the option button on your keyboard and the text should change to Clean build folder...
Choose that option, and it will additionally delete the derivative data folder that caches alot of things including the texture atlas, and I've found to cause problems like you have described. If you rename files in the atlas, thats typically when I have experienced this issue myself.
I don't like that this option is something you kind of have to work for, would be nice to have this second option there WITHOUT having to hold down the option key.
If this doesn't solve the problem, you truly have a naming problem imo.
Note
You can also delete the derivative data folders from the Organizer window.

Resize NSPopupButton to its selected title

I've a NSPopupButton and I want it so resize itself to fit the selected title.
[NSPopupButton sizeToFit] doesn't fit my needs because the popup is resized to the largest title item not to the current selected one
I've tried in may ways without success the closer is
#define ARROW_WIDTH 20
NSDictionary *displayAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[popup font], NSFontAttributeName, nil];
NSSize titleSize = [popup.titleOfSelectedItem sizeWithAttributes:displayAttributes] + ARROW_WIDTH;
But the constant value ARROW_WIDTH is a really dirty and error prone solution.
TextWrangler encoding combo on status bar works like I need
The way I've handled these problems with text fields is to try the resizing with a text field that you never add to the view hierarchy. You call sizeToFit on an object that you have no intention of reusing, then use that to figure out how wide your actual control needs to be to fit what you need to do.
So, in pseudo code, you'd do this (assuming you're using ARC, YMMV for non-ARC projects as this will leak):
NSArray *popupTitle = [NSArray arrayWithObject: title];
NSPopUpButton *invisiblePopup = [[NSPopUpButton alloc] initWithFrame: CGRectZero pullsDown: YES];
// Note that you may have to set the pullsDown bool to whatever it is with your actual popup button.
[invisiblePopup addItemWithTitle: #"selected title here"];
[invisiblePopup sizeToFit];
CGRect requiredFrame = [invisiblePopup frame];
self.actualPopup.frame = requiredFrame;
For projects with autolayout override method intrinsicContentSize in subclass of NSPopUpButton
class NNPopUpButton: NSPopUpButton {
override var intrinsicContentSize: NSSize {
let fakePopUpButton = NSPopUpButton(frame: NSZeroRect, pullsDown: false)
fakePopUpButton.addItem(withTitle: title)
fakePopUpButton.sizeToFit()
var requiredFrame = fakePopUpButton.frame
requiredFrame.size.width -= 35 // reserved space for key equivalent
return requiredFrame.size
}
}

Creating a reference to a UIImageView from a string

I have a series of UIImageViews which are created in IB. I want to show and hide these depending on some button presses. I currently have a method which says if 1 is pressed then hide 2,3,4 then if 2 is pressed then hide 1,3,4. This works but I am trying to improve my code for an update.
My background is actionscript so I'm not sure if what I am trying to do is the right thing.
I basically want to evaluate a string reference to UIImageView, in AS I would use eval(string).
The method I am using is to create a string from a string and a number, so I get "image1". All works, but then I need to evaluate this into a UIImageView so I can update the alpha value.
Firstly is this possible and if not how should I be doing this? I'm starting to think that having this set up in interface builder maybe isn't helping?
That's probably not a good way to work. What you want is an array of imageViews. Then you just need a numeric index, and you can go through the array of imageViews hiding everything that doesn't have the chosen index.
But how do you get an array of imageViews? See How to make IBOutlets out of an array of objects? It explains how to use IBOutletCollection.
If you have a separate button for each view, put those into an IBOutletCollection too. That way you can have something like this:
- (IBAction) imageButtonPressed:(id) sender;
{
// The sender is the button that was just pressed.
NSUInteger chosenIndex = [[self imageButtons] objectAtIndex:sender];
for (NSUInteger imageIndex = 0; imageIndex < [[self imageViews] count]; imageIndex++)
{
// Hide all views other than the one associated with the pressed button.
if (imageIndex != chosenIndex)
{
[[[self imageViews] objectAtIndex:imageIndex] setHidden:YES];
}
else
{
[[[self imageViews] objectAtIndex:imageIndex] setHidden:NO];
}
}
}
If you really, really need to associate the string image1 with an imageView, you can construct an NSDictionary associating your controls with unique string identifiers for later lookup. NSDictionary is powerful, but I'm drawing a blank on reasons why this would be needed.
NSMutableDictionary *viewLookup;
[viewLookup setObject:[[self imageViews] objectAtIndex:0] forKey:#"image0"];
[viewLookup setObject:[[self imageViews] objectAtIndex:1] forKey:#"image1"];
[viewLookup setObject:[[self imageViews] objectAtIndex:2] forKey:#"image2"];
[viewLookup setObject:[[self imageButtons] objectAtIndex:0] forKey:#"button0"];
// ...
// Can now look up views by name.
// ...
NSString *viewName = #"image1";
UIView *viewFound = [viewLookup objectForKey:viewName];
[viewFound doSomething];

How can I make NSCollectionView finish drawing before the view containing it is made visible?

I'm trying to clean up the UI on my application (built for 10.5) and one thing that's rather annoying is that when I swap to the library I reload the contents and the nscollectionview that displays the items fades the old content out before fading the new ones in. I don't mind the fade in/out of old/new items, but the way i've programmed the view swapping should make the library reload happen before the the nsview that contains the nscollectionview is inserted into the main window.
Anyone had this sort of problem before?
Here's an abridged version of how the swap between different parts of the application to the library goes:
In the main controller:
-(void)setMainPaneToLibrary {
if(viewState == VIEW_STATE_LIBRARY)
return;
[libraryController refreshDevices:self];
[libraryController refreshLibraryContents];
[menuController setSelectionToIndex:0];
viewState = VIEW_STATE_LIBRARY;
[self removeSubview]; //clear the area
[mainPane addSubview:[libraryController view]];
}
In the library controller:
-(void)refreshLibraryContents {
[self loadLibraryContents:[rootDir stringByAppendingPathComponent:currentDir]];
}
-(void)loadLibraryContents:(NSString *)folderToLoad {
int i;
int currentSelection = [libraryArrayController selectionIndex]; //used to preserve selection and smooth folder changing
[libraryArrayController setContent:nil];
//expand the path to load
folderToLoad = [folderToLoad stringByExpandingTildeInPath];
NSFileManager * fileManager = [NSFileManager defaultManager];
//load the files in the folder
NSArray * contents = [fileManager directoryContentsAtPath:folderToLoad];
//create two seperate arrays for actual files and folders - allows us to add them in order
NSMutableArray * directories = [[NSMutableArray alloc] init];
NSMutableArray * musicFiles = [[NSMutableArray alloc] init];
//... a load of stuff filling the file arrays ...
for(i=0;i<[directories count];i++) {
[libraryArrayController addObject:[directories objectAtIndex:i]];
}
for(i=0;i<[musicFiles count];i++) {
[libraryArrayController addObject:[musicFiles objectAtIndex:i]];
}
if(currentSelection >= 0) {
[libraryArrayController setSelectionIndex:currentSelection];
}
[directories release];
[musicFiles release];
}
The issue is not that the collection view hasn't finished drawing, it's that it animates the adding and removal of the subviews. It may be possible to create a transaction that disables layer actions while you're reloading the collection view's content. Listing 2 (temporarily disabling a layer's actions) shows how to do this. I haven't tested this, so I don't know if this will do what you want, but this is where I'd start.

Resources