Set UIImage based on tag - objective-c

I'm working on a script that loops through an array of LED UIImageView's that are setup in numerical order and are selected by tag. Depending on the step (aka number) it's on the led image is shown as on or off. The main goal of this method is to take the current step and display the "LED on" image while subtracting it by one and display the "LED off" image for the previous step. So, only one LED should be lit up at a time.
Unfortunately, I can only get the "LED on" image to display correctly. All of the LED's in the sequence light up, but they never turn off. My first guess is that I'm not subtracting the NSInterger in the right way. However, when I check the log, everything is where it should be. If the current step is 2, previous is 1. No idea why this isn't working. Thanks!
sequencerLocation and previousLocation are both setup as properties.
- (void)clockLoop:(UInt8)seqClockPulse
{
//cast to an int to use in loop
NSInteger stepCount = sequencerSteps;
//increment sequencer on pulse in
sequencerLocation++;
if(sequencerLocation > stepCount)
{
sequencerLocation = 1;
}
//setup previous step location
previousLocation = (sequencerLocation - 1);
if (previousLocation == 0)
{
previousLocation = stepCount;
}
//change led color in led array
for (UIImageView *led in sequencerLEDArray)
{
if(led.tag == sequencerLocation)
{
UIImageView *previousLed = (UIImageView *)[led viewWithTag:previousLocation];
[previousLed setImage:[UIImage imageNamed:#"images/seq_LED_off.png"]];
NSLog(#"Previous: %d", previousLocation);
UIImageView *currentLed = (UIImageView *)[led viewWithTag:sequencerLocation];
[currentLed setImage:[UIImage imageNamed:#"images/seq_LED_on.png"]];
NSLog(#"Current: %d", sequencerLocation);
}
}
}

//change led color in led array
for (UIImageView *led in sequencerLEDArray)
{
if(led.tag == sequencerLocation)
{
// I THINK the problem is here
// UIImageView *previousLed = (UIImageView *)[led viewWithTag:previousLocation];
// TRY THIS instead
UIImageView *previousLed = [led.superview viewWithTag:previousLocation];
[previousLed setImage:[UIImage imageNamed:#"images/seq_LED_off.png"]];
NSLog(#"Previous: %d", previousLocation);
// HERE you don't need to search for the tag you already tested for it in your if statement
UIImageView *currentLed = (UIImageView *)[led viewWithTag:sequencerLocation];
[currentLed setImage:[UIImage imageNamed:#"images/seq_LED_on.png"]];
NSLog(#"Current: %d", sequencerLocation);
}
}
viewWithTag:
Discussion
This method searches the current view and all of its subviews for the specified view.
So when you search for led by it's tag from itself, it's returning itself, but when you search its brother it's not finding it, that is why I'm suggesting the led.superview as the place to search for your tag, the parent should be able to find its other child.

Related

User defined iOS interface

Introduction
I am writing an app where the interface on a screen can vary depending on user input on the previous screen. What I'm trying to do right now is load up x amount of sliders, where I get x from the previous screen. The real issue I'm having is won't each slider I use have to be named and defined? I've thought of a way to do this but I'm not entirely sure how to implement if you could advise me it would really be appreciated
Pseudo Code
-(void) loadInterface {
// Set up a frame
CGRect myFrame = CGRectMake(10.0f, 100.0f, 250.0f, 25.0f);
for (int i = 0; i < x; i++) {
[self newSlider]
// Retrieve info here about the slider and pass it to newSlider
}
}
-(void) newSlider (info about slider) {
// Here I need the code to load a slider
// If I use something like this:
// UISlider *slider1 = [[UISlider alloc] initWithFrame:myFrame];
// The next time this method is loaded the slider will be overwritten right?
}
You could use an NSMutableDictionary to keep track of all your sliders, although maybe you do not need that at all.
In newSlider you create a slider and you should add it to some view (I assume self) through
[self addSubview:slider];
All sliders thus created will appear at the position where you created them and will not disappear unless you remove them from the view.
You could take this snippet as an example of how you can create a slider:
CGRect frame = CGRectMake(0.0, 0.0, 200.0, 10.0);
UISlider *slider = [[UISlider alloc] initWithFrame:frame];
[slider addTarget:self action:#selector(sliderAction:) forControlEvents:UIControlEventValueChanged];
If you define correctly frame, you can make all of your sliders align nicely in your UI.
When a slider action is carried through by the user, the sliderAction: method is called, which has an argument referring to the slider:
- (void)sliderAction:(UISlider* slider) {
...
}
so you know exactly which slider was actioned and this is generally enough to handle the action.
In any case, as I said, if you want to keep track of all the sliders, add them to a NSDictionary at the moment of creation:
[self.sliderDict setObject:newSlider forKey:#"nth-slider-name"];
You don't need new variables for each slider, because the scope of the UISlider instance you use in your newSlider method is limited to that method. Meaning the variable will be free for use once the program exits the method.
You should also pass which slider it is to that method, this can then be used to position it and to give the view a tag for later use:
-(void) loadInterface {
// Set up a frame
CGRect myFrame = CGRectMake(10.0f, 100.0f, 250.0f, 25.0f);
for (int i = 0; i < x; i++) {
[self newSlider:i]
// Retrieve info here about the slider and pass it to newSlider
}
}
-(void) newSlider:(int)sliderNumber {
UISlider *slider = [[UISlider alloc] initWithFrame:CGRectMake(0, sliderNumber * 100, 320, 50)];
slider.tag = sliderNumber;
[self.view addSubview:slider];
}

Automatically adjust height of a NSTableView

I have asked this question once before, but I'm just not very satisfied with the solution.
Automatically adjust size of NSTableView
I want to display a NSTableView in a NSPopover, or in a NSWindow.
Now, the window's size should adjust with the table view.
Just like Xcode does it:
This is fairly simple with Auto Layout, you can just pin the inner view to the super view.
My problem is, that I can't figure out the optimal height of the table view.
The following code enumerates all available rows, but it doesn't return the correct value, because the table view has other elements like separators, and the table head.
- (CGFloat)heightOfAllRows:(NSTableView *)tableView {
CGFloat __block height;
[tableView enumerateAvailableRowViewsUsingBlock:^(NSTableRowView *rowView, NSInteger row) {
// tried it with this one
height += rowView.frame.size.height;
// and this one
// height += [self tableView:nil heightOfRow:row];
}];
return height;
}
1. Question
How can I fix this? How can I correctly calculate the required height of the table view.
2. Question
Where should I run this code?
I don't want to implement this in a controller, because it's definitely something that the table view should handle itself.
And I didn't even find any helpful delegate methods.
So I figured best would be if you could subclass NSTableView.
So my question 2, where to implement it?
Motivation
Definitely worth a bounty
This answer is for Swift 4, targeting macOS 10.10 and later:
1. Answer
You can use the table view's fittingSize to calculate the size of your popover.
tableView.needsLayout = true
tableView.layoutSubtreeIfNeeded()
let height = tableView.fittingSize.height
2. Answer
I understand your desire to move that code out of the view controller but since the table view itself knows nothing about the number of items (only through delegation) or model changes, I would put that in the view controller. Since macOS 10.10, you can use preferredContentSize on your NSViewController inside a popover to set the size.
func updatePreferredContentSize() {
tableView.needsLayout = true
tableView.layoutSubtreeIfNeeded()
let height = tableView.fittingSize.height
let width: CGFloat = 320
preferredContentSize = CGSize(width: width, height: height)
}
In my example, I'm using a fixed width but you could also use the calculated one (haven't tested it yet).
You would want to call the update method whenever your data source changes and/or when you're about to display the popover.
I hope this solves your problem!
You can query the frame of the last row to get the table view's height:
- (CGFloat)heightOfTableView:(NSTableView *)tableView
{
NSInteger rows = [self numberOfRowsInTableView:tableView];
if ( rows == 0 ) {
return 0;
} else {
return NSMaxY( [tableView rectOfRow:rows - 1] );
}
}
This assumes an enclosing scroll view with no borders!
You can query the tableView.enclosingScrollView.borderType to check whether the scroll view is bordered or not. If it is, the border width needs to be added to the result (two times; bottom and top). Unfortunately, I don't know of the top of my head how to get the border width.
The advantage of querying rectOfRow: is that it works in the same runloop iteration as a [tableView reloadData];. In my experience, querying the table view's frame does not work reliably when you do a reloadData first (you'll get the previous height).
Interface Builder in Xcode automatically puts the NSTableView in an NSScrollView. The NSScrollView is where the headers are actually located. Create a NSScrollView as your base view in the window and add the NSTableView to it:
NSScrollView * scrollView = [[NSScrollView alloc]init];
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:YES];
[scrollView setAutohidesScrollers:YES];
[scrollView setBorderType:NSBezelBorder];
[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSTableView * table = [[NSTableView alloc] init];
[table setDataSource:self];
[table setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle];
[scrollView setDocumentView:table];
//SetWindow's main view to scrollView
Now you can interrogate the scrollView's contentView to find the size of the NSScrollView size
NSRect rectOfFullTable = [[scrollView contentView] documentRect];
Because the NSTableView is inside an NSScrollView, the NSTableView will have a headerView which you can use to find the size of your headers.
You could subclass NSScrollView to update it's superview when the table size changes (headers + rows) by overriding the reflectScrolledClipView: method
I'm not sure if my solution is any better than what you have, but thought I'd offer it anyway. I use this with a print view. I'm not using Auto Layout. It only works with bindings – would need adjustment to work with a data source.
You'll see there's an awful hack to make it work: I just add 0.5 to the value I carefully calculate.
This takes the spacing into account but not the headers, which I don't display. If you are displaying the headers you can add that in the -tableView:heightOfRow: method.
In NSTableView subclass or category:
- (void) sizeHeightToFit {
CGFloat height = 0.f;
if ([self.delegate respondsToSelector:#selector(tableView:heightOfRow:)]) {
for (NSInteger i = 0; i < self.numberOfRows; ++i)
height = height +
[self.delegate tableView:self heightOfRow:i] +
self.intercellSpacing.height;
} else {
height = (self.rowHeight + self.intercellSpacing.height) *
self.numberOfRows;
}
NSSize frameSize = self.frame.size;
frameSize.height = height;
[self setFrameSize:frameSize];
}
In table view delegate:
// Invoke bindings to get the cell contents
// FIXME If no bindings, use the datasource
- (NSString *) stringValueForRow:(NSInteger) row column:(NSTableColumn *) column {
NSDictionary *bindingInfo = [column infoForBinding:NSValueBinding];
id object = [bindingInfo objectForKey:NSObservedObjectKey];
NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
id value = [[object valueForKeyPath:keyPath] objectAtIndex:row];
if ([value isKindOfClass:[NSString class]])
return value;
else
return #"";
}
- (CGFloat) tableView:(NSTableView *) tableView heightOfRow:(NSInteger) row {
CGFloat result = tableView.rowHeight;
for (NSTableColumn *column in tableView.tableColumns) {
NSTextFieldCell *dataCell = column.dataCell;
if (![dataCell isKindOfClass:[NSTextFieldCell class]]) continue;
// Borrow the prototype cell, and set its text
[dataCell setObjectValue:[self stringValueForRow:row column:column]];
// Ask it the bounds for a rectangle as wide as the column
NSRect cellBounds = NSZeroRect;
cellBounds.size.width = [column width]; cellBounds.size.height = FLT_MAX;
NSSize cellSize = [dataCell cellSizeForBounds:cellBounds];
// This is a HACK to make this work.
// Otherwise the rows are inexplicably too short.
cellSize.height = cellSize.height + 0.5;
if (cellSize.height > result)
result = cellSize.height;
}
return result;
}
Just get -[NSTableView frame]
NSTableView is embed in NSScrollView, but has the full size.

Image generator ( generateCGImagesAsynchronouslyForTimes method) doesn't give live results

Pretty new to cocoa development and really stuck with probably a fundamental problem.
So in short, my app UI looks like a simple window with a nsslider at the bottom. What I need is to generate N images and place them, onto N nsviews in my app window.
What it does so far:
I'm clicking on the slider (holding it) and dragging it. While I'm dragging it nothing happens to my views (pictures are not generated). When I release the slider the pictures got generated and my view get filled with them.
What I want:
- I need the views to be filled with pictures as I'm moving the slider.
I figured out the little check box on the NSSlider properties, which is continuous, and I'm using it, but my image generator still doesn't do anything until I release the slider.
Here is my code:
// slider move action
- (IBAction)sliderMove:(id)sender
{
[self generateProcess:[_slider floatValue];
}
// generation process
- (void) generateProcess:(Float64) startPoint
{
// create an array of times for frames to display
NSMutableArray *stops = [[NSMutableArray alloc] init];
for (int j = 0; j < _numOfFramesToDisplay; j++)
{
CMTime time = CMTimeMakeWithSeconds(startPoint, 60000);
[stops addObject:[NSValue valueWithCMTime:time]];
_currentPosition = initialTime; // set the current position to the last frame displayed
startPoint+=0.04; // the step between frames is 0.04sec
}
__block CMTime lastTime = CMTimeMake(-1, 1);
__block int count = 0;
[_imageGenerator generateCGImagesAsynchronouslyForTimes:stops
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,AVAssetImageGeneratorResult result, NSError *error)
{
if (result == AVAssetImageGeneratorSucceeded)
{
if (CMTimeCompare(actualTime, lastTime) != 0)
{
NSLog(#"new frame found");
lastTime = actualTime;
}
else
{
NSLog(#"skipping");
return;
}
// place the image onto the view
NSRect rect = CGRectMake((count+0.5) * 110, 500, 100, 100);
NSImageView *iView = [[NSImageView alloc] initWithFrame:rect];
[iView setImageScaling:NSScaleToFit];
NSImage *myImage = [[NSImage alloc] initWithCGImage:image size:(NSSize){50.0,50.0}];
[iView setImage:myImage];
[self.windowForSheet.contentView addSubview: iView];
[_viewsToRemove addObject:iView];
}
if (result == AVAssetImageGeneratorFailed)
{
NSLog(#"Failed with error: %#", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled)
{
NSLog(#"Canceled");
}
count++;
}];
}
}
If you have any thoughts or ideas, please share with me, I will really appreciate it!
Thank you
In order to make your NSSlider continuous, open your window controller's XIB file in Interface Builder and click on the NSSlider. Then, open the Utilities area
select the Attributes Inspector
and check the "Continuous" checkbox
under the Control header. Once you've done this, your IBAction sliderMove: will be called as the slider is moved rather than once the mouse is released.
Note: Alternatively, with an
NSSlider *slider = //...
one can simply call
[slider setContinuous:YES];

UISegmentedControl with Bezeled Style uncenters titles on Device

for clarification I'll just add 2 overlaid Screenshots, one in Interface Builder, the other on the device.
The lower UISegmentedControl is fresh out of the library with no properties edited, still it looks different on the Device (in this case a non-Retina iPad, though the problem is the same for Retina-iPhone) (Sorry for the quick and dirty photoshopping)
Any ideas?
EDIT: I obviously tried the "alignment" under "Control" in the Utilities-Tab in Interface Builder. Unfortunately none of the settings changed anything for the titles in the UISegment. I don't think they should as they are not changing titles in Interface Builder either.
EDIT2: Programmatically setting:
eyeSeg.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
doesn't make a difference either.
Found the Problem "UISegmentedControlStyleBezeled is deprecated. Please use a different style."
See also what-should-i-use-instead-of-the-deprecated-uisegmentedcontrolstylebezeled-in-io
Hmm...have you checked the alignment? Maybe that's the case.
You can recursively search the subviews of the UISegmentedControl view for each of the UILabels in the segmented control and then change the properties of each UILabel including the textAlignment property as I've shown in a sample of my code. Credit to Primc's post in response to Change font size of UISegmented Control for suggesting this general approach to customizing the UILabels of a UISegmentedControl. I had been using this code with the UISegmentedControlStyleBezeled style by the way even after it was deprecated although I have recently switched to UISegmentedControlStyleBar with an adjusted frame height.
- (void)viewDidLoad {
[super viewDidLoad];
// Adjust the segment widths to fit the text. (Will need to calculate widths if localized text is ever used.)
[aspirationControl setWidth:66 forSegmentAtIndex:0]; // Navel Lint Collector
[aspirationControl setWidth:48 forSegmentAtIndex:1]; // Deep Thinker
[aspirationControl setWidth:49 forSegmentAtIndex:2]; // Mental Wizard
[aspirationControl setWidth:64 forSegmentAtIndex:3]; // Brilliant Professor
[aspirationControl setWidth:58 forSegmentAtIndex:4]; // Nobel Laureate
// Reduce the font size of the segmented aspiration control
[self adjustSegmentText:aspirationControl];
}
- (void)adjustSegmentText:(UIView*)view {
// A recursively called method for finding the subviews containing the segment text and adjusting frame size, text justification, word wrap and font size
NSArray *views = [view subviews];
int numSubviews = views.count;
for (int i=0; i<numSubviews; i++) {
UIView *thisView = [views objectAtIndex:i];
// Typecast thisView to see if it is a UILabel from one of the segment controls
UILabel *tmpLabel = (UILabel *) thisView;
if ([tmpLabel respondsToSelector:#selector(text)]) {
// Enlarge frame. Segments are set wider and narrower to accomodate the text.
CGRect segmentFrame = [tmpLabel frame];
// The following origin values were necessary to avoid text movement upon making an initial selection but became unnecessary after switching to a bar style segmented control
// segmentFrame.origin.x = 1;
// segmentFrame.origin.y = -1;
segmentFrame.size.height = 40;
// Frame widths are set equal to 2 points less than segment widths set in viewDidLoad
if ([[tmpLabel text] isEqualToString:#"Navel Lint Collector"]) {
segmentFrame.size.width = 64;
}
else if([[tmpLabel text] isEqualToString:#"Deep Thinker"]) {
segmentFrame.size.width = 46;
}
else if([[tmpLabel text] isEqualToString:#"Mental Wizard"]) {
segmentFrame.size.width = 47;
}
else if([[tmpLabel text] isEqualToString:#"Brilliant Professor"]) {
segmentFrame.size.width = 62;
}
else {
// #"Nobel Laureate"
segmentFrame.size.width = 56;
}
[tmpLabel setFrame:segmentFrame];
[tmpLabel setNumberOfLines:0]; // Change from the default of 1 line to 0 meaning use as many lines as needed
[tmpLabel setTextAlignment:UITextAlignmentCenter];
[tmpLabel setFont:[UIFont boldSystemFontOfSize:12]];
[tmpLabel setLineBreakMode:UILineBreakModeWordWrap];
}
if (thisView.subviews.count) {
[self adjustSegmentText:thisView];
}
}
}
The segmented control label text has an ugly appearance in IB but comes out perfectly centered and wrapped across 2 lines on the device and in the simulator using the above code.

How do I pass the user touch from one object to another object?

I am developing an application that allows the user at a certain point to drag and drop 10 images around. So there are 10 images, and if he/she drags one image onto another, these two are swapped.
A screenshot of how this looks like:
So when the user drags one photo I want it to reduce its opacity and give the user a draggable image on his finger which disappears again if he drops it outside of any image.
The way I have developed this is the following. I have set a UIPanGesture for these UIImageViews as:
for (UIImageView *imgView in editPhotosView.subviews) {
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(photoDragged:)];
[imgView addGestureRecognizer:panGesture];
imgView.userInteractionEnabled = YES;
[panGesture release];
}
Then my photoDragged: method:
- (void)photoDragged:(UIPanGestureRecognizer *)gesture {
UIView *view = gesture.view;
if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imgView = (UIImageView *)view;
if (gesture.state == UIGestureRecognizerStateBegan) {
imgView.alpha = 0.5;
UIImageView *newView = [[UIImageView alloc] initWithFrame:imgView.frame];
newView.image = imgView.image;
newView.tag = imgView.tag;
newView.backgroundColor = imgView.backgroundColor;
newView.gestureRecognizers = imgView.gestureRecognizers;
[editPhotosView addSubview:newView];
[newView release];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [gesture translationInView:view.superview];
[view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
[gesture setTranslation:CGPointZero inView:view.superview];
}
else { ... }
}
}
Thus as you see I add a new UIImageView with 0.5 opacity on the same spot as the original image when the user starts dragging it. So the user is dragging the original image around. But what I want to do is to copy the original image when the user drags it and create a "draggable" image and pass that to the user to drag around.
But to do that I have to pass the user touch on to the newly created "draggable" UIImageView. While it's actually set to the original image (the one the user touches when he starts dragging).
So my question is: How do I pass the user's touch to another element?.
I hope that makes sense. Thanks for your help!
Well, you can pass the UIPanGestureRecognizer object to another object by creating a method in your other object which takes the gesture recognizer as a parameter.
- (void)myMethod:(UIPanGestureRecognizer *)gesture
{
// Do stuff
}
And call from your current gesture recognizer using....
[myOtherObject myMethod:gesture];
Not entirely sure I'm understanding your question here fully. :-/
Maybe:
[otherObject sendActionsForControlEvents:UIControlEventTouchUpInside];
Or any other UIControlEvent
In the end I decided to indeed drag the original image and leave a copy at the original place.
I solved the issue with the gesture recognizers I was having by re-creating them and assigning them to the "copy", just like PragmaOnce suggested.