Building Custom iOS UI Components with XIB and Programmatic Layout

A custom UI component begins with an Interface Builder file. Start by creating a new file and selecting the User Interface category, then choose View. This generates a .xib file containing an empty view.

Design your reusable interface within this .xib file. Ensure you connect the File's Owner's Class in the "Custom Class" section to the corresponding Objective-C class you will create.

Create a new Cocoa Touch Class that subclasses UIView. This serves as the backing code for the visual component.

Header File (.h):

#import <UIKit/UIKit.h>

@class AppItem;

@interface AppTileView : UIView

@property (nonatomic, strong) AppItem *itemData;
+ (insatncetype)createTileView;

@end

Implementation File (.m):

#import "AppTileView.h"
#import "AppItem.h"

@interface AppTileView()
@property (weak, nonatomic) IBOutlet UIImageView *iconImageView;
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UIButton *actionButton;
- (IBAction)actionButtonPressed:(UIButton *)sender;
@end

@implementation AppTileView

+ (instancetype)createTileView {
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSArray *nibItems = [mainBundle loadNibNamed:@"AppTileView" owner:nil options:nil];
    return [nibItems lastObject];
}

- (void)setItemData:(AppItem *)itemData {
    _itemData = itemData;
    self.iconImageView.image = [UIImage imageNamed:itemData.imageName];
    self.nameLabel.text = itemData.displayName;
}

- (IBAction)actionButtonPressed:(UIButton *)sender {
    sender.enabled = NO;
    UILabel *statusLabel = [[UILabel alloc] init];
    statusLabel.text = @"Processing...";
    statusLabel.backgroundColor = [UIColor blackColor];
    
    CGRect superviewBounds = self.superview.bounds;
    CGFloat labelWidth = 200;
    CGFloat labelHeight = 30;
    CGFloat labelX = (superviewBounds.size.width - labelWidth) / 2;
    CGFloat labelY = (superviewBounds.size.height - labelHeight) / 2;
    statusLabel.frame = CGRectMake(labelX, labelY, labelWidth, labelHeight);
    
    statusLabel.textColor = [UIColor redColor];
    statusLabel.textAlignment = NSTextAlignmentCenter;
    statusLabel.font = [UIFont boldSystemFontOfSize:17];
    statusLabel.alpha = 0.0;
    statusLabel.layer.cornerRadius = 10;
    statusLabel.layer.masksToBounds = YES;
    
    [UIView animateWithDuration:1.5 animations:^{
        statusLabel.alpha = 0.6;
    } completion:^(BOOL finished) {
        if (finished) {
            [UIView animateWithDuration:1.5 delay:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
                statusLabel.alpha = 0.0;
            } completion:^(BOOL finished) {
                [statusLabel removeFromSuperview];
            }];
        }
    }];
    [self.superview addSubview:statusLabel];
}
@end

This completes the custom component. To use it, you need a data model to load information from a property list.

Model Header File:

#import <Foundation/Foundation.h>

@interface AppItem : NSObject
@property (nonatomic, copy) NSString *displayName;
@property (nonatomic, copy) NSString *imageName;
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)itemWithDictionary:(NSDictionary *)dict;
@end

Model Implementation File:

#import "AppItem.h"

@implementation AppItem
- (instancetype)initWithDictionary:(NSDictionary *)dict {
    if (self = [super init]) {
        self.displayName = dict[@"name"];
        self.imageName = dict[@"icon"];
    }
    return self;
}
+ (instancetype)itemWithDictionary:(NSDictionary *)dict {
    return [[self alloc] initWithDictionary:dict];
}
@end

The view controller utilizes the component by loading model data and arranging the views.

ViewController Implementation:

#import "ViewController.h"
#import "AppItem.h"
#import "AppTileView.h"

@interface ViewController ()
@property (nonatomic, strong) NSArray *appList;
@end

@implementation ViewController
- (NSArray *)appList {
    if (!_appList) {
        NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"app.plist" ofType:nil];
        NSArray *dataArray = [NSArray arrayWithContentsOfFile:plistPath];
        NSMutableArray *models = [NSMutableArray array];
        for (NSDictionary *dict in dataArray) {
            AppItem *item = [AppItem itemWithDictionary:dict];
            [models addObject:item];
        }
        _appList = models;
    }
    return _appList;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSInteger itemsPerRow = 3;
    CGFloat containerWidth = self.view.frame.size.width;
    CGFloat tileWidth = 75;
    CGFloat tileHeight = 90;
    CGFloat topPadding = 30;
    CGFloat horizontalSpacing = (containerWidth - itemsPerRow * tileWidth) / (itemsPerRow + 1);
    CGFloat verticalSpacing = horizontalSpacing;
    
    for (NSInteger idx = 0; idx < self.appList.count; idx++) {
        AppItem *currentItem = self.appList[idx];
        AppTileView *tile = [AppTileView createTileView];
        
        NSInteger column = idx % itemsPerRow;
        NSInteger row = idx / itemsPerRow;
        CGFloat originX = horizontalSpacing + column * (tileWidth + horizontalSpacing);
        CGFloat originY = topPadding + row * (tileHeight + verticalSpacing);
        tile.frame = CGRectMake(originX, originY, tileWidth, tileHeight);
        
        [self.view addSubview:tile];
        tile.itemData = currentItem;
    }
}
@end

