- 投稿日:2021-02-25T23:28:25+09:00
iPhoneでLEDを制御してみよう!
みなさん、こんばんは
【watnowテックカレンダーの25日目】
今日はwatnowのおかだが担当します
よろしくお願いしますはじめに
今回は私が卒業研究で触ったSpeech Recognition API(音声認識API)とESP-WROOM-32を用いてLEDを制御してみます.
動作環境及び開発環境
swift5.3.2
Xcode Version 12.4
MacOS Catalina
ios14.4Speechフレームワークとは
ios10から追加されたフレームワークで音声認識して文字起こししたりとか色々できちゃうやつです!
ESP-WROOM-32とは
通称ESP-32と呼ばれるArduinoとWIFI通信を兼ねたモジュールみたいなのです!
流れ
Arduino側でLEDの制御とWIFI通信の設定を行い,xcode側で認識した音声に応じたURLにアクセスすることで設定したLEDが光ります!!
Arduino側コード
#include <WiFi.h> const char* ssid = "あなたのSSID"; const char* password = "あなたのパス"; WiFiServer server(80); unsigned long time_data = 0; unsigned long second = 0; void setup() { Serial.begin(115200); pinMode(4, OUTPUT); //front pinMode(2, OUTPUT); //back pinMode(15, OUTPUT); //right pinMode(0, OUTPUT); //left delay(10); // We start by connecting to a WiFi network Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); server.begin(); } void loop(){ WiFiClient client = server.available(); // listen for incoming clients if (client) { // if you get a client, Serial.println("New Client."); // print a message out the serial port String currentLine = ""; // make a String to hold incoming data from the client while (client.connected()) { // loop while the client's connected if (client.available()) { // if there's bytes to read from the client, char c = client.read(); // read a byte, then Serial.write(c); // print it out the serial monitor if (c == '\n') { // if the byte is a newline character // if the current line is blank, you got two newline characters in a row. // that's the end of the client HTTP request, so send a response: if (currentLine.length() == 0) { // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK) // and a content-type so the client knows what's coming, then a blank line: client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); client.println(); // the content of the HTTP response follows the header: client.print("Click <a href=\"/ST\">here</a> to turn the LED on pin All off.<br>"); //stop client.print("Click <a href=\"/FF\">here</a> to turn the LED on pin 4 on.<br>"); //front client.print("Click <a href=\"/BB\">here</a> to turn the LED on pin 2 on.<br>"); //back client.print("Click <a href=\"/RI\">here</a> to turn the LED on pin 15 on.<br>"); //right client.print("Click <a href=\"/LE\">here</a> to turn the LED on pin 0 on.<br>"); //left // The HTTP response ends with another blank line: client.println(); // break out of the while loop: break; } else { // if you got a newline, then clear currentLine: currentLine = ""; } } else if (c != '\r') { // if you got anything else but a carriage return character, currentLine += c; // add it to the end of the currentLine } if (currentLine.endsWith("GET /ST")) { stop(); } if (currentLine.endsWith("GET /FF")) { Serial.printf("前"); stop(); digitalWrite(4, HIGH); } if (currentLine.endsWith("GET /BB")) { Serial.printf("後"); stop(); digitalWrite(2, HIGH); } if (currentLine.endsWith("GET /RI")) { Serial.printf("右"); stop(); digitalWrite(15, HIGH); } if (currentLine.endsWith("GET /LE")) { Serial.printf("左"); stop(); digitalWrite(0, HIGH); } } } // close the connection: client.stop(); Serial.println("Client Disconnected."); } } void stop(){ digitalWrite(4, LOW); digitalWrite(2, LOW); digitalWrite(15, LOW); digitalWrite(0, LOW); }arduino側の操作
SSIDとPASSWORDを自分のWIFIのものに書き換えてコンパイルしてください.
次にWEBブラウザでIPアドレスにアクセスして設定したボタンを押してLEDが思い通りに点滅するか確認してください.
Xcode側コード
import UIKit import Speech class ViewController: UIViewController { //認識する言語 private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))! private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? private var recognitionTask: SFSpeechRecognitionTask? private var audioEngine = AVAudioEngine() @IBOutlet weak var textView: UITextView! @IBOutlet weak var fromtButton: UIButton! @IBOutlet weak var rightButton: UIButton! @IBOutlet weak var backButton: UIButton! @IBOutlet weak var leftButton: UIButton! @IBOutlet weak var stopButton: UIButton! override func viewDidLoad() { super.viewDidLoad() SFSpeechRecognizer.requestAuthorization { (status) in OperationQueue.main.addOperation { switch status { case .authorized: do { try self.startSpeech() } catch{ print("startError: \(error)") } self.startTimer() default: print("error") } } } } func stopOnButton() { let url = URL(string: "http://192.168.10.XX/ST")! var request = URLRequest(url: url) request.httpMethod = "GET" let connection = NSURLConnection(request: request, delegate:nil, startImmediately: true) } func frontOnButton() { let url = URL(string: "http://192.168.10.XX/FF")! var request = URLRequest(url: url) request.httpMethod = "GET" let connection = NSURLConnection(request: request, delegate:nil, startImmediately: true) } func backOnButton() { let url = URL(string: "http://192.168.10.XX/BB")! var request = URLRequest(url: url) request.httpMethod = "GET" let connection = NSURLConnection(request: request, delegate:nil, startImmediately: true) } func rightOnButton() { let url = URL(string: "http://192.168.10.XX/RI")! var request = URLRequest(url: url) request.httpMethod = "GET" let connection = NSURLConnection(request: request, delegate:nil, startImmediately: true) } func leftOnButton() { let url = URL(string: "http://192.168.10.XX/LE")! var request = URLRequest(url: url) request.httpMethod = "GET" let connection = NSURLConnection(request: request, delegate:nil, startImmediately: true) } func startSpeech() throws { if let recognitionTask = recognitionTask { recognitionTask.cancel() self.recognitionTask = nil } let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.record, mode: .measurement, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) let recognitionRequest = SFSpeechAudioBufferRecognitionRequest() self.recognitionRequest = recognitionRequest recognitionRequest.shouldReportPartialResults = true recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] (result, error) in guard let `self` = self else { return } if let result = result { self.textView.text = result.bestTranscription.formattedString print(result.bestTranscription.formattedString) //特定の文字を発話した際に処理を行う if result.bestTranscription.formattedString.contains("前のライト") { self.setBackgroundColor(color: .blue) self.frontOnButton() } else if result.bestTranscription.formattedString.contains("後のライト") { self.setBackgroundColor(color: .yellow) self.backOnButton() }else if result.bestTranscription.formattedString.contains("右のライト") { self.setBackgroundColor(color: .orange) self.rightOnButton() }else if result.bestTranscription.formattedString.contains("左のライト") { self.setBackgroundColor(color: .purple) self.leftOnButton() }else if result.bestTranscription.formattedString.contains("停止") { self.setBackgroundColor(color: .white) self.stopOnButton() } } } let recordingFormat = audioEngine.inputNode.outputFormat(forBus: 0) audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in self.recognitionRequest?.append(buffer) } audioEngine.prepare() try? audioEngine.start() } func stopSpeech() { guard let task = self.recognitionTask else { fatalError("Error") } task.cancel() task.finish() self.audioEngine.inputNode.removeTap(onBus: 0) self.audioEngine.stop() self.recognitionRequest?.endAudio() } private func setBackgroundColor(color: UIColor) { DispatchQueue.main.async { self.view.backgroundColor = color } self.stopSpeech() do { try self.startSpeech() } catch { print("startError: \(error)") } } //1分でタイムアウトして音声認識されなくなるのでタイマーでループ private func startTimer(){ Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in self.stopSpeech() do { try self.startSpeech() } catch{ print("startError: \(error)") } } } @IBAction func stopAction(_ sender: Any) { self.stopOnButton() } @IBAction func frontAction(_ sender: Any) { self.frontOnButton() } @IBAction func backAction(_ sender: Any) { self.backOnButton() } @IBAction func rightAction(_ sender: Any) { self.rightOnButton() } @IBAction func leftAction(_ sender: Any) { self.leftOnButton() } }xcode側の操作
各Butttonに設定してあるURLを各ボタンのURLに変えてください.
viewController
おわりに
どうでしたでしょうか.今回はiphoneで音声認識させてLEDを制御するということをやってみました.工夫次第で色々なことに応用できると思うのでぜひ色々やってみてください!!
watnowでは、チャレンジ精神旺盛な仲間を募集しています
興味を持たれた方はぜひ@watnowsにDMしてください
- 投稿日:2021-02-25T19:17:55+09:00
【Swift】ViewControllerのライフサイクル
はじめに
この記事では、ViewControllerのライフサイクルについて簡単にまとめました。
ライフサイクルの発火の流れ
①loadView(View読み込み前)
②viewDidLoad(View読み込み後)
③viewWillAppear(View表示前)
④viewWillLayoutSubviews(SubViewレイアウト前)
⑤viewDidLayoutSubviews(SubViewレイアウト後)
⑥viewDidAppear(View表示後)
⑦viewWillDisappear(View除去前)
⑧viewDidDisappear(View除去後)目次
- loadView
- viewDidLoad
- viewWillAppear
- viewWillLayoutSubviews
- viewDidLayoutSubviews
- viewDidAppear
- viewWillDisappear
- viewDidDisappear
1. loadView
- タイミング・・・viewがnilの状態でviewを呼び出そうとした時
- 活用例 ・・・Viewの追加や制約の追加など、StoryBoardで行う操作のコードでの実装
2. viewDidLoad
- タイミング・・・viewをメモリにロードした後
- 活用例 ・・・クラス内で利用するオブジェクトの初期化
3. viewWillAppear
- タイミング・・・viewをview階層に追加する直前、もしくはviewを表示するためにアニメーションが設定される前
- 活用例 ・・・表示されるたびにviewを更新したい処理
4. viewWillLayoutSubviews
- タイミング・・・viewがSubviewをレイアウトする前
- 活用例 ・・・特になし
5. viewDidLayoutSubviews
- タイミング・・・viewがSubviewをレイアウトした後
- 活用例 ・・・特になし
6. viewDidAppear
- タイミング・・・viewがview階層に追加された後
- 活用例 ・・・ログの送信、操作のブロック
7. viewWillDisappear
- タイミング・・・viewがview階層から削除される前
- 活用例 ・・・特になし
8. viewDidDisappear
- タイミング・・・viewがview階層から削除された後
- 活用例 ・・・特になし
- 投稿日:2021-02-25T12:08:31+09:00
【iOS】Firebase Authentication + Firebase RealTime DataBase でログイン機能を実装する
環境
- Xcode 12.3 (12C33)
- Firebase/Auth 7.4.0
- Firebase/Database 7.4.0
作成するアプリ
- 社用メールアドレスでログイン後出退勤を記録できるアプリ
今回実現する機能
- メールアドレスとパスワードでログイン
- ログイン成功した場合にはアカウントに紐づく以下の情報を返却する
- 姓
- 名
- 従業員識別キー(出退勤記録に用いる)
- ログインできるメールアドレスとパスワードはあらかじめ発行しておく
- アプリ等からの新規アカウント登録は行わない
手順
- Firebase プロジェクトを作成
- アプリを登録
- GoogleService-Info.plist をDLしプロジェクトに追加
- Firebase/Auth および Firebase/Database を Pod で導入
- Firebase のコンソール画面から Authentication を開きメールアドレスによるログインを有効化
- 社員のメールアドレスを登録
- json で Realtime Database にインポートするファイルを作成
- Firebase のコンソール画面から Realtime Database を開き作成しておいたファイルをインポート
- Realtime Database のルールの設定を変更しログインしていないアプリからデータを読み込めないようにする
- アプリ側でサインイン機能を実装する
- アプリ側でデータ取得機能を実装する
1〜4は本題ではないため以下記事を参照
5. Firebase のコンソール画面から Authentication を開きメールアドレスによるログインを有効化
- Firebase のコンソール画面から対象となるプロジェクトを選択
- 左側のメニューから「Authentication」を選択する
- Sign-in method タブを選択する
- メール/パスワードを選択する
- 「有効にする」をONにする
6. 社員のメールアドレスを登録する
- Firebase のコンソール画面の左側のメニューから「Authentication」を選択する
- Users タブを選択する
- 「ユーザーを追加」ボタンをクリックする
- メールアドレス・パスワードを記入し「ユーザーを追加」ボタンをクリックする
7. json で Realtime Database にインポートするファイルを作成
- 以下の様に json ファイルを作成
従業員データ{ "users" : { "(ユーザーUID1)" : { "last_name" : "テスト", "first_name" : "太郎", "key" : "(従業員識別キー1)" }, "(ユーザーUID2)" : { "last_name" : "試験", "first_name" : "花子", "key" : "(従業員識別キー2)" } } }
- ユーザーUID には Authentication > Users で確認できるユーザーUIDを利用する
8. Firebase のコンソール画面から Realtime Database を開き作成しておいたファイルをインポート
- Firebase のコンソール画面の左側のメニューから「Realtime Database」を選択する
- 「データ」タブを選択する
- タブ以下の画面右上の点が3つ縦に並んでいるボタンをタップする
- 「JSONをインポート」を選択する
- 7で作成したJSONファイルを選択する
- 「インポート」を選択する
- 画面に「"users"」から始まるツリー構造が追加されていることを確認する
9. Realtime Database のルールの設定を変更しログインしていないアプリからデータを読み込めないようにする
- Firebase のコンソール画面の左側のメニューから「Realtime Database」を選択する
- 「ルール」タブを選択する
- 「ルールを編集」が選択されていなければ選択する
- 以下の通り編集する
ルール{ "rules": { ".read": false, ".write": false, "users" : { "$uid": { ".read": "auth.token != null && $uid === auth.uid" } } } }
- 編集後権限の設定状況に問題がなければ「公開」を選択する
ルールの設定について
- ルールはカスケード式に適用される
- 上流で true としていた場合設定されない限りは下流は全て true になる
- 下流で false に設定されるとそこから下流は全て false になる
- 今回は全体としては外部からの読み書きを禁止する
- usersのうちの
$uid
データについて以下のルールを適用する
- 認証トークンが存在し、かつ
$uid
(データのキー) が認証情報のユーザーUIDと一致するデータについては読み込み権限を付与する$uid
は単純に変数名なので$a
などでもOKauth
からログイン後の認証情報にアクセス可能- 詳細は以下より
10. アプリ側でサインイン機能を実装する
AppDelegate
- アプリ起動時に Firebase と接続する
AppDelegate.swiftimport Firebase import UIKit // MARK: - AppDelegate @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Firebase に接続する FirebaseApp.configure() return true } // 以下略サインイン画面
- メールアドレスとパスワードをUIから取得し結果を completion で処理する
- サインインに成功した場合 completion に返却される
AuthDataResult?
に ユーザーUID が含まれているため、これを用いてRealtime Database からデータを取得するSignInViewController.swiftimport FirebaseAuth import UIKit // MARK: - SignInViewController final class SignInViewController: UIViewController { // 中略 @IBAction private dynamic func signInButtonTapped(_ sender: Any) { guard let email = self.emailTextField?.text, let password = self.passwordTextField?.text else { return } Auth.auth().signIn(withEmail: email, password: password) { (result: AuthDataResult?, _) in if let userId = result?.user.uid { // サインイン成功時の処理 } else { // サインイン失敗時の処理 } } } // 以下略11. アプリ側でデータ取得機能を実装する
構造体の準備
- 取得したデータを入れる構造体を作成する
FBEmployee.swiftimport FirebaseDatabase import Foundation // MARK: - FBEmployee struct FBEmployee { // 姓 let lastName: String // 名 let firstName: String // 従業員識別キー let employeeKey: String init(by snapshot: FirebaseDatabase.DataSnapshot) { guard let dictionary = snapshot.value as? NSDictionary, let lastName = dictionary["last_name"] as? String, let firstName = dictionary["first_name"] as? String, let employeeKey = dictionary["key"] as? String, else { fatalError() } self.lastName = lastName self.firstName = firstName self.employeeKey = employeeKey } }データの取得
- Realtime Database はツリー構造になっている
- Realtime Database へのリファレンスを作成し、そこから
child
ノードを探索するイメージでアクセス- ユーザーUIDがデータのキーとなるように Realtime Database を作成したため、Realtime Database の
child
にuserId
を入れてアクセスする- 今回取得するデータはほぼ変化することがなく、ログイン時に一度取得できれば良いため
observeSingleEvent(of:with:withCancel:)
を用いる
observe(_:with:withCancel)
を用いる場合、利用しなくなったリスナーのデタッチを行う必要があるため注意SignInViewController.swiftimport FirebaseAuth import FirebaseDatabase import UIKit // MARK: - SignInViewController final class SignInViewController: UIViewController { // 中略 @IBAction private dynamic func signInButtonTapped(_ sender: Any) { guard let email = self.emailTextField?.text, let password = self.passwordTextField?.text else { return } Auth.auth().signIn(withEmail: email, password: password) { (result: AuthDataResult?, _) in if let userId = result?.user.uid { // サインイン成功時の処理 let reference = FirebaseDatabase.Database.database().reference() reference.child("users").child(userId).observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in // データ取得成功時の処理 // 従業員データのデコード処理 let employee = FBEmployee(by: snapshot) // ここから画面遷移等を実行する } withCancel: { _ in // データ取得失敗時の処理 } } else { // サインイン失敗時の処理 } } } // 以下略参考
Firebase 公式リファレンス
- 投稿日:2021-02-25T10:59:22+09:00
【SwiftUI】Buttonにおける .padding() の位置を気をつけようという話
どういうことか
.padding()
の書く位置によって表示が変わる!どうなるのか
Button(action: { print("押された") }){ Text("ボタン") .fontWeight(.bold) .font(.system(size: 20)) .foregroundColor(Color.white) .background(Color.blue) .frame(maxWidth: .infinity, minHeight: 48) .padding(.horizontal, 32) }これだとこう
ちょっと見づらいが、想定通り画面端からの
padding
が効いている。
ところがButton(action: { print("押された") }){ Text("ボタン") .fontWeight(.bold) .font(.system(size: 20)) .foregroundColor(Color.white) .background(Color.blue) .padding(.horizontal, 32) .frame(maxWidth: .infinity, minHeight: 48) }
frame
の後ではなく前に書くと効かなくなってしまう。なんてことだ。
で、padding
だけじゃなくてcornerRadius
とかも場所で効いたり効かなかったりが変わるっぽい。気をつけよう。
- 投稿日:2021-02-25T09:32:43+09:00
UIRefreshControlの位置を変える
デフォルトではCollectionViewの上部に出てくるUIRefreshControl。
これの位置を変えたい。
ふつうにframeで設定できるんじゃないかと思ってやってみるとlet refreshControl = UIRefreshControl() collectionView.refreshControl = refreshControl refreshControl.frame = CGRect(x: 0, y: 100, width: collectionView.bounds.width, height: 60)位置変わらない。。。。。
どうすればいいかというと、
コンテナになるviewを作って位置設定して、refreshControlをaddSubViewすると機能する。
let refreshControl = UIRefreshControl() collectionView.refreshControl = refreshControl let containerView = UIView(frame: CGRect(x: 0, y: 100, width: collectionView.bounds.width, height: 60)) collectionView.addSubview(containerView) // 1. containerView.addSubView(refreshControl) // 2. // refreshControlをコンテナにaddSubViewする前にコンテナを // collectionViewやtableViewにaddSubViewしないとrefreshControlが表示されない。 refreshControl.addTarget(self, action: #selector(refreshFunc), for: .valueChanged)refreshControlをコンテナにaddSubViewする前にコンテナをcollectionViewやtableViewにaddSubViewしないとrefreshControlが表示されないので注意。
?
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.comCore MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2021-02-25T09:07:54+09:00
Xcodeでコードを自動生成することで生産性を上げる
XcodeでClean Architecture + MVVMのコードを自動生成させるジェネレーターを作りました!
ファイルを自動的に生成させることができるようになるので便利です。https://github.com/yosuke1985/Clean-MVVM-Generator
この記事は以下の記事の構成でファイルを生成しております。
ある程度複雑なiOSアプリに必要なClean Architectureのベストプラクティスを考えてみた https://qiita.com/YOSUKE8080/items/c036c9cc17bbee773019
前置き
クリーンアーキテクチャを採用しているのですが、
たとえば、一つのHogeというViewを生成するのに以下のファイルを生成しています。
HogeViewController.swift
HogeBuilder.swift
HogeViewModel.swift
HogeRouter.swift
HogeStoryboard.swiftこれらのファイルがViewの数ごとに生成する必要があるのですが、簡単な作業ですがクラス名、変数名が統一されているか慎重に確認する必要があるので、結構神経も使います。
自分でテンプレートを作ることで動的にファイルを生成することができるようになるこのようにしてファイルを生成できます。作り方
必要なもの
- テンプレートであるswiftファイル
- Plistファイル
// // ___FILENAME___ // ___PROJECTNAME___ // // Created by ___FULLUSERNAME___ on ___DATE___. // import RxCocoa import RxSwift import UIKit class ___VARIABLE_sceneName___ViewController: UIViewController { var viewModel: ___VARIABLE_sceneName___ViewModel! var bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() viewModel.setUp() setBind() } private func setBind() { } }FILEBASENAME ベースのファイル名
FILENAME ファイル名
PROJECTNAME プロジェクト名
FULLUSERNAME 氏名
DATE 日付など、もとから定義されているものに加えて、 以下のようにPlistに変数名を定義して、テンプレート作成時に変数として値をセットすることもできます。
_VARIABLE_sceneName_
使い方
command + Nで必要なテンプレートを選ぶだけ
5つのファイルを自動で生成できる
Repositoryの生成
UseCaseの生成
あとがき
プロジェクトを汚すことなく使用することができますので、おすすめです!
参考
Installing the Clean Swift Template in Xcode
https://medium.com/swift2go/installing-the-clean-swift-template-in-xcode-6b4367006827
- 投稿日:2021-02-25T00:21:20+09:00
【Swift5】アプリで画像を保存する方法【ローカル編】
はじめに
この記事では、ユーザーのカメラロールなどから読み込んだ画像をローカルに保存したり呼び出したりする方法を解説していきます。
本記事では FileSystem を用いた保存を実装するので、UserDefaultsでは容量オーバーで保存できなかった画像も保存する事ができるようになります。(UserDefaultsで保存することができるのは8MBまで)
そのため、アプリの背景画像などにユーザーの写真を使用したりアイコン画像の設定などを行えるようにする際にとても便利な方法です。環境
・macOS catalina version 10.15.7
・Xcode version 12.2
・swift 5.3.1概要
今回紹介する方法では、以下の3つ(+2つ)の関数を定義して画像の保存と読み出しを実装します。
- store関数:画像ファイルの書き込み(保存)を行う
- retrieveImage関数:画像ファイルの読み出しを行う
- filePath関数:書き込みや読み出しの際に使うファイルパスの作成を行う
- save関数:エラーを防ぎつつstore関数を呼び出すための関数 (任意)
- display関数:エラーを防ぎつつretrieveImage関数を呼び出すための関数 (任意)
1. store関数の定義
store関数は保存したい画像を引数にとり、その画像をUserDefaultローカルに保存する関数です。
画像保存をする処理を行いたいクラス内に以下のように宣言てして、使いたいタイミングで呼び出して使う事ができます。//store関数の定義 func store(image: UIImage, forKey key: String) { if let pngRepresentation = image.pngData() { if let filePath = filePath(forKey: key) { do { try pngRepresentation.write(to: filePath, options: .atomic) } catch let error { print(error) } } } }
2. retrieveImage関数の定義
retrieveImage関数はstore関数を用いて保存した画像を読み出すための関数です。
store関数で保存する際に自由に設定したkey(String型)をとり、対応する画像を読み出します。//retrieveImage関数の定義 func retrieveImage(forKey key: String) -> UIImage? { if let filePath = self.filePath(forKey: key), let fileData = FileManager.default.contents(atPath: filePath.path), let image = UIImage(data: fileData) { return image } return nil }
3. filePath関数
filePath関数はsotre関数やretrieveImage関数で画像の保存や読み出しを行う際に、画像の保存場所を決めるために必要な処理を行う関数です。返り値として画像のURLを返してくれます。
こちらもstore関数やretrieveImage関数と同じクラス内で宣言します。//filePath関数 func filePath(forKey key: String) -> URL? { let fileManager = FileManager.default guard let documentURL = fileManager.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first else { return nil } return documentURL.appendingPathComponent(key + ".png") }
4. save関数
save関数はわざわざ宣言しなくても画像の保存は行えますが、処理の順番によるエラーなどを防ぐために宣言してsave関数で保存を行うととても便利です。
以下のコードは同時に最大一枚までの画像を保存するコードになっています。
以下でselectedImageは保存したい画像(UIImage)です。UIImagePickerControllerなどで取得したユーザーの画像をselectedImageに代入するようにしてください。
また、この関数も他の関数同様に保存や読み出しを行いたいクラス内で宣言してください。//save関数の宣言 @objc func save() { if let imageForImage = selectedImage { DispatchQueue.global(qos: .background).async { self.store(image: imageForImage, forKey: "Image") } } }
5. display関数の定義
こちらもsave関数と同様に宣言しなくても問題はないのですが、エラーを防ぐために宣言して用いると便利です。こちらも同時に一枚までの画像を保存&取得する際のコードを示しました。
以下で、imageView は読み出した画像を表示するためのUIImageViewです。//display関数の宣言 @objc func display() { DispatchQueue.global(qos: .background).async { if let savedImage = self.retrieveImage(forKey: "Image") { DispatchQueue.main.async { imageView.image = savedImage } } } }
まとめ
以下に今回紹介紹介したコードをまとめました。
実際に画像の保存や読み出しを行う際は、以下の関数の定義を行った上で、適宜save関数やdisplay関数などを処理を行いたいタイミングで呼び出して使用してください。//画像保存&読み出しを行うクラス内 func store(image: UIImage, forKey key: String) { if let pngRepresentation = image.pngData() { if let filePath = filePath(forKey: key) { do { try pngRepresentation.write(to: filePath, options: .atomic) } catch let error { print(error) } } } } func retrieveImage(forKey key: String) -> UIImage? { if let filePath = self.filePath(forKey: key), let fileData = FileManager.default.contents(atPath: filePath.path), let image = UIImage(data: fileData) { return image } return nil } func filePath(forKey key: String) -> URL? { let fileManager = FileManager.default guard let documentURL = fileManager.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first else { return nil } return documentURL.appendingPathComponent(key + ".png") } @objc func save() { if let imageForImage = selectedImage { DispatchQueue.global(qos: .background).async { self.store(image: imageForImage, forKey: "Image") } } } @objc func display() { DispatchQueue.global(qos: .background).async { if let savedImage = self.retrieveImage(forKey: "Image") { DispatchQueue.main.async { imageView.image = savedImage } } } }
参考にしたページ
https://developer.apple.com/documentation/foundation/data/1779858-write
https://programmingwithswift.com/save-images-locally-with-swift-5/
自作アプリ
私たちは、自作のリマインダーアプリ「タスクリマインダー -TaskReminder 課題管理-」を公開しています。
ダウンロードはこちらから!
https://apple.co/3jJZ1Cyhttps://apple.co/3jJZ1Cy