20210226のSwiftに関する記事は16件です。

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_sixth

lowerCamelCaseの文字列を任意の文字列で連結できます。

(※)先頭の大文字を小文字に差し替える
var replacedFirstCharWithLowercaseChar: String では、
replacingOccurrences(of:with:)も試しましたが、
後続している大文字始まりの文字列の塊があると、その大文字も差し代わってしまうので
insert(contentsOf: at:)dropFirst(_:)を用いて処理しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS14】アプリにウィジェット(WidgetKit)を導入してみた

watnowテックカレンダー2020/2/26】

今日はwatnowのryotaが担当します
よろしくお願いします

 はじめに

2020年9月にiOS14がリリースされ、その追加機能の一つとしてウィジェットという機能が利用可能になりました。

Apple公式のWidget Kitについて
Apple Introducing WidgetKit

ウィジェットはアプリの一部情報をiOSのホーム画面へ表示させられる機能です。この機能を用いればアプリを開かずにちょっとした情報を確認することができます。またウィジェットに任意の画面へのリンクを設定することもできユーザのアプリの利用率も向上させられます。
一方でWidgetKitを作成するにはswiftUIで実装する必要があり、なかなか導入しにくいと思う部分もあると思いますが、やってみると簡単に作成することができたので共有しようと思います。
image.png

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
image.png
真ん中らへんにWidget Extensionがあるのでクリックし、Next
image.png
Product Nameを適当に決めてFinish
image.png
するとProduct Name以下にこのようなファイルが出来上がっていると思います。
それぞれの役割として

  • widgetDemo.swift
    • =>データやViewとかの処理をまとめて行う。追加時には時刻を表示する場合のサンプルコードが記入されている。もし独自のウェイジェットを実装していくならViewとかでファイルを分けるとわかりやすい。
  • widgetDemo.intendefinition 
    • => メインのアプリとデータを共有したりするときにここで設定する
  • Assets.xcassets
    • => 静的な画像データとかはここにおく
  • info.plist 
    • => Widgetの設定プロパティ

image.png
以下のようにWidgetExtensionのViewが表示されています。

image.png
この状態で アプリのビルドを行い、ホーム画面を長押し=>widgetを追加する
これでホーム画面に現在の時間が表示されるウィジェットが追加されます。時計を表示させるだけなら実はこれだけで実装となります。
image.png
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 11.59.45.png

APIからデータをとってきて表示させる

ここまでで、すでにホーム画面にWidgetを追加して、時間を表示させることができました。

ここをベースに開発していくことでアプリ側からデータを受け取って表示させたりサーバからデータを取得して表示させたりすることができます。
では今回はAPIからデータをとってきて表示させるWidgetを開発しようと思います。

完成はこんな感じです。今回はNewsAPIからJSONで技術系のニュースデータを取得してその1つめのデータをWidgetに追加するようにします。
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 17.47.27.png

まずはニュースを取得するために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通信を許可する方法

image.png

まずは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の情報を表示させるだけでしたが、これを発展させてクリックした場にアプリで記事を表示させるとより実用的になっていくかと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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. 

スクリーンショット 2021-02-26 17.31.51.png

調べてみると同様の症状が発生している人は多く見かけましたが、解決に至っている人を見つけられませんでした。

解決方法

大前提としてApple Developer Programに加入している必要があります。

公式ドキュメントでその様な記述は見つけられませんでしたが、無料アカウントだと実機ビルドでできない様です。

1.まずはじめに、下記リンクよりApp ClipのIdentifierを作成します。

2.次に画像のような手順で、親アプリのIDを選択し、AppClip用のIDを作成します。

スクリーンショット 2021-02-26 17.39.27.png
スクリーンショット 2021-02-26 17.40.01.png

3.Identifierが作成完了すると、Signingのエラーが消えてビルドできる様になります。

スクリーンショット 2021-02-26 17.41.39.png

最後に

Apple Developer Programに加入していないと実機でのビルドができないので、
AppClipを試そうとしている方は気をつけてください
ではまた?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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. 

スクリーンショット 2021-02-26 17.31.51.png

調べてみると同様の症状が発生している人は多く見かけましたが、解決に至っている人を見つけられませんでした。

解決方法

大前提としてApple Developer Programに加入している必要があります。

公式ドキュメントでその様な記述は見つけられませんでしたが、無料アカウントだと実機ビルドでできない様です。

1.まずはじめに、下記リンクよりApp ClipのIdentifierを作成します。

2.次に画像のような手順で、親アプリのIDを選択し、AppClip用のIDを作成します。

スクリーンショット 2021-02-26 17.39.27.png
スクリーンショット 2021-02-26 17.40.01.png

作成する際に、On Demand Install Capableにチェックがついているか確認してください。
スクリーンショット 2021-02-26 17.56.19.png

3.Identifierが作成完了すると、Signingのエラーが消えてビルドできる様になります。

スクリーンショット 2021-02-26 17.41.39.png

最後に

Apple Developer Programに加入していないと実機でのビルドができないので、
AppClipを試そうとしている方は気をつけてください
ではまた?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift/FirebaseAuth] 一度ログインしたらログイン画面をスキップする方法

