【Swift】Invalid document reference. Document...について

Firebaseを利用中に下記のエラーに遭遇。

'Invalid document reference. Document references must have an even number of segments, but User has 1'

 

直訳すると、
"ドキュメントを参照できません。参照には偶数のセグメントが必要、しかしユーザーは既に1つ持っています。"
という意味。

 

僕の場合、

db.collection("User").document(userDataClass.userID).getDocument()
↓
db.collection("User").getDocument()

(db = Firestore.firestore())
に直したら治った。

 

原因は、

  • collection > documentの階層が標準
  • collection.add()やcollection.set()ならOK
  • 階層の数が多すぎる/少なすぎる

かな。

 

もう全然わからん!!

【Swift】staticで1つのインスタンスを共有

 

 

解説

 

全ファイルで変数や値を共有したくありませんか?

 

クラスのインスタンスって基本的に生成したファイル内でしか使えません。

 

関数やメソッドなら、渡したい引数さえ用意すれば、後は処理や返り値を返してくれるので、必要な時にインスタンスを作ればいいだけです。

 

しかし、変数の場合はどうでしょうか?

 

インスタンスは1つ1つが別物なので、別のファイルで使うには、逐一渡し続ける必要があります。

 

例えば、インスタンスAを他のファイルで使う場合、関数などで渡します。

f:id:nekokichi_yos2:20190401141700p:plain

 

ですが、もし何十個ものファイルで使う場合、

  • 常に保持し続けないといけない
  • 今現在の変数や値の状態が把握できない
  • 余計にコードが増える

などの弊害が起こります。

f:id:nekokichi_yos2:20190401141638p:plain

 

そこで、使いたいインスタンスを1つのファイルに固定させ、必要なときに呼び出せれば、わざわざ遷移先に渡す必要などありません。

 

お金の管理に例えると、

  • 毎日ランダムで選んだ社員に任せる

ではなく

  • 1つの銀行口座に任せる

です。

f:id:nekokichi_yos2:20190401141526p:plain

 

オリジナルアプリ開発でFirebaseを使った際、どこからでもアクセスできるインスタンスがあれば、新たにデータを取得/編集/保存する際、わざわざバケツリレーしなくても、必要な時に参照するだけでいいのでめっちゃ楽でした。

 

それでは、staticを使った共有インスタンスを使い方を紹介します。

 

ストーリーボード

 

今回は、

  • 画面起動時に初期値と変更後の値
  • 遷移後に現在の値と変更後の値

を表示します。

 

f:id:nekokichi_yos2:20190331174346p:plain

 

ソースコード

 

「呼び出される側」

class object: NSObject {
    //頭にstatic
    //現クラスのインスタンスを代入
    static let instance = object()
    var number = 0
    var string = "a"
}

 

「呼び出す側 - ViewController」

class ViewController: UIViewController {
    
    //object.swift
    let sample = object.instance

    override func viewDidLoad() {
        super.viewDidLoad()
        //インスタンスの初期値
        print("\(sample.number) " + sample.string)
        //値を変更
        sample.number = 100
        sample.string = "string"
        //変更した値を表示
        print("\(sample.number) " + sample.string)
    }

}

 

「呼び出す側 - ViewController2」

class ViewController2: UIViewController {

    //object.swift
    let sample = object.instance
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //インスタンスの初期値
        print("\(sample.number) " + sample.string)
        //値を変更
        sample.number = 777
        sample.string = "end"
        //変更した値を表示
        print("\(sample.number) " + sample.string)
    }
}

 

 

出力結果

f:id:nekokichi_yos2:20190401141114p:plain

1,2行目がViewController、

3,4行目がViewController2、での出力結果。

【Swift】Firebase - メアド&パスワードで会員登録/ログイン

 

完成図

会員登録

 

f:id:nekokichi_yos2:20190211113425p:plain

 

ログイン

 

f:id:nekokichi_yos2:20190211113439p:plain

 

パスワードリセット

 

f:id:nekokichi_yos2:20190211113550p:plain

 

解説

 

会員登録

Auth.auth().createUser(withEmail: mailForm.text!, password: passForm.text!) { (user, error) in
   Auth.auth().currentUser?.sendEmailVerification(completion: { (error) in
   })
}
  • メアドとパスワードを入力すると、入力したメアドに認証メールが送られる
  • 認証メールにあるリンクを押せば、会員登録が完了
  • ※注意:パスワードは7文字以上じゃないと会員登録できない

 

f:id:nekokichi_yos2:20190211131002p:plain

f:id:nekokichi_yos2:20190211130949j:plain

 

ログイン

Auth.auth().signIn(withEmail: mailForm.text!, password: passForm.text!) { (user, error) in
}

 

ログアウト

if Auth.auth().currentUser != nil {
         //ログアウト成功
         do {
             try Auth.auth().signOut()
         //ログアウト失敗
} catch let signOutError as NSError { print (signOutError) } }

 

パスワードリセット

