20200122のiOSに関する記事は11件です。

UISearchBarのキャンセルボタンをアニメーション付きで表示したい話

UISearchBarをタップして編集状態中だけキャンセルボタンを表示したいと、
ちょびっと悩んだので備忘録として書いときます。

↓こんな感じのやつ
qqytl-ojzlm.gif

アニメーション無しの場合

showsCancelButtonを使うことで表示の制御はできますが、アニメーションはありません。

searchBar.showsCancelButton = true

アニメーション有りの場合

setShowsCancelButtonを使いましょう

searchBar.setShowsCancelButton(true, animated: true)

実装例

コピペをこよなく愛する者たちへの贈り物です?

import UIKit

class  ViewController: UIViewController,UISearchBarDelegate {

    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()

        // デリゲート設定
        searchBar.delegate = self
    }

    // 検索バー編集開始時にキャンセルボタン有効化
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar){
        searchBar.setShowsCancelButton(true, animated: true)
    }

    // キャンセルボタンでキャセルボタン非表示
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
        searchBar.setShowsCancelButton(false, animated: true)
    }

    // エンターキーで検索
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
        searchBar.resignFirstResponder()
        searchBar.setShowsCancelButton(false, animated: true)
    }

    // 入力された文字出力
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        print(searchText)
    }
}

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

UITextFieldで文字を入力した後にキーボードをしまう方法

今回の記事を書く理由

初学者に多い気がしますが、UITextFieldで文字を入力した後、キーボードが勝手に閉じてくれないのでそのためのコードを書く必要があります。

意外と何回も調べていたりしていたためメモの代わりに書きます。

今回は説明を交えながら書いてきます。

xcodeでreturnの文字を変更

まずxcodeでreturnの文字を変更することができます。

スクリーンショット 2020-01-22 14.11.32.png

ソースを追加

全体像

この後説明していきます。

class ViewController : UIViewController, UITextFieldDelegate {
    @IBOutlet var textField : UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.textField.delegate = self
    }
    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}

説明

まず、UITextFieldDelegateを設定します。

class ViewController : UIViewController, UITextFieldDelegate {
}

そうしたらviweDidLoadでデリゲートを書きます。ここでtextFieldのdelegateをselfにまかせます。

override func viewDidLoad() {
    super.viewDidLoad()
    self.textField.delegate = self
}

そうしたらtextFieldでreturn(Done)が押されたときの挙動を書きます。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
}

これで思った通りの動きになるかと思います。

swiftでキーボードの動きが怪しかったら参考にしてみてください。

参考

https://qiita.com/SRAUFactory/items/7314793bd51e4075a208

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

FirebaseのRemoteConfig使ってみた

はじめに

最近になってFirebaseのRemoteConfigを使う機会があったので、
このサイトに残しておこうかと思います。
ほぼ、ドキュメント通りなので、
気になる方だけ読んでいただければと思います。

RemoteConfigとは

アプリをアップデートすることなく、アプリの動作や外観を変更できる機能になります。
行なっている事としてはRemote Config Serverから設定されているパラメーターを取得して、
その値によって動作や外観を変更します。
RemoteConfigとは

環境

Xcode 11.3
swift 5
Firebase 6.15.0

FireBaseのインストール

インストール手順は省きます。
以下、公式ドキュメント
Firebase を iOS プロジェクトに追加する

RemoteConfig設定

Remote Config ドキュメント

remoteConfig = RemoteConfig.remoteConfig()
let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
remoteConfig.configSettings = settings

ドキュメントにあるようにシングルトンオブジェクトを作成し、通信を抑えるために同期する間隔を設定します。。。。が、早速ハマりました、、、、

settings.minimumFetchInterval = 0

フェッチ間隔を上記のように設定しても反映されいっぽい、、、(なぜ?)
一応、設定の優先度は以下のようです。

  1. fetch(long) のパラメータ
  2. FIRRemoteConfigSettings.MinimumFetchInterval のパラメータ
  3. デフォルト値(12 時間)

