TweetFollow Us on Twitter

The Road to Code: More Cocoa Bits

Volume Number: 24 (2008)
Issue Number: 06
Column Tag: The Road to Code

The Road to Code: More Cocoa Bits

Interface Builder UI tweaking and introduction to table view

by Dave Dribin

Customizing the User Interface

Last month in The Road to Code we went over how to customize application behavior using notifications and delegates. This month we're going start off talking about how you can customize the user interface in Interface Builder. There's a lot you can tweak without writing a line of code.

Window Resizing

One important aspect of user interface design is how the window and its views and controls react to resizing. Improper resizing may confuse the user and lead to a bad user experience. You control how resizing affects each and every view and control within Interface Builder. Unfortunately, the defaults are rarely useful. For example, if we resize our Hello World window with the default settings, it will look similar to Figure 1.

Figure 1: Improper window resizing

As you resize, all the controls will be bunched up in the upper-left portion of the window. This isn't really a good use of the extra space. The text fields on the right should expand to the full width of the window, and the Calculate button should also stick to the right side of the window. Thus we want the window to look similar to Figure 2 when resized.

Figure 2: Proper window resizing

I've also kept the Calculate button on the bottom of the window. You could also keep it pinned towards the top, but for demonstration purposes, I'm going to keep it on the bottom. In order to make our window properly resize like this, we need to open Interface Builder.

Select the text field next to the Rectangle Width label. Next, open up the Inspector panel and select the Size tab; it's third from the left with the ruler icon. It should look similar to Figure 3.

Figure 3: Size panel of the Inspector

This panel contains information about the size of the selected view. The Autosizing section is the part that dictates how the control behaves when the window is resized. The left portion of the autosizing section is called the springs and struts. There are six lines you can click on and activate, two on the inside and four on the outside. The two on the inside are called springs and the four on the outside are called struts as noted in Figure 4.

Figure 4: Autosizing springs and struts

When a specific string or strut is activated, it is colored red and has a solid line. When not activated, it is colored light red with a dotted line. An activated spring indicates that the width or height of the view expands and contracts in proportion to its superview. An activated strut indicates that the view maintains a fixed distance between its edge and the same edge of its superview. The right portion of autosizing section is an animated preview showing you how the settings will affect the view. You should see the animation change as you activate and deactivate springs and struts.

The animated preview is handy, but sometimes you need to actually run the application and try resizing the window to verify your settings. While you can go back to Xcode and run the application, Interface Builder allows you to simulate the interface without switching to Xcode. Choose the File > Simulate Interface menu or Command-R to do this. While simulating the interface, you may resize the window and watch how the views react. When you are done, you need to quit the Cocoa Simulator using the menu or Command-Q. Between the animation preview and the simulator, you should be able to find the correct springs and struts settings for your application without ever switching to Xcode.

The default setting, as shown in Figure 4 above, is to have the top and left struts activated and no springs activated. This explains why all of our controls are bunched up in the upper left portion of the window. For our text fields, we want to activate the horizontal spring and the left, right, and top struts. This will cause them to resize horizontally while staying pinned on the left, right, and top. The final settings should look like Figure 5.

Figure 5: Autosizing for text fields

For the Calculate button, we don't want any springs activated. The default settings have only the top and left struts activated, so we need to change this again. We want only the right and bottom struts activated. This will keep the button pinned to the lower right portion of the window. The final settings should look like Figure 6.

Figure 6: Autosizing for button

Try simulating the user interface, and see if it behaves correctly. It should now work as shown in Figure 2. Once it works, save the nib file, switch back to Xcode, and run the application. Again, the controls should resize appropriately.

There's one final aspect of our resizing that's not quite right. While making the window bigger works fine, it is possible to resize the window so small such that not all of our controls will fit in the window. To prevent this, we can specify a minimum size for our window. The window's sizes can be viewed and set in the Size pane of the Inspector window, as well. If you select the window by clicking on its title bar the Size pane should look similar to Figure 7.

