IOS開發學習筆記十二 實現一個簡單的答題器
效果圖:專案地址
- 專案新增素材,新增plist檔案,並新增plist裡面的字典資料對應的model物件
module標頭檔案:
#import <Foundation/Foundation.h>
@interface CZQuestion : NSObject
@property (nonatomic, copy) NSString *answer;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray *options;
- (instancetype)initWithDict:(NSDictionary *)dict;
+ (instancetype)questionWithDict:(NSDictionary *)dict;
@end
module檔案:
#import "CZQuestion.h"
@implementation CZQuestion
- (instancetype)initWithDict:(NSDictionary *)dict
{
if (self = [super init]) {
self .answer = dict[@"answer"];
self.title = dict[@"title"];
self.icon = dict[@"icon"];
self.options = dict[@"options"];
}
return self;
}
+ (instancetype)questionWithDict:(NSDictionary *)dict
{
return [[self alloc]initWithDict:dict];
}
@end
ViewController:
@interface ViewController ()
// 所有問題的資料都在這個陣列中
@property (nonatomic, strong) NSArray *questions;
@end
@implementation ViewController
// 懶載入資料
- (NSArray *)questions
{
if (_questions == nil) {
// 載入資料
NSString *path = [[NSBundle mainBundle] pathForResource:@"questions.plist" ofType:nil];
NSArray *arrayDict = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *arrayModel = [NSMutableArray array];
// 遍歷把字典轉模型
for (NSDictionary *dict in arrayDict) {
CZQuestion *model = [CZQuestion questionWithDict:dict];
[arrayModel addObject:model];
}
_questions = arrayModel;
}
return _questions;
}
@end
- 介面搭建:為頁面新增一個UIImageView來作為背景,在頁面添加當前索引,標題,用label實現;再新增右上角分數,以及下方的四個按鈕,這些都是用Button實現;再新增一個圖片瀏覽控制元件,因為需要點選放大,這裡使用button實現,再新增一個Button,實現圖片放大時候的遮罩背景;在底部新增兩個View,在程式碼中動態來新增和移除答案按鈕。
- 為這些控制元件新增對應的屬相變數和點選事件
#import "ViewController.h"
#import "CZQuestion.h"
@interface ViewController () <UIAlertViewDelegate>
// 所有問題的資料都在這個陣列中
@property (nonatomic, strong) NSArray *questions;
// 控制題目索引, 型別的int型別屬性, 預設沒有賦值一開始就是0
@property (nonatomic, assign) int index;
// 記錄頭像按鈕原始的frame
@property (nonatomic, assign) CGRect iconFrame;
@property (weak, nonatomic) IBOutlet UILabel *lblIndex;
@property (weak, nonatomic) IBOutlet UIButton *btnScore;
@property (weak, nonatomic) IBOutlet UILabel *lblTitle;
@property (weak, nonatomic) IBOutlet UIButton *btnIcon;
@property (weak, nonatomic) IBOutlet UIButton *btnNext;
@property (weak, nonatomic) IBOutlet UIView *answerView;
@property (weak, nonatomic) IBOutlet UIView *optionsView;
// 用來引用那個“陰影”按鈕的屬性
@property (weak, nonatomic) UIButton *cover;
- (IBAction)btnNextClick;
- (IBAction)bigImage:(id)sender;
// 頭像按鈕的單擊事件
- (IBAction)btnIconClick:(id)sender;
// 提示
- (IBAction)btnTipClick;
@end
- 實現狀態列白色文字效果。
// 改變狀態列的文字顏色為白色
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
// 隱藏狀態列
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- 實現點選”下一題”功能。
使用一個index屬性來記錄當前顯示的題目的索引。點選”下一題”的時候, 從陣列中獲取對應的題目, 並顯示到對應的介面控制元件上。解決最後一題之後再次點選”下一題”索引越界問題。
sender.enabled = (self.index != self.questions.count - 1)
注意: 設定按鈕圖片的時候, 通過呼叫
setImage:forState:UIControlStateNormal]
方法來設定。
- 思路1:初始化的時候顯示第一個問題到介面上。
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化顯示第一題
self.index = -1;
[self nextQuestion];
}
- 思路2: “檢視大圖”功能的實現思路。
1> 新增一個”陰影”按鈕, 因為該”陰影”要實現點選, 所以用”按鈕”。
2> 然後再把”頭像按鈕”顯示在”陰影”上面。
3> 通過動畫的方式改變”頭像按鈕”的frame(位置和尺寸)變成大圖效果。
- 思路3:點選”遮罩陰影”, 回到小圖
1> 通過動畫慢慢將”遮罩陰影”的透明度變為0
2> 通過動畫慢慢將”頭像圖片”的frame修改為原來的位置
3> 在動畫執行完畢後, 移除”遮罩陰影”
- 思路4:動態生成”答案按鈕”。
0> 在點選”下一題”按鈕中實現該功能
1> 建立一個UIView來存放所有的”答案按鈕”
2> 根據每個問題的答案的文字個數來建立按鈕
3> 每次建立按鈕之前, 先把舊的按鈕都刪除
4> 指定每個”答案按鈕”的尺寸和中間的margin, 然後計算第一個按鈕的x值(marginLeft)。
5> 在迴圈中, 計算每個按鈕的x值(y值都是0)。
- 思路5: 動態生成”待選項按鈕”。
0> 在點選”下一題”按鈕中實現該功能
1> 建立一個UIView來存放所有的”待選項按鈕”
2> 根據每個問題的options陣列中的元素個數來建立按鈕
3> 每次建立按鈕之前, 先把舊的按鈕都刪除
4> 指定每個”答案按鈕”的尺寸和中間的margin, 然後計算第一個按鈕的x值(marginLeft)。
5> 計算每個按鈕的y值。
- 思路6 實現”待選按鈕”的單擊事件
1> 隱藏當前被點選的”待選按鈕”
2> 將當前被點選的”待選按鈕”的文字顯示到”答案按鈕”的左起第一個為空的按鈕上
3> 如果”答案按鈕”已經全部填滿了, 那麼就不允許再點選”待選按鈕”了。
(
* 注意: 只要父控制元件不能處理事件, 那麼子控制元件也無法處理事件。*
如果”答案按鈕”文字填滿了, 則設定option view禁止與使用者互動
self.optionsView.userInteractionEnabled = NO;當用戶再次點選”答案按鈕”時 或 點選”下一題”後在建立”待選按鈕”的時候再次啟用option view與使用者的互動功能
self.optionsView.userInteractionEnabled = YES;
)思路7: 實現”答案按鈕”的單擊事件
1> 設定被點選的”答案按鈕”文字為空(nil)
2> 設定與當前被點選的”答案按鈕”相對應的”待選按鈕”顯示出來
(
* 注意: 這裡注意一個bug, 當答案按鈕中有兩個相同的文字的option按鈕時的問題*
** 解決:
- 為每個option按鈕設定一個唯一的tag
- 在點選某個option按鈕的時候, 把option按鈕的text和tag都設定到answer按鈕上
- 在點選answer按鈕的時候, 判斷answer按鈕的文字與tag同時都與某個option按鈕匹配時, 再顯示這個option按鈕
)
3> 設定所有”答案按鈕”的文字顏色為黑色
- 思路8: 在”待選按鈕”的單擊事件中, 判斷當前的答案的正確性
每次點選”待選按鈕”都需要做判斷, 如果答案按鈕”滿了”, 再去判斷正確性
- 如果正確:
1> 那麼設定”答案按鈕”的文字顏色為藍色
2> +100分
3> 0.5秒鐘後自動跳轉到下一題(如果遇到最後一題, 出現”陣列越界”問題, 需要在”下一題”中做判斷)
(
在self.index++ 之後判斷, 如果陣列越界了, 那麼就彈出一個UIAlertView提示使用者。
) - 如果錯誤:
1> 答案按鈕的文字設定為紅色
思路9:點選”提示”按鈕
1> 扣除100分
2> 清空所有”答案按鈕”的文字(相當於點選了每一個”答案按鈕”, 不是簡單的設定”答案按鈕”的文字為nil)
3> 並將正確答案的第一個文字設定到第一個”答案按鈕”上(相當於正確答案的option按鈕被點選了), 通過呼叫字串的substringToIndex:來擷取第一個字元的字串
對程式碼進行封裝,得到ViewCotroller初步的程式碼:
#import "ViewController.h"
#import "CZQuestion.h"
@interface ViewController () <UIAlertViewDelegate>
// 所有問題的資料都在這個陣列中
@property (nonatomic, strong) NSArray *questions;
// 控制題目索引, 型別的int型別屬性, 預設沒有賦值一開始就是0
@property (nonatomic, assign) int index;
// 記錄頭像按鈕原始的frame
@property (nonatomic, assign) CGRect iconFrame;
@property (weak, nonatomic) IBOutlet UILabel *lblIndex;
@property (weak, nonatomic) IBOutlet UIButton *btnScore;
@property (weak, nonatomic) IBOutlet UILabel *lblTitle;
@property (weak, nonatomic) IBOutlet UIButton *btnIcon;
@property (weak, nonatomic) IBOutlet UIButton *btnNext;
@property (weak, nonatomic) IBOutlet UIView *answerView;
@property (weak, nonatomic) IBOutlet UIView *optionsView;
// 用來引用那個“陰影”按鈕的屬性
@property (weak, nonatomic) UIButton *cover;
- (IBAction)btnNextClick;
- (IBAction)bigImage:(id)sender;
// 頭像按鈕的單擊事件
- (IBAction)btnIconClick:(id)sender;
// 提示
- (IBAction)btnTipClick;
@end
@implementation ViewController
// 懶載入資料
- (NSArray *)questions
{
if (_questions == nil) {
// 載入資料
NSString *path = [[NSBundle mainBundle] pathForResource:@"questions.plist" ofType:nil];
NSArray *arrayDict = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *arrayModel = [NSMutableArray array];
// 遍歷把字典轉模型
for (NSDictionary *dict in arrayDict) {
CZQuestion *model = [CZQuestion questionWithDict:dict];
[arrayModel addObject:model];
}
_questions = arrayModel;
}
return _questions;
}
// 改變狀態列的文字顏色為白色
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
// 隱藏狀態列
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化顯示第一題
self.index = -1;
[self nextQuestion];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
// 點選下一題
- (IBAction)btnNextClick {
// 移動到下一題
[self nextQuestion];
}
// 顯示大圖
- (IBAction)bigImage:(id)sender {
// 記錄一下頭像按鈕的原始frame
self.iconFrame = self.btnIcon.frame;
// 1.建立大小與self.view一樣的按鈕, 把這個按鈕作為一個"陰影"
UIButton *btnCover = [[UIButton alloc] init];
// 設定按鈕大小
btnCover.frame = self.view.bounds;
// 設定按鈕背景色
btnCover.backgroundColor = [UIColor blackColor];
// 設定按鈕透明度
btnCover.alpha = 0.0;
// 把按鈕加到self.view中
[self.view addSubview:btnCover];
// 為陰影按鈕註冊一個單擊事件
[btnCover addTarget:self action:@selector(samllImage) forControlEvents:UIControlEventTouchUpInside];
// 2. 把圖片設定到陰影的上面
// 把self.view中的所有子控制元件中, 只把self.btnIcon顯示到最上層
[self.view bringSubviewToFront:self.btnIcon];
// 通過self.cover來引用btnCover
self.cover = btnCover;
// 3. 通過動畫的方式把圖片變大
CGFloat iconW = self.view.frame.size.width;
CGFloat iconH = iconW;
CGFloat iconX = 0;
CGFloat iconY = (self.view.frame.size.height - iconH) * 0.5;
[UIView animateWithDuration:0.7 animations:^{
// 設定按鈕透明度
btnCover.alpha = 0.6;
// 設定圖片的新的frame
self.btnIcon.frame = CGRectMake(iconX, iconY, iconW, iconH);
}];
}
// 頭像按鈕的單擊事件
- (IBAction)btnIconClick:(id)sender {
if (self.cover == nil) {
// 顯示大圖
[self bigImage:nil];
} else {
[self samllImage];
}
}
// 點選"提示"按鈕
- (IBAction)btnTipClick {
// 1. 分數-1000
[self addScore:-1000];
// 2. 把所有的答案按鈕"清空"(其實這裡的"清空"最好是呼叫每個答案按鈕的單擊事件)
for (UIButton *btnAnswer in self.answerView.subviews) {
// 讓每個答案按鈕點選一下
[self btnAnswerClick:btnAnswer];
}
// 3. 根據當前的索引, 從資料陣列中(self.questions)中找到對應的資料模型
// 從資料模型中獲取正確答案的第一個字元, 把待選按鈕中和這個字元相等的那個按鈕點選一下
CZQuestion *model = self.questions[self.index];
//擷取正確答案中的第一個字元"字串"
NSString *firstChar = [model.answer substringToIndex:1];
// 根據firstChar在option按鈕中找到對應的option 按鈕, 讓這個按鈕點選一下
for (UIButton *btnOpt in self.optionsView.subviews) {
if ([btnOpt.currentTitle isEqualToString:firstChar]) {
[self optionButtonClick:btnOpt]; // 設定某個option 按鈕點選一下
break;
}
}
}
// "陰影"的單擊事件
- (void)samllImage
{
[UIView animateWithDuration:0.7 animations:^{
// 1. 設定btnIcon(頭像)按鈕的frame還原
self.btnIcon.frame = self.iconFrame;
// 2. 讓"陰影"按鈕的透明度變成0
self.cover.alpha = 0.0;
} completion:^(BOOL finished) {
if (finished) {
// 移出"陰影"按鈕
[self.cover removeFromSuperview];
// 當頭像圖片變成小圖以後, 再把self.cover設定成nil
self.cover = nil;
}
}];
}
// 實現UIAlertView的代理方法
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// NSLog(@"%ld", buttonIndex);
if (buttonIndex == 0) {
// 讓程式在回到第0個問題
self.index = -1;
[self nextQuestion];
}
}
// 下一題
- (void)nextQuestion
{
// 1. 讓索引++
self.index++;
// 判斷當前索引是否越界, 入股索引越界, 則提示使用者
if (self.index == self.questions.count) {
//NSLog(@"答題完畢!!!!");
// 彈出一個對話方塊
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"操作提示" message:@"恭喜通關!" delegate:self cancelButtonTitle:@"確定" otherButtonTitles:nil];
// 顯示對話方塊
[alertView show];
return;
}
// 2. 根據索引獲取當前的模型資料
CZQuestion *model = self.questions[self.index];
// 3. 根據模型設定資料
[self settingData:model];
// 4. 動態建立"答案按鈕"
[self makeAnswerButtons:model];
// 5. 動態建立"待選按鈕"
[self makeOptionsButton:model];
}
// 建立待選按鈕
- (void)makeOptionsButton:(CZQuestion *)model
{
// 0. 設定option view 可以與使用者互動
self.optionsView.userInteractionEnabled = YES;
// 1. 清除待選按鈕的view中的所有子控制元件
[self.optionsView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
// 2. 獲取當前題目的待選文字的陣列
NSArray *words = model.options;
// 3. 根據待選文字迴圈來建立按鈕
// 指定每個待選按鈕的大小
CGFloat optionW = 35;
CGFloat optionH = 35;
// 指定每個按鈕之間的間距
CGFloat margin = 10;
// 指定每行有多少個按鈕
int columns = 7;
// 計算出每行第一個按鈕距離左邊的距離
CGFloat marginLeft = (self.optionsView.frame.size.width - columns * optionW - (columns - 1) * margin) / 2;
for (int i = 0; i < words.count; i++) {
// 建立一個按鈕
UIButton *btnOpt = [[UIButton alloc] init];
// 給每個option 按鈕一個唯一的tag值
btnOpt.tag = i;
// 設定按鈕背景
[btnOpt setBackgroundImage:[UIImage imageNamed:@"btn_option"] forState:UIControlStateNormal];
[btnOpt setBackgroundImage:[UIImage imageNamed:@"btn_option_highlighted"] forState:UIControlStateHighlighted];
// 設定按鈕文字
[btnOpt setTitle:words[i] forState:UIControlStateNormal];
// 設定文字顏色為黑色
[btnOpt setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// 計算當前按鈕的列的索引和行的索引
int colIdx = i % columns;
int rowIdx = i / columns;
CGFloat optionX = marginLeft + colIdx * (optionW + margin);
CGFloat optionY = 0 + rowIdx * (optionH + margin);
// 設定按鈕frame
btnOpt.frame = CGRectMake(optionX, optionY, optionW, optionH);
// 把按鈕新增到optionsView中
[self.optionsView addSubview:btnOpt];
// 為待選按鈕註冊單擊事件
[btnOpt addTarget:self action:@selector(optionButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
}
// 待選按鈕的單擊事件
- (void)optionButtonClick:(UIButton *)sender
{
// 1. 隱藏當前被點選的按鈕
sender.hidden = YES;
// 2. 把當前被點選的按鈕的文字顯示到第一個為空的"答案按鈕"上
//NSString *text = [sender titleForState:UIControlStateNormal]; // 獲取按鈕指定狀態下的文字
NSString *text = sender.currentTitle; // 獲取按鈕當前狀態下的文字
// 2.1 把文字顯示到答案按鈕上
// 遍歷每一個答案按鈕
for (UIButton *answerBtn in self.answerView.subviews) {
// 判斷每個"答案按鈕"上的文字是否為nil
if (answerBtn.currentTitle == nil) {
// 把當前點選的待選按鈕的文字設定給對應的答案按鈕
[answerBtn setTitle:text forState:UIControlStateNormal];
// 把當前點選的待選按鈕的tag值也設定給對應的答案按鈕
answerBtn.tag = sender.tag;
break;
}
}
// 3. 判斷答案按鈕是否已經填滿了
// 一開始假設答案按鈕是填滿的
BOOL isFull = YES;
// 宣告一個用來儲存使用者輸入的答案的字串
NSMutableString *userInput = [NSMutableString string];
for (UIButton *btnAnswer in self.answerView.subviews) {
if (btnAnswer.currentTitle == nil) {
isFull = NO;
break;
} else {
// 如果當前答案按鈕上面有文字, 那麼就把這個文字拼接起來
[userInput appendString:btnAnswer.currentTitle];
}
}
// 如果已經填滿, 則禁止option view 控制元件與使用者的互動
if (isFull) {
// 禁止"待選按鈕"被點選
self.optionsView.userInteractionEnabled = NO;
// 獲取當前題目的正確答案
CZQuestion *model = self.questions[self.index];
// 4. 如果答案按鈕被填滿了, 那麼就判斷使用者點選輸入的答案是否與標準答案一致,
if ([model.answer isEqualToString:userInput]) {
// 如果一致, 則設定答案按鈕的文字顏色為藍色, 同時在0.5秒之後跳轉下一題
// 0. 如果正確+100分
[self addScore:100];
//1. 設定所有的答案按鈕的文字顏色為 藍色
[self setAnswerButtonsTitleColor:[UIColor blueColor]];
// 延遲0.5秒後, 跳轉到下一題
[self performSelector:@selector(nextQuestion) withObject:nil afterDelay:0.5];
} else {
// 如果答案不一致(答案錯誤), 設定答案按鈕的文字顏色為紅色
// 設定所有的答案按鈕的文字顏色為 紅色
[self setAnswerButtonsTitleColor:[UIColor redColor]];
}
}
}
// 根據指定的分數, 來對介面上的按鈕進行加分和減分
- (void)addScore:(int)score
{
// 1. 獲取按鈕上現在分值
NSString *str = self.btnScore.currentTitle;
// 2. 把這個分值轉換成數字型別
int currentScore = str.intValue;
// 3. 對這個分數進行操作
currentScore = currentScore + score;
// 4. 把新的分數設定給按鈕
[self.btnScore setTitle:[NSString stringWithFormat:@"%d", currentScore] forState:UIControlStateNormal];
}
// 統一設定答案按鈕的文字顏色
- (void)setAnswerButtonsTitleColor:(UIColor *)color
{
// 遍歷每一個答案按鈕, 設定文字顏色
for (UIButton *btnAnswer in self.answerView.subviews) {
[btnAnswer setTitleColor:color forState:UIControlStateNormal];
}
}
// 載入資料, 把模型資料設定到介面的控制元件上
- (void)settingData:(CZQuestion *)model
{
// 3. 把模型資料設定到介面對應的控制元件上
self.lblIndex.text = [NSString stringWithFormat:@"%d / %ld", (self.index + 1), self.questions.count];
self.lblTitle.text = model.title;
[self.btnIcon setImage:[UIImage imageNamed:model.icon] forState:UIControlStateNormal];
// 4. 設定到達最後一題以後, 禁用"下一題按"鈕
self.btnNext.enabled = (self.index != self.questions.count - 1);
}
// 建立答案按鈕
- (void)makeAnswerButtons:(CZQuestion *)model
{
// 這句話的意思:讓subviews這個陣列中的每個物件, 分別呼叫一次removeFromSuperview方法, 內部執行了迴圈,無需我們自己來些迴圈
[self.answerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
// 5.1 獲取當前答案的文字的個數
NSInteger len = model.answer.length;
// 設定按鈕的frame
CGFloat margin = 10; // 假設每個按鈕之間的間距都是10
CGFloat answerW = 35;
CGFloat answerH = 35;
CGFloat answerY = 0;
CGFloat marginLeft = (self.answerView.frame.size.width - (len * answerW) - (len - 1) * margin) / 2;
// 5.2 迴圈建立答案按鈕, 有幾個文字就建立幾個按鈕
for (int i = 0; i < len; i++) {
// 建立按鈕
UIButton *btnAnswer = [[UIButton alloc] init];
// 設定按鈕的背景圖
[btnAnswer setBackgroundImage:[UIImage imageNamed:@"btn_answer"] forState:UIControlStateNormal];
[btnAnswer setBackgroundImage:[UIImage imageNamed:@"btn_answer_highlighted"] forState:UIControlStateHighlighted];
// 計算按鈕的x值
CGFloat answerX = marginLeft + i * (answerW + margin);
btnAnswer.frame = CGRectMake(answerX, answerY, answerW, answerH);
// 設定答案按鈕的文字顏色
[btnAnswer setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
// 把按鈕加到answerView中
[self.answerView addSubview:btnAnswer];
// 為答案按鈕註冊單擊事件
[btnAnswer addTarget:self action:@selector(btnAnswerClick:) forControlEvents:UIControlEventTouchUpInside];
}
}
// 引數sender, 就表示當前點選的答案按鈕
- (void)btnAnswerClick:(UIButton *)sender
{
// 0. 啟用option view與使用者的互動
self.optionsView.userInteractionEnabled = YES;
// 1. 設定所有的答案按鈕的文字顏色為黑色
[self setAnswerButtonsTitleColor:[UIColor blackColor]];
// 2. 在"待選按鈕"中找到與當前被點選的答案按鈕文字相同待選按鈕,設定該按鈕顯示出來。
for (UIButton *optBtn in self.optionsView.subviews) {
// 比較判斷待選按鈕的文字是否與當前被點選的答案按鈕的文字一致
// if ([sender.currentTitle isEqualToString:optBtn.currentTitle]) {
// optBtn.hidden = NO;
// break;
// }
if (sender.tag == optBtn.tag) {
optBtn.hidden = NO;
break;
}
}
// 1. 清空當前被點選的答案按鈕的文字
[sender setTitle:nil forState:UIControlStateNormal];
}
@end