- 投稿日:2020-04-03T23:12:23+09:00
簡単にFirebase Storageに複数のファイルをアップロードする[swift]
概要
複数のファイルのアップロードを以下のように扱えるようにStorageUploaderを作りました。どれくらいアップロードされているのか監視もできます。
sample.swiftlet uploader = StorageUploader() // アップロードしたいファイルを追加 uploader.addFile(file:URL.init("video"), childRef:videoChildRef, metadata:videoMetadata) uploader.addData(data:imageData, childRef:imageChildRef, metadata:imageMetadata) // アップロード状況の監視 uploader.progressHandler = {completedCount, totalCount in print("\(100 * completedCount / totalCount)%") } // アップロード完了時の処理 uploader.completeHandler = { metadatas in print("complete") } // アップロード失敗時の処理 uploader.failureHandler = { error in // switch (StorageErrorCode(rawValue: error.code)!) { ... print("fail") } // アップロード開始 uploader.start()StorageUploaderのコード
StorageUploader.swiftimport Foundation import FirebaseStorage class StorageUploader { fileprivate struct SUComponent { var file:URL? var data:Data? var childRef:StorageReference? var metadata:StorageMetadata? var isComplete = false } var progressHandler: (_ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void = {_, _ in } var completeHandler: (_ metaDatas: [StorageMetadata]) -> Void = {_ in } var failureHandler: (_ error: NSError?) -> Void = {_ in } fileprivate var components = [SUComponent]() fileprivate var uploadTasks = [StorageUploadTask]() fileprivate var totalUnitCount:Int64 = 1 fileprivate var completedUnitCount:Int64 = 0 fileprivate var isfailure = false func addFile(file:URL, childRef:StorageReference, metadata:StorageMetadata) { var uploadInfo = SUComponent() uploadInfo.file = file uploadInfo.childRef = childRef uploadInfo.metadata = metadata components.append(uploadInfo) } func addData(data:Data, childRef:StorageReference, metadata:StorageMetadata) { var uploadInfo = SUComponent() uploadInfo.data = data uploadInfo.childRef = childRef uploadInfo.metadata = metadata components.append(uploadInfo) } func start() { prepareUpload() progressHandler(completedUnitCount, totalUnitCount) } fileprivate func prepareUpload() { components.forEach { uploadInfo in let childRef = uploadInfo.childRef let metadata = uploadInfo.metadata if let file = uploadInfo.file { guard let uploadTask = childRef?.putFile(from: file, metadata: metadata) else { return } observeStatus(uploadTask) } else if let data = uploadInfo.data { guard let uploadTask = childRef?.putData(data, metadata: metadata) else { return } observeStatus(uploadTask) } } } fileprivate func observeStatus(_ uploadTask: StorageUploadTask) { uploadTasks.append(uploadTask) uploadTask.observe(.progress) { snapshot in var currentCompletedUnitCount:Int64 = 0 var currentTotalUnitCount:Int64 = 0 self.uploadTasks.forEach { uploadTask in currentCompletedUnitCount += (uploadTask.snapshot.progress?.completedUnitCount ?? 0) currentTotalUnitCount += (uploadTask.snapshot.progress?.totalUnitCount ?? 0) } self.completedUnitCount = currentCompletedUnitCount self.totalUnitCount = currentTotalUnitCount self.progressHandler(currentCompletedUnitCount, currentTotalUnitCount) } uploadTask.observe(.success) { snapshot in if self.completedUnitCount == self.totalUnitCount { let metaDatas = self.uploadTasks.map { uploadTask -> StorageMetadata in return uploadTask.snapshot.metadata ?? StorageMetadata() } self.completeHandler(metaDatas) } } uploadTask.observe(.failure) { snapshot in if !self.isfailure { self.isfailure = true self.uploadTasks.forEach { uploadTask in if uploadTask.snapshot.progress?.isCancellable ?? false { uploadTask.cancel() } } let error = snapshot.error as NSError? self.failureHandler(error) } } } }エラーハンドリング
エラーから以下の情報がわかる。
sample.swiftswitch (StorageErrorCode(rawValue: error.code)!) { case .unknown: // An unknown error occurred break case .objectNotFound: // No object exists at the desired reference break case .bucketNotFound: // No bucket is configured for Firebase Storage break case .projectNotFound: // No project is configured for Firebase Storage break case .quotaExceeded : // Quota on your Firebase Storage bucket has been exceeded // If you're on the free tier, upgrade to a paid plan // If you're on a paid plan, reach out to Firebase support break case .unauthenticated: // User is unauthenticated. Authenticate and try again break case .unauthorized: // User is not authorized to perform the desired action // Check your rules to ensure they are correct break case .retryLimitExceeded : // The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded // Try uploading again break case .nonMatchingChecksum : // File on the client does not match the checksum of the file received by the server // Try uploading again break case .downloadSizeExceeded : // Size of the downloaded file exceeds the amount of memory allocated for the download // Increase memory cap and try downloading again break case .cancelled: // User cancelled the operation break case .invalidArgument : // An invalid argument was provided break /* ... */ default: // A separate error occurred. This is a good place to retry the upload. break }
- 投稿日:2020-04-03T22:43:22+09:00
ReSwiftで複数のReducerってどうするん?
はじめに
ReSwiftの勉強をしていて、Storeに登録するReducerが複数の時どうするんだっけ?と思ったのでメモです。
CounterExample
ReSwiftの公式の「CounterExample」を見ていて、以下のコードがありました。
AppDelegate.swiftimport UIKit import ReSwift // The global application store, which is responsible for managing the appliction state. let mainStore = Store<AppState>( reducer: counterReducer, //<---複数の時どうするの? state: nil ) @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {Reducerを複数登録する時ってどうすればいいんだろう?と疑問に思いました。
調べてみたら、Issuesにありましたね。Is_it_possible_to_specify_an_array_of_reducers?#340func combineReducers<T>(_ first: @escaping Reducer<T>, _ remainder: Reducer<T>...) -> Reducer<T> { return { action, state in let firstResult = first(action, state) let result = remainder.reduce(firstResult) { result, reducer in return reducer(action, result) } return result } } let reducer = combineReducers(first, second, third) // <--- これを使う!stateやらreducerやらもう少し細かく分けると、自作しないといけないような気がしますが、とりあえずこいつを使っていくことにします。
- 投稿日:2020-04-03T21:46:10+09:00
CoreDataを使ったTableViewでスワイプ削除を行うには
■はじめに
IntやStringの配列をTableViewに表示していてスワイプ削除を行いたい場合はこちらをお勧めします=>(スワイプでtableviewのセル削除)
CoreDataとTableViewはセットで使われることが多いです。
CoreDataを扱っている記事のほとんどがTableViewで取得したデータを表示しているためその辺でつまづいてもいくつか記事をあさって行けば問題ありません。
しかし、TableViewのCellをスワイプで削除する方法についての記事は少なく、どの記事も古い。しかもCoreDataとセットで扱ってる記事がないので、雑なコードだけでも載せておきますので、参考にしてみてください。
■動作環境
- Xcode Version 11.3.1
- Swift5
■ソースコード
ViewController.swift//〜〜 //割愛 //〜〜 // セルの削除許可を設定 func tableView(_ tableView: UITableView,canEditRowAt indexPath: IndexPath) -> Bool{ return true } // スワイプ削除 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { do { let fetchRequest = NSFetchRequest<NSManagedObject>(entityName:"FoodInformation") // 条件指定 ↓条件(Int型は%d)↓ ↓値↓ fetchRequest.predicate = NSPredicate(format: "foodId = %d", CoreDataArray[indexPath.row].foodId) let data = try managedObjectContext.fetch(fetchRequest) // 指定した条件の内容を削除 for task in data { managedObjectContext.delete(task) } // 削除したDBの変更内容を保存 (UIApplication.shared.delegate as! AppDelegate).saveContext() // CoreDataArrayはこのクラスの変数。DBよりあとに削除 self.CoreDataArray.remove(at: indexPath.row) //(※1) } catch { print("Fetching Failed.") } // TableViewのCellを削除 tableView.deleteRows(at: [indexPath], with: .automatic) } //〜〜 //割愛 //〜〜僕がつまづいたのは(※1)のCoreDataを保存した配列の削除の部分です。他の記事でこの部分を先に書くというものがありました。しかしそれはCoreDataを使うことを考慮していないものだったのでかなり惑わされてしまいました(笑)
このソースコードを、DelegateとかDataSourceとかセルのタップアクションとかみたいにはっつければ動きます。
entityNameや条件などは自身のソースコードに合うように変更してください。
- 投稿日:2020-04-03T21:46:10+09:00
【Swift5】CoreDataを使ったTableViewでスワイプ削除を行うには
■はじめに
IntやStringの配列をTableViewに表示していてスワイプ削除を行いたい場合はこちらをお勧めします=>(スワイプでtableviewのセル削除)
CoreDataとTableViewはセットで使われることが多いです。
CoreDataを扱っている記事のほとんどがTableViewで取得したデータを表示しているためその辺でつまづいてもいくつか記事をあさって行けば問題ありません。
しかし、TableViewのCellをスワイプで削除する方法についての記事は少なく、どの記事も古い。しかもCoreDataとセットで扱ってる記事がないので、雑なコードだけでも載せておきますので、参考にしてみてください。
■動作環境
- Xcode Version 11.3.1
- Swift5
■ソースコード
ViewController.swift//〜〜 //割愛 //〜〜 // セルの削除許可を設定 func tableView(_ tableView: UITableView,canEditRowAt indexPath: IndexPath) -> Bool{ return true } // スワイプ削除 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { do { let fetchRequest = NSFetchRequest<NSManagedObject>(entityName:"FoodInformation") // 条件指定 ↓条件(Int型は%d)↓ ↓値↓ fetchRequest.predicate = NSPredicate(format: "foodId = %d", CoreDataArray[indexPath.row].foodId) let data = try managedObjectContext.fetch(fetchRequest) // 指定した条件の内容を削除 for task in data { managedObjectContext.delete(task) } // 削除したDBの変更内容を保存 (UIApplication.shared.delegate as! AppDelegate).saveContext() // CoreDataArrayはこのクラスの変数。DBよりあとに削除 self.CoreDataArray.remove(at: indexPath.row) //(※1) } catch { print("Fetching Failed.") } // TableViewのCellを削除 tableView.deleteRows(at: [indexPath], with: .automatic) } //〜〜 //割愛 //〜〜僕がつまづいたのは(※1)のCoreDataを保存した配列の削除の部分です。他の記事でこの部分を先に書くというものがありました。しかしそれはCoreDataを使うことを考慮していないものだったのでかなり惑わされてしまいました(笑)
このソースコードを、DelegateとかDataSourceとかセルのタップアクションとかみたいにはっつければ動きます。
entityNameや条件などは自身のソースコードに合うように変更してください。
- 投稿日:2020-04-03T19:13:12+09:00
iOSアプリのAmazonリンクからAmazonアプリを起動する方法
Info.plistにLSApplicationQueriesSchemesというキーでTypeをArrayにし、そこにアイテムを追加し、TypeをStringでvalueをcom.amazon.mobile.shopping.webにします
アソシエイトリンクの場合は、短縮URL
https://amzn.to/xxx
を使うとAmazon モバイルは現在amzn.toでご利用いただけません
というエラーがでるので通常URLを使う。urlString = "アソシエイトリンクの通常URLからhttps://を除いた文字列" let urlStringWithSchema = "com.amazon.mobile.shopping.web://" + urlString guard let urlWithSchema = URL(string: urlStringWithSchema) else { return } if UIApplication.shared.canOpenURL(urlWithSchema){ UIApplication.shared.open(urlWithSchema, options: [:], completionHandler: nil) } else { guard let url = URL(string: urlString) else { return } UIApplication.shared.open(url, options: [:], completionHandler: nil) }一度アプリの該当画面からAmazonアプリを開く許可をすると、以後は許可を求められずに勝手にAmazonアプリが開く。これは Cunstom URL Schemas
Apple Developer のリファレンス等にあるような、
URL Types
に値を追加する云々は、あくまでSafariなどでブラウジングしていて特定のURLを踏んだときに自社アプリを起動させるための設定なので混同しないように。参考文献
https://lima.world/iphone-url/
https://qiita.com/zakiyamaaaaa/items/96c6fb8ee5a93c963bbc
https://stackoverflow.com/questions/27693062/iphone-ios-amazon-scheme-uri-not-working-anymore-amzn-version-4-3-1-deep-li
※amzn://content/item?id=<some valid id>
という方式は使えなくなったようだ。
- 投稿日:2020-04-03T17:27:04+09:00
[swift]textViewにplaceHolder的なのをつけてみる
はじめに
こんにちは。swiftはじめて約1年のMomijiです!
今回ははじめての記事となるのですが、textViewにplaceHolder的なのをつけてみるということで書かせていただきます。
⚠️かなり自己流です。
textFieldにはあって、なんでtextViewにはplaceHolderないんでしょうね〜コード
とりあえず、全体のコードです。
import UIKit class SSubjectsDetailViewController: UIViewController,UITextViewDelegate { @IBOutlet weak var textView: UITextView! override func viewDidLoad() { textView.text = "内容を入力" textView.textColor = .lightGray textView.delegate = self } func textViewDidBeginEditing(_ textView: UITextView) { textView.text = "" textView.textColor = .black } func textViewDidEndEditing(_ textView: UITextView) { if textView.text == "" { textView.text = "内容を入力" textView.textColor = .lightGray } } }解説
textView.text = "内容を入力" textView.textColor = .lightGray内容を入力のところは、ご自分でpalceHolderに設定したい文字を入れてください。
placeHolderぽくするためにtextViewの文字色をlightgrayにしました。func textViewDidBeginEditing(_ textView: UITextView) { textView.text = "" textView.textColor = .black }textViewDidBeginEditingで、textViewがタップされた時の挙動を表します。
つまり、「textViewがタップされた時、textViewの文字を無くして、色を黒にする」
という処理を行なっています。func textViewDidBeginEditing(_ textView: UITextView) { if textView.text == "" { textView.text = "内容を入力" textView.textColor = .lightGray } }ここは、placeHolderを実装するだけならいらない部分なのかもしれないのですが、
textViewに何も文字が書いていない場合のtextViewの処理を表しています。まとめ
う〜ん、やっぱりtextViewにもplaceHolderは欲しいですね〜
冒頭にも書きましたが、かなり自己流ですが、一応placeHolder的なものはできました。
たくさんの方の役に立つと嬉しいです。
質問がある方は、コメント欄まで〜
- 投稿日:2020-04-03T12:07:44+09:00
【Swift】Modalで表示した子画面の閉じるイベントを親画面で検知する
概要
iOS13からModalで画面遷移した際のアニメーションが変更されました。
それによって、Modal画面を閉じた際に動くライフサイクルメソッドが変わりました。
(今まではViewWillAppearが動いていたので、そこで更新等の処理を書いていた人も多いはず。困った、、、)Modalで表示した子画面の閉じるイベントを親画面で検知する方法を備忘として残しておきます。
環境
Xcode: 11.4
Swift: Swift5手順1
適当なViewControllerを2つ用意。
ここでは遷移元を「ParentViewController」遷移先を「ChildViewController」とします。ParentViewController.swiftimport UIKit final class ParentViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }ChildViewController.swiftimport UIKit final class ChildViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }手順2
ChildViewControllerが閉じた際のイベントは以下のメソッドで検知可能です。
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController「UIAdaptivePresentationControllerDelegate」に準拠することで使用可能です。
ParentViewControllerに以下を実装します。ParentViewController.swiftimport UIKit final class ParentViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let storyboard = UIStoryboard.init(name: "Child", bundle: nil) let vc = storyboard.instantiateViewController(withIdentifier: "AccountViewController") // delegateを設定 vc.presentationController?.delegate = self self.present(vc, animated: true, completion: nil) } } extension ParentViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // ChildViewControllerのDismissを検知 print("closed") } }最後に
iOS13が出てから半年以上経っているのに調べるまで知りませんでした。
ライフサイクルメソッドに書かなくていいので実装がスッキリしますね!
誰かのお役に立てれば幸いです!
- 投稿日:2020-04-03T11:29:06+09:00
画像ファイルの種類を判別するDataのExtension
Swiftでファイルを取扱う際、
URL
であればpathExtension
で拡張子を取得することができます。しかしData
からファイルの種類を特定する場合、基本機能だけでは不可能です。NSDataから判別する記事を書いている方がいたので、これを元にData
から画像の種別を特定するExtensionを作成しました。コード全貌 (+解説)
extension Data { enum ImageType: String, CaseIterable { case jpg case png case gif case bmp var header: UInt8 { switch self { case .jpg: return 0xff case .png: return 0x89 case .gif: return 0x47 case .bmp: return 0x42 } } var `extension`: String { return "." + rawValue } } var imageType: ImageType? { var values = [UInt8](repeating: 0, count: 1) copyBytes(to: &values, count: 1) return ImageType.allCases.first { $0.header == values[0] } } }画像ファイルの種別はバイナリデータのヘッダを見ればわかる(らしい)ので、その情報を持つ
enum ImageType
を定義しておき、var imageType
内で実際のヘッダとの比較を行っています。判別する画像の種類を増やしたいときはenum ImageType
のcase
を追加すればよいです。楽ですね。まとめ
正直バイナリとか触ったことなくて不安しかないので、間違い等あればご指摘お願いします...
参考文献
- 投稿日:2020-04-03T10:42:07+09:00
UICollectionViewCellのFade Inアニメーションを作る
この記事について
UICollectionViewCellのFade Inアニメーションの作り方
How To
UICollectionViewのdelegateの
willDisplay
メソッドでalphaをアニメーションする。注意: デフォルトではアニメーション中はuserControllがdisableされるので、optionsに
.allowUserInteraction
を設定する。これにより、アニメーション中にもスクロールできる。override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { cell.alpha = 0 UIView.animate(withDuration: 0.5, delay: 0.0, options: .allowUserInteraction, animations: { cell.alpha = 1 }, completion: nil) }Result
*輪郭の形が変わって見えるのはgifデータの問題です
- 投稿日:2020-04-03T00:12:46+09:00
Swift Optionalのベストプラクティス
概要
つい最近までObjective-CでiOSアプリを作っていた筆者が,Swift移行を進めた際に,最も頭を悩ませたのがOptionalの扱い。もちろん,?や!やif let, guard letは理解できるけども,ベストな方法がなかなか決まらなかった。以下,現状で筆者が思うベストな使い方をまとめておきます。
改善提案や「こんな場合はどうするの?」は大歓迎です。
!がついている変数はいつ使う?
例えば,UIViewControllerのサブクラスでは,よく以下のような定義をしますね。
sample.swift@IBoutlet weak var titleLabel:UILabel!Storyboardを使用する際の定番の実装です。これの意味をよく考えてみると,!のついた変数宣言をいつ使うのかが理解できます。
前提として,!のついた変数はOptional型であり,nilを許容します。それがなぜ,Outletの宣言に使われるのか。それは,ViewControllerのインスタンス化とStoryboardの読み込みタイミングが関係しています。
ViewControllerがインスタンス化された時には,まだStoryboardは読み込まれていないわけですね。その時点でViewControllerのtitleLabelはnilになってしまうので,titleLabelはnilを許容しなければなりません。
ただし,一度Storyboardが読み込まれ,titleLabelにUILabelのインスタンスがセットされた後は,このプロパティが変更されることは(ほぼ)ありません。こういう場合に!つきの変数宣言を使用します。最初はnilだが,一度値をセットしたら2度と変更しないようなものは!付きで宣言するということですね。?がついている変数,何もついていない変数の宣言
!付きの変数についてはわかったけども,?や何もついていない変数についてはどうするべきか。
例えば,何らかの処理が進行中かどうかを示すフラグは,通常Boolで宣言します。Sample.swiftvar isProcessing = falseこの種の変数はnilは入らないので,宣言と同時に値を代入します。!や?はつきません。
逆に,nilをうまく使う場合としては,例えば何らかのSNS的なアプリで,ユーザーが自分のプロフィール画像を選択するケース。画像の選択はUIImagePickerControllerで良いと思いますが,そこから受け取った画像をどうするか。Sample.swiftvar selectedImage:UIImage? = nilこのように選択された画像を保持しておくプロパティを宣言しておき,ユーザーが画像を選択した場合には,その画像をこのプロパティにセットします。
何も選択されていない場合にはnilになっているので,if let...でnilチェックし,nilじゃない場合にはプロフィール画像を更新するわけですね。if let... はいつ使う? Dictionaryからデータを取り出す場合
ご存知の通り,!や?つきの変数にnilが入っていないかを判定するために使われるのが,if letやguard letです。文法的な定義は他の記事を参考していただくとして,問題はこれらの上手な使い方です。
例えば,JSONでサーバから受け取ったデータ。dictionaryなのだが,[String:Any]だとする。何らかのキーで辞書から値を取り出した場合,値がnilの場合もあるし,nilじゃなかった場合でもStringやIntにキャストしなきゃいけない。キャストが失敗することもある。こういう場合,if let...とas?を使用した書き方がベストプラクティスになるでしょう(自信なし)。sample.swiftlet dict:[String:Any] = ["key_string":"string", "key_int":10] if let string = dict["key_string"] as? String{ print("stringありまっせ") } else{ print("値がなかった場合のデフォルト値が必要なら,ここで決めましょう") }as? は,キャストが失敗した場合nilを返します。ここでas!を使ってしまうと,キャストが失敗するとアプリはクラッシュ。as? とif let...を使って値を安全に取り出すわけですね。
guard let ... elseは,いつ使う?
これも文法的な定義は他に譲るとして,この構文の重要な特徴としてあげられるのが,else句では必ずreturnしなきゃいけないということ。つまり,この構文の意味としては「値がnilだったらそこで処理を中断」ということです。
かつてObjective-Cの時代にも「値がnilだったらそこでreturn」というコードはよく書かれていたのだけども,guard let...はまさにそれですね。特にここでサンプルは挙げませんが,guard let...はそういうものだと思います。(自信なし)最後に
他にもOptionalの扱いに迷うケースがありましたら,ぜひコメントお寄せください。間違いの指摘も歓迎です。