20200403のSwiftに関する記事は10件です。

簡単にFirebase Storageに複数のファイルをアップロードする[swift]

概要

複数のファイルのアップロードを以下のように扱えるようにStorageUploaderを作りました。どれくらいアップロードされているのか監視もできます。

sample.swift
let 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.swift
import 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.swift
switch (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
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReSwiftで複数のReducerってどうするん?

はじめに

ReSwiftの勉強をしていて、Storeに登録するReducerが複数の時どうするんだっけ?と思ったのでメモです。

CounterExample

ReSwiftの公式の「CounterExample」を見ていて、以下のコードがありました。

AppDelegate.swift
import 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?#340
func 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やらもう少し細かく分けると、自作しないといけないような気がしますが、とりあえずこいつを使っていくことにします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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や条件などは自身のソースコードに合うように変更してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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や条件などは自身のソースコードに合うように変更してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのAmazonリンクからAmazonアプリを起動する方法

Info.plistにLSApplicationQueriesSchemesというキーでTypeをArrayにし、そこにアイテムを追加し、TypeをStringでvalueをcom.amazon.mobile.shopping.webにします

アソシエイトリンクの場合は、短縮URLhttps://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>という方式は使えなくなったようだ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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的なものはできました。
たくさんの方の役に立つと嬉しいです。
質問がある方は、コメント欄まで〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Modalで表示した子画面の閉じるイベントを親画面で検知する

概要

iOS13からModalで画面遷移した際のアニメーションが変更されました。
それによって、Modal画面を閉じた際に動くライフサイクルメソッドが変わりました。
(今まではViewWillAppearが動いていたので、そこで更新等の処理を書いていた人も多いはず。困った、、、)

Modalで表示した子画面の閉じるイベントを親画面で検知する方法を備忘として残しておきます。

環境

Xcode: 11.4
Swift: Swift5

手順1

適当なViewControllerを2つ用意。
ここでは遷移元を「ParentViewController」遷移先を「ChildViewController」とします。

ParentViewController.swift
import UIKit

final class ParentViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
  }
}
ChildViewController.swift
import UIKit

final class ChildViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
  }
}

手順2

ChildViewControllerが閉じた際のイベントは以下のメソッドで検知可能です。

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController

「UIAdaptivePresentationControllerDelegate」に準拠することで使用可能です。
ParentViewControllerに以下を実装します。

ParentViewController.swift
import 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が出てから半年以上経っているのに調べるまで知りませんでした。
ライフサイクルメソッドに書かなくていいので実装がスッキリしますね!
誰かのお役に立てれば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像ファイルの種類を判別する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 ImageTypecaseを追加すればよいです。楽ですね。

まとめ

正直バイナリとか触ったことなくて不安しかないので、間違い等あればご指摘お願いします...

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

2020-04-03-1.gif

*輪郭の形が変わって見えるのはgifデータの問題です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift
var isProcessing = false

この種の変数はnilは入らないので,宣言と同時に値を代入します。!や?はつきません。
逆に,nilをうまく使う場合としては,例えば何らかのSNS的なアプリで,ユーザーが自分のプロフィール画像を選択するケース。画像の選択はUIImagePickerControllerで良いと思いますが,そこから受け取った画像をどうするか。

Sample.swift
var 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.swift
let 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の扱いに迷うケースがありましたら,ぜひコメントお寄せください。間違いの指摘も歓迎です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む