[SwiftUI] TwitterClone: ProfileView

Junyoung Parkยท2022๋…„ 11์›” 15์ผ
0

SwiftUI

๋ชฉ๋ก ๋ณด๊ธฐ
99/136
post-thumbnail

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

TwitterClone: ProfileView

๊ตฌํ˜„ ๋ชฉํ‘œ

  • ํ”„๋กœํ•„ ํ—ค๋” ๋ทฐ UI ๊ตฌํ˜„

๊ตฌํ˜„ ํƒœ์Šคํฌ

  • ํ”„๋กœํ•„ ์ •๋ณด UI
  • ํ•„ํ„ฐ ๋ฒ„ํŠผ ๋ณ€๊ฒฝ UI
  • ํŠธ์œ„ํ„ฐ ํ”ผ๋“œ UI

ํ•ต์‹ฌ ์ฝ”๋“œ

private 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(.clear)
                            .frame(height: 3)
                    }
                }
                .onTapGesture {
                    withAnimation(.easeInOut) {
                        selectedFilter = item
                    }
                }
            }
        }
        .overlay(Divider().offset(x: 0, y: 16))
    }
  • matchedGeometryEffect๋Š” ๋™์ผํ•œ ๋„ค์ž„ ์ŠคํŽ˜์ด์Šค ๊ฐ„์˜ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋™์ผํ•˜๊ฒŒ ์ธ์‹ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฉ”์†Œ๋“œ
  • ์„œ๋กœ ๋‹ค๋ฅธ ์„ธ ๊ฐœ์˜ Capsule() ๋ทฐ๊ฐ€ ์„ธ ๊ฐœ ์กด์žฌ
  • ํ˜„์žฌ @State ํ”„๋กœํผํ‹ฐ ๊ฐ’์„ ํ†ตํ•ด ์„ ํƒ๋œ ํ•„ํ„ฐ๋ฅผ ์ฒดํฌ โ†’ ํ•„ํ„ฐ ์„ ํƒ์„ ํ†ตํ•ด ํ•ด๋‹น ํ”„๋กœํผํ‹ฐ ๊ฐ’ ๋ณ€๊ฒฝ โ†’ ์„ ํƒ๋œ Capsule() ๋ณ€๊ฒฝ, ๋„ค์ž„ ์ŠคํŽ˜์ด์Šค ๊ฐ™์œผ๋ฏ€๋กœ ์ด๋™ ํšจ๊ณผ

์†Œ์Šค ์ฝ”๋“œ

import SwiftUI

struct ProfileView: View {
    @State private var selectedFilter: TweetFilterViewModel = .tweets
    @Namespace private var animation
    var body: some View {
        VStack(alignment: .leading) {
            headerView
            actionButtons
            userInfoDetails
            tweetFilterBar
            tweetsView
            Spacer()
        }
    }
}
  • ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ํ™œ์šฉํ•ด ํ•„ํ„ฐ ํšจ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํ”„๋กœํ•„ ๋ทฐ UI ๊ตฌํ˜„
extension ProfileView {
    private var headerView: some View {
        ZStack(alignment: .bottomLeading) {
            Color(.systemBlue)
                .ignoresSafeArea()
            VStack {
                Button {
                    
                } label: {
                    Image(systemName: "arrow.left")
                        .resizable()
                        .frame(width: 20, height: 16)
                        .foregroundColor(.white)
                        .offset(x: 16, y: 12)
                }

                Circle()
                    .frame(width: 72, height: 72)
                .offset(x: 16, y: 24)
            }
        }
        .frame(height: 96)
    }
    
    private 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)
                    .foregroundColor(.black)
                    .overlay(
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(Color.gray, lineWidth: 0.75)
                    )
            }
        }
        .padding(.trailing)
    }
    
    private var userInfoDetails: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack {
                Text("Peter Parker")
                    .font(.title2)
                    .bold()
                Image(systemName: "checkmark.seal.fill")
                    .foregroundColor(Color(.systemBlue))
            }
            Text("@SpiderMan")
                .font(.subheadline)
                .foregroundColor(.gray)
            Text("Casted in Next Movie")
                .font(.subheadline)
                .padding(.vertical)
            HStack(spacing: 24) {
                HStack {
                    Image(systemName: "mappin.and.ellipse")
                    Text("NewYork")
                }
                HStack {
                    Image(systemName: "link")
                    Text("www.your_friendly_neighborhood.com")
                        .foregroundColor(Color(.systemBlue))
                }
            }
            .font(.caption)
            .foregroundColor(.gray)
            
            HStack(spacing: 24) {
                HStack(spacing: 4) {
                    Text("1")
                        .font(.subheadline)
                        .bold()
                    Text("Following")
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                HStack {
                    Text("7.77M")
                        .font(.subheadline)
                        .bold()
                    Text("Followers")
                        .font(.caption)
                        .foregroundColor(.gray)
                }
            }
            .padding(.vertical)
        }
        .padding(.horizontal)
    }
    
    private 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(.clear)
                            .frame(height: 3)
                    }
                }
                .onTapGesture {
                    withAnimation(.easeInOut) {
                        selectedFilter = item
                    }
                }
            }
        }
        .overlay(Divider().offset(x: 0, y: 16))
    }
    private var tweetsView: some View {
        ScrollView {
            LazyVStack {
                ForEach(0..<10, id:\.self) { _ in
                    TweetRowView()
                        .padding()
                }
            }
        }
    }
}
  • ํ•„ํ„ฐ UI์„ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ํ†ตํ•œ ๊ตฌํ˜„
  • ํŠธ์œ„ํŠธ ๋ทฐ๋Š” ๊ธฐ์กด์˜ ๋กœ์šฐ ๋ทฐ ์žฌ์‚ฌ์šฉ
import Foundation

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"
        }
    }
}
  • ์ด๋„˜์„ ํ†ตํ•ด ํ•„ํ„ฐ ํ—ค๋” ๊ด€๋ฆฌ

๊ตฌํ˜„ ํ™”๋ฉด

๋„ค์ž„์ŠคํŽ˜์ด์Šค๋ฅผ ํ™œ์šฉํ•œ matchedGeometryEffect๋Š” ๊ธฐ์กด์˜ @State๋ฅผ ํ™œ์šฉํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ดํŽ™ํŠธ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๊ฐ„๋‹จํ•˜๊ณ  ์ข‹์€ UI ํšจ๊ณผ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์—์„œ ์ต์ˆ™ํ•ด์ง€์ž!

profile
JUST DO IT

0๊ฐœ์˜ ๋Œ“๊ธ€