20210412のSwiftに関する記事は15件です。

App Storeリリース時に提出するスクショを作成する時に役立つサイト集

はじめに いつも、検索かけるのでQiitaにまとめます。完全自分用です。 端末の画像の入手 Marketing Resources and Identity Guidelines | Apple Developer 提出に必要なスクリーンショットの枚数とサイズの確認 スクリーンショットの仕様 | App Store Connect ヘルプ Simulatorのスクリーンショット方法 // ターミナルにて xcrun simctl io booted screenshot --mask black <画像名>.<拡張子> スクリーンショット後の端末の角やノッチ部分の加工を行う bannerkoubou 画像の背景を削除する 画像の背景を削除 - remove.bg JPGに変換したり、PNGに変換したりなど iloveIMG | 画像編集用オンラインツール おわりに 良かったら参考にして下さい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UserDefaultsをもう少しちゃんと理解する

「UserDefaults消したーーーーい」と思って調べるとこのようなコードと出会った。 let appDomain = Bundle.main.bundleIdentifier UserDefaults.standard.removePersistentDomain(forName: appDomain!) なんとなくBundle Identifierを取ってきて指定すると消せるんだなというのはわかるが、UserDefaultsを消すときにBundle Identifierが必要なのかとか色々考えてしまったのでUserDefaultsについて調べてみました。 バージョン Xcode: v11.3 Swift: v.5.3.2 iOS: v14.4 対象読者 iOSアプリ開発初心者 UserDefaultsをなんとなく使っている人 内容 UserDefaultsとは UserDefaultsはユーザーごとの設定など保存するために提供されたKey-Valueストアです。また、UserDefaultsに保存された値は、削除しない限り永続的に保存され続けます。 ご存知ではあると思いますが、ユーザー個人のデータを保存する簡易的なデータベースみたいな感じですね。 Bundle IdentifierとUserDefaultsの関係は? 本題です。 結論から言うと、UserDefaultsを介して保存されるアプリ毎の設定を格納するファイル名にBundle Identifierが使われるという関係です。つまり、UserDefaultsはデータの保存領域で、Bundle Identifierはその領域の識別子として使われるという関係です。 つまりどういう事? ここからはコードを使って説明します。 UserDefaultsは初めて書き込みが行われた時にアプリのRoot/Library/Preferencesに Bundle Identifier名.plistというファイルをデバイス内に生成します。 まず、アプリのRoot/Library/PreferencesにBundle Identifier名.plistが生成されていないことを確認しましょう。 Library/PreferencesまでのパスはFileManagerを使うことで取得できます。 ViewController.swift class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() getFileNamesFromPreferences() // 何も出力されない } func getFileNamesFromPreferences() { // Libraryまでのファイルパスを取得 let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] // filePathにPreferencesを追加 let preferences = filePath.appendingPathComponent("Preferences") // Library/Preferences内のファイルのパスを取得 guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else { return } // Library/Preferences内のファイル名を出力 fileNames.compactMap { fileName in print(fileName.lastPathComponent) } } } Bundle Identifier名.plistを生成してみる UserDefaultsに値を保存することでBundle Identifier名.plistが生成されるか実際に確認してみましょう。 ViewController.swift import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() getFileNamesFromPreferences() // 何も出力されない + UserDefaults.standard.set("Value1", forKey: "test") // UserDefaultsに値を保存 + getFileNamesFromPreferences() // com.test.UserDefaults.plist } func getFileNamesFromPreferences() { // Libraryまでのファイルパスを取得 let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] // filePathにPreferencesを追加 let preferences = filePath.appendingPathComponent("Preferences") // Library/Preferences内のファイルのパスを取得 guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else { return } // Library/Preferences内のファイル名を出力 fileNames.compactMap { fileName in print(fileName.lastPathComponent) } } } お手元で実行してみるとプロジェクトのBundle Identifier名.plistが保存されていることがわかると思います。 ちなみにpreferencesの値を使ってターミナルからも確認できます。 removePersistentDomain(forName:) UserDefaultsに書き込みを行った際に生成される.plistファイルはBundle Identifier名でドメイン(簡単に言うと領域, 区画みたいなやつ)として認識されるみたいで、ドメイン名を指定することでドメイン内の値をすべて消すことができるみたいです。 ViewController.swift import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() getFileNamesFromPreferences() // 何も出力されない UserDefaults.standard.set("Value1", forKey: "test") // UserDefaultsに値を保存 getFileNamesFromPreferences() // com.test.UserDefaults.plist + UserDefaults.standard.removePersistentDomain(forName: "com.test.UserDefaults.plist") // ドメイン内の値を全て削除 + getFileNamesFromPreferences() // com.test.UserDefaults.plist } func getFileNamesFromPreferences() { // Libraryまでのファイルパスを取得 let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] // filePathにPreferencesを追加 let preferences = filePath.appendingPathComponent("Preferences") // Library/Preferences内のファイルのパスを取得 guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else { return } // Library/Preferences内のファイル名を出力 fileNames.compactMap { fileName in print(fileName.lastPathComponent) } } } 実行結果からわかるように、.plistファイル自体は削除されないみたいです。 終わりに 今回はUserDefaultsの内部構造に踏み込んでみました。 iOSの勉強は初めてからずっと心にあったモヤモヤが解消されてよかったです。 みなさんの参考になれば幸いです。 参考文献 swift - アプリ内で保存したファイルの一覧を取得したい - スタック・オーバーフロー Swift5でDocumentディレクトリのファイルにアクセスする - Qiita 【Swift】URLでパスを扱うときに便利なプロパティとメソッド - Qiita UserDefaults under the hood ??‍ | by Aaina jain | Swift India | Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARKit SceneKit かわいいアニメーション・レシピ・ブック