Figure 7: Window size

You can see that our window has neither a minimum nor maximum size. Click on the Has Minimum Size checkbox to enable a minimum size. I think the window as currently laid out makes a nice minimum size, so click the Use Current button to set the minimum size to the window's current size. If you now simulate the user interface, you should be able to resize the window larger and smaller, as before, but you should not be allowed to resize it any smaller than the defined size. This is perfect, so save the nib file. If you want, you may provide a maximum size, too. I'm going to keep the maximum size disabled so the user can make the window as large as they want.

Resizing the window in Interface Builder

You can now properly resize the window in a running application, but what about resizing the window in Interface Builder? You can resize the window by dragging the resize corner, just as in a running application. However, the controls inside the window do not resize according to their autosizing settings. This may be fine if you are resizing the window to make room for more views. However, if you just want to make the window larger or smaller, it can be a chore to go through and manually resize each view and control after resizing the window. Fortunately there is a trick. If you hold down the Command key while resizing the window in Interface Builder, the controls will also resize according to their autosizing settings. If, for some reason, you forget to hold down Command while resizing the window, Interface Builder allows you to undo the resize. Then you can redo it while holding down the Command key.

Formatters

By default, text fields can display and accept any text. Sometimes, however, the text fields need stricter formatting. Our rectangle program is a perfect example. The rectangle values are all floating point numbers. Maybe we would like to control how many decimal points are displayed. Also, our user interface doesn't stop the user from entering letters into the width and height fields. Other applications may like to format numbers in text fields as currency or percentages.

Luckily Cocoa has just the solution for us, called formatters. Formatters can be added to any text field to customize how objects are presented to the user. Interface Builder comes with two built-in types of formatters that you will use frequently: number formatters and date formatters. Number formatters take a number, usually a floating point number, and format it better. They are quite flexible and can be used to customize how decimal points are used, as well as allowing you to add comma separators for numbers over a thousand and format numbers as currency and percentages. To apply a formatter to a text field, drag it from the Library panel on top of a text field, as in Figure 8. Date formatters are used to customize the presentation of NSDate objects.

Figure 8: Adding a number formatter

The formatter is now attached to the text field. Interface Builder shows this by adding an icon of the formatter just below the text field as shown in Figure 9.

Figure 9: Text field with an attached formatter

By clicking on the formatter icon, you can change its attributes. Click on the formatter now, and the Attributes pane of the Inspector panel should update as in Figure 10. The Style pull-down menu offers various pre-configured formats. Set the Style to be Decimal.

Figure 10: Number formatter attributes

Now add a formatter with the Decimal style to the other numeric text fields, as well. If you would like, you can customize the decimal field to show one digit after the decimal point by setting the Minimum Fraction Digits to 1. When you run the application with formatters in place, numbers over 1,000 should be formatted appropriately as shown in Figure 11.

Figure 11: An application with formatters

Formatters not only convert from an object to text, but they also convert text back into an object. For number formatters, they are converted to NSNumber objects. The most important benefit we get out of using number formatters for input is that it restricts the user input to numbers only. If the user enters letters, the formatter will remove them from the text field.

With the autosizing properly set up, the minimum window size in place, and formatters on the text fields, our user interface is now nicely polished. And we didn't have to write a single line of code. All the tweaking was done using Interface Builder. The final code for this project may be found on the MacTech website.

Table Views

I'd like to shift gears a bit and get back to code and talk about a popular control called a table view. A table view is a control, like buttons and text fields, and is implemented by the NSTableView class. It displays rows and columns of information, similar to a spreadsheet application. It is a powerful control that is used in many applications.

Because a table view may display large amounts of information, the view class itself does not hold all the data. Instead, the table view requires a separate class that provides the data called a data source. Whenever the table view needs to display data for a specific row and column, it asks the data source for this information.

