- 投稿日: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:55:41+09:00
画面遷移時のNavigationBar/TabBarの表示・非表示操作
はじめに、
NavigationController, TabBar、よく使いますね。基本的に、それなりにコンテンツがあるアプリだとこの2つは組み込まれているんじゃないでしょうか。
画面によってはナビバーを非表示にしたり、タブバーを非表示にしたり状況に応じて変えなければいけない場合もあると思います。
非表示設定といえば、いつの世もisHidden
系ですが、なるほどぉとなった部分があるので備忘録。こんな場合あるよね
複数のVCから特定のVCに遷移するケース。
2階層以降なのでタブバー が非表示になるのはわかりますが、デザインのあれこれでナビバーがない画面を実装する時、隠す実装をする。そして、元の画面に戻った時用に再表示する。表示/非表示系メモ
このケースを実装する時の発想として以下のものがある。
Q : SharedVCが表示されたら隠して、閉じたら表示する
A : まとめて設定すると、いろんな画面に対応できないQ : 遷移元のVCをクラス判定して表示の分岐をしてみては
A : かなり限定的ならいいが、所々でシェアされてるのでより抽象化共通処理を狙いたいという部分で、以下の結論に至った。
コード
// 遷移元VCがナビバー・タブバーを持っているかの状態を保持するインスタンス private var hasPreviousVCNaviBar = true private var hasPreviousVCTabBar = true override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 遷移元の状態を設定 hasPreviousVCNaviBar = navigationController?.isNavigationBarHidden == true ? false : true hasPreviousVCTabBar = tabBarController?.tabBar.isHidden == true ? false : true // SharedVC自体の表示処理をする navigationController?.isNavigationBarHidden == true tabBarController?.tabBar.isHidden = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // VCが消滅するときに、保存した遷移元の状態に合わせて設定し直す navigationController?.isNavigationBarHidden = !hasPreviousVCNaviBar tabBarController?.tabBar.isHidden = !hasPreviousVCTabBar }注意点
遷移元VCにアクセス?
注意したいのは、 遷移元のVCにここでアクセスしてナビタブの状態を確認しようとする行為がよろしくないということ。
画面ごとにナビタブの非表示設定をしているわけではなく、ナビタブの配下にviewControllers
があるのでlet prevVC = presentingViewController prevVC.navigationControllerとしても、上で設定変えちゃってますよということ。階層をイメージしていればわかる話何ですが。
hidesBottomBarWhenPushedでタブバー隠し
hidesBottomBarWhenPushed
もタブバーの非表示処理ですが、これらライフサイクルメソッドに突入してからの実装だとそのVCに反映されません。
なので、これを実装する場合はsharedVC.hidesBottomBarWhenPushed = true show(sharedVC, sender: nil)という風に遷移前に設定する必要があります。
これでもいいですが、遷移メソッド実装するごとに設定をするのが面倒です。
遷移メソッド自体をカスタムしたりもできると思いますが、大元で設定する方が楽です。NavigationBar の hide処理多すぎ問題
navigationController?.navigationBar.isHidden = true navigationController?.isNavigationBarHidden == true navigationController?.setNavigationBarHidden(true, animated: true)なんか3つほどあります。
.setNavigationBarHidden
に関してはアニメーションを付与したいかどうかの違いです。
.navigationBar.isHidden
と.isNavigationBarHidden
の厳密な違いはわからないのですが、.isHidden
は上記の条件だと機能しませんでした。(エラーはなく補完もあるのに)ナビバーを非表示処理する際は
.setNavigationBarHidden
良さそうですね。まとめ
該当するもの(型とか)で分岐という方法もあるが、コンテンツの肥大化等を考慮したときに、出来るだけ抽象化しておくのは大きいメリットだなと感じました。
あと.isHidden
の違いが気になる。
- 投稿日: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-22T13:34:28+09:00
Playgroundで動作するミニマムなUITableViewControllerのサンプルコード
import UIKit import PlaygroundSupport class MyTableViewController: UITableViewController { private lazy var items: [String] = { return [ "The Swift Programming Language", "The Swift Programming Language", "The Swift Programming Language" ] }() override func viewDidLoad() { super.viewDidLoad() // セルの区切り線を消すハック self.tableView.tableFooterView = UIView() } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) cell.detailTextLabel?.text = "ここはsubtitle : 1行目テキスト\n2行目テキスト" cell.detailTextLabel?.numberOfLines = 2 cell.detailTextLabel?.lineBreakMode = .byWordWrapping let item = items[indexPath.row] cell.textLabel?.text = item cell.textLabel?.lineBreakMode = .byWordWrapping cell.textLabel?.numberOfLines = 0 return cell } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.items.count } } PlaygroundPage.current.liveView = MyTableViewController()
- 投稿日:2020-01-22T11:26:08+09:00
Playgroundで動作するミニマムなUIViewControllerのサンプルコード
import UIKit import PlaygroundSupport class MyViewController : UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white let label = UILabel() label.text = "Hello World!" label.sizeToFit() label.center = self.view.center self.view.addSubview(label) } } PlaygroundPage.current.liveView = MyViewController()
- 投稿日:2020-01-22T10:58:04+09:00
SwiftUI Tutorial : Creating and Combining Viewsやってみた
SwiftUIの勉強でTutorialをやっていってるメモ書きです。
ちなみに今回は「Creating and Combining Views」をやっていきます。リポジトリはこちら「toshihirooya/SwiftUI-Tutorial」
大体時間にして40minぐらいのチュートリアルみたいです。
あと、チュートリアルの検証環境は下記のような感じ。
環境 バージョン mac os 10.15.2 Xcode 11.3 Section 1. Create a New Project and Explore the Canvas
- 検証用のプロジェクトを用意する。
- Project名「Landmarks」
- User Interfaceはもちろん「SwiftUI」を選択する。
- Xcodeの
Resume
ボタンタップするとプレビューが表示されます。- デフォルトのプロジェクトだとSwiftUIでは下記のような二つのStructで構成されていて、一つ目の構造体がViewを定義するもので、二つ目の構造体がプレビューを構成しています。
ContentView.swiftimport SwiftUI struct ContentView: View { var body: some View { Text("Hello, World!") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }Section 2. Customize the Text View
- 続いてTextViewをカスタマイズしていきます。
- まずはコードを下記のように修正して
- Textの文字列を「Tutorial Rock」にする。
- fontを「.title」に設定
- テキストの色を「.green」に設定
- TextのアトリビュートをメソッドチェーンでつなげてBuilder形式っぽく設定していく感じ
ContentView.swiftstruct ContentView: View { var body: some View { Text("Tutorial Rock") .font(.title) .foregroundColor(.green) } }
- あと、PreviewからもViewの修正ができるようです。
PreviewでViewを ⌘+Click
でメニュー表示Show SwiftUI Inspector...
選択で設定を変更できる
- あと、コードを
⌘ + クリック
してもViewのアトリビュートを設定できる
Textを ⌘ + Click
するShow SwiftUI Inspector...で設定できる
- 最後にコードを修正すると自動でPreviewにも反映されます。
Section 3. Combine Views Using Stacks
- 続いてStackを駆使してViewを並べていきます。
- HStackにembeddedされているTextの間にSpacerを設定するとTextが画面の端にくるぐらいのSpaceが入るみたい。
ContentView.swiftstruct ContentView: View { var body: some View { VStack(alignment: .leading) { Text("Tutorial Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } } .padding() } }Section 4. Create a Custom Image View
- 続いてカスタムViewを作っていきます。作るCustomViewはImageを円形に切り抜くViewです。
- まずはSwiftUIの新しいファイルを追加します。
- ファイルが追加できたらImageを画面中央に追加します。
CircleImage.swiftstruct CircleImage: View { var body: some View { Image("safari") } } struct CircleImage_Previews: PreviewProvider { static var previews: some View { CircleImage() } }
- 続いてCircleImageを作っていきます。
Imageを円形にした Imageの淵を灰色にした 影をつけた Imageの淵を白色にした CircleImage.swiftstruct CircleImage: View { var body: some View { Image("safari") .clipShape(Circle()) // Imageを円形に切り出す .overlay(Circle().stroke(Color.white, lineWidth: 4)) // 切り出したImageの淵に線を描く .shadow(radius: 10) // 影をつける } }Section 5. Use UIKit and SwiftUI Views Together
- 続いてMapKitを使ってMapを表示するviewの作成をやっていきます
MapView.swiftimport SwiftUI import MapKit struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ view: MKMapView, context: Context) { let coordinate = CLLocationCoordinate2D( latitude: 34.011286, longitude: -116.166868) let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0) let region = MKCoordinateRegion(center: coordinate, span: span) view.setRegion(region, animated: true) } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView() } }
- Previewの再生ボタンを押すと地図が表示される。
Section 6. Compose the Detail View
- あとは作ったViewを組み合わせていきます。
ContentView.swiftimport SwiftUI struct ContentView: View { var body: some View { VStack { MapView() .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage() .offset(y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text("Tutorial Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } } .padding() Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- 最後に完成形がこちら
- 投稿日: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:27:25+09:00
【続編】SwiftUI 導入で LLVM のドキュメント読む羽目になった話
はじめに
こちらの記事は 前編 の続編となります。事前にこちらの記事もご覧ください。
今回もどこかで発表したわけでもないでも関わらずスライドを多用して説明していきますので、最後まで見ていただけると嬉しいです!前編の振り返り
前回はこのような考察でまとめていました。
詳細は「SwiftUI 導入で LLVM のドキュメント読む羽目になった話」を御覧ください。前編の訂正
まずは訂正をさせてください。エラー文の解釈を間違えておりました。
実はここ、かなり重要なことを教えてくれています。カバレッジを取得するためのランタイムライブラリが呼び出せなかったと言ってくれていました。Linker flags に
-fprofile-instr-generate
を追加すると動く理由早速ではありますが、前回からハッキリしなかった理由について解説していきます。
説明の都合上、理想の挙動から説明していくことにします。
Swift コンパイラである swiftc に対して-fprofile-instr-generate
を指定してアプリのソースコードをビルドすることによって、カバレッジ情報を取得するための処理がアプリのコードに挿入されます。この「カバレッジ情報を取得する処理」というのは、コードカバレッジランタイムライブラリ内の関数呼び出しがメインです。
先程の図では点線の矢印で表現していましたが、これには理由があります。 swiftc によるコンパイル時には 呼び出し処理 が追加されるだけで実際の処理が含まれないからです。
なので、 Linker に対して「コードカバレッジランタイムライブラリのリンク」を命じなければならないのです。したがって、 Linker に対して渡される-fprofile-instr-generate
はコードカバレッジランタイムライブラリとリンクさせるためのものだったのです。
先程、 Linker に対して-fprofile-instr-generate
を指定すると説明しましたが、本来これは Xcode が自動で行っているはずです。その証拠に、 XcodePreviews 以外ではカバレッジを有効にした状態でも正常にビルドができます。
結論ですが、 Xcode のバグ(と思われる)で XcodePreviews のためのビルドのときのみ、コードカバレッジの設定を反映する処理(コードカバレッジが有効であれば Linker に対して-fprofile-instr-generate
を渡す処理)が欠如しているのだと思います※。
なので、 Linker flags に対して明示的に-fprofile-instr-generate
を指定してあげなければならなかったのです。※ Static Framework が含まれる場合に発生したように、ここに記載していないものも含む一定の条件で発生するものだと思われます。
学び
今回、この結論にいたるまでに多くの方からアドバイスなどをいただきました。
本当にありがとうございます!!さいごに
学びとして書いてますが、そもそもカバレッジは日常的に有効にしておくべきではありません。
そんなミスをしていたからこそ遭遇してしまった問題だったわけですが、すごく勉強になりました!この記事も「この問題の解決策!」ではないですが、知識として多くのエンジニアに届けばと思います。最後まで読んでいただき、ありがとうございます!
- 投稿日: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