본문 바로가기

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

21. DB와 연결하기 #3

DB와 연결하기 #3
ProfileView에 실제 데이터 연결하기


ProfileView에 실제 데이터 연결하기
| ProfileView

더보기

Source

import SwiftUI
import Kingfisher

struct ProfileView: View {
    @State private var selectedFilter: TweetFilterViewModel = .tweets
    @Environment(\.dismiss) var dismiss
    @Namespace var animation
    private let user: User

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

    var body: some View {
        VStack(alignment: .leading) {
            headerView

            actionButtons

            userInfoDetails

            tweetFilterBar

            tweetsView

            Spacer()
        }
    }
}

extension ProfileView {
    var headerView: some View  {
        ZStack(alignment: .bottomLeading) {
            Color(.systemBlue)
                .ignoresSafeArea()

            VStack {
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "arrow.left")
                        .resizable()
                        .frame(width: 20, height: 16)
                        .foregroundColor(.white)
                }
                .offset(x: 16, y: 12)

                KFImage(URL(string: user.profileImageUrl))
                    .resizable()
                    .scaledToFill()
                    .clipShape(Circle())
                    .frame(width: 72, height: 72)
                    .offset(x: 16, y: 24)
            }
        }
        .frame(height: 96)
        .toolbar(.hidden, for: .navigationBar)
    }

    var actionButtons: some View {
        HStack(spacing: 12) {
            Spacer()

            Image(systemName: "bell.badge")
                .font(.title3)
                .padding(6)
                .overlay {
                    Circle()
                        .stroke(Color.gray, lineWidth: 0.75)
                }

            Button {

            } label: {
                Text("Edit Profile")
                    .font(.subheadline).bold()
                    .frame(width: 120, height: 32)
                    .overlay {
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(Color.gray, lineWidth: 0.75)
                    }
            }
        }
        .padding(.trailing)
    }

    var userInfoDetails: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack {
                Text(user.fullname)
                    .font(.title2).bold()

                Image(systemName: "checkmark.seal.fill")
                    .foregroundColor(Color(.systemBlue))
            }

            Text("@\(user.username)")
                .font(.subheadline)
                .foregroundColor(.gray)

            Text("Eureka!")
                .font(.subheadline)
                .padding(.vertical)

            HStack(spacing: 24) {
                HStack {
                    Image(systemName: "mappin.and.ellipse")

                    Text("Syracuse, Italia")
                }

                HStack {
                    Image(systemName: "link")

                    Text("https://chillog.page")
                }
            }
            .font(.caption)
            .foregroundColor(.gray)

            UserStatsView()
                .padding(.vertical
            )
        }
        .padding(.horizontal)
    }

    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)
        }
    }

    var tweetsView: some View {
        ScrollView {
            LazyVStack {
                ForEach(0 ... 9, id: \.self) { _ in
//					TweetRowView()
//						.padding()
                }
            }
        }
    }
}

지금까지 완성한 ProfileView는 생성자를 사용해 단순히 계정의 정보를 표시하고 있지만,
그 아래각각의 탭에 해당하는 Tweet들을 나열해 표시할 필요가 있다.
여러 기능 중 Tweets 탭에 해당하는 Profile에 표시되는 사용자의 Tweet을 나열해 표시하도록 구현한다.

ProfileView에 실제 데이터 연결하기
| TweetService

fetchTweets

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)
    }
}

원형은 TweetService의 fetchTweets 메서드다.
이 메서드는 tweets collection의 모든 데이터를 받아오는 메서드로,
기능은 거의 비슷하지만 불필요한 정보를 받아오게 되는 문제가 있다.

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)
    }
}

fetchTweets(foruid:) 메서드는 파라미터로 uid를 전달받아 해당 uid의 사용자가 작성한 Tweet을 반환하는 역할을 한다.
Firestore 명령어에서 collection 지정과 getDocuments 사이에 whereField 메서드가 추가돼
파라미터로 전달받은 uid를 그대로 사용하는 부분만 제외하면 fetchTweets 메서드와 다른 부분이 존재하지 않는다.