In last month's article, we talked about notifications and delegates as a way to customize user interface behavior in code, and data sources are very similar to delegates. A data source class must also implement specific methods. A table view may have both a delegate and a data source. The table view's delegate is only used for customizing the behavior of the view itself, just like the window and application delegates from last month. The same class may even implement the table view data source and delegate methods.

Let's go over a simple example of a table view and its data source. We're going to ignore the table view delegate and use the default behavior. Create a new Cocoa Application project in Xcode and open up its MainMenu.nib file in Interface Builder. You should have a blank window. Now, find the table view in the Library panel and drag it to the window. Resize the table view so that there is a small border around all sides. Finally, active all the springs and struts for the table view. This should allow the table view to expand vertically and horizontally as the window is resized.

Next, let's look at the attributes for a table view that are available in Interface Builder. Because a table view is enclosed in a scroll view, it can sometimes be a bit tricky to choose the table view. With the Attributes pane of the Inspector panel selected, click on the table view. Most likely, this will select the outer scroll view, which will have attributes that look like Figure 12. You may customize how the scroll bars, or scrollers, are displayed from this panel. The default is fine for us.

Clicking on the table view again should select the table view itself, and the Attributes pane should look similar to Figure 13. Now you can customize attributes of the table view itself, such as how many columns it has and various selection possibilities. Again, we're going to leave the default settings, but we will be coming back to this shortly.

Figure 12: Scroll view attributes

Oh, and because selecting the scroll view and table view can be a bit tricky, I'm going to let you in on a little secret that may help out. You can customize how Interface Builder shows the components in the nib. If you select list mode from the toolbar, you will be presented with a hierarchical list of components, instead of just icons. From this view, you may open up the disclosure triangles to select the scroll view or table view directly, as in Figure 14. Using list mode is often very helpful in selecting nested views, such as table views, tab views, and split views.

With the table view selected, you can now select individual table columns and change their attributes. After selecting the first table column, set its title and identifier to Column 1 and One, respectively, as shown in Figure 15. Now change the second column to have its title and identifier to be Column 2 and Two, respectively.

Figure 13: Table view attributes

Figure 14: List mode of Interface Builder

Figure 15: Table column attributes

If you save the nib and run the application from Xcode, you should get a window and table view, but the table view should be empty. In order to get data into the table view, we need to implement a data source in code. Create a new Objective-C class and name it MyController. In order for this class to be a table view data source, it must implement at least two methods. Here's the first one:

- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView;
{
    return 3;
}

The purpose of this method is self-explanatory: it sets the number of rows for a given table view. While the number of columns is set in Interface Builder, the number of rows is set in your code. For demonstration purposes we are returning a constant value of 3, which means three rows will be displayed.

Every row and column refers to a particular value in the table called a table cell. The second method that you must implement provides the actual data for each cell:

- (id) tableView: (NSTableView *) tableView
objectValueForTableColumn: (NSTableColumn *) tableColumn
            row: (NSInteger) rowIndex
{
    NSString * identifier = [tableColumn identifier];
    return [NSString stringWithFormat:
            @"%@ %d", identifier, rowIndex];
}

This method gets called for each and every cell in the table. You'll notice that it passes a table column object, NSTableColumn, and a row index. You may be surprised that it does not pass a column index, too. The problem is that the user is free to re-order columns, and this would make the column index pretty useless. From column object, we can get the identifier we setup in Interface Builder. The identifier is a much more reliable value than the column index, thus you can see why using a unique and meaningful identifier is very useful. For testing purposes, we are returning a string using the table column identifier and the row index.

We're almost ready to run our application, but first we need to tell the table view that MyController is its data source. Be sure to save all your files in Xcode and switch to Interface Builder. Find NSObject in the Library panel and drag it to you MainMenu.nib window. Set the class of this new object to be MyController in the Inspector panel. This creates an instance of the MyController class inside the nib.

