4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Life is Tech ! KantoAdvent Calendar 2022

Day 3

FirebaseとSwiftyJSONを使ってSNSを作る

Last updated at Posted at 2022-12-02

はじめに

はじめまして!!Life is Tech!でメンターをしているえーえすです。
Life is Tech ! Kanto Advent Calendar 2022の3日目ということで、今回はプログラミングをやっているなら必ず一回は作ってみたいSNSの中でも、特に「画像も投稿できるSNS」のデータ処理まわりを考えてみたいなと思います。また、プログラミングを勉強している人が書く前提でこの記事を作っているので、多少遠回りですがJSON型などの概念と直にふれあう形を採用しています。何をしているかの理解を最優先にできたらいいなと思っています。

概要

FirebaseRealtimeDatabase・FirebaseStorageとSwiftyJSONを組み合わせて、Twitterみたいに画像と文章を投稿できるSNSのデータ取り扱い部分を作ってみようと思います。

できること

  • 画像と文章を投稿→Firebaseで保存
  • 投稿を取得→SwiftyJSONでJSONから任意のclassにする

投稿の処理

投稿のclass定義

今回は投稿のclassを作ってデータの取り扱いをしようかなと思います。

Post.swift
class Post {
    
    var id = UUID()
    var text = ""
    var image = UIImage()
    var imageURL = ""

    init(text: String, image: UIImage) {
        
        self.text = text
        self.image = image
    }
}

編集機能をつけるかもしれない想定で定数を使っていません。CodableとかRealmとか使ってローカルでのデータ管理するのもいいですね。

Firebaseに投稿

大まかな流れとしては、画像をFirebaseStorageに投稿→URLを取得し、それを含めてFirebaseRealtimeDatabaseに投稿する という感じで行こうと思います。

画像のアップロード
まずはUIから投稿ボタンが押されたとき、こんな感じになると思います。

PostView.swift
let post = Post(text: text, image: imageView.image)

FirebaseStorageを作るときはロケーションの設定を間違えないように気をつけてください!!!間違えるとプロジェクト作り直しになります。
313791572_485554010028738_2923033290674908022_n.png

まずは画像をアップロードします。いくつか文法はあると思いますがこんな感じで。

FirebaseAPI.swift
let post = Post(text: text, image: imageView.image)
guard let data = post.image.jpegData(compressionQuality: 0.2) else {
            
    return
}
        
let imageIDRef = imageRef.child("gs://~~~.appspot.com").child(post.id)
        
let uploadTask = imageIDRef.putData(data)
        