別にfetch(long)のパラメータに設定してないんだけどなぁ〜
仕方ないのでfetch(long)のパラメータで設定すると、今度はうまく反映されるようになりました。

var expirationDuration = 0.0

    func fetch() {
        self.remoteConfig.fetch(withExpirationDuration: self.expirationDuration) {
    (status, error) in {}
    }

ってことで続き

デフォルト値の設定

struct TestRemoteConfig: Codable {
    var isTest: Bool
}

class RemoteConfigTest {
    var defaultValue: NSObject {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        let value = try? encoder.encode(TestRemoteConfig(isTest: true))
        return value as NSObject? ?? "" as NSObject
    }

    func setDefault() {
        remoteConfig.setDefaults(["test_remote_config": self.defaultValue])
    }
}

デフォルト値を設定しておくことで、
RemoteConfigがバックエンドから値を取得していない場合や、
値が無い場合でも正常にアプリを動作させることができます。

デフォルト値はplistファイルから取得できますが、今回はNSDictionaryオブジェクトを作成してます。

パラメータの設定

FireBaseコンソール画面でパラメーターを設定します。
Json形式の方が扱いやすと思うので今回はJson形式で設定。

スクリーンショット 2020-01-22 21.02.00.png

値のフェッチと有効化

remoteCongig.fetch(withExpirationDuration: self.expirationDuration) { (status, error) in 
    if let error = error {
        // error処理
     }

     if case .success = status {
         // 有効化
         self.remoteConfig.activate { (error) in
             if let error = error {
             // error処理
             }
         }
     }
}

パラメータの取得

remoteConfig.configValue(forKey: "test_remote_config").dataValue

上記のメソッドでfetchされた値を取得できます。

    func configValue() throws -> TestRemoteConfig {
        let data = self.remoteConfig.configValue(forKey: "test_remote_config").dataValue
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try decoder.decode(TestRemoteConfig.self, from: data)
    }

Json形式で定義していたので、デコードすれば"isTest"の値を自由に使えるようになります。
以上がRemote Configになります。

この機能を用いてA/Bテストも実施できるので是非やってみましょう!!

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

SceneKitで環境マップ

SCNScene *scene = [SCNScene sceneNamed:@"art.scnassets/my_scene.scn"];

// 背景に表示用
scene.background.contents = @[@"art.scnassets/px.png", @"art.scnassets/nx.png", @"art.scnassets/py.png", @"art.scnassets/ny.png", @"art.scnassets/pz.png", @"art.scnassets/nz.png"];

// ライティング用
scene.lightingEnvironment.contents = @[@"art.scnassets/px.png", @"art.scnassets/nx.png", @"art.scnassets/py.png", @"art.scnassets/ny.png", @"art.scnassets/pz.png", @"art.scnassets/nz.png"];

// モデル
SCNNode *modelNode = [scene.rootNode childNodeWithName:@"MyModel" recursively:YES];
SCNMaterial *material =[[SCNMaterial alloc] init];
material.locksAmbientWithDiffuse = true;
material.diffuse.contents = [UIColor redColor];
material.roughness.contents = @0.02;
material.lightingModelName = SCNLightingModelPhysicallyBased;

modelNode.geometry.firstMaterial = material;

[scene.rootNode addChildNode:modelNode];

真っ先にほしい環境マップではあるのだけれど、そのままの情報がなかった。
.hdrを使えたりするのかもしれない。Swiftでも同じ。Swiftが何かは知らない。

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

houstonを使ってrailsサーバーからプッシュ通知を送る時に設定できる値のメモ

1. 概要

"houston"というgemを使って、ruby on railsのサーバーからiOSアプリにプッシュ通知を送るようにしました。

今回は本文・SE・バッジ数だけの一番シンプルな通知を実装しましたが、GitHubのUsageを参照すると、通知データに対して他にも設定できる項目があるようです

将来細かい設定が必要になった時に忘れないように、調べたことを書いておきます。

2. メモ

Usage.rb
require 'houston'

# 定数の宣言はメソッド外でする
APN = Houston::Client.development
APN.certificate = File.read('/path/to/apple_push_notification.pem')

# 通知を送りたいデバイスのdevice tokenを代入
token = '<ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969>'

# houstonのインスタンスを生成
notification = Houston::Notification.new(device: token)
# 通知メッセージを設定
notification.alert = 'Hello, World!'

# アプリアイコンの右上に表示するバッジの数字を設定(この数字がそのまま表示されるので、加算するための計算はこの前に行っておく)
notification.badge = 57
# 通知を受信した時の音声ファイル(カスタムする場合は事前に用意)
notification.sound = 'sosumi.aiff'

# これをtrueにしないと通知を受信しない
notification.content_available = true

# リッチ通知を実装する際はtrueを設定
notification.mutable_content = true
# リッチ通知を実装する際は、XcodeのプロジェクトからUNNotificationContentExtensionのInfo.plistのcategoryを以下の値と同じにする
notification.category = 'INVITE_CATEGORY'

# 通知の設定は"AnyHashable("aps"): {...}"の値として送信されるが、それ以外にデータを付け足したい場合はここを設定する
notification.custom_data = { foo: 'bar' }

# 以下の値は"AnyHashable("aps"): {...}"の値に含むことができるが、使い道がわからない……
notification.url_args = %w[boarding A998]
notification.thread_id = 'notify-team-ios'

# 通知を送信する
APN.push(notification)

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

houstonを使ってrailsサーバーからプッシュ通知を送る時に設定できる値のメモ、あと多言語対応

1. 概要

"houston"というgemを使って、ruby on railsのサーバーからiOSアプリにプッシュ通知を送るようにしました。

今回は本文・SE・バッジ数だけの一番シンプルな通知を実装しましたが、GitHubのUsageを参照すると、通知データに対して他にも設定できる項目があるようです

将来細かい設定が必要になった時に忘れないように、調べたことを書いておきます。

2. メモ

Usage.rb
require 'houston'

# 定数の宣言はメソッド外でする
APN = Houston::Client.development
APN.certificate = File.read('/path/to/apple_push_notification.pem')

# 通知を送りたいデバイスのdevice tokenを代入
token = '<ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969>'

# houstonのインスタンスを生成
notification = Houston::Notification.new(device: token)
# 通知メッセージを設定
notification.alert = 'Hello, World!'

# アプリアイコンの右上に表示するバッジの数字を設定(この数字がそのまま表示されるので、加算するための計算はこの前に行っておく)
notification.badge = 57
# 通知を受信した時の音声ファイル(カスタムする場合は事前に用意)
notification.sound = 'sosumi.aiff'

# これをtrueにしないと通知を受信しない
notification.content_available = true

# リッチ通知を実装する際はtrueを設定
notification.mutable_content = true
# リッチ通知を実装する際は、XcodeのプロジェクトからUNNotificationContentExtensionのInfo.plistのcategoryを以下の値と同じにする
notification.category = 'INVITE_CATEGORY'

# 通知の設定は"AnyHashable("aps"): {...}"の値として送信されるが、それ以外にデータを付け足したい場合はここを設定する
notification.custom_data = { foo: 'bar' }

# 以下の値は"AnyHashable("aps"): {...}"の値に含むことができるが、使い道がわからない……
notification.url_args = %w[boarding A998]
notification.thread_id = 'notify-team-ios'

# 通知を送信する
APN.push(notification)

3. (追記) 通知の多言語対応

プッシュ通知の多言語対応について書いた記事があまり見当たらなかったので、ついでにメモしておきます。

以下の方法を用いると、アプリ内の表示と同じように端末の言語設定に応じた通知の多言語対応をすることができます。

Localizable.strings
"GAME_PLAY_REQUEST_FORMAT" = "%@ and %@ have invited you to play Monopoly";
Usage.rb
notification.alert = {
  # Localizable.stringsに記述したkeyを入れる
  "loc-key" : "GAME_PLAY_REQUEST_FORMAT",
  # 変数を設定した場合は配列で指定する
  "loc-args" : [ "Jenna", "Frank"]
}

引用元:Creating the Remote Notification Payload

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

【Swift】[weak self]付のクロージャに親スコープの変数を渡したい!!!

小ネタです。

UI更新処理ってメインスレッドでやらないといけないので、
DispatchQueue.main.async {[weak self] in …… }の中に書くじゃないですか?
このときに、関数内のスコープを持った変数をクロージャに渡したい、ということがありました。

class ViewController {
    func soramissionLoaded() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self] in
            //どうしたらいい?
        }
    }
}

