- 投稿日:2020-01-22T23:40:17+09:00
UISearchBarのキャンセルボタンをアニメーション付きで表示したい話
UISearchBarをタップして編集状態中だけキャンセルボタンを表示したいと、
ちょびっと悩んだので備忘録として書いときます。アニメーション無しの場合
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) } }
- 投稿日:2020-01-22T21:53:28+09:00
UITextFieldで文字を入力した後にキーボードをしまう方法
今回の記事を書く理由
初学者に多い気がしますが、UITextFieldで文字を入力した後、キーボードが勝手に閉じてくれないのでそのためのコードを書く必要があります。
意外と何回も調べていたりしていたためメモの代わりに書きます。
今回は説明を交えながら書いてきます。
xcodeでreturnの文字を変更
まずxcodeでreturnの文字を変更することができます。
ソースを追加
全体像
この後説明していきます。
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でキーボードの動きが怪しかったら参考にしてみてください。
参考
- 投稿日:2020-01-22T21:16:40+09:00
FirebaseのRemoteConfig使ってみた
はじめに
最近になってFirebaseのRemoteConfigを使う機会があったので、
このサイトに残しておこうかと思います。
ほぼ、ドキュメント通りなので、
気になる方だけ読んでいただければと思います。RemoteConfigとは
アプリをアップデートすることなく、アプリの動作や外観を変更できる機能になります。
行なっている事としてはRemote Config Serverから設定されているパラメーターを取得して、
その値によって動作や外観を変更します。
RemoteConfigとは環境
Xcode 11.3
swift 5
Firebase 6.15.0FireBaseのインストール
インストール手順は省きます。
以下、公式ドキュメント
Firebase を iOS プロジェクトに追加するRemoteConfig設定
remoteConfig = RemoteConfig.remoteConfig() let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 remoteConfig.configSettings = settingsドキュメントにあるようにシングルトンオブジェクトを作成し、通信を抑えるために同期する間隔を設定します。。。。が、早速ハマりました、、、、
settings.minimumFetchInterval = 0フェッチ間隔を上記のように設定しても反映されいっぽい、、、(なぜ?)
一応、設定の優先度は以下のようです。
- fetch(long) のパラメータ
- FIRRemoteConfigSettings.MinimumFetchInterval のパラメータ
- デフォルト値(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形式で設定。値のフェッチと有効化
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テストも実施できるので是非やってみましょう!!
- 投稿日:2020-01-22T18:20:36+09:00
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が何かは知らない。
- 投稿日:2020-01-22T17:36:36+09:00
houstonを使ってrailsサーバーからプッシュ通知を送る時に設定できる値のメモ
1. 概要
"houston"というgemを使って、ruby on railsのサーバーからiOSアプリにプッシュ通知を送るようにしました。
今回は本文・SE・バッジ数だけの一番シンプルな通知を実装しましたが、GitHubのUsageを参照すると、通知データに対して他にも設定できる項目があるようです
将来細かい設定が必要になった時に忘れないように、調べたことを書いておきます。
2. メモ
Usage.rbrequire '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)
- 投稿日:2020-01-22T17:36:36+09:00
houstonを使ってrailsサーバーからプッシュ通知を送る時に設定できる値のメモ、あと多言語対応
1. 概要
"houston"というgemを使って、ruby on railsのサーバーからiOSアプリにプッシュ通知を送るようにしました。
今回は本文・SE・バッジ数だけの一番シンプルな通知を実装しましたが、GitHubのUsageを参照すると、通知データに対して他にも設定できる項目があるようです
将来細かい設定が必要になった時に忘れないように、調べたことを書いておきます。
2. メモ
Usage.rbrequire '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.rbnotification.alert = { # Localizable.stringsに記述したkeyを入れる "loc-key" : "GAME_PLAY_REQUEST_FORMAT", # 変数を設定した場合は配列で指定する "loc-args" : [ "Jenna", "Frank"] }
- 投稿日:2020-01-22T17:15:20+09:00
【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 inDispatchQueue.main.async {[weak self], word inDispatchQueue.main.async {[weak self](word) inDispatchQueue.main.async {[weak self](w: String = word) inDispatchQueue.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まさかそのまま書けば使えるなんて思わず、色々な書き方を試してしまいました。。。?
- 投稿日:2020-01-22T10:45:56+09:00
[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 で同じデータを表示します。
アプリの設定(2つのアプリ共通)
2つのアプリで設定してください。
- 「Signing & Capabilities」から「App Groups」を追加する
- グループ名を追加する
ファイル作成(2つのアプリ共通)
- Storyboard
- ここでは、"AppGroups.storyboard" とします
- ViewController.swift
- ここでは、"AppGroupsViewController.swift" とします
画面を作成(データを表示・変更するアプリ)
Storyboard に、以下を載っけます。
- 共有データを表示するラベル
- 共有データを変更するテキストフィールド
- 共有データを変更するボタン
画面とソースの紐付け(データを表示・変更するアプリ)
ソースコード上に紐付けします。
/// 共有データを表示するラベル @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 に、以下を載っけます。
- 共有データを表示するラベル
画面とソースの紐付け(データを表示のみするアプリ)
ソースコード上に紐付けします。
ソースコード(データを表示のみアプリ)
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 } }
- 投稿日:2020-01-22T09:09:26+09:00
[swift5]iOSアプリでカメラ機能を実装
iPhoneに入っているカメラアプリのようなカメラを呼び出す機能を実装する方法を紹介します!
本ページでは、UIImagePickerController を利用します。自分でカスタマイズしたカメラを実装したい場合は、UIImagePickerController ではなく AVFoundation を利用する必要があります。
動作環境
対象 バージョン iOS 13.3 macOS Catalina 10.15.2 Xcode 11.3.1 Swift 5.1.3 カメラ機能の実装
本ページでは、2画面用意します。
- ボタンを用意します
- ボタンを押下すると、カメラ画面に遷移します
- 写真をとり、写真を利用を押下すると、写真が保存されはじめの画面に戻ります
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) } }
- 投稿日:2020-01-22T02:08:54+09:00
【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の数をカウントすることができます。参考文献
- 投稿日:2020-01-22T00:13:49+09:00
プロキシーパターンをSwift5で実装する
※この記事は「全デザインパターンをSwift5で実装する」https://qiita.com/satoru_pripara/items/3aa80dab8e80052796c6 の一部です。
The Proxy(プロクシ)
0. プロクシの意義
ある特定のオブジェクトに直接アクセスさせず、間接的にアクセスするようにするパターンをプロクシパターンと言う(Proxyは代理というような意味)。
具体的には、
・ バーチャルプロクシ
・ リモートプロクシ
・ プロテクティブプロクシ
の三種がある。注意点は、プロクシを経由せずに直接目的のオブジェクトにアクセスできるような抜け道を用意してはならないという事である。それではプロクシパターンの意味がなくなってしまう。
1. Virtual Proxy(バーチャルプロクシ)
オブジェクト生成にコストがかかる場合、その生成のタイミングを本当にオブジェクトが必要になるまで遅らせるパターンの事を言う。
Swiftでは、変数の前に
lazy
修飾詞をつける事で比較的簡単に実現できる。ImageProxy.swiftpublic 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.playgroundguard 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.swiftimport 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.playgroundimport 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.swiftpublic 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.playgroundimport 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