🙄

未経験エンジニアが35個のクソアプリをリリースした振り返り(1作目) 実装例あり

2023/05/12に公開
自己紹介

私の職業はエンジニアではありません。理学療法士という職業であり、怪我や疾患を患った方に対して、身体的なリハビリを行う職業です。職務経歴は、病院勤務3年→在宅分野で訪問リハビリ4年(現職) です。在宅分野で、働いていると、アナログな場面を多々見かける機会が多く、会社の電子カルテがiPadになったことをきっかけに、swiftを学習し、iOSのアプリ開発を始めることにしました。2023年4月時点で、35個(主にUIKitで、3作はSwiftUI、1作はUnity)のアプリをリリースしました。それらを背景を踏まえながら、コードとともに振り返りを、2023年4月からして行きたいと思います。

2022年の振り返りは、以下のnoteに簡単に記しているので、興味ある方はのぞいみてください。
https://note.com/ryoryoryowww/n/n9c27604a84a0
アプリ集も良かったら、のぞいてみてください。
https://sites.google.com/view/muranakar

コードの書き方に関しては、
アプリ道場サロン(https://community.camp-fire.jp/projects/view/281055)
で学んだり、Twitterで猛者猛者エンジニアの方から学んだことばかりです。

1作目アプリリース時点から、本業以外にやってきたことの振り返り

時期 内容
2022年1月 1作目リリース
同年3月 2,3作目リリース
同年5月 4,5作目リリース
同年6月 6-11作目リリース
同年7月 12-16作目リリース
同年8月 17-20作目リリース
同年9月 21-22作目リリース
同年10月 23-28作目リリース
同年11月 簿記3級受験(合格)
同年12月 Unityでアプリ作成開始
2023年1月 29(Unity製),30作目リリース 、FP3級受験(合格)
同年2月 31,32作目リリース
同年3月 33-35作目リリース

自己紹介は、一度読んだ方は飛ばせるように、折りたたみにしています。
興味のある方は覗いていただければと思います。

今回は1作目のアプリを振り返ります。まず、この振り返りを行う前に、アプリをダウンロードして、全体を触っていただけるとコードを理解しやすいと思います。なぜこのような実装をしているかのイメージがつかみやすいです。そのため、テキトウな値を入力して、アプリで遊んでいただけると嬉しいです。

FIMというアプリで、
https://apps.apple.com/jp/app/fim/id1606480076
で無料でダウンロード可能です。

スクリーンショット 2023-04-16 16.23.23.png

アプリの概要

FIMとは、一つの動作(食事、入浴など)に対して7段階の評価を行う評価指標です。この7段階は評価項目ごとに、こまかい評価基準があるが、あいまいに記録してしまうことがありました。また、電子カルテとは別で、記録して多職種との情報共有として医療従事者に郵送する書類に、身体の動作レベルを評価して記録する必要がありました。そのため、アプリを用いている事によって、①正しく評価できる、②ペーパーレスになる、と思い、アプリを作成しリリースしました。

このアプリ作成で学んだこと

・UIKitの簡単な実装(Button、Slider、TextView、TextField、TableView,など)
・AutoLayoutの実装
Dictionary、Arrayなどの使用方法
RealmでのローカルにおけるCRUD
質問事項をJsonで保存し、JSON→構造体への変換(Decodable)
・Line、Twitterへの共有
・検査結果のコピー&ペースト
検査結果のPDF出力
など

これらの一部の実装例をピックアップして、取り上げていく。

Realmを用いてModelの作成

このアプリは、一つのアプリ(一つの端末)で、複数人の検査者が使用するために作られている。なぜなら、病院のリハビリ室では、1つのiPadを複数人で用いられる場合がある。
そのため、以下の条件を踏まえたクラス・構造体である必要がある。
・複数の検査者のデータを保存する必要がある。
・検査者は、複数人の対象者を検査するため、複数人の対象者のデータを保存する必要がある。
・対象者は、複数回一定の期間毎に、検査をおこなう。そのため、複数の検査結果を保存する必要がある。

データベースとしては、以下のモデル名・プロパティを持ったModelを生成する必要があった。
スクリーンショット 2023-04-16 17.07.12.png

実装例は以下の通りである。
*以下の実装例は、アンチパータンです。なぜかは実装例の後に記載しています。考えながら読んでみてください。
修正した例は、2作目の記事で記載する予定です。

FIM.swift

import Foundation
import RealmSwift

// MARK: - Assessor 評価者
final class Assessor: Object {
    @objc dynamic var uuidString = UUID().uuidString
    @objc dynamic var name = ""
    var targetPersons = List<TargetPerson>()
    var uuid: UUID? {
        UUID(uuidString: uuidString)
    }
    override class func primaryKey() -> String? {
        "uuidString"
    }

    convenience init(name: String) {
        self.init()
        self.name = name
    }
}

// MARK: - TagetPerson 対象者
final class TargetPerson: Object {
    @objc dynamic var uuidString = UUID().uuidString
    @objc dynamic var name = ""
    var FIM = List<FIM>()
    var uuid: UUID? {
        UUID(uuidString: uuidString)
    }
    let assessors = LinkingObjects(fromType: Assessor.self, property: "targetPersons")

    override class func primaryKey() -> String? {
        "uuidString"
    }

    convenience init(name: String) {
        self.init()
        self.name = name
    }
}

// MARK: - FIM 評価指標
final class FIM: Object {
    @objc dynamic var uuidString = UUID().uuidString
    @objc dynamic var eating = 0
    @objc dynamic var grooming = 0
    @objc dynamic var bathing = 0
    @objc dynamic var dressingUpperBody = 0
    @objc dynamic var dressingLowerBody  = 0
    @objc dynamic var toileting = 0
    @objc dynamic var bladderManagement = 0
    @objc dynamic var bowelManagement = 0
    @objc dynamic var transfersBedChairWheelchair = 0
    @objc dynamic var transfersToilet = 0
    @objc dynamic var transfersBathShower = 0
    @objc dynamic var walkWheelchair = 0
    @objc dynamic var stairs = 0
    @objc dynamic var comprehension = 0
    @objc dynamic var expression = 0
    @objc dynamic var socialInteraction = 0
    @objc dynamic var problemSolving = 0
    @objc dynamic var memory = 0
    @objc dynamic var createdAt: Date?
    @objc dynamic var updatedAt: Date?

    let targetPersons = LinkingObjects(fromType: TargetPerson.self, property: "FIM")
    /// 運動項目合計値
    var sumTheMotorSubscaleIncludes: Int {
        eating + grooming + bathing + dressingUpperBody +
        dressingLowerBody + toileting + bladderManagement +
        bowelManagement + transfersBedChairWheelchair +
        transfersToilet + transfersBathShower +
        walkWheelchair + stairs
    }
    /// 認知項目合計値
    var sumTheCognitionSubscaleIncludes: Int {
        comprehension + expression + socialInteraction + problemSolving + memory
    }
    /// 全合計値
    var sumAll: Int {
        eating + grooming + bathing + dressingUpperBody + dressingLowerBody
        + toileting + bladderManagement + bowelManagement
        + transfersBedChairWheelchair + transfersToilet
        + transfersBathShower + walkWheelchair
        + stairs + comprehension + expression
        + socialInteraction + problemSolving + memory
    }

    var uuid: UUID? {
        UUID(uuidString: uuidString)
    }

    override class func primaryKey() -> String? {
        "uuidString"
    }

    convenience init(
        eating: Int,
        grooming: Int,
        bathing: Int,
        dressingUpperBody: Int,
        dressingLowerBody: Int,
        toileting: Int,
        bladderManagement: Int,
        bowelManagement: Int,
        transfersBedChairWheelchair: Int,
        transfersToilet: Int,
        transfersBathShower: Int,
        walkWheelchair: Int,
        stairs: Int,
        comprehension: Int,
        expression: Int,
        socialInteraction: Int,
        problemSolving: Int,
        memory: Int,
        createdAt: Date? = nil,
        updatedAt: Date? = nil
    ) {
        self.init()
        self.eating = eating
        self.grooming = grooming
        self.bathing = bathing
        self.dressingUpperBody = dressingUpperBody
        self.dressingLowerBody = dressingLowerBody
        self.toileting = toileting
        self.bladderManagement = bladderManagement
        self.bowelManagement = bowelManagement
        self.transfersBedChairWheelchair = transfersBedChairWheelchair
        self.transfersToilet = transfersToilet
        self.transfersBathShower = transfersBathShower
        self.walkWheelchair = walkWheelchair
        self.stairs = stairs
        self.comprehension = comprehension
        self.expression = expression
        self.socialInteraction = socialInteraction
        self.problemSolving = problemSolving
        self.memory = memory
        if let createdAt = createdAt {
             self.createdAt = createdAt
        }
        if let updatedAt = updatedAt {
            self.updatedAt = updatedAt
        }
    }
}

FIMRepository.swift

import Foundation
import RealmSwift

final class FIMRepository {
    // swiftlint:disable:next force_cast
    private let realm = try! Realm()

    // MARK: - AssessorRepository
    // 全評価者の呼び出し
    func loadAssessor() -> [Assessor] {
        let assessors = realm.objects(Assessor.self)
        let assessorsArray = Array(assessors)
        return assessorsArray
    }
    // 評価者UUIDによる評価者(一人)の呼び出し
    func loadAssessor(assessorUUID: UUID) -> Assessor? {
        let assessor = realm.object(ofType: Assessor.self, forPrimaryKey: assessorUUID.uuidString)
        return assessor
    }
    // 対象者UUIDによる評価者(一人)の呼び出し
    func loadAssessor(targetPersonUUID: UUID) -> Assessor? {
        guard let fetchedTargetPerson = realm.object(
            ofType: TargetPerson.self,
            forPrimaryKey: targetPersonUUID.uuidString
        ) else { return nil }
        return fetchedTargetPerson.assessors.first
    }
    // 評価者の追加
    func apppendAssessor(assesor: Assessor) {
        // swiftlint:disable:next force_cast
        try! realm.write {
            realm.add(assesor)
        }
    }
    // 評価者の更新
    func updateAssessor(uuid: UUID, name: String) {
        // swiftlint:disable:next force_cast
        try! realm.write {
            let assessor = realm.object(ofType: Assessor.self, forPrimaryKey: uuid.uuidString)
            assessor?.name = name
        }
    }
    // 評価者の削除
    func removeAssessor(uuid: UUID) {
        guard let assessor = realm.object(ofType: Assessor.self, forPrimaryKey: uuid.uuidString) else { return }
        // swiftlint:disable:next force_cast
        try! realm.write {
            realm.delete(assessor)
        }
    }

    // MARK: - TargetPersonRepository
    // 一人の評価者が評価するor評価した、対象者の配列の呼び出し
    func loadTargetPerson(assessorUUID: UUID) -> [TargetPerson] {
        let assessor = realm.object(ofType: Assessor.self, forPrimaryKey: assessorUUID.uuidString)
        guard let targetPersons = assessor?.targetPersons else { return [] }
        let targetPersonsArray = Array(targetPersons)
        return targetPersonsArray
    }
    // 一人の対象者のUUIDから、一人の対象者の呼び出し
    func loadTargetPerson(targetPersonUUID: UUID) -> TargetPerson? {
        let targetPerson = realm.object(ofType: TargetPerson.self, forPrimaryKey: targetPersonUUID.uuidString)
        return targetPerson
    }

    // 一つのFIMのUUIDから、そのFIMがどの対象者かの呼び出し
    func loadTargetPerson(fimUUID: UUID) -> TargetPerson? {
        guard let fetchedFIM = realm.object(ofType: FIM.self, forPrimaryKey: fimUUID.uuidString) else { return nil }
        return fetchedFIM.targetPersons.first
    }
    //  一人の評価者の対象者の追加
    func appendTargetPerson(assessorUUID: UUID, targetPerson: TargetPerson) {
        guard let list = realm.object(
            ofType: Assessor.self,
            forPrimaryKey: assessorUUID.uuidString
        )?.targetPersons else { return }
        // swiftlint:disable:next force_cast
        try! realm.write {
            list.append(targetPerson)
        }
    }
    // 一人の対象者のデータ更新
    func updateTargetPerson(uuid: UUID, name: String) {
        try! realm.write {
            let targetPerson = realm.object(ofType: TargetPerson.self, forPrimaryKey: uuid.uuidString)
            targetPerson?.name = name
        }
    }
    // 一人の対象者のデータ削除
    func removeTargetPerson(targetPersonUUID: UUID) {
        guard let fetchedTagetPerson = realm.object(
            ofType: TargetPerson.self,
            forPrimaryKey: targetPersonUUID.uuidString
        ) else { return }
        // swiftlint:disable:next force_cast
        try! realm.write {
            realm.delete(fetchedTagetPerson)
        }
    }

    // MARK: - FIMRepository
    // 一つのFIMのUUIDから、FIMのデータの呼び出し
    func loadFIM(fimUUID: UUID) -> FIM? {
        let fim = realm.object(ofType: FIM.self, forPrimaryKey: fimUUID.uuidString)
        return fim
    }
    // 一人の対象者のUUIDから、複数のFIMのデータの呼び出し(並び替えあり)
    func loadFIM(
        targetPersonUUID: UUID,
        sortedAscending: Bool
    ) -> [FIM] {
        let fimList = realm.object(
            ofType: TargetPerson.self,
            forPrimaryKey: targetPersonUUID.uuidString
        )?.FIM.sorted(
            byKeyPath: "createdAt",
            ascending: sortedAscending
        )
        guard let fimList = fimList else { return [] }
        let fimListArray = Array(fimList)
        return fimListArray
    }
    //  一人の対象者のFIMデータの追加
    func appendFIM(targetPersonUUID: UUID, fim: FIM) {
        guard let list = realm.object(
            ofType: TargetPerson.self,
            forPrimaryKey: targetPersonUUID.uuidString
        )?.FIM else { return }
        // swiftlint:disable:next force_cast
        try! realm.write {
            fim.createdAt = Date()
            list.append(fim)
        }
    }

    func updateFIM(fimItemNumArray: [Int], fimUUID: UUID) {
        // swiftlint:disable:next force_cast
        try! realm.write {
            let loadedFIM = realm.object(ofType: FIM.self, forPrimaryKey: fimUUID.uuidString)
            loadedFIM?.eating = fimItemNumArray[0]
            loadedFIM?.grooming = fimItemNumArray[1]
            loadedFIM?.bathing = fimItemNumArray[2]
            loadedFIM?.dressingUpperBody = fimItemNumArray[3]
            loadedFIM?.dressingLowerBody = fimItemNumArray[4]
            loadedFIM?.toileting = fimItemNumArray[5]
            loadedFIM?.bladderManagement = fimItemNumArray[6]
            loadedFIM?.bowelManagement = fimItemNumArray[7]
            loadedFIM?.transfersBedChairWheelchair = fimItemNumArray[8]
            loadedFIM?.transfersToilet = fimItemNumArray[9]
            loadedFIM?.transfersBathShower = fimItemNumArray[10]
            loadedFIM?.walkWheelchair = fimItemNumArray[11]
            loadedFIM?.stairs = fimItemNumArray[12]
            loadedFIM?.comprehension = fimItemNumArray[13]
            loadedFIM?.expression = fimItemNumArray[14]
            loadedFIM?.socialInteraction = fimItemNumArray[15]
            loadedFIM?.problemSolving = fimItemNumArray[16]
            loadedFIM?.memory = fimItemNumArray[17]
            loadedFIM?.updatedAt = Date()
        }
    }
    // FIMデータの削除
    func removeFIM(fimUUID: UUID) {
        guard let fetchedFIM = realm.object(ofType: FIM.self, forPrimaryKey: fimUUID.uuidString) else { return }
        // swiftlint:disable:next force_cast
        try! realm.write {
            realm.delete(fetchedFIM)
        }
    }
}

アンチパータンの理由としては、全体的にRealmに依存したコードになってしまうためです。

問題点

Realmからデータベースツールを変更する際に(例えばRealmから、CoreDataに変更するetc)、コード修正部分が多いことが欠点になります。ViewContoller(以下VCに略)上でも、class 〇〇:Object {} と定義しているRealmのモデルを呼び出す際に用いるクラスになってしまいます。そのため、VCファイルがRealmの影響を受けています。

修正点

VCファイルがRealmに依存しないように、Repositoryファイルで、structの型に変更する必要があります。そのため、Realmのモデルとは別に、構造体として、structを作成する必要があります。2作目で実装例を提示しますので、少々お待ちください。一応アンチパータンとして記しておきます。

PDF出力に関して

スクリーンショット 2023-04-16 18.21.32.png
上記の画像のようなPDF出力を行った。
実装例を記す。

FIMPDF.swift
import Foundation
import QuickLook

final class FIMPDF {
    private let assessor: Assessor
    private let targetPerson: TargetPerson
    private let fim: FIM

    // 検査者は誰で、対象者は誰で、いつの検査結果かを、PDFの出力する構造体の初期値として設定する必要がある。
    init(assessor: Assessor,
         targetPerson: TargetPerson,
         fim: FIM) {
        self.assessor = assessor
        self.targetPerson = targetPerson
        self.fim = fim
    }

    // PDFのデータをNSDataとして出力する。
    private func getPDF() -> NSData {
        let renderer = UIPrintPageRenderer()
        let paperSize = CGSize(width: 595.2, height: 841.8)
        let paperFrame = CGRect(origin: .zero, size: paperSize)
        renderer.setValue(paperFrame, forKey: "paperRect")
        renderer.setValue(paperFrame, forKey: "printableRect")
        let formatter = UIMarkupTextPrintFormatter(markupText: makeHTMLString())
        renderer.addPrintFormatter(formatter, startingAtPageAt: 0)
        let pdfData = NSMutableData()
        UIGraphicsBeginPDFContextToData(pdfData, .zero, [:])
        for pageI in 0..<renderer.numberOfPages {
            UIGraphicsBeginPDFPage()
            renderer.drawPage(at: pageI, in: UIGraphicsGetPDFContextBounds())
        }
        UIGraphicsEndPDFContext()
        return pdfData
    }
    
    // PDFのファイル名を設定して、PDFとして書き出す。
    func saveToTempDirectory() -> URL? {
        let tempDirectory = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
        let fileName = "FIM-" + "\(targetPerson.name)-\(String(describing: fim.createdAt!))" + ".pdf"
        let filePath = tempDirectory.appendingPathComponent(fileName)
        do {
            try getPDF().write(to: filePath)
            return filePath
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }

    //どのようなPDFの内容かを、HTML・CSSで書き出す。
    //このHTML・CSSの中に、検査者・対象者・検査結果に関して、書き出す。
    // swiftlint:disable:next function_body_length
    private func makeHTMLString() -> String {
        var createdAtString = "--"
        if let createdAt = fim.createdAt {
            createdAtString  = dateFormatter(date: createdAt)
        }
        return """
        <!DOCTYPE html>
        <html>
            <head>
                    <title>FIM結果</title>
            <style>
                    table, th, td {
                      border: 1px solid black;
                      border-collapse: collapse;
                    }
            </style>
            <body>
                <h1>\(targetPerson.name) 様</h1>
                <h2 style="text-align:right"> 作成日 \(createdAtString)</h2>
                <h2>FIM(Functional Independence Measure、機能的自立度評価表)</h2>
                <table style="width:100%">
        <tr>
                    <td colspan="2">項目</td>
                    <td colspan="2">点数</td>
                </tr>
                <tr>
                    <td rowspan="6">セルフケア(42点)</td>
                    <td>A 食事(箸・スプーン)</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.eating).string)</td>
                </tr>
                <tr>
                    <td>B 整容</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.grooming).string)</td>
                </tr>
                <tr>
                    <td>C 清拭</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.bathing).string)</td>
                </tr>
                <tr>
                    <td>D 更衣(上半身)</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.dressingUpperBody).string)</td>
                </tr>
                <tr>
                    <td>E 更衣(下半身)</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.dressingLowerBody).string)</td>
                </tr>
                <tr>
                    <td>F トイレ</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.toileting).string)</td>
                </tr>
                <tr>
                    <td rowspan="2">排泄(14点)</td>
                    <td>G 排尿コントロール</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.bladderManagement).string)</td>
                </tr>
                <tr>
                    <td>H 排便コントロール</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.bowelManagement).string)</td>
                </tr>
                <tr>
                    <td rowspan="3">移乗(21点)</td>
                    <td>I ベッド、椅子、車椅子</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.transfersBedChairWheelchair).string)</td>
                </tr>
                <tr>
                    <td>J トイレ</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.transfersToilet).string)</td>
                </tr>
                <tr>
                    <td>K 浴槽、シャワー</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.transfersBathShower).string)</td>
                </tr>
                <tr>
                    <td rowspan="2">移動(14点)</td>
                    <td>L 歩行、車椅子</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.walkWheelchair).string)</td>
                </tr>
                <tr>
                    <td>M 階段</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.stairs).string)</td>
                </tr>
                <tr>
                    <td rowspan="2">コミュニケーション(14点)</td>
                    <td>N 理解(聴覚、視覚)</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.comprehension).string)</td>
                </tr>
                <tr>
                    <td>O 表出(音声、非音声)</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.expression).string)</td>
                </tr>
                <tr>
                    <td rowspan="3">社会認識(21点)</td>
                    <td>P 社会的交流</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.socialInteraction).string)</td>
                </tr>
                <tr>
                    <td>Q 問題解決</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.problemSolving).string)</td>
                </tr>
                <tr>
                    <td>R 記憶</td>
                    <td>1-7点</td>
                    <td>\(FIMString(fim: fim.memory).string)</td>
                </tr>
                <tr>
                    <td colspan="2">合計</td>
                    <td>18 - 126点</td>
                    <td>\(fim.sumAll)</td>
                </tr>
                </table>
                </body>
            </html>
        """
    }
}

// 初期値としてInt型を設定して、値が0であれば、「未入力」、値が0以外であればそのまま値をString型に変更するクラスである。
// アンチパターン。何をしているクラスかわからないし、`0=「未入力」` ではなく、`nil=「未入力」`とすべき
final private class FIMString {
    fileprivate var string = ""

    init(fim: Int) {
        if fim == 0 {
            self.string = "未入力"
            return
        }
        self.string = String(fim)
    }
}

private extension FIMPDF {
    func dateFormatter(date: Date) -> String {
        let dateFormatter = Foundation.DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP")
        dateFormatter.dateStyle = .medium
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let dateString = dateFormatter.string(from: date)
        return dateString
    }
}

JSONファイルを、独自の構造体に変換する

JSONファイルを用いる理由としては、各項目ごとに注意点や、評価基準が異なるため、JSONで管理したほうが早いと思い、JSONファイルを作成しました。詳しく説明すると、検査を行うにあたって、各動作(食事、入浴、排泄etc)の各項目は、1-7点で採点を行い、その採点基準が各項目の1-7点ごとに異なっており、評価するにあたっての注意点を配慮する必要があります。

まず、最初にJSONファイルから変換する前に、ファイルに関連する構造体を定義する必要がある。

/// FIMの採点基準
    struct FimScoringCriteria: Decodable {
        var fimItem: String
        var seven: String
        var six: String
        var five: String
        var four: String
        var three: String
        var two: String
        var one: String
        var attention: String
    }

FimScoringCriteriaの中身は、fimItemは各項目の名前、sevenoneまでは、その各評価項目ごとに、1点から7点まで、細かく採点基準があり、その採点基準の説明文が入力される。attentionは各項目の採点前に注意しておくべき注意点の文章が入力される。

次に変換するための、JsonFileを用意する。

FIM.json
[
    {
        "fimItem": "食事",
        "seven": "完全自立",
        "six": "配膳前にきざんでもらってあり、1人で食べている。\n自助具を使用して自立していたり、エプロンを自分でかけて使用する。",
        "five": "肉を切る\n蓋をあける\nエプロンをかける。",
        "four": "口の中に食物が溜まっていないか、\n介助者が指で確認している場合",
        "three": "自助具をつけてもらい、\n食物をスプーンにのせてもらうと、\n自分で口に運び、\n嚥下する",
        "two": "・口に運ぶ\n・飲み込む\nの1つのみ行える",
        "one": "咀嚼や嚥下は可能であるが、口にまったく運べない。\n口に運ぶことのほうが、採点では重視されている。",
        "attention": "ーー注意点ーー\n"
    },
    {
        "fimItem": "整容",
        "seven": "歯・義歯を磨く\n櫛などで髪をとかす\n手洗い\n洗顔\n行っていればひげそりまたは化粧\nを全て自力で行っている。",
        "six": "時間を要す\n自助具を使用している",
        "five": "歯磨き粉を歯ブラシにつけてもらう。\nタオルを準備してもらう。\n自助具を準備・装着してもらう",
        "four": "歯・義歯を磨く\n櫛などで髪をとかす\n手洗い\n洗顔\n行っていればひげそりまたは化粧\n5項目のうち、1項目、介助を受けている",
        "three": "歯・義歯を磨く\n櫛などで髪をとかす\n手洗い\n洗顔\n行っていればひげそりまたは化粧\n5項目のうち2項目、介助を受けている",
        "two": "歯・義歯を磨く\n櫛などで髪をとかす\n手洗い\n洗顔\n行っていればひげそりまたは化粧\n5項目のうち、どの項目も半分以上介助を受けている",
        "one": "歯・義歯を磨く\n櫛などで髪をとかす\n手洗い\n洗顔\n行っていればひげそりまたは化粧\n5項目のうち、4項目、介助を受けている",
        "attention": "ーー注意点ーー\n整容は5つの動作の集まりです。\n1) 口腔ケア 2)洗顔\n3)手洗い 4)整髪\n5)化粧または髭剃り\nただし、5つめの化粧または髭剃りは、行う必要がないことが多いため、していない場合は、その他の4項目で評価します。それぞれの項目について、何%しているかを評価し、平均します。"
    }
 
           // 省略
]

以下の関数を用いて、JSONファイルから、独自の構造体へ変換する。

private var fimScoringCriteria: [FimScoringCriteria] = []

// 省略

// MARK: - JSONファイルのデコーダー 
 private func decodeFimJsonFile() {
        let data: Data?
        guard let file = Bundle.main.url(forResource: "FIM", withExtension: "json") else {
            fatalError("ファイルが見つかりません。メソッド名:[\(#function)]")
        }
        do {
            data  = try Data(contentsOf: file)
        } catch {
            fatalError("ファイルをロード不可。メソッド名:[\(#function)]")
        }

        do {
            guard let data = data else {
                fatalError("dataの中身が入っていない。メソッド名:[\(#function)]")
            }
            let decoder = JSONDecoder()
            fimScoringCriteria = try decoder.decode([FimScoringCriteria].self, from: data)
        } catch {
            fatalError("パース不可。メソッド名:[\(#function)]")
        }
    }

修正点

変換後にfimScoringCriteriaというArrayに値を代入しているが、この関数名からは、Arrayにデコードした値を代入していると理解できないので、✕。関数名を変更するか、配列に代入せずに配列を関数の返り値に変更する必要がある。

Dictionary・Arrayを用いて、UIButtonと関連付ける。

スクリーンショット 2023-04-17 20.18.27.png
スクリーンショット 2023-04-17 20.18.39.png

1のボタンを押した時に、1のボタンに関連する文章
3のボタンを押した時に、3のボタンに関連する文章
を表示するような挙動を行いたい時に、Dictionary・Arrayを用いて、関連付けて管理すると、個人的にわかりやすかったです。Twitterの猛者猛者エンジニアの方に、教えていただきました。

final class AssessmentViewController: UIViewController {
    @IBOutlet private weak var textView: UITextView!
    @IBOutlet private weak var button1: UIButton!
    @IBOutlet private weak var button2: UIButton!
    @IBOutlet private weak var button3: UIButton!
    @IBOutlet private weak var button4: UIButton!
    @IBOutlet private weak var button5: UIButton!
    @IBOutlet private weak var button6: UIButton!
    @IBOutlet private weak var button7: UIButton!
    private var buttons: [UIButton] {
        [
            button1, button2, button3, button4, button5, button6, button7
        ]
    }

    // 各ボタンと関連付ける際に、用いる文字列の配列。
    // この配列は、FIMの項目ごとに、配列の文字列が変化する。
    private var fimItemText: [String] {
        [
            fimScoringCriteria[fimItemCount].one,
            fimScoringCriteria[fimItemCount].two,
            fimScoringCriteria[fimItemCount].three,
            fimScoringCriteria[fimItemCount].four,
            fimScoringCriteria[fimItemCount].five,
            fimScoringCriteria[fimItemCount].six,
            fimScoringCriteria[fimItemCount].seven
        ]
    }
    // UIButtonが押された際に、そのボタンに合った文章を管理するため、辞書型で関連付けた。
    private var dictionaryButtonAndString: [UIButton: String] {
        [UIButton: String](uniqueKeysWithValues: zip(buttons, fimItemText))
    }
    @IBAction private func selectedFIMNum(sender: UIButton) {
        // ボタンが選択された際に、そのボタンと関連づけられた文字列を、テキストビューに反映させる。
        textView.text = dictionaryButtonAndString[sender]
    }

}

@IBActionで 引数にUIButtonを設定して、押されたUIButtonを用いて、Dictionaryで対応した文章を呼び出す挙動になっています。

以上振り返りでした。

書いてみて思ったのですが、全部振り返るのは面倒ですね。。
ぼちぼちやって行きます。

他にも良い方法があれば、コメントいただけると大変うれしいです。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします!

https://sites.google.com/view/muranakar
個人でアプリを作成しているので、良かったら覗いてみてください!

Discussion