Building a data driven UI - iphone

I have searched throughout SO on this topic for iPhone and everything points to the WWDC 2010 coverage, so yes I'm aware of that. But can anyone point me to a more detailed resource from where I can learn how to build a robust system for presenting different user interfaces on an app depending on the data that I'm presenting? I am fetching the data in a JSON format and my UI needs to vary depending on what I get out of the JSON parser.
Any books or online resources that give a detailed look into this topic?
Thanks!

I recently had the same issue in one of my apps (navigation style) and the way I solved it is fairly simple.
I had a user_type flag in my JSON response, and depending on that flag I would push a different view controller.
For example, given my JSON response is stored in a NSMutableDictionary called "response"
if ([response objectForKey:#"account_type"] == 1) {
/*
initialize user_type 1 viewController
*/
[self.navigationController pushViewController:userType1ViewController animated:YES];
else if ([response objectForKey:#"account_type"] == 2) {
/*
initialize user_type 2 viewController
*/
[self.navigationController pushViewController:userType2ViewController animated:YES];
}
You can have as many different user_types as you want.
Editing after clarification in comments
You will likely be manually drawing these views.
I would subclass UIView for each of the different question types you have listed (including properties for common elements like question title, choices, etc). Assuming that questions with the same question type will have the same layout.
Then you can cycle through your JSON response (once it's in an array or dictionary) and lay it out.
If it's a multiple choice question, make a new multipleChoiceQuestion view, add its properties, then addSubview to the main feed view. Then, for your next view, you will need to set the frame to be:
nextQuestion.frame = CGRectMake(0, 0 + firstQuestion.frame.size.height, height, width);
This will ensure that your second question is drawn right below the first question.

Related

NSMutableArray Difficulty

If you wanna see the code Im having problem with, here is the link:
Code
My question is connected with my past question.
I'm really having problem with my NSMutableArray, I'm currently using iCarousel for my slotMachine object(slot1 and slot2). My app works this way:
From PhotoViewController I made a view that has thumbnail images, then assign its frame with button. So if 1 image was pressed, it will save that integer via NSUserDefaults.
Then I will retrieve it in my carouselViewController
Im thinking of adjusting the array but I can't.
I also have tried my question here:
Comparing with NSMutableArray
If only I can do it the same as Array 2 it would be much easy, but still not working.
(ADDITIONAL INFO:)
I have done it this way, have a Viewcontroller that contains the UIImageView with a button in it, so when the user taps it, my CustomPicker pops up. My CustomPicker contains the image on what the user have picked on the camera roll. So each button has a specific value sent to my iCarouselView using NSUserDefaults. carousel1 for First slot and carousel2 for Second slot.
Here is what I wanna do: I want to forcefully make it stop to the index the user picks. (Which Im doing in my carouselDidEndScrollingAnimtaion)
In my carouselDidEndScrollingAnimation method i tested all of my condition(individually) it works perfectly in terms of comparing.
Then when I combine the conditions, the first Two comparison or STOP is RIGHT, but the next two are always wrong. Or sometimes Got mixed up.
I need to scroll the two specific indexes/integer which was User Picked( I already done that) was able to scroll 2 pairs of them but then the next two were always wrong because I think there indexes were adjusting.
PICTURES:
Image Below is my PhotoViewController which contained the Comparing Stage SETTING of my game.UIImageVIew with UIButton.Image that will be put in the number according to it will be Forcefully and should be forcefully shown.
When my iCarousel start then it stops for example in the image below(Which is not the same as the above):
Will be forcefully scroll to the inputted image in the PhotoViewController
Into:
Summary:
Its like this. I have a settingsView from there, I will import my images(Multiple) for Slot1 & Slot2.
Then in another View the PhotoViewController that is where the image above is shown. THe first column corresponds to 1st slot followed by the 2nd slot. if a view is pressed (for example No. 1 of Slot 1 it will load a thumbnail of images loading the images picked from Picker for the Slot 1.
You will have to do it 4 times(pair) ----> The displayed here I get their indexes via NSUserDefaults via button.tag then send to iCarouselView.
Then when you are done (pressed Done button) it will go to iCarouselView then, as shown above thats the view of it.
When pressed it will spin for couple of seconds, then when finished but not stop at the user picked in the PhotoView it will forcefully scroll to that index.
QUESTION:
Is there a way to make my array or my iCarousel.view not adjust their indexes when Im deleting. To still retain my indexes the right way. Or are there other solution like adjusting my array, the same as adjusting my PhotoViewController picked indexes too. Because I think that when my array retain their indexes even deleting I would be able to solve this problem. But still can't.
Hope you understand my question.
Is there a way to make my array or my iCarousel.view not adjust their indexes when Im deleting. To still retain my indexes the right way. Or are there other solution like adjusting my array, the same as adjusting my PhotoViewController picked indexes too. Because I think that when my array retain their indexes even deleting I would be able to solve this problem. But still can't.
The only way you have to modify the way iCarousel manages its indexes is by modifying the code. Indeed, if you look at the removeViewAtIndex method in iCarousel.m, you will see that indexes are managed through an NSDictionary, and at the moment of deleting, the dictionary is rearranged (items are reordered). You could take that method:
- (void)removeViewAtIndex:(NSInteger)index
{
NSMutableDictionary *newItemViews = [NSMutableDictionary dictionaryWithCapacity:[itemViews count] - 1];
for (NSNumber *number in [self indexesForVisibleItems])
{
NSInteger i = [number integerValue];
if (i < index)
{
[newItemViews setObject:[itemViews objectForKey:number] forKey:number];
}
else if (i > index)
{
[newItemViews setObject:[itemViews objectForKey:number] forKey:[NSNumber numberWithInteger:i - 1]];
}
}
self.itemViews = newItemViews;
}
You could apply the same logic to your array, so that the carousel and your array keep in sync. Of course, if you store the indexes somewhere (slot1/slot2/slot2/slot4?), you should also update their values after removing an element.
On the other hand, I think that what you are asking here is how to do something that you believe would solve the problem you have, but you are not really explaining what the problem is. Indeed, if I understand you correctly, what you do is:
spinning the carousel;
when the carousel stops, if it is not by chance on the desired item, you "force" it to scroll to that item.
There is no reason why this should not work after deleting some elements (unless iCarousel has some bugs, then the solution would be catching the bug). The only part is knowing which index is the one you would like to move to.
As a suggestion, I would start off by simplifying your delegate carouselDidEndScrollingAnimation method. Indeed, your carouselDidEndScrollingAnimation has a parameter called carousel, well, I think this is the only carousel you should ever be referring to in that method. If you don't see it, this is the reasoning: each of your carousel will stop scrolling and the carouselDidEndScrollingAnimation will be called; so that method will be called twice. Each time that method is executed you will modify the state of both carousel1 and carousel2 (by calling scrollToItemAtIndex); therefore, on each carousel you will call scrollToItemAtIndex twice.
This does no sound very correct to me. So you should find a way to scroll only carousel1 when carouselDidEndScrollingAnimation is called for carousel1 and to scroll only carousel2 when carouselDidEndScrollingAnimation is called for carousel2.
More generally, another point I would like to raise is that the idea of:
letting a carousel stop;
scrolling it again so that it reaches the desired position;
does not seem the best implementation possible since the user would see the carousel stopping and then starting over again.
The way I would approach this is by modifying directly iCarousel implementation so that it supports this specific behavior you need.
Concretely, give a look at the step method in iCarousel.m. This is called at each frame to produce the carousel animation. Now, in this method there is decelerating branch:
else if (decelerating)
{
CGFloat time = fminf(scrollDuration, currentTime - startTime);
CGFloat acceleration = -startVelocity/scrollDuration;
CGFloat distance = startVelocity * time + 0.5f * acceleration * powf(time, 2.0f);
scrollOffset = startOffset + distance;
[self didScroll];
if (time == (CGFloat)scrollDuration)
{
decelerating = NO;
if ([delegate respondsToSelector:#selector(carouselDidEndDecelerating:)])
{
[delegate carouselDidEndDecelerating:self];
}
if (scrollToItemBoundary || (scrollOffset - [self clampedOffset:scrollOffset]) != 0.0f)
{
if (fabsf(scrollOffset/itemWidth - self.currentItemIndex) < 0.01f)
{
//call scroll to trigger events for legacy support reasons
//even though technically we don't need to scroll at all
[self scrollToItemAtIndex:self.currentItemIndex duration:0.01];
}
else
{
[self scrollToItemAtIndex:self.currentItemIndex animated:YES];
}
}
else
{
CGFloat difference = (CGFloat)self.currentItemIndex - scrollOffset/itemWidth;
if (difference > 0.5)
{
difference = difference - 1.0f;
}
else if (difference < -0.5)
{
difference = 1.0 + difference;
}
toggleTime = currentTime - MAX_TOGGLE_DURATION * fabsf(difference);
toggle = fmaxf(-1.0f, fminf(1.0f, -difference));
}
}
}
and you see that when the carousel stops decelerating, it is scrolled again. This is exactly the same as you are doing, so you might find a way to modify this code and have the carousel scrolls exactly to the index you need. In this way you would get a far smoother spinning of the carousel.
Hope this helps and apologies for the lengthy reply.
Its a little difficult to know what the issue is here. Are you using a single NSMutableArray for the images and using the NSUserDefaults value to get the object at the index in the array?
Im not 100% sure on what is happening. What does the user do(and in what view) and what is triggered after that(which view is presented).
Are you trying to stop the "spinning" images on the image that is the same as the one picked from the previous view?
According to your images above, the images are off by a single index. Is this the case every time? Maybe there is an issue with your fetching from the array.
If you give me some more info I can help.
I looked through the code you pasted again and I think this might be your issue
if (twoSlot1 > [(UIImageView*)[self.carousel2 currentItemView] tag]){
[self.carousel1 scrollToItemAtIndex:(-twoSlot1)-2 duration: 3.5f];
} else {
[self.carousel1 scrollToItemAtIndex:-twoSlot1 duration: 3.5f];
}
On all other code blocks like that you have this where you call each carousel. In the above code you call carousel 1 twice.
if (slot2 > [(UIImageView*)[self.carousel1 currentItemView] tag]){
[self.carousel1 scrollToItemAtIndex:(-slot2)-2 duration: 3.0f];
} else {
[self.carousel1 scrollToItemAtIndex:-slot2 duration: 3.0f];
}
if (twoSlot2 > [(UIImageView*)[self.carousel2 currentItemView] tag]){
[self.carousel2 scrollToItemAtIndex:(-twoSlot2)-2 duration: 3.5f];
} else {
[self.carousel2 scrollToItemAtIndex:-twoSlot2 duration: 3.5f];
}
You call self.carousel1 when you should be calling number 2.
Is this correct?
Referring to your question. You want an array that does not change its members' indexes when a member is deleted from the array.
I guess you could use an NSMutableDictionary. It is an associative array so to say, where the indexes are of your choice and they remain unchanged when you delete a member from in between.
You may still use 0..n as your Index. You can still use some methods that you are familiar with from NSArray, such as count. You can use an enumerator to go through all members of the dictionary. On the other hand you can still use your for-loops as you are used to use them with arrays. Just be prepared that a) objectForKey:i may return nil if the key/index does not exist (e.g. was deleted) and that count retuns the number of the objects but not the highest index+1 as it does with arrays.
Not sure if I understand completely, but when one of the elements in your mutable array is deleted, rather than just deleting it, maybe insert it with another "dummy" place holder object? That way your indexes won't change at all when a delete occurs
I'm having a hard time understanding your overall problem, but from what I can gather the crux of your question is this:
Is there a way to make my array or my iCarousel.view not adjust their indexes when Im deleting.
I don't know whether it will solve your bigger issue, but using an NSMutableDictionary to simulate an array should allow you to do this. You can simply use the indices as the keys to the dictionary, and then when you remove the item associated with an index, no other indices will be adjusted as a result. For example:
NSMutableDictionary *arrayDict = [[NSMutableDictionary alloc] init];
[arrayDict setValue:foo forKey:[NSNumber numberWithInt:[arrayDict count]]];
[arrayDict setValue:bar forKey:[NSNumber numberWithInt:[arrayDict count]]];
[arrayDict setValue:fooBar forKey:[NSNumber numberWithInt:[arrayDict count]]];
And then you can access the object an at index with [arrayDict objectForKey:[NSNumber numberWithInt:index]].
Note that using an NSNumber for the key parameter of setValue:forKey: will generate a warning, but you can safely ignore this (or use the string representation if it bothers you).

iphone sql - data corrupted/ missing when reading

I'm using SQL to store data in my application. I read data in the app delegate and store it in an array like so.
(First I read from the database and store in aFlashcardSet and then this)
// Add the flashcardSet to the main Array
[mainSetsArray addObject:aFlashcardSet];
In my next view I then copy the data from the app delegate.
flashcardsAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
self.setsArray = delegate.mainSetsArray;
Then I pass one object from the set to the final view.
cardDetailViewController.thisCardSet = [setsArray objectAtIndex:row];
The problem is when I read the data in the final view and use to set UI elements the app crashes, the code works fine with data hard coded during the second phase and the database data is shown fine during the second phase (to populate a table view). I've tried outputting the data at all stages and it is all correct until the final view where it either crashes or shows incorrect values (file names or random letters rather than the actual text).
I have also tried to read the database data inside the final view and set it to thisCardSet but it still suffers the same problem.
Any ideas?
Thanks.
The problem was inside one of my custom classes I wasn't using self, so I was writing.
aString = string;
Rather than -
self.aString = string;
So I was losing data down the line, feel really stupid but hope it help's someone else =].

How to Load an array into OpenFlow

I'm trying to implement openFlow in my project but I cant seem to get the images to show up on my uiview. What isnt clear to me is once I have the dictionary of image links, how do i tell AFOpenView that I want to use that dictionary object as my data source?
I've looked at the demo code and I see that when the flickr request finishes, he saves a copy of the dictionary results, counts them, and then tells OpenFlowView that there are x number of images, but what is never clear is how he tells OpenFlowView to use the dictionary with the results?
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse:(NSDictionary *)inResponseDictionary
{
// Hold onto the response dictionary.
interestingPhotosDictionary = [inResponseDictionary retain];
int numberOfImages = [[inResponseDictionary valueForKeyPath:#"photos.photo"] count];
[(AFOpenFlowView *)self.view setNumberOfImages:numberOfImages];
}
See here: http://blog.objectgraph.com/index.php/2010/04/09/how-to-add-coverflow-effect-on-your-iphone-app-openflow/
This tutorial seems to suggest that you have to call the view's setImage method multiple times, once per image.
This tells me that the implementation is confusing and weird, but for this you have to blame the component's author.
The images are loaded on demand in the 'updateCoverImage:' method of AFOpenFlowView.m
'updateCoverImage:' calls 'openFlowView:requestImageForIndex:' in AFOpenFlowViewController.m, which uses interestingPhotosDictionary.
So, it is called on demand whenever an image needs to be loaded. It wraps an operation queue so the images are loaded outside the main thread.

Cocoa Touch: are dynamically XML-defined views possible?

first post. Been an iPhone developer intern for about five weeks now. I've read a lot of introductory Apress material, but please take it easy if I make some vocabulary violations. So far I've been able to find answers by searching and lurking. However I now have a task for which I can find little relevant information.
My iPhone application currently uses a rigid view hierarchy for selection of items. The MainViewController links to (err, Subviews?) a TableView for selecting any from a list of factories. Selecting a factory then loads a TableView for various statistics about that factory. The data to populate these tables loads from a remote JSON server by http.
I would like to have an XML definition of the view hierarchy on the remote server from which the application dynamically constructs the view structure. In this way the view structure is not hard-coded into the client (the iPhone ViewControllers/nibs) and offers more flexibility for reorganizing the content server-side.
Is this possible, and has anybody accomplished this? Most relevant answer was Dynamic UI in iphone, however upon reading I feel Apple's guide to Serializing/Archiving departs quickly from what I am trying to do. Can somebody explain its relevance or point to another resource?
Yes, it is entirely possible, and I (as well as others) have done something similar. Essentially, you will have to write a couple classes:
One is a Controller class to parse the XML to get the content you need upon download into a format you can use in a Model class. There are a number of OSS libraries out there, but don't use Cocoa's XML classes on the iPhone because they are just too slow when parsing. Be sure to double-check the license that comes with the OSS library so that you don't run afoul of GPL and the like.
The second class you'll need is one that will translate your model into a view hierarchy and place it on screen on the fly as the user moves through the content. This is to implement as individual steps, but the challenge is managing the content placement workflow.
The content noted in your question does depart a bit from the core task at hand but it can still be relevant to your interests. At the very least it may give you an idea on how to approach certain aspects of your project.
Here's a follow-up. I have accomplished what I wanted to do.
I have a ListViewController class and a Repository model/object. To parse XML, I used XPathQuery, Matt Gallagher's adaptation of libxml2 for Cocoa (http://cocoawithlove.com/2008/10/using-libxml2-for-parsing-and-xpath.html).
The ListViewController tracks the current XML path. When a cell in the table view is selected, the corresponding path is concatenated to the existing path. The ListViewController creates and pushes another instance of itself and PerformXMLPathQuery() returns an array to populate the table view cells. The Repository holds an NSMutableData object and an NSString with current path. Both are required for XPath Query:
NSArray *PerformXMLXPathQuery(NSData *document, NSString *query);
PerformXMLPathQuery does a lot of back work for you. This is how I used it to get what I wanted from the XML:
-(NSArray*)getListFromRepositoryWithPath:(NSString *)path {
// Get the nodes referred to by the path
NSArray *nodes = PerformXMLXPathQuery(repository.data, path);
// Build an array with the node names
NSMutableArray* sublist = [[NSMutableArray alloc] init];
for (NSDictionary* node in nodes) {
for (id key in node) {
if ([key isEqualToString:#"nodeChildArray"]) {
NSArray* subNodes = [node objectForKey:#"nodeChildArray"];
for (NSDictionary* subNode in subNodes) {
for (id subKey in node) {
if ([subKey isEqualToString:#"nodeName"]) {
[sublist addObject:[subNode objectForKey:subKey]];
}
}
}
}
}
// Ignore duplicate entries in the data
if ([sublist count] > 0) {
return [NSArray arrayWithArray:sublist];
}
}
return [NSArray arrayWithArray:sublist];
}
When a row is selected, I used didSelectRowAtIndexPath to prepare the next path and view controller:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *nextXpathQueryString = [self.xpathQueryString copy];
// Build the path string for the next view's XML path query
if ([[nextXpathQueryString lastPathComponent] isEqualToString:#"*"]) {
nextXpathQueryString = [nextXpathQueryString stringByDeletingLastPathComponent];
nextXpathQueryString = [nextXpathQueryString stringByAppendingPathComponent:#ROOTNAME];
}
nextXpathQueryString = [nextXpathQueryString stringByAppendingPathComponent:[repository.currentListToDisplay objectAtIndex:indexPath.row]];
// Navigation logic. Create and push another view controller.
ListViewController *detail = [[ListViewController alloc] init];
// Populate the new ViewController
[detail setRepository:repository];
[detail setTitle:nextXpathQueryString];
[detail setXpathQueryString:nextXpathQueryString];
[nextXpathQueryString release];
UIBarButtonItem *doneButton = [[self navigationItem] rightBarButtonItem];
[[detail navigationItem] setRightBarButtonItem:doneButton animated:YES];
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detail animated:YES];
[detail release];
}
The cool thing is each instance of ListViewController keeps its own path string and content array. No fooling around with nested tree objects or pointers.
Essentially I have a flexible XML tree browser for the iPhone. Thanks Stack Overflow.

What's the best approach to asynchronous image caching on the iPhone?

I'm creating an iPhone app that will pull data down from a Web API, including email addresses. I'd like to display an image associated with each email address in table cells, so I'm searching the Address Book for images and falling back on a default if the email address isn't in the book. This works great, but I have a few concerns:
Performance: The recipes I've found for looking for an address book record by Email address (or phone number) are reportedly rather slow. The reason for this is that one must iterate over every address book record, and for each one that has an image, iterate over all email addresses to find a match. This can be time-consuming for a large address book, of course.
Table Cells: So I thought I'd gather up all the email addresses for which I need to find images and find them all at once. This way I iterate through the book only once for all addresses. But this doesn't work well for table cells, where each cell corresponds to a single email address. I'd either have to gather all the images before displaying any cells (potentially slow), or have each cell look up each image as it loads (even slower, as I'd need to iterate through the book to find a match for each email address).
Asynchronous Lookup: So then I thought I'd look them up in bulk, but asynchronously, using NSInvocationOperation. For each image found in AddressBook, I'd save a thumbnail in the app sandbox. Then each cell could just reference this file and show the default if it doesn't exist (because it's not in the book or hasn't yet been found). If the image is later found in the asynchronous lookup, the next time the image needs to be displayed it would suddenly appear. This might work well for periodic regeneration of images (for when images have been changed in the address book, for example). But then for any given instance of my app, an image may not actually show up for a while.
Asynchronous Table Cell Lookup: Ideally, I'd use something like markjnet's asynchronous table cell updating to update table cells with an image once it has been downloaded. But for this to work, I'd have to spin off an NSInvocationOperation job for each cell as it's displayed and if the cached icon is missing from the sandbox. But then we're back to inefficiently iterating through the entire address book for each oneā€”and that can be a lot of them if you've just downloaded a whole bunch of new email addresses.
So my question is: How do others do this? I was fiddling with Tweetie2, and it looks like it updates displayed table cells asynchronously. I assume it's sending a separate HTTP request for every image it needs. If so, I imagine that searching the local address book by email address isn't any less efficient, so maybe that's the best approach? Just not worry about the performance issues associated with searching the address book?
If so, is saving a thumbnail image in the sandbox the best approach to caching? And if I wanted to create a new job to update all the thumbnails with any changes in the address book say once a day, what's the best approach to doing so?
How do the rest of you solve this sort of problem? Suggestions would be much appreciated!
Regardless of what strategy you use for the actual caching of images, I would only make one pass through the Address Book data each time you get a batch of email addresses, if possible. (And yes, I would do this asynchronously.)
Create an NSMutableDictionary which will serve as your in-memory cache for search results. Initialize this dictionary with each email address from the download as a key, with a sentinel as that key's value (such as [NSNull null]).
Next, iterate through each ABRecordRef in the Address Book, calling ABRecordCopyValue(record, kABPersonEmailProperty) and looping through the results in each ABMultiValue that is returned. If any of the email addresses are keys in your cache, set [NSNumber numberWithInt:ABRecordGetRecordId(record)] as the value of that key in your dictionary.
Using this dictionary as a lookup index, you can quickly obtain the images of ABRecordRefs for only the email addresses that you are currently displaying in your table view given the user's current scroll position, as suggested in hoopjones's answer. You can add an address book change listener to invalidate your cache, trigger another indexing operation, and then update the view, if your application needs that level of "up-to-date-ness".
I'd use the last method you listed (Asynchronous Table Cell Lookup) but only look images for the current records being displayed. I overload the UIScrollViewDelegate methods to find out when a user has stopped scrolling, and then only start making requests for the current visible cells.
Something like this (this is slightly modified from a tutorial I found on the web which I can't find now, apologies for not citing the author) :
- (void)loadContentForVisibleCells
{
NSArray *cells = [self.table visibleCells];
[cells retain];
for (int i = 0; i < [cells count]; i++)
{
// Go through each cell in the array and call its loadContent method if it responds to it.
AddressRecordTableCell *addressTableCell = (AddressRecordTableCell *)[[cells objectAtIndex: i] retain];
[addressTableCell loadImage];
[addressTableCell release];
addressTableCell = nil;
}
[cells release];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
// Method is called when the decelerating comes to a stop.
// Pass visible cells to the cell loading function. If possible change
// scrollView to a pointer to your table cell to avoid compiler warnings
[self loadContentForVisibleCells];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate)
{
[self loadContentForVisibleCells];
}
}
Once you know what address records are currently visible, just doing a search for those (5 -7 records probably) will be lightning fast. Once you grab the image, just cache it in a dictionary so that you don't have to redo the request for the image later.
You seem to try to implement lazy images loading in UITableView.
there's a good example from Apple, I'm referencing it here :
Lazy load images in UITableView
FYI, I've released a free, powerful, and easy library for doing asynchronous image loading and fast file caching: HJ Managed Objects
http://www.markj.net/asynchronous-loading-caching-images-iphone-hjobjman/