I'm trying to subclass UIImageView in order to create an instance of the subclass which plays different animations, depending on a variable I send it. My initial code (before subclassing) for playing a specific 2 frame animation looked like this. 'bubbleAnimationTemp' is just a UIImageView object I declared in the header:
UIImage *animImage1 = [UIImage imageNamed:#"HappyAnim1.png"];
UIImage *animImage2 = [UIImage imageNamed:#"HappyAnim2.png"];
NSArray *images = [[NSArray alloc] initWithObjects:animImage1, animImage2, nil];
bubbleAnimationTemp.animationImages = images;
[images release];
bubbleAnimationTemp.animationDuration = 0.5;
bubbleAnimationTemp.animationRepeatCount = 5;
[bubbleAnimationTemp startAnimating];
So then I tried subclassing UIImageView like so:
#import <UIKit/UIKit.h>
#import "Interaction.h"
#interface BubbleAnimation : UIImageView {
UIImage *emotAnim1;
UIImage *emotAnim2;
}
#property (nonatomic, retain) UIImage *emotAnim1;
#property (nonatomic, retain) UIImage *emotAnim2;
- (BubbleAnimation *)initWithMood:(NSString *)mood;
#end
#import "BubbleAnimation.h"
#implementation BubbleAnimation
#synthesize emotAnim1;
#synthesize emotAnim2;
- (BubbleAnimation *)initWithMood:(NSString *)mood {
if (self = [super init]) {
NSLog(#"Mood: %#", mood);
if ([mood isEqualToString:kGood]) {
emotAnim1 = [[UIImage alloc] initWithContentsOfFile:([[NSBundle mainBundle] pathForResource:#"HappyAnim1" ofType:#"png"])];
emotAnim2 = [[UIImage alloc] initWithContentsOfFile:([[NSBundle mainBundle] pathForResource:#"HappyAnim2" ofType:#"png"])];
//emotAnim1 = [UIImage imageNamed:#"HappyAnim1.png"];
//emotAnim2 = [UIImage imageNamed:#"HappyAnim2.png"];
}
else if ([mood isEqualToString:kNeutral]) {
emotAnim1 = [UIImage imageNamed:#"NeutralAnim1.png"];
emotAnim2 = [UIImage imageNamed:#"NeutralAnim2.png"];
}
else {
emotAnim1 = [UIImage imageNamed:#"SadAnim1.png"];
emotAnim2 = [UIImage imageNamed:#"SadAnim2.png"];
}
NSArray *images = [[NSArray alloc] initWithObjects:emotAnim1, emotAnim2, nil];
self.animationImages = images;
[images release];
}
return self;
}
As you can see, I tried two different approaches for creating the UIImages to add to the UIImageView. But the problem I'm having is that nothing shows up when the animation plays.
I also tried simply copying the code from the first method into this subclass, so the process is essentially the same, but still nothing appears.
I've checked the documentation for notes on subclassing UIImageView but there doesn't seem to be anything I'm missing. I've made sure to change the 'UIImageView' object I placed in Interface Builder into a 'BubbleAnimation' object, so it's not that.
Any help as to why nothing appears would be very much appreciated. Thanks as always!
Michael
****************UPDATE****************
Well, thanks to Kalle's advice below, this is all fixed. However, now a similar issue is reoccurring and I wonder what I'm doing wrong.
Basically, I want to have a small heart that appears in the thought bubble, alongside the animation. I've added a UIImage to the BubbleAnimation class like so:
#interface BubbleAnimation : UIImageView {
UIImage *emotAnim1;
UIImage *emotAnim2;
UIImage *heart;
}
#property (nonatomic, retain) UIImage *heart;
- (void)setMood:(NSString *)mood;
#end
And synthesise it in the implementation as usual. Then I set the heart to the correct colour in the setMood method:
- (void)setMood:(NSString *)mood {
if ([mood isEqualToString:kGood]) {
emotAnim1 = [UIImage imageNamed:#"Good1.png"];
emotAnim2 = [UIImage imageNamed:#"Good2.png"];
self.heart = [UIImage imageNamed:#"HeartRed.png"];
}
else if ...
In IB, I've added a hidden UIImageView object and linked it to a UIImageView IBOutlet in my ViewController called bubbleHeart. When the thought bubble appears, I use the following code to display the animations and the heart:
[bubbleAnimation setMood:charachter.mood];
self.bubbleHeart.image = bubbleAnimation.heart;
bubbleAnimation.animationDuration = kAnimationDuration;
bubbleAnimation.animationRepeatCount = kAnimationRepeatCount;
[bubbleAnimation startAnimating];
bubbleHeart.hidden = FALSE;
The problem is, the animation appears, but the little heart doesn't. I've tried various approaches - I created the UIImageView in the BubbleAnimation class, instead of using a UIImage, and tried initialising it in various different ways, but no joy. If I call something like self.bubbleHeart = [[UIImageView alloc] initWithImage:bubbleAnimation.heart]; then presumably I'm reinitialising the variable so that doesn't work. Any idea why it's not appearing?
Many thanks!
Michael
In your original code, you never allocated a new instance of BubbleAnimation, and that's why it worked fine.
You have an object in the XIB file which is the BubbleAnimation object, and that's all fine, but the problem is that you're allocating a new instance of BubbleAnimation instead of using the one you have in your revised code.
Even if you were to use the one you have, you'd have to change the init method, since the system will call initWithCoder:, not initWithMood:.
The best thing is most likely to change the init function to something else, e.g. a setMood:, which modifies the image view. Then you re-use the same image view every time.
Then you'd just set it up using something like...
[bubbleAnimation setMood:character.mood];
bubbleAnimation.animationDuration = 0.5;
bubbleAnimation.animationRepeatCount = 5;
[bubbleAnimation startAnimating];
Related
I need to add a save extension selector with a text label next to it to my NSSavePanel. In the screenshot attached I try to demonstrate that I succeeded in adding an NSComboBox to my panel with the function setAccessoryView. However I have no idea how to create a custom NSView, which includes both an NSComboBox and an NSTextView or equivalent. I found no tutorials on the internet (or if I found one it was extremely outdated) showing how to create custom NSViews in objective-C in Cocoa on MacOS.
How can I create a custom NSView containing a combobox and a text label? Or how can I add two "stock" NSViews to the same NSSavePanel? Please be as detailed in your answer as possible, as I have very limited objective-c experience.
You asked how to create an NSView in Objective-C with an NSTextField and an NSComboBox as subviews.
Basically, you could define them in Interface Builder and programmatically set the resulting view in Objective-C as the accessoryView of the NSSavePanel. Alternatively, the custom NSView could be created entirely in Objective-C, which is probably the easier option here.
After instantiating an NSView, you can use addSubview: to add an NSTextField and an NSComboBox accordingly. Then you can use NSLayoutConstraints to set up Auto Layout, which takes care of sizing the accessoryView and arranging the subviews properly based on the width of the dialog.
If you create the views programmatically and use Auto Layout, you must explicitly set translatesAutoresizingMaskIntoConstraints to NO.
Should you want to set the allowedContentTypes, a textual mapping of the displayed extension to UTType via a NSDictionary might be useful.
If you set the delegate of the NSComboBox to self, then you will be informed about changes of the user selection in the NSComboBox via comboBoxSelectionDidChange:.
If the things discussed are implemented appropriately in code, it might look something like this for a self-contained example:
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "ViewController.h"
#interface ViewController () <NSComboBoxDelegate>
#property (nonatomic, strong) NSSavePanel *savePanel;
#property (nonatomic, strong) NSDictionary<NSString *, UTType*> *typeMapping;
#end
#implementation ViewController
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_typeMapping = #{
#"jpeg": UTTypeJPEG,
#"png": UTTypePNG,
#"tiff": UTTypeTIFF
};
}
return self;
}
- (NSView *)accessoryView {
NSTextField *label = [NSTextField labelWithString:#"Filetypes:"];
label.textColor = NSColor.lightGrayColor;
label.font = [NSFont systemFontOfSize:NSFont.smallSystemFontSize];
label.alignment = NSTextAlignmentRight;
NSComboBox *comboBox = [NSComboBox new];
comboBox.editable = NO;
for (NSString *extension in self.typeMapping.allKeys) {
[comboBox addItemWithObjectValue:extension];
}
[comboBox setDelegate:self];
NSView *view = [NSView new];
[view addSubview:label];
[view addSubview:comboBox];
comboBox.translatesAutoresizingMaskIntoConstraints = NO;
label.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:#[
[label.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-12],
[label.widthAnchor constraintEqualToConstant:64.0],
[label.leadingAnchor constraintEqualToAnchor:view.leadingAnchor constant:0.0],
[comboBox.topAnchor constraintEqualToAnchor:view.topAnchor constant:8.0],
[comboBox.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:8.0],
[comboBox.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:-8.0],
[comboBox.trailingAnchor constraintEqualToAnchor:view.trailingAnchor constant:-20.0],
]];
return view;
}
- (void)comboBoxSelectionDidChange:(NSNotification *)notification {
NSComboBox *comboBox = notification.object;
NSString *selectedItem = comboBox.objectValueOfSelectedItem;
NSLog(#"### set allowedContentTypes to %# (%#)", selectedItem, self.typeMapping[selectedItem]);
[self.savePanel setAllowedContentTypes:#[ self.typeMapping[selectedItem] ]];
}
- (IBAction)onSave:(id)sender {
NSWindow *window = NSApplication.sharedApplication.windows.firstObject;
self.savePanel = [NSSavePanel new];
self.savePanel.accessoryView = [self accessoryView];
[self.savePanel beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
if (result != NSModalResponseOK) {
return;
}
NSURL *fileURL = self.savePanel.URL;
NSLog(#"### selectedFile: %#", fileURL);
}];
}
- (void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
}
#end
Finally, a screenshot of the above demo code in action looks like this:
Press Cmd-N to add a new file to your project. Choose a View file to add a xib file that has a custom view.
Open the xib file and add the controls to the custom view. Press the Add button in the project window toolbar to access the user interface elements.
Use the NSNib class to load the xib file and get the custom view.
Is there a way to change MKAnnotationView style (like from red label with number to green colored label with number).
I want to change this style according to distance from target. My annotation is moving, with user.
I dont want to use remove / add annotation, because it causes "blinking".
Can it be done someway?
UPDATE:
I am adding code, how I am doing it right now
MKAnnotationView *av = [mapView viewForAnnotation:an];
if ([data->type isMemberOfClass:[UserAnnotationImage class]])
{
UIImage *img = [UIImage imageNamed: ((UserAnnotationImage *)data->type)->url];
[av setImage:img];
}
else if ([data->type isMemberOfClass:[UserAnnotationLabel class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationLabel *)data->type)->lbl;
av.frame = ((UserAnnotationLabel *)data->type)->lbl.frame;
}
else if ([data->type isMemberOfClass:[UserAnnotationView class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationView *)data->type)->view;
av.frame = ((UserAnnotationView *)data->type)->view.frame;
}
Sadly, its not working :(
Yes, basically you get a reference to the annotation view and update its contents directly.
Another way, if you have a custom annotation view class, is to have the annotation view monitor the changes it is interested in (or have something outside tell it) and update itself.
The first approach is simpler if you are using a plain MKAnnotationView or MKPinAnnotationView.
Wherever you detect that a change to the view is needed, get a reference to the view by calling the map view's viewForAnnotation instance method. This is not the same as calling the viewForAnnotation delegate method.
Once you have a reference to the view, you can modify as needed and the changes should appear immediately.
An important point is that the logic you use to update the view outside the delegate method and the logic you have in the viewForAnnotation delegate method must match. This is because the delegate method may get called later (after you've updated the view manually) by the map view and when it does, the code there should take the updated data into account.
The best way to do that is to have the annotation view construction code in a common method called both by the delegate method and where you update the view manually.
See change UIImage from MKAnnotation in the MKMapView for an example that updates just the annotation view's image.
For an example (mostly an idea for an approach) of updating the view using a custom annotation view class, see iPad Mapkit - Change title of "Current Location" which updates the view's pin color periodically (green, purple, red, green, purple, red, etc).
There are too many unknowns in your code to explain why it doesn't work. For example:
What is data? Is it annotation-specific (is it related to an)? What is type? Does it change after the annotation has been added to the map?
Why is data storing entire view objects like a UILabel or UIView instead of just the underlying data that you want to show in those views?
imageNamed requires the image to be a resource in the project (not any arbitrary url)
Don't use a tag of 0 (that's the default for all views). Start numbering from 1.
You get a view using viewWithTag but then replace it immediately with another view.
I'll instead give a more detailed but simple(r) example...
Assume you have an annotation class (the one that implements MKAnnotation) with these properties (in addition to coordinate, title, and subtitle):
#property (nonatomic, assign) BOOL haveImage;
#property (nonatomic, copy) NSString *labelText;
#property (nonatomic, copy) NSString *imageName;
#property (nonatomic, assign) CLLocationDistance distanceFromTarget;
To address the "important point" mentioned above (that the viewForAnnotation delegate method and the view-update-code should use the same logic), we'll create a method that is passed an annotation view and configures it as needed based on the annotation's properties. This method will then be called both by the viewForAnnotation delegate method and the code that manually updates the view when the annotation's properties change.
In this example, I made it so that the annotation view shows the image if haveImage is YES otherwise it shows the label. Additionally, the label's background color is based on distanceFromTarget:
-(void)configureAnnotationView:(MKAnnotationView *)av
{
MyAnnotationClass *myAnn = (MyAnnotationClass *)av.annotation;
UILabel *labelView = (UILabel *)[av viewWithTag:1];
if (myAnn.haveImage)
{
//show image and remove label...
av.image = [UIImage imageNamed:myAnn.imageName];
[labelView removeFromSuperview];
}
else
{
//remove image and show label...
av.image = nil;
if (labelView == nil)
{
//create and add label...
labelView = [[[UILabel alloc]
initWithFrame:CGRectMake(0, 0, 50, 30)] autorelease];
labelView.tag = 1;
labelView.textColor = [UIColor whiteColor];
[av addSubview:labelView];
}
if (myAnn.distanceFromTarget > 100)
labelView.backgroundColor = [UIColor redColor];
else
labelView.backgroundColor = [UIColor greenColor];
labelView.text = myAnn.labelText;
}
}
The viewForAnnotation delegate method would look like this:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotationClass class]])
{
static NSString *myAnnId = #"myann";
MKAnnotationView *av = [mapView dequeueReusableAnnotationViewWithIdentifier:myAnnId];
if (av == nil)
{
av = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:myAnnId] autorelease];
}
else
{
av.annotation = annotation;
}
[self configureAnnotationView:av];
return av;
}
return nil;
}
Finally, the place where the annotation's properties change and where you want to update the annotation view, the code would look something like this:
ann.coordinate = someNewCoordinate;
ann.distanceFromTarget = theDistanceFromTarget;
ann.labelText = someNewText;
ann.haveImage = YES or NO;
ann.imageName = someImageName;
MKAnnotationView *av = [mapView viewForAnnotation:ann];
[self configureAnnotationView:av];
Well, here's the situation:
I've got...
a custom class with an UIImageView-property, let's call it the Enemy-class
a ViewController
a NSMutableArray to help create multiple instances of Enemy (called enemies)
and what I want:
be able to create an unlimited amount of Enemy-Instances through a method in my ViewController (like [self spawnEnemy];, where self is the ViewController)
and, subsequently, display the UIImageView property (let's call it "enemyImage") on the view that is controlled by my ViewController
I've tried something like this:
-(Enemy *) spawnEnemy
{
Enemy *tempEnemy = [Enemy new];
[enemies addObject:(Enemy*)tempEnemy];
[self.view addSubview:(UIImageView*)[[enemies objectAtIndex:[enemies count]] enemyImage]];
//randomLocation is declared in the Enemy-Class and just assigns a random
//CGPoint to self.enemyImage.center
[[enemies objectAtIndex:[enemies count]] randomLocation];
return [[enemies objectAtIndex:[enemies count]]createEnemy];
}
This runs without errors, randomLocation gets called (tried with NSLog), AND if I do something like this in another Method of ViewController:
[[self spawnEnemy] enemyTestMethod];
enemyTestMethod is being executed as well.
But still, no enemieViews are displayed on the screen...
What am I doing wrong?
Thank you so much for your help and time.
==== Edit ====
Here's the relevant code from Enemy.h/Enemy.m:
#interface Enemy : NSObject
{
UIImageView *enemyImage;
}
#property (nonatomic, retain) IBOutlet UIImageView *enemyImage;
-(Enemy*) createEnemy;
//Enemy.m
#implementation Enemy
#synthesize enemyImage, speed;
-(Enemy *) createEnemy
{
self.enemyImage = [[UIImageView alloc] init];
[self.enemyImage setImage:[UIImage imageNamed:#"enemy.png"]];
return self;
}
I also corrected the last line in the spawnEnemy-Method to properly send createEnemy.
You don't include the code in Enemy where you alloc/init the UIImageView property. Unless this code explicitly specifies a CGRect with the size and origin that you want, the view will be initialized with CGRectZero, which means even if you're correctly adding the subview (and it looks like you are) you still won't see it anywhere.
Post the Enemy code, and the problem will probably be immediately apparent.
Have you called -createEnemy before you added them to your view?
OK, you've got this checked.
Then maybe you should check as #MusiGenesis suggested.
To do this, you need to inspect the properties of your enemyImage.
You can do this in either of the following ways:
print its frame.size by:
NSLog(#"enemyImage frame size: %#", NSStringFromCGRect(enemy.enemyImage));
set a breakpoint where you feel great, and check with your debugger:
p (CGRect)[[enemy enemyImage] frame]
I'm really new to app development and I am building an app (that will be more complex but this is just the basis of it which I will later expand). What I have currently is view with a UIImageView and UIButton, code below. I can get the button to set an image from the resources folder, but what I want is for the button to update the UIImageView with a new image each time it is press e.g. press once = image1, pressed twice = image2...etc From what I have been reading I think I should have the images in an array with a key or something then I can just update it with ++ I know this is really vague but as I said I'm really new so any help would be greatly appreciated!
#interface NextImageViewController : UIViewController {
IBOutlet UIImageView *imageView;
}
- (IBAction) nextImagePush;
#end
#implementation NextImageViewController
- (IBAction) nextImagePush {
UIImage *img = [UIImage imageNamed:#"image1"];
[imageView setImage:img];
}
#interface NextImageViewController : UIViewController {
IBOutlet UIImageView *imageView;
NSInteger imageCount;
NSArray * imageArray;
}
- (IBAction) nextImagePush;
#end
#implementation NextImageViewController
- (IBAction) nextImagePush {
UIImage *img = [UIImage imageNamed:[imageArray objectAtIndex:imageCount]];
[imageView setImage:img];
imageCount++;
if (imageCount >= [imageArray count]){
imageCount = 0;
}
}
- (void) viewDidLoad {
imageArray = [[NSArray alloc]
initWithObjects:#"image1", #"image2", #"image3", nil];
imageCount = 0;
}
Try something like that. You wil have to set the initial image and change the count depending on what you set it.
You can simply create a NSArray or NSMutableArray and fill it with all the images you want to show. Then, using a variable visible at global scope (ie a field of your interface), you can use
[imageView setImage:[arrayOfImages objectAtIndex:(index++ % [arrayOfImages count])]];
This code will also show the first image after the last one is displayed.
A really rudimentary way to do this is set a integer. Define it as 0,
and the first time you click the button you add 1 to it.
This definitely isn't working code, but its just to help you get a start. I strongly suggest a book like Big Nerd Ranch's book on iOS or Beginning IPhone 4 Development.
-(IBAction)pic
if variable == 0 {
//show the first image
variable++;
}
else if variable == 1 {
//show the second image
variable++;
}
I'm creating an instance of a viewController, and then trying to set the text on of it's properties, a UILabel.
BoyController *boyViewController = [[BoyController alloc] initWithNibName:#"BoyView" bundle:nil];
NSString *newText = [astrology getSignWithMonth:month withDay:day];
boyViewController.sign.text = newText;
NSLog(#" the boyviewcontroller.sign.text is now set to: %#", boyViewController.sign.text);
[newText release];
I tried this, but it didn't work...
So I tried the following:
BoyController *boyViewController = [[BoyController alloc] initWithNibName:#"BoyView" bundle:nil];
UILabel *newUILabel = [[UILabel alloc] init];
newUILabel.text = [astrology getSignWithMonth:month withDay:day];
boyViewController.sign = newUILabel;
NSLog(#" the boyviewcontroller.sign.text is now set to: %#", newUILabel.text);
[newUILabel release];
But no avail..
I'm not sure why I can't set the text property of the UILabel "sign" in boyViewController..
The problem here is that the initializer does not actually load the nib file into memory. Instead, loading the nib is delayed until your application requests the view controller's view property. As such, your controller's sign property is null when you access it.
Manually requesting the controller's view property would make your example work...
BoyController *boyViewController = [[BoyController alloc] initWithNibName:#"BoyView" bundle:nil];
[boyViewController view]; // !!!: Calling [... view] here forces the nib to load.
NSString *newText = [astrology getSignWithMonth:month withDay:day];
boyViewController.sign.text = newText;
// and so on...
However, I'd guess that what you're really trying to do is create and configure your view controller before setting it free to do it's own thing. (Perhaps to display it modally, say.) Calling [... view] manually is not going to be a long-term solution.
Better is to set a separate property on your view controller for the label text and then implement viewDidLoad to assign it to the label:
#interface BoyViewController : UIViewController {
IBOutlet UILabel *label;
NSString *labelText;
}
#property(nonatomic, copy)NSString *labelText;
#end
#implementation
#synthesize labelText;
- (void)viewDidLoad
{
[label setText:[self labelText]];
}
// and so on...
#end
This has the added benefit of your label text being reset in case the view is purged during a low memory event.
Did you bind your outlets at Interface Builder?
It seems that you need to bind sign outlet of the first example into Interface Builder in order to actually set that text to whatever you want.
Once you bind your outlet to the actual UI component at Interface Builder, then you should be able to do something like:
NSString *newText = [astrology getSignWithMonth:month withDay:day];
[[boyViewController sign] setText:newText];
This is what you need to know about binding.
Your second example does not make sense at all to me.