20210426のSwiftに関する記事は12件です。

辞書Dictionary型の配列 swift まとめ

■辞書型の配列にはキーと値がある。型がそれぞれ統一されている。 基本はね... キーがString型なら全てのキーがStringになる。 値がInt型なら全ての値がInt型になる。 var dic1 = ["name":"aoka","age":"28"] print(dic1) //結果: ["name":"aoka","age":"28"] var dic2 = ["apple":100,"banana"200,"orange":300] print(dic2) //結果: ["apple":100,"banana"200,"orange":300] 後にAny型を使った紹介をするそれを使えば型をバラバラに入れれる。 ■空の辞書型の配列を作ろう //これでキーはString型,値はInt型の空の配列ができる。 var dic3 = Dictionary<String:Int>() //これをprintすれば空の辞書型配列だとわかる。 print(dic3) //結果: [:] ■データの代入方法 //空の辞書型配列があると仮定する。 var dic3 = Dictionary<String:Int>() dic3["apple"] = 100 dic3["banana"] = 200 print(dic3) //結果: ["apple":100,"banana": 200] ■データを取り出してみよう オプショナルバインディングしないとoptional(値)で返ってくる optional(値)だと使えない!!!! var dic4 = ["apple":100,"banana"200,"orange":300] print(dic4["apple"]) //結果: optional(100) となり使えない!!!!! //オプショナルバインディング if let price = dic4["apple"] { print(price) } //結果: 100 となり使える!!! //デタラメなキー入れたらただのnilになる。 print(dic4["APPLE"]) //結果: nil ■データの上書き var dic4 = ["apple":100,"banana"200,"orange":300] dic4["banana"] = 999 if let price = dic4["banana"] { print(dic4["banan"]) } //結果: 999 //データの消去もできる。 dic4["aplle"] = nil if let price = dic4["apple"] { print(dic4["apple"]) } else { print("データをnilにして消したからこっちが表示される。") } //結果: データをnilにして消したからこっちが表示される。 ■Any型を使い色んな型を値に指定してみた!! //値をAny型とする。 var dic5:Dictionary<String:Any> = ["name":"aoka", "age":28, "birthday":Date()] //値がAny型なのでオプショナルバインディングして確認 これは本当にString型なのか等... if let name = dic5["name"] as? Stirng { print(dic5["name"]) } //結果: aoka if let age = dic5[age] as? Int { print(dic5["age"]) } //結果: 28 if let birthDay = dic5["birthday"] as? Date { print(birthDay) } //結果: 2021-04-26 14:31:50 +0000 参考にさせて頂いた動画です!  https://www.youtube.com/watch?v=IJIJawKJTLc&list=PLQ5rERkGSxF9_soz3Ns-SpURWsy0WmbJQ&index=51 アプリ道場サロン
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Firebaseの基本的な使い方まとめ