ジャンプ extension SCNNode { func jump(duration:Double){ let up = SCNAction.move(by: SCNVector3(0, 1, 0), duration: duration) let down = SCNAction.move(by: SCNVector3(0, -1, 0), duration: duration/2) self.runAction(SCNAction.repeatForever(SCNAction.sequence([up,down,SCNAction.wait(duration: duration)]))) } } いきおいあまったジャンプ 反動を小さなmoveとしてシーケンスで。 extension SCNNode { func popin(){ let popup = SCNAction.move(by: SCNVector3(0, 2.25, 0), duration: 0.6) let down = SCNAction.move(by: SCNVector3(0, -0.3, 0), duration: 0.2) let up = SCNAction.move(by: SCNVector3(0, 0.1, 0), duration: 0.2) let down1 = SCNAction.move(by: SCNVector3(0, -0.15, 0), duration: 0.2) down.timingMode = .easeOut up.timingMode = .easeIn down1.timingMode = .easeOut let pop = SCNAction.sequence([popup,down,up,down1]) self.runAction(pop) } } くるくる 永遠リピートで回し続ける。 durationを上げると速度が上がります。 extension SCNNode { func rotate(duration:Double) { let rotate = SCNAction.rotateBy(x: 0, y: 6.28, z: 0, duration: duration) self.runAction(SCNAction.repeatForever(rotate)) } } sceneを回転させる SCNSceneのrootNodeをSceneViewのrootNodeにaddChildNodeして、回転アニメーションをつけると、sceneが回転しているように見えます。 こういうシーンを回転させると ライトミスってますごめんなさい。 let worldNode = SCNScene(named: "art.scnassets/world.scn")!.rootNode sceneView.scene.rootNode.addChildNode(worldNode) worldNode.position = SCNVector3(0, -1.5, 0) worldNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 3))) しっぽふりふり 回転軸用のノードをかませると、geometry の中心以外でも回転させられます。 アクション回数を指定できます。 let dogTale = SCNNode() // 回転の中心となる、空のノード dogTale.addChildNode(actualTale) actualeTale.position = SCNVector3(0,0.3,0) // 実際の尻尾の位置をずらす let taleFurifuri = SCNAction.repeat(SCNAction.sequence([ SCNAction.rotateTo(x: 0.5, y: 0, z: 0.7, duration: 0.05),SCNAction.rotateTo(x: 0.5, y: 0, z: -0.7, duration: 0.05)]),count: 16) dogTale.runAction(taleFurifuri) オブジェクトの中心の回転と、回転軸をずらす場合を同時に表示するとこういうイメージです。 // 軸をずらしたやーつ let empty = SCNNode() empty.addChildNode(dog) dog.position = SCNVector3(0, 0.7, 0) empty.position = SCNVector3(0, 0, -2) // 中心で回るやーつ let centerDog = dog.clone() centerDog.position = SCNVector3(0, 0, -2) sceneView.scene.rootNode.addChildNode(empty) sceneView.scene.rootNode.addChildNode(centerDog) empty.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 0.5))) centerDog.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 0.5))) 変身 isHiddenを入れ替えると、変化してるっぽく見えます。 変身アニメーションの完了ハンドラーの中でさりげなく変化させると、ぽく見えませんか? ちなみに、回転と移動を同時に呼ぶこともできます。 fox.runAction(SCNAction.sequence([SCNAction.move(by: SCNVector3(x: 0, y: 1.5, z: 0), duration: 0.5),SCNAction.move(by: SCNVector3(x: 0, y: -1.5, z: 0), duration: 0.5)])) fox.runAction(SCNAction.rotateBy(x: 6.28319, y: 0, z: 0, duration: 1),completionHandler: { fox.isHidden = true girl.isHidden = false }) ランダム・ムービング 移動パラメーターをランダムな数値にすることで、バラバラに動きます。 for fish in fishes { let randomTime = Double.random(in: 3...8) let randomPosition = SCNVector3(Float.random(in: -2...2),Float.random(in: -2...2),Float.random(in: -5...0)) fish.runAction(SCNAction.move(to: randomPosition, duration: randomTime)) } ふわふわ(Bless) 元のサイズに戻したい時は、scale(by: )ではなくscale(to: )を使うと便利 omochi.runAction(SCNAction.repeatForever(SCNAction.sequence([SCNAction.scale(to: 1.1, duration: 1),SCNAction.scale(to: 1, duration: 0.5)]))) ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLを使ったアプリを作っています。 機械学習関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SceneKit かわいいアニメーション・レシピ・ブック

