【iOS】これまでに使用してきた技術集
これまでにリリースしてきたアプリの使用した技術と学んだことを振り返ります。
初回のアプリリリース日が2022年1月23日です。
現時点(2024/06/22時点)で、合計約12万ダウンロードです。
- UIKit 34個
- SwiftUI 5個(内の2つがWatchOS)
- Unity 1個
の合計40個のアプリをリリースしました。
開発した順番で振り返っていきます。
FIM:生活の自立度を評価する!病院で必須評価!
-
UITableView
によるリスト表示 -
UITextField
による入力処理 -
NavigationController
を用いた画面遷移 -
AutoLayout
を用いてUI実装 -
Xib
によるTableViewのCell表示 - RepositoryパターンでのRealmのCRUD操作
-
QuickLook
を用いたPDF出力 - JSONから構造体への変換(Decodable)
- Line、Twitterへの共有機能
個別タイム測定:一人ひとりの時間を計測
- QuartzCoreを用いたタイマー実装。
→ CADisplayLinkを用いてタイマー実装。(画面のリフレッシュレートに同期しているため。) - 今回のアプリとは別で、Timerを用いた実装も、サンプルアプリで行っている。
HDS-R:認知症を簡単に検査する:痴呆症を評価する
- UIImageViewを用いた画像の表示
- UITapGestureRecognizerを用いたタップ処理
- UIVisualEffectView(frame: view.frame)を用いた背景のぼかし
- UIViewControllerTransitioningDelegate、UIViewControllerAnimatedTransitioningによる画面遷移アニメーションのカスタマイズ
医療・介護にまつわる事業所を探すためのマップ
厚生労働省の医療・介護にまつわる事業所のデータベースを用いて実装。
- CoreLocationを用いたマップ表示
MKMapView,CLLocationManager,MKPointAnnotation(),MKCoordinateRegion()【緯度・経度用いて】,MKMapViewDelegate,CLLocationManagerDelegateを用いて - PickerViewKeyboardDelegateを用いてPickeViewの表示
- UIActivityIndicatorViewを用いて、インジケーターの表示実装
- UISegmentを用いて、条件ごとの検索結果の表示
- WebKitをもちいて、事業所の詳細を閲覧できるように実装
もじつなぐ:認知症 発達障害の方の脳トレゲーム
- csvファイルを構造体へデコード
- CoreGraphics,UIBezierPath,CALayer,Drawing などを用いて描画処理
みつけもじ:認知症 発達障害の脳トレゲーム
上に表示されている文字を、探してタップするアプリ。
UICollectionViewの実装
- UICollectionViewDataSourceを用いて、値の設定
- UICollectionViewDelegateを用いて、タップされた際の処理の設定
- UICollectionViewDelegateFlowLayoutを用いてレイアウトの設定
声日記・議事録・診察レコーダー
一部仕様は変更しているが、大まかな機能は同じ。
- 録音ボタンのアニメーションの実装
- AVFoundation(AVAudioRecorderを用いて録音)
- Speechを用いた音声認識
- NotificationCenterを用いて、バックグラウンド遷移したかを通知
- FileManagerを用いた録音データ(.m4a)のCRUD
- FSCalendar(ライブラリー)を用いて、カレンダーの実装
- Calendarを用いたカレンダーの処理
- AVAudioPlayerを用いた、音声ファイルの再生、再生開始時点の指定、スキップの実装
- AVAudioPlayerDelegateを用いた、音声データ再生終了時の処理
- UITextViewDelegateを用いた、テキスト入力中のデータ保存処理
コミュボード:耳が遠い方とのコミュニケーションツール
難聴の高齢者の方とのコミュニケーションを円滑にするためのアプリ
- ローカライズ対応
- iPhone、iPadのAutoLayoutでのUI調整
- SKStoreReviewControllerを用いた、レビュー処理
- ATTrackingManagerを用いたトラッキング実装
- Admobを用いた広告実装
最高順位は、iPadの メディカル カテゴリー で 3位になりました。
印刷カレンダー:簡単シンプルに年月プリントできる
PDFでカレンダーを月ごとに印刷することができる。
- カレンダーを作成する際に、HTML・CSSのコードを swiftで実装
例
protocol Builder {
func build() -> String
}
struct Cell: Builder {
func build() -> String {
var content = "<div style=\"font-size: 30px;\">\(day)</div>"
if let schedule1 = schedule1 {
content += "<div style=\"font-size: 20px;\">\(schedule1)</div>"
}
if let schedule2 = schedule2 {
content += "<div style=\"font-size: 20px;\">\(schedule2)</div>"
}
if let schedule3 = schedule3 {
content += "<div style=\"font-size: 20px;\">\(schedule3)</div>"
}
return "<td>\(content)</td>"
}
init(day: Int, weekDay: WeekDay) {
self.day = day
self.weekDay = weekDay
self.schedule1 = nil
self.schedule2 = nil
self.schedule3 = nil
}
init(day: Int,schedule1: String?,schedule2: String?,schedule3: String?, weekDay: WeekDay) {
self.day = day
self.weekDay = weekDay
self.schedule1 = schedule1
self.schedule2 = schedule2
self.schedule3 = schedule3
}
init(day: Int,schedule1: String?, weekDay: WeekDay) {
self.day = day
self.weekDay = weekDay
self.schedule1 = schedule1
self.schedule2 = nil
self.schedule3 = nil
}
var schedule1: String?
let schedule2: String?
let schedule3: String?
let day: Int
let weekDay: WeekDay
}
struct Row: Builder {
func build() -> String {
"<tr height=\"160\">\(cells.map{ $0.build() }.joined())</tr>"
}
let cells: [Cell]
}
struct Table: Builder {
func build() -> String {
"<table>\(rows.map{ $0.build() }.joined())</table>"
}
let rows: [Row]
}
- PDFデータの作成
- HTML,CSSを用いた、PDFでカレンダー作成
- QuickLookを用いたPDFでカレンダー表示
家族爆弾ゲーム-写真を使って,みんなでドキドキ
黒ひげ危機一発のようなハズレが一つ紛れていて、それを押すと動画が流れる。
- UIImagePickerControllerを用いた写真の選択
- RSKImageCropper(ライブラリー)を用いた、写真データの切り取り
- UIGraphicsBeginImageContext,UIGraphicsGetCurrentContextを用いた描画処理
- 描画処理から、 UIGraphicsGetImageFromCurrentImageContextを用いて画像を作成
- 画像データの切り取った部分を透明にするために、pngデータに変更
脳トレ123:ランダム表示&難易度調整が可能!
数字Version
ひらがなVersion
- コードでUIButtonを生成したり、削除実施。
- ランダムにUIButtonが表示されるため、重ならないようにするための実装。
ボタンの重なりを確認するためのコード
struct ButtonPosition {
var minX: CGFloat
var maxX: CGFloat
var minY: CGFloat
var maxY: CGFloat
func isOverlap(labelPosition: ButtonPosition) -> Bool {
let rangeX = minX...maxX
let rangeY = minY...maxY
let isOverlapMinX = rangeX.contains(labelPosition.minX)
let isOverlapMaxX = rangeX.contains(labelPosition.maxX)
let isOverlapMinY = rangeY.contains(labelPosition.minY)
let isOverlapMaxY = rangeY.contains(labelPosition.maxY)
if isOverlapMinX && isOverlapMinY { return true }
if isOverlapMinX && isOverlapMaxY { return true }
if isOverlapMaxX && isOverlapMinY { return true }
if isOverlapMaxX && isOverlapMaxY { return true }
return false
}
}
- UIDeviceを用いて、デバイスごとに表示する文字の量を変更している。iPhone,iPad別。
デバイスをBoolで判定するためのコード
struct DeviceType {
static func isIPhone() -> Bool {
return UIDevice.current.userInterfaceIdiom == .phone
}
static func isIPad() -> Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
}
脳トレ:ピクトグラムで注意力UP!ランダム表示&難易度調整
真ん中に指定された画像が表示され、それと同じ画像をタップする。
画像が回転しているため、メンタルローテーションのトレーニングになります。
- 画像をランダムに回転させる実装
例
let randomRotationAngle = CGFloat(Float(Array(1...360).randomElement()!))
newImageView.transform = CGAffineTransform(rotationAngle: randomRotationAngle)
四則演算の記号を入れるゲーム
- 計算の実装
例
enum GameModeNumCount {
case two
case three
case four
case five
}
enum CalculationMethod: CaseIterable {
case addition
case subtraction
case multiplication
case division
func textSymbol() -> String {
switch self {
case .addition:
return "+"
case .subtraction:
return "-"
case .multiplication:
return "*"
case .division:
return "/"
}
}
func textName() -> String {
switch self {
case .addition:
return "addition"
case .subtraction:
return "subtraction"
case .multiplication:
return "multiplication"
case .division:
return "division"
}
}
}
struct FillBlankCalculation {
var num1: Int
var num2: Int
var num3: Int?
var num4: Int?
var num5: Int?
var result: Double?
var gameModeNumCount: GameModeNumCount
var correctAnswerCalculationMethods: [CalculationMethod]
var answerCalculationMethods: [CalculationMethod]
init (gameModeNumCount: GameModeNumCount) {
self.gameModeNumCount = gameModeNumCount
let randomNumArray = Array(1...9)
num1 = randomNumArray.randomElement()!
num2 = randomNumArray.randomElement()!
switch gameModeNumCount {
case .two:
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!]
case .three:
num3 = randomNumArray.randomElement()!
correctAnswerCalculationMethods = [CalculationMethod.allCases.randomElement()!,CalculationMethod.allCases.randomElement()!]
case .four:
num3 = randomNumArray.randomElement()!
num4 = randomNumArray.randomElement()!
correctAnswerCalculationMethods = [CalculationMethod.allCases.randomElement()!,CalculationMethod.allCases.randomElement()!,CalculationMethod.allCases.randomElement()!]
case .five:
num3 = randomNumArray.randomElement()!
num4 = randomNumArray.randomElement()!
num5 = randomNumArray.randomElement()!
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!]
}
answerCalculationMethods = []
result = calculateResult(correctAnswerCalculationMethods)
}
mutating func processWhenTheCorrectAnswer() {
resetAllNumAndResultAndCalculation()
}
mutating func processWhenTheInCorrectAnswer() {
answerCalculationMethods = []
}
mutating func resetAllNumAndResultAndCalculation() {
answerCalculationMethods = []
correctAnswerCalculationMethods = []
initializeAllNum()
initializeCalculationMethods()
result = calculateResult(correctAnswerCalculationMethods)
}
mutating func initializeAllNum() {
let randomNumArray = Array(1...9)
num1 = randomNumArray.randomElement()!
num2 = randomNumArray.randomElement()!
switch gameModeNumCount {
case .two:
break
case .three:
num3 = randomNumArray.randomElement()!
case .four:
num3 = randomNumArray.randomElement()!
num4 = randomNumArray.randomElement()!
case .five:
num3 = randomNumArray.randomElement()!
num4 = randomNumArray.randomElement()!
num5 = randomNumArray.randomElement()!
}
}
func calculateResult(_ calculationMethods: [CalculationMethod]) -> Double {
var resultText = ""
switch gameModeNumCount {
case .two:
resultText = "\(Double(num1))" + "\(calculationMethods[0].textSymbol())" + "\(Double(num2))"
case .three:
resultText = "\(Double(num1))" + "\(calculationMethods[0].textSymbol())" + "\(Double(num2))" + "\(calculationMethods[1].textSymbol())" + "\(Double(num3!))"
case .four:
resultText = "\(Double(num1))" + "\(calculationMethods[0].textSymbol())" + "\(Double(num2))" + "\(calculationMethods[1].textSymbol())" + "\(Double(num3!))" + "\(calculationMethods[2].textSymbol())" + "\(Double(num4!))"
case .five:
resultText = "\(Double(num1))" + "\(calculationMethods[0].textSymbol())" + "\(Double(num2))" + "\(calculationMethods[1].textSymbol())" + "\(Double(num3!))" + "\(calculationMethods[2].textSymbol())" + "\(Double(num4!))" + "\(calculationMethods[3].textSymbol())" + "\(Double(num5!))"
}
let expression = NSExpression(format: resultText)
let answer = expression.expressionValue(with: nil, context: nil) as! Double
return answer
}
mutating func initializeCalculationMethods() {
switch gameModeNumCount {
case .two:
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!]
case .three:
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!,CalculationMethod.allCases.randomElement()!]
case .four:
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!]
case .five:
correctAnswerCalculationMethods =
[CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!,
CalculationMethod.allCases.randomElement()!]
}
}
/// 回答するときに用いるメソッド
mutating func answer(calculationMethod: CalculationMethod) {
answerCalculationMethods.append(calculationMethod)
}
func isCorrectTheAnswer() -> Bool {
let answerResult = calculateResult(answerCalculationMethods)
if result == answerResult { return true }
return false
}
}
- GameKitを用いた Game Centerの実装
例
struct GameCenter {
var player: GKLocalPlayer
func authenticateLocalPlayer() {
let player = GKLocalPlayer.local
player.authenticateHandler = { _, error in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
}
}
func showLocationTopLeft() {
GKAccessPoint.shared.isActive = true
GKAccessPoint.shared.location = .topLeading
}
func showLocationBottomLeft() {
GKAccessPoint.shared.isActive = true
GKAccessPoint.shared.location = .bottomLeading
}
static func hiddenUI() {
GKAccessPoint.shared.isActive = false
}
func sendLeaderboardWithID(score: Int,leaderboardID: String) {
if GKLocalPlayer.local.isAuthenticated {
GKLeaderboard.submitScore(
score,
context: 0,
player: GKLocalPlayer.local,
leaderboardIDs: [leaderboardID]) { error in
print(error ?? "")
}
} else {
print("GameCenterにログインしていません")
}
}
}
- AVFoundation (AVAudioPlayerなど) を用いて、正解時の音声出力
例
extension GameViewController: AVAudioPlayerDelegate {
func playSound(name: String) {
guard let path = Bundle.main.path(forResource: name, ofType: "mp3") else {
print("音源ファイルが見つかりません")
return
}
do {
// AVAudioPlayerのインスタンス化
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
// AVAudioPlayerのデリゲートをセット
audioPlayer.delegate = self
audioPlayer.prepareToPlay()
if audioPlayer.isPlaying {
audioPlayer.stop()
audioPlayer.currentTime = 0
}
// 音声の再生
audioPlayer.play()
} catch {
}
}
}
記憶力に特化した脳トレ
記憶力に特化した脳トレ-シンプルで簡単にトレーニング
- 画像 と 色を用いたゲームロジック、CustomViewを作成(MagicNumberが多く、反省)
例
enum ShapesAll {
static let allUIColor: [UIColor] = [.blue,.darkGray,.magenta,.brown,.cyan,.green,.lightGray,.orange,.purple,.red,.systemIndigo]
static let allColorAndColorTextDictionary: [UIColor: String] =
[.blue: "blue",.darkGray: "darkGray",.magenta: "magenta",.brown:"brown",.cyan:"cyan",.green:"green",.lightGray:"lightGray",.orange:"orange",.purple:"purple",.red:"red",.systemIndigo:"systemIndigo"]
static let allImageName: [String] = ["circle","circle.fill","square","square.fill","app","app.fill","rectangle","rectangle.fill","rectangle.portrait","rectangle.portrait.fill","capsule","capsule.fill","capsule.portrait","capsule.portrait.fill","oval","oval.fill","oval.portrait","oval.portrait.fill","triangle","triangle.fill","diamond","diamond.fill","octagon","octagon.fill","hexagon","hexagon.fill","pentagon","pentagon.fill","seal","seal.fill","rhombus","rhombus.fill","shield","shield.fill","rectangle.roundedtop","rectangle.roundedtop.fill","rectangle.roundedbottom","rectangle.roundedbottom.fill"]
static func randomUIColor() -> UIColor {
return allUIColor.randomElement()!
}
static func randomImageName() -> String {
return allImageName.randomElement()!
}
}
struct Shape {
let color: UIColor
let colorText: String
let imageName: String
init() {
self.color = ShapesAll.randomUIColor()
self.colorText = ShapesAll.allColorAndColorTextDictionary[color]!
self.imageName = ShapesAll.randomImageName()
}
}
class CustomImageView: UIImageView {
let shape: Shape = Shape()
let width = 60
let height = 60
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.frame.size = .init(width: width, height: height) // 位置とサイズの指定
self.tintColor = shape.color
self.image = UIImage(systemName: shape.imageName)
self.isUserInteractionEnabled = true
}
func isSameShape(customImageView: CustomImageView) -> Bool {
if self.shape.color == customImageView.shape.color &&
self.shape.imageName == customImageView.shape.imageName {
return true
}
return false
}
}
struct ImageViewPosition {
var minX: CGFloat
var maxX: CGFloat
var minY: CGFloat
var maxY: CGFloat
func isOverlap(imageViewPosition: ImageViewPosition) -> Bool {
let rangeX = minX...maxX
let rangeY = minY...maxY
let isOverlapMinX = rangeX.contains(imageViewPosition.minX)
let isOverlapMaxX = rangeX.contains(imageViewPosition.maxX)
let isOverlapMinY = rangeY.contains(imageViewPosition.minY)
let isOverlapMaxY = rangeY.contains(imageViewPosition.maxY)
if isOverlapMinX && isOverlapMinY { return true }
if isOverlapMinX && isOverlapMaxY { return true }
if isOverlapMaxX && isOverlapMinY { return true }
if isOverlapMaxX && isOverlapMaxY { return true }
return false
}
}
struct ShapeGame {
let defaultNumber: [Int]
var problemSampleImageViewNumber: Int
let imageViewWidthAndHeight: Int
var problemRangeX: ClosedRange<CGFloat>
var problemRangeY: ClosedRange<CGFloat>
var answerRangeX: ClosedRange<CGFloat>
var answerRangeY: ClosedRange<CGFloat>
var answerRandomImageViewPosition: [ImageViewPosition] = []
var answerRandomCustomImageView: [CustomImageView] = []
var problemRandomImageViewPosition: [ImageViewPosition] = []
var problemRandomCustomImageView: [CustomImageView] = []
mutating func reset() {
answerRandomImageViewPosition = []
answerRandomCustomImageView = []
problemRandomImageViewPosition = []
problemRandomCustomImageView = []
configureAnswerRandomImageViewPosition()
configureAnswerRandomCustomImageView()
configureProblemRandomImageViewPosition()
configureProblemRandomCustomImageView()
}
mutating func resetAnswerRandomImageViewPosition() {
answerRandomImageViewPosition = []
configureAnswerRandomImageViewPosition()
for i in 1...answerRandomImageViewPosition.count {
let index: Int = i - 1
answerRandomCustomImageView[index].frame = CGRect(
x: Int(answerRandomImageViewPosition[index].minX),
y: Int(answerRandomImageViewPosition[index].minY),
width: imageViewWidthAndHeight,
height: imageViewWidthAndHeight
)
}
}
mutating func configureAnswerRandomImageViewPosition() {
for i in defaultNumber {
while answerRandomImageViewPosition.count < i {
let randomValueX = CGFloat.random(in: answerRangeX)
let randomValueY = CGFloat.random(in: answerRangeY)
let newImageViewPosition = ImageViewPosition(
minX: randomValueX,
maxX: randomValueX + CGFloat(imageViewWidthAndHeight),
minY: randomValueY,
maxY: randomValueY + CGFloat(imageViewWidthAndHeight)
)
let boolArray = answerRandomImageViewPosition.compactMap { imageViewPosition in
return imageViewPosition.isOverlap(imageViewPosition: newImageViewPosition)
}
if boolArray.contains(true) {
continue
}
answerRandomImageViewPosition.append(newImageViewPosition)
}
}
}
mutating func configureAnswerRandomCustomImageView() {
for i in Array(1...answerRandomImageViewPosition.count) {
let index: Int = i - 1
let customImageView = CustomImageView(
frame: CGRect(
x: Int(answerRandomImageViewPosition[index].minX),
y: Int(answerRandomImageViewPosition[index].minY),
width: imageViewWidthAndHeight,
height: imageViewWidthAndHeight
)
)
answerRandomCustomImageView.append(customImageView)
}
}
mutating func configureProblemRandomImageViewPosition() {
let problemSampleImageViewArrayIndex = Array(0...problemSampleImageViewNumber - 1)
for i in problemSampleImageViewArrayIndex {
while problemRandomImageViewPosition.count - 1 < i {
let randomValueX = CGFloat.random(in: problemRangeX)
let randomValueY = CGFloat.random(in: problemRangeY)
let newImageViewPosition = ImageViewPosition(
minX: randomValueX,
maxX: randomValueX + CGFloat(imageViewWidthAndHeight),
minY: randomValueY,
maxY: randomValueY + CGFloat(imageViewWidthAndHeight)
)
let boolArray = problemRandomImageViewPosition.compactMap { imageViewPosition in
return imageViewPosition.isOverlap(imageViewPosition: newImageViewPosition)
}
if boolArray.contains(true) {
continue
}
problemRandomImageViewPosition.append(newImageViewPosition)
}
}
}
mutating func configureProblemRandomCustomImageView() {
problemRandomCustomImageView = Array( answerRandomCustomImageView.prefix(problemSampleImageViewNumber))
var index = 0
let newProblemRandomCustomImageView = problemRandomCustomImageView.map { customImageview in
customImageview.frame = CGRect(
x: Int(problemRandomImageViewPosition[index].minX),
y: Int(problemRandomImageViewPosition[index].minY),
width: imageViewWidthAndHeight,
height: imageViewWidthAndHeight
)
index += 1
return customImageview
}
problemRandomCustomImageView = newProblemRandomCustomImageView
}
}
曜日・干支・山手線を用いた脳トレクイズ
曜日・干支・山手線を用いた脳トレクイズ
- ゲームロジックの実装 曜日version
例
//曜日
enum DayOfTheWeek: Int, Equatable {
case sunday = 1
case monday = 2
case tuesday = 3
case wednesday = 4
case thursday = 5
case friday = 6
case saturday = 7
func textJapanese() -> String {
switch self {
case .sunday:
return "日"
case .monday:
return "月"
case .tuesday:
return "火"
case .wednesday:
return "水"
case .thursday:
return "木"
case .friday:
return "金"
case .saturday:
return "土"
}
}
//曜日の判定
static func set(for date: Date) -> Self {
let calendar = Calendar(identifier: .gregorian)
let weekdayNumber = calendar.component(.weekday, from: date)
let weekDay = DayOfTheWeek(rawValue: weekdayNumber)!
return weekDay
}
}
//Quiz用のイニシャライザ
extension DayOfTheWeek {
init?(quizValue: Int) {
// 7で割りきれた余りによって判定
var quizValue = quizValue % 7
if quizValue == 0 {
quizValue = 7
}
//マイナスだったら1週間分に進めて曜日を決定
if quizValue < 0 {
quizValue = quizValue + 7
}
self.init(rawValue: quizValue)
}
}
//Quizのレベル
enum QuizLevel {
case hard
case normal
case easy
}
enum DateTextFromNum {
case dateAfter(dateNum: Int)
case dateBefore(dateNum: Int)
init(dateNum: Int) {
if dateNum > 0 {
self = .dateAfter(dateNum: dateNum)
} else if dateNum < 0{
self = .dateBefore(dateNum: dateNum)
} else {
fatalError("DateTextFromNumに、引数に誤りがあります。")
}
}
func text() -> String {
switch self {
case .dateAfter(dateNum: 2):
return "明後日"
case .dateAfter(dateNum: 1):
return "明日"
case .dateBefore(dateNum: -1):
return "昨日"
case .dateBefore(dateNum: -2):
return "一昨日"
case .dateAfter(dateNum: let dateNum):
return "\(dateNum)日後"
case .dateBefore(dateNum: let dateNum):
return "\(abs(dateNum))日前"
}
}
}
struct Quiz {
private let level: QuizLevel
private let today = Date()
private let firstDateNumber: Int
private let secondDateNumber: Int
private let thirdDateNumber: Int
var text: String {
let firstText = DateTextFromNum(dateNum: firstDateNumber).text()
let secondText = DateTextFromNum(dateNum: secondDateNumber).text()
let thirdText = DateTextFromNum(dateNum: thirdDateNumber).text()
let totalText: String
switch level {
case .hard: totalText = "今日の\(firstText)の\n\(secondText)の\n\(thirdText)は、\n何曜日?"
case .normal: totalText = "今日の\(firstText)の\n\(secondText)は、\n何曜日?"
case .easy: totalText = "今日の\(firstText)は、\n何曜日?"
}
return totalText
}
init(level: QuizLevel) {
self.level = level
var randomNumber: Int {
Array(-7...7)
.filter{ $0 != 0 }
.randomElement()!
}
firstDateNumber = randomNumber
secondDateNumber = randomNumber
thirdDateNumber = randomNumber
}
//Test用
init(first: Int, second: Int, third: Int, level : QuizLevel) {
firstDateNumber = first
secondDateNumber = second
thirdDateNumber = third
self.level = level
}
func answer() -> DayOfTheWeek {
let weekDay = DayOfTheWeek.set(for: today)
// 今日の曜日に各クイズの日数を合算
let weekDayNumber: Int
switch level {
case .hard:
weekDayNumber = weekDay.rawValue + firstDateNumber + secondDateNumber + thirdDateNumber
case .normal:
weekDayNumber = weekDay.rawValue + firstDateNumber + secondDateNumber
case .easy:
weekDayNumber = weekDay.rawValue + firstDateNumber
}
return DayOfTheWeek(quizValue: weekDayNumber)!
}
}
struct WeekDayGame {
private var quiz: Quiz
let quizLevel: QuizLevel
var correctCount: Int = 0
var missCount: Int = 0
init(level: QuizLevel) {
quiz = Quiz(level: level)
quizLevel = level
}
init(testQuiz: Quiz,level: QuizLevel) {
quiz = testQuiz
quizLevel = level
}
func displayQuizText() -> String {
quiz.text
}
mutating func reset() {
self.quiz = Quiz(level: quizLevel)
}
mutating func answer(input: DayOfTheWeek) -> Bool {
let answer = quiz.answer()
if answer == input {
self.correctCount += 1
} else {
self.missCount += 1
}
return answer == input ? true : false
}
func answer() -> DayOfTheWeek{
return quiz.answer()
}
}
数字を用いた記憶力を鍛える脳トレ
- UIBottonから、CustomButtonを作成して実装
例
class CustomButton: UIButton {
let width = 60
let height = 60
var showedNumber: Int?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.frame.size = .init(width: width, height: height) // 位置とサイズの指定
self.isUserInteractionEnabled = true
}
}
- Buttonのポジションと、重なっているかどうかを判定
例
struct ButtonPosition {
var minX: CGFloat
var maxX: CGFloat
var minY: CGFloat
var maxY: CGFloat
func isOverlap(buttonPosition: ButtonPosition) -> Bool {
let rangeX = minX...maxX
let rangeY = minY...maxY
let isOverlapMinX = rangeX.contains(buttonPosition.minX)
let isOverlapMaxX = rangeX.contains(buttonPosition.maxX)
let isOverlapMinY = rangeY.contains(buttonPosition.minY)
let isOverlapMaxY = rangeY.contains(buttonPosition.maxY)
if isOverlapMinX && isOverlapMinY { return true }
if isOverlapMinX && isOverlapMaxY { return true }
if isOverlapMaxX && isOverlapMinY { return true }
if isOverlapMaxX && isOverlapMaxY { return true }
return false
}
}
動いている数字を順番通りにタップする脳トレ
脳トレ123:動く数字をタップ!ランダム表示!高齢者向け
Unity製です。
物理エンジンを用いて実装する必要があり、Unityの方が物理エンジンの実装がやりやすく、今回用いた。
チュートリアルとして、ブロック崩し、脱出ゲームも実装しました。
Unityは、Game空間の中の一つのオブジェクト(立方体・球)に対して、C#のスクリプトを設定できるところに、面白さを感じた。ただUnityはGUI操作が多く、UIKitの方がコード管理する部分が多く、個人的に実装しやすかったため、この1作のみとした。
- PDFで 文章を出力の実装
例
final class PDF {
private let goalFontSize: Int
private let toDoFontSize: Int
private let goalTexts: [String]
private let toDoTexts: [String]
private let space: Int
init(
goalFontSize: Int,
toDoFontSize: Int,
goalText: String,
toDoText: String,
space: Int
) {
self.goalFontSize = goalFontSize
self.toDoFontSize = toDoFontSize
let goalArray = goalText.split(whereSeparator: \.isNewline).map { String($0) }
self.goalTexts = goalArray
let toDoArray = toDoText.split(whereSeparator: \.isNewline).map { String($0) }
self.toDoTexts = toDoArray
self.space = space
}
private func getPDF() -> NSData {
let renderer = UIPrintPageRenderer()
let verticalPaperSize = CGSize(width: 595.2, height: 841.8)
let paperFrame = CGRect(origin: .zero, size: verticalPaperSize)
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
}
func saveToTempDirectory() -> URL? {
let tempDirectory = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
let fileName = "GoalToDo" + ".pdf"
let filePath = tempDirectory.appendingPathComponent(fileName)
do {
try getPDF().write(to: filePath)
return filePath
} catch {
print(error.localizedDescription)
return nil
}
}
// swiftlint:disable:next function_body_length
private func makeHTMLString() -> String {
return """
<!DOCTYPE html>
<html>
<head>
<title></title>
<style>
table,
th,
tr,
td {
table-layout: fixed;
text-align: left;
vertical-align: top;
width: 100%;
border: 1px solid black;
border-collapse: collapse;
margin: 0%;
}
</style>
<body>
<div id="YCalTbl" style="font-size: 50px;padding: \(space)px" class="calleft">
<div><span style="font-size: 50px;"></span></div>
<table border="1" cellpadding="10" cellspacing="0">
<tbody>
\(changeGoalTextHTML())
\(changeToDoTextHTML())
</tbody>
</table>
</div>
</body>
</html>
"""
}
func changeGoalTextHTML() -> String {
var totaltext = ""
goalTexts.forEach { goalText in
totaltext +=
"""
<div><span style="font-size: \(goalFontSize)px;">\(goalText)</span></div>
"""
}
totaltext += "<br>"
return totaltext
}
func changeToDoTextHTML() -> String {
var totaltext = ""
toDoTexts.forEach { toDoText in
totaltext +=
"""
<div><span style="font-size: \(toDoFontSize)px;">\(toDoText)</span></div>
"""
}
return totaltext
}
}
- Countの履歴をRealmでCRUD (Repositoryパターンで実装)
例
final class CountHistoryRepasitory {
private let realm = try! Realm()
// 並び替えあり
func loadCountHistoryAndRemoveMoreThan300(ascending: Bool) -> [CountHistory] {
let realmCountHistorys = realm
.objects(RealmCountHistory.self)
.sorted(byKeyPath: "createdAt",ascending: ascending)
let realmCountHistorysArray = Array(realmCountHistorys)
let countHistorys = realmCountHistorysArray.map {CountHistory(managedObject: $0) }
// 先頭の300個以外削除
Array(countHistorys.dropFirst(300)).forEach { countHistory in
removeCountHistory(countHistory: countHistory)
}
let first100CountHistorys = Array(countHistorys.prefix(300))
return first100CountHistorys
}
func apppendCountHistory(countHistory: CountHistory) {
try! realm.write {
let realmCountHistory = countHistory.managedObject()
realm.add(realmCountHistory)
}
}
func removeCountHistory(countHistory: CountHistory) {
guard let countHistory = realm.object(
ofType: RealmCountHistory.self,
forPrimaryKey: countHistory.uuidString
) else { return }
// swiftlint:disable:next force_cast
try! realm.write {
realm.delete(countHistory)
}
}
}
struct CountHistory {
var uuidString = UUID().uuidString
var countValue: Int
var createdAt: Date = Date()
var uuid: UUID? {
UUID(uuidString: uuidString)
}
}
class RealmCountHistory: Object {
@Persisted(primaryKey: true)
var uuidString = ""
@Persisted
var countValue: Int
@Persisted
var createdAt: Date
var uuid: UUID? {
UUID(uuidString: uuidString)
}
convenience init(countValue: Int,createdAt: Date) {
self.init()
self.countValue = countValue
self.createdAt = createdAt
}
}
private extension CountHistory {
init(managedObject: RealmCountHistory) {
self.uuidString = managedObject.uuidString
self.countValue = managedObject.countValue
self.createdAt = managedObject.createdAt
}
func managedObject() -> RealmCountHistory {
let realmCountHistory = RealmCountHistory()
realmCountHistory.uuidString = self.uuidString
realmCountHistory.countValue = self.countValue
realmCountHistory.createdAt = self.createdAt
return realmCountHistory
}
}
- カウントが設定した達成値に到達したら、音がなる必要があるので、達成値をRealmでCRUD
例
final class AchievementRepasitory {
private let realm = try! Realm()
// 並び替えあり
func loadAchievement(ascending: Bool) -> [Achievement] {
let realmAchievements = realm
.objects(RealmAchievement.self)
.sorted(byKeyPath: "value", ascending: ascending)
let realmAchievementsArray = Array(realmAchievements)
let achievements = realmAchievementsArray.map {Achievement(managedObject: $0) }
return achievements
}
func apppendAchievement(achievement: Achievement) {
try! realm.write {
let realmAchievement = achievement.managedObject()
realm.add(realmAchievement)
}
}
func removeAchievement(achievement: Achievement) {
guard let achievement = realm.object(
ofType: RealmAchievement.self,
forPrimaryKey: achievement.uuidString
) else { return }
// swiftlint:disable:next force_cast
try! realm.write {
realm.delete(achievement)
}
}
}
struct Achievement {
var uuidString = UUID().uuidString
var value: Int
var uuid: UUID? {
UUID(uuidString: uuidString)
}
}
class RealmAchievement: Object {
@Persisted(primaryKey: true)
var uuidString = ""
@Persisted
var value: Int
var uuid: UUID? {
UUID(uuidString: uuidString)
}
convenience init(value: Int) {
self.init()
self.value = value
}
}
private extension Achievement {
init(managedObject: RealmAchievement) {
self.uuidString = managedObject.uuidString
self.value = managedObject.value
}
func managedObject() -> RealmAchievement {
let realmAchievement = RealmAchievement()
realmAchievement.uuidString = self.uuidString
realmAchievement.value = self.value
return realmAchievement
}
}
OKかCancelが表示されるアラート、テキストフィールドありのアラートの 実装
例
final class Alert {
static func okAndCancelAlert(vc: UIViewController, title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) {
let cancelAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
cancelAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler))
cancelAlertVC.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
vc.present(cancelAlertVC, animated: true, completion: nil)
}
static func textFieldAlert(vc: UIViewController, title: String, message: String,text: String, placeholder: String, securyText: Bool,isNumberPad: Bool, handler: ((String?) -> Void)? = nil) {
let textFieldAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
textFieldAlertVC.addTextField { (textField) in
textField.placeholder = placeholder
textField.text = text
if isNumberPad {
textField.keyboardType = .numberPad
}
}
textFieldAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
handler?(textFieldAlertVC.textFields?.first?.text)
}))
textFieldAlertVC.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
vc.present(textFieldAlertVC, animated: true, completion: nil)
}
}
参考URL
- MBCircularProgressBar(ライブラリー)を用いてProgressBarの実装
- Realmの初期値追加の処理
- Timerのロジック 【Timer用のデリゲートを作成して】(多いため一部のみ)
→休憩・トレーニングのタイミングで、読み上げ・音声出力を実装する必要があったため、デリゲートを用いて処理を分けた。
例
import Foundation
enum TimerMode {
case notRunning
case running
case continueRunning
case pausing
}
class CountDownTimer {
var delegate: CountDownTimerDelegate!
enum TraningType {
case training
case rest
case restBetweenSets
case preparation
}
var isPause: Bool {
switch timerMode {
case .notRunning,.running, .continueRunning:
return false
case .pausing:
return true
}
}
var isRunning: Bool {
switch timerMode {
case .running,.continueRunning:
return true
case .notRunning, .pausing:
return false
}
}
var isContinueRunning: Bool {
switch timerMode {
case .notRunning,.running, .pausing:
return false
case .continueRunning:
return true
}
}
var trainigModeText: String {
switch trainigMode {
case .training:
return NSLocalizedString("training", comment: "")
case .rest:
return NSLocalizedString("rest", comment: "")
case .restBetweenSets:
return NSLocalizedString("restBetweenSets", comment: "")
case .preparation:
return NSLocalizedString("preparation", comment: "")
}
}
---
省略
---
func start() {
print("スタート")
timerMode = .running
delegate?.functionAllWhenStartTimerMode()
startPreparation()
}
func continueRunning() {
print("再開")
timerMode = .continueRunning
delegate?.functionAllWhenStartTimerMode()
isImmediatelyAfterContinueRunning = true
switch trainigMode {
case .training:
startTraning()
case .rest:
startRest()
case .restBetweenSets:
startRestBetweenSets()
case .preparation:
startPreparation()
}
}
func pause() {
print("一時停止")
timerMode = .pausing
delegate?.functionAllWhenStartTimerMode()
timer.invalidate()
}
func reset() {
trainigMode = .preparation
timerMode = .notRunning
delegate?.functionAllWhenStartTimerMode()
timer.invalidate()
resetInitValue()
delegate?.functionWhenReset()
}
---
省略
---
}
protocol CountDownTimerDelegate {
func functionAllStart()
func functionTraningStart()
func functionTraningInterval()
func functionTraningWhenTheValueReachesZero()
---
省略
---
func functionAllWhenStartTimerMode()
func functionWhenReset()
func functionWhenTimerEnds()
func functionWhenAllInterval()
func functionWhenAllThreeSecondsAgo()
}
以下全て SwiftUIで作成
テンプレートが用意されている読み上げアプリ
CSVファイルの作成(CSVからデコード+スタティックメソッドで管理)
例
CSVデータ一覧
struct CSV {
private static let allFileName: [String] = Array(1...22).map { String($0) }
private static var countCalledConvertMethod = 0
private static func callCSVDataText(fileName: String) -> String {
var csvString: String = ""
if let path = Bundle.main.path(forResource: fileName, ofType: "csv") {
do {
csvString = try String(contentsOfFile: path, encoding: .utf8)
} catch {
print("Error reading CSV file: \(error)")
}
} else {
print("Error finding CSV file")
}
return csvString
}
private static func convertCSVDataToDictionary(_ csvData: String) -> [String: [String]] {
var dictionary: [String: [String]] = [:]
let rows = csvData.components(separatedBy: .newlines)
if let key = rows.first {
let values = Array(rows.dropFirst())
countCalledConvertMethod += 1
let renamedKey = String(countCalledConvertMethod) + "." + key
dictionary[renamedKey] = values
}
return dictionary
}
static func convertAllCSVDateToDictionary() -> [String: [String]] {
var dictionary: [String: [String]] = [:]
allFileName.forEach { fileName in
let csvText = callCSVDataText(fileName: fileName)
let csvfileDictonary = convertCSVDataToDictionary(csvText)
dictionary.merge(csvfileDictonary) { (_, new) in new }
}
return dictionary
}
}
- ScrollViewを用いたタグのタイトル表示
- Listを用いたリスト表示
- @AppStorage,@State, @Environment,@FocusState
- UIViewRepresentableを用いて、SwiftUI上でUIKitを用いて、バナー広告表示
- ダークモード対応、文字サイズ変更
- onTapGesture,onReceive,.onPreferenceChange,onAppear などを用いた実装
ポモドーロタイマー
- ロック画面ウィジェットの実装
- ローカル通知の実装
例
static private func makeNotification(afterTime: Int,timerMode: TimerMode) {
let notificationIdentifier = "\(UUID())"
let notificationDate = Date().addingTimeInterval(TimeInterval(afterTime))
let dateComp = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: notificationDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComp, repeats: false)
let content = UNMutableNotificationContent()
switch timerMode {
case .work:
content.title = NSLocalizedString("workStart", comment: "")
case .rest:
content.title = NSLocalizedString("breakStart", comment: "")
}
content.sound = UNNotificationSound.default
let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
static func remove() {
let center = UNUserNotificationCenter.current()
center.removeAllPendingNotificationRequests()
}
- Timerのロジックを入れたTimerManagerの作成(ObservableObject,@Published を用いて)
例
enum TimerState {
case stopped, running, paused
}
enum TimerMode: String, CaseIterable ,Identifiable {
case work = "作業"
case rest = "休憩"
var id: String { self.rawValue }
}
class TimerManager: ObservableObject {
@Published var timerMode: TimerMode = .work
@Published var timerState: TimerState = .stopped
@Published var progress: Double = 1.0
@Published var setCount = 0
@AppStorage("workTimeIndex") var workTimeIndex = 2 {
willSet {
remainingTime = workTimes[newValue]
}
}
@AppStorage("breakTimeIndex") var breakTimeIndex = 0 {
willSet {
remainingTime = breakTimes[newValue]
}
}
@Published var timer: Timer?
@Published var remainingTime: TimeInterval = 25 * 60
let workTimes: [TimeInterval] = [5 * 60, 10 * 60, 25 * 60, 45 * 60,60 * 60]
let breakTimes: [TimeInterval] = [5 * 60, 10 * 60, 15 * 60]
var startTime: Date?
var pauseTime: Date?
private var elapsedTimeFromStart: TimeInterval {
if let startTime = startTime {
let resultElapsedTime = Date().timeIntervalSince(startTime)
return resultElapsedTime
} else {
return 0
}
}
init() {
remainingTime = workTimes[workTimeIndex]
}
func start(workTimeIndex: Int, breakTimeIndex: Int) {
startTime = Date()
timerState = .running
timerMode = .work
initWorkTime(workTimeIndex: workTimeIndex)
initBreakTime(breakTimeIndex: breakTimeIndex)
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.timerMode = self.timerModeRelativeToElapsedTime()
self.setCount = self.timerSetsRelativeElapsedTime()
self.remainingTime = self.floorTimeInterval(self.elapsedTimeFromEachTimerMode())
self.progress = self.remainingTime / self.totalTimeByTimerMode
}
}
func floorTimeInterval(_ timeInterval: TimeInterval) -> TimeInterval {
return TimeInterval(Int(timeInterval))
}
func pause() {
timerState = .paused
pauseTime = Date()
timer?.invalidate()
timer = nil
}
func resume() {
timerState = .running
let timeThatTheTimerHasAlreadyElapsed = startTime?.timeIntervalSince(pauseTime!)
startTime = Date().addingTimeInterval(timeThatTheTimerHasAlreadyElapsed!)
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.timerMode = self.timerModeRelativeToElapsedTime()
self.setCount = self.timerSetsRelativeElapsedTime()
self.remainingTime = self.floorTimeInterval(self.elapsedTimeFromEachTimerMode())
self.progress = self.remainingTime / self.totalTimeByTimerMode
}
}
func reset() {
timer?.invalidate()
timer = nil
setCount = 0
timerState = .stopped
timerMode = .work
initWorkTime(workTimeIndex: workTimeIndex)
}
func initWorkTime(workTimeIndex: Int) {
self.workTimeIndex = workTimeIndex
remainingTime = totalTimeByTimerMode
progress = 1.0
}
func initBreakTime(breakTimeIndex: Int) {
self.breakTimeIndex = breakTimeIndex
remainingTime = totalTimeByTimerMode
progress = 1.0
}
var totalTimeByTimerMode: TimeInterval {
switch timerMode {
case .work:
return workTimes[workTimeIndex]
case .rest:
return breakTimes[breakTimeIndex]
}
}
func timerModeRelativeToElapsedTime() -> TimerMode {
let workTime = workTimes[workTimeIndex]
let breakTime = breakTimes[breakTimeIndex]
let totalTime = workTime + breakTime
let remainder = Int(elapsedTimeFromStart) % Int(totalTime)
if remainder < Int(workTime) {
return .work
} else if Int(workTime) <= remainder {
return .rest
}
fatalError()
}
func timerSetsRelativeElapsedTime() -> Int {
let workTime = workTimes[workTimeIndex]
let breakTime = breakTimes[breakTimeIndex]
let totalTime = workTime + breakTime
let set = Int(elapsedTimeFromStart) / Int(totalTime)
return set
}
func elapsedTimeFromEachTimerMode() -> TimeInterval {
let workTime = workTimes[workTimeIndex]
let breakTime = breakTimes[breakTimeIndex]
let totalTime = workTime + breakTime
let remainder = Int(elapsedTimeFromStart) % Int(totalTime)
if remainder < Int(workTime) {
return workTime - TimeInterval(remainder)
} else if Int(workTime) <= remainder {
let result = remainder - Int(workTime)
return breakTime - TimeInterval(result)
}
fatalError()
}
}
-ViewをComponent化して、実装(View・@Bindingを用いて)
例 ProgressViewを用いて
struct CircularProgressView: View {
@Binding var timeLeft: TimeInterval
@Binding var progress: Double
@Binding var setCount: Int
let totalTime: TimeInterval
var color: Color
let timeFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
init(timeLeft: Binding<TimeInterval>,progress: Binding<Double>,setCount: Binding<Int>,color: Color) {
self._timeLeft = timeLeft
self.totalTime = timeLeft.wrappedValue
self.color = color
self._progress = progress
self._setCount = setCount
}
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.3)
.foregroundColor(.gray)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.foregroundColor(color)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
VStack{
Text(timeFormatter.string(from: timeLeft) ?? "")
.font(Font(UIFont.monospacedDigitSystemFont(ofSize: 80, weight: .regular)))
.fontWeight(.bold)
.minimumScaleFactor(0.5)
.frame(maxWidth: .infinity)
.fixedSize(horizontal: true, vertical: false)
Text("\(setCount)" + NSLocalizedString("set", comment: ""))
}
}
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView(
timeLeft: .constant(25 * 60),
progress: .constant(1.0),
setCount: .constant(1),
color: Color.init("ThemeColor")
)
}
}
運動時などで用いるカウントダウン・アップの読み上げアプリ
読み上げ カウントダウン
- AVSpeechSynthesizerDelegateを用いたSwiftUIでのデリゲートの実装
- ViewModelを用いた実装
以下は学習記録
ポケモンAPIを用いた通信処理の学習 (リリースはしていない)
ポケモンAPIを用いたテスト周りの学習 (リリースはしていない)
以上です。
振り返ってみると、書ききれば切りがないほど、課題が多いです。ですが、沢山の技術に触れることができ、一つ一つ苦労したことも、良い思い出です。これからは、デザインパターン、設計、テスト周りの学習を進めていきたい。
以上になります。自分がどのようなアプリを作成しているのか、ポートフォリオ的な記事を記載しておきたいと思い、このような記事を書きました。
他にも良い方法があれば、コメントいただけると大変うれしいです。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします!
個人でアプリを作成しているので、良かったら覗いてみてください!
Discussion