はじめに  Firebaseを使っていく中で学びになったことをまとめてみようと思います! Firebaseのセットアップなどは省きます。エラーハンドリングも今回は最低限にしています。 Authentication メールアドレス認証を有効にする ユーザー情報の作成 guard let email = emailTextField.text, let password = passwordTextField.text else { return } Auth.auth().createUser(withEmail: email, password: password) { result, error in if let error = error { print("情報の作成に失敗\(error)") return } print("情報の作成に成功") } このような感じでユーザー情報がAuthに保存される 認証済みかの判定 if Auth.auth().currentUser?.uid == nil { print("認証済み") } Firestore Model import Foundation import FirebaseFirestore struct User { let email: String let username: String let createdAt: Timestamp init(dic: [String: Any]) { self.email = dic["email"] as? String ?? "" self.username = dic["username"] as? String ?? "" self.createdAt = dic["createdAt"] as? Timestamp ?? Timestamp() } } Firestoreに情報を保存する 先ほどのAuth.auth().createUser(withEmail: , password: )でresultが返ってくるので、そのresultからuserのuidを取得する。 Firestore.firestore().collection("users").document(uid).setData(docData)でFirestoreのusersという名前をつけた collectionのuidという名前をつけたdocumentにsetDataで保存している。 guard let uid = result?.user.uid else { return } guard let username = self.usernameTextField.text else { return } let docData = [ "email": email, "username": username, "createdAt": Timestamp(), ] as [String: Any] Firestore.firestore().collection("users").document(uid).setData(docData) { error in if let error = error { print("Firestoreへの保存に失敗しました\(error)") return } print("Firestoreへの保存に成功しました") } Firestoreから情報を取得する getDocumentsでFirestoreに保存したユーザー情報を取得する。 Firestore.firestore().collection("users").getDocuments { snapshots, error in if let error = error { print("ユーザー情報の取得に失敗しました\(error)") return } snapshots?.documents.forEach { snapshot in let dic = snapshot.data() let user = User(dic: dic) } } Storage Storageに画像を保存する データが大きすぎるので、jpegData(compressionQuality:)でデータを小さくしています。 fileNameはNSUUID().uuidStringを使って適当な文字列にしました。 Storage.storage().reference().child("profile_image").child(fileName)でStorageのreferenceの子要素にprofile_imageフォルダを作り、その中にfileNameという名前をつけたimageを保存していきます。uploadImageをstorageRef.putData(uploadImage, metadata: nil)で実際に保存します。 guard let image = imageView.image else { return } guard let uploadImage = image.jpegData(compressionQuality: 0.3) else { return } let fileName = NSUUID().uuidString let storageRef = Storage.storage().reference().child("profile_image").child(fileName) storageRef.putData(uploadImage, metadata: nil) { data, error in if let error = error { print("Storageへの保存に失敗しました\(error)") return } print("Storageへの保存に成功しました") } URLを取得する downloadURLでURLを取得できる。 storageRef.downloadURL { url, error in if let error = error { print("Storageからのダウンロードに失敗しました\(error)") return } print("Storageからのダウンロードに成功しました") } おわりに 随時更新予定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

どんな型でも代入できるAny型とは swift

■Any型とは型の一つで何でも屋さん IntでもStringでもなんの型も入る ■注意:: 中身取り出す時はオプショナルバインディングで取り出すと安全。 var a:Any = 123 //a as? Intで aはInt型と指定してる。 合っていたら if let b = a as? Int { print(a) } //結果: 123 ■Any型を指定した変数は後から違う型でも代入できる!!! 上の続き 普通に型推論でvar a = 123にしてたら下記のように文字列型を入れるとエラーになる。 だがAny型やったらなんでも入るから新たに文字列型を入れれるんだよこれが! a = "こんにちわ" if let b = a as? String { print(b) } //結果: こんにちわ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

世界で一番わかりやすいデリゲートのお話