Creating a dynamic word-guessing puzzle involves programmatic button generation and launch screen customization. Update the app's icon and launch screen by replacing the default assets in Images.xcassets.

Define a data model for the puzzle.

Question Model Header:

#import <Foundation/Foundation.h>

@interface QuizQuestion : NSObject
@property (nonatomic, copy) NSString *solution;
@property (nonatomic, copy) NSString *imageName;
@property (nonatomic, copy) NSString *questionText;
@property (nonatomic, strong) NSArray *choices;
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)questionWithDictionary:(NSDictionary *)dict;
@end

Question Model Implementation:

#import "QuizQuestion.h"

@implementation QuizQuestion
- (instancetype)initWithDictionary:(NSDictionary *)dict {
    if (self = [super init]) {
        self.solution = dict[@"answer"];
        self.questionText = dict[@"title"];
        self.imageName = dict[@"icon"];
        self.choices = dict[@"options"];
    }
    return self;
}
+ (instancetype)questionWithDictionary:(NSDictionary *)dict {
    return [[self alloc] initWithDictionary:dict];
}
@end

Main view controller logic handles the game flow.

ViewController Interface:

#import <UIKit/UIKit.h>
#import "QuizQuestion.h"

@interface GameViewController : UIViewController <UIAlertViewDelegate>
@property (nonatomic, strong) NSArray *quizData;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, assign) CGRect originalImageFrame;
@property (weak, nonatomic) IBOutlet UILabel *questionCounterLabel;
@property (weak, nonatomic) IBOutlet UIButton *scoreButton;
@property (weak, nonatomic) IBOutlet UILabel *questionLabel;
@property (weak, nonatomic) IBOutlet UIButton *imageButton;
@property (weak, nonatomic) IBOutlet UIButton *nextButton;
@property (weak, nonatomic) IBOutlet UIView *answerContainer;
@property (weak, nonatomic) IBOutlet UIView *choicesContainer;
@property (weak, nonatomic) UIButton *dimmingView;
- (IBAction)proceedToNextQuestion;
- (IBAction)enlargeImage:(id)sender;
- (IBAction)imageButtonTapped:(id)sender;
- (IBAction)hintButtonTapped;
@end

ViewController Implementation (Key Methods):

