Help us understand the problem. What is going on with this article?

FirebaseでiOS版簡易SNSを作成する。(タイムライン編 後編)

はじめに

今回、Firebaseを使ってiOS版簡易SNSを作成する記事になっています。
この記事では タイムライン上に編集可能なユーザー名を表示できる ・ ユーザーがログアウトできる などを行うまでの部分を行なっています。
また、前回の続きという扱いで進めていきます。

タイムラインにユーザー名を表示

AppUserクラスをデータベースに保存 & 取得

  • TimelineViewControllerのviewWillAppearメソッドでAppUserをデータベースに保存。

usersコレクションの中にme.userIDをキーとして保存します。この時点ではユーザー名は作成していないのでuserIDのみ保存します。

TimelineViewController.swift
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    (省略)
    // 追加
    database.collection("users").document(me.userID).setData([
        "userID": me.userID
        ], merge: true)    
}
  • PostクラスのuserNameにデフォルト値をいれます。

仕様として、Post.userNameに値が入っていなければ匿名と表示するようにします。
このとき、現在の実装だとas! Stringなので、Post.userNameがnilであるとクラッシュしてしまうので、Postクラスを次のように変更します。

現在の実装.swift
userName = data["userName"] as! String
修正後のコード.swift
userName = data["userName"] as? String ?? "匿名"
  • 加えて、AppUserをデータベースから取得するコードを同じくviewWillAppearメソッド内に記述します。
// 追加
database.collection("users").document(me.userID).getDocument { (snapshot, error) in
    if error == nil, let snapshot = snapshot, let data = snapshot.data() {
        self.me = AppUser(data: data)
    }
}
コラム: Firestoreからデータを取得するコード
  • 1つのドキュメントを取得する場合
Firestore.firestore().collection("コレクション名").document("ドキュメント名").getDocument { (snapshot, error) in
    // ここに通信後の処理を書く。 snapshot.data()でドキュメントのデータが取得可能
}
  • コレクション内の全てのドキュメントを取得する場合
Firestore.firestore.collection("コレクション名").getDocuments { (snapshots, error) in
    // ここに通信後の処理を書く。
    // snapshot.documentsで全てのドキュメントが取得できる。
    // document.data()でドキュメントのデータが取得可能
    for document in snapshot!.documents {
        let data = document.data()
    }
}

セル1つ1つに対して、ユーザーデータを取得し、ユーザー名を表示する。

追加するコード.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel?.text = postArray[indexPath.row].content

    // 追加。それぞれの記事を投稿したユーザーをPostクラスのsenderIDを元に取得している。
    database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in
        if error == nil, let snapshot = snapshot, let data = snapshot.data() {
            let appUser = AppUser(data: data)
            cell.detailTextLabel?.text = appUser.userName // 今回は、ユーザー名をdetailTextLabelに表示。
        }
    }
    return cell
}

細かくみていきます。

  • 記事を投稿したデータの取得

これは、取得したいユーザーのIDをキーとしているドキュメントをusersコレクション内から指定してあげる必要があります。

postArray[indexPath.row].senderID

これでその投稿に適したユーザーIDを取得することは可能です。

なので、以下のように書くことでその投稿に適切な投稿者のデータを取ってこれます。

database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in    
}
  • 取得したユーザー名をセルに反映

今回は、デフォルトのセルを使用しています。柔軟性は低いので実際の場面ではTimelineTableViewCellなどを作成してUIをカスタマイズしていくことが好ましいですが、現状はデフォルトのセルを使用します。ここでcell.detailTextLabelというものを使用します。

detailTextLabelの作成の仕方

スクリーンショット 2019-09-01 22.06.11.png

下のようにDetailが追加されていればうまくいっています。

スクリーンショット 2019-09-01 22.09.07.png

用意したdetailTextLabelにユーザー名を表示する。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
    cell.textLabel?.text = postArray[indexPath.row].content
    database.collection("users").document(postArray[indexPath.row].senderID).getDocument { (snapshot, error) in
        // ここを追加。
        // エラーがnilで、データが入っていることを確認。
        if error == nil, let snapshot = snapshot, let data = snapshot.data() {
            let appUser = AppUser(data: data)
            cell.detailTextLabel?.text = appUser.userName
        }
    }
    return cell
}
  • 現在の状態