この記事は何? iOSアプリ開発を学び始めると、大抵の初学者はデリゲートがなかなか理解できずに苦しみます。私もその1人でした。しかし、それも今となっては昔の話。いつの間にか「そんなものなんだな」と理解したつもりになって、適当に使っていました。 ここらで一旦、「誰にでもわかるように説明できるのか」をテーマに解説してみたいと思いました。 実行環境 Swift 5.3 Xcode 12.4 macOS 11.2 はじめに デリゲートを理解するためには、プロトコルを知っておく必要があります。 必要最低限のプロトコルについて、こちらで解説しています。 ハンズオン 実際にコードを書きながら、説明していきます。 まずは、次のようなプロトコルを定義しておきます。 protocol LanguageDelegate { func hello() } このLanguageプロトコルは、hello()メソッドの実装を要件としています。 つまり、このプロトコルへの適合は、「その言語を知っているなら、挨拶ができる」と保証することを意味します。 実際に、そのようなクラスを定義してみましょう。 class Japanese: LanguageDelegate { func hello() { print("やあ") } } let japanese = Japanese() これは、日本語をモデル化するクラスです。 hello()メソッドを実装しているので、LanguageDelegateプロトコルに適合しています。 インスタンスも作成しておきます。 英語やイタリア語のクラスも、同じように定義してみましょう。 class English: LanguageDelegate { func hello() { print("Hello") } } let english = English() class Italiano: LanguageDelegate { func hello() { print("Ciao") } } let italiano = Italiano() ここまではプロトコルのお話なので、難しいことはないですね。 ここからがデリゲートの出番です。 それでは、「挨拶できる人間」をクラスとして定義してみましょう。 class Person { func greeting() { // say something... } } let someOne = Person() このクラスをインスタンス化した「誰かさんsomeOne」は、何語でどんな挨拶ができるでしょうか? その方法をこの「誰かさん」が決めるのではなく、他のクラスに「おまかせ」するのがデリゲートです。 本来、デリゲート(Delegate)とは「移譲する」という意味です。 Personクラスの定義に、以下の2行を追記しましょう。 class Person { var delegate: LanguageDelegate? // おまかせ先 func greeting() { delegate?.hello() // おまかせ先の言語で挨拶 } } let someOne = Person() ここでは、変数delegateの型が: LanguageDelegateと明示されていることに注目します。最初に定義したプロトコルを型として利用していますね。つまり、この変数プロパティには「LanguageDelegateに適合した型」ならなんでも割り当てることができます。 日本語を示すインスタンスを設定してみましょう。 someOne.delegate = japanese someOne.greeting() // prints "やぁ" 「誰かさん」は日本語が喋れるようになったので、挨拶も「やぁ」と出力されます。 英語やイタリア語を喋れるようにするには、インスタンスのdelegateプロパティにそれぞれの言語インスタンスを設定します。 someOne.delegate = english someOne.greeting() // prints "Hello" someOne.delegate = italiano someOne.greeting() // prints "Ciao" これがいわゆる「デリゲート・パターン」です。 someOne.greeting()というコードが3回、実行されていますが出力結果はどれも異なる挨拶です。 デリゲート先に実装を移譲しているからですね。 何が嬉しいのか デリゲート・パターンの利点は、ここからです。 あなたが王様になって、新しい国を作ったとしましょう。もちろん、使用言語も独自のものです。 以下のように、クラスとして定義しましょう。 class MyKingdom: LanguageDelegate { func hello() { print("?") } } let myKingdom = MyKingdom() とてもファンキーな挨拶ですね。飛沫も飛ばないので安全です。 「誰かさん」も、この国の言語を喋れるようにデリゲート先を設定しましょう。 someOne.delegate = myKingdom someOne.greeting() // prints "?" Personクラスの実装に触れることなく、「誰かさん」の挙動を変更できました。 デリゲート・パターンのメリットは、「実装方法を定義するときではなく、後で決定できる」ことです。 さらに、「別の誰か」を作成しましょう。 この「別の誰か」は、イタリア語を喋ります。 let anoterOne = Person() anotherOne.delegate = italiano anotherOne.greeting() // prints "Ciao" someOneとanotherOneは、同じPersonクラスのインスタンスなのに、greeting()メソッドの挙動が異なります。 それぞれのインスタンスは、デリゲート先が別の言語クラスになっているからですね。 これも、デリゲート・パターンの利点です。 説明は以上です いかがだったでしょうか? 長くなってしまいましたが、iOSやビューコントローラの概念と切り離して、できるだけシンプルにデリゲート・パターンを説明してきました。 このように説明することで、自分自身の理解も整理することができました。 感想なども、ぜひコメントしていただけると嬉しいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XCUIElementの使い方をざっくりまとめてみた

XCUITest周りで時間を浪費しないために XCUIElementTypeQueryProviderの定義済みクエリを使うことで、テストで利用するUIを特定することができます。 しかし、まとまった記事が少なく扱い方が見つからず時間を浪費したものもいくつかあったので自分で発見したやり方も含めて簡単にまとめてみます。 ※下記を省略してます SampleViewUITest.swift let app = XCUIApplication() AccessibilityIdentifierを使えるパターン UIView SampleViewController.swift view.accessibilityIdentifier = "sample_view" SampleViewUITest.swift let view = app.otherElements["sample_view"] UIButton SampleViewController.swift let button = UIButton() button.accessibilityIdentifier = "sample_button" // サンプルというタイトルをセット button.setTitle("サンプル", for: .normal) SampleViewUITest.swift let button = app.buttons["sample_button"] // ボタンのタイトルを確認 XCTAssertEqual(button.label, "サンプル") UILabel SampleViewController.swift let label = UILabel() label.accessibilityIdentifier = "sample_label" // サンプルというテキストをセット label.text = "サンプル" SampleViewUITest.swift let label = app.staticTexts["sample_label"] // ラベルのテキストを確認 XCTAssertEqual(label.label, "サンプル") UITextField (SecureTextEntryではない) テキストの中身は.value as! Stringでキャストして使います。 SampleViewController.swift let textField = UITextField() textField.accessibilityIdentifier = "sample_textField" // サンプルというテキストをセット textField.text = "サンプル" SampleViewUITest.swift let textField = app.textFields["sample_textField"] // ラベルのテキストを確認 XCTAssertEqual(textField.value as! String, "サンプル") UITextField (SecureTextEntry) テキストの中身は.value as! Stringでキャストして使うと 中身が"・・・・"になるのでバリデーションチェックなどには使えません。 ※いい方法あれば教えて欲しいです。 SampleViewController.swift let textField = UITextField() textField.isSecureTextEntry = true textField.accessibilityIdentifier = "sample_textField" // サンプルというテキストをセット textField.text = "サンプル" SampleViewUITest.swift let textField = app.secureTextFields["sample_textField"] // ラベルのテキストを確認 XCTAssertEqual(textField.value as! String, "サンプル") // failure UITabBarとUITabBarItem SampleViewController.swift tabBarController?.tabBar.accessibilityIdentifier = "sample_tabBar" tabBarItem.accessibilityIdentifier = "sample_barItem" SampleViewUITest.swift let tabBar = app.tabBars["sample_tabBar"] let barItem = tabBar.buttons["sample_barItem"] UIImageView .isAccessibilityElement = true これが無いと使えません。 SampleViewController.swift let imageView = UIImageView() imageView.isAccessibilityElement = true imageView.accessibilityIdentifier = "sample_image" SampleViewUITest.swift let tabBar = app.tabBars["sample_tabBar"] let barItem = tabBar.buttons["sample_barItem"] AccessibilityIdentifierを使えないパターン SwipeMenu .accessibilityIdentifier が設定できないので .accessibilityLabel を設定しておくとうまく拾える。 SampleViewController.swift let action = UIContextualAction() action.accessibilityLabel = "leadingSwipe_action" SampleViewUITest.swift let action = app.tables.cells.firstMatch.buttons["leadingSwipe_action"] AlertとAlertAction ラベル名でしか拾えない。※いい方法あれば誰か教えて欲しいです。 SampleViewController.swift let alert = UIAlertController() alert.view.accessibilityIdentifier = "alert_view" let cancelAction = UIAlertAction( title: "キャンセル", style: UIAlertAction.Style.cancel, handler: nil)) // accessibilityIdentifierを指定しても使えない。 cancelAction.accessibilityIdentifier = "alert_cancel_button" ... SampleViewUITest.swift let alert = app.alerts["alert_view"] // accessibilityIdentifierを指定しても使えない。 // let cancelAction = alert.buttons["alert_cancel_button"] // ラベル名で拾える let cancelAction = alert.buttons["キャンセル"] それでも拾い方がわからない時 例えばUISegmentedControl内のSFシンボルを使ったボタン 順番で拾うパターン SampleViewUITest.swift let segmentedControl = app.segmentedControls["sample_segmentedControl"] let button01 = segmentedControl.buttons.element(boundBy: 0) // 拾いたいボタンのindex 順番が使えないパターン(動的に変化するなど) SFシンボルの特有のラベル名を事前に調べておきます。 ※SFシンボル名とは若干異なるようです。 SampleViewUITest.swift let segmentedControl = app.segmentedControls["sample_segmentedControl"] for button in segmentedControl.buttons.allElementsBoundByIndex { print(button.label) } その他Tips 便利だったので覚書程度に。 色やイメージのテストについて 色やイメージ自体は拾えないが、変更の挙動があったかどうかを accessibilityIdentifierで拾うことでカバーしました。 SampleViewController.swift // 色の設定のたびにaccessibilityIdentifierを付け直しています。 button.tintColor = isStar ? .systemYellow : systemGray button.accessibilityIdentifier = isStar ? "star_button" : "notStar_button" SampleViewUITest.swift let isStar = app.buttons["star_button"].exists キーボードのReturnKey(DoneKey) extensionでまとめておくと便利でした。 extension_XCUIApplication.swift extension XCUIApplication { // 日本語と英語のキーボードのみ対応 var doneKey: XCUIElement { // キーボードの表示アニメーションのラグを考慮して1秒探します。 if keyboards.buttons["完了"].waitForExistence(timeout: 1) { return keyboards.buttons["完了"] } else { return keyboards.buttons["Done"] } } var returnKey: XCUIElement { // キーボードの表示アニメーションのラグを考慮して1秒探します。 if keyboards.buttons["改行"].waitForExistence(timeout: 1) { return keyboards.buttons["改行"] } else { return keyboards.buttons["Return"] } } } キーボードの入力と強制タップ こちらもextensionでまとめておくと便利でした。 強制タップはタップできないオブジェクトもタップしたい時に使います。 UIImagePickerController で画像を選択するのに使いました。 extension_XCUIElement.swift extension XCUIElement { // テキストに新しい文字列を入力する func clearAndEnterText(text: String) { guard let stringValue = self.value as? String else { XCTFail("Tried to clear and enter text into a non string value") return } self.tap() let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) self.typeText(deleteString) self.typeText(text) } // 強制タップ func forceTap() { if self.isHittable { self.tap() } else { let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)) coordinate.tap() } } } 最後に 初めてUITestを実装してみましたが、意外と面白かったです。 慣れてくるといろんな挙動が表現できるようになるものの時間も結構かかりました。 つまり変更に強いテストを書く必要があります。 プロジェクトにマッチしたPageObjectパターンの設計や accessibilityIdentifierをうまくまとめる方法など工夫がまだまだ必要だと感じました。 そもそも無理にUITestでテストする必要がないこともあるかもしれません。 ライブラリを使ったテストなども試して、ベストプラクティスを見つけようと思います。 参考にした記事や書籍 iOSアプリ開発自動テストの教科書 XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する Xcode7からのUI Testing&XCUIElementの基本操作まとめ Neither element nor any descendant has keyboard focusを対処する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

REPLモード

SwiftにはREPLモードでプログラムを動かすことができます。 実行方法 ターミナルを開き、 swift もしくは xcrun swift で、Swift REPLモードを起動できます。 ステートメント 1> let name = "Tanaka" name: String = "Tanaka" 2> print("Hello, \(name)") Hello, Tanaka Swiftのステートメントを入力すると、REPLEは対話形式で応答します。 関数 5> func sample(name: String) { 6. return name 7. } REPLでは関数も書くことができます。 関数をかく時は、最初の一文の行番号には最後に>がつきます。関数ステートメントのように、連続する行のステートメントは、行番号の後ろに.ピリオドがつきます。 終了方法 REPLを終了したい時は、 :exit で終了することができます。 まとめ 実際の開発で使うことはほとんどないですが、何か新しい技術や計算処理を実験したい時などは、PlaygroundやREPLが使えそうです。 参考 [増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plusシリーズ) 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ジェネリクス関数とジェネリクスプロトコル

ジェネリクス関数 通常の関数は、引数の型を決めたり、返り値の型を決めなければいけません。 しかし、ジェネリクト関数では、Int型でもString型でもどんな型でも対応してくれることができます。 書き方はこんな感じ。 //ジェネリクスを指定 func sample<T>(x:T, y:T) -> T { return x + y } ジェネリクス関数を使用したい時は、関数名の後ろにをつけます。Tじゃなくても良いんですが、一般的にはT。 これだと関数内で演算が行われることになるので、その場合は数値でないといけなくなります。 その場合は下記のように「関数名<T: Numeric」と指定することで、数値型要素を持つ関数であることを宣言します。 //ジェネリクスを指定 func sample<T:Numeric>(x:T,y:T) -> T { return x + y } こうすることで、Int, Double, Floatなど数値型であればどの型にも対応することが可能になります。 ジェネリクスプロトコル プロトコルにジェネリクスを使用することで、Int型とString型のクラスを定義したりすることができます。 //プロトコルの定義 protocol FruitsProtocol { associatedtype T var item:T { get } func fruitvalue(item:T) } //クラス(Int型) class Fruits1:FruitsProtocol { var item:Int = 100 func fruitvalue(item: Int) { self.item += 100 } } //クラス(String型) class Fruits2:FruitsProtocol { var item:String = "100" func fruitvalue(item: String) { self.item = item + "円です" } } プロトコルでジェネリクスを使用する時は、 associatedtype T のように任意の型を指定します。 参考 [増補改訂第3版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plusシリーズ) 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】InputAccessoryViewを使う

はじめに 今回はInputAccessoryViewというものを使って、以下のようなものを作ってみます。 実装 以下のように、textViewとbuttonをおいたInputAccessoryView.xibを作ります。 そしてInputAccessoryView.swiftに以下のようにコードを記述します。 import UIKit final class InputAccessoryView: UIView { @IBOutlet private weak var textView: UITextView! @IBOutlet private weak var sendButton: UIButton! override init(frame: CGRect) { super.init(frame: frame) loadNib() setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - setup nib private extension InputAccessoryView { func loadNib() { let nib = UINib(nibName: String(describing: InputAccessoryView.self), bundle: nil) guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { return } view.frame = self.bounds self.addSubview(view) } } // MARK: - setup views private extension InputAccessoryView { func setupViews() { textView.layer.cornerRadius = 15 sendButton.layer.cornerRadius = 15 } } 次に、xibとswiftファイルを紐付けします。以下のように、File's OwnerのClassを先ほど実装したInputAccessoryViewにします。 ここで、File's Ownerのこちらの画面が黄色くなっている場合はIBOutletを接続し直しましょう。 そして、最後にViewControllerを以下のように実装してみます。 import UIKit final class ViewController: UIViewController { private var myInputAccessoryView: InputAccessoryView = { let view = InputAccessoryView() view.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 60) return view }() override var inputAccessoryView: UIView? { myInputAccessoryView } override var canBecomeFirstResponder: Bool { true } } これで先ほどのようなキーボードの上にtextViewとbuttonが表示されてくれるはずです。 ドキュメント 参考 おわりに こんな便利なものがあったとは、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swift computed property 確認用

computed propertyのがgetが参照しているstored propertyに値が入っている状態でcomputed propertyを呼ぶとgetが出力される。 computed propertyに値が代入すると、setが呼ばれ、参照しているstored propertyの値が変化する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI でちょっぴりリッチアニメーション

はじめに 執筆環境:Xcode 12.4 / macOS Big Sur 11.2.3 この記事は 寄付を身近にする dim. の開発を行う中での学びを共有する目的で執筆しております。 TechCrunch さんにも取り上げていただきましたので御覧ください。 アニメーション このように、ロゴが一定区間障害物を避けるアニメーションの実装方法をご紹介します! 1. Offset が取れる前提で記述 SwiftUI で何が難しいかというと、 ScrollView の offset 取得 だということは有名かと思います。 なので、一旦その offset が取れる前提でアニメーションを記述してみます。 struct ContentView: View { @State private var offset: CGRect? // 問題はこれをどのように更新するか var body: some View { ScrollView() { GridView() // スクロールできるコンテンツ View .padding(.top, 50) // ロゴなどを表示させる領域を確保 } .overlay(topAreaView, alignment: .top) } } アニメーション方法は他にも考えられますが、ここでは最もシンプルな方法と判断したものでご紹介します。 extension ContentView { private var topAreaView: some View { VStack { Spacer() Assets.logo.image.toImage // ロゴ画像 .resizable() .scaledToFit() .frame(height: 35) // ロゴのサイズ指定 Spacer() .frame(height: logoBottomHeight) // ここを変動させることで動かす } } } Y軸方向に最大値・最小値、アニメーション時の座標値の3種類に分けて計算した数値を返します。 細かな計算式は実装方法によって異なるため、省略しております。 extension ContentView { private var logoBottomHeight: CGFloat { let max = ... // ロゴが上昇する最大値Y let diff = ... // offset?.origin.y を利用してスクロール量計算 if diff > .zero && diff <= max { return diff // アニメーション可能 } else if diff > .zero { return max // 最大位置で停止 } else { return .zero // 最小位置で停止 } } } 2. Offset の取得 Offset value の伝達には preference を利用します。 今回は offset を管理したいので CGRect を型に指定します。 struct OffsetPreferenceKey: PreferenceKey { typealias Value = CGRect static var defaultValue: Value = .zero static func reduce(value: inout Value, nextValue: () -> Value) {} } offset は GeometryReader 経由で取得取得可能です。 ダミーの Color.clear を準備し、その offset を preference 側に渡しています(このようにダミーを多用する方法は パフォーマンス低下を招くため多用厳禁 です。参考としてご覧ください)。 struct OffsetReader: View { var body: some View { GeometryReader { geometry in Color.clear .preference( key: OffsetPreferenceKey.self, value: geometry.frame(in: .frameLayer) ) } } } private extension CoordinateSpace { static let frameLayer: CoordinateSpace = .named("frameLayer") } 3. 結合 OffsetReader(View)をスクロールされるコンテンツのバックグラウンドに設定 onPreferenceChange 経由で offset の変化を取得 offset を更新 以上が結合のために追加した処理になります。 注意点としては、 offset の更新が onAppear 呼び出し前に行われた際、 動作が安定しない場合がある 点です。以下の例をそのまま利用せず、実際に実装してみて 動作が安定しなかったとき、参考に してみてください。 struct ContentView: View { @State private var offset: CGRect? @State private var isOnAppear = false var body: some View { ScrollView() { GridView() .padding(.top, 50) .background(OffsetReader()) // リーダーをバックグラウンドに設定 } .overlay(topAreaView, alignment: .top) .onAppear { isOnAppear = true } // preference 経由で値変化を取得 .onPreferenceChange(OffsetPreferenceKey.self, perform: { offset in // 値変化をアニメーションに対応 withAnimation { // onAppear が呼ばれる前にアニメーション制御が走ると不具合が発生する場合あり if isOnAppear { self.offset = offset // offset を更新 } } }) } } さいごに 最後までご覧いただきありがとうございます。 今回は「 希望のアニメーションを実現する 」ことを最優先にした場合の方法でご紹介しました。ぜひ、パフォーマンスのことも頭の片隅に置いた上で参考にしていただければと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI で複数の画面を一度に戻る方法

はじめに 執筆環境:Xcode 12.4 / macOS Big Sur 11.2.3 ホーム>決済画面>決済完了画面 このように3回以上の画面遷移を行ったとき、一度に「ホーム」に戻る方法を解説いたします。 この記事は 寄付を身近にする dim. の開発を行う中での学びを共有する目的で執筆しております。 TechCrunch さんにも取り上げていただきましたので御覧ください。 概要 一気に消さないといけない画面が多い場合 Environment を利用する 一気に消さないといけない画面が少ない場合 dismiss を伝搬させる方法でも良いかも 0. 基礎 自分自身を閉じようとする場合は、以下のような処理で実装が可能です。 struct TransitionSampleView: View { @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> var body: some View { Button( "dismiss", action: { // TransitionSampleView を閉じる presentationMode.wrappedValue.dismiss() } ) } } sheet などで画面遷移を実現している場合、 Binding されている isPresented の値を false にすることで画面を閉じることも可能です。 struct TransitionSampleView: View { @State var isNext: Bool = false // 何かしらの方法でこれを切り替える var body: some View { Text("SAMPLE") .sheet(isPresented: $isNext) { Text("NEXT") } } } このどちらを利用した実装方法であるかを意識しながら読み進めていただくと、より理解が深まるかと思います。 1. dismiss 伝搬 sheet などの onDismiss で自身を閉じる処理を記述する方法です。 ホーム>決済画面>決済完了画面 このように遷移させて決済完了画面からホームに戻るとき、決済画面の sheet に以下を実装するイメージです。 struct TransitionSampleView: View { @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> @State var isNext: Bool = false // 何かしらの方法でこれを切り替える var body: some View { Text("SAMPLE") .sheet( isPresented: $isNext, onDismiss: { // 遷移先が閉じたとき、自身も閉じる presentationMode.wrappedValue.dismiss() }, content: { Text("NEXT") } ) } } メリット:非常にシンプル デメリット:一瞬で戻るというよりは、1画面ずつ段階的に戻るので、画面数が多いとユーザ体験を悪化させる可能性がある 2. Environment EnvironmentKey を用いた方法です。 ホーム>決済画面>決済完了画面 このように遷移させて決済完了画面からホームに戻るとき、ホームに環境変数設定、決済完了画面に環境変数を利用した dismiss 処理を記述します。 // 閉じる・開くの制御なので Bool struct PresentingModalKey: EnvironmentKey { static let defaultValue = Binding<Bool>.constant(false) } // 自作環境変数 extension EnvironmentValues { var isPresentingModal: Binding<Bool> { get { self[PresentingModalKey.self] } set { self[PresentingModalKey.self] = newValue } } } まずは、遷移元の実装です。 dismiss 伝搬とほぼ変わりませんが、 遷移先の画面に対して Environment を設定している 箇所が追加になっています。逆に PresentationMode は 利用していない ことがわかるかと思います。 struct ContentView: View { @State var isPresentingModal: Bool = false var body: some View { Button( "NEXT", action: { isPresentingModal = true } ) // 環境変数として利用する変数をトリガーとする .sheet(isPresented: $isPresentingModal) { NavigationView { Text("NEXT") } // 子 View に環境変数を設定 .environment(\.isPresentingModal, $isPresentingModal) } } } 最後に、遷移後の実装です。 Environment value の取得方法は SwiftUI で最初から実装されているものを利用するときと同様です。 struct CompletedView: View { // 環境変数の取得 @Environment (\.isPresentingModal) var isPresentedModally var body: some View { Button( "CLOSE", action: { // 環境変数に対して状態変更を適応 isPresentedModally.wrappedValue = false } ) } } メリット:一瞬で複数画面を閉じることができる デメリット:Environment はアクセス可能領域が自然と広くなるので乱用厳禁、 sheet だけでの遷移の場合正常に動作しない(記事執筆段階) おわりに 最後までご覧いただきありがとうございます。 UIKit でも画面遷移周りをどのように管理するかは、非常に重要なポイントなので、今後も引き続き SwiftUI を利用する場合の画面遷移管理方法を模索していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI & Combine オススメライブラリ3選!

はじめに 想定読者: SwiftUI / Combine を メイン にしたネイティブアプリ開発を行っている(予定な)方 執筆環境:Xcode 12.4 / macOS Big Sur 11.2.3 この記事は 寄付を身近にする dim. の開発を行う中での学びを共有する目的で執筆しております。 TechCrunch さんにも取り上げていただきましたので御覧ください。 前提 SwiftUI がメインで UIKit は極力使わない デザイナーさんのデザインが実現できていること Combine を利用し、 RxSwift などは利用しない 1. FirebaseSDK を Combine で制御 rever-ai/CombineFirebase が非常に参考になります。 主観にはなりますが、 非常にシンプルなライブラリ になっておりますので、ライブラリとして導入するのではなく必要な部分を参考に 独自実装 する方法でも良いかと思います。 Combine とは直接的な関係のないものになりますが、追加でご紹介するのが alickbass/CodableFirebase です。 レスポンスを Codable で扱いたい場合に便利です。 2. UIKit の要素にアクセスしたいと思ったら SwiftUI はこの記事執筆段階でまだ未熟です。 例えば「UIKit.UITableView.separatorStyle を .none にするみたいなことが SwiftUI.List でもしたいなぁ」などと1度は思って検索したことがあるはずです。そんな希望を叶えてくれるのが SwiftUI-Introspect/SwiftUI-Introspect です。 先程の例であれば以下のように実装可能になります。 List { Text("Item 1") Text("Item 2") } .introspectTableView { tableView in tableView.separatorStyle = .none } SwiftUI から利用できる UIKit 風 UI ライブラリなどを都度導入するよりも汎用的な SwiftUI-Introspect を利用するほうが 依存が1つで済む ので 外部依存の最小化 の点でもオススメできるのではと考えています。 また、処理が分散しにくいので、将来的に SwiftUI が成長したとき にも対応スピードが高まるのではないかと考えています。 3. Alamofire を Combine で制御(近い未来) Alamofire に依存する形で作成された Moya をご存知でしょうか。 このライブラリが遂に Combine に対応しようとしています。 Combine 発表後、 issue で多くの議論がされていましたが、 Alpha 段階まで来ているようです。正式リリースが楽しみですね! さいごに 最後までご覧いただきありがとうございます。 外部ライブラリとの依存は最小限にするためにも 慎重な判断 が必要です。参考にしていただけますと幸いです。 SwiftUI で FirebaseSDK を利用する場合、まだ未対応な機能も多く存在します。 こちらも参考にどうぞ SwiftUI ベースのプロジェクトでの FCM 実装注意点
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む