Auth.auth().sendPasswordReset(withEmail: resetEmail!, completion: { (error) in
      DispatchQueue.main.async {
          if error != nil {
              self.alert(title: "メールを送信しました。", message: "メールでパスワードの再設定を行ってください。", actiontitle: "OK")
          } else {
              self.alert(title: "エラー", message: "このメールアドレスは登録されてません。", actiontitle: "OK")
          }
      }
})
  • パスワードリセットの際、リセット用のメールが送られる
  • メールに貼ってあるリンクを押せば、リセット後のパスワードを入力
  • 入力すれば、パスワードが更新される

 

f:id:nekokichi_yos2:20190211131007j:plain

f:id:nekokichi_yos2:20190211130958p:plain

f:id:nekokichi_yos2:20190211130953p:plain

 

ストーリーボード

 

f:id:nekokichi_yos2:20190211113111p:plain

 

ソースコード

会員登録画面

import UIKit
import Firebase

class SignUp: UIViewController,UITextFieldDelegate {

    //メールアドレスとパスワード
    @IBOutlet weak var mailForm: UITextField!
    @IBOutlet weak var passForm: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //UITextFieldのデリゲート
        mailForm.delegate = self
        passForm.delegate = self
        
        //UITextFieldに枠線,角丸を施す
        mailForm.layer.borderWidth = 1
        mailForm.layer.cornerRadius = 10
        passForm.layer.borderWidth = 1
        passForm.layer.cornerRadius = 10
    }
    
    //会員登録
    @IBAction func signup(_ sender: Any) {
        //2つのフォームが入力されてる場合
        if mailForm.text != "" && passForm.text != "" {
            //入力したパスワードが7文字以上の場合
            if (passForm.text?.count)! > 6  {
                //会員登録開始
                Auth.auth().createUser(withEmail: mailForm.text!, password: passForm.text!) { (user, error) in
                    //ログイン成功
                    if error == nil {
                        //登録メアドに確認のメールを送る
                        Auth.auth().currentUser?.sendEmailVerification(completion: { (error) in
                            //エラー処理
                            if error != nil {
                                let storyboard = UIStoryboard(name: "Main", bundle:Bundle.main)
                                let rootViewController = storyboard.instantiateViewController(withIdentifier: "main")
                                UIApplication.shared.keyWindow?.rootViewController = rootViewController
                            } else {
                            }
                        })
                    //ログイン失敗
                    } else {
                        self.alert(title: "エラー", message: "ログイン失敗", actiontitle: "OK")
                    }
                }
            //入力したパスワードが6文字以下の場合
            } else {
                self.alert(title: "エラー", message: "7文字以上のパスワードを入力してください。", actiontitle: "OK")
            }
        //いずれかのフォームが未入力の場合
        } else {
            self.alert(title: "エラー", message: "入力されてない箇所があります。", actiontitle: "OK")
        }
    }

    //ログイン画面へ移行
    @IBAction func changeToLogin(_ sender: Any) {
        performSegue(withIdentifier: "changesignin", sender: nil)
    }
    
    //アラート
    func alert(title:String,message:String,actiontitle:String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: actiontitle, style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
   
    //Returnでキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    //画面外タップでキーボードを閉じる
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        mailForm.resignFirstResponder()
        passForm.resignFirstResponder()
    }

}

ログイン画面/ログアウト

import UIKit
import Firebase

class SignIn: UIViewController,UITextFieldDelegate {

    @IBOutlet weak var mailForm: UITextField!
    @IBOutlet weak var passForm: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mailForm.delegate = self
        passForm.delegate = self
        
        mailForm.layer.borderWidth = 1
        mailForm.layer.cornerRadius = 10
        passForm.layer.borderWidth = 1
        passForm.layer.cornerRadius = 10
    }
    
    @IBAction func remindPassword(_ sender: Any) {
        let remindPasswordAlert = UIAlertController(title: "パスワードをリセット", message: "メールアドレスを入力してください", preferredStyle: .alert)
        remindPasswordAlert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
        remindPasswordAlert.addAction(UIAlertAction(title: "リセット", style: .default, handler: { (action) in
            let resetEmail = remindPasswordAlert.textFields?.first?.text
            Auth.auth().sendPasswordReset(withEmail: resetEmail!, completion: { (error) in
                DispatchQueue.main.async {
                    if error != nil {
                        self.alert(title: "メールを送信しました。", message: "メールでパスワードの再設定を行ってください。", actiontitle: "OK")
                    } else {
                        self.alert(title: "エラー", message: "このメールアドレスは登録されてません。", actiontitle: "OK")
                    }
                }
            })
        }))
        remindPasswordAlert.addTextField { (textField) in
            textField.placeholder = "test@gmail.com"
        }
        self.present(remindPasswordAlert, animated: true, completion: nil)
    }
    
    @IBAction func signin(_ sender: Any) {
        //ちゃんと入力されてるかの確認
        if mailForm.text != "" && passForm.text != "" {
            Auth.auth().signIn(withEmail: mailForm.text!, password: passForm.text!) { (user, error) in
                if error == nil {
                    //ホーム画面へ移行
                    let storyboard = UIStoryboard(name: "Main", bundle:Bundle.main)
                    let rootViewController = storyboard.instantiateViewController(withIdentifier: "main")
                    UIApplication.shared.keyWindow?.rootViewController = rootViewController
                } else {
                    self.alert(title: "エラー", message: "メールアドレスまたはパスワードが間違ってます。", actiontitle: "OK")
                }
            }
        } else {
            self.alert(title: "エラー", message: "入力されてない箇所があります。", actiontitle: "OK")
        }
    }
    
    @IBAction func changeTosignup(_ sender: Any) {
        performSegue(withIdentifier: "changeTosignup", sender: nil)
    }
    
    //アラート
    func alert(title:String,message:String,actiontitle:String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: actiontitle, style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        mailForm.resignFirstResponder()
        passForm.resignFirstResponder()
    }

}

 