正解

先に正解を書くと、

class ViewController {
    func soramissionLoaded() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self, word] in
            print(word) //->これを渡したい
        }
    }
}

で渡せました。
(この例だとself使ってませんが、実際のコードではselfにもアクセスする必要がありました)

というかそもそも、クロージャ内からwordにアクセス可能だったので、これでもいけます。

class ViewController {
    func soramissionLoaded() {
        var word = "これを渡したい"
        DispatchQueue.main.async {[weak self] in
            print(word) //->これを渡したい
        }
    }
}

下の書き方がいいと思います。

ダメな例

以下、NG集。

DispatchQueue.main.async {[weak self] word in
DispatchQueue.main.async {[weak self], word in
DispatchQueue.main.async {[weak self](word) in
DispatchQueue.main.async {[weak self](w: String = word) in
DispatchQueue.main.async {[weak self](word = self.word) in

よくわかる解説

とりあえず動けばいいだけなら、ここまで読んでいただければOK。
ちょっとモヤった(循環参照的なやつ大丈夫なのか?とか)ので、NGな例がなぜNGなのか調べました。
DispatchQueueクラスのasyncメソッドの仕様と、Swiftのキャプチャリスト(Capture List)の知識が必要です。

asyncメソッドの仕様

DispatchQueue.main.async {[weak self](word) in

これを書くと、

Contextual closure type '@convention(block) () -> Void' expects 0 arguments, but 1 was used in closure body

こんな感じで怒られると思います。
エラ〜メッセージがちょっと難しくて、最初わかんなかったんですが、考えてみたら当然で、
async()の引数として要求しているクロージャは() -> Void型のクロージャで、引数はありません。

async()の定義
public func async(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)

定義見ると引数ずらずらありますが、group/qos/flagsはデフォルト引数がそれぞれ指定されているので、
メインスレッド呼びたいだけの時は省略することが多いです。
最後のクロージャもトレイリングクロージャで引数名書かないので、普段使ってる書き方と定義が直感的に紐づかないですね。

キャプチャリストとは

クロージャは安易に使うと安易に循環参照を生みます。
それで[weak self]とかつけてるわけなんですが、実はこの[]がキャプチャリストです。
(僕はさっきはじめて知りました)

Resolving Strong Reference Cycles for Closures
(日本語版)2.16.5. クロージャによる強い参照の循環 | 自動参照カウント | Swift

クロージャの実行スコープは、親クラスとは別になるので、クロージャ内部で使う定数やら変数やらをキャプチャしてやる必要があります。

というかそもそもクロージャは呼び出されたスコープ内はキャプチャしてる

「クロージャに親スコープの変数渡したい時は、キャプチャリスト内に書いてね!」という結論にしようと思って記事を書いていたんですが、
調べてみたら、そもそもクロージャは呼び出されたスコープ内はキャプチャしてることが判明しました。

Capturing Values
2.7.3. 値のキャプチャ | クロージャ | Swift

まさかそのまま書けば使えるなんて思わず、色々な書き方を試してしまいました。。。?

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

[swift5]iOSアプリでApp Groupsを実装

アプリ間でのデータ共有を実装する方法を紹介します!
本ページでは、App Groups を利用します。

動作環境

対象 バージョン
iOS 13.3
macOS Catalina 10.15.2
Xcode 11.3.1
Swift 5.1.3

データ共有機能(App Groups)の実装

アプリ間でデータを共有するため、アプリは2つ作成します。

  1. データを表示・変更するアプリ
  2. データを表示のみするアプリ

1, 2 で同じデータを表示します。

アプリの設定(2つのアプリ共通)

2つのアプリで設定してください。

  1. 「Signing & Capabilities」から「App Groups」を追加する
  2. グループ名を追加する

ファイル作成(2つのアプリ共通)

  • Storyboard
    • ここでは、"AppGroups.storyboard" とします
  • ViewController.swift
    • ここでは、"AppGroupsViewController.swift" とします

画面を作成(データを表示・変更するアプリ)

Storyboard に、以下を載っけます。

  1. 共有データを表示するラベル
  2. 共有データを変更するテキストフィールド
  3. 共有データを変更するボタン

画面とソースの紐付け(データを表示・変更するアプリ)

ソースコード上に紐付けします。

/// 共有データを表示するラベル
@IBOutlet weak var valueLabel: UILabel!
/// 共有データを変更するテキストフィールド
@IBOutlet weak var updateValueTextField: UITextField!
/// 変更ボタンを押下した際の処理
/// - Parameter sender: 変更ボタン
@IBAction func updateValueButton(_ sender: Any) {
    valueLabel.text = updateValueTextField.text

    let userDefaults = UserDefaults(suiteName: groupID)
    userDefaults?.set(valueLabel.text, forKey: "DataStore")
}

ソースコード(データを表示・変更するアプリ)

import UIKit

class AppGroupsViewController: UIViewController {

    let groupID = "(グループ名を記入してください)"
    /// 共有データを表示するラベル
    @IBOutlet weak var valueLabel: UILabel!
    /// 共有データを変更するテキストフィールド
    @IBOutlet weak var updateValueTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        let userDefaults = UserDefaults(suiteName: groupID)
        userDefaults?.register(defaults: ["DataStore": "default"])
        valueLabel.text = userDefaults?.object(forKey: "DataStore") as? String
    }

    /// 変更ボタンを押下した際の処理
    /// - Parameter sender: 変更ボタン
    @IBAction func updateValueButton(_ sender: Any) {
        valueLabel.text = updateValueTextField.text

        let userDefaults = UserDefaults(suiteName: groupID)
        userDefaults?.set(valueLabel.text, forKey: "DataStore")
    }
}

画面を作成(データを表示のみするアプリ)

Storyboard に、以下を載っけます。

  1. 共有データを表示するラベル

画面とソースの紐付け(データを表示のみするアプリ)

ソースコード上に紐付けします。

ソースコード(データを表示のみアプリ)

import UIKit

class AppGroupsViewController: UIViewController {

    let userDefaults = UserDefaults.standard
    let groupId = "(グループ名を記入してください)"

    /// 共有データを表示するラベル
    @IBOutlet weak var valueLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        let userDefaults = UserDefaults(suiteName: groupId)
        userDefaults?.register(defaults: ["DataStore": "default"])
        valueLabel.text = userDefaults?.object(forKey: "DataStore") as? String
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[swift5]iOSアプリでカメラ機能を実装

iPhoneに入っているカメラアプリのようなカメラを呼び出す機能を実装する方法を紹介します!
本ページでは、UIImagePickerController を利用します。

自分でカスタマイズしたカメラを実装したい場合は、UIImagePickerController ではなく AVFoundation を利用する必要があります。

動作環境

対象 バージョン
iOS 13.3
macOS Catalina 10.15.2
Xcode 11.3.1
Swift 5.1.3

カメラ機能の実装

本ページでは、2画面用意します。

  1. ボタンを用意します
  2. ボタンを押下すると、カメラ画面に遷移します
  3. 写真をとり、写真を利用を押下すると、写真が保存されはじめの画面に戻ります

Info.plistの修正

Info.plist に2つ項目を追加します。

  • Privacy - Camera Usage Description
    • カメラを呼び出すために追加する
  • Privacy - Photo Library Addtions Usage Description
    • 写真を保存する写真アプリを利用するために追加する

ファイル作成

  • Storyboard
    • ここでは、"Camera.storyboard" とします
  • ViewController.swift
    • ここでは、"CameraViewController.swift" とします

画面を作成

Storyboard に、カメラを呼び出すためのボタンを載っけます。

  • カメラ画面は実装する必要がないため、自分自身ではカメラ機能を呼び出す1画面のみ作成します

画面とソースの紐付け

カメラを呼び出すためのボタンをソースコード上に紐付けします。

/// UIImagePickerController カメラを起動する
// - Parameter sender: "UIImagePickerController" ボタン
@IBAction func startUiImagePickerController(_ sender: Any) {
}

カメラを呼び出す

はじめに、Delegate を実装します。

class CameraTopViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
}

次に、ボタン押下時に呼び出されるメソッド内に以下を実装します。

/// UIImagePickerController カメラを起動する
/// - Parameter sender: "UIImagePickerController"ボタン
@IBAction func startUiImagePickerController(_ sender: Any) {
    let picker = UIImagePickerController()
    picker.sourceType = .camera
    picker.delegate = self
    // UIImagePickerController カメラを起動する
    present(picker, animated: true, completion: nil)
}

最後に、カメラを呼び出した後の処理を実装します。

/// シャッターボタンを押下した際、確認メニューに切り替わる
/// - Parameters:
///   - picker: ピッカー
///   - info: 写真情報
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    let image = info[.originalImage] as! UIImage
    // "写真を使用"を押下した際、写真アプリに保存する
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
    // UIImagePickerController カメラが閉じる
    self.dismiss(animated: true, completion: nil)
}

ソースコード

import UIKit

class CameraViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { {

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

    /// UIImagePickerController カメラを起動する
    /// - Parameter sender: "UIImagePickerController"ボタン
    @IBAction func startUiImagePickerController(_ sender: Any) {
        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = self
        // UIImagePickerController カメラを起動する
        present(picker, animated: true, completion: nil)
    }

    /// シャッターボタンを押下した際、確認メニューに切り替わる
    /// - Parameters:
    ///   - picker: ピッカー
    ///   - info: 写真情報
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        let image = info[.originalImage] as! UIImage
        // "写真を使用"を押下した際、写真アプリに保存する
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        // UIImagePickerController カメラが閉じる
        self.dismiss(animated: true, completion: nil)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【swift】画像のデータサイズを取得し、バリデーション を実装する

方法

var image:UIImage? = ... //画像を読み込む

//
let imageData:Int = NSData(data: image.jpegData(compressionQuality: 1)!).count

//※画像のデータサイズをKBで表示。
let dataToKB = Double(imageData) / 1000.0

//バリデーションを実装。(10MB以下のみ保存可能)
if dataToKB < 10000.0 {

      print("画像を保存できます")

      //画像をimageViewに描写させたりする
      self.imageView.image = image
} else {

      print("画像データが過大です。10MB以下可能です")
}

詳しく

jpegData(compressionQuality: CGFloat)

func jpegData(compressionQuality: CGFloat) -> Data?

指定された画像を含むデータオブジェクトをJPEG形式で返します。

compressionQuality: CGFloatとは?
0.0〜1.0の値として表される、結果のJPEG画像の品質。値0.0は最大圧縮(または最低品質)を表し、値1.0は最小圧縮(または最高品質)を表します。

今回は、最高品質(オリジナルの画質)でデータサイズを確認したいため、compressionQuality: 1にします。

countする

NSData(data: image.jpegData(compressionQuality: 1)!).count

NSData(data: Data)で、
別のデータオブジェクトの内容でデータオブジェクトを初期化します。

画像をNSData型に変換することで、countメソッドを使うことができます。
countすることで、byteの数をカウントすることができます。

参考文献

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

プロキシーパターンをSwift5で実装する

※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。

The Proxy(プロクシ)

0. プロクシの意義

ある特定のオブジェクトに直接アクセスさせず、間接的にアクセスするようにするパターンをプロクシパターンと言う(Proxyは代理というような意味)。

具体的には、
・ バーチャルプロクシ
・ リモートプロクシ
・ プロテクティブプロクシ
の三種がある。

注意点は、プロクシを経由せずに直接目的のオブジェクトにアクセスできるような抜け道を用意してはならないという事である。それではプロクシパターンの意味がなくなってしまう。

1. Virtual Proxy(バーチャルプロクシ)

オブジェクト生成にコストがかかる場合、その生成のタイミングを本当にオブジェクトが必要になるまで遅らせるパターンの事を言う。

Swiftでは、変数の前にlazy修飾詞をつける事で比較的簡単に実現できる。

ImageProxy.swift
public protocol RemoteImage: CustomStringConvertible {
    init(url: URL)
    var image: UIImage? {get}
    var url: URL {get}
    var hasContent: Bool {get}
}

extension RemoteImage {
    public var description: String {
        let description = self.hasContent ? "Image available. Retrieved from \(self.url.absoluteString)" : "No image available yet!"
        return description
    }
}

public class ImageProxy: RemoteImage {
    public required init(url: URL) {
        self.url = url
    }

    //lazy修飾詞をつける
    public lazy var image: UIImage? = { [unowned self] in
        var result: UIImage?
        if let img = try? UIImage(data:
            Data(contentsOf: self.url)
            ) {
            result = img
            self.hasContent = true
        }

        return result
    }()

    public let url: URL
    public var hasContent: Bool = false
}

プロクシを実際に利用してみると以下のようになる。

VirtualImageProxy.playground
guard let imageURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

let imageProxy = ImageProxy(url: imageURL)

print(imageProxy)// No image available yet!

let image = imageProxy.image
print(imageProxy)//Image available. Retrieved from https://developer.apple.com/swift/images/swift-og.png

プロクシを生成した段階ではimageプロパティは実際に生成されておらず、実際にimageプロパティにアクセスした時初めて生成されている事がわかる。

この例のように、ネットワークを介してデータをダウンロードして画像を生成すると言う重い処理がある場面では、最初にオブジェクトを生成するのでなく実際にアクセスした段階で生成する事でリソースの節約につながる。

2. Remote Proxy(リモートプロクシ)

リモートプロクシは、ネットワーク接続などコストのかかる処理を実際に必要になるタイミングまで延期するプロクシパターンを言う。

具体的には、ネットワーク接続に必要な情報(URL,クロージャなど)を渡す処理と、実際にネットワーク接続を行う処理を分離し、後者を前者とは別のタイミングで行えるようにする。

RemoteDataProxy.swift
import Foundation

public protocol RemoteData {
    func data(url: URL, completionHandler: @escaping(Error?, Data?) -> Void) -> RemoteData

    func run()
}

public class RemoteDataProxy: RemoteData {
    fileprivate var callback: ((Error?, Data?) -> Void)?
    fileprivate var url: URL?
    public init() {}

    //URL,コンプリーションハンドラを渡す処理
    public func data(url: URL, completionHandler: @escaping(Error?, Data?) -> Void) -> RemoteData {
        self.url = url
        self.callback = completionHandler
        return self
    }

    //実際にネットワーク接続を行う処理
    public func run() {
        if let callback = self.callback,
            let url = self.url {
            URLSession.shared.dataTask(with: url) {(data, response, error) in
                guard let data = data, error == nil else {
                    print("Could not download data from URL \(url.absoluteString). Reason: \(error!.localizedDescription)")
                    callback(error, nil)
                    return
                }

                print("Data successfully fetched from URL \(url.absoluteString)")
                callback(nil, data)
            }.resume()

            print("Downloading data from URL \(url.absoluteString)")
        } else {
            print("run() called before invoking data(url: completionHandler:)")
        }
    }
}

そして下記のようにdata(func:completionHandler:)メソッドをrun()メソッドを別のタイミングで呼ぶことで、コストのかかるネットワーク接続処理を自由なタイミングまで延期できる。もし実際のネットワーク接続処理が必要なくなったら行わなくて済むことになるため、やはりリソースの削減につながる。

RemoteProxy.playground
import Foundation
import PlaygroundSupport

guard let dataURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

//URL、クロージャなど接続処理に必要な情報を渡す。この時点では接続は行われない
let dataProxy = RemoteDataProxy().data(url: dataURL) {(error, data) in
    guard error == nil else {
        print("Could not retrieve data from URL \(dataURL.absoluteString)")
        return
    }

    print("\(data?.count ?? 0) bytes retrieved from URL \(dataURL.absoluteString)")
}

//Playgroundで非同期処理を許可する
PlaygroundPage.current.needsIndefiniteExecution = true

//延期されたネットワーク接続処理
dataProxy.run()

3. Protective Proxy(プロテクティブプロクシ)

個人情報などセンシティブな情報にアクセスさせる際、権限が無い者に見られては困るため、必ず認証を経てから行いたい場合がある。

このように目的のオブジェクトへのアクセスを制限し、認証を経てからでないとできないようにするパターンをプロテクティブプロクシという。

先に実装したImageProxyクラスを利用する形で、さらに認証機能を追加したSecureImageProxyクラスを実装する。

認証機能は、新しく作成したAuthenticatorクラスを利用する。

Authenticator.swift
public protocol Authenticating {
    var isAuthenticated: Bool {get}
    func authenticate(user: String) -> Bool
}

public class Authenticator: Authenticating {
    static public let shared = Authenticator()

    //認証が行われたか否かを表すBool型変数
    public var isAuthenticated: Bool = false

    //接続が許可されているユーザー名の一覧
    fileprivate let userWhiteList = ["John", "Mary", "Steve"]

    fileprivate let syncQueue = DispatchQueue(label: "com.leakka.authQueue")

    fileprivate init() {}

    //許可されたユーザーか否かを確認するメソッド
    public func authenticate(user: String) -> Bool {
        var result = false
        self.syncQueue.sync {
            result = self.userWhiteList.contains(user) ? true : false

            if result {
                print("Authorized!")
                self.isAuthenticated = true
            } else {
                print("Error: Unauthorized!")
                self.isAuthenticated = false
            }
        }
        return result
    }
}

さらにプロキシクラスは以下のようになる。

ImageProxy.swift
//private修飾詞に変更
private class ImageProxy: RemoteImage {
//中略
}

ImageProxyクラスの公開範囲をprivateに変更している。

これは、後述のSecureImageProxyでなく直接ImageProxyを使用し、認証を回避するというような事態を避けるためである。

ImageProxy.swift
//認証用のプロクシクラスを追加
public class SecureImageProxy: RemoteImage {

    //認証が完了していれば画像を返す
    public var image: UIImage? {
        get {
            return Authenticator.shared.isAuthenticated ? self.imageProxy.image : nil
        }
    }

    public let url: URL
    public var hasContent: Bool = false

    //ImageProxyクラスをprivateで保持
    fileprivate lazy var imageProxy: ImageProxy = ImageProxy(url: self.url)

    public required init(url: URL) {
        self.url = url
    }
}

実際に使用してみると、以下のようになる。

VirtualImageProxy.playground
import Foundation
import PlaygroundSupport

guard let imageURL = URL(string: "https://developer.apple.com/swift/images/swift-og.png") else {
    fatalError("Could not create URL")
}

let secureImageProxy = SecureImageProxy(url: imageURL)

print(secureImageProxy)// No image available yet!

Authenticator.shared.authenticate(user: "Jim")//Error: Unauthorized!
if secureImageProxy.image != nil {
    print("Proxy has a valid image.")
}

Authenticator.shared.authenticate(user: "John")//Authorized!
if secureImageProxy.image != nil {
    print("Proxy has a valid image.")//Proxy has a valid image.
}

PlaygroundPage.current.needsIndefiniteExecution = true

誤ったユーザー名では認証に失敗し画像にアクセスできない。

正しいユーザー名で認証に成功した後のみ、画像にアクセスできていることがわかる。

https://github.com/Satoru-PriChan/ProxyDemo

参考文献: https://www.amazon.com/Design-Patterns-Swift-implement-Improve-ebook/dp/B07MDD3FQJ

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