2018/01/23

Swift – 4択クイズアプリのサンプル(Swift3.0)

備忘録。
四択クイズのサンプルコード。
最終的にはテキストフィールドにフリーで入力した内容との整合性チェックを行いたい。

問題文の読み出し

import Foundation

class QuestionData {
    // 問題文
    var question: String

    // 選択肢1
    var answer1: String
    // 選択肢2
    var answer2: String
    // 選択肢3
    var answer3: String
    // 選択肢4
    var answer4: String

    // 正解の番号
    var correctAnswerNumber: Int

    // ユーザが選択した選択肢の番号
    var userChoiceAnswerNumber: Int?

    // 問題文の番号
    var questionNo: Int = 0

    // クラスが生成された時の処理
    init(questionSourceDataArray: [String]) {
        question = questionSourceDataArray[0]
        answer1 = questionSourceDataArray[1]
        answer2 = questionSourceDataArray[2]
        answer3 = questionSourceDataArray[3]
        answer4 = questionSourceDataArray[4]
        correctAnswerNumber = Int(questionSourceDataArray[5])!
    }

    // ユーザが選択した答えが正解かどうか判定する
    func isCorrect() -> Bool {
        // 答えが一致しているかどうか判定する
        if correctAnswerNumber == userChoiceAnswerNumber {
            // 正解
            return true
        }
        // 不正解
        return false
    }
}

// クイズデータ全般の管理と生成を管理するクラス
class QuestionDataManager {

    // シングルトンのオブジェクトを生成
    static let sharedInstance = QuestionDataManager()

    // 問題を格納するための配列
    var questionDataArray = [QuestionData]()

    // 現在の問題のインデックス
    var nowQuestionIndex = 0

    // 初期化処理
    private init() {
        // シングルトンであることを保証するためにprivateで宣言しておく
    }

    // 問題文の読み込み処理
    func loadQuestion() {
        // 格納済みの問題文であればいったん削除しておく
        questionDataArray.removeAll()
        // 現在の問題のインデックスを初期化
        nowQuestionIndex = 0

        // csvファイルパスを取得
        guard let csvFilePath = Bundle.main.path(forResource: "question", ofType: "csv") else {
            // csvファイルなし
            print("csvファイルが存在しません")
            return
        }

        do {
            let csvStringData = try String(contentsOfFile: csvFilePath, encoding: String.Encoding.utf8)
            // csvデータを1行ずつ読み込む
            csvStringData.enumerateLines { (line, stop) in
                // カンマ区切りで分割
                let questionSourceDataArray = line.components(separatedBy: ",")
                // 問題データを格納するオブジェクトを作成
                let questionData = QuestionData(questionSourceDataArray: questionSourceDataArray)
                // 問題を追加
                self.questionDataArray.append(questionData)
                // 問題番号を設定
                questionData.questionNo = self.questionDataArray.count
            }
        } catch let error {
            print("csvファイル読み込みエラーが発生しました\(error)")
            return
        }
    }

    // 次の問題を取り出す
    func nextQuestion() -> QuestionData? {
        if nowQuestionIndex < questionDataArray.count {
            let nextQuestion = questionDataArray[nowQuestionIndex]
            nowQuestionIndex += 1
            return nextQuestion
        }
        return nil
    }


}

開始画面

import UIKit

class StartViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.

        // 問題文の読込
        QuestionDataManager.sharedInstance.loadQuestion()

        // 遷移先画面の呼び出し
        guard let nextViewController = segue.destination as? QuestionViewController else {
            // 取得できずに終了
            return
        }

        // 問題文の取り出し
        guard let questionData = QuestionDataManager.sharedInstance.nextQuestion() else {
            // 取得できずに終了
            return
        }

        // 問題文のセット
        nextViewController.questionData = questionData
    }

    // タイトルに戻ってくるときに呼び出される処理
    @IBAction func goToTitle(_ segue: UIStoryboardSegue) {

    }

}

問題と回答チェック


import UIKit
import AudioToolbox

class QuestionViewController: UIViewController {

    var questionData: QuestionData!
    @IBOutlet weak var questionNoLabel: UILabel!         // 問題番号ラベル
    @IBOutlet weak var questionTextView: UITextView!     // 問題文テキストビュー
    @IBOutlet weak var answer1Button: UIButton!          // 選択肢1ボタン
    @IBOutlet weak var answer2Button: UIButton!          // 選択肢2ボタン
    @IBOutlet weak var answer3Button: UIButton!          // 選択肢3ボタン
    @IBOutlet weak var answer4Button: UIButton!          // 選択肢4ボタン
    @IBOutlet weak var correctImageView: UIImageView!    // 正解時のイメージビュー
    @IBOutlet weak var incorrectImageView: UIImageView!  // 不正解時のイメージビュー

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        // 初期データ設定処理。前画面で設定済みのquestionDataから値を取り出す
        questionNoLabel.text = "Q.\(questionData.questionNo)"
        questionTextView.text = questionData.question
        answer1Button.setTitle(questionData.answer1, for: UIControlState.normal)
        answer2Button.setTitle(questionData.answer2, for: UIControlState.normal)
        answer3Button.setTitle(questionData.answer3, for: UIControlState.normal)
        answer4Button.setTitle(questionData.answer4, for: UIControlState.normal)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // 選択肢1をタップ
    @IBAction func tapAnswer1Button(_ sender: Any) {
        questionData.userChoiceAnswerNumber = 1      // 選択した答えの番号を保存する
        goNextQuestionWithAnimation()                // 次の問題に進む
    }

