- 投稿日:2021-02-26T22:51:54+09:00
SF SymbolsのsystemNameでタイポを防ぐために....
環境
- Swift 5.3.2
実装
enum SFSymbol { case rectangleAndPencilAndEllipsis case ... var image: UIImage? { switch self { case .rectangleAndPencilAndEllipsis: return UIImage(systemName: "rectangle.and.pencil.and.ellipsis") case ... } } } let image = SFSymbol.rectangleAndPencilAndEllipsis.imageたったこれだけのことなのに、
よく分からない寄り道をしました。その作業工程で
正規表現を使わずにlowerCamelCaseをsnake_case等に変換するStringのExtension
という副産物ができたので合わせてご覧ください。経緯
SF Symbols
の画像を複数箇所で利用する場合let image = UIImage(systemName: "rectangle.and.pencil.and.ellipsis")毎回文字列指定するのはタイポ起きそうだし長いしなんだかなぁと思っていました。
過ち
その一
早速足を踏み外します。
enum SFSymbol { case rectangleAndPencilAndEllipsis case ... var name: String { switch self { case .rectangleAndPencilAndEllipsis: return "rectangle.and.pencil.and.ellipsis" case ... } } } let image = UIImage(systemName: SFSymbol.rectangleAndPencilAndEllipsis.name)この時点でなんで気づかないんですかね。
その二
微調整が入ります。
enum SFSymbol { case rectangleAndPencilAndEllipsis case ... var name: String { switch self { case .rectangleAndPencilAndEllipsis: return "rectangle.and.pencil.and.ellipsis" case ... } } } extension UIImage { convenience init?(sfSymbol: SFSymbol) { self.init(systemName: sfSymbol.name) } } let image = UIImage(sfSymbol: .rectangleAndPencilAndEllipsis)何がしたかったんですかね。
その三
過去の自分:「nameで返してる文字列も無くしてしまえばいいのでは?」
副産物が出来上がります。
extension String { var splitLowercaseStrings: [String] { guard self.includesUppercaseChar else { return [self] } let (lowercaseString, otherString) = self.dividedLowercaseStringBeforeUppercaseCharAndOther let replacedOtherString = otherString.replacedFirstCharWithLowercaseChar guard replacedOtherString.includesUppercaseChar else { return [lowercaseString, replacedOtherString] } return [[lowercaseString], replacedOtherString.splitLowercaseStrings] .flatMap { $0 } } private var includesUppercaseChar: Bool { filter { $0.isUppercase }.count != 0 } private var dividedLowercaseStringBeforeUppercaseCharAndOther: (prefixString: String, otherString: String) { guard let uppercaseCharIndex = firstIndex(where: { $0.isUppercase }) else { return (self, "") } let targetRange = startIndex..<uppercaseCharIndex let firstLowercaseString = String(self[targetRange]) var otherString = self otherString.removeSubrange(targetRange) return (firstLowercaseString, otherString) } private var replacedFirstCharWithLowercaseChar: String { var string = self let firstUppercaseChar = string.first! let firstLowercaseChar = firstUppercaseChar.lowercased() let secondIndex = string.index(after: string.startIndex) string.insert(contentsOf: firstLowercaseChar, at: secondIndex) return String(string.dropFirst()) } } enum SystemSymbol: String { case rectangleAndPencilAndEllipsis case ... } let systemImageName = SystemSymbol.rectangleAndPencilAndEllipsis.rawValue.splitLowercaseStrings.joined(separator: ".") let image = UIImage(systemName: systemImageName)地獄ですね。
最終的に
そもそも
SF Symbols
を複数箇所で利用していませんでした。余談
副産物について
let ordinalNumbers = "firstSecondThirdFourthFifthSixth" print("dot: \(ordinalNumbers.splitLowercaseStrings.joined(separator: "."))") // dot: first.second.third.fourth.fifth.sixth print("underScore: \(ordinalNumbers.splitLowercaseStrings.joined(separator: "_"))") // underScore: first_second_third_fourth_fifth_sixthlowerCamelCaseの文字列を任意の文字列で連結できます。
(※)先頭の大文字を小文字に差し替える
var replacedFirstCharWithLowercaseChar: String
では、
replacingOccurrences(of:with:)も試しましたが、
後続している大文字始まりの文字列の塊があると、その大文字も差し代わってしまうので
insert(contentsOf: at:) と dropFirst(_:)を用いて処理しています。
- 投稿日:2021-02-26T22:46:13+09:00
【Swift】 メールアドレスの形式チェック(バリデーション)の実装
TextFieldが空じゃないかつメールアドレスの形式も合っているならボタンを押せるようにする。
import UIKit class AccountViewController: UIViewController { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var SignUpButton: UIButton! override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self } // バリデーション func validateEmail(candidate: String) -> Bool { let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}" return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: candidate) } } extension ViewController: UITextFieldDelegate { func textFieldDidChangeSelection(_ textField: UITextField) { let emailIsEmpty = emailTextField.text?.isEmpty ?? true // もしTextFieldが空じゃないかつメールアドレスの形式も合っているならボタンを押せるようにする。 if emailIsEmpty && validateEmail(candidate: emailTextField.text!) { SignUpButton.isEnabled = false } else { SignUpButton.isEnabled = true } } }
- 投稿日:2021-02-26T18:33:35+09:00
【iOS14】アプリにウィジェット(WidgetKit)を導入してみた
【watnowテックカレンダー2020/2/26】
今日はwatnowのryotaが担当します
よろしくお願いしますはじめに
2020年9月にiOS14がリリースされ、その追加機能の一つとしてウィジェットという機能が利用可能になりました。
Apple公式のWidget Kitについて
Apple Introducing WidgetKitウィジェットはアプリの一部情報をiOSのホーム画面へ表示させられる機能です。この機能を用いればアプリを開かずにちょっとした情報を確認することができます。またウィジェットに任意の画面へのリンクを設定することもできユーザのアプリの利用率も向上させられます。
一方でWidgetKitを作成するにはswiftUIで実装する必要があり、なかなか導入しにくいと思う部分もあると思いますが、やってみると簡単に作成することができたので共有しようと思います。
Widget Kitを導入する
まずWidget Kitを導入するにあったってQ&Aです。(主に僕が思ってたことです)
Swiftでプロジェクトを作ってるけど、SwiftUIのWidget Kitってそもそも共存できるの?
- SwiftUIはSwift言語で使用できるUI構築フレームワークということもあり問題なく共存できます。
ただしWidget KitはiOS14以降のみの対応のため、それより以前のバージョンの端末では使用できない点に注意してください。Swiftしか触ったことないけどSwiftUIでコードを書くのはちょっと..
- Swiftを書いたことのある方にとってSwiftUIはなんとなくであれば実装しやすい言語だと思います。swiftUIではソースコードと、iPhone画面を同時に表示するプレビュー機能(名称はCanvas)があるので簡単にViewを確認できます。Widget Kitを導入する場合でもそこまで複雑な処理やUIを実装しない限り、実装するコードは少なくてすみます。
Widgetにメモリ制限とかはあるの?
- Widget Kitではホーム画面に常時設置されるという特性上、厳しいメモリ制限があるようです。トータルで最大30MB以上のデータはロードできないといった報告があるようです。似たような例としてApp extensionsを実装したこちらの記事によると、使用する機種によって使用できるメモリ量も増減するようです。いずれにせよ複雑なグラフの描画や、解像度の高いデータの取得などは行わない方が良さそうです。
- メモリ制限とかについて
10 Tips on Developing iOS 14 WidgetsもしSwiftUIを初めて使うという方はまずは、公式のチュートリアルから始めるとわかりやすいです。
Appleが掲載している公式のSwiftUIチュートリアル
* Introducing SwiftUI上記のSwiftUIチュートリアルを日本語で解説されているページもあります
* 【第1回】日本語版SwiftUIチュートリアル【基本要素を学ぶ】導入
まずはWidgetKitのデータの取り扱い方についてはこちらの記事が非常にわかりやすくまとまっているので概要を掴むとわかりやすいです。
【iOS14】Widget(WidgetKit) まとめWidget Kitを導入していきます。
プロジェクトを作成したあとに、File=>New=>Target
真ん中らへんにWidget Extensionがあるのでクリックし、Next
Product Nameを適当に決めてFinish
するとProduct Name以下にこのようなファイルが出来上がっていると思います。
それぞれの役割として
- widgetDemo.swift
- =>データやViewとかの処理をまとめて行う。追加時には時刻を表示する場合のサンプルコードが記入されている。もし独自のウェイジェットを実装していくならViewとかでファイルを分けるとわかりやすい。
- widgetDemo.intendefinition
- => メインのアプリとデータを共有したりするときにここで設定する
- Assets.xcassets
- => 静的な画像データとかはここにおく
- info.plist
- => Widgetの設定プロパティ
以下のようにWidgetExtensionのViewが表示されています。
この状態で アプリのビルドを行い、ホーム画面を長押し=>widgetを追加する
これでホーム画面に現在の時間が表示されるウィジェットが追加されます。時計を表示させるだけなら実はこれだけで実装となります。
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 11.59.45.pngAPIからデータをとってきて表示させる
ここまでで、すでにホーム画面にWidgetを追加して、時間を表示させることができました。
ここをベースに開発していくことでアプリ側からデータを受け取って表示させたりサーバからデータを取得して表示させたりすることができます。
では今回はAPIからデータをとってきて表示させるWidgetを開発しようと思います。完成はこんな感じです。今回はNewsAPIからJSONで技術系のニュースデータを取得してその1つめのデータをWidgetに追加するようにします。
まずはニュースを取得するためにNewsAPIを利用するのでそのAPIKeyを取得します。News系のAPIで探してみるとこれが多く使われてるみたいです。APIKeyを取得したら以下のようにGETするとレスポンスがかえってきます。
http://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=APIキーWidgetKitがHTTP通信を行うことができるようにinfo.plistを編集してあげる必要があります。HTTPS通信が必須化しているためHTTP通信を行うには許可する必要があります。
今回編集するのはアプリのinfo.plistではなく、Widgetのフォルダに作成されたinfo.plistを編集する必要があることに注意してください。
HTTP通信の許可する方法についてはこちらが参考になります。
iOSアプリのhttp通信を許可する方法まずはCodableなモデルを作成していきます。
JSONからモデルを作成する時は、Convert JSON into gorgeousが便利です。JSONを入力するだけでいい感じにモデルを作ってくれます。import WidgetKit import SwiftUI import Intents import Foundation // MARK: - News struct News: Codable { let status: String? let totalResults: Int? let articles: [Article]? } // MARK: - Article struct Article: Codable { let source: Source? let author, title, articleDescription: String? let url: String? let urlToImage: String? let publishedAt: String? let content: String? enum CodingKeys: String, CodingKey { case source, author, title case articleDescription = "description" case url, urlToImage, publishedAt, content } } // MARK: - Source struct Source: Codable { let id: ID? let name: Name? } enum ID: String, Codable { case techcrunch = "techcrunch" } enum Name: String, Codable { case techCrunch = "TechCrunch" }次はWidgetKitのメインとなる部分です。今回は最初からあるSimpleEntryにnewsDataの項目を追加しています。取得したデータからTimelineEntryを作成し、それをもとにTimelineを構成していくイメージです。
import WidgetKit import SwiftUI import Intents struct Provider: IntentTimelineProvider { @ObservedObject var NewsStore = SessionStore() func placeholder(in context: Context) -> SimpleEntry { #Widgetが読み込み中の時に呼ばれる SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: [] as? News) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { #Widgetを追加しようとするときに例として表示される #ダミーのデータをいれるとよい let entry = SimpleEntry(date: Date(), configuration: configuration, newsData: [] as? News) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] /// 1時間ごとに更新する let refresh = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date() NewsStore.fetch{ newData in entries.append(SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: newData)) let timeline = Timeline(entries: entries, policy: .after(refresh)) completion(timeline) } } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent let newsData:News? } struct newsWidgetEntryView : View { var entry: Provider.Entry var body: some View { VStack{ URLImageView(url: URL(string: entry.newsData?.articles?[0].urlToImage ?? "")!) .aspectRatio(contentMode: .fill) Text(entry.newsData?.articles?[0].title ?? "") .foregroundColor(.black) .font(.headline) .padding(8) } } } @main struct newsWidget: Widget { let kind: String = "newsWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in newsWidgetEntryView(entry: entry) } .configurationDisplayName("News Widget") .description("News Widgetの説明") #大きなwidgetのみ対応する .supportedFamilies([.systemLarge]) } } struct newsWidget_Previews: PreviewProvider { static var previews: some View { newsWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: [] as? News)) .previewContext(WidgetPreviewContext(family: .systemSmall)) } }データを取得するクラスはこんな感じで実装しています。
import Foundation import Combine final class SessionStore: ObservableObject { @Published var current : News? init(){ self.fetch{ val in print(val) } } } extension SessionStore{ func fetch(completion : @escaping(News)->()){ NewsClient.fetchSummary { self.current = $0 completion($0) } } }最後にURLから画像を読み込む処理についてです。
Swiftで非同期で画像を取得するときはKing Fisherとかが便利なんですが、今回はSwiftUIで実装するということもあり処理を別途書く必要があります。import SwiftUI struct URLImageView: View { let url: URL @ViewBuilder var body: some View { if let data = try? Data(contentsOf: url), let uiImage = UIImage(data: data) { Image(uiImage: uiImage) .resizable() } else { Image(systemName: "photo") .resizable() } } }以上で、Widgetの実装は終わりになります。
これでWidgetを追加すると最新のニュースが表示されます。今回使用したサンプルプロジェクトは以下になります。
https://github.com/ryota2425/widgetExample最後に
WidgetKitはSwiftUIでの実装で難しそうに見えますが、ぜひチャレンジしてみてください。
今回はAPIの情報を表示させるだけでしたが、これを発展させてクリックした場にアプリで記事を表示させるとより実用的になっていくかと思います!
- 投稿日:2021-02-26T17:55:42+09:00
Provisioning profile "iOS Team Provisioning Profile: Hoge" doesn't support the On Demand Install Capable capability. の対処法
AppClipsを実機ビルドしようとするとこのようなエラーが発生しました。
Provisioning profile "iOS Team Provisioning Profile: Hoge" doesn't support the On Demand Install Capable capability.調べてみると同様の症状が発生している人は多く見かけましたが、解決に至っている人を見つけられませんでした。
解決方法
大前提としてApple Developer Programに加入している必要があります。
公式ドキュメントでその様な記述は見つけられませんでしたが、無料アカウントだと実機ビルドでできない様です。
1.まずはじめに、下記リンクよりApp ClipのIdentifierを作成します。
2.次に画像のような手順で、親アプリのIDを選択し、AppClip用のIDを作成します。
3.Identifierが作成完了すると、Signingのエラーが消えてビルドできる様になります。
最後に
Apple Developer Programに加入していないと実機でのビルドができないので、
AppClipを試そうとしている方は気をつけてください
ではまた?
- 投稿日:2021-02-26T17:55:42+09:00
【AppClips】Provisioning profile "iOS Team Provisioning Profile: Hoge" doesn't support the On Demand Install Capable capability. の対処法
AppClipsを実機ビルドしようとするとこのようなエラーが発生しました。
Provisioning profile "iOS Team Provisioning Profile: Hoge" doesn't support the On Demand Install Capable capability.調べてみると同様の症状が発生している人は多く見かけましたが、解決に至っている人を見つけられませんでした。
解決方法
大前提としてApple Developer Programに加入している必要があります。
公式ドキュメントでその様な記述は見つけられませんでしたが、無料アカウントだと実機ビルドでできない様です。
1.まずはじめに、下記リンクよりApp ClipのIdentifierを作成します。
2.次に画像のような手順で、親アプリのIDを選択し、AppClip用のIDを作成します。
作成する際に、On Demand Install Capableにチェックがついているか確認してください。
3.Identifierが作成完了すると、Signingのエラーが消えてビルドできる様になります。
最後に
Apple Developer Programに加入していないと実機でのビルドができないので、
AppClipを試そうとしている方は気をつけてください
ではまた?
- 投稿日:2021-02-26T16:10:37+09:00
[Swift/FirebaseAuth] 一度ログインしたらログイン画面をスキップする方法
Firebase Authentication を使ってユーザーの認証を行うアプリを作る際、一度新規登録・ログインをしたら次アプリを開くときログイン画面をスキップしてくれるコードを紹介します。
この記事ではすでに FirebaseAuth を使って新規登録・ログイン画面と処理が完成してることを前提とします。これらのやり方は Firebase Official Docが丁寧に説明してくれてるので参考にしてください!
Code
この処理は
SceneDelegate.swift
で行います。
まず FirebaseAuth ライブラリをインポートします。SceneDelegate.swiftimport FirebaseAuthSceneDelegate クラスの中に以下のメソッドを書きます。
SceneDelegate.swiftfunc skipLogin() { //使ってるストーリーボード(何もいじってない限り ファイル名は"Main.storyboard" なので "Main" と記入。 let storyboard = UIStoryboard(name: "Main", bundle: nil) //ログイン後に飛びたいストーリボード。Identifier は Main.storyboard で設定。 let homeViewController = storyboard.instantiateViewController(identifier: "HomeVC") //rootViewController (初期画面)を homeViewController にする。 window?.rootViewController = homeViewController //画面を表示。 window?.makeKeyAndVisible() }
skipLogin()
メソッドが書き終わったらあとは次のメソッドの中に呼ぶだけです!SceneDelegate.swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } -----ここから付け足す----- //もし一度ログインしたユーザーだったら skipLogin() を呼ぶ。 if Auth.auth().currentUser != nil { skipLogin() } //rootController がデフォルトで新規登録・ログイン画面についていれば else文はいらない。 }これで、一度ログインしたら次からログイン画面をスキップできるようになります!
- 投稿日:2021-02-26T16:07:32+09:00
Cloud Firestoreについてまとめる
はじめに
備忘録として、Firebase Cloud Firestore(以下、Firestore)についてまとめていきます。
そもそもFirebaseが何なのか?というのは今回は割愛します。
Firestoreを知る前に
データベースには大きく分類すると、
RDBMS
とNoSQL
に分けることができ、前者はSQL
というコンピューター言語を使用して、データの書き込み・読み取りを行います。対して、後者の
NoSQL
は文字通り、SQL言語を使用せずにデータの書き込み・読み取りを行っています
。主な違いは管理方法にあり、RDBMSは
1つのキーに対して複数の付随するデータを管理
しているのに対し、NoSQLは1つのキーに対して1つの付随するデータを管理
しています。・RDBMS 「1キー:複数の付随データ」
・NoSQL 「1キー:1つの付随データ」
Firestoreでは
NoSQLでデータが管理
されており、クラウドデータベース
になっています。クラウドデータベースとは
手元にサーバーやソフト等の環境を持たずにデータベースを利用することができるサービス
の事です。これはデータベースに限った話ではなく、手元にサーバーやソフトなどの環境がなくても利用できるサービスは
クラウドサービス
としてまとめられます。この主な例として他にも
Gmail
やiCloud
などがあり、Firebase
も該当します。どれも利用する側は環境構築を1からしなくても、インターネットに繋がっていればサービスを利用することが出来ます。
Firestoreとは
Googleが提供している
NoSQLクラウドデータベース
です。
Cloud Firestore | Firebase公式ドキュメントを見てもらうと分かるようにFirestoreでは
コレクション
、ドキュメント
等の用語が登場します。・ドキュメント
あらゆるデータを長期間保存する場所
です。例えば、
humanドキュメント
が作られた場合は下記のようになります。?human(ドキュメント) first(フィールド): "Larry"(値) last: "Page" born: 1973各ドキュメントには
キー(フィールド)
と値
のペアが含まれており、ドキュメントID
で識別されています。ドキュメントIDは
重複
せず、通常は追加と同時にランダムなIDが付与され、ユーザーIDなどの独自のキー等を自分で指定することも可能
です。更に、ドキュメント内のデータを
マップ(ドキュメント内で複雑にネストされたオブジェクト)で構造化
することも出来ます。?human name: first: "Larry" last: "Page" born: 1973ドキュメントは
JSON
に似ており、公式もJSONと基本的には同じ
と公言しています。・コレクション
複数のドキュメントをまとめるフォルダみたいなもの
です。仕組みとしては
データをドキュメントに書き込み、そのドキュメントをコレクションにまとめる
といった感じです。公式ドキュメントの以下の画像がわかりやすいと思います。
(出典: Cloud Firestore データモデル | Firebase)例えば、
foodコレクション
を作成して、様々な食べ物を表すドキュメントを格納した場合このようになります。?food(コレクション) ?meet(ドキュメント) kind(フィールド): "ChickenMeat"(文字列型の値) Id: 12345678(数値型の値) ?fruit kind: "Apple" Id: 87654321Firestoreでは、Bool値、数値、文字列、タイムスタンプなど、様々なデータ型の値がサポートされています。
コレクションは、自分から
作成・削除をする必要がなく
、最初のドキュメントを追加した時に勝手に作られ、コレクション内のドキュメントを全て削除すると自動的にコレクションも削除されます。そして、コレクションには
ドキュメントのみしか含めることができません
。なので、コレクションに対して生のフィールド
や他のコレクションを含める
ことは許されていません。・リファレンス
データベース内の場所を参照するだけの軽量なオブジェクト
です。先ほどのfoodコレクション内のドキュメントを参照してみましょう。
?food ?meet kind: "ChickenMeat" Id: 12345678 ?fruit kind: "Apple" Id: 87654321let foodDocumentRef = Firestore.firestore().collection("food").document("meet")これで、コレクション内のドキュメントを参照することが出来ます。
もちろん、コレクションへのリファレンスも作成可能です。
let foodDocumentRef = Firestore.firestore().collection("food")
コレクションリファレンス
とドキュメントリファレンス
は2種類の異なるもの
であり、それぞれ別の操作が可能です。例えば、コレクションリファレンスを使用して
コレクション内のドキュメントに対するクエリを実行
したり、ドキュメントリファレンスを使用して個々のドキュメントを読み書き
したりできます。ドキュメントまたはコレクションへのパスを文字列として指定し、
スラッシュ(/)
で区切ってリファレンスを作成することもできます。let meetDocumentRef = Firestore.firestore().document("food/meet")・サブコレクション
例えば、メッセージとチャットルームを使ったチャットアプリがあったとします。
下記のようなroomsコレクションがあって、ここに
メッセージを保存する場所
を指定しなくてはいけません。ですが、Firestoreのドキュメントは軽量
にする必要があり、膨大な数のメッセージを格納するのは厳しいようになっています。?rooms ?roomA name: "my Room" ?roomB name: "dog talk" . . .そういう場合には
サブコレクション
を使用します。
これは、特定のドキュメントに関連付けられたコレクション
です。roomsコレクション内の全てのルームドキュメントに
messageサブコレクション
を作成してみましょう。?rooms ?roomA name: "my Room" ?message(サブコレクション) ?message1(ドキュメント) from: "taka" msg: "Hello World!" ?messsage2 from: "rio" msg: "Hi!" ?roomB . . .次のコードを使用してサブコレクション内のメッセージへの
リファレンス
も作成可能です。let messageRef = Firestore.firestore() .collection("rooms").document("roomA") .collection("message").document("message1")更に
サブコレクション内のドキュメントもサブコレクションを格納
できるため、深くネストできます。最大100レベル
までネスト可能です。しかし、注意すべき点が2つあります。
①コレクションとドキュメントが常に交互になるようにしなくてはいけない
なので、コレクション内のコレクションやドキュメント内のドキュメントは
参照できません
。②ドキュメントを削除しても、その中のサブコレクションは削除できない
例えば、親ドキュメントを削除する時にサブコレクション内のドキュメントも削除する場合は手動で削除する必要があります。
おわりに
Firestoreについてまとめました。
今回よく分かったことは困ったら公式ドキュメントを見た方が良いという事です。
次は、Firestoreのセキュリティルールについてまとめようと思います。
- 投稿日:2021-02-26T15:41:07+09:00
CI(Bitrise * Fastlane)でSwift Package Managerをキャッシュする
一部のCocoaPodsをSwiftPMに移行した際に、
いろいろ調べたのでToDoを書いておきます前提
CocoaPodsからの移行はXcodeGen導入済みProjectにて行いました。
XcodeGen導入済み環境でSwiftPMを導入する方法についてはこちらやること
- Package.resolvedをgit管理下に置く
- Bitriseに環境変数を登録
- BitriseのCache Push Stepを定義
- Fastlaneのlane内でキャッシュを利用
Package.resolvedをgit管理下に置く
プロジェクトファイルをgitから除外している場合、
Package.resolvedをgit管理下に置きますgitignore# XcodeGen *.xcodeproj *.xcworkspace # Cache SwiftPM for CI !*.xcworkspace/xcshareddata/swiftpm/Package.resolvedBitriseに環境変数を登録
後述しますが、
BitriseのCache Push Stepに下記記述を追加することで、指定場所にcacheを保存することが可能です。
{cache保存先} -> {Package.resolvedのPath}
今回は、cache保存先を示す環境変数
BITRISE_SWIFTPM_CACHE_PATH
を、
Package.resolvedのpathを環境変数SWIFTPM_PACKAGE_RESOLVED_PATH
を作成します。BitriseのCache Push Stepを定義
BitriseのCache Push Stepの
cache paths
に前述した通り
{cache保存先} -> {Package.resolvedのPath}
を追加します。これにより、SwiftPMのcacheがpushされるようになります。
Fastlaneのlane内でキャッシュを利用
gymの
cloned_source_packages_path
にBITRISE_SWIFTPM_CACHE_PATH
を渡すことで、
SwiftPMのcacheを利用します。fastfilegym( workspace: "MyApp.xcworkspace", scheme: "MyApp", configuration: "Debug", export_method: "ad-hoc", cloned_source_packages_path: ENV["BITRISE_SWIFTPM_CACHE_PATH"], )キャッシュの確認方法
BitriseのSetting -> Manage Build Caches (BETA) からファイルをダウンロードして、
内部にSwiftPMのファイル群があればokです
- 投稿日:2021-02-26T14:55:55+09:00
【SwiftUI】NavigationViewの基本的な使い方(前編)
前回に引き続き、アプリに使用した技術をアウトプットしていきます。
解説する私のアプリはこちら。
わかりすと -勉強サポートアプリ-NavigationView
階層的な画面遷移を管理するビュー。
NavigationLink
NavigationView内で、画面遷移を行うトリガーになるビューと遷移先のビューの両方を指定できるビュー。NavigationViewとセットで使う。
実際の動作
※拡大推奨
動画のポイント
- NavigationViewの外側の領域は切り替わらず、遷移先にも表示される。(動画内のヘルプボタン)
- 遷移先の画面の左上には、前の画面にもどるためのボタンが表示される。
- もどるボタンにはタイトルがそのまま書かれる。
コード
今回作成したサンプルアプリのソースコードはこちら
まず、一番上の階層から見ていきましょう。
ContentView.swiftimport SwiftUI struct ContentView: View { let regions = ["カントー地方", "ジョウト地方", "ホウエン地方", "シンオウ地方"] var body: some View { VStack{ // ここから HStack{ Spacer() Button(action: {}, label: { Text("ヘルプ") }) } .padding(.horizontal) // ここまではNavigationViewに含まれない //ここから NavigationView { List(0..<4) { n in NavigationLink(regions[n], destination: Region(id: n, regionName: regions[n])) } .navigationTitle("そらをとぶ") } //ここまでがNavigationViewの領域 } } }基本として、遷移する(切り替わる)のはNavigationViewの{}内の要素だけです。上のソースコードの「ヘルプ」というラベルのボタンはNavigationViewに含まれないため、リンクをタップしても切り替わらずに画面上部に残っています(動画を確認してください)。今回の「ヘルプ」ボタンのように、何らかの理由で遷移先にもずっと表示させたいものがある場合はNavigationViewに含めない方が良いでしょう。
次にNavigationViewの中を見ていきます。
NavigationViewの中にはNavigationLinkが書かれており、Listの繰り返し処理で4つ並ぶようにしています。上で説明したとおり、NavigationLinkは画面遷移のトリガーと遷移先のビューを指定できます。今回は
regions[n]
(配列regionsのn番目の要素)がトリガー、Regionビュー
が遷移先となっています。Regionビューのソースコードはこちら。
Region.swiftimport SwiftUI struct Region: View { let id: Int //地方によって表示を変えるためのプロパティ let regionName: String //地方の名前をタイトルにするためのプロパティ var body: some View { //NavigationViewは書かない List(0..<4) { n in NavigationLink(regionsArray[id][n], destination: Flight(city: regionsArray[id][n])) .navigationTitle(regionName) } } } //各地方ごとのデータ let kanto = ["マサラタウン", "トキワシティ", "ニビシティ", "ハナダシティ"] let joto = ["ワカバタウン", "ヨシノシティ", "キキョウシティ", "ヒワダタウン"] let hoen = ["ミシロタウン", "コトキタウン", "トウカシティ", "カナズミシティ"] let shino = ["フタバタウン", "マサゴタウン", "クロガネシティ", "コトブキシティ"] //idに対応する配列を取り出すための配列 let regionsArray = [kanto, joto, hoen, shino]ここで見てほしいのは、NavigationLinkはあるのにNavigationViewの記述がどこにも書かれていないことです。NavigationViewは一番上の階層となるビューに書くだけで、遷移先にも機能を持たせることができます。
逆に、下の階層にもNavigationViewを書き、次の画面に遷移するとこのようになります。
画面左上に、前の画面にもどるためのボタンが2つ並んでいることがわかると思います。これはNavigationViewの中にNavigationViewを作っていると判断された結果です。このような表示にしたい場合は別ですが、基本は一番上の階層にのみNavigationViewを書くことをおすすめします。
次回はnavigationTitleについて書きます。
参考文献
SwiftUI徹底入門 金田浩明(著)(【注意】Xcode12非対応のものです)
- 投稿日:2021-02-26T14:54:29+09:00
PoCでiOSアーキテクチャにMVPを採用した話
TL;DR
PoC(検証)のための新規iOSアプリ開発で、iOSアプリ開発初心者がアーキテクチャにMVPを採用した理由と、MVPを実装するする上で気づいたことを共有するものです。
結論
- アーキテクチャの検討に当たっては、①開発要件に合うものの中で、②経験があるor経験がなくても最も理解しやすそうな、設計パターンをサクッと決定するのがよさそうです
- MVPのアーキテクチャパターンにおいては、プレゼンテーションロジックの分離が重要で、その際に活用できるのがProtocol
この記事の効能
- 新規のiOSアプリ開発でアーキテクチャを決定する一助になるかもしれません
- MVPの実装において筆者が大切だと思った、「プレゼンテーションロジックの分離」について理解が深められます
MVPの選定
手順
今回はデザイナーさんが作成してくださったワイヤーフレームがあったのでそれを参考にしながら行いました
1. 開発する画面を列挙する
2. 各画面や画面間で管理する状態を書き出す
3. 上記のステップでまとめた開発要件を元に設計パターンを考える(例)画面・状態を書き出している様子- 管理すべき状態 - 画面をまたぐもの - セッション(ログイン)情報 - サインアップ画面 - IDとパスワードが不正の状態(validationに利用) - カート画面 - カート内のアイテム - 合計金額 etc.今回の前提条件
a. ページ間にまたがる状態はログインの有無だけで、状態管理が複雑でない
b. 検証用のiOSアプリで1+α人程度での小規模な開発
c. 開発者はiOSアプリ開発初心者選択肢
MVC, MVP, MVVM, Flux, Redux
上記の全てについて一通り把握し検討を行いましたが、前提条件のaにあるように複雑な状態管理がないことから、MVCとMVPに絞りました。また他の設計パターンでは、導入に当たって外部のライブラリを使用すべきであったことも選定対象から外れた理由となりました。(例:MVVMにおけるRxSwiftなど)
MVCとMVPの2つの中から選定した理由としては、①決済などテストすべき項目を扱うためテストしやすい方がいい、②MVCから発展したものだからMVPの方が使いやすい(はず)になります。アーキテクチャパターン選定のまとめ
結構時間をかけて真剣に設計パターンの検討を行いましたが、後から見返すとやってみないと分からないことも多いなと思います。また、実際に開発を進めて考えていく中でやっと理解が進むものもあります。結論として、アーキテクチャの検討に当たっては、①開発要件に合うものの中で、②経験があるor経験がなくても最も理解しやすそうな、設計パターンをサクッと決定してしまうことが良さそうかなと思っています。(BFFの設計も考えたことがあり、その経験も踏まえて。)
今回に関して言うと、ページをまたがる複雑な状態管理が必要なく、少人数での開発という開発要件で、比較的単純な(だと感じた)MVPを採用しました。MVPにおけるプレゼンテーションロジック分離の重要性
MVPについて
MVPの中にも、データの流れによってPassive ViewとSupervising Controllerという2つの方式があります。
Passive ViewはViewとModelのデータのやり取りにおいて必ずPresenterを経由するもので、データの流れが理解しやすく、全てのプレゼンテーションロジックのテストが可能です。一方でSupervising Controllerは、Model→Viewの流れにおいて、更新の通知により変更を知らせることができ、Presenterを挟まない分冗長なデータの流れを回避することができます。今回は、テストが容易な点と開発の簡潔な点を重視してPassive Viewを採用しました。
それぞれの詳しい図解と解説は以下の記事が分かりやすかったです。(ただ、Supervising Controllerについては以下の記事ではData bindingになっていますが、参考文献のようにオブザーバー同期を使うものもあります)
StackOverFlowの「MVPとMVCの違い」についての回答を読んでみたPassive Viewにおける各コンポーネントの役割
コンポーネント 役割 Model コマンドを受けて自身を更新(Presenterからのみアクセスされ、Viewとは直接の関わりをもちません) View ユーザーの入力を受け、Presenterに伝える(Presenterからの描画指示に従うだけで、完全に受け身な立ち位置になります) Presenter ViewからのコマンドをModelに送る・Modelからの変更を受け取り、Viewを更新(すべてのプレゼンテーションロジックを受けもちます) (参考文献より)
補足:プレゼンテーションロジックとは
「View(ユーザーの入力など)やModel(データの変更など)の変更から、画面の更新・遷移を行うロジックのこと」Protocolの活用によるプレゼンテーションロジックの分離
実装している中で、プレゼンテーションロジックがViewに紛れていることに気付きました。今回はiOSアプリでよく用いられるViewControllerというファイルにViewの役割を持たせたのですが、ViewControllerのファイルの中に直接プレゼンテーションロジックを書き込んでいたのです。これはひとえに、各コンポーネントの役割を完全に理解できていなかったことが原因であると考えます。そこで以下の記事を見つけ、Protocolの活用法に気づくと共に、リファクタを行なってプレゼンテーションロジックをViewControllerから取り除きました。
MVP VS MVVM in iOS using swift実装に当たっては、上記の記事よりもPresenterの役割が分かりやすいコードがあったのでそちらを参考にしました。
以下が参考にしたコードです。(参考文献で扱われているコードです。)
ポイントは、protocolの定義において、ユーザーからの入力(SearchUserPresenterInput)と、画面更新・遷移(SearchUserPresenterOutput)を分けて行うことで、それぞれの役割を明確にしている部分です。
MVPにおいてPresenterを実装するということ
簡単なことではありますが、気づくのに時間がかかったポイントを挙げます。
「全てのプレゼンテーションロジックを受けもつPresenterを実装する」ということは、
- プレゼンテーションロジックは全てPresenterから呼び出されるべき(=Viewが呼び出すことはなく、また当然Viewに直接プレゼンテーションロジックを記述することもない)
- ユーザーからの入力はInputProtocol、画面の描画はOutputProtocolで定義し、OutputProtocolの実装自体はViewControllerで継承して行うのがよい
ということであると理解しました。
(追記)この記事にいただいたご質問・ご意見
MVPは難しくないの?
参考文献では、MVCの責務を再分割したもの(MVCの三要素を60°回転したもの)であると言及されていたため、そこまで複雑なものではないと考えていました。
また以下の記事でも、MVVMやReduxと比べれば、規模を小さく開発できると考えられます。
手を動かしてMVPパターンを体験する自分としては、記事で説明したようにPresenterを正しく理解できないなどのポイントはあるものの、小規模開発にも適したパターンであると考えます。今回、iOSアプリ開発が初めてであったことを考えると、MVC(Cocoa MVC)の習得にも学習コストは存在するので、MVPとそれほど差異はないのではないかと考えています。
MVCとの違いは?
MVCに対するMVPの利点として挙げられるのは、テストの容易性と作業分担のしやすさになります。これはPresenterという役割を作り、プレゼンテーションロジックを取り出したことによるものかと思われます。
特にPassive Viewの場合は、データの流れが単一なので実装に迷うことがないというメリットもあると、実装する中で感じました。AndroidにもMVPはある?
あるようです。
Architecture of Android Appsまた、Androidには公式のアーキテクチャガイドがあるようで、素敵だなと思いました。
アプリ アーキテクチャ ガイド参考
iOSアプリ設計パターン入門
iOSアプリ開発に限らず、設計パターンを学びたい方に損得抜きで強くおすすめです。歴史的経緯も含めた設計パターンの丁寧な解説、さらに各設計パターンのサンプルコードで構成されています。
- 投稿日:2021-02-26T14:51:24+09:00
トリガーをボタンにしたピンクの音色再生その2
7日目のアプリ
トリガーをボタンにした音声再生その2
画面キャプチャ
以下の流れで作りました。
- storyboadに複数ボタンを配置。
- 上記要素をViewController.swiftへoptionドラッグして紐付ける
- 紐付けができたら、ViewController.swiftでコードを書く
できたこと
- 複数のボタンをトリガーにした音声ファイルの再生方法がわかった。
- ボタンのcurrentTitleを取得して、音声ファイル名に紐付けて再生する方法がわかった。 ##書いたコードを共有します!
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } var player: AVAudioPlayer! func playSound(pressedKey: String) { let url = Bundle.main.url(forResource: pressedKey, withExtension: "wav") player = try! AVAudioPlayer(contentsOf: url!) player.play() } @IBAction func keyPressed(_ sender: UIButton) { playSound(pressedKey: sender.currentTitle!) } }感想
func()のかっこ()の中は、変数的なものと、その変数的なものがIntなのかStringなのかみたいな型を記載すること。
例: func playSound(pressedKey: String)
で、
関数を呼び出すときに、()の中に、引数を入れること
例: playSound(pressedKey: sender.currentTitle!)
ふむむ。
- 投稿日:2021-02-26T14:51:24+09:00
7日目トリガーをボタンにしたピンクの音色再生その2
7日目のアプリ
トリガーをボタンにした音声再生その2
画面キャプチャ
以下の流れで作りました。
- storyboadに複数ボタンを配置。
- 上記要素をViewController.swiftへoptionドラッグして紐付ける
- 紐付けができたら、ViewController.swiftでコードを書く
できたこと
- 複数のボタンをトリガーにした音声ファイルの再生方法がわかった。
- ボタンのcurrentTitleを取得して、音声ファイル名に紐付けて再生する方法がわかった。 ##書いたコードを共有します!
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } var player: AVAudioPlayer! func playSound(pressedKey: String) { let url = Bundle.main.url(forResource: pressedKey, withExtension: "wav") player = try! AVAudioPlayer(contentsOf: url!) player.play() } @IBAction func keyPressed(_ sender: UIButton) { playSound(pressedKey: sender.currentTitle!) } }感想
func()のかっこ()の中は、変数的なものと、その変数的なものがIntなのかStringなのかみたいな型を記載すること。
例: func playSound(pressedKey: String)
で、
関数を呼び出すときに、()の中に、引数を入れること
例: playSound(pressedKey: sender.currentTitle!)
ふむむ。
- 投稿日:2021-02-26T14:03:12+09:00
unable to build chain to self-signed root for signerの解決方法
平穏な開発ライフを過ごしていた、ある日突然それは訪れました?
Warning: unable to build chain to self-signed root for signer "Apple Development: HogeHoge (0000000000)" /Users/Hoge/Library/Developer/Xcode/DerivedData/hoge-app-hogehogehogehoge/Build/Products/Debug-iphoneos/hoge-app.app: errSecInternalComponent当時の俺「そんなに時間掛からないかな?」
この時私はこのエラーに一ヶ月弱悩まされることをまだ知らなかった...
試してできなかった方法
試したこと 結果 再起動 何回再起動してもだめだった キーチェーンのリセット バックアップをとって証明書を再度インストールしたけどだめだった。 Xcode再インストール Xcode関連ファイルを全て削除した上で再インストールしたけどだめだった。 キーチェーンのロックとロック解除繰り返し stackoverflowで見かけた方法だけどだめだった。 キーチェーンのロック解除コマンド これも同様にだめだった。 キーチェーンをロックしてビルド ビルド時にポップアップが出て許諾してもだめだった。 証明書信頼設定をシステムデフォルトに変更 これは最初からシステムデフォルトだった。 証明書の秘密鍵のアクセス制御に、codesignとxcodeを追加 これも追加したがダメだった。追加されてない場合は許可ダイアログが出るので関係なさそう。 OSのアップデート BigSurにしたらわんちゃん治るかなって思ったけど無理だった。 プロビジョニングファイル削除→再度インポート これも意味なかったみたい。 carthageのキャッシュ削除からの再ビルド carthageのライブラリの署名のところでエラーが出るからわんちゃんって思ったけど関係なさそうだった。 解決方法
https://developer.apple.com/account/resources/certificates/list
上記のリンクから既存のDevelopmentやDistributionのCertificatesを削除後に、新規に作成しなおします。
プロビジョニングファイルも同様に作成しなおしてください。
この方法であっさりと解決できてしまいました。最後に
codesignコマンドで手動署名する際、sudoしたら正常に署名できましたが、sudo無しだと上記のエラーが発生しました。
このことから、今回のエラーは既存の証明書の権限周りに不具合が出たと考えています。
同様のエラーが発生した同志の助けになることを願っています。
ではまた?
- 投稿日:2021-02-26T14:03:12+09:00
【Xcode】unable to build chain to self-signed root for signerの解決方法
平穏な開発ライフを過ごしていた、ある日突然それは訪れました?
Warning: unable to build chain to self-signed root for signer "Apple Development: HogeHoge (0000000000)" /Users/Hoge/Library/Developer/Xcode/DerivedData/hoge-app-hogehogehogehoge/Build/Products/Debug-iphoneos/hoge-app.app: errSecInternalComponent当時の俺「そんなに時間掛からないかな?」
この時私はこのエラーに一ヶ月弱悩まされることをまだ知らなかった...
試してできなかった方法
試したこと 結果 再起動 何回再起動してもだめだった キーチェーンのリセット バックアップをとって証明書を再度インストールしたけどだめだった。 Xcode再インストール Xcode関連ファイルを全て削除した上で再インストールしたけどだめだった。 キーチェーンのロックとロック解除繰り返し stackoverflowで見かけた方法だけどだめだった。 キーチェーンのロック解除コマンド これも同様にだめだった。 キーチェーンをロックしてビルド ビルド時にポップアップが出て許諾してもだめだった。 証明書信頼設定をシステムデフォルトに変更 これは最初からシステムデフォルトだった。 証明書の秘密鍵のアクセス制御に、codesignとxcodeを追加 これも追加したがダメだった。追加されてない場合は許可ダイアログが出るので関係なさそう。 OSのアップデート BigSurにしたらわんちゃん治るかなって思ったけど無理だった。 プロビジョニングファイル削除→再度インポート これも意味なかったみたい。 carthageのキャッシュ削除からの再ビルド carthageのライブラリの署名のところでエラーが出るからわんちゃんって思ったけど関係なさそうだった。 解決方法
https://developer.apple.com/account/resources/certificates/list
上記のリンクから既存のDevelopmentやDistributionのCertificatesを削除後に、新規に作成しなおします。
プロビジョニングファイルも同様に作成しなおしてください。
この方法であっさりと解決できてしまいました。最後に
codesignコマンドで手動署名する際、sudoしたら正常に署名できましたが、sudo無しだと上記のエラーが発生しました。
このことから、今回のエラーは既存の証明書の権限周りに不具合が出たと考えています。
同様のエラーが発生した同志の助けになることを願っています。
ではまた?
- 投稿日:2021-02-26T11:54:07+09:00
?yawacom - ?画面を作っていく
2021/02/26現在編集中
ログイン画面の作成
1つ前の記事でユーザ登録画面を作った要領でログイン画面も作っていきます.
下記のような流れになるように作っていきます.
今回はおおまかに3つ
?テキストボックスとボタンで構成されるログイン画面を作る
?ログイン画面から新規登録画面に遷移するボタンがある
?ログインしたらやわらかさを記入する画面に遷移する
これらの動きを持つ画面を作ります!?テキストボックスとボタンで構成されるログイン画面を作る
まずはユーザ名・パスワードを打つためのテキストボックス2つとログインボタンを作ります.一つ前の記事の要領でとりあえずviewDidLoadにぶちこんでます??♂️
またtextFieldShouldReturnを使用して,テキストボックスに文字を入力して改行を押したらキーボードが隠れるようにします.これをしないとユーザー名などを打ち込んでもキーボードがしまえずログインボタンが押せないハメになります.LoginViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() print("ログインビューコントローラー") self.viewModel = LoginViewModel() self.view.backgroundColor = .white self.userName.backgroundColor = UIColor(named: "textbox") self.password.backgroundColor = UIColor(named: "textbox") self.loginButton.backgroundColor = .systemGray self.loginButton.tintColor = .white self.loginButton.layer.cornerRadius = 10 // textFiel の情報を受け取るための delegate を設定するとtextFieldShouldReturnとかが呼ばれる self.userName.delegate = self self.password.delegate = self } func textFieldShouldReturn(_ textField: UITextField) -> Bool { // キーボードを閉じる textField.resignFirstResponder() return true }?ログイン画面から新規登録画面に遷移する
ログインボタンの下にある『新規登録する』(registraionButton)を押すと新規登録画面に遷移するようにします.
流れとしては
1. LoginViewController で registraionButton の input(onTapRegistraionButton) を作ってTap判定する
2. LoginViewModel で input の registraionButton を showRegistraionView にいれる
3. LoginViewModel の output に showRegistraionView をいれる
4. LoginViewController の output.showRegistraionView で遷移先のVCをstartする
のようになります.1. LoginViewController で registraionButton の input(onTapRegistraionButton) を作ってTap判定する
とりあえずLoginViewModelのファイルだけ生成します.ファイルの新規作成をして中身は下記のように用意しておきます.
LoginViewModel.swiftimport Foundation import RxSwift import RxCocoa class LoginViewModel: ViewModelType { private let bag = DisposeBag() func transform(input: Input) -> Output { } struct Input { let onTapRegistraionButton: Signal<Void> // onTapRegistraionButtonをTapしたかをいれる } struct Output { let showRegistraionView: Driver<Void> // 新規登録画面に遷移するかどうかをいれる } }次にLoginViewControllerでLoginViewModelを宣言し,viewDidLoadでLoginViewModelのtransform関数を呼ぶようにします.
LoginViewController.swiftprivate var viewModel: LoginViewModel! override func viewDidLoad() { super.viewDidLoad() ...略 let input = createInput() let output = viewModel.transform(input: input) setupOutput(output) }さて中身を書いていきます❣️
LoginViewControllerで『registraionButtonを押した!』という判定をします.ここではRxSwiftの
rx.tap.asSignal()
を使用します.LoginViewController.swiftprivate func createInput() -> LoginViewModel.Input { return LoginViewModel.Input( onTapRegistraionButton: self.registraionButton.rx.tap.asSignal() ) }2. LoginViewModel で input の registraionButton を showRegistraionView にいれる
1のLoginViewControllerでTapしたかどうかを
.emit
で監視します?
『押された!』ってなったらshowRegistraionViewをacceptします.LoginViewModel.swiftfunc transform(input: Input) -> Output { let showRegistraionView = PublishRelay<Void>() input.onTapRegistraionButton .emit(onNext: { showRegistraionView.accept(()) }) .disposed(by: self.bag) return LoginViewModel.Output( showRegistraionView: showRegistraionView.asDriverOnErrorJustComplete() ) }3. LoginViewModel の output に showRegistraionView をいれる
2でacceptされたらOutputにいれることで『新規登録画面に遷移していいよ!』ってことを知らせます.
2のコードのLoginViewModel.swiftreturn LoginViewModel.Output( showRegistraionView: showRegistraionView.asDriverOnErrorJustComplete() )ここの部分です.
4. LoginViewController の output.showRegistraionView で遷移先のVCをstartする
3のOutputでLoginViewModelから『新規登録画面に遷移していいよ!』と送られてきたかどうかを
.drive(onNext:
で監視します?
送られてきたら前の記事の要領でRegistrationViewController.start(self)
で画面遷移します!LoginViewController.swiftprivate func setupOutput(_ output: LoginViewModel.Output) { output.showRegistraionView .drive(onNext: { [unowned self] in RegistrationViewController.start(self) }).disposed(by: bag) }以上で新規登録画面に遷移ができます!
前の記事から新規登録画面の見た目をすこし変えてしまいましたが,動きはこんな感じになります.
?ログインしたらやわらかさを記入する画面に遷移する
途中です??♂️
詰まったところ
LoginViewController.swiftprivate let viewModel: LoginViewModel init(viewModel: LoginViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") }このように書くとstartのところの
LoginViewController.swiftlet nextVC = LoginViewController(nibName: "LoginView", bundle: nil)で
Type of expression is ambiguous without more context
というエラーになってしまう.
なので今回はinitを省略しvarなどの宣言には!
をつけています.(!
がないと'LoginViewController' has no initializersになる)
- 投稿日:2021-02-26T09:17:58+09:00
【SwiftUI】PreviewProviderにおけるBinding変数のエラーをなんとかする
どういうことか
フルスクリーンのモーダルで遷移する場合、遷移元から遷移先の
@Binding
にBool値を渡したのち、例えば『戻る』というボタンを押した際にこのBool値をfalse
にする必要がある。
そうした場合、遷移先のPreviewProvider
にてエラーが出る。
なんも書いてないと
Missing argument for parameter
とか、それでfixした場合は
Cannot convert value of type 'Binding<Bool>.Type' to expected argument type 'Binding<Bool>'
とか、なんか書いてみても
Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
こんな感じで間違っている。
バインディング変数を初期化する
// バインディング変数 @Binding var isModalActive: Bool // 中略 struct IndexView_Previews: PreviewProvider { static var previews: some View { // isModalActiveのバインディング変数を初期化 IndexView(isModalActive: .constant(false)) } }おわり(´・ω・`)