본문 바로가기

프로젝트/Twitter Clone App (w∕Firebase)

19. 기능 구현 #7

기능 구현 #7
Tweet 가져오기


Tweet 가져오기
| Tweet Model

Firebase에 저장된 Tweet의 모습이다.
이 데이터들을 앱에서 보여줄 수 있도록 객체화가 필요하다.

import FirebaseFirestoreSwift
import Firebase

struct Tweet: Identifiable, Codable {
    @DocumentID var id: String?
    let caption: String
    let timestamp: Timestamp
    let uid: String
    var likes: Int

    var user: User?
}

Identifiable과 Codable 프로토콜을 채용한 Tweet 구조체를 하나 정의했다.
Tweet의 id에 해당하는 DocumentID는 Identifiable의 필수 요소인 id로 사용된다.
기타 다른 속성도 데이터에 맞도록 변수를 선언해 준다.

Tweet 가져오기
| TweetRowView

HStack(alignment: .top, spacing: 12) {
    Circle()
        .frame(width: 56, height: 56)
        .foregroundColor(Color(.systemBlue))

    //user info & tweet caption
    VStack(alignment: .leading, spacing: 4) {
        //user info
        HStack {
            Text("Marcus")
                .font(.subheadline).bold()

            Group {
                Text("@philosophy")

                Text("2d")
            }
            .foregroundColor(.gray)
            .font(.caption)
        }

        //tweet caption
        Text("Who burned my beard?!")
            .font(.subheadline)
            .multilineTextAlignment(.leading)
    }
}

기존의 TweetRowView는 DB의 데이터가 아닌 임시 데이터를 표시하고 있었다.
이제는 DB에 저장 돼 있는 Tweet을 표시하도록 수정할 차례다.

struct TweetRowView: View {
    let tweet: Tweet

