20201127のSwiftに関する記事は7件です。

初心者による妄想DI(モノマネ芸人の巻)

DIとは

引用元:weblio辞書
フルスペル:Dependency Injection
読み方:ディーアイ
別名:依存性注入,依存性の注入

DIとは、プログラミングにおけるデザインパターン(設計思想)の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入(Inject)してゆくという概念のことである。


依存性の注入???

※依存性と言う翻訳は間違いのようです。
DI != 依存性の注入
@loveeさんありがとうございます!


依存という言葉が私には抽象的で理解しずらいので
まず依存という概念から理解するために
私なりの具体例を考えてみました。


「モノマネ芸人」「歌手」


モノマネ芸人」はマネるため常に「歌手」を監視しています。
そして「歌手」が歌詞を変えると自分が歌う歌詞も変わります。

一方「歌手」は「モノマネ芸人」に合わせる必要はないので
モノマネ芸人」の歌詞が変わろうと「歌手」の歌詞はかわりません。

この状況を
モノマネ芸人」は「歌手」に依存しているといえます。


具体例をコードにしてみます。

歌手クラスを作ります。

class 歌手クラス {
    let 名前: String = "和田ア〇子"
    let 歌詞: String = "あの頃は~♪"
    func 歌う() {
        print("「\(歌詞)」")
    }
}
let 歌手 = 歌手クラス()
歌手.歌う()
//「あの頃は~♪」

続いてモノマネ芸人クラスを作ります。

class モノマネ芸人クラス {
    let 名前: String = "シャチ〇コ"
    let 本人: 歌手クラス = 歌手クラス() // もはや和田ア〇子
    func マネる() {
        本人.歌う()
        print("by \(名前)")
    }
}
let モノマネ芸人 = モノマネ芸人クラス()
モノマネ芸人.マネる()
// 「あの頃は~♪」
// by シャチ〇コ

これでシャチ〇コ和田ア〇子に依存している状態を
作ることができました。


この状態だと
インスタンス化すると自動的に
歌手 = 和田ア〇子
モノマネ芸人 = シャチ〇コ
が確定してしまうし
モノマネ芸人は和田ア〇子のマネしかできません。


「シャチ〇コ」「ミス〇ル」を歌ったりするにはどうすればいいだろうか。。。


まず歌手クラスはイニシャライザで名前と歌詞を決められるようにしましょう。

class 歌手クラス {
    let 名前: String 
    let 歌詞: String
    init(名前: String, 歌詞: String) {
        self.名前 = 名前
        self.歌詞 = 歌詞
    }
    func 歌う() {
        print("「\(歌詞)」")
    }
}
let ミス〇ル = 歌手クラス(名前: "ミス〇ル", 歌詞: "シーソーゲーム♪")
ミス〇ル.歌う()

// 「シーソーゲーム♪」

これで和田ア〇子以外の歌手を作れるようになりました。


モノマネ芸人もイニシャライザで名前と本人(歌手)を決められるようにしましょう。

class モノマネ芸人クラス {
    let 名前: String 
    let 本人: 歌手クラス
    init(名前: String, 本人: 歌手クラス) {
        self.名前 = 名前
        self.本人 = 本人
    }
    func マネる() {
        本人.歌う()
        print("by \(名前)")
    }
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ミス〇ル)
モノマネ芸人.マネる()

// 「シーソーゲーム♪」
// by シャチ〇コ

これでめでたくミス〇ルを歌うことができました。


実は今のがDIの内の一つなのです。

Initializer Injection
そのままですがイニシャライズ時に依存先のオブジェクトを注入することです。

今回だとこの部分です。

    init(名前: String, 本人: 歌手クラス) 

イニシャライズで依存先の歌手を代入してますね。


ではもう一つのDIである
Method Injectionという方法があります
こちらはどんなものかと言うとその名の通り
メソッドを呼ぶ時に依存先のオブジェクトを注入することです。
次はメソッドインジェクションを使った
モノマネ芸人のコードを見てみましょう。


