Clone App/Twitter

[Twitter Clone] Show Tweet in Home View

밤새는 탐험가89 2024. 6. 12. 22:47

🟨 구현 화면

 

 

 

🟨 Tweet.swift

import Foundation


struct Tweet: Codable, Identifiable {
    var id = UUID().uuidString
    let author: TwitterUser
    let authorID: String
    let tweetContent: String
    var likesCount: Int
    var likers: [String]
    let isReply: Bool
    let parentReference: String?
}

 

 

🟨 DatabaseManager.swift

  • collectionTweets(retreiveTweets ...) 메서드 생성 
    • firebase database 내에서 author.id를 통해 얻은 id를 기준으로 Tweet 타입의 데이터를 불러온다. 
import Foundation
import Firebase
import FirebaseFirestoreSwift
import FirebaseFirestoreCombineSwift
import Combine



class DatabaseManager {
    
    static let shared = DatabaseManager()
    
    
    let db = Firestore.firestore()
    let usersPath: String = "users"
    let tweetsPath: String = "tweets"
    
    
    ...

    func collectionTweets(retreiveTweets forUserID: String) -> AnyPublisher<[Tweet], Error> {
        db.collection(tweetsPath).whereField("author.id", isEqualTo: forUserID)
            .getDocuments()
            .tryMap(\.documents)
            .tryMap { snapshots in
                try snapshots.map({
                    try $0.data(as: Tweet.self)
                })
            }
            .eraseToAnyPublisher()
    }

 

 

🟥 tryMap?

  • map이랑 비슷한데 앞에 try가 붙었다.
  • tryMap은 제공된 error-throwing closure를 사용하여 upstream publisher의 모든 요소를 변환한다.
  • tryMap같은 경우, 클로져가 오류를 발생하면 publish를 종료한다.

 

 

🟨 HomeViewViewModel.swift

  • fetchTweets() 메서드 생성 
    • userID에 맞는 Tweet 타입의 데이터를 호출한다. 
  • retreiveUser() 메서드 내에 handleEvents를 통해 fetchTweets() 를 호출한다.
import Foundation
import Combine
import FirebaseAuth


final class HomeViewViewModel: ObservableObject {
    
    @Published var user: TwitterUser?
    @Published var error: String?
    @Published var tweets: [Tweet] = []
    
    private var subscriptions: Set<AnyCancellable> = []
    
    
    func retreiveUser() {
        guard let id = Auth.auth().currentUser?.uid else { return }
        DatabaseManager.shared.collectionUsers(retreive: id)
            .handleEvents(receiveOutput: { [weak self] user in
                self?.user = user
                self?.fetchTweets()
            })
            .sink { [weak self] completion in
                if case .failure(let error) = completion {
                    self?.error = error.localizedDescription
                }
            } receiveValue: { [weak self] user in
                self?.user = user
            }
            .store(in: &subscriptions)
    }
    
    
    func fetchTweets() {
        guard let userID = user?.id else { return }
        DatabaseManager.shared.collectionTweets(retreiveTweets: userID)
            .sink { [weak self] completion in
                if case .failure(let error) = completion {
                    self?.error = error.localizedDescription
                }
            } receiveValue: { [weak self] retreivedTweets in
                self?.tweets = retreivedTweets
            }
            .store(in: &subscriptions)

    }
}

 

 

 

🟨 HomeViewController.swift

  •  bindView() 메서드 내에 $tweets 값을 연결하여, 변동이 있을 때마다 timelineTableView를 새로고침하게 한다.
import UIKit
import FirebaseAuth
import Combine

class HomeViewController: UIViewController {
    
    private var viewModel = HomeViewViewModel()
    
    private var subscriptions: Set<AnyCancellable> = []
    
 

    override func viewDidLoad() {
        super.viewDidLoad()
   			
        ... 
        bindViews()    
    }
    
    @objc private func didTapSignOut() {
        try? Auth.auth().signOut()
        handleAuthentication()
    }
    ...
    
    // 프로필뷰에서 네비게이션 숨기기에 따라 홈 -> 프로필 -> 홈으로 돌아오는 과정에서 홈에도 네비게이션이 숨기기로 나오는 것을 고침
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.navigationBar.isHidden = false
        
        // 로그인 여부 확인
        handleAuthentication()
        
        viewModel.retreiveUser()
    }
    
    ... 
    
    func bindViews() {
        ...
        viewModel.$tweets.sink { [weak self] _ in
            DispatchQueue.main.async {
                self?.timelineTableView.reloadData()
            }
        }
        .store(in: &subscriptions)
    }
    ... 
}

extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.tweets.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: TweetTableViewCell.identifier, 
                                                       for: indexPath) as? TweetTableViewCell  
        else {
            return UITableViewCell()
        }
        
        let tweetModel = viewModel.tweets[indexPath.row]
        cell.configureTweet(with: tweetModel.author.displayName, 
        					username: tweetModel.author.username, 
                            tweetTextContent: tweetModel.tweetContent, 
                            avatarPath: tweetModel.author.avatarPath)
        
    
        cell.delegate = self
        
        return cell
    }
    
}
...

 

 

🟨 TweetTableViwCell.swift

  •  configureTweet() 메서드 생성 
    • 각 UI에 연결하여 값을 보여준다.
import UIKit


// 데이터 전달 목적으로 델리게이트 패턴 사용 - 프로토콜 선언 - 1
protocol TweetTableViewCellDelegate: AnyObject {
    func tweetTableViewCellDidTapReply()
    func tweetTableViewCellDidTapRetweet()
    func tweetTableViewCellDidTapLike()
    func tweetTableViewCellDidTapShare()
}



class TweetTableViewCell: UITableViewCell {
    
    // 대리자 선언 - 2
    weak var delegate: TweetTableViewCellDelegate?
    
    static let identifier = "TweetTableViewCell"
    
    private let actionSpacing: CGFloat = 60
    
    ... 
    
    func configureTweet(with displayName: String, username: String, tweetTextContent: String, avatarPath: String) {
        displayNameLabel.text = displayName
        userNameLabel.text = "@\(username)"
        tweetTextContentLabel.text = tweetTextContent
        avatarImageView.sd_setImage(with: URL(string: avatarPath))
    }
}