@implementation GameViewController
// Lazy load data
- (NSArray *)quizData {
    if (!_quizData) {
        NSString *dataPath = [[NSBundle mainBundle] pathForResource:@"questions.plist" ofType:nil];
        NSArray *rawArray = [NSArray arrayWithContentsOfFile:dataPath];
        NSMutableArray *parsedArray = [NSMutableArray array];
        for (NSDictionary *dict in rawArray) {
            QuizQuestion *q = [QuizQuestion questionWithDictionary:dict];
            [parsedArray addObject:q];
        }
        _quizData = parsedArray;
    }
    return _quizData;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.currentIndex = -1;
    [self loadNextQuestion];
}
// Transition to the next question
- (void)loadNextQuestion {
    self.currentIndex++;
    if (self.currentIndex >= self.quizData.count) {
        UIAlertView *completionAlert = [[UIAlertView alloc] initWithTitle:@"Complete" message:@"Congratulations!" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [completionAlert show];
        return;
    }
    QuizQuestion *currentQuestion = self.quizData[self.currentIndex];
    [self displayQuestion:currentQuestion];
    [self createAnswerButtonsForQuestion:currentQuestion];
    [self createChoiceButtonsForQuestion:currentQuestion];
}
- (void)displayQuestion:(QuizQuestion *)question {
    self.questionCounterLabel.text = [NSString stringWithFormat:@"%ld / %lu", (self.currentIndex + 1), (unsigned long)self.quizData.count];
    self.questionLabel.text = question.questionText;
    [self.imageButton setImage:[UIImage imageNamed:question.imageName] forState:UIControlStateNormal];
    self.nextButton.enabled = (self.currentIndex != self.quizData.count - 1);
}
// Generate answer placeholder buttons
- (void)createAnswerButtonsForQuestion:(QuizQuestion *)question {
    [self.answerContainer.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    NSInteger charCount = question.solution.length;
    CGFloat buttonSize = 35;
    CGFloat spacing = 10;
    CGFloat containerWidth = self.answerContainer.frame.size.width;
    CGFloat startX = (containerWidth - (charCount * buttonSize) - (charCount - 1) * spacing) / 2;
    for (NSInteger i = 0; i < charCount; i++) {
        UIButton *answerBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        [answerBtn setBackgroundImage:[UIImage imageNamed:@"btn_answer"] forState:UIControlStateNormal];
        [answerBtn setBackgroundImage:[UIImage imageNamed:@"btn_answer_highlighted"] forState:UIControlStateHighlighted];
        CGFloat xPos = startX + i * (buttonSize + spacing);
        answerBtn.frame = CGRectMake(xPos, 0, buttonSize, buttonSize);
        [answerBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [answerBtn addTarget:self action:@selector(answerButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
        [self.answerContainer addSubview:answerBtn];
    }
}
// Generate choice buttons
- (void)createChoiceButtonsForQuestion:(QuizQuestion *)question {
    self.choicesContainer.userInteractionEnabled = YES;
    [self.choicesContainer.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    NSArray *optionLetters = question.choices;
    CGFloat buttonSize = 35;
    CGFloat spacing = 10;
    NSInteger columns = 7;
    CGFloat containerWidth = self.choicesContainer.frame.size.width;
    CGFloat startX = (containerWidth - columns * buttonSize - (columns - 1) * spacing) / 2;
    for (NSInteger i = 0; i < optionLetters.count; i++) {
        UIButton *choiceBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        choiceBtn.tag = i;
        [choiceBtn setBackgroundImage:[UIImage imageNamed:@"btn_option"] forState:UIControlStateNormal];
        [choiceBtn setBackgroundImage:[UIImage imageNamed:@"btn_option_highlighted"] forState:UIControlStateHighlighted];
        [choiceBtn setTitle:optionLetters[i] forState:UIControlStateNormal];
        [choiceBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        NSInteger col = i % columns;
        NSInteger row = i / columns;
        CGFloat xPos = startX + col * (buttonSize + spacing);
        CGFloat yPos = row * (buttonSize + spacing);
        choiceBtn.frame = CGRectMake(xPos, yPos, buttonSize, buttonSize);
        [choiceBtn addTarget:self action:@selector(choiceButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
        [self.choicesContainer addSubview:choiceBtn];
    }
}
// Handle choice selection
- (void)choiceButtonTapped:(UIButton *)sender {
    sender.hidden = YES;
    NSString *selectedLetter = sender.currentTitle;
    for (UIButton *answerBtn in self.answerContainer.subviews) {
        if (answerBtn.currentTitle == nil) {
            [answerBtn setTitle:selectedLetter forState:UIControlStateNormal];
            answerBtn.tag = sender.tag;
            break;
        }
    }
    BOOL allFilled = YES;
    NSMutableString *userAnswer = [NSMutableString string];
    for (UIButton *answerBtn in self.answerContainer.subviews) {
        if (answerBtn.currentTitle == nil) {
            allFilled = NO;
        } else {
            [userAnswer appendString:answerBtn.currentTitle];
        }
    }
    if (allFilled) {
        self.choicesContainer.userInteractionEnabled = NO;
        QuizQuestion *currentQuestion = self.quizData[self.currentIndex];
        if ([currentQuestion.solution isEqualToString:userAnswer]) {
            [self updateScore:100];
            [self setAnswerButtonsColor:[UIColor blueColor]];
            [self performSelector:@selector(loadNextQuestion) withObject:nil afterDelay:0.5];
        } else {
            [self setAnswerButtonsColor:[UIColor redColor]];
        }
    }
}
// Handle answer button tap to clear selection
- (void)answerButtonTapped:(UIButton *)sender {
    self.choicesContainer.userInteractionEnabled = YES;
    [self setAnswerButtonsColor:[UIColor blackColor]];
    for (UIButton *choiceBtn in self.choicesContainer.subviews) {
        if (sender.tag == choiceBtn.tag) {
            choiceBtn.hidden = NO;
            break;
        }
    }
    [sender setTitle:nil forState:UIControlStateNormal];
}
- (void)setAnswerButtonsColor:(UIColor *)color {
    for (UIButton *btn in self.answerContainer.subviews) {
        [btn setTitleColor:color forState:UIControlStateNormal];
    }
}
- (void)updateScore:(NSInteger)points {
    NSInteger current = [self.scoreButton.currentTitle integerValue];
    current += points;
    [self.scoreButton setTitle:[NSString stringWithFormat:@"%ld", (long)current] forState:UIControlStateNormal];
}
@end

Tags: iOS Development UI Components XIB Objective-C UIKit

Posted on Tue, 21 Jul 2026 16:57:35 +0000 by NYSiRacer