スクリーンショット 2019-09-01 22.14.11.png

ユーザー情報変更画面を作成

今回は、TimelineViewControllerに新たにスワイプジェスチャーを作成して、下から上にスワイプを行うと、ユーザー情報変更画面に遷移するようにしたいと思います。

  • Main.storyboardにUIViewControllerを1つ追加し以下の部品を追加します。
部品名 個数
UILabel 1
UITextField 1
UIButton 3

以下のように配置してみます。大体自由で大丈夫です。

スクリーンショット 2019-09-02 12.32.09.png

  • TimelineViewControllerから新しく作成した画面をセグエで繋ぎます。セグエのIdentifierはSettingsとします。スクリーンショット 2019-09-02 12.38.09.png

  • SettingsViewControllerを作成・編集

まずは、以下の状態から始めていきます。

SettingsViewController.swift
import UIKit
import Firebase

class SettingsViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var userNameTextField: UITextField! // 変更するユーザー名を入力するところ

    override func viewDidLoad() {
        super.viewDidLoad()
        userNameTextField.delegate = self // delegate指定
    }

    // returnキーを押したときの処理
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder() // キーボードを閉じる
        return true
    }

    // 前の画面に戻るボタン
    @IBAction func back() {
        dismiss(animated: true, completion: nil)
    }

    // 保存ボタンを押したときに呼ばれる。
    @IBAction func save() {
    }

    // ログアウトボタンを押したときに呼ばれる。
    @IBAction func logout() {

    }
}

画面遷移を行う

  • 1.5秒間画面を長押ししたときTimelineViewControllerからSettingsViewControllerに画面遷移する。

まずは、長押しを検知するためにUILongPressGestureRecognizerを追加し、view(画面全体)にジェスチャーを追加します。

TimelineViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    // ここから下を追加
    let press = UILongPressGestureRecognizer(target: self, action: #selector(pressScreen))
    press.minimumPressDuration = 1.5
    view.isUserInteractionEnabled = true
    view.addGestureRecognizer(press)
}

次に、pressScreenメソッドを実装していきます。と言っても内容は簡単でperformSegueを呼ぶだけです。Identifierは先ほど設定したSettingsにし、senderは次の画面でもme(ユーザー自身の情報)っは使用したいので、meを引数に渡します。

TimelineViewController.swift
@objc
func pressScreen() {
    performSegue(withIdentifier: "Settings", sender: me)
}

その次に、prepareメソッドを呼んで、値渡しの処理を書いていきます。

現在、prepareメソッドは以下のようになっていると思います。

現在のコード.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let destination = segue.destination as! AddViewController
    destination.me = sender as! AppUser
}

これを次のように変更します。

編集後のコード.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {        
    if segue.identifier == "Add" {
        let destination = segue.destination as! AddViewController
        destination.me = sender as! AppUser
    } else if segue.identifier == "Settings" {
        let destination = segue.destination as! SettingsViewController
        destination.me = me
    }
}

segue.identifierでどのセグエが呼ばれていたかを分けることができます。これは2つ以上のセグエが現在のViewControllerから呼ばれる可能性がある場合に有効です。

  • SettingsViewControllermeプロパティを追加します。
SettingsViewController.swift
class SettingsViewController: UIViewController, UITextFieldDelegate {

    (省略)

    var me: AppUser! // 追加

    (省略)
}

現在のユーザー名を初期値としてUITextField.textに設定します。

変更画面で今のユーザー名を間違えないようにユーザー名を初期値としてテキストフィールドに表示します。

class SettingsViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var userNameTextField: UITextField! // 変更するユーザー名を入力するところ
    var me: AppUser! 

    override func viewDidLoad() {
        super.viewDidLoad()
        userNameTextField.delegate = self // デリゲートを指定
        userNameTextField.text = me.userName // 追加: 現在のユーザー名をテキストに表示するコード
    }
}

変更したユーザー名を保存する

@IBAction func save() { }にコードを追加していきます。

SettingsViewController.swift
//
//  SettingsViewController.swift
//  SNSApp
//
//  Created by Fumiya Tanaka on 2019/09/02.
//  Copyright © 2019 Fumiya Tanaka. All rights reserved.
//