class モノマネ芸人クラス {
    let 名前: String 
    init(名前: String) {
        self.名前 = 名前
    }
    func マネる(本人: 歌手クラス) {
        本人.歌う()
        print("by \(名前)")
    }
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ")
モノマネ芸人.マネる(本人: ミス〇ル)

// 「シーソーゲーム♪」
// by シャチ〇コ

「マネる」メソッドの引数で依存先である歌手インスタンスを代入しています。
こちらもめでたくミス〇ルを歌うことができました。


しかし!


実はまだ違和感が残っています。

そもそもの話なのですが
モノマネ芸人って、歌だけじゃないですよね?


モノマネといえば
歌手や俳優やスポーツ選手や芸人など、、、
なんなら動物のまねだってします.

要するに
披露する人をマネるのです!

いわば「芸能人」に依存すればいいのです!

※動物は人じゃないですね、、、


「何かしらを持ち披露する人を芸能人とする」

このように
「共通な属性を抜き出し、これを一般的な概念として捉えること」
抽象化といいます。
SOLID原則というオブジェクト指向の設計原則によると

  • 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
  • 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

つまり歌手もモノマネ芸人も芸能人という抽象に依存するように設計するとよさそうです。


プロトコル

Swiftではプロトコルを使うことで抽象を表現できます。

protocol 芸能人 {
    var 名前: String { get }
    var : String { get }
    func 披露() 
}

名前を持っていて
披露することができるやつ

すなわち芸能人プロトコルです。


このプロトコルで歌手を表現してみます。

class 歌手クラス: 芸能人 {
    let 名前: String
    let : String
    init(名前: String, : String){
        self.名前 = 名前
        self. = 
    }
    func 披露() {
        print("「\()」")
    }
}
let 小田〇正 = 歌手クラス(名前: "小田〇正", : "言葉にできない〜♪")
小田〇正.披露()
// 「言葉にできない〜♪」


試しにポケ〇ンクラスも作ってみます。

class ポケ〇ンクラス: 芸能人 {
    let 名前: String
    let : String
    init(名前: String, : String){
        self.名前 = 名前
        self. = 
    }
    func 披露() {
        print("サ〇シ「いけ!\(名前)!」")
        print("「\()」")
    }
}
let ピ〇チュウ = ポケ〇ンクラス(名前: "ピ〇チュウ", : "ピッカーーー!")
ピ〇チュウ.披露()
// サ〇シ「いけ!ピ〇チュウ!」
// 「ピッカーーー!」


モノマネ芸人側はこうなります。

class モノマネ芸人クラス {
    let 名前: String 
    let 本人: 芸能人
    init(名前: String, 本人: 芸能人) {
        self.名前 = 名前
        self.本人 = 本人
    }
    func マネる() {
        本人.披露()
        print("by \(名前)")
    }
}
let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ピ〇チュウ)
モノマネ芸人.マネる()

// サ〇シ「いけ!ピ〇チュウ!」
// 「ピッカーーー!」
// by シャチ〇コ

ずいぶん自由がきく設計となり
モノマネ芸人に様々な
芸能人をDIできるようになりました。

しかしもうちょっとお付き合いいただきたい。

芸能人String型しか扱えないのも
修正したいです。

protocol 芸能人 {
    var 名前: String { get }
    var : String { get } // これです。
    func 披露() 
}

Swiftでは動的に型を指定する仕組みとして
ジェネリクスと言う仕組みを使います。
protocolに使う場合は

assosiatedtype 型変数

で型変数を定義しておいて
class内にて

typealias 型変数 = 指定の型

としてクラス側で型を決めることができます。


今回の場合だと

protocol 芸能人 {
    associatedtype 芸種 // この型変数が↓
    var 名前: String { get }
    var : 芸種 { get } // ここにも反映されます
    func 披露() 
}

※型変数と言う言葉は正しいかどうかわかりませんがわかりやすいので今回はそう呼ばせてください!


[String]を持った歌手クラス

