So I'm subclassing SChartCrosshairMultiValueTooltip to implement a custom multi-value (OHLC) tooltip. I can get the OHLC data from the chart OK. But I'm having difficulty understanding how to present the data in the way the Shinobi frameworks intend me to.
Specifically, I do not understand how to use the labels property. I first assumed that it would be pre-populated with labels generated by the superclass calling keyValueDisplayPairsForDataPoint:onSeries:withXAxis:withYAxis:, but that is not correct as the debugger reveals that the labels array is empty.
So I tried calling that method and manually adding UILabels to the labels array in my overridden - (void)setDataPoint:(id<SChartData>)dataPoint fromSeries:(SChartSeries *)series fromChart:(ShinobiChart *)chart method, but nothing is shown. So then I tried adding them as a subviews to self.view. That works, but because I've just added them myself they are not styled according to the tooltip, and anyway that just feels wrong since there's virtually no point subclassing if I have to do everything myself. I also have to ensure [self layoutContents] is not called for this to work, which feels double-wrong.
The code that works as described above looks like this:-
- (void)setDataPoint:(id<SChartData>)dataPoint fromSeries:(SChartSeries *)series fromChart:(ShinobiChart *)chart
{
SChartMultiYDataPoint *dp = (SChartMultiYDataPoint *)dataPoint;
NSDictionary *dict = [self keyValueDisplayPairsForDataPoint:dp onSeries:series withXAxis:chart.xAxis withYAxis:chart.yAxis];
for (UILabel *label in self.labels)
{
[label removeFromSuperview];
}
[self.labels removeAllObjects];
CGFloat y = 0;
const CGFloat kHeight = 25.0f;
for (id key in [dict allKeys])
{
UILabel *keyLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, y, 80, kHeight)];
keyLabel.text = key;
UILabel *valueLabel = [[UILabel alloc] initWithFrame:CGRectMake(80, y, 80, kHeight)];
valueLabel.text = dict[key];
[self.labels addObject:keyLabel];
[self.labels addObject:valueLabel];
y += kHeight;
[self addSubview:keyLabel];
[self addSubview:valueLabel];
}
}
- (void)setPosition:(struct SChartPoint)pos onCanvas:(SChartCanvas *)canvas
{
// [self layoutContents]; // do NOT call this
self.frame = CGRectMake(0, 0, 160, 100);
}
Does anyone know a better way?
Related
I have 2 labels.
First label, named "label" is placed inside every view within the carousel. The string/text of the label is the view's index.
label.text = [[items1 objectAtIndex:index] stringValue];
I also have a second label (outside the carousel) named "outsideLabel".
I want the outsideLabel's string/text to be the view's index aswell (always the view being in front of the carousel).
outsideLabel.text = [[items1 objectAtIndex:index] stringValue];
Somehow I am doing it wrong and wonder how I shall code this in order to show the proper number in outsideLabel's string/text (always the view being in front). The code somewhat shows the correct numbers but get messed up when scrolling backwards in the carousel. The carouseltype is timeMachine.
My current code:
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
//create new view if no view is available for recycling
if (view == nil)
{
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textColor = [UIColor whiteColor];
if (carousel == carousel1)
{
CGRect test = CGRectMake(10, 10, 20, 20);
self.label.frame = test;
}
else {
CGRect test = CGRectMake(50, 40, 40, 40);
self.label.frame = test;
}
[view addSubview:label];
}
else
{
label = [[view subviews] lastObject];
}
if (carousel == carousel1)
{
//items in this array are numbers
outsideLabel.text = [[items1 objectAtIndex:index] stringValue];
label.text = [[items1 objectAtIndex:index] stringValue];
((UIImageView *)view).image = [UIImage imageNamed:[view1background objectAtIndex:index]];
}
else
{
//not relevant....
}
return view;
}
From the code you've provided, it looks like you're not initializing the outsideLabel in the right place. To be safe, you should initialize all your subviews inside the block where you are checking if the view is nil. Another safe convention is to assign tags to all your subviews, so that you can later retrieve them from views that are reused, as in the code below. For easy reference, and to avoid errors, I define constants for these tags at the top of my implementation file, like this:
#define INSIDE_LABEL_TAG 1
#define OUTSIDE_LABEL_TAG 2
This is much safer, since it doesn't depend on the structure of the views, as is the case with your code, where you get the last view:
label = [[view subviews] lastObject];
Try initializing outsideLabel inside that block, and use tags. The pattern used in initialization is identical to that used for the subviews of UITableView cells in a UITableViewDataSource delegate:
(UITableViewCell * _Nonnull)tableView:(UITableView * _Nonnull)tableView
cellForRowAtIndexPath:(NSIndexPath * _Nonnull)indexPath
Here is some pseudo code that shows where I would use tags and initialize the outsideLabel:
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
//create new view if no view is available for recycling
if (view == nil)
{
//Configure the view
...
/* Initialize views for all carousels */
//Initialize the insideLabel and set its tag
...
insideLabel.tag = INSIDE_LABEL_TAG;
//Initialize the outsideLabel and set its tag
...
outsideLabel.tag = OUTSIDE_LABEL_TAG;
if (carousel == carousel1)
{
//Do any carousel-specific configurations
}
//Add all subviews initialized in this block
[view addSubview:label];
[view addSubview:outsideLabel];
}
else
{
//Get the subviews from an existing view
insideLabel = (UILabel *)[view viewWithTag:INSIDE_LABEL_TAG];
outsideLabel = (UILabel *)[view viewWithTag:OUTSIDE_LABEL_TAG];
}
if (carousel == carousel1)
{
//Set the values for each subview
} else {
//Other carousels...
}
return view;
}
It looks to me like you want the "time machine" style carousel. I don't see your code setting the carousel type anywhere. Don't you need to set the carousel type?
My project is design a View with numerous label, image, textField without storyboard or Nib. Do I need manually alloc and add every single thing to the view? I think this is very overkill but I have no idea how to do it any other way. Example:
UILabel *firstLabel =[UILabel alloc] init];
firstLabel.frame = (0,x,20,10) ;
firstLabel.text = ...;
firstLabel.font = ...;
firstLabel.textColor = ...;
.................
[self.view addSubView:firstLabel];
UILabel *secondLabel =[UIlabel alloc] init];
secondLabel.frame = (0,y,20,10);
secondLabel.text = ...;
secondLabel.font = ...;
secondLabel.textColor = ...;
.................
[self.view addSubView:secondLabel];
UILabel *thirdLabel =[UIlabel alloc] init];
thirdLabel.frame = (0,z,20,10);
thirdLabel.text = ...;
thirdLabel.font = ...;
thirdLabel.textColor = ...;
.................
[self.view addSubView:thirdLabel];
Should I put all of them in viewDidLoad or loadView or init method?
Or I just need make a method for CreatLabel and use it again and again? How to do it?
If I understand you correctly you ask how to apply DRY (Don't repeat yourself) to this code.
«Two of more — use a for» Edsger W. Dijkstra
or
«Two of more — use an enumeration» vikingosegundo
- (void)viewDidLoad { // or loadView, see Rob's answer
[super viewDidLoad];
NSArray *properties= #[#{#"color": [UIColor orangeColor], #"text": #"Ceterum censeo"},
#{#"color": [UIColor cyanColor], #"text": #"Carthaginem esse"},
#{#"color": [UIColor purpleColor], #"text": #"delendam"}];
[properties enumerateObjectsUsingBlock:^(NSDictionary *properties, NSUInteger idx, BOOL *stop) {
UILabel *l = [[UILabel alloc] initWithFrame:CGRectMake(0, (idx + 1) * 20, 200, 20)];
l.backgroundColor = properties[#"color"];
l.text = properties[#"text"];
l.font= [UIFont italicSystemFontOfSize:12];
[self.view addSubview:l];
}];
}
Ok, why do I prefer this block-based enumeration here?
Because it has an mutation guard and gives me the correct index for free that I need for the frame.
A C for-loop for (int idx = 0; idx < [properties count]; ++idx) gives me the correct index but I must include an extra statement to get the object NSDictionary *property = properties[idx]; and as it has no mutation guard: it could be changed during iteration which might lead to bad things.
A fast enumeration for(NSDictionary *property in properties) has such a mutation guard and is even slightly faster enumerating than the block enumeration. But it has the big disadvantage that if I need the index, I must call NSUIndex idx = [properties indexForObject:property]; causing a quadratic runtime performance instead of a linear: bye bye, speed advantage. And even worse: if an array contains identical objects it will only find the first one repeatedly — a good chance of creating false data.
Depending on the amount of the code, it might be useful to move this into a helper method — but this is more about taste.
As your question in the end is about readability, I want to share another matter of taste: I like to encapsulate object creation into a distinct scope:
Either by using an implicit block
UILabel *label = ^{
UILabel *l =[[UILabel alloc] initWithFrame:CGRectMake(0, (idx + 1) * 20, 200, 20)];
l.backgroundColor = properties[#"color"];
l.text = properties[#"text"];
l.font= [UIFont italicSystemFontOfSize:12];
return l;
}();
[self.view addSubview:label];
or a statement expression
UILabel *label = ({
UILabel *l =[[UILabel alloc] initWithFrame:CGRectMake(0, (idx + 1) * 20, 200, 20)];
l.backgroundColor = properties[#"color"];
l.text = properties[#"text"];
l.font= [UIFont italicSystemFontOfSize:12];
l;
});
[self.view addSubview:label];
If you're programmatically creating view from scratch, you'd do that in loadView. (See Creating a View Programmatically.) If, however, you have NIB/storyboard for the top level view and you are merely adding subviews, then you could do that in viewDidLoad.
Regarding the creation of a bunch of labels, yes, you could use subroutine. Or, assuming there is a regular pattern that dictates where these are positioned, you might even use a for loop in which you increment the y coordinate or build the constraints. (You can save references to these views in an array or use tag values to keep track of them.) But however you do it, yes, you'd want to minimize the amount of repeated code you write (simplifying life from a maintenance perspective, if nothing else).
It is okay to put in ViewDidLoad() but use custom method if there are large number of labels.
I'm trying to make my View Controllers leaner by abstracting common UI away into UIView subclasses and just instantiating them in my VCs, much like I would create HTML templates in web dev.
Here is my VC:
- (void)addEmptyView {
self.emptyHomeView = [[PLOTEmptyHomeView alloc] initWithFrame:CGRectMake(0, 10, self.view.bounds.size.width, self.view.bounds.size.height - 113) andUser:self.userModel.user];
[self.view addSubview:self.emptyHomeView];
}
And the UIView I subclass and instantiate:
- (id)initWithFrame:(CGRect)frame andUser:(NSDictionary *)user {
self = [super initWithFrame:frame];
if(self){
self.user = user;
[self drawUI];
}
return self;
}
- (void)drawUI {
self.arrowUp = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"arrow-up"]];
CGRect arrowUpFrame = self.arrowUp.frame;
arrowUpFrame.origin.x = self.bounds.size.width - 60;
arrowUpFrame.size.width = 33.75;
arrowUpFrame.size.height = 33;
self.arrowUp.frame = arrowUpFrame;
[self addSubview:self.arrowUp];
self.welcome = [[UILabel alloc] initWithFrame:CGRectMake(0, 167.5, self.bounds.size.width, 22.5)];
NSArray *nameParts = [self.user[#"name"] componentsSeparatedByString:#" "];
self.welcome.text = [NSString stringWithFormat:#"Hey %#", nameParts[0]];
self.welcome.font = [UIFont fontWithName:#"Avenir-Light" size:24];
self.welcome.textColor = [UIColor plotPlaceholderGrey];
self.welcome.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.welcome];
}
Now this code all works fine, but for my own understanding, am I abusing the init method? I'm also aware of layoutSubviews & drawRect in UIView but i'm not sure if I should be using them in the above scenario?
Any pointers are appreciated...
Your not abusing of init method, but you have to declare all your ui objects and all their properties without define the frame and in the layoutsubviews, you define the frame of all your object. It's my way to declare an custom view.
- (id)initWithFrame:(CGRect)frame andUser:(NSDictionary *)user {
self = [super initWithFrame:frame];
if(self){
self.user = user;
self.arrowUp = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"arrow-up"]];
[self addSubview:self.arrowUp];
self.welcome = [[UILabel alloc] init];
NSArray *nameParts = [self.user[#"name"] componentsSeparatedByString:#" "];
self.welcome.text = [NSString stringWithFormat:#"Hey %#", nameParts[0]];
self.welcome.font = [UIFont fontWithName:#"Avenir-Light" size:24];
self.welcome.textColor = [UIColor plotPlaceholderGrey];
self.welcome.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.welcome];
}
return self;
}
- (void)layoutSubviews {
CGRect arrowUpFrame = self.arrowUp.frame;
arrowUpFrame.origin.x = self.bounds.size.width - 60;
arrowUpFrame.size.width = 33.75;
arrowUpFrame.size.height = 33;
self.arrowUp.frame = arrowUpFrame;
self.welcome.frame =CGRectMake(0, 167.5, self.bounds.size.width, 22.5)];
}
Hope it will help you.
Optimally, you can instantiate and add subviews in your method as shown, but do actual layout involving frames in layoutSubviews. The code you have will work as is, but if your initialize would be from a nib or something that doesn't provide the core frame (or autolayout was involved) you may not truly know the proper bounds/frame at init time. Often layout code is dependent on other factors that may change so layoutSubviews is a more proper location.
Have been searching alot for this, but I think I'm doing something stupidly wrong. I have a lot of labels on my second screen, and they all have the same properties. That's why I create them via a method.
In ViewDidLoad I do this:
[self screenTwoLabelMaker:firstNameLabel withFrame:CGRectMake(30, 200, 200, 40) withText:#"First Name"];
That method is this one:
- (UILabel *)screenTwoLabelMaker:(UILabel *)sender withFrame:(CGRect)frame withText:(NSString *)text
{
sender = [[UILabel alloc] init];
sender.text = text;
sender.frame = frame;
sender.font = [UIFont systemFontOfSize:labelFontSize];
sender.textColor = [UIColor grayColor];
[self.scrollView addSubview:sender];
return sender;
}
Then I do this:
NSLog(#"firstNameLabel x: %f y:%f w:%f h:%f", firstNameLabel.frame.origin.x, firstNameLabel.frame.origin.y, firstNameLabel.frame.size.width, firstNameLabel.frame.size.height);
But the result is this:
MyApp[9608:60b] firstNameLabel x: 0.000000 y:0.000000 w:0.000000 h:0.000000
The weird thing is that the label is being put on the screen on the right place with the right text. So everything should be sunny and warm, no? Well, I also call a similar method for buttons, where there is a tag included. That tag is never active on the button (nil), so further programming for that button is killing me. For now I create them all by hand. Do you guys know what I'm doing wrong?
Your method returns your new label, so you need to assign it to the variable that you want to hold the reference. You don't need to pass in the variable to the method as the first thing you do is overwrite it. Use the following code -
firstNameLabel = [self screenTwoMakeLabelwithFrame:CGRectMake(30, 200, 200, 40) text:#"First Name"];
- (UILabel *)screenTwoMakeLabelwithFrame:(CGRect)frame text:(NSString *)text
{
UILabel newLabel = [[UILabel alloc] init];
newLabel.text = text;
newLabel.frame = frame;
newLabel.font = [UIFont systemFontOfSize:labelFontSize];
newLabel.textColor = [UIColor grayColor];
[self.scrollView addSubview:newLabel];
return newLabel;
}
I'm working on an iPad app and at some points, I need to show a popover with options for a user to pick from. For this, I use a UITableView in a UIPopoverController. The problem is that, on an iPad (not on the simulator), when scrolling the tableview, I get a sort of "double vision" effect, where it appears like two sets of of the list exist. One that is stationary, and one that scrolls up and down.
I construct the popover like this:
self.fClassTypeList = [[NSMutableArray alloc] init];
[self.fClassTypeList removeAllObjects];
NSUInteger stringLength = 0;
(populate self.fClassTypeList, and set stringLength to the size of the longest entry)
[self setContentSizeForViewInPopover:CGSizeMake(stringLength * 15.0f, [self.fClassTypeList count] * 30)];
CGFloat tableBorderLeft = 5;
CGFloat tableBorderRight = 5;
CGRect viewFrame = self.view.frame;
viewFrame.size.width -= tableBorderLeft + tableBorderRight; // reduce the width of the table
self.fListOfItems = [[UITableView alloc] initWithFrame:viewFrame style:UITableViewStylePlain];
self.fListOfItems.delegate = self;
self.fListOfItems.dataSource = self;
[self.view addSubview:self.fListOfItems];
I put in the viewDidLayoutSubviews(…) part of the view controller, maybe I should put it somewhere else? I am not sure why this happens on the actual machine, but not the simulator.
-viewDidLayoutSubviews is a weird place to put allocations because that method can be called multiple times. So as far as your main issue goes, I believe you should move your allocations into the -init method, and move your layout code into your -viewWillAppear method.
- (id)init
{
self = [super init];
if (self)
{
self.fClassTypeList = [[NSMutableArray alloc] init];
self.fListOfItems = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.fListOfItems.delegate = self;
self.fListOfItems.dataSource = self;
[self.view addSubview:self.fListOfItems];
}
return self;
}
- (void )viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSUInteger stringLength = 0;
CGFloat tableBorderLeft = 5;
CGFloat tableBorderRight = 5;
CGRect viewFrame = self.view.frame;
viewFrame.size.width -= tableBorderLeft + tableBorderRight; // reduce the width of the table
self.fListOfItems.frame = viewFrame;
[self setContentSizeForViewInPopover:CGSizeMake(stringLength * 15.0f, [self.fClassTypeList count] * 30)];
}
This promotes better memory management.
As an added bonus, I would recommend you refactor the
[self setContentSizeForViewInPopover:CGSizeMake(stringLength * 15.0f, [self.fClassTypeList count] * 30)]; method into a setter method of fClassTypeList. Even better is to simply call -viewWillAppear: in that same setter method instead. This will promote good scalability as you (or someone else) continues to build upon this code later on.
It's a little confusing to see what exactly you're trying to accomplish in this code because it's so hardcoded so let me know if I'm missing the mark you're looking for (w/ an explanation why) and I'll make an edit.
Cheers