ログアウト

import UIKit
import Firebase

class ViewController: UIViewController{
    
    //ログアウト
    @IBAction func signout(_ sender: Any) {
        //ログアウト開始
        if Auth.auth().currentUser != nil {
            //ログアウト成功
            do {
                try Auth.auth().signOut()
            //ログアウト失敗
            } catch let signOutError as NSError {
                print (signOutError)
            }
        } else {
            return
        }
        
        //ログイン画面に戻る
        let storyboard = UIStoryboard(name: "Main", bundle:Bundle.main)
        let rootViewController = storyboard.instantiateViewController(withIdentifier: "gosignin")
        UIApplication.shared.keyWindow?.rootViewController = rootViewController
    }
    
}

 

【Swift】スクロール画面に大量のUIButtonを等間隔で配置

 

完成図

 

f:id:nekokichi_yos2:20190211113825p:plain

 

解説

 

「手順」

  1. ScrollViewを設置
  2. UIViewを生成
  3. UIButtonを生成
  4. UIButton.frameを設定
  5. UIButton.tagを設定
  6. UIButtonに文字や処理を設定
  7. UIViewに設置
  8. 3~7をループ
  9. ScrollViewにUIViewを設置
  10. ScrollView.contenSizeとUIView.bounds.sizeを同じにする

 

「ポイント」

  • for文のループ内にある変数iにかける数値がUIButton同士の幅となる
  • 横ならx座標を増加させ、縦ならy座標を増加させればいい

 

ストーリーボード

f:id:nekokichi_yos2:20190211113903p:plain

 

ソースコード

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var scrollView1: UIScrollView!
    @IBOutlet weak var scrollView2: UIScrollView!
    
    var vc1 = UIView()
    var vc2 = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        verticalScroll()
        horizontalScroll()
    }
    
    //アラート
    @objc func alert() {
        let alert = UIAlertController(title: "成功", message: "タップ", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
    
    //横スクロール
    func horizontalScroll() {
        //vcのframe
        vc1.frame = CGRect(x: 0, y: 0, width: 1000, height: 55)
        
        //上部のスクロールビューに多数のボタンを配置
        for i in 0...6 {
            let button = UIButton()
            //サイズ
            
            button.frame = CGRect(x: (i*100), y: 30, width: 80, height: 55)
            //タグ
            button.tag = i
            //buttonに文字を挿入
            setTitleForButton(tag: button.tag, button: button)
            //button.titleの色
            button.setTitleColor(.white, for: .normal)
            button.layer.borderWidth = 1
            //buttonに処理を追加
//            button.addTarget(self, action: #selector(alert), for: .touchUpInside)
            //vcに載せる
            vc1.addSubview(button)
        }
        
        //スクロールビューにvcを配置
        scrollView1.addSubview(vc1)
        scrollView1.contentSize = vc1.bounds.size
    }
    
    //縦スクロール
    func verticalScroll() {
        //vcのframe
        vc2.frame = CGRect(x: 0, y: 0, width: 240, height: 1000)
        
        //上部のスクロールビューに多数のボタンを配置
        for i in 0...6 {
            let button = UIButton()
            //サイズ
            button.frame = CGRect(x: 120, y: (i*100), width: 80, height: 55)
            //タグ
            button.tag = i
            //buttonに文字を挿入
            setTitleForButton(tag: button.tag, button: button)
            //button.titleの色
            button.setTitleColor(.white, for: .normal)
            button.layer.borderWidth = 1
            //buttonに処理を追加
//            button.addTarget(self, action: #selector(alert), for: .touchUpInside)
            //vcに載せる
            vc2.addSubview(button)
        }
        
        //スクロールビューにvcを配置
        scrollView2.addSubview(vc2)
        scrollView2.contentSize = vc2.bounds.size
    }

    //スクロールビューのボタンに文字を入れる
    func setTitleForButton(tag:Int, button:UIButton){
        switch tag {
        case 0:
            button.setTitle("最新", for: .normal)
        case 1:
            button.setTitle("人気", for: .normal)
        case 2:
            button.setTitle("フォロー", for: .normal)
        case 3:
            button.setTitle("文学", for: .normal)
        case 4:
            button.setTitle("社会", for: .normal)
        case 5:
            button.setTitle("科学", for: .normal)
        case 6:
            button.setTitle("ビジネス", for: .normal)
        default:
            break
        }
    }

}