Hooking this object up to the table view is similar connecting a delegate. You need to control-drag from the table view to the controller object. Be sure you are dragging from the table view and not the scroll view or the table column. Use list mode, if you have trouble selecting the table view. Once you control-drag from the table view to the controller object, Interface Builder should popup a menu allowing you to choose the outlet for connection. Two choices exists for table views: dataSource and delegate. Choose dataSource from the menu, as shown in Figure 16.

Figure 16: Connecting a data source

Now, save the nib, switch back to Xcode, and run the application. If all goes well, you should see a window similar to Figure 17. There should be three rows in the table and the contents of each table cell should be a mix of the table column identifier and the row index. You can even re-order the table columns if you want, and the table cell values should remain the same.

Figure 17: Simple table application

A Useful Table Example

Admittedly, that example is not very useful, so let's spice it up. Let's bring back our trusty Rectangle class and create an application to show a list of rectangles along with their area and perimeters. Add the Rectangle.h and Rectangle.m files to your Xcode project. For reference, the header file is shown in Listing 1.

Listing 1: Rectangle.h header file

#import <Foundation/Foundation.h>
@interface Rectangle : NSObject
{
    float _leftX;
    float _bottomY;
    float _width;
    float _height;
}
@property float leftX;
@property float bottomY;
@property float width;
@property float height;
@property (readonly) float area;
@property (readonly) float perimeter;
- (id) initWithLeftX : (float) leftX
             bottomY: (float) bottomY
              rightX: (float) rightX
                topY: (float) topY;
@end

Go to Interface Builder and select the table view. We need to setup our table with four columns, each with the following header titles: Width, Height, Area, and Perimeter. In the table view's attributes, set the number of columns to 4. Resize the window so that all four columns are available. Then, select each column and change the title and identifiers appropriately. In order to keep things simple, let's keep the identifiers the same as the titles. Finally, make sure each column is not editable. The attributes for the width column are shown in Figure 18. Make sure the other three columns are setup in a similar fashion.

Figure 18: Width column attributes

Just as we can add formatters to text fields, you can also add them to table columns. Add number formatters to each of the table columns by dragging from the Library to the text cells of each table column. Set their style to be Decimal, as we did earlier.

We are finished with the user interface, so save the nib and switch to Xcode. Our controller class will keep an array of rectangles and display them in the table. Let's start out by populating the array in our constructor with a few rectangles:

- (id) init
{
    self = [super init];
    if (self == nil)
        return nil;
    
    _rectangles = [[NSMutableArray alloc] init];
    
    Rectangle * rectangle;
    rectangle = [[Rectangle alloc] initWithLeftX   : 0
                                         bottomY: 0
                                          rightX: 10
                                            topY: 20];
    [_rectangles addObject: rectangle];
    // Without garbage collection you need:
    // [rectangle release];
    
    rectangle = [[Rectangle alloc] initWithLeftX   : 0
                                         bottomY: 0
                                          rightX: 500
                                            topY: 100];
    [_rectangles addObject: rectangle];
    // Without garbage collection you need:
    // [rectangle release];
    
    return self;
}

Be sure to add the _rectangles instance variable to the header file. Now, we're going to enable garbage collection for our project, too. This allows use to avoid releasing the rectangle instances and avoid implementing a destructor. Click on the project name in the Groups & Files list on the left. Then choose the File > Get Info menu to open up the Inspector panel. Select the Build tab, find the Objective-C Garbage Collection build setting and set it to Supported.

If you do not want to enable garbage collection, uncomment the release calls to rectangle and implement the dealloc method to release the _rectangles array. You also need to release the rectangle instances after adding them to the _rectangles array, as described in the comments. Remember the array retains objects, so you need to release them if you no longer need them.

Now we need to modify our data source methods to use the array of rectangles, instead of hard coded values. The numberOfRowsInTableView method is easy. It returns the number of elements in the _rectangles array:

- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
{
    return [_rectangles count];
}

The tableView:objectValueForTableColumn:row: method is a bit more complex. It needs to return the proper rectangle property based on the identifier. The other tricky part is that the method returns an id type. This means we need to wrap our float values inside an NSNumber instance, as well:

