[SwiftUI] TwitterClone: Upload Tweets

Junyoung ParkΒ·2022λ…„ 11μ›” 18일
0

SwiftUI

λͺ©λ‘ 보기
109/136
post-thumbnail
post-custom-banner

πŸ”΄ Let's Build Twitter with SwiftUI (iOS 15, Xcode 13, Firebase, SwiftUI 3.0)

TwitterClone: Upload Tweets

κ΅¬ν˜„ λͺ©ν‘œ

  • νŠΈμœ„ν„° μ—…λ‘œλ“œ 및 데이터 패치

κ΅¬ν˜„ νƒœμŠ€ν¬

  • νŠΈμœ„ν„° μž‘μ„± λ·°λ₯Ό ν†΅ν•œ νŒŒμ΄μ–΄μŠ€ν† μ–΄ 데이터 μž‘μ„±
  • νŠΈμœ„ν„° 패치 ν•¨μˆ˜λ₯Ό ν†΅ν•œ 데이터 λ””μ½”λ”©
  • 데이터 퍼블리셔 ꡬ독을 ν†΅ν•œ UI 패치
  • λ¦¬ν”„λ ˆμ‹œλ₯Ό ν†΅ν•œ 데이터 패치

핡심 μ½”λ“œ

import Foundation

class UploadTweetViewModel: ObservableObject {
    let service = TweetService()
    @Published var caption: String = ""
    @Published var didUploadTweet: Bool = false
    func uploadTweet() {
        service.uploadTweet(caption: caption) { [weak self] success in
            self?.didUploadTweet = success
        }
    }
}
  • νŠΈμœ„ν„° μž‘μ„± 및 패치 κ΄€λ ¨ 데이터 μ„œλΉ„μŠ€ 클래슀λ₯Ό μ‚¬μš©ν•˜λŠ” λ·° λͺ¨λΈ
  • νŠΈμœ„ν„° μž‘μ„± λ·°μ—μ„œ ν•΄λ‹Ή λ·° λͺ¨λΈμ„ μ‚¬μš©
import Foundation
import SwiftUI
import FirebaseFirestoreSwift
import Firebase

struct TweetModel: Codable, Identifiable {
    @DocumentID var id: String?
    let uid: String
    let caption: String
    let likes: Int
    let timestamp: Timestamp
    var user: UserModel?
}
  • μž‘μ„±λ˜λŠ” νŠΈμœ„ν„°λŠ” ν•΄λ‹Ή 데이터 μ–‘μ‹μœΌλ‘œ μž‘μ„±
  • νŒŒμ΄μ–΄μŠ€ν† μ–΄μ˜ λ‹€νλ¨ΌνŠΈ IDλ₯Ό @DocumentID ν”„λ‘œν† μ½œμ„ λ”°λ¦„μœΌλ‘œμ¨ μžλ™μœΌλ‘œ λ””μ½”λ”© μ‹œ λΆ€μ—¬λ°›μŒ
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(error.localizedDescription)
                    completion(false)
                } else {
                    print("Upload Tweet Did Succeed")
                    completion(true)
                }
            }
    }
    
    func fetchTweets(completion: @escaping(Result<[TweetModel], Error>) -> Void) {
        Firestore.firestore().collection("tweets")
            .order(by: "timestamp", descending: true)
            .getDocuments { snapshot, error in
            guard
                let documents = snapshot?.documents,
                error == nil else {
                if let error = error {
                    completion(.failure(error))
                } else {
                    completion(.failure(URLError(.badURL)))
                }
                return
            }
            let tweets = documents.compactMap({try? $0.data(as: TweetModel.self)})
            completion(.success(tweets))
        }
    }
    
    func fetchTweets(for uid: String, completion: @escaping(Result<[TweetModel], Error>) -> Void) {
        Firestore.firestore().collection("tweets")
            .whereField("uid", isEqualTo: uid)
            .getDocuments { snapshot, error in
                guard
                    let documents = snapshot?.documents,
                    error == nil else {
                    if let error = error {
                        print(error.localizedDescription)
                        completion(.failure(error))
                    } else {
                        completion(.failure(URLError(.badURL)))
                    }
                    return
                }
                let tweets = documents.compactMap({ try? $0.data(as: TweetModel.self)}).sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue() })
                completion(.success(tweets))
            }
    }
}
  • νŠΈμœ„ν„°λ₯Ό μ—…λ‘œλ“œν•˜λŠ” ν•¨μˆ˜, 성곡 μ—¬λΆ€λ₯Ό ν•Έλ“€λŸ¬λ‘œ 리턴
  • μ—…λ‘œλ“œλœ νŠΈμœ„ν„°λ₯Ό 전체 λ˜λŠ” μœ μ € 아이디λ₯Ό 톡해 κ°€μ Έμ˜¨ λ’€ λ¦¬ν„΄ν•˜λŠ” ν•¨μˆ˜