uploadTask.observe(.success) { _ in
            
    imageIDRef.downloadURL { url, error in
                
        if let url = url {
                    
            let downloadURL: String = url.absoluteString
...

一行ごとに解説していきます

guard let data = post.image.jpegData(compressionQuality: 0.2) else {
            
    return
}

ここでimage.jpegDataとしているのは、image.pngDataだとなぜか90°くらい回転した画像がFirebaseに保存されることがあるからです…jpegだと今のところそういったバグは確認されてません。compressionQualityで圧縮率を決めてる感じですね。おもたすぎるファイルはアップロードできないので、ここで調節するのもいいでしょう。

let imageIDRef = imageRef.child("gs://~~~.appspot.com").child(post.id)
        
let uploadTask = imageIDRef.putData(data)

imageIDRefで保存する場所のようなものを指定しています。gs://~~~.appspot.comはFirebaseのWebページから取ってきましょう。imageIDRef.putDataとすることで、その場所に画像データを保存する指示を出せます。

uploadTask.observe(.success) { _ in
            
    imageIDRef.downloadURL { url, error in
                
        if let url = url {
                    
            let downloadURL: String = url.absoluteString

observe(.success)は、uploadTaskが成功、つまりFirebaseに画像データがちゃんと保存できた時に呼び出されます。今回は省略しますが、observe(.failure)で失敗を監視できます。そしてdownloadURLにその保存された画像のURLをとってくる感じとなっています。補足ですがif let elseでエラー処理などもできますね。

さて、URLがとってこれたのでこれを含めたその他データをFirebaseRealtimeDatabaseに投稿します。

文章とURLのアップロード

FirebaseAPI.swift
self.ref.child(post.id).updateChildValues([
    "id": post.id,
    "text": post.text,
    "imageURL": downloadURL
])

refprivate var ref = Database.database().reference()みたいな定義がなされてます。idの場所にid含めてデータを記載します。(idの中にid入れなくてもいいんじゃね?って感じですが、取得の際の構造がちょいわかりやすいからこうします)

投稿時のコード全体

FirebaseAPI.swift
private var ref = Database.database().reference()

func uploadPost(post: Post) {

    guard let data = post.image.jpegData(compressionQuality: 0.2) else {
                
        return
    }
            
    let imageIDRef = imageRef.child("gs://~~~.appspot.com").child(post.id)
            
    let uploadTask = imageIDRef.putData(data)
    
    uploadTask.observe(.success) { _ in
                
        imageIDRef.downloadURL { url, error in
                    
            if let url = url {
                        
                let downloadURL: String = url.absoluteString
                
                self.ref.child(post.id).updateChildValues([
                    "id": post.id,
                    "text": post.text,
                    "imageURL": downloadURL
                ])
            }
        }
    }
}

Firebaseからデータを取得

DatabaseからURLなど情報を取得→URLを使って画像を取得→表示 という流れで行きたいですね。
さてここでTimelineViewに投稿をすべて表示する時、そこにあるPostの配列を更新→didSetを使って変更を検出して画面に反映、という流れにしたいので、全てのデータを仮の配列に保存し、最後にいっぺんに送るという形を作っていきましょう。

文章とURLを取得
SwiftyJSONはここで使います

FirebaseAPI.swift
import SwiftyJSON
FirebaseAPI.swift
func getAllPosts(completionHandler: @escaping ([Post]) -> Void) {
        
    ref.observeSingleEvent(of: .value, with: { snapshot in
            
        let data = snapshot.value

        if data is NSNull || data == nil {
                
            print("error")
            completionHandler([Post]())

        }else {
            
            let parsedData = data as! NSDictionary
                
            let posts = self.parseArray(parsedData: parsedData)
                
            completionHandler(posts)
        }
            
    }) { error in
            
        print(error.localizedDescription)
    }
}

func parseArray(posts: NSDictionary) -> [Post] {
    
    let keys = parsedData.allKeys as! [String]
        
    var posts = [Post]()
        
    for key in keys {
            
        let postData = parsedData[key] as! NSDictionary
            
        getImage(url: postData["imageURL"] as! String, completionHandler: { image in
                
            var post = Post(text: postData["text"] as! String, image: image!)
                
            post.id = postData["id"] as! String
                
            posts.append(post)
        })
    }
        
    return posts
}

func getImage(url: String, completionHandler: @escaping (UIImage?) -> Void) {
        
    do {
            
        let data = try Data(contentsOf: URL(string: url)!)
        completionHandler(UIImage(data: data)!)
            
    } catch let error {
            
        print("Error : \(error.localizedDescription)")
    }
}

さて、先にparseArrayから解説していきますね。(名前があんまり良くないかも。)

    let keys = parsedData.allKeys as! [String]

さて、ここが(この方法で)Firebaseからデータを取ってくる上でいっっっちばん難しくて大事な部分です
Firebaseに上がっているJSON型は、投稿のデータそれぞれが投稿のIDに紐づけられてます。なので他の人の投稿などはIDがわからず、普通にJSONに立ち向かおうとするとどうやって奥に潜り込んでいけばいいのかわからなくなります。3年前の僕はここで大いに詰まったのをよく覚えてます。

そこでまずは、JSONをNSDictionaryに変えます。そうすることでID部分を.allKeysと取り出すことができるようになります!

    var posts = [Post]()
        
    for key in keys {
            
        let postData = parsedData[key] as! NSDictionary

データを運ぶように配列を用意し、各IDに対してparsedData[key] as! NSDictionaryと投稿データを取り出していますね。(取り出したデータもNSDictionaryとしてしまっていますが、こうでなくてもいいのかも。)

        getImage(url: postData["imageURL"] as! String, completionHandler: { image in

そして保存されていた画像のURLから画像を取得します。先にgetImageに移ります。

func getImage(url: String, completionHandler: @escaping (UIImage?) -> Void) {
        
    do {
            
        let data = try Data(contentsOf: URL(string: url)!)
        completionHandler(UIImage(data: data)!)
            
    } catch let error {
            
        print("Error : \(error.localizedDescription)")
    }
}

ここは定石通りみたいな感じありますが、urlからData(contents of: ~)と画像の取得を試みて、ダメだったらcatchに逃げるみたいなそんな感じですね。できたらcompletionHandlerに画像を手渡し、続きが実行されます。

            var post = Post(text: postData["text"] as! String, image: image!)
                
            post.id = postData["id"] as! String
                
            posts.append(post)
        })
    }
        
    return posts

画像が取得できてそうなのでPostを作っていきます。
(このコード書いたの3年くらい前なのですが、なんか!を多用したり取得できてない時の対応がおざなりだったりであんまりいいコードじゃないなぁって思ったりしてます)

さて、JSONで取ってきたデータがめでたくPostの配列にできる機構ができたので、Firebaseからデータを取ってくる本体を書いていきます。

func getAllPosts(completionHandler: @escaping ([Post]) -> Void) {
        
    ref.observeSingleEvent(of: .value, with: { snapshot in
            
        let data = snapshot.value

        if data is NSNull || data == nil {
                
            print("error")
            completionHandler([Post]())

        }else {
            
            let parsedData = data as! NSDictionary
                
            let posts = self.parseArray(parsedData: parsedData)
                
            completionHandler(posts)
        }
            
    }) { error in
            
        print(error.localizedDescription)
    }
}

refは投稿の時に使ったのと同じだと思ってください。Database.database().reference()みたいな難しそうな書き方してますが、実際はURLと同じ、データの住所を書いたものですね。
ref.observeSingleEvent(of: .value, with: { snapshot inでデータがsnapshotに入ってきます。(必要に応じてrefに.child("~")などをつけましょう)それが空っぽで無いかを探ろうとするとなんかNSNullnil両方のチェックが必要らしいですね。データが空で無いなら先ほどのメソッドでPostの配列を作り、これを返します。

おおまかな流れとして、DatabaseからID・文章・画像URLを取得→取得したURLを使って画像を取得→取得したこれらからPostのインスタンスを作成→配列に加え、返す という感じになっています。それぞれが完了してから矢印を辿っていかないといけないというのも大変なところです。completionHandlerの中なら動作が完了するまで時間が止まるので、それをうまく使ってあげましょう。

まとめ

さて長々と画像含めた投稿の流れを作ってきましたが、実はこんなまどろっこしいことしなくてもCodableを使えば秒でJSONからPostの配列を作ることができます。しかしそれだとJSONとは何者なのかとかのいい練習にはならないんじゃ無いかな…という考えが僕の中にあるので、この方法を採用することもあったりなかったり。
また、もう一つの難関箇所として、DatabaseとStorageを併用することでデータ取得の流れをコードに反映させることが難しかったりします。こういったことを理解し、多少これらに慣れる足がかりとして画像付きSNSってのはいいテーマなんじゃ無いかなと思っています。

メンバーにこれをやってもらう時には、文法とか知らなきゃ無理だろ!ってところは最初から教えて、.allKeysとかの疑問が出たらそこは教えて、あとは流れを自分で頑張って実装してもらうみたいなのがいいのかなって思ってます。

さてさて明日は、つぼつぼがzoomについてのめちゃ有能記事を投稿してくれるみたいなので!楽しみにしています!

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?