    // 選択肢2をタップ
    @IBAction func tapAnswer2Button(_ sender: Any) {
        questionData.userChoiceAnswerNumber = 2      // 選択した答えの番号を保存する
        goNextQuestionWithAnimation()                // 次の問題に進む
    }

    // 選択肢3をタップ
    @IBAction func tapAnswer3Button(_ sender: Any) {
        questionData.userChoiceAnswerNumber = 3      // 選択した答えの番号を保存する
        goNextQuestionWithAnimation()                // 次の問題に進む
    }

    // 選択肢4をタップ
    @IBAction func tapAnswer4Button(_ sender: Any) {
        questionData.userChoiceAnswerNumber = 4      // 選択した答えの番号を保存する
        goNextQuestionWithAnimation()                // 次の問題に進む
    }

    // 次の問題にアニメーション付きで進む
    func goNextQuestionWithAnimation() {
        // 正解しているか判定する
        if questionData.isCorrect() {
            // 正解のアニメーションを再生しながら次の問題へ遷移する
            goNextQuestionWithCorrectAnimation()
        } else {
            // 不正解のアニメーションを再生しながら次の問題へ遷移する
            goNextQuestionWithIncorrectAnimation()
        }
    }

    // 次の問題に正解のアニメーション付きで遷移する
    func goNextQuestionWithCorrectAnimation() {
        // 正解を伝える音を鳴らす
        AudioServicesPlayAlertSound(1025)

        // アニメーション
        UIView.animate(withDuration: 2.0, animations: {
            // アルファ値を1.0に変化させる(初期値はStoryboardで0.0に設定済み)
            self.correctImageView.alpha = 1.0
        }) { (Bool) in
            self.goNextQuestion() // アニメーション完了後に次の問題に進む
        }
    }

    // 次の問題に不正解のアニメーション付きで遷移する
    func goNextQuestionWithIncorrectAnimation() {
        // 不正解を伝える音を鳴らす
        AudioServicesPlayAlertSound(1006)
        // アニメーション
        UIView.animate(withDuration: 2.0, animations: {
            // アルファ値を1.0に変化させる(初期値はStoryboardで0.0に設定済み)
            self.incorrectImageView.alpha = 1.0
        }) { (Bool) in
            self.goNextQuestion() // アニメーション完了後に次の問題に進む
        }
    }

    // 次の問題へ遷移する
    func goNextQuestion() {
        // 問題文の取り出し
        guard let nextQuestion =
            QuestionDataManager.sharedInstance.nextQuestion() else {
                // 問題文がなければ結果画面へ遷移する
                // StoryboardのIdentifierに設定した値(result)を指定して
                // ViewControllerを生成する
                if let resultViewController = storyboard?.instantiateViewController(withIdentifier: "result") as? ResultViewController {
                    // StoryboardのSegueを利用しない明示的な画面遷移処理
                    present(resultViewController, animated: true, completion: nil)
                }
                return
        }
        // 問題文がある場合は次の問題へ遷移する
        // StoryboardのIdentifierに設定した値(question)を設定して
        // ViewControllerを生成する
        if let nextQuestionViewController =
            storyboard?.instantiateViewController(withIdentifier: "question")
                as? QuestionViewController {
            nextQuestionViewController.questionData = nextQuestion
            // StoryboardのSegueを利用しない明示的な画面遷移処理
            present(nextQuestionViewController, animated: true,
                    completion: nil)
        }
    }

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    }
    */

}

回答ページ


import UIKit

class ResultViewController: UIViewController {

    @IBOutlet weak var correctPercentLabel: UILabel! // 正解率ラベル

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        // 問題数を取得する
        let questionCount =
            QuestionDataManager.sharedInstance.questionDataArray.count
        // 正解数を取得する
        var correctCount: Int = 0
        // 正解数を計算する
        for questionData in
            QuestionDataManager.sharedInstance.questionDataArray {
                if questionData.isCorrect() {
                    // 正解数を増やす
                    correctCount += 1
                }
        }
        // 正解率を計算する
        let correctPercent: Float =
            (Float(correctCount) / Float(questionCount)) * 100
        // 正解率を小数第一位まで計算して画面に反映する
        correctPercentLabel.text =
            String(format: "%.1f", correctPercent) + "%"
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    }
    */

}

お知らせ

ヒヨコ歩数計という歩きながらヒヨコが育っていくアプリを作って、いろんな方に結構使ってもらっています。
RealmSwift, Admobの動画・インステ・バナー広告、UICollectionView、iOS-Charts、UITableViewを使用しているので、是非ご利用ください!