- (id) tableView: (NSTableView *) tableView
objectValueForTableColumn: (NSTableColumn *) tableColumn
            row: (NSInteger) rowIndex
{
    NSString * identifier = [tableColumn identifier];
    Rectangle * rectangle = [_rectangles objectAtIndex: rowIndex];
    float value;
    if ([identifier isEqualToString: @"Width"])
        value = rectangle.width;
    else if ([identifier isEqualToString: @"Height"])
        value = rectangle.height;
    else if ([identifier isEqualToString: @"Area"])
        value = rectangle.area;
    else if ([identifier isEqualToString: @"Perimeter"])
        value = rectangle.perimeter;
    
    return [NSNumber numberWithFloat: value];
}

Running our application now should result in a window similar to Figure 19. It should show our two rectangles, along with their corresponding area and perimeter.

Figure 19: Initial rectangle table

Try resizing the window and see if it works correctly. Also try editing the individual table cells. Because we setup our table columns to be uneditable, it should not be allowed. The full listings for MyController are shown in Listing 2 and Listing 3.

Listing 2: MyController.h header file

#import <Cocoa/Cocoa.h>
@interface MyController : NSObject
{
    NSMutableArray * _rectangles;
}
@end
#import "MyController.h"
#import "Rectangle.h"
@implementation MyController
Listing 3: MyController.m source file
- (id) init
{
    self = [super init];
    if (self == nil)
        return nil;
    
    _rectangles = [[NSMutableArray alloc] init];
    
    Rectangle * rectangle;
    rectangle = [[Rectangle alloc] initWithLeftX   : 0
                                         bottomY: 0
                                          rightX: 10
                                            topY: 20];
    [_rectangles addObject: rectangle];
    // Without garbage collection you need:
    // [rectangle release];
    
    rectangle = [[Rectangle alloc] initWithLeftX   : 0
                                         bottomY: 0
                                          rightX: 500
                                            topY: 100];
    [_rectangles addObject: rectangle];
    // Without garbage collection you need:
    // [rectangle release];
    
    return self;
}
- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
{
    return [_rectangles count];
}
- (id) tableView: (NSTableView *) tableView
objectValueForTableColumn: (NSTableColumn *) tableColumn
            row: (NSInteger) rowIndex
{
    NSString * identifier = [tableColumn identifier];
    Rectangle * rectangle = [_rectangles objectAtIndex: rowIndex];
    float value;
    if ([identifier isEqualToString: @"Width"])
        value = rectangle.width;
    else if ([identifier isEqualToString: @"Height"])
        value = rectangle.height;
    else if ([identifier isEqualToString: @"Area"])
        value = rectangle.area;
    else if ([identifier isEqualToString: @"Perimeter"])
        value = rectangle.perimeter;
    
    return [NSNumber numberWithFloat: value];
}
@end

Conclusion

Once again, we've covered a lot of ground in just one article. There's a lot more we can do with a table view, too much to cover in a single article. Next month, we'll explore how to add and remove rectangles from the table. That is, if you haven't gone ahead and figured it out on your own.


Dave Dribin has been writing professional software for over eleven years. After five years programming embedded C in the telecom industry and a brief stint riding the Internet bubble, he decided to venture out on his own. Since 2001, he has been providing independent consulting services, and in 2006, he founded Bit Maki, Inc. Find out more at http://www.bitmaki.com/ and http://www.dribin.org/dave/.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more

Jobs Board

Sublease Associate Optometrist- *Apple* Val...
Sublease Associate Optometrist- Apple Valley, CA- Target Optical Date: Apr 20, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92307 **Requisition Read more
*Apple* Systems Administrator - JAMF - Syste...
Title: Apple Systems Administrator - JAMF ALTA is supporting a direct hire opportunity. This position is 100% Onsite for initial 3-6 months and then remote 1-2 Read more
Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.