import Foundation

class FeedViewModel: ObservableObject {
    @Published var tweets: [TweetModel] = []
    private let service = TweetService()
    private let userService = UserService()
    
    init() {
        fetchTweets()
    }
    
    func fetchTweets() {
        service.fetchTweets { [weak self] result in
            switch result {
            case .success(let tweets):
                self?.tweets = tweets
                for idx in 0..<tweets.count {
                    let uid = tweets[idx].uid
                    
                    self?.userService.fetchUser(with: uid, completion: { result in
                        switch result {
                        case .failure(let error): print(error.localizedDescription)
                        case .success(let user): self?.tweets[idx].user = user
                        }
                    })
                }
            case .failure(let error): print(error.localizedDescription)
            }
        }
    }
}
  • 전체 ν”Όλ“œλ₯Ό λ³΄μ—¬μ£ΌλŠ” ν”Όλ“œ λ·°κ°€ μ‚¬μš©ν•˜λŠ” λ·° λͺ¨λΈ
 ScrollView {
                LazyVStack {
                    ForEach(viewModel.tweets) { tweet in
                        TweetRowView(tweet: tweet)
                            .padding()
                    }
                }
            }
            .refreshable {
                viewModel.fetchTweets()
            }
  • ν”Όλ“œλ₯Ό λ³΄μ—¬μ£ΌλŠ” 슀크둀 뷰에 refreshable을 톡해 pullToRefreshλ₯Ό ν•  λ•Œλ§ˆλ‹€ μžλ™μœΌλ‘œ λ·° λͺ¨λΈμ˜ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ„λ‘ μ„ μ–Έ
@ObservedObject private var viewModel: TweetRowViewModel
    
    init(tweet: TweetModel) {
        _viewModel = ObservedObject(wrappedValue: TweetRowViewModel(tweet: tweet))
    }
  • 각 νŠΈμœ„ν„° ν”Όλ“œμ— ν•΄λ‹Ήν•˜λŠ” 둜우 λ·°λ₯Ό μ΄λ‹ˆμ…œλΌμ΄μ¦ˆν•˜λŠ” μˆ˜λ‹¨
  • νŠΈμœ„ν„° λͺ¨λΈμ„ νŒŒλΌλ―Έν„°λ‘œ 받은 λ’€ ObserableObjectλ₯Ό μ΄λ‹ˆμ…œλΌμ΄μ¦ˆν•˜κΈ° μœ„ν•΄ μ‚¬μš©
import Foundation
import SwiftUI

class TweetRowViewModel: ObservableObject {
    let tweet: TweetModel
    @Published var profileImage: UIImage?
    
    init(tweet: TweetModel) {
        self.tweet = tweet
        if let urlString = tweet.user?.profileImageURL {
            downloadProfileImage(with: urlString)
        }
    }
    
    private func downloadProfileImage(with urlString: String) {
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard
                let response = response as? HTTPURLResponse,
                response.statusCode >= 200,
                response.statusCode < 400,
                error == nil,
                let data = data,
                let image = UIImage(data: data) else { return }
            DispatchQueue.main.async { [weak self] in
                self?.profileImage = image
            }
        }
        .resume()
    }
    
}
  • ν•΄λ‹Ή νŠΈμœ„ν„° 데이터λ₯Ό 톡해 λ·° λͺ¨λΈμ€ ν•΄λ‹Ή 데이터λ₯Ό μƒμˆ˜λ‘œ 가지고 있음과 λ™μ‹œμ— 주어진 μœ μ € ν”„λ‘œν•„ 이미지 URL을 톡해 이미지λ₯Ό λ‹€μš΄λ‘œλ“œ 및 패치
  • @Published둜 λ°›κ³  μžˆλŠ” κΉŒλ‹­μ— ν•΄λ‹Ή λ·°μ—μ„œ 이미지 패치 여뢀에 따라 이미지λ₯Ό μƒˆλ‘­κ²Œ λ Œλ”λ§ κ°€λŠ₯

κ΅¬ν˜„ ν™”λ©΄

profile
JUST DO IT
post-custom-banner

0개의 λŒ“κΈ€