본문 바로가기

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

13. DB와 연결하기 #2

DB와 연결하기 #2
프로필 사진 표시하기


프로필 사진 표시하기
| Kingfisher import 하기

Kingfisher

 

GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

A lightweight, pure-Swift library for downloading and caching images from the web. - GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

github.com

URL을 사용해 네트워크에서 쉽게 이미지를 다운로드하여 사용할 수 있는 Package다.
Swift를 사용해 만들어 졌기에 간단하게 사용할 수 있다.

페이지의 링크를 사용해 Swift Package Manager로 추가해 주면 된다.

프로필 사진 표시하기
| ContentView > toolbar

import SwiftUI
import Kingfisher

struct ContentView: View {
    @State private var showMenu = false
    @EnvironmentObject var viewModel: AuthViewModel

    var body: some View {

kingfisher를 설치했다면 사용할 코드에서 import해 사용하면 된다.
Kingfisher로 import 했다.

extension ContentView {
    var mainInterfaceView: some View {
        ZStack(alignment: .topLeading) {
           .
           .
           .
        }
        .navigationTitle("Home")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                Button {
                    withAnimation(.easeInOut) {
                        showMenu.toggle()
                    }
                } label: {
                    Circle()
                        .frame(width: 32, height: 32)
                }
            }
        }
        .onAppear {
            showMenu = false
        }
    }
}

Tweet들을 나열해 보여주는 FeedView는 좌측 상단에 프로필 이미지를 표시하고,
터치 시 현재 로그인된 계정의 정보와 프로필 사진을 보여주는 SideMenu를 표시한다.
지금은 표시할 이미지가 없어 파란색 원에 불과하지만 현재 접속 중인 계정의 프로필 사진을 표시하도록 구현한다.

        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                if let user = viewModel.currentUser {
                    
                }
            }
        }

현재 접속정보를 얻기 위해 if-let 문을 사용해 currentUser 속성을 확인한다.

        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                if let user = viewModel.currentUser {
                    Button {
                        withAnimation(.easeInOut) {
                            showMenu.toggle()
                        }
                    } label: {
                       
                    }
                }
            }
        }

기능 자체는 기존의 Button과 동일하게 SideMenue를 toggle 하도록 구현한다.

        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                if let user = viewModel.currentUser {
                    Button {
                        withAnimation(.easeInOut) {
                            showMenu.toggle()
                        }
                    } label: {
                        KFImage(URL(string: user.profileImageUrl))
                            .resizable()
                            .scaledToFill()
                            .clipShape(Circle())
                            .frame(width: 32, height: 32)
                    }
                }
            }
        }

KFImage 메서드의 parameter에 현재 계정의 profileImageUrl을 전달하기만 하면 간단히 이미지를 불러온다.
디자인에 맞게 이미지를 변형할 수 있도록 resizable 등의 modifier를 적용하면 된다.

더보기

Source


import SwiftUI
import Kingfisher

struct ContentView: View {
    @State private var showMenu = false
    @EnvironmentObject var viewModel: AuthViewModel

    var body: some View {
        Group {
            //need login
            if viewModel.userSession == nil {
                LoginView()
            } else {
                //login successfuly
                mainInterfaceView
            }
        }
    }
}

extension ContentView {
    var mainInterfaceView: some View {
        ZStack(alignment: .topLeading) {
            MainTabView()
                .toolbar(showMenu ? .hidden : .visible)

            if showMenu {
                ZStack {
                    withAnimation(.easeInOut) {
                        Color(.black)
                            .opacity(showMenu ? 0.25 : 0.0)
                    }
                }.onTapGesture {
                    withAnimation(.easeInOut) {
                        showMenu = false
                    }
                }
                .ignoresSafeArea()
            }

            SideMenuView()
                .frame(width: 300)
                .offset(x: showMenu ? 0 : -300, y: 0)
        }
        .navigationTitle("Home")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar{
            ToolbarItem(placement: .navigationBarLeading) {
                if let user = viewModel.currentUser {
                    Button {
                        withAnimation(.easeInOut) {
                            showMenu.toggle()
                        }
                    } label: {
                        KFImage(URL(string: user.profileImageUrl))
                            .resizable()
                            .scaledToFill()
                            .clipShape(Circle())
                            .frame(width: 32, height: 32)
                    }
                }
            }
        }
        .onAppear {
            showMenu = false
        }
    }
}

프로필 사진 표시하기
| ProfileView

import SwiftUI
import Kingfisher

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

마찬가지로 Kingfisher를 import 한다.

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
    }

ProfileView는 대상 사용자의 정보를 표시한다.
따라서 파라미터를 하나 만들어 User 타입의 데이터를 받아 올 수 있도록 구현한다.
전달된 User 데이터는 ProfileView가 초기화 되는 시점에 user로 바인딩된다.

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

전달된 user 데이터의 profileImageUrl 속성을 사용해 KFImage 메서드를 호출해 이미지를 받아 온다.
디자인에 맞춰 resizable 등의 modifier를 적용한다.

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

UserInfoDetails에서는 전달된 user의 계정과 이름을 표시한다.
각각 username과 fullname 속성을 전달한다.