ジャンプ extension SCNNode { func jump(duration:Double){ let up = SCNAction.move(by: SCNVector3(0, 1, 0), duration: duration) let down = SCNAction.move(by: SCNVector3(0, -1, 0), duration: duration/2) self.runAction(SCNAction.repeatForever(SCNAction.sequence([up,down,SCNAction.wait(duration: duration)]))) } } いきおいあまったジャンプ 反動を小さなmoveとしてシーケンスで。 extension SCNNode { func popin(){ let popup = SCNAction.move(by: SCNVector3(0, 2.25, 0), duration: 0.6) let down = SCNAction.move(by: SCNVector3(0, -0.3, 0), duration: 0.2) let up = SCNAction.move(by: SCNVector3(0, 0.1, 0), duration: 0.2) let down1 = SCNAction.move(by: SCNVector3(0, -0.15, 0), duration: 0.2) down.timingMode = .easeOut up.timingMode = .easeIn down1.timingMode = .easeOut let pop = SCNAction.sequence([popup,down,up,down1]) self.runAction(pop) } } くるくる 永遠リピートで回し続ける。 durationを上げると速度が上がります。 extension SCNNode { func rotate(duration:Double) { let rotate = SCNAction.rotateBy(x: 0, y: 6.28, z: 0, duration: duration) self.runAction(SCNAction.repeatForever(rotate)) } } sceneを回転させる SCNSceneのrootNodeをSceneViewのrootNodeにaddChildNodeして、回転アニメーションをつけると、sceneが回転しているように見えます。 こういうシーンを回転させると ライトミスってますごめんなさい。 let worldNode = SCNScene(named: "art.scnassets/world.scn")!.rootNode sceneView.scene.rootNode.addChildNode(worldNode) worldNode.position = SCNVector3(0, -1.5, 0) worldNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 3))) しっぽふりふり 回転軸用のノードをかませると、geometry の中心以外でも回転させられます。 アクション回数を指定できます。 let dogTale = SCNNode() // 回転の中心となる、空のノード dogTale.addChildNode(actualTale) actualeTale.position = SCNVector3(0,0.3,0) // 実際の尻尾の位置をずらす let taleFurifuri = SCNAction.repeat(SCNAction.sequence([ SCNAction.rotateTo(x: 0.5, y: 0, z: 0.7, duration: 0.05),SCNAction.rotateTo(x: 0.5, y: 0, z: -0.7, duration: 0.05)]),count: 16) dogTale.runAction(taleFurifuri) オブジェクトの中心の回転と、回転軸をずらす場合を同時に表示するとこういうイメージです。 // 軸をずらしたやーつ let empty = SCNNode() empty.addChildNode(dog) dog.position = SCNVector3(0, 0.7, 0) empty.position = SCNVector3(0, 0, -2) // 中心で回るやーつ let centerDog = dog.clone() centerDog.position = SCNVector3(0, 0, -2) sceneView.scene.rootNode.addChildNode(empty) sceneView.scene.rootNode.addChildNode(centerDog) empty.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 0.5))) centerDog.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 0.5))) 変身 isHiddenを入れ替えると、変化してるっぽく見えます。 変身アニメーションの完了ハンドラーの中でさりげなく変化させると、ぽく見えませんか? ちなみに、回転と移動を同時に呼ぶこともできます。 fox.runAction(SCNAction.sequence([SCNAction.move(by: SCNVector3(x: 0, y: 1.5, z: 0), duration: 0.5),SCNAction.move(by: SCNVector3(x: 0, y: -1.5, z: 0), duration: 0.5)])) fox.runAction(SCNAction.rotateBy(x: 6.28319, y: 0, z: 0, duration: 1),completionHandler: { fox.isHidden = true girl.isHidden = false }) ランダム・ムービング 移動パラメーターをランダムな数値にすることで、バラバラに動きます。 for fish in fishes { let randomTime = Double.random(in: 3...8) let randomPosition = SCNVector3(Float.random(in: -2...2),Float.random(in: -2...2),Float.random(in: -5...0)) fish.runAction(SCNAction.move(to: randomPosition, duration: randomTime)) } ふわふわ(Bless) 元のサイズに戻したい時は、scale(by: )ではなくscale(to: )を使うと便利 omochi.runAction(SCNAction.repeatForever(SCNAction.sequence([SCNAction.scale(to: 1.1, duration: 1),SCNAction.scale(to: 1, duration: 0.5)]))) ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLを使ったアプリを作っています。 機械学習関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift クロージャとコールバック関数とは 分かりやすく解説