class 歌手クラス: 芸能人 {
    typealias 芸種 = [String] //←ここでprotocolの芸種という型変数に[String]型を代入
    var 名前: String
    var : 芸種 // ここも
    init(名前: String, : 芸種){ //ここも
        self.名前 = 名前
        self. = 
    }
    func 披露() {
        for g in  { // 配列で定義しているのでイテレータ回せちゃいます
            print("「\(g)」")
        }

    }
}
let 小田〇正 = 歌手クラス(名前: "小田〇正", : ["ら〜らーらー","ららーらー","言葉ぁに","できなぁい"])
小田〇正.披露()
// 「ら〜らーらー」
// 「ららーらー」
// 「言葉ぁに」
// 「できなぁい」

Int型の芸を持ったスクワットクラス

class スクワットクラス: 芸能人 {
    typealias 芸種 = Int
    let 名前: String
    let : 芸種
    init(名前: String, : 芸種){
        self.名前 = 名前
        self. = 
    }
    func 披露() {
        print("レッツスクワット!")
        for i in 1... {
            print("\(String(i))!")
        }
        print("ビクトリー!")
    }
}
let ビリー = スクワットクラス(名前: "ビリー", : 5)
ビリー.披露()

// レッツスクワット!
// 1!
// 2!
// 3!
// 4!
// 5!
// ビクトリー!


しかしここで注意です。
associatedtypeを持ったprotocolはプロパティの型としてとして利用できません

class モノマネ芸人クラス {
    let 名前: String
    let 本人: 芸能人 // ここです。
    ...

こんなエラーが出ます。

error: protocol '芸能人' can only be used as a generic constraint 
because it has Self or associated type requirements

google翻訳によると
「プロトコル '芸能人'は、自己または関連する型の要件があるため、ジェネリック制約としてのみ使用できます。」

つまり型として使うにはtypealiasなどで型を指定しないと制約として機能しないので
ストアドプロパティには直接定義できなくなるのです。
ただしジェネリック制約としてのみ使用できるようです。


エラーメッセージに言われた通り
ひと工夫してジェネリクスを使って動的な型(クラス)を指定すると実現が可能です。

class モノマネ芸人クラス<T: 芸能人>{
    let 名前: String 
    let 本人: T
    init(名前: String, 本人: T) {
        self.名前 = 名前
        self.本人 = 本人
    }
    func 披露() {
        本人.披露()
        print("by \(名前)")
    }
}

そうすると
小田〇正をDIしたり

let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: 小田〇正)
モノマネ芸人.披露()
// 「ら〜らーらー」
// 「ららーらー」
// 「言葉ぁに」
// 「できなぁい」
// by シャチ〇コ


ビリーをDIしたり
も出来ちゃうのです。

let モノマネ芸人 = モノマネ芸人クラス(名前: "シャチ〇コ", 本人: ビリー)
モノマネ芸人.披露()
// レッツスクワット!
// 1!
// 2!
// 3!
// 4!
// 5!
// ビクトリー!
// by シャチ〇コ


最後に

DIは手段であり目的ではないのですが
設計に関するドキュメントを読むとよく出てきて
さらっと流されることもしばしばあります。DIに限りませんがw
私はそこから先に進めないことが多々あったので
もし同じ状態の方がいらっしゃれば
一旦DIのイメージが沸いた状態で
ドキュメントに戻るとより理解が深まるのではないかという想いで
敢えてDIという手段自体に焦点を当てた記事にしました。

有識者からすると突っ込みどころは満載だろうと予測しております。
可能であれば間違いやご指摘をたくさんいただけると幸いです。

以上!!

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

Firebase Authenticationを使ってiOSアプリのログイン機能を実装する

最近、iOSアプリにFirebase Authentication を使ってSMS認証を実装する機会がありました。
実際に利用してみた感想としては、ログイン機能新規会員登録 などの複雑を要する機能を比較的簡単に品質の高く実装できるのでおすすめ出来ると感じました。

前提

 

参考となる資料

Firebaseはドキュメントが充実しており、環境構築に関しては公式のドキュメントの手続きに沿えば基本的に問題ないと感じました。

Firebaseへ電話番号の送信を行う

