π΄ Let's Build Twitter with SwiftUI (iOS 15, Xcode 13, Firebase, SwiftUI 3.0)
func fetchUsers(completion: @escaping(Result<[UserModel], Error>) -> Void) {
Firestore.firestore().collection("users")
.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 users = documents.compactMap({try? $0.data(as: UserModel.self)})
completion(.success(users))
}
}
import Foundation
class ExploreViewModel: ObservableObject {
@Published var users = [UserModel]()
private let service = UserService()
init() {
fetchUsers()
}
func fetchUsers() {
service.fetchUsers { [weak self] result in
switch result {
case .success(let users): self?.users = users
case .failure(let error): print(error.localizedDescription)
}
}
}
}
@Pbulished
νλ‘ν μ½μ λ°λ₯΄κ³ μλ μ μ μ 보 λ°°μ΄ λ°μ΄ν°μ ν΄λΉ κ°μ μ£ΌκΈ°import SwiftUI
struct ExploreView: View {
@ObservedObject private var viewModel = ExploreViewModel()
@State private var showProfileView: Bool = false
@State private var selectedUser: UserModel?
var body: some View {
ZStack {
VStack {
ScrollView {
LazyVStack {
ForEach(viewModel.users) { user in
UserRowView(user: user)
.onTapGesture {
selectedUser = user
showProfileView.toggle()
}
}
}
}
}
.navigationTitle("Explore")
.navigationBarTitleDisplayMode(.inline)
}
.background(
NavigationLink(destination: ProfileLoadingView(user: $selectedUser), isActive: $showProfileView, label: {
EmptyView()
})
)
}
}
selectedUser
, showProfileView
λ± κ΄λ ¨ νλ‘νΌν° λ³μ κ°μ μ μ΄selectedUser
κ°μ λ°μΈλ©μΌλ‘ λκΈΈ λ μ΅μ
λλ‘ λκΈΈ μ λ°μ μμΌλ―λ‘ κ³§λ°λ‘ νλ‘ν λ·°λ‘ λμ΄κ°λ κ² μλλΌ λ°μΈλ©μΌλ‘ λμ΄κ° κ°μ if let
μΌλ‘ λ°μμ€ λ€ μ¬μ©νλ λ‘λ© λ·°λ‘ λκΈ°κΈ°import SwiftUI
struct ProfileLoadingView: View {
@Binding var user: UserModel?
init(user: Binding<UserModel?>) {
self._user = user
}
var body: some View {
ZStack {
if let user = user {
ProfileView(user: user)
}
}
}
}
import SwiftUI
struct ProfileView: View {
@ObservedObject private var viewModel: ProfileViewModel
@State private var selectedFilter: TweetFilterViewModel = .tweets
@Namespace private var animation
init(user: UserModel) {
_viewModel = ObservedObject(wrappedValue: ProfileViewModel(user: user))
}
var body: some View {
VStack(alignment: .leading) {
if let profileImage = viewModel.profileImage {
ProfileHeaderView(profileImage: profileImage)
}
actionButtons
userInfoDetail
tweetFilterBar
tweetsView
Spacer()
}
.toolbar(.hidden)
}
}
ObservedObject
λ‘ κ΄μ°° μ€μΈ λ·° λͺ¨λΈ λ΄μ λ°μ΄ν° νΌλΈλ¦¬μ
λ‘ μ΄λ―Έμ§ ν¨μΉ ν λ€μ λ·° λ λλ§import Foundation
import SwiftUI
class ProfileViewModel: ObservableObject {
let user: UserModel
@Published var profileImage: UIImage?
init(user: UserModel) {
self.user = user
downloadProfileImage(with: user.profileImageURL)
}
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()
}
}
UserRowView
λν νλ‘ν λ·°μ λ‘μ§κ³Ό λ§μ°¬κ°μ§λ‘ μ΄λμ
λΌμ΄μ¦λ λ 건λ€λ°μ μ μ λ°μ΄ν°λ₯Ό ν΅ν΄ λ·° λͺ¨λΈμ μ΄λμ
λΌμ΄μ¦, λμμ μ΄λ―Έμ§ ν¨μΉ λ° νΌλΈλ¦¬μ
ꡬλ
μ ν΅ν UI ν¨μΉν μμ μμλ μ΄λ―Έμ§λ₯Ό λ€μ΄λ‘λλ°μ λ μ€νΌλ, λν΄νΈ μ΄λ―Έμ§ λ±μ μ£Όμ§ μκ³ λ€μ΄λ‘λλ°μ λ€ κ³§λ°λ‘ νλ©΄μ λνλκΈ° λλ¬Έμ UXκ° μμ°μ€λ½μ§ λͺ»νλ€. λ§μΌ λ°μ μν¨λ€λ©΄ μ€μΌλ ν€ λ·°, μ€νΌλ λ±μ ν΅ν΄ "νμ¬ λ€μ΄λ‘λλ°κ³ μμ"μ μ μ μκ² μμν μ μμ κ²μ΄λ€!