본문 바로가기

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

25. DB와 연결하기 #4

DB와 연결하기 #4
좋아요 한 Tweet 표시하기


ProfileView의 Likes 탭에는 해당 계정이 작성했던 Tweet도, 답변을 달았던 Tweet도 아닌 좋아요를 눌렀던 Tweet을 표시해야 한다.
이전에 구현한 좋아요 기능에서 사용하는 user-likes 하위 collection 활용하면 굉장히 간단하게 구현할 수 있다.

좋아요 한 Tweet 표시하기
| TweetService, fetchLikedTweets

fetchTweets(foruid:)

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

        let tweets = documents.compactMap({
            try? $0.data(as: Tweet.self)
        })
        completion(tweets.sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue() }))
    }
}

메서드의 원형은 앞서 구현한 fetTweets다.
해당 메서드는 uid를 사용해 Firebase의 일치하는 Tweet을 받아오게 되는데,
비슷한 방식으로 필터링 규칙만 조금 달라질 뿐이다.

func fetchLikedTweets(forUid uid: String, completion: @escaping([Tweet]) -> Void) {
    var tweets = [Tweet]()

    Firestore.firestore().collection("users").document(uid).collection("user-likes").getDocuments { snapshot, _ in
        guard let documents = snapshot?.documents else {
            return
        }
    }
}

우선 profileView에 표시되고 있는 사용자의 user-likes 하위 collection을 전부 가져온다.

users collection에서 일치하는 uid의 Document를 찾고,
해당 Document 내에서 다시 한번 user-likes 하위 collection에 접근한다.
이후 user-likes에 저장된 모든 Documents를 받아와
snapshot으로 바인딩 한 뒤, 내용을 documents로 다시 한번 바인딩한다.

func fetchLikedTweets(forUid uid: String, completion: @escaping([Tweet]) -> Void) {
    var tweets = [Tweet]()

    Firestore.firestore().collection("users").document(uid).collection("user-likes").getDocuments { snapshot, _ in
        guard let documents = snapshot?.documents else {
            return
        }

        documents.forEach { document in
            let tweetID = document.documentID

            Firestore.firestore().collection("tweets").document(tweetID).getDocument { snapshot, _ in
                guard let tweet = try? snapshot?.data(as: Tweet.self) else {
                    return
                }

                tweets.append(tweet)

                completion(tweets)
            }
        }
    }
}

바인딩이 성공한 경우 documents를 열거하며 해당하는 Tweet을 Tweet 구조체로 변환한 뒤,
tweets 배열에 담아 completion에 전달한다.

좋아요 한 Tweet 표시하기
| ProfileViewModel

ProfileView에 표시할 데이터를 만들기 위해 ProfileViewModel을 수정한다.

class ProfileViewModel: ObservableObject {
    @Published var tweets = [Tweet]()
    @Published var likedTweets = [Tweet]()

    private let service = TweetService()
    let user: User
    .
    .
    .

현재 ProfileView에 표시되고 있는 계정의 Tweet들은 tweets 배열에 저장되므로,
좋아요 한 Tweet을 저장하기 위해 likedTweets 배열을 새로 선언해 준다.

func fetchLikedTweets() {
    guard let uid = user.id else {
        return
    }

    service.fetchLikedTweets(forUid: uid) { tweets in
        self.likedTweets = tweets

        for i in 0 ..< tweets.count {
            let uid = tweets[i].uid

            self.userService.fetchUser(withUid: uid) { user in
                self.likedTweets[i].user = user
            }
        }
    }
}

새로 구현한 fetchLikedTweets를 재정의 해 준다.
해당 메서드는 좋아요 한 Tweet들의 배열을 반환하므로
completionHandler에서는 Tweet 표시에 필요한 계정 정보를 채워 새로 선언한 likedTweets 배열에 그대로 저장해 주면 된다.

init(user: User) {
    self.user = user
    self.fetchUserTweets()
    self.fetchLikedTweets()
}

ProfileViewModel은 ProfileView가 처음 화면에 표시되는 시점에 호출된다.
따라서 생성자에 방금 재정의한 fetchLikedTweets를 호출해 likedTweets 배열에 데이터를 채울 수 있도록 한다.

좋아요 한 Tweet 표시하기
| ProfileView와 ProfileViewModel의 연동

ProfileView에는 TweetsFilterBar가 존재한다.
ProfileViewModel에는 해당하는 View에 알맞게 표시될 수 있도록 배열을 가지고 있는데,
이 둘을 연결하는 작업이 필요하다.

TweetView > tweetFilterBar

var tweetFilterBar: some View {
HStack {
    ForEach(TweetFilterViewModel.allCases, id: \.rawValue) { item in
        VStack {
            Text(item.title)
                .font(.subheadline)
                .fontWeight(selectedFilter == item ? .semibold : .regular)
                .foregroundColor(selectedFilter == item ? .black : .gray)

            if selectedFilter == item {
                Capsule()
                    .foregroundColor(Color(.systemBlue))
                    .frame(height: 3)
                    .matchedGeometryEffect(id: "filter", in: animation)
            } else {
                Capsule()
                    .foregroundColor(Color(.clear))
                    .frame(height: 3)
            }
        }
        .onTapGesture {
            withAnimation(.easeOut) {
                self.selectedFilter = item
            }
        }
    }
}
.overlay {
    Divider().offset(x: 0, y: 16)
}

TweetFilterBar는 TweetFilterViewModel을 열거하여 표시되고 있고,
선택된 tab은 selectedFilter에 저장돼 tab 간 전환이 동작한다.

TweetFilterViewModel

enum TweetFilterViewModel: Int, CaseIterable {
    case tweets
    case replies
    case likes

    var title: String {
        switch self {
        case .tweets: return "Tweets"
        case .replies: return "Replies"
        case .likes: return "Likes"
        }
    }
}

TweetFilterViewModel은 위와 같이 case로 각각의 tab을 구별하고 있다.
따라서 현재 선택한 tab이 사용하고 있는 TweetFilterViewModel의 case를 지표로 해당하는 배열을 표시하기만 하면 된다.

func tweets(forFilter filter: TweetFilterViewModel) -> [Tweet] {
    switch filter {
    case .tweets:
        return tweets
    case .replies:
        return tweets
    case .likes:
        return likedTweets
    }
}

forFilter 파라미터로 현재 선택된 tab의 TweetFilterViewModel의 case를 받아 분기하여 해당하는 배열을 반환한다.

ProfileView > tweetsView
before

var tweetsView: some View {
    ScrollView {
        LazyVStack {
            ForEach(viewModel.tweets) { tweet in
                TweetRowView(tweet: tweet)
                    .padding()
            }
        }
    }
}

ProfileView > tweetsView
after

var tweetsView: some View {
    ScrollView {
        LazyVStack {
            ForEach(viewModel.tweets(forFilter: self.selectedFilter)) { tweet in
                TweetRowView(tweet: tweet)
                    .padding()
            }
        }
    }
}

앞서 구현한 tweets(forFilter:)를 호출하고,
현재 선택된 tab을 전달하기 위해 selectedFilter를 그대로 전달하기만 하면 된다.
반환되는 배열은 completionHandler에서 TweetRowView에 전달해 표시하기만 하면 된다.

결과

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

26. 기본 UI 구현하기 #9  (0) 2023.01.20
24. 기능 구현 #10  (0) 2023.01.20
23. 기능 구현 #9  (0) 2023.01.20
22. 버그수정 #2  (0) 2023.01.18
21. DB와 연결하기 #3  (0) 2023.01.18