クロージャとコールバック関数について Firebaseのユーザー登録のソースコードを読んでいて思った事がある。 それがここ。 Auth.auth().createUser(withEmail: email, password: password) { (res, err) in if let err = err { print("Auth情報の保存に失敗しました。\(err)") return } まさにここで使われている構文の書き方がクロージャとコールバック関数を使っている。 つまり、この構文を理解するにはその2つを知る必要がある。 クロージャとは クロージャとは一言で言えば変数や処理を一つの{}で囲んだまとまり クロージャの基本構文 {(引数名1: 型, 引数名2: 型...) -> 戻り値の型 in クロージャの実行時に実行される文 必要に応じてreturn文で戻り値を返却する } クロージャを実際に使ったプログラム let double = { (x: Int) -> Int in x * 2 } double(2) //実行結果 4 クロージャは変数に入れることもできる。変数に入れられるのならそれを関数の引数にも渡すことも可能。 入れなくても直がきで渡せるけど。 こんな感じ import UIKit //乗算関数 let multi = {(x:Int,y:Int) -> (Int) in return x*y } //出力関数 func test(multifunc:(Int,Int) -> Int, b:Int, c:Int){ let result = multifunc(b,c) print(result) } test(multifunc: multi,b: 5,c: 3) //実行結果 15 コードを見ると test関数の引数としてmulti変数(中身はクロージャ)が渡されている。 このような感じで関数を渡す事ができる。 コールバック関数とは コールバック関数とは一言で言えば 関数の呼び出し先から、呼び出しもとの関数を呼び出すこと(ややこしい) これはコードを見ればすぐに理解できるはず。 import UIKit func test(x:Int,y:Int,callback:(String,Bool) ->()){ if(x+y > 10){ callback("二つの数字を足した結果は5より大きいです",true) }else{ callback("二つの数字を足した結果は5より小さいため処理を中断します。",false) return } print("最後まで処理が実行されました。") } test(x:4,y:6){(result,flag) in if(flag){ print(result) }else{ print(result) } return } これはコールバック関数で10未満の数字が入力されると処理が中断される。 実際この程度のプログラムならコールバック関数など描かなくてもいい、、、 じゃあどういった時に使うのかというと、ユーザーの認証情報を登録するときに パスワードは何桁以上と言う制約を儲けたときに、登録情報にエラーが発生したとして コールバック関数にエラー情報をのせて呼び出しもとに処理を移行させるとおいう使い方がある。 それがFirebaseで使われる。 createuser関数であり、エラーが発生すればその情報を取り扱う事ができるし、エラーではなく登録に成功すればresultと言う引数を受け取る事ができる。 以下がシンプル化したコールバック関数の認証情報登録のコード //登録ユーザー情報 let name = "Matchan" let password = "aiuio" test(name: name, password: password){(err,res) in if let err = err { print(err) return } //オプショナル型のnilチェック guard let result = res else { return } print(result) //登録が成功した後の処理を書く } //クロージャとコールバック関数を使った関数test func test(name: String, password: String, callback:(String?,String?) -> ()){ if(password.count<7){ callback("パスワードが6文字以下のためセキュリティ上登録できません",nil) return }else { callback(nil, "認証情報の登録が成功しました") } } 認証情報にエラーがあれば、 if let err = err { print(err) return } エラー出力されて処理が中断される、処理が成功ならその後の処理が継続して行われる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

業務でSwiftUIを使って画面構築してみた話

業務の中でSwiftUIを使って画面構築出来る機会がありましたので、実際に構築し終えての所感をまとめておきたいと思います。 やったこと 昨年、アプリの対応バージョンがiOS13以上となったので、SwiftUIが導入できるようになりました。 ちょうどUITableViewControllerを利用して構築されていたヘルプページの改修を予定していたので、こちらをSwiftUIへ置き換えてみました。 簡単に構成を示しておきます。 画面構成 今回作成したViewは、Objective-Cで作成したUIKitのViewとSwiftのUIKitのViewに挟まれた場所にありました。 この構成が後に悩みのタネとなります。 クラス構成 MVPベースに作成していますが、Presenterの持つBindingされた値をSwiftUIのViewクラスへ渡すような構成となっているため、実質的にはMVVMのようになっています。 今回作成した画面の遷移元がUINavigationControllerを利用したViewControllerだったため、SwiftUIのNavigationViewを利用しての画面遷移実装ができませんでした(後述)。 ですので、画面遷移の実装はViewControllerで行わせつつ、レイアウト部分をSwiftUIのViewクラスで実装しています。 ? 良い点 コードがシンプル 今回作成したViewに関連する箇所のコードのみを抽出すると、以下のようになっていました。 UIKit: ViewController = 60行(UI関連のみ) + Storyboard(XML) SwiftUI: 75行(Preview除く) Storyboardを使わなくなる分、SwiftUIクラス側の記述が大きくなるかと想像していましたが、2〜3割くらいコードが増えるだけで済んでいます。 作業においては、レイアウト作成中にStoryboard/Xib ⇔ Swiftファイルを行き来する必要がないため、集中しやすいと感じました。 純粋なSwiftのコードで記述できるので、複数人で同一Viewを編集するような状況になってもStoryboard/Xibのような厄介なコンフリクトが起きる心配もないですね。 Previewでレビュー効率を上げる SwiftUIリリース時に導入されたPreviewの機能では、複数のPreviewを表示できます。 APIのレスポンスに応じて表示の切り替えを行うViewでは各StateごとのPreviewを表示することで開発効率を上げることができます。 また、レビュー時にも各Previewを参照することでレイアウトを確認できるので、モックの準備などが不要になり非常に効率的かと思います。 Previews.swift struct Sample_Previews: PreviewProvider { static var previews: some View { // プレビューを3つ並べる SampleView(presenter: successPresenter) SampleView(presenter: loadingPresenter) SampleView(presenter: failedPresenter) } // ローディング成功時のプレビュー static var successPresenter: SamplePresenter { let userInterface = SampleViewController() let presenter = SamplePresenter(userInterface: userInterface) presenter.state = .success(itemList: itemList) return presenter } // ローディング中のプレビュー static var loadingPresenter: SamplePresenter { let userInterface = SampleViewController() let presenter = SamplePresenter(userInterface: userInterface) presenter.state = .loading return presenter } // ローディング失敗時のプレビュー static var failedPresenter: SamplePresenter { let userInterface = SampleViewController() let presenter = SamplePresenter(userInterface: userInterface) presenter.state = .failed return presenter } // サンプル表示用のデータモデル static let itemList: [Item] = [ Item(id: 1, title: "問い合わせ1") ] } 表示例 ? 困る点 効率的な記述のできるSwiftUIですが、一方で既存アプリへの導入時にはちょっと困ったこともあります。 UINavigationControllerとNavigationViewの互換性がない UINavigationController&ViewControllerで構成されたViewからSwiftUIで構成されたViewへ遷移させたい場合、UINavigationControllerの内容をNavigationViewに引き継ぐことができません。 UINavigationControllerを使用したViewからNavigationViewを使用したSwiftUIのViewへ単純に遷移させてしまうと、NavigationBarが2重に表示されるような状態になります。 このようなケースにおいては、SwiftUIのNavigationViewとNavigationLinkを使わず、ViewControllerからUIHostingControllerを利用してSwiftUIのViewを呼び出していくのがベターかと思います。 Combine前提にしている処理が多々 例としてアラートのメソッドを挙げますが、こちらにはBinding型の引数があり、表示のコントロールを行うのにはController層、Presenter層などからBindingされた値を渡してあげる必要があります。 public func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable public func alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View UIKitだとViewControllerからpresentしてあげるだけでしたが、要領が変わっているので設計上注意が必要です。 SwiftUIでは、多くのクラスでデータバインディングを前提とした構成を採用しているため、現状ではMVVMが一番スムーズに導入出来るアーキテクチャなのではないかと思います。 (ラクマアプリはMVPアーキテクチャベースとなっているため、このViewだけイレギュラーな構成となってしまいました?) UIKitではデフォルトで用意されている部品が一部ない ActivityIndicator Accessory付きのButton PageControl(iOS13で非対応) etc... UIKitに存在した上記の部品たちはSwiftUIでは用意されていないので、UIKitのクラスをSwiftUIで使えるようラップして使えるようにするか、自作する必要があります。 下記のようなLoadingViewはSwiftUI用に新規作成しています。 Previews.swift struct LoadingView: View { var text: String = "読み込み中..." var body: some View { ZStack { Color(UIColor.clear) VStack { ActivityIndicator(style: .large) Text(text) .bold() .foregroundColor(Color(ColorPalette.textWhite)) .multilineTextAlignment(.center) }.frame(minWidth: 130, idealWidth: 130, minHeight: 130, idealHeight: 130) .compositingGroup() .background(RoundedRectangle(cornerRadius: 10) .fill(ColorPalette.indicatorBackground)) .shadow(radius: 6) .edgesIgnoringSafeArea(.all) } .transition(AnyTransition.opacity.animation(.easeOut(duration: 0.3))) } } struct ActivityIndicator: UIViewRepresentable { typealias UIViewType = UIActivityIndicatorView var style: UIActivityIndicatorView.Style = .medium func makeUIView(context: Context) -> UIActivityIndicatorView { let indicator = UIActivityIndicatorView(style: style) indicator.color = .white return indicator } func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { uiView.startAnimating() } } iOS13⇔iOS14間で表示差分がある 細かなところですが、iOS13とiOS14の間で表示の異なる部分がありました。下記に一例を示します。 このあたりは実装時の確認や検証をしっかり行っていく必要がありますね。 表示時のアニメーションの有無 iOS13でListを使用した画面を表示する場合、デフォルトでのアニメーションが挿入されませんでした。 iOS13 iOS14 Listのデザイン iOS13ではListのgroupごとにカード型のような表示となっていますが、iOS14では従来のUITableViewの表示に準ずるデザインとなっています。 iOS13 iOS14 Previewが重い XcodeでのPreview利用時にObjective-Cを利用しているライブラリのビルドが頻繁に走るため、表示の更新に分単位の時間がかかることがありました。 (このあたりはM1搭載のMacでは速くなっているのでしょうか?) あとがき 既存アプリへのSwiftUIの導入にはいくつかの障壁がありますが、新規作成のViewであったり独立性の高い機能であればSwiftUIを積極的に選択肢に入れるのもアリかと思います? また、今後の導入に備えて既存のObjective-Cのコードを駆逐しておいたり、MVVMなどのアーキテクチャを導入しておいたりしておくことも大事かと思います。 導入のメリットも大きいですし、今後のUI構築はSwiftUIへシフトしていく可能性も高いと思いますので、積極的に使っていきたいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RxSwiftのSubjectとRelayについて簡単にまとめる

はじめに RxSwiftの勉強を始めてObservableについてある程度理解できたので、まとめとして残しておきます。 BehaviorSubject BehaviorSubjectは初期値を設定することができ、onNext, onError, onCompleteの3つのイベントを流すことができます。 //初期値に0を設定、Intが流れるストリームを作成 let behaviorSubject = BehaviorSubject<Int>(value: 0) //ストリームを購読して、流れてきたイベントに応じた処理を定義 behaviorSubject .subscribe(onNext: { value in print(value) }, onCompleted: { print("onCompleted") }) .disposed(by: bag) behaviorSubject.onNext(1) behaviorSubject.onNext(2) behaviorSubject.onNext(3) behaviorSubject.onCompleted() // 0 // 1 // 2 // 3 // onCompleted (エラーは流れないのでonNextとonCompletedの場合だけを定義しています) PublishSubject PublishSubjectは初期値の設定ができず、イベントはonNext, onError, onCompleteの3つのイベントを流すことができます。 let publishSubject = PublishSubject<Int>() //イベントが流れてきたときの処理 publishSubject.subscribe { value in print(value) } .disposed(by: bag) //onNextでイベントを流せる publishSubject.onNext(1) publishSubject.onNext(2) publishSubject.onNext(3) publishSubject.onCompleted() // next(1) // next(2) // next(3) // completed BehaviorRelay BehaviorRelayは初期値を設定することができ、イベントはonNextのみ流れます。 let behaviorRelay = BehaviorRelay<Int>(value: 0) behaviorRelay .subscribe(onNext: { value in print(value) }) .disposed(by: bag) behaviorRelay.accept(1) behaviorRelay.accept(2) behaviorRelay.accept(3) // 0 // 1 // 2 // 3 RelayはonNextではなくacceptでイベントを流します。 PublishRelay PublishRelayは初期値の設定ができず、イベントはonNextのみ流れます。 let behaviorRelay = PublishRelay<Int>() behaviorRelay .subscribe(onNext: { value in print(value) }) .disposed(by: bag) behaviorRelay.accept(1) behaviorRelay.accept(2) behaviorRelay.accept(3) // 1 // 2 // 3 使い分け Subject 通信処理等でエラー処理を行いたいとき Relay UIに表示するデータを格納するとき
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewControllerのライフサイクルについて

はじめに ViewControllerのライフサイクルとオーバーライド時の適切な処理を備忘録として書きます。 ViewControllerのライフサイクル 以下のようになっており、上から順に呼ばれます。 ① func loadView() ↓ ② func viewDidLoad() ↓ ③ func viewWillApper(_:) ↓ ④ func viewWillLayoutSubviews(_:) ↓ ⑤ func viewDidLayoutSubviews(_:) ↓ ⑥ func viewDidAppear(_:) ↓ ⑦ func viewWillDisappear(_:) ↓ ⑧ func viewDidDisappear(_:) ①loadView() viewを生成するメソッドであり、1度だけ呼ばれます。 Storyboardやxibなど、InterfaceBuilderを使用している場合は このメソッドをオーバライドしてはいけません。 loadView() | Apple Developer Documentation ・適切な処理 コードでviewを作成する場合などに適しています。 その際には、super.loadView()は呼びません。 ②viewDidLoad() viewがメモリに読み込まれた後に1度だけ呼ばれます。 viewDidLoad() | Apple Developer Documentation ・適切な処理 viewに対する追加の初期化処理やネットワーク通信など1度だけ行う何らかの処理などが適しています。 基本的な初期化の処理は大体ここで行いますが、このタイミングではviewのframeが確定されていないので正確なframeを取得することが出来ません。 viewDidLayoutSubviews(_:)以降にviewのサイズに関連した処理を書く必要があります。 ③viewWillAppear(_:) viewが画面に表示される直前に呼ばれます。 loadView()やviewDidLoad()と違って 遷移やTab Bar切り替え時など画面が表示されようとする度に呼び出されます。 一方でホーム画面からの復帰や、コントロールセンターを閉じたりした時には呼ばれません。 viewWillAppear(_:) | Apple Developer Documentation ・適切な処理 viewが画面に表示される前に実行する必要のある処理や 画面を表示するたびにviewを更新したい場合などに適しています。 ④viewWillLayoutSubviews(_:) viewがSubviewをレイアウトする前に呼ばれます。 デフォルト実装では何もしません。 viewWillLayoutSubviews(_:) | Apple Developer Documentation ⑤viewDidLayoutSubviews(_:) viewがSubviewをレイアウトした後に呼ばれ、デフォルト実装では何もしません。 viewDidLoad()の部分で書いたように これ以降のタイミングではないと正確なframeは取得できません。 viewDidLayoutSubviews(_:) | Apple Developer Documentation ・適切な処理 viewのサイズに関係する処理などが適しています。 ⑥viewDidApper(_:) viewが画面に表示された後に呼ばれます。 このメソッドが呼ばれたタイミングでは、すでにviewが描画されている状態なので ここでviewの生成を行えば、おかしな事になったりするので注意が必要です。 viewDidAppear(_:) | Apple Developer Documentation ・適切な処理 アニメーション開始、動画の再生やデータのフェッチなど viewが画面に表示された後、すぐに必要となる処理を行うのに適しています。 ⑦viewWillDisappear(_:) viewが表示されなくなる直前に呼ばれます。 具体的には、画面遷移やTab Barによる切り替え時に呼ばれます。 viewWillDisappear(_:) | Apple Developer Documentation ⑧viewDidDisappear(_:) 遷移などでviewが完全に表示されなくなった時に呼ばれます。 viewDidDisappear(_:) | Apple Developer Documentation ・適切な処理 何らかの終了処理などをここで行うのに適しています。 おわりに loadView()やviewDidLoad()以外のメソッドはライフサイクルの中で複数回呼ばれるので、viewの追加や重い処理などには注意が必要です。 間違っていればコメントして下さると有り難いです! 参考資料 ViewControllerのライフサイクル iOS開発におけるUIViewControllerのライフサイクルイベントまとめ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]日付が連続しているかを確認する方法(連続ログイン日数的な機能)

この記事でわかること ※2つの日にちが連続した日付であるかを確認する方法 おまけ:現在地点(今回は日本)の時刻を取得する方法 はじめに 2つの日にちが連続した日付であるかを確認する方法を紹介します。連続ログイン日数の記録的な機能の実装ができるようになります。Date型、Calender型を使って実装した方法を備忘録的に記録しておきます。 実装した手順 ①配列に"何か"を実行した時の日時を記録する ②後日"何か"を実行した時に、その日時と配列に記録された最後の要素を比べ、1日前であるかを確認 ③1日前であれば、連続した日付で"何か"を実行していると判断 今回のポイント 下記の2つのメソッドが今回のキーです。 func addingTimeInterval(_ timeInterval: TimeInterval) -> Date ①引数にした秒数後、または秒数前の日時を取得することができます。マイナスの値を使うことで前の日時、プラスの値を使うことで後の日時を取得できます。今回は引数に24時間前(-60 * 60 * 24)を入れることで実行した1日前の日時を取得します。 引数にはTimeInterval型(時間の長さを表現する)を使用します。 func isDate(_ date1: Date, inSameDayAs date2: Date) -> Bool ②2つの引数の日にちが同じであるかどうかを確かめます。今回は①のメソッドを用いて、今回の実行日を1日戻した後、記録されている最後の日時が昨日と同じ日かどうかを判定するのに使います。同じ日であればtrue、別日であればfalseが返ってきます。 コード 前半部分はおまけである、日本時間の取得方法を書いています。 import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() //イギリス時間の取得 let britishTime = Date() print("イギリス時間: ", britishTime) //イギリス時間: 2021-04-12 05:00:11 +0000 //日本時間の取得 //1.現在地点の取得 let timeZone = TimeZone.current // Asia/Tokyo (current) //2.イギリスと現在地点の時差を秒数で取得 let tmSeconds = timeZone.secondsFromGMT() // 32400 //3.イギリス時間に2で取得した時差(秒)を足すことで日本時間を取得 //ポイントで紹介したaddingTimeIntervalを使用します let japanTime = britishTime.addingTimeInterval(TimeInterval(tmSeconds)) print("日本時間: ", japanTime) //日本時間: 2021-04-12 14:00:11 +0000 //↓ここから連続ログイン機能についての話 //疑似的に一昨日、昨日とログインしたという配列を作ります var didActivityDateArray: [Date] = [] let date1 = japanTime.addingTimeInterval(-60 * 60 * 24 * 2) //一昨日 let date2 = japanTime.addingTimeInterval(-60 * 60 * 24) //昨日 didActivityDateArray.append(date1) didActivityDateArray.append(date2) //1.今日の日付を1日戻す let yesterDay = japanTime.addingTimeInterval(-60 * 60 * 24) //2.1で戻した日時yesterDayと配列の最後の日(データの日付が同じかを確認) //trueだったら変数をインクリメントするなどすることで連続ログイン日数の記録が可能 let isSameDay = Calendar.current.isDate(yesterDay, inSameDayAs: didActivityDateArray.last!) //true } } 最後に いかがだったでしょうか?この他にももっと効率の良い書き方もあると思いますので、もしご存知のかたがいらっしゃったら教えていただけると幸いです。ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】添字付けについて

はじめに 今回は添字付け(subscript)についてみていきたいと思います。 subscript subscriptとは、複数個のプロパティがある時に、配列のように、アクセスできるようにする機能です。配列以外のリストやデータ構造でも利用できます。 struct FoodMenu { let menu = ["menu1", "menu2", "menu3"] var submenu = ["submenu1", "submenu2", "submenu3"] var count: Int { menu.count + submenu.count } subscript(i: Int) -> String { get { return i < 3 ? menu[i] : submenu[i-3] } set { if i > 2 && i < 6 { submenu[i-3] = newValue } } } } var foodMenu = FoodMenu() for i in 0..<foodMenu.count { print(foodMenu[i]) } //menu1 //menu2 //menu3 //submenu1 //submenu2 //submenu3 foodMenu[i]としているので、subscribeのgetが呼ばれます。 i < 3 ? menu[i] : submenu[i-3]から、出力結果からもわかるように、配列menuと配列submenuが繋がったような配列foodMenuになっていますね。 次はsetを使ってみます。 var foodMenu = FoodMenu() foodMenu[1] = "set1" foodMenu[5] = "set5" for i in 0..<foodMenu.count { print(foodMenu[i]) } //menu1 //menu2 //menu3 //submenu1 //submenu2 //set5 "set1"は条件i > 2 && i < 6を満たさないのでsetできていないのがわかります。 また、以下のように、subscriptに対してもstaticを使うこともでき、その場合はFace[2]のようにアクセスすることができます。 struct Face { static subscript(i: Int) -> String { let index = (i < 0) ? 0 : (i > 4 ? 4 : i) let faces = ["?", "?", "?", "?", "?"] return faces[index] } } print(Face[3]) // ? おわりに おわりです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】mutatingとstaticについて

はじめに 今回はメソッドやプロパティによってインスタンスの値が変更される場合に用いられる、mutatingと全てのインスタンスから共通して利用したい場合に用いられるstaticについてみていきましょう。 mutating 先ほど、メソッドやプロパティによってインスタンスの値が変更される場合に用いられる、mutatingと言いましたが、実はこれ、構造体(struct)についてのみつけるキーワードになります。つまり、クラスには必要ありません。 例を見ていきましょう。 struct Clock { var hour = 0 var min = 0 mutating func advance(min: Int) { let m = self.min + min if m >= 60 { self.min = m % 60 // インスタンスの値が変更されている let t = self.hour + m / 60 self.hour = t % 24 // インスタンスの値が変更されている } else { self.min = m // インスタンスの値が変更されている } } mutating func inc() { self.advance(min: 1) } } プロパティを直接書き換えるメソッドのadvance(min:)にmutatingをつけています。さらに、そのメソッドを呼び出しているinc()メソッドにもmutatingが必要です。 mutatingをつけていないと以下のようなエラーがでてきてしまいます。 static staticがついたメソッドをタイプメソッドといいます。 staticは全てのインスタンスから共通して利用したい場合に用いられます。 struct SimpleDate { var year: Int var month: Int var day: Int static func isLeap(_ y: Int) -> Bool { return (y % 4 == 0) && (y % 100 != 0 || y % 400 == 0) } static func daysOfMonth(_ m: Int, year: Int) -> Int { switch m { case 2: return Self.isLeap(year) ? 29 : 28 // isLeap(year)だけでもいい case 4, 6, 9, 11: return 30 default: return 31 } } func leapYear() -> Bool { return SimpleDate.isLeap(year) // 構造体名.タイプメソッド } } staticのついたプロパティやメソッドにアクセスするときは、構造体名.タイプメソッドなどのようにしますが、同じ構造体内でstaticプロパティやメソッドどうしにアクセスする場合はSelf.タイプメソッドやSelfの省略もできます。 //isLeapもdaysOfMonthもstaticで同じ構造体なので、構造体名やSelfは書かなくても良い static func daysOfMonth(_ m: Int, year: Int) -> Int { switch m { case 2: return isLeap(year) ? 29 : 28 case 4, 6, 9, 11: return 30 default: return 31 } } しかし、daysOfMonthメソッドのstaticをとると以下のようなエラーになり、Self.isLeap()にするか、SimpleDate.isLeap()にしないといけません。 同様の理由で、leapYear()メソッドには構造体名またはSelfが必要になります。 おわりに おわりです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] 高階関数に利用できる高階関数なnot関数

これはなに? 趣味の世界です。 問題 Arrayをfilterする時に使用するFilter用の関数を用意しています。 func isValid(_ i: Int) -> Bool { i > 0 && i <= 100 || i > 1000 } これでvalidな要素のみの配列が取得可能です。 let validItems = array.filter(isValid) このとき同時にvalidではない要素も取得したいとします。普通に書けばこうなります。 let invalidItems = array.filter { !isValid($0) } 対称性が崩れて見えて気持ち悪いですね。 解決策 問題を解決するために not関数を用意します。 func not<T>(_ condition: @escaping (T) -> Bool) -> (T) -> Bool { { !condition($0) } } これを利用すると、validではない要素の取得はこうなります。 let invalidItems = array.filter(not(isValid)) 対称性が保たれましたね。 おまけ この not関数はこのままで2変数関数に対して利用可能です。 func f(_ i: Int, _ s: String) -> Bool { i == Int(s) } let a = (0..<100).map(String.init).shuffled() print( zip(0..., a) .filter(not(f)) // OK )
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでbitでフラグ管理する

動機 bitでフラグ管理してsetみたいなものを作りたい。 必要なbitオペレーション フラグが立っているかどうかの確認 (bit & 1<<i) != 0 iビット目を立てる bit | (1<<i) iビット目を消す bit & ~(1<<i) 作ったもの struct BitSet { var bit: Int = 0 func contain(i: Int) -> Bool { return (bit & 1<<i) > 0 } mutating func insert(i: Int, raise: Bool) { self.bit = raise ? bit | (1<<i) : bit & ~(1<<i) } } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】RxSwift勉強してみたPart7

はじめに 前回 今回はOptionalな値が流れるストリームに対してさまざまなことをできるようにする拡張ライブラリーであるRxOptionalをみていこうかと思います。 RxOptional filterNil() nilをはじいてくれるオペレーター Observable<String?> .of("100", nil, "200") .filterNil() //Observable<String?> -> Observable<String> .subscribe { print($0) } //100 //200 replaceNilWith() nilを置き換えることができるオペレーター Observable<String?> .of("100", nil, "200") .replaceNilWith("300") .subscribe { print($0) } //100 //300 //200 errorOnNil() nilが検出されたときにエラーをながすオペレーター Observable<String?> .of("100", nil, "200") .errorOnNil() .subscribe { print($0) } //100 //Found nil while trying to unwrap type<Optional<String>> filterEmpty() 空の配列や辞書をはじいてくれるオペレーター Observable<[String]> .of(["1"], [], ["2", "3"]) .filterEmpty() .subscribe { print($0) } //["1"] //["2", "3"] おわりに RxSwiftって拡張ライブラリーたくさんありますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】RxSwift勉強してみたPart6

はじめに 前回 今回はWKWebViewを使った以下のような簡単なアプリを作ります。 GitHub WebViewRxSwiftフォルダにあります 実装 import UIKit import RxSwift import RxCocoa import RxOptional import WebKit final class WKWebViewController: UIViewController { @IBOutlet private weak var webView: WKWebView! @IBOutlet private weak var progressView: UIProgressView! private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupWebView() } private func setupWebView() { let loadingObservable = webView.rx.observe(Bool.self, "loading") .filterNil() .share() loadingObservable .map { return !$0 } .bind(to: progressView.rx.isHidden) .disposed(by: disposeBag) loadingObservable .bind(to: UIApplication.shared.rx.isNetworkActivityIndicatorVisible) .disposed(by: disposeBag) loadingObservable .map { [weak self] _ in return self?.webView.title } .bind(to: navigationItem.rx.title) .disposed(by: disposeBag) webView.rx.observe(Double.self, "estimatedProgress") .filterNil() .map { return Float($0) } .bind(to: progressView.rx.progress) .disposed(by: disposeBag) let url = URL(string: "https://www.google.com/") let urlRequest = URLRequest(url: url!) webView.load(urlRequest) } } 解説 filterNil() RxOptionalライブラリを利用することで使えるようになります。 nilの場合は値を流さず、nilでない場合はアンラップして値をながすオペレーターです。 RxOptionalを使わない場合 Observable<String?> .of("1", nil, "3") .filter { $0 != nil } .map { $0! } .subscribe { print($0) } RxOptionalを使う場合 Observable<String?> .of("1", nil, "3") .filterNil() .subscribe { print($0) } share() ColdなObservableをHotなObservableに変換するオペレーター 以下のコードはtextFieldへのテキスト入力を監視し、ストリームの途中で値を加工して複数のlabelへbindしています。 let text = textField.rx.text .map { text -> String in print("call") } text .bind(to: label.rx.text) .disposed(by: disposeBag) text .bind(to: label2.rx.text) .disposed(by: disposeBag) text .bind(to: label3.rx.text) .disposed(by: disposeBag) ここで、textFieldに123と入力すると、"call"は9回呼ばれます。値を入力するたびにmapが3回呼ばれるためです。これは、データーベースアクセスするもの、通信処理が発生するものでは好ましくありません。 なぜこの現象が起こるのかをみていきたいと思います。 textField.rx.textはControlProperty<String?>として定義されています。 ControlPropertyはUI要素のプロパティで使われ、メインスレッドで値が購読されることが保証されているColdなObservableです。 ColdなObservableとは、subscribe(bind)した時点で読み込まれ、複数回subscribeするとその都度ストリームが生成されるという仕組みです。 今回は3回subscribe(bind)したので、3個のストリームが生成されます。すると、値が変更されたときにオペレーターが3回実行されてしまいます。 これを解決するには複数回購読してもオペレーターを一回実行するだけで済ませれば良さそうです。つまり、ColdなObservableをHotなObservableに変換します。そのためにshare()と言うオペレーターを使います。 let text = textField.rx.text .map { text -> String in print("call") } .share() text .bind(to: label.rx.text) .disposed(by: disposeBag) text .bind(to: label2.rx.text) .disposed(by: disposeBag) text .bind(to: label3.rx.text) .disposed(by: disposeBag) こうすることで、123と入力したときに"call"が三つだけ出力されていたら成功です。 おわりに 次回 RxSwift少しずつわかってきました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む