【iOS】これまでに使用した技術
ポートフォリオです。以下の順番で記載します。
- リリースしたアプリの概要
- 発信活動
- 使用した技術の詳細説明
リリースしたアプリの概要
これまでリリースしたiOSアプリは
- UIKit 34個
- SwiftUI 5個(内の2つがWatchOS)
- Unity 1個
の合計40個のアプリをリリースしました。
成果
合計約12万ダウンロードです。(2024/06/22時点)
iPadのメディカルカテゴリーで3位になりました。
コミュニケーション支援ツール(筆談・文字パッド)アプリのおすすめランキングで1位になりました。(2024年7月12日時点)
発信活動
発信活動はZenn・Qiitaで行っています。現在はZennを中心に発信しています。
2024年は合計40本の記事を投稿しています。(2024/07/18時点)
Zenn
直近であれば、HealthKit・SwiftData関連の発信をしました。
Qiita
直近であれば、業務で用いたReactNativeの発信をしました。
学習したアーキテクチャ
これまでに学習したアーキテクチャのサンプルコードを以下に記します。
MVP
MVVM
VIPER
使用した技術の詳細説明
全てのアプリの使用した技術を振ります。
FIM:生活の自立度を評価する
一人ひとりの動作レベルを評価するツールです。
-
UITableView
によるリスト表示 -
UITextField
による入力処理 -
NavigationController
を用いた画面遷移 - Repositoryパターンでの
Realm
のCRUD操作 -
QuickLook
を用いたPDF出力 - JSONから構造体への変換(
Decodable
を用いて) - Line、Twitterへの共有機能
個別タイム測定:一人ひとりの時間を計測
タイマー機能を用いた検査を行うツールです。
-
QuartzCore
を用いたタイマー実装。
→ 画面のリフレッシュレートに同期させるために、CADisplayLink
を用いてタイマー実装。 - 今回のアプリとは別で、
Timer
を用いた実装をサンプルアプリで実装。
HDS-R:認知症を簡単に検査する:痴呆症を評価する
認知症の検査で用いるツールです。
-
UIImageView
を用いた画像の表示 -
UITapGestureRecognizer
を用いたタップ処理 -
UIVisualEffectView()
を用いた背景のぼかし -
UIViewControllerTransitioningDelegate
、UIViewControllerAnimatedTransitioning
による画面遷移アニメーションのカスタマイズ
医療・介護にまつわる事業所を探すためのマップ
医療・介護に関連した事業所を検索するツールです。事業所別に合計8個のマップアプリを作成しました。
厚生労働省の医療・介護にまつわる事業所のデータベースを用いて実装。
-
MapKit
・Core Location
を用いたマップ表示 -
PickerViewKeyboardDelegate
を用いたPickerView
の制御 -
UIActivityIndicatorView
を用いて、インジケーターの表示実装 -
UISegment
を用いて、条件ごとの検索結果の表示 -
WebKit
をもちいて、事業所の詳細情報を表示する実装
もじつなぐ:認知症 発達障害の方の脳トレゲーム
- csvファイルを構造体へデコード
-
Core Graphics
,Core Animation
,Drawing
(UIBezierPath
)などを用いて描画処理
みつけもじ:認知症 発達障害の脳トレゲーム
上部に表示される一つの文字を、中央部分の文字のリストから探す脳トレアプリです。
UICollectionView
の実装
-
UICollectionViewDataSource
を用いて、値の設定 -
UICollectionViewDelegate
を用いて、タップされた際の処理の設定 -
UICollectionViewDelegateFlowLayout
を用いてレイアウトの設定
声日記・議事録・診察レコーダー
音声認識・録音・データ保存・カレンダー機能を用いた、音声・文字記録アプリです。
- 録音ボタンのアニメーションの実装
-
AVFoundation
(AVAudioRecorder
を用いて録音) -
Speech
を用いた音声認識 -
NotificationCenter
を用いて、バックグラウンド遷移の有無の検出 -
FileManager
を用いた録音データ(.m4a)のCRUD処理 -
FSCalendar
(ライブラリー)を用いて、カレンダーの実装 -
Calendar
を用いた日付処理 -
AVAudioPlayer
を用いた、音声ファイルの再生、再生開始時点の指定、スキップの実装 -
AVAudioPlayerDelegate
を用いた、音声データ再生終了時の処理 -
UITextViewDelegate
を用いた、テキスト入力中のデータ保存処理
コミュボード:耳が遠い方とのコミュニケーションツール
難聴の高齢者の方とのコミュニケーションアプリです。
筆談機能・音声認識機能を使用できます。
-
Speech
を用いた音声認識 - ローカライズ対応
- iPhone、iPadのAutoLayoutでのUI調整
-
StoreKit
を用いた、レビュー処理 -
ATTrackingManager
を用いたトラッキング実装 -
Admob
を用いた広告実装 -
Firebase Crashlytics
を用いたクラッシュログレポートツールの実装
印刷カレンダー:簡単シンプルに年月プリントできる
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
を用いて、デバイスごとに表示する文字の数を変更する実装
デバイスを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、C#を用いて作成したアプリです。Unityを用いた理由は、物理エンジンを用いて実装する必要があり、Unityの方が実装コストが低いためです。また、Unityにチャレンジしたい気持ちもあり、開発に取り掛かりました。Unityのチュートリアルでは、ブロック崩し・脱出ゲームも実装しました。Unityで面白さを感じたポイントは、Game空間の中の一つのオブジェクトに対して、C#のスクリプト設定してオブジェクトの制御を行う部分です。
目標設定アプリ
入力した目標をPDF出力し、紙で印刷するアプリです。
- 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
}
}
筋トレタイマー
筋トレ・ストレッチ時に用いるアプリです。
カウントの間隔をカスタマイズが可能であり、トレーニングプランのテンプレートを用意しています。
- 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からデコード+staticメソッドで管理)
例
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
を用いた実装 - バナー広告がSwiftUIに対応していないため、SwiftUI上でUIKitの実装(
UIViewRepresentable
を用いて) - ダークモード対応、文字サイズ変更
-
.onTapGesture
,.onReceive
,.onPreferenceChange
,.onAppear
などのmodifierを用いた実装
ポモドーロタイマー(SwiftUI)
ポモドーロタイマーを実装したアプリです。
- ロック画面ウィジェットの実装
- ローカル通知の実装
例
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化して、実装
例 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")
)
}
}
読み上げ カウントダウン(SwiftUI)
音声でカウントしたいときに用いるアプリです。
-
AVSpeechSynthesizerDelegate
を用いたSwiftUIでのデリゲートの実装 - ViewModelを用いた実装
リズム運動 (SwiftUI・WatchOS)
音声でカウントしたいときに用いるWatchOS対応アプリです。
ポモドーロタイマー (SwiftUI・WatchOS)
ポモドーロタイマー専用のWatchOS対応アプリです。
文字認識アプリ(UIKit)
保存している写真・撮影した写真を使用して、画像の文字認識で文字出力するアプリです。
- Vision・VisionKitを用いた文字認識機能
画像生成AI(SwiftUI)
画像生成AIのAPIを用いたアプリです。
-
String Catalog
を用いたローカライズ対応 - StabilityAI REST APIドキュメントを参考にAPIClientの実装
-
SwiftData
を用いたデータ管理
最後に
沢山の技術に触れることができ、一つ一つ苦労したことも良い思い出です。
ポートフォリオを記載する必要があり、この記事を書きました。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします。
個人でアプリを作成しているので、良かったら覗いてみてください。
Discussion