ProfileView에 실제 데이터 연결하기
| ProfileViewModel

ProfileView에 표시할 데이터를 전달하기 위해 ProfileViewModel을 정의한다.
해당 클래스의 원형은 FeedViewModel의 fetchTweets 메서드가 원형이다.

FeedViewModel

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

    init() {
        fetchTweets()
    }

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

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

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

ProfileViewModel

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

    private let service = TweetService()
    let user: User

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

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

        service.fetchTweets(foruid: uid) { tweets in
            self.tweets = tweets

            for i in 0 ..< tweets.count {
                self.tweets[i].user = self.user
            }
        }
    }
}

가장 큰 차이점은 ProfileView는 FeedView와 다르게 어떤 사용자인지 판단할 지표가 필요하다는 점이다.
파라미터로 User 형식의 user 변수를 사용하고, 해당 변수를 토대로 fetchUserTweets를 호출한다.

ProfileView에 실제 데이터 연결하기
| ProfileView

import SwiftUI
import Kingfisher

struct ProfileView: View {
    @State private var selectedFilter: TweetFilterViewModel = .tweets
    @ObservedObject var viewModel: ProfileViewModel
    @Environment(\.dismiss) var dismiss
    @Namespace var animation
//	private let user: User

앞서 정의한 ProfileViewModel을 ObservedObject 변수로 할당하고,
ProfileViewModel에 중복된 user 변수가 존재하므로 해당 라인은 삭제한다.

init(user: User) {
    //self.user = user
    self.viewModel = ProfileViewModel(user: user)
}

user 변수를 초기화 하는 코드도 ProfileViewModel을 사용해 진행하도록 코드를 수정한다.

var headerView: some View  {
        ZStack(alignment: .bottomLeading) {
            Color(.systemBlue)
                .ignoresSafeArea()

            VStack {
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "arrow.left")
                        .resizable()
                        .frame(width: 20, height: 16)
                        .foregroundColor(.white)
                }
                .offset(x: 16, y: 12)

                KFImage(URL(string: viewModel.user.profileImageUrl))
                    .resizable()
                    .scaledToFill()
                    .clipShape(Circle())
                    .frame(width: 72, height: 72)
                    .offset(x: 16, y: 24)
            }
        }
        .frame(height: 96)
        .toolbar(.hidden, for: .navigationBar)
    }
var userInfoDetails: some View {
    VStack(alignment: .leading, spacing: 4) {
        HStack {
            Text(viewModel.user.fullname)
                .font(.title2).bold()

            Image(systemName: "checkmark.seal.fill")
                .foregroundColor(Color(.systemBlue))
        }

        Text("@\(viewModel.user.username)")
            .font(.subheadline)
            .foregroundColor(.gray)

        Text("Eureka!")
            .font(.subheadline)
            .padding(.vertical)

        HStack(spacing: 24) {
            HStack {
                Image(systemName: "mappin.and.ellipse")

                Text("Syracuse, Italia")
            }

            HStack {
                Image(systemName: "link")

                Text("https://chillog.page")
            }
        }
        .font(.caption)
        .foregroundColor(.gray)

        UserStatsView()
            .padding(.vertical
        )
    }
    .padding(.horizontal)
}

사용자의 정보도 위치가 바뀐 user 변수에 맞도록 코드를 수정한다.

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

tweetsView는 새롭게 연결된 ProfileViewModel인 viewModel에서 표시할 tweets 배열을 전달하도록 코드를 수정한다.

결과

이제 계정별 ProfileView에서는 각각의 사용자에 대한 알맞은 정보와 작성한 Tweet이 보이게 된다.

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

23. 기능 구현 #9  (0) 2023.01.20
22. 버그수정 #2  (0) 2023.01.18
20. 기능 구현 #8  (0) 2023.01.17
19. 기능 구현 #7  (0) 2023.01.14
18. 기능 구현 #6  (0) 2023.01.13