import UIKit
import Firebase

class SettingsViewController: UIViewController, UITextFieldDelegate {

    (省略)

    // 保存ボタンを押したときに呼ばれる。
    @IBAction func save() {
        let newUserName = userNameTextField.text!
        Firestore.firestore().collection("users").document(me.userID).setData([
            "userName": newUserName
        ], merge: true) { error in // ここの merge: true がポイント
            if error == nil {
                self.dismiss(animated: true, completion: nil) // errorがなく、正常に終了していたらタイムラインの画面に戻る
            }
        }
    }

    (省略)
}
  • コードのミソは、merge: trueです。

mergeをtrueにすると、データを部分的に更新することができます。

updateだと、更新されないフィールドを消してしまいます。

user1:
        "a": 1,
        "b": 2,
        "c": 3,

これを "c": 4にアップデートする時に、merge: trueにすると、

user1:
        "a": 1,
        "b": 2,
        "c": 4,

となって良い感じですが、updateDataを使用すると

user1:
        "c": 4,

となって、更新されなかったフィールドは残りません。注意が必要ですが基本的にはmerge: trueで問題ないと思います。

logout機能を追加

  • @IBAction func logout() { }にコードを追加していきます。

ログアウトする処理は下の一行で可能です。

try? Auth.auth().signOut()

なので、以下のようにコードを書きます。

SettingsViewController.swift
@IBAction func logout() {
    try? Auth.auth().signOut()
    let accountViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! AccountViewController
    present(accountViewController, animated: true, completion: nil)
}

StoryboardでSettingsViewControllerの関連付けを行う

  • クラスをSettingsViewControllerに設定する。

スクリーンショット 2019-09-10 23.00.40.png

  • それぞれの部品に対して関連付けを行う。

スクリーンショット 2019-09-10 23.14.40.png

実行してみましょう。

現在のDemo

demo

ここで、気づいた方もいるかもしれませんが、このアプリは現在ログイン機能がないのにログアウト機能だけがある状態です。

また、
Auth.auth.createUserメソッドは既に使用されているメールアドレスでの新規登録ができないので、ログイン機能(サインイン機能)を実装していきます。

サインイン機能

サインイン画面の作成・Segueの作成

  • 以下のGifのように新しく作成したViewControllerにAccountViewControllerからSegueを作成します。

Demo

  • AccountViewController.swiftを編集

AccountViewControllerのprepareメソッドを以下のように編集します。
これによって、Timeline画面に遷移するときのみ、値渡しを行うことができます。

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "Timeline" {
        let nextViewController = segue.destination as! TimelineViewController
        let user = sender as! User
        nextViewController.me = AppUser(data: ["userID": user.uid])
    }
}
  • UI部品を配置します。

スクリーンショット 2019-09-13 12.22.28.png

  • TimelineViewControllerにSegueで繋ぎます。SegueのIdentifierをTimelineに設定します。

SignInViewControllerを作成

  • SignInViewController.swift
SignInViewController.swift
import UIKit
import Firebase

class SignInViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet var emailTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "Timeline" {
            let user = sender as! User
            let destination = segue.destination as! TimelineViewController
            destination.me = AppUser(data: ["userID": user.uid])
        }
    }

    @IBAction func tappedSignInButton() {
        let email = emailTextField.text!
        let password = passwordTextField.text!

        Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
            if error == nil, let result = result, result.user.isEmailVerified {
                self.performSegue(withIdentifier: "Timeline", sender: result.user)
            }
        }
    }
}

基本的な部分はAccountViewController.swiftとほとんど同じです。
違いとしては、
Auth.auth.createUserで新規アカウント登録なのか、Auth.auth.signInでサインインするのかという違いです。

これで、関連付けを行えばサインインが実装できました。ログアウトができるけど、サインインができないという変なアプリではなくりました!

最後に

この記事を通して

  • Userの表示名を変更
  • ログアウト・サインイン処理

といったことが可能になったと思います。

いいね機能やブロック機能やプロフィール画像などまだSNSとしてやりきれていない部分が多いので、緩く残りを更新して行けたらいいなと感じています。最後まで読んでいただきありがとうございました。

関連記事

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away