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