더보기

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

프로필 사진 표시하기
| SideMenu

struct SideMenuView: View {
    @EnvironmentObject var authViewModel: AuthViewModel

    var body: some View {
        if let user = authViewModel.currentUser {
            VStack(alignment: .leading, spacing: 32) {
                VStack(alignment: .leading) {
                    Circle()
                        .frame(width: 48, height: 48)

                    VStack(alignment: .leading, spacing: 4) {
                        Text(user.fullname)
                            .font(.headline)

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

                    UserStatsView()
                        .padding(.vertical)
                }
                .padding(.leading)

                ForEach(SideMenuViewModel.allCases, id: \.rawValue) { viewModel in
                    if viewModel == .profile {
                        NavigationLink(destination: ProfileView()) {
                            SideMenuRowView(viewModel: viewModel)
                        }
                    } else if viewModel == .logout {
                        Button {
                            authViewModel.signOut()
                        } label: {
                            SideMenuRowView(viewModel: viewModel)
                        }
                    } else {
                        SideMenuRowView(viewModel: viewModel)
                    }
                }

                Spacer()
            }
            .background(Color.white)
        }
    }
}

기존까지는 사진을 제외한 계정의 이름과 사용자 명만 표시했다.
위에서 Menu 버튼을 변경한 것과 같은 방법으로 이미지를 대체하면 된다.

import SwiftUI
import Kingfisher

struct SideMenuView: View {
    @EnvironmentObject var authViewModel: AuthViewModel

    var body: some View {
        if let user = authViewModel.currentUser {

Kingfisher를 import 한다.

var body: some View {
    if let user = authViewModel.currentUser {
        VStack(alignment: .leading, spacing: 32) {
            VStack(alignment: .leading) {
                KFImage(URL(string: user.profileImageUrl))
                    .resizable()
                    .scaledToFill()
                    .clipShape(Circle())
                    .frame(width: 48, height: 48)

                VStack(alignment: .leading, spacing: 4) {
                    Text(user.fullname)
                        .font(.headline)

                    Text("@\(user.username)"

currentUser의 profileImageUrl을 KFImage에 전달해 이미지를 받아온다.
디자인할 수 있도록 resizable을 포함한 modifier를 적용한다.

ForEach(SideMenuViewModel.allCases, id: \.rawValue) { viewModel in
    if viewModel == .profile {
        NavigationLink(destination: ProfileView(user: user)) {
            SideMenuRowView(viewModel: viewModel)
        }
    } else if viewModel == .logout {
        Button {
            authViewModel.signOut()
        } label: {
            SideMenuRowView(viewModel: viewModel)
        }

SideMenu의 Profile 메뉴는 현재 사용자의 프로필을 표시한다.
NavigationLink로 ProfileView를 연결하고 파라미터로 현재 접속 중인 계정을 전달한다.

더보기

Source


import SwiftUI
import Kingfisher

struct SideMenuView: View {
    @EnvironmentObject var authViewModel: AuthViewModel

    var body: some View {
        if let user = authViewModel.currentUser {
            VStack(alignment: .leading, spacing: 32) {
                VStack(alignment: .leading) {
                    KFImage(URL(string: user.profileImageUrl))
                        .resizable()
                        .scaledToFill()
                        .clipShape(Circle())
                        .frame(width: 48, height: 48)

                    VStack(alignment: .leading, spacing: 4) {
                        Text(user.fullname)
                            .font(.headline)

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

                    UserStatsView()
                        .padding(.vertical)
                }
                .padding(.leading)

                ForEach(SideMenuViewModel.allCases, id: \.rawValue) { viewModel in
                    if viewModel == .profile {
                        NavigationLink(destination: ProfileView(user: user)) {
                            SideMenuRowView(viewModel: viewModel)
                        }
                    } else if viewModel == .logout {
                        Button {
                            authViewModel.signOut()
                        } label: {
                            SideMenuRowView(viewModel: viewModel)
                        }
                    } else {
                        SideMenuRowView(viewModel: viewModel)
                    }
                }

                Spacer()
            }
            .background(Color.white)
        }
    }
}

프로필 사진 표시하기
| ExploreView

struct ExploreView: View {
    var body: some View {
        NavigationStack {
            VStack {
                ScrollView {
                    LazyVStack {
                        ForEach(0 ... 25, id: \.self) { _ in
                            NavigationLink {
//								ProfileView(user: <#T##User#>)
                            } label: {
                                UserRowView()
                            }

                        }
                    }
                }
            }
            .navigationTitle("Explore")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

지금은 전달할 사용자가 존재하지 않는다.
오류를 무시하고 구현 사항을 확인할 수 있도록 ProfileView를 표시하는 부분을 주석처리 하거나,
임시 User 객체를 선언해 전달할 수 있도록 한다.
이번엔 주석처리로 진행했다.

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

15. 기능구현 #4  (0) 2023.01.11
14. 버그 수정 #1  (0) 2023.01.11
12. DB와 연결하기 #1  (0) 2023.01.05
11. 기능 구현하기 #3  (0) 2022.12.22
10. 기능 구현하기 #2  (0) 2022.12.20