How to programmatically add a label in a custom subview? - ios

I'm designing a card game (cards against humanity) for iOS, there are two types of cards, the whites ones, and the black ones. So I've implemented a CardView class which inherits from UIView, and two class, WhiteCardView and BlackCardView which both inherit from CardView.
As you guess, almost only the color will change between those two, but I still want two different view class for those two (even if they are almost the same at the moment). So, I have four questions about that.
EDIT: I mnage to find out some answer by myself, but for the last one, I'm lost.
1.I need advice on my implementation, I don't know if I'm doing it right to set the colors of the cards which is stored in CardView but set in the init of my WhiteCardView. I think I have to override only one init, but I don't know which one. So if someone can confirm or correct what I already did, that would be great. Thanks
Here is my CardView code:
IB_DESIGNABLE
#interface CardView : UIView
-(void)customDrawUI;
-(void)setData;
#property (nonatomic) UIColor* IBInspectable secondaryColor;
#property (nonatomic) UIColor* IBInspectable primaryColor;
#property(nonatomic) UILabel* txtLabel;
#end
#implementation CardView
-(void)customDrawUI{
self.layer.cornerRadius=5.0;
self.backgroundColor = self.primaryColor;
}
-(void)setData{
self.txtLabel.text = #"some text";
}
#end
And the WhiteCardView code:
IB_DESIGNABLE
#interface WhiteCardView : CardView
#end
#implementation WhiteCardView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect myFrame = self.bounds;
CGContextSetLineWidth(context, 1);
CGRectInset(myFrame, 5, 5);
[self.secondaryColor set];
UIRectFrame(myFrame);
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self) {
self.primaryColor= [UIColor whiteColor];
self.secondaryColor= [UIColor blackColor];
[super customDrawUI];
[super setData];
}
return self;
}
#end
2.In interface builder (where I correctly define the custom class for each of my cards), my views aren't live rendered, the code in my drawRect method WhiteCardView is executed but I can see no change.
And when I run my app, I get the same result. Just white rectangle, without any rounded angle.
Any idea why there is no changing in my view?
EDIT: I didn't put the IBDESIGNABLE tag in the right place, now it works !
3.Is there any way to execute that line: self.backgroundColor = self.primaryColor; in the parent drawRect method ? Because it's currently in both: White and BlackCardView, and there is certainly a way to do it in the parent class (I'll add common code to those two class, so I need a way to do it).
Maybe it's possible to do this in the init from CardView, but as it's not working at the moment, I can't test it.
EDIT: I find out, the setBackground couldn't be called from drawRect, so I define a new method in the CardView class to do that. And I call it from the init methods.
4.The most important part: how is it possible to add a label on each card (not via interface builder but via the CardView class), and be able to set its content programmatically?
I defined a property for that label, and I set its value but I have no idea how to render it.

Related

programmatically added uiview does not displayed