Firebase Authentication を使ってユーザーの認証を行うアプリを作る際、一度新規登録・ログインをしたら次アプリを開くときログイン画面をスキップしてくれるコードを紹介します。

この記事ではすでに FirebaseAuth を使って新規登録・ログイン画面と処理が完成してることを前提とします。これらのやり方は Firebase Official Docが丁寧に説明してくれてるので参考にしてください!

Code

この処理は SceneDelegate.swift で行います。
まず FirebaseAuth ライブラリをインポートします。

SceneDelegate.swift
import FirebaseAuth

SceneDelegate クラスの中に以下のメソッドを書きます。

SceneDelegate.swift
    func 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()
     }

ちなみに storyboard ID はここで設定します。
スクリーンショット 2021-02-26 午後4.01.11.png

skipLogin()メソッドが書き終わったらあとは次のメソッドの中に呼ぶだけです!

SceneDelegate.swift
    func 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文はいらない。
    }

これで、一度ログインしたら次からログイン画面をスキップできるようになります!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cloud Firestoreについてまとめる

はじめに

備忘録として、Firebase Cloud Firestore(以下、Firestore)についてまとめていきます。

そもそもFirebaseが何なのか?というのは今回は割愛します。

Firestoreを知る前に

データベースには大きく分類すると、RDBMSNoSQLに分けることができ、前者はSQLというコンピューター言語を使用して、データの書き込み・読み取りを行います。

対して、後者のNoSQLは文字通り、SQL言語を使用せずにデータの書き込み・読み取りを行っています

主な違いは管理方法にあり、RDBMSは1つのキーに対して複数の付随するデータを管理しているのに対し、NoSQLは1つのキーに対して1つの付随するデータを管理しています。

・RDBMS 「1キー:複数の付随データ」

・NoSQL 「1キー:1つの付随データ」

FirestoreではNoSQLでデータが管理されており、クラウドデータベースになっています。

クラウドデータベースとは

手元にサーバーやソフト等の環境を持たずにデータベースを利用することができるサービスの事です。

これはデータベースに限った話ではなく、手元にサーバーやソフトなどの環境がなくても利用できるサービスはクラウドサービスとしてまとめられます。

この主な例として他にもGmailiCloudなどがあり、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と基本的には同じと公言しています。

・コレクション

複数のドキュメントをまとめるフォルダみたいなものです。

仕組みとしてはデータをドキュメントに書き込み、そのドキュメントをコレクションにまとめるといった感じです。

公式ドキュメントの以下の画像がわかりやすいと思います。
firestoreImage.png
(出典: Cloud Firestore データモデル | Firebase)

例えば、foodコレクションを作成して、様々な食べ物を表すドキュメントを格納した場合このようになります。

?food(コレクション)
  ?meet(ドキュメント)
     kind(フィールド): "ChickenMeat"(文字列型の値)
     Id: 12345678(数値型の値)
  ?fruit
     kind: "Apple"
     Id: 87654321

Firestoreでは、Bool値、数値、文字列、タイムスタンプなど、様々なデータ型の値がサポートされています。

コレクションは、自分から作成・削除をする必要がなく、最初のドキュメントを追加した時に勝手に作られ、コレクション内のドキュメントを全て削除すると自動的にコレクションも削除されます。

そして、コレクションにはドキュメントのみしか含めることができません。なので、コレクションに対して生のフィールド他のコレクションを含めることは許されていません。

・リファレンス

データベース内の場所を参照するだけの軽量なオブジェクトです。

先ほどのfoodコレクション内のドキュメントを参照してみましょう。

?food
  ?meet
     kind: "ChickenMeat"
     Id: 12345678
  ?fruit
     kind: "Apple"
     Id: 87654321
let 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のセキュリティルールについてまとめようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.resolved

Bitriseに環境変数を登録

後述しますが、
BitriseのCache Push Stepに下記記述を追加することで、指定場所にcacheを保存することが可能です。

{cache保存先} -> {Package.resolvedのPath}

今回は、cache保存先を示す環境変数BITRISE_SWIFTPM_CACHE_PATHを、
Package.resolvedのpathを環境変数SWIFTPM_PACKAGE_RESOLVED_PATHを作成します。

スクリーンショット 2021-02-26 15.36.18.png

BitriseのCache Push Stepを定義

BitriseのCache Push Stepのcache pathsに前述した通り
{cache保存先} -> {Package.resolvedのPath}
を追加します。

これにより、SwiftPMのcacheがpushされるようになります。

スクリーンショット 2021-02-26 15.35.48.png

Fastlaneのlane内でキャッシュを利用

gymのcloned_source_packages_pathBITRISE_SWIFTPM_CACHE_PATHを渡すことで、
SwiftPMのcacheを利用します。