[注意点] 日本の電話番号コード +81 を電話番号の先頭に付与する必要があるので、注意してください。

func sendPhoneNumber(phoneNumber: String) {

    let num = "+81" + phoneNumber
    // Firebaseへ電話番号を送信
    PhoneAuthProvider.provider().verifyPhoneNumber(num, uiDelegate: nil) { [weak self] verificationID, error in
        if let error = error { // Errorの場合はこちらへ }
        if let id = id {
            // 取得したauthVerificationIDをUserDefaultsへ保存
            UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
            // SMS画面へ遷移させる
        }
    }
}

verificationID を永続領域に保存するメリットについては、Firebaseの公式ドキュメントに記載がある通りです。

確認 ID を保存し、アプリの読み込み時に復元します。これにより、ユーザーがログインフローを完了する前にアプリが終了した場合でも(たとえば SMS アプリへの切り替え時など)、有効な確認 ID を残すことができます。

確認コードを使ってユーザーをログインさせる

以下の関数を使ってユーザの クレデンシャル を発行します。
クレデンシャル とは、ユーザのIDやPWなどのユーザ情報におけるパラメータの総称です。
クレデンシャルを生成するための関数の第一引数には電話番号を送信する際に永続領域に保存した verificationID を渡し、第二引数にはユーザが実際に入力した確認コードを渡します。

コールバックでログイン結果が返されるので、返ってきた結果によってエラー文言を出し分けたり、インジケータを表示してユーザに通信状態を伝えるとより良いと思いました。

// ユーザから入力された確認コードで
func sendSMSAuthCode(code: String) {
    // クレデンシャルの発行
    let id = userDefault.object(forKey: "authVerificationID") as? String ?? ""
    let credential = PhoneAuthProvider.provider().credential(withVerificationID: id, verificationCode: code)

    Auth.auth().signIn(with: credential) { [weak self] authResult, error in
        if let error = error {
            guard let errorCode = AuthErrorCode(rawValue: error._code) else { return }
            switch errorCode {
            case .invalidVerificationCode: print("認証コードが正しくありません")
            default: print("エラーが起きました。しばらくしてから再度お試しください。")
            }
        }
        if let _ = authResult {
            let currentUser = Auth.auth().currentUser
            currentUser?.getIDTokenForcingRefresh(true) { idToken, _ in
                guard let _ = idToken else {
                    print("トークンの取得に失敗しました")
                    return
                }
                // LOGIN SUCCESS!!!!
            }
        }
    }
}

以上で、ログイン機能は完了で??

アプリ初回起動時にログイン状態にあるかどうかを取得したい

ログイン機能は実装できましたが、アプリ起動時にユーザにログインフローを毎回踏んで貰うのは現実的ではありません。
Firebaseは特にユーザに操作をさせず、ログイン済みかどうかのステータスを取得する事ができます。したがって、AppDelegateやユーザ情報を取得する処理の中で以下の処理を実行するとログイン済みの場合はtokenが取得出来ます。
私はとりあえず、 tokenの取得成功 サインインが必要 エラー のステータスを用意してみました。

enum FirebaseTokenCallbackResult { case success(token: String), needSignIn, error(error: Error?) }

func fetchFirebaseToken() -> Observable<FirebaseTokenCallbackResult> {
    return Observable.create { observer in
        guard let currentUser = Auth.auth().currentUser else {
            // 自分自身のアカウントが取得できなかった場合は、新規会員登録のフローを踏む
            observer.onNext(.needSignIn)
            return Disposables.create()
        }
        currentUser.getIDToken(completion: { [weak self] idToken, error in
           if let token = idToken {
                self?.firebaseToken = token
                return observer.onNext(.success(token: token))
            }
            return observer.onNext(.error(error: error))
        })
        return Disposables.create()
    }
}

注意点

AppDelegateで以下の処理を呼び出して初期化すると思いますが、以下の処理が実行されるよりも先にAuthなどの処理を呼び出してしまうとクラッシュする可能性があります。そのため、以下の処理が実行されるよりも先に currentUser などを取得しないよう注意が必要です。

FirebaseApp.configure()

以上です。

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

Swift Xibファイルを使い始めるとloadNibNamedでexc_bad_access 落ちました

お仕事の方のプロジェクトでは、
xibファイルがたくさん採用されたため、最近xibファイルを使い始めました。

xibファイルを追加するソースコードの箇所で落ちて、
EXC_BAD_ACCESSが発生してはまったのでメモしておきます。

Xcodeで新規ファイルを作成する時に、Viewを選択すると、xibファイルが生成、
Viewで必要なパーツをドラッグ、CocoaTouchClassでソースコードで書きます。

例えば、PlayerViewのxibファイル(Custom View)を作った。
同じ名前でCocoaTouchClassを作り、このようにコードを書きます。

class PlayerView: UIView {

    var playerLayer:AVPlayerLayer?
    @IBOutlet weak var timeLabel: UILabel!

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer?.frame = self.bounds
    }

// 以下も書く場合があるようですが、これの有無による違いがよくわかりません。
//    override init(frame: CGRect) {
//        super.init(frame: frame)
//        loadFromNib()
//    }

    required public init?(coder aDecoder: NSCoder){
        super.init(coder: aDecoder)
        loadFromNib()
    }

    private func loadFromNib(){
        let view = Bundle.main.loadNibNamed("PlayerView", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
    }
}

そうすると、xibファイル(Interface)がcocoa touch class(ソースコード)と関連付できます。
しかし、ビルドしてみるとこの行

let view = Bundle.main.loadNibNamed("PlayerView", owner: self, options: nil)?.first as! UIView

でthread 1: exc_bad_access (code=2, address=0x7ffee5b3eff8)が発生して落ちました。
いろいろ記事を見ましたが、この記事が救ってくれました。

私の場合、原因がこれです!
FileOwnerをPlayerViewに設定し、
xigファイル内のViewのCustomClassも重ねてPlayerViewに設定してしまったからです。
Viewで重複設定した分外したら、落ちなくなりました。
スクリーンショット 2020-11-27 17.28.49.png

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

touchesbeganによるダブルタップ

備忘録メモ
少し詰まったので!

ダブルタップしたときの位置座標を取得したかった。
TapGestureRecognizerだとsenderとかが色々めんどくさそうだったので、
touchesbeganで実装

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first{
            if touch.tapCount == 2{
              print("double tap!")
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モーダル遷移

ストーリーボードによるモーダル遷移から、前の画面に戻る方法

self?.dismiss(animated: true, completion: nil)

ストーリーボードによるモーダル遷移から、2つ前の画面に戻る方法

self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)```
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 上で巨大なファイルを復号する方法

エキサイトで刺身にたんぽぽを添える仕事をしているエンジニアの坂田です。
cocorus と言うアプリの iOS版 を開発したりしてます。

突然ですが 1GB 超の巨大なファイルの暗号化/復号を iOS (iPhone 実機)上で行おうとして OS 側からアプリを落とされる経験をされたことはありますか?
自分はあります。
当記事では巨大なファイルの復号を iOS 上で行うための方法を紹介します。

想定

  • マシン
    iPhone (iOS 14等)
  • 言語
    Swift 5
  • 想定暗号
    aes-128-cbc

一般的な復号コード

おそらくセオリー通り実装すると下記のようなコードになるかと思います。

import CommonCrypto

class CommonCryptoUtility {
    /// 中略

    /// 復号処理
    ///
    /// - Parameters:
    ///   - encryptedData: 暗号化されたData型リソース
    ///   - keyData: 暗号キーバイナリ
    ///   - initializationVectorData: 初期化ベクターバイナリ
    ///   - isPadding: PKCS#7 パディングの有無
    /// - Returns: 復号されたData型リソース
    /// - Throws: CommonCryptoUtilityError のいずれか
    func decrypt(
        encryptedData: Data,
        keyData: Data,
        initializationVectorData: Data,
        isPadding: Bool = true
    ) throws -> Data {
        // 復号化後のメモリーサイズ
        let decryptBufferLength: Int = encryptedData.count
        // 復号化後のメモリを確保
        var decryptBufferData = Data(count: decryptBufferLength)
        // デコード処理バイト数
        var executeBytesCount: size_t = 0

        // デコード
        let options: UInt32 = isPadding ? CCOptions(kCCOptionPKCS7Padding) : 0
        do {
            try keyData.withUnsafeBytes { (keyBytes: UnsafeRawBufferPointer) in
                try initializationVectorData.withUnsafeBytes { (ivBytes: UnsafeRawBufferPointer) in
                    try encryptedData.withUnsafeBytes { (encryptedDataPointer: UnsafeRawBufferPointer) in
                        try decryptBufferData.withUnsafeMutableBytes { decryptBufferDataPointer in
                            guard let keyBytesBaseAddress = keyBytes.baseAddress,
                                let ivBytesBaseAddress = ivBytes.baseAddress,
                                let encryptedDataBytesBaseAddress = encryptedDataPointer.baseAddress,
                                let decryptBufferDataBytesBaseAddress = decryptBufferDataPointer.baseAddress
                            else {
                                throw CommonCryptoUtilityError.decryptFailedWhenConvertBaseAddress
                            }

                            let cryptStatus = CCCrypt(
                                CCOperation(kCCDecrypt),
                                CCAlgorithm(kCCAlgorithmAES),
                                options,
                                keyBytesBaseAddress,
                                keyData.count,
                                ivBytesBaseAddress,
                                encryptedDataBytesBaseAddress,
                                encryptedData.count,
                                decryptBufferDataBytesBaseAddress,
                                decryptBufferLength,
                                &executeBytesCount
                            )

                            guard cryptStatus == kCCSuccess else {
                                throw CommonCryptoUtilityError.decryptFailed
                            }
                        }
                    }
                }
            }
        } catch {
            throw CommonCryptoUtilityError.decryptFailedWhenConvertUnsafePointer
        }

        return decryptBufferData
    }

    // 中略
}

ただ、上記のような実装だと暗号化データをすべて渡すので、このままだとメモリを食いつぶして iOS にアプリを落とされてしまいます。
なので、この関数をラップして分割して復号したくなるのが心情です。
幸いなことに、AES 128 CBC と言う暗号モードは16バイト単位での暗号化を行っており、暗号ブロックの一つ前のブロックがそのまま今のブロックの初期化ベクターになるとのことです。
つまり、前の16バイトさえわかればどのブロックからでも複合できるということになります。

指定ブロックを取り出すための復号処理のラッパー

上記を踏まえてブロック毎に復号するように上記メソッドを呼び出すラッパーを実装すると以下のようになります。

import CommonCrypto

class CommonCryptoUtility {
    // 中略

    let blockSizeAES128: UInt64 = UInt64(kCCBlockSizeAES128)

    /// 指定された範囲を復号する
    /// - 2 GiB 以内のファイルのみ扱うものとする(32Bit)
    /// - 8 EiB 以内のファイルのみ扱うものとする(64Bit)
    /// - Parameters:
    ///   - sourceURL: ファイルURL
    ///   - keyData: 暗号キーデータ
    ///   - firstInitVectorData: 初期化ベクターデータ
    ///   - requiredSliceOffset: 切り出すオフセット
    ///   - sliceLength: 切り出すバイト数
    /// - Returns: 複合したバイナリデータ
    func decrypt(
        readFrom sourceURL: URL,
        keyData: Data,
        firstInitVectorData: Data,
        offset requiredSliceOffset: UInt64,
        sliceLength: Int
    ) throws -> Data {
        let fileHandler = try FileHandle(forReadingFrom: sourceURL)
        defer {
            fileHandler.closeFile()
        }
        guard let sourceFileSize = MediaManager.getFileSize(from: sourceURL) else {
            throw CommonCryptoUtilityError.destinationStreamOpenFailed
        }
        guard requiredSliceOffset + UInt64(sliceLength) <= sourceFileSize else {
            throw CommonCryptoUtilityError.requestSizeGreaterThanSource
        }
        guard 0 < sliceLength else {
            throw CommonCryptoUtilityError.lengthLessThanOne
        }

        let alignedOffset4decrypt: UInt64 = requiredSliceOffset / blockSizeAES128 * blockSizeAES128
        let heading: Int = Int(requiredSliceOffset - alignedOffset4decrypt)
        let totalDecryptBlockAmount: Int = (sliceLength + heading) / kCCBlockSizeAES128 + 1
        let totalDecryptBlockLength: Int = totalDecryptBlockAmount * kCCBlockSizeAES128
        let dropLength: Int = totalDecryptBlockLength - (sliceLength + heading)

        // iv設定
        let initVectorData: Data
        if alignedOffset4decrypt == 0 {
            initVectorData = firstInitVectorData
        } else {
            fileHandler.seek(toFileOffset: alignedOffset4decrypt - blockSizeAES128)
            initVectorData = fileHandler.readData(ofLength: kCCBlockSizeAES128)
        }
        fileHandler.seek(toFileOffset: UInt64(alignedOffset4decrypt))

        // 復号
        let encryptedChunkData = fileHandler.readData(ofLength: Int(totalDecryptBlockLength))
        let decryptedChunk: Data = try decrypt(
            encryptedData: encryptedChunkData,
            keyData: keyData,
            initializationVectorData: initVectorData,
            isPadding: alignedOffset4decrypt + UInt64(totalDecryptBlockLength) == sourceFileSize
        )

        // 要求されたバイト数を切り出し
        let extractData = decryptedChunk.subdata(in: heading..<(totalDecryptBlockLength - dropLength))

        return extractData
    }

    // 中略
}

これでどのオフセットからでもデータを切り出すことが出来るようになりました。

分割して復号しつつ保存する

最終目的である、暗号化されたファイルを読んで復号し、ファイルへ書き出すように上記メソッドを更にラップすると以下のようになります。

import CommonCrypto

class CommonCryptoUtility {
    // 中略

    // 最大処理チャンクサイズ(100MiB)
    let cryptChunkSize: Int = 104857600

    /// ファイルへ復号
    ///
    /// - 暗号化ファイルを読み出し、CommonCryptoUtility.chunkSize毎に復号処理をしてテンポラリファイルへ書き出す。
    /// - Parameters:
    ///   - sourceURL: 暗号化されたファイルのURL
    ///   - destinationURL: 保存先のURL
    ///   - keyData: 暗号キーバイナリ
    ///   - firstInitVectorData: 初期化ベクターバイナリ
    /// - Throws: CommonCryptoUtilityError のいずれか
    func decrypt(
        readFrom sourceURL: URL,
        storeTo destinationURL: URL,
        keyData: Data,
        firstInitVectorData: Data
    ) throws {
        do {
            // ファイルストリームを用意
            guard let destinationData = OutputStream(url: destinationURL, append: false),
                let _sourceFileSize = MediaManager.getFileSize(from: sourceURL)
            else {
                throw CommonCryptoUtilityError.destinationStreamOpenFailed
            }
            destinationData.open()
            defer {
                // ファイルストリームを閉じる
                destinationData.close()
            }
            let sourceFileSize = Int(_sourceFileSize)
            var totalSliceSize = 0
            while totalSliceSize < sourceFileSize {
                let currentChunkSize = sourceFileSize - totalSliceSize > cryptChunkSize ? cryptChunkSize : sourceFileSize - totalSliceSize
                let decryptedChunk: Data
                do {
                    // チャンク毎に復号
                    decryptedChunk = try decrypt(
                        readFrom: sourceURL,
                        keyData: keyData,
                        firstInitVectorData: firstInitVectorData,
                        offset: UInt64(totalSliceSize),
                        sliceLength: currentChunkSize
                    )
                } catch {
                    throw error
                }
                var decryptedBytes = [UInt8](decryptedChunk)
                // 保存先に追記
                let writeResult = destinationData.write(&decryptedBytes, maxLength: decryptedBytes.count)
                guard writeResult >= 0 else {
                    throw CommonCryptoUtilityError.failedToWriteToDestination
                }
                totalSliceSize += currentChunkSize
            }
        }
    }

    // 中略
}

これで実用的なメモリ使用量でファイルの復号が出来るようになりました。
しかしながら、複数の巨大な暗号化ファイルを連続して復号すると、mach_task_basic_info() などで正しくメモリを開放しているように見えても、Instruments 上ではメモリ使用量が増え続けました。
どうやら iOS 上(.app コンテナを実行する vm 上?)ではまだ開放されていないようで、それらが開放されるまで若干の時差があるようです。
なので、連続して複合処理をする場合は数秒のディレイを入れながら復号すると成功するかと思います。

参考

https://github.com/backslash-f/aescryptable/blob/master/Sources/AESCryptable/AESCryptable.swift

最後に

良いのか悪いのか cocorus iOS版 の実コードから引っ張ってきています。
ま、アプリはコンテンツが主体なのでこの程度は問題ないでしょう。
それに、これを実装するため参考にしたコードを公開している方々へ、せめてものお返しとして辛かった部分をあえて公開することにしました。

ちなみに切り出しているので上記に書かれていない enum などあるので実際使う場合は適宜置き換えるなり定義するなりしないと動きません。
コードも回りっくどいのは試行錯誤の跡だと思って大目に見てください。
(改善案はお待ちしてます!)

あ、そうそう。
エキサイト株式会社ではエンジニアを随時募集しているようです。
気軽な気持ちで応募してみても良いのではないでしょうか?
https://www.wantedly.com/companies/excite

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

【Swift】Twitterでシェアする機能を追加する

アプリから下記のようなTwitterのツイート画面に遷移させる方法です。
example

動作環境

動作環境 バージョン
Xcode 12.1
iOS 14.1
Swift 5

コピペで使えるTwitterシェア関数

func shareOnTwitter() {

        //シェアするテキストを作成
        let text = "AppからTwitterでシェアする"
        let hashTag = "#ハッシュタグ"
        let completedText = text + "\n" + hashTag

        //作成したテキストをエンコード
        let encodedText = completedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)

        //エンコードしたテキストをURLに繋げ、URLを開いてツイート画面を表示させる
        if let encodedText = encodedText,
            let url = URL(string: "https://twitter.com/intent/tweet?text=\(encodedText)") {
            UIApplication.shared.open(url)
        }
    }

実装サンプル

Twitterでシェアボタンを押すと、上記のshareOnTwitter関数が実施されるサンプルです。

イメージ.GIF

解説

1.作成したテキストをURLに繋げれるように、addingPercentEncodingを行う

let encodedText = completedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)

作成したテキストにパーセントエンコードを行い、URLで使用できるようにします。

2.TwitterのURLにエンコードしたテキストを繋げて、そのURLを開いてツイート画面を表示する

if let encodedText = encodedText,
            let url = URL(string: "https://twitter.com/intent/tweet?text=\(encodedText)") {
            UIApplication.shared.open(url)
        }

・オプショナル型のencodeTextをオプショナルバインディング
・アンラップしたencodedTextURL(string:twitter.com/intent/tweet?text=(encodedText))に繋げる
・作成したURL(string:twitter.com/~)をオプショナルバインディング
UIApplication.shared.open(_ url: URL)にアンラップしたURLを入れる

UIApplication.shared.openとは?

Apple公式ドキュメントより

Attempts to open the resource at the specified URL asynchronously.

特定したURLを非同期で開くのを試みます。的な感じでしょうか。

まぁ、開いて欲しいURLを入れたら、そのページを開いてくれるようです!

まとめ

アプリが広まる仕掛けが必要と言うことで、今回個人開発アプリに実装しました。
特にフレームワークを使う必要も無いですし、簡単に実装できるので是非導入してみてください☺︎

何か誤りがありましたら、優しく教えていただけると幸いです?‍♂️

参考

iOS の Twitter シェア簡易実装 2019

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