    var body: some View {
        VStack(alignment: .leading) {
            //profile image & user info & tweet
            HStack(alignment: .top, spacing: 12) {

Tweet 구조체를 받아 사용할 수 있도록 tweet 변수를 선언했다.

HStack(alignment: .top, spacing: 12) {
    Circle()
        .frame(width: 56, height: 56)
        .foregroundColor(Color(.systemBlue))

    //user info & tweet caption
    VStack(alignment: .leading, spacing: 4) {
        //user info
        HStack {
            Text("Marcus")
                .font(.subheadline).bold()

            Group {
                Text("@philosophy")

                Text("2d")
            }
            .foregroundColor(.gray)
            .font(.caption)
        }

        //tweet caption
        Text(tweet.caption)
            .font(.subheadline)
            .multilineTextAlignment(.leading)
    }
}

이후엔 TextView에 tweet의 caption 속성을 전달하기만 하면 된다.

더보기

Source

struct TweetRowView: View {
    let tweet: Tweet

    var body: some View {
        VStack(alignment: .leading) {
            //profile image & user info & tweet
            HStack(alignment: .top, spacing: 12) {
                Circle()
                    .frame(width: 56, height: 56)
                    .foregroundColor(Color(.systemBlue))

                //user info & tweet caption
                VStack(alignment: .leading, spacing: 4) {
                    //user info
                    HStack {
                        Text("Marcus")
                            .font(.subheadline).bold()

                        Group {
                            Text("@philosophy")

                            Text("2d")
                        }
                        .foregroundColor(.gray)
                        .font(.caption)
                    }

                    //tweet caption
                    Text(tweet.caption)
                        .font(.subheadline)
                        .multilineTextAlignment(.leading)
                }
            }

            //action buttons
            HStack {
                Button {
                    // action
                } label: {
                    Image(systemName: "bubble.left")
                }

                Spacer()

                Button {
                    // action
                } label: {
                    Image(systemName: "arrow.2.squarepath")
                }

                Spacer()

                Button {
                    // action
                } label: {
                    Image(systemName: "heart")
                }

                Spacer()

                Button {
                    // action
                } label: {
                    Image(systemName: "bookmark")
                }
            }
            .font(.subheadline)
            .padding()
            .foregroundColor(.gray)

            Divider()
        }
    }
}

Tweet 가져오기
| TweetService

import Firebase

struct TweetService {
    func uploadTweet(caption: String, completion: @escaping(Bool) -> Void) {
        guard let uid = Auth.auth().currentUser?.uid else {
            return
        }

        let data = ["uid": uid,
                    "caption": caption,
                    "likes": 0,
                    "timestamp": Timestamp(date: Date())] as [String : Any]

        Firestore.firestore().collection("tweets").document().setData(data) { error in
            if let error = error {
                print("Failed to upload \(error.localizedDescription)")
                completion(false)
                return
            }

            completion(true)
        }
    }
}

TweetService는 Tweet에 관련된 기능을 모아 구현하는 구조체다.
이전에 업로드를 구현했으므로, 이번에는 반대로 받아오는 기능을 구현하면 된다.

func fetchTweets(completion: @escaping([Tweet]) -> Void) {

}

메서드의 이름은 fetchTweets으로 'fetch'는 DB에서 원하는 데이터를 받아 온다는 의미로 흔히 쓰인다.
해당 메서드는 받아온 Tweet들을 배열에 저장해 CompletionHandler에 전달하는 기능을 한다.

func fetchTweets(completion: @escaping([Tweet]) -> Void) {
    Firestore.firestore().collection("tweets").getDocuments { snapshot, _ in

    }
}

사용자들이 업로드한 tweet들은 DB의 tweets collection에 저장된다.
해당 collection에 저장된 모든 tweet을 getDocuments 메서드를 사용해 가져온다.

func fetchTweets(completion: @escaping([Tweet]) -> Void) {
    Firestore.firestore().collection("tweets").getDocuments { snapshot, _ in
        guard let documents = snapshot?.documents else {
            return
        }

        let tweets = documents.compactMap({
            try? $0.data(as: Tweet.self)
        })
        completion(tweets)
    }
}

가져 올 tweet이 없다면 그대로 종료하고,
가져올 tweet이 있다면 앞서 정의한 Tweet Model의 형태로 배열에 저장해 completion에 전달한다.

더보기

Source

import Firebase

struct TweetService {
    func uploadTweet(caption: String, completion: @escaping(Bool) -> Void) {
        guard let uid = Auth.auth().currentUser?.uid else {
            return
        }

        let data = ["uid": uid,
                    "caption": caption,
                    "likes": 0,
                    "timestamp": Timestamp(date: Date())] as [String : Any]

        Firestore.firestore().collection("tweets").document().setData(data) { error in
            if let error = error {
                print("Failed to upload \(error.localizedDescription)")
                completion(false)
                return
            }

            completion(true)
        }
    }

    func fetchTweets(completion: @escaping([Tweet]) -> Void) {
        Firestore.firestore().collection("tweets").getDocuments { snapshot, _ in
            guard let documents = snapshot?.documents else {
                return
            }

            let tweets = documents.compactMap({
                try? $0.data(as: Tweet.self)
            })
            completion(tweets)
        }
    }
}

Tweet 가져오기
| FeedViewModel

class FeedViewModel: ObservableObject {
    @Published var tweets = [Tweet]()
    let service = TweetService()

    init() {
        fetchTweets()
    }

    func fetchTweets() {
        service.fetchTweets { tweets in
            self.tweets = tweets
        }
    }
}

FeedView는 Twitter 앱의 주요 화면으로, 사용자들이 올린 Tweet이 표시된다.
TweetService에서 정의한 fetchTweets 메서드를 호출하도록 메서드를 재정의한다.
이렇게 반환된 Tweet들은 Published 변수인 tweets에 저장된다.

Tweet 가져오기
| FeedView

struct FeedView: View {
    @State private var showNewTweetView = false

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            ScrollView {
                LazyVStack {
                    ForEach(0 ... 20, id: \.self) { _ in
                        TweetRowView()
                            .padding()
                    }
                }
            }

            Button {
                showNewTweetView.toggle()
            } label: {
                Image("tweet")
                    .resizable()
                    .renderingMode(.template)
                    .frame(width: 28, height: 28)
                    .padding()
            }
            .background(Color(.systemBlue))
            .foregroundColor(.white)
            .clipShape(Circle())
            .padding()
            .fullScreenCover(isPresented: $showNewTweetView) {
                NewTweetView()
            }
        }
    }
}

지금까지의 FeedView는 실제 Tweet를 가져올 수 없었기 때문에
ForEah를 사용해 임시로 생성한 TweetRowView를 호출할 뿐이었다.

struct FeedView: View {
    @State private var showNewTweetView = false
    @ObservedObject var viewModel = FeedViewModel()

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            ScrollView {

앞서 정의한 FeedViewModel을 사용할 수 있도록 ObseredObject 변수로 FeedViewModel을 호출한다.
생성자에서 fetchTweets 메서드를 호출하도록 돼있으므로 이렇게 호출하는 순간 DB에서 Tweet을 받아오게 된다.

ScrollView {
    LazyVStack {
        ForEach(viewModel.tweets) { tweet in
            TweetRowView(tweet: tweet)
                .padding()
        }
    }
}

임시 데이터로 TweetRowView를 표시하던 ForEach문에 실제 데이터를 연결한다.
viewModel의 tweets 변수는 배열의 형식으로, 이들을 tweet으로 열거해 TweetRowView로 전달한다.

더보기

Source

struct FeedView: View {
    @State private var showNewTweetView = false
    @ObservedObject var viewModel = FeedViewModel()

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            ScrollView {
                LazyVStack {
                    ForEach(viewModel.tweets) { tweet in
                        TweetRowView(tweet: tweet)
                            .padding()
                    }
                }
            }

            Button {
                showNewTweetView.toggle()
            } label: {
                Image("tweet")
                    .resizable()
                    .renderingMode(.template)
                    .frame(width: 28, height: 28)
                    .padding()
            }
            .background(Color(.systemBlue))
            .foregroundColor(.white)
            .clipShape(Circle())
            .padding()
            .fullScreenCover(isPresented: $showNewTweetView) {
                NewTweetView()
            }
        }
    }
}

'프로젝트 > Twitter Clone App (w∕Firebase)' 카테고리의 다른 글

21. DB와 연결하기 #3  (0) 2023.01.18
20. 기능 구현 #8  (0) 2023.01.17
18. 기능 구현 #6  (0) 2023.01.13
17. 기능 구현 #5  (0) 2023.01.12
16. 코드 가독성 개선  (0) 2023.01.11