fastfile
    gym(
      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です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】NavigationViewの基本的な使い方(前編)

前回に引き続き、アプリに使用した技術をアウトプットしていきます。
解説する私のアプリはこちら。
わかりすと -勉強サポートアプリ-

NavigationView

階層的な画面遷移を管理するビュー。

公式リファレンス

NavigationLink

NavigationView内で、画面遷移を行うトリガーになるビュー遷移先のビューの両方を指定できるビュー。NavigationViewとセットで使う。

公式リファレンス

実際の動作

※拡大推奨

動画のポイント

  • NavigationViewの外側の領域は切り替わらず、遷移先にも表示される。(動画内のヘルプボタン)
  • 遷移先の画面の左上には、前の画面にもどるためのボタンが表示される。
  • もどるボタンにはタイトルがそのまま書かれる。

コード

今回作成したサンプルアプリのソースコードはこちら

まず、一番上の階層から見ていきましょう。

ContentView.swift
import 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.swift
import 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を書き、次の画面に遷移するとこのようになります。

スクリーンショット 2021-02-26 14.06.58.png

画面左上に、前の画面にもどるためのボタンが2つ並んでいることがわかると思います。これはNavigationViewの中にNavigationViewを作っていると判断された結果です。このような表示にしたい場合は別ですが、基本は一番上の階層にのみNavigationViewを書くことをおすすめします

次回はnavigationTitleについて書きます。

参考文献

SwiftUI徹底入門 金田浩明(著)(【注意】Xcode12非対応のものです)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 ViewSupervising 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アプリ開発に限らず、設計パターンを学びたい方に損得抜きで強くおすすめです。歴史的経緯も含めた設計パターンの丁寧な解説、さらに各設計パターンのサンプルコードで構成されています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

トリガーをボタンにしたピンクの音色再生その2

7日目のアプリ

トリガーをボタンにした音声再生その2

画面キャプチャ

以下の流れで作りました。

  1. storyboadに複数ボタンを配置。
  2. 上記要素をViewController.swiftへoptionドラッグして紐付ける
  3. 紐付けができたら、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!)
ふむむ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

7日目トリガーをボタンにしたピンクの音色再生その2

7日目のアプリ

トリガーをボタンにした音声再生その2

画面キャプチャ

以下の流れで作りました。

  1. storyboadに複数ボタンを配置。
  2. 上記要素をViewController.swiftへoptionドラッグして紐付ける
  3. 紐付けができたら、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!)
ふむむ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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無しだと上記のエラーが発生しました。
このことから、今回のエラーは既存の証明書の権限周りに不具合が出たと考えています。
同様のエラーが発生した同志の助けになることを願っています。
ではまた?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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無しだと上記のエラーが発生しました。
このことから、今回のエラーは既存の証明書の権限周りに不具合が出たと考えています。
同様のエラーが発生した同志の助けになることを願っています。
ではまた?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

?yawacom - ?画面を作っていく

2021/02/26現在編集中

ログイン画面の作成

1つ前の記事でユーザ登録画面を作った要領でログイン画面も作っていきます.
下記のような流れになるように作っていきます.
今回はおおまかに3つ
?テキストボックスとボタンで構成されるログイン画面を作る
?ログイン画面から新規登録画面に遷移するボタンがある
?ログインしたらやわらかさを記入する画面に遷移する
これらの動きを持つ画面を作ります!

?テキストボックスとボタンで構成されるログイン画面を作る

まずはユーザ名・パスワードを打つためのテキストボックス2つとログインボタンを作ります.一つ前の記事の要領でとりあえずviewDidLoadにぶちこんでます??‍♂️
またtextFieldShouldReturnを使用して,テキストボックスに文字を入力して改行を押したらキーボードが隠れるようにします.これをしないとユーザー名などを打ち込んでもキーボードがしまえずログインボタンが押せないハメになります.

LoginViewController.swift
    override 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.swift
import 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.swift
    private 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.swift
private 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.swift
    func 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.swift
        return LoginViewModel.Output(
            showRegistraionView: showRegistraionView.asDriverOnErrorJustComplete()
        )

ここの部分です.

4. LoginViewController の output.showRegistraionView で遷移先のVCをstartする

3のOutputでLoginViewModelから『新規登録画面に遷移していいよ!』と送られてきたかどうかを.drive(onNext:で監視します?
送られてきたら前の記事の要領でRegistrationViewController.start(self)で画面遷移します!

LoginViewController.swift
    private func setupOutput(_ output: LoginViewModel.Output) {
        output.showRegistraionView
            .drive(onNext: { [unowned self] in
                RegistrationViewController.start(self)
            }).disposed(by: bag)
    }

以上で新規登録画面に遷移ができます!
前の記事から新規登録画面の見た目をすこし変えてしまいましたが,動きはこんな感じになります.

?ログインしたらやわらかさを記入する画面に遷移する

途中です??‍♂️

詰まったところ

LoginViewController.swift
    private 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.swift
let nextVC = LoginViewController(nibName: "LoginView", bundle: nil)

Type of expression is ambiguous without more contextというエラーになってしまう.
なので今回はinitを省略しvarなどの宣言には!をつけています.(!がないと'LoginViewController' has no initializersになる)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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))
    }
}

おわり(´・ω・`)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む