I'm trying to create a uiview programmatically and adding it to a stack_view which was also programmatically created and added to the view.
This is the code:
the .h
#interface ViewController : UIViewController
#property (strong, nonatomic, nullable) UIStackView * stack;
#end
the .m
#implementation viewController
#synthesize stack;
- (void) viewDidLoad {
[super viewDidLoad];
CGRect * vframe = self.view.frame;
// - Option 1
stack = [[UIStackView alloc] initWithFrame:CGRectMake(vframe.origin.x, 100,vframe.size.width,300)];
stack.axis = UILayoutConstraintAxisHorizontal;
stack.aligment = UIStackViewAligmentTop;
stack.distribution = UIStackViewDistributionFill;
[self.vew addSubview:stack];
// Option 2
// stack.traslateAutorezisingMaskIntoConstraints = NO;
// [stack.leadingAnchor constraintsEqualToAnchor: self.view.leadingAnchor].active = YES;
// [stack.topAnchor constraintsEqualToAnchor: self.view.topAnchor constant: 100].active = YES;
UIView * pView = [[UIView alloc] init];
pView.backgroundColor = [UIColor blueColor];
[stack addArrangedSubview:pView];
}
#end
This code does not show the view at all, I've tried to prove the option 2 (appeared commented in the code) and it does not work either. It is not supposed that the view, upon inserted in the stack, will get the size of the stack, since the distribution of it is "Fill"?. None of this work either even if I define Pview with frame=CGRectMakeRect(0,0,self.view.frame.size.width,100), for instance.
What am I doing wrong?
EDIT: I already fix the misspelled in the code (the * in the CGRect and the self.vew instead of self.view). I made these mistakes when I was manually copying the code, I did not copy and paste the code; tha's is why is made them and that's why the original code compile well
To diagnose this first test the pView. Init it with a fixed size like CGRectMake(100,100,100,100) and add it directly to the viewContoller's view. You should be able to see it no problem if not you have a deeper issue.
If that goes well try the UIStackView with a fixed size. Color its background to see it better if you need. If you still dont see it then double check it still has the correct frame in viewDidAppear of the viewController. It might have adjusted itself after creation. If that is correct go to Debug -> View Debugging -> Capture View Heirachy in Xcode after you have it up in the simulator. If you dont see it there then there was an issue adding it as a subview (note the typo [self.vew addSubview:stack];)
If that goes well then there is a problem with [stack addArrangedSubview:pView]. Similar to the previous step, loop through all the arrangedSubviews in UIStackView in viewDidAppear of its viewController.
CGRect vframe = self.view.frame;
In viewDidLoad, your self.view.frame has not been calculated yet. You need to do this in viewDidLayoutSubviews or viewDidAppear. both of these will get called multiple times so be careful.
EDIT:
As suggested below by danh (I overlooked it) you should remove * from the above line , also, you have several misspelling in your code, don't know how this code really even compiled for you.

IBDesignable UILabel setting text to NSLocalizedString in drawRect or willMoveToSuperview crashes Xcode

I am trying to retroactively localize an app by subclassing an IBDesignable to have a property like so:
#property IBInspectable NSString *localizedKey;
Then I thought I could easily override the proper function in the UILabel's lifecycle to do this:
if (self.localizedKey) self.text = NSLocalizedString(self.localizedKey, nil);
Here's an example of what I tried to do:
- (void) drawRect:(CGRect)rect {
NSLog(#"roundedlabel");
[super drawRect:rect];
[self.layer setCornerRadius:cornerRadius];
[self.layer setMasksToBounds:YES];
[self.layer setBorderWidth:borderWidth];
[self.layer setBorderColor:[borderColor CGColor]];
self.clipsToBounds = YES;
}
- (void) willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
NSLog(#"roundedlabel");
if (self.localizedKey) self.text = NSLocalizedString(self.localizedKey, nil);
}
I also tried moving the if (self.localizedKey) to various locations in drawRect.
My plan was to then set up the Localizable.strings file manually with known localizedKeys assigned working through the XIB files. However, I am finding two things.
It seems as though the normal methods called in the life cycle
of a view are not being called for my IBDesignable UILabel subclass.
I have entered log statements into drawRect and
willMoveToSuperview, and these log statements never print out.
XCode almost always crashes & immediately closes when I try to open
a xib in Interface Builder that contains this subclassed label (so no error messages).
At run time, when I see a view that involves an affected xib, I don't see any sign of the localized string AND I don't see 'roundedlabel' printed anywhere in my log.
What's going on? In particular:
(1) How come these functions don't run at all for my subclassed IBDesignable UILabel subclass?
(2) Is there a way to do what I want to do?
I'd create an extension on UILabel and use runtime key values in the storyboard / XIB to set the localisation keys.
extension UILabel {
func setHBLocalisationKey(key: String) {
self.text = ...
}
}
or
#implementation UILabel (HBLocalisation)
- (void)setHBLocalisationKey:(NSString *)key {
self.text = NSLocalizedString(key, nil);
}
#end
This will now work on any label without requiring a custom subclass and it won't tamper with the Xcode UI which seems to be causing some issues with your current approach.

Create an mask on an UIView to colorise all UIImageView subviews in black and white

I have an UIView which contains UIImagesViews with images loaded from a remote server.
The images loaded from the server are in color.
I would like create a mask on my UIView which put in black and white all its contents including the images.
But I don't want to have to put the mask on each picture when they are ready.
I tried to create a subclass of UIView :
#interface FriezeView : UIView
#property (nonatomic, strong) UIView *mask;
#property (nonatomic, strong) UIView *container;
#end
The init method of this class:
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
self.container = [[UIView alloc] initWithFrame:frame];
self.mask = [[UIView alloc] initWithFrame:frame];
[self addSubview:self.container];
[self addSubview:self.mask];
}
return self;
}
Then I use the mask UIView to colorise the container UIView.
[_frieze.container addSubview:myImageView];
_frieze.mask.layer.backgroundColor = [[UIColor grayColor] CGColor];
_frieze.mask.backgroundColor = [UIColor grayColor];
But it doesn't work.
Any suggestions ?
No, you can't create a mask that will convert images to B&W.
What you CAN do is to write code that uses a Core Image Filter to convert the images to B&W.
You could use the "CIColorMonochrome" filter with an inputIntensity value 0 1, you could use "CIPhotoEffectMono", or you could use the "Color Controls" filter with an inputSaturation value of 0.
You COULD create a "MonochromeImage" subclass of UIImageView that would take an image as input and convert it to a monochrome equivalent for display.
I have a sample project called CIFilterTest (link) on github that lets you use all these filters and more. It should give you enough information to get started.

