๐ด Let's Build Twitter with SwiftUI (iOS 15, Xcode 13, Firebase, SwiftUI 3.0)
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()
}
}
}
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()
}
}
}
}
}
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 ํจ๊ณผ๋ฅผ ์ ๊ณตํ ์ ์๋ค๋ ์ ์์ ์ต์ํด์ง์!