【iOS】未経験エンジニアが35個のアプリをリリースした振り返り(1作目)実装例あり
自己紹介
私の職業はエンジニアではありません。理学療法士という職業であり、怪我や疾患を患った方に対して、身体的なリハビリを行う職業です。職務経歴は、病院勤務3年→在宅分野で訪問リハビリ4年(現職) です。在宅分野で、働いていると、アナログな場面を多々見かける機会が多く、会社の電子カルテがiPadになったことをきっかけに、swiftを学習し、iOSのアプリ開発を始めることにしました。2023年4月時点で、35個(主にUIKitで、3作はSwiftUI、1作はUnity)のアプリをリリースしました。それらを背景を踏まえながら、コードとともに振り返りを、2023年4月からして行きたいと思います。
2022年の振り返りは、以下のnoteに簡単に記しているので、興味ある方はのぞいみてください。
アプリ集も良かったら、のぞいてみてください。コードの書き方に関しては、
アプリ道場サロン(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というアプリで、
で無料でダウンロード可能です。アプリの概要
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を生成する必要があった。
実装例は以下の通りである。
*以下の実装例は、アンチパータンです。なぜかは実装例の後に記載しています。考えながら読んでみてください。
修正した例は、2作目の記事で記載する予定です。
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
}
}
}
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出力に関して
上記の画像のようなPDF出力を行った。
実装例を記す。
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
は各項目の名前、seven
〜one
までは、その各評価項目ごとに、1点から7点まで、細かく採点基準があり、その採点基準の説明文が入力される。attention
は各項目の採点前に注意しておくべき注意点の文章が入力される。
次に変換するための、JsonFileを用意する。
[
{
"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と関連付ける。
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のフォローよろしくお願いいたします!
個人でアプリを作成しているので、良かったら覗いてみてください!
Discussion