Instantiate several draggable tiles from Storyboard - test code and screenshots attached

In Xcode 5 I have created a single view iPhone app and checked it into GitHub.
I would like to display 5 draggable tiles with random letters and their values.
I would prefer to define the tile inside the Storyboard and then instantiate it (5 times) from there - because this way it is easy for me to edit the tile (for example move the labels inside the tile).
Currently my project looks like this (here fullscreen of Xcode):
At the moment have just one tile in the Storyboard and it is draggable:
I have added a Tile class, but don't know how to connect its outlets (because I can only ctrl-drag to the ViewController.h, but not to the Tile.h):
Here Tile.h:
#interface Tile : UIView
// XXX How to connect the outlets?
#property (weak, nonatomic) IBOutlet UIImageView *background;
#property (weak, nonatomic) IBOutlet UILabel *letter;
#property (weak, nonatomic) IBOutlet UILabel *value;
#end
And Tile.m:
#import "Tile.h"
static NSString* const kLetters = #"ABCDEFGHIJKLMNOPQRSTUWVXYZ";
static UIImage* kTile;
static UIImage* kDragged;
#implementation Tile
+ (void)initialize
{
// do not run for derived classes
if (self != [Tile class])
return;
kTile = [UIImage imageNamed:#"tile"];
kDragged = [UIImage imageNamed:#"dragged"];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSString* randomLetter = [kLetters substringWithRange:[kLetters rangeOfComposedCharacterSequenceAtIndex:random()%[kLetters length]]];
int randomInteger = (int)arc4random_uniform(10);
_background.image = kTile;
_letter.text = randomLetter;
_value.text = [NSString stringWithFormat:#"%d", randomInteger];
}
return self;
}
#end
Finally ViewController.m:
#import "ViewController.h"
static int const kNumTiles = 5;
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
for (int i = 0; i < kNumTiles; i++) {
// TODO: add a Tile to the VC here
}
}
- (IBAction)dragTile:(UIPanGestureRecognizer *)recognizer
{
UIView *tile = recognizer.view;
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:[tile superview]];
[tile setCenter:CGPointMake(tile.center.x + translation.x,
tile.center.y + translation.y)];
[recognizer setTranslation:CGPointZero inView:tile.superview];
// TODO: instead of tile.png display dragged.png with shadow
}
}
#end
In the latter file, I don't know
How to instantiate 5 tiles from the Storyboard?
How to display shadow (the dragged.png) when dragging a tile?
UPDATE:
As suggested by Fogmeister (thanks!) I have added a new file Tile.xib
(Selected in Xcode menu: File -> New -> File... -> User Interface -> View)
Then I've set the Custom Class to Tile, but where can I set the (square) dimensions?
(Here fullscreen)
UPDATE 2:
For Tile.xib I've set Size to Freeform, Drawing to Opaque and dimensions to 100 x 100 (here fullscreen):
Why does my custom UIView have white corners? How to make the background transparent?
Also I wonder, how to switch of the display of the battery in Interface Builder?
How to instantiate 5 tiles from the storyboard.
I think the thing to realise here is that storyboards are not the solution for everything but rather should be used with the suite of tools that already existed.
For instance. Creating multiple instances of views in this way is not something that can be done very well using Storyboards. Storyboards should be thought of as providing the overall backbone of the app.
To do this I'd do it one of two ways...
First Way
Create a new NIB file called Tile.xib and layout your single Tile view in there. Connect the outlets up to the Tile class file. Now in your view controller you can load the Tile class using the nib for layout...
Tile *tile = [[[NSBundle mainBundle] loadNibNamed:#"Tile" owner:self options:nil] firstObject];
tile.frame = //blah
[self.view addSubview:tile];
Second Way
Or forget the nib and load the Tile view all in code in your Tile.m file. Then load it like...
Tile *tile = [[Tile alloc] initWithFrame:someFrame];
[self.view addSubview:tile];
How to display shadow (the dragged.png) when dragging a tile?
For this you need to set the shadow on the layer of the tile view...
tile.layer.shadowColor = [UIColor blackColor].CGColor;
tile.layer.shadowRadius = 4.0;
// etc...
You can read more about shadows here...
https://developer.apple.com/library/mac/documentation/graphicsimaging/reference/CALayer_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004500-CH1-SW78
Edited after comment
To do this I would have a BOOL property on Tile.h called something like isDragging. Or even an enum called TileState with TileStateDragging and TileStateStatic.
Then have a method...
- (void)setDragging:(BOOL)dragging
{
_dragging = dragging;
if (_dragging) {
//set to the shadow image.
} else {
//set the none shadow image.
}
}
Some other things to note
Currently you have code inside initWithFrame of the Tile class but you are loading the class using a nib (storyboard in this case). This will run the method initWithCoder not initWithFrame so this code will never get run.
If you want to run code when the class is created you might be best using the method awakeFromNib instead.
The problem seems to be solved but I'd still put in my 2 pennies and show you another solution just so that you have a complete picture. You can instantiate a UICollectionView in the storyboard and define a UICollectionViewCell prototype inline (in the storyboard). The cell is also a regular view so you can put your complete tile view hierarchy in there (all in the storyboard). You will then need to define a custom UICollectionViewLayout subclass that will provide layout attributes for the tiles (those can change over time and may be animated just like any other views). The use of collection view will impose a structure on your code that will:
scale to any number of tiles,
support tasks like animated insertion and deletion of the tiles (you still need to write code in your layout subclass but all the APIs are already defined for you and you don't need to invent the design),
separate the layout code from game logic (which is of course not exclusive to this solution but still nice to get without even thinking of it :).
This looks like an overkill for this particular toy project but you might need to consider collection view for more complex situations.

UIControl Lifecycle

Is there a documented Lifecycle for a UIControl somewhere?
Here's why I ask:
Suppose I have a UITextField. I could easily wire up a button that changes the border color like so:
myTextField.layer.borderColor = [[UIColor redColor] CGColor];
Now suppose I have a custom control that's a subclass of UIControl. That same code will not change the border color unless I also issue setNeedsLayout, like so:
[myControl setNeedsLayout];
Is there an event method somewhere that I need to implement to make this work without the setNeedsLayout?
For future generations, here's how I solved the problem on my own.
In my .h file:
#property (nonatomic, strong, setter = setBorderColor:) UIColor *borderColor;
In my .m file:
- (void)setBorderColor:(UIColor *)clr {
borderColor = clr;
myControl.layer.borderColor = borderColor.CGColor;
}
Works like a charm.

Resources