오늘은 상세 페이지 내 메이트를 추가할 수 있는 로직을 구현하였다.
이전에 가지고 있던 유저 데이터를 사용하기엔 필요없는 데이터를 많이 가져오는 것 같아 새로운 메이트에서 필요한 값만 가져와서 사용할 수 있도록 구현하였다.
모델 데이터 생성
struct UserSummary: Codable, Hashable {
let uid: String
let email: String
let displayName: String
let photoURL: String?
var isMate: Bool
func hash(into hasher: inout Hasher) {
hasher.combine(uid)
}
static func == (lhs: UserSummary, rhs: UserSummary) -> Bool {
return lhs.uid == rhs.uid
}
}
먼저 유저 메이트 구현에 사용할 데이터만 가져올 수 있도록 구현하였다.
아이디 단위로 저장할 수 있도록 아이디와 이메일 , 닉네임으로 검색될 수 있도록 하는 모델 , 그리고 프로필 이미지를 가져오고 , 메이트 추가 여부를 판단하는 불값을 가져와 모델을 구성했다.
MateVC 구현
import UIKit
import FirebaseFirestore
class MateViewController: UIViewController {
weak var delegate: MateViewControllerDelegate?
var users: [UserSummary] = []
var filteredUsers: [UserSummary] = []
var addedMates: [UserSummary] = [] {
didSet {
updateAddedMatesCollectionViewVisibility()
}
}
let searchBar = UISearchBar().then {
$0.backgroundImage = UIImage()
$0.placeholder = "메이트를 검색해주세요"
}
let addedMatesView = UIView()
let addedMatesCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 5, left: 32, bottom: 5, right: 32)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = .clear
collectionView.register(AddedMateCell.self, forCellWithReuseIdentifier: AddedMateCell.identifier)
return collectionView
}()
let tableView = UITableView().then {
$0.backgroundColor = .white
$0.register(MateTableViewCell.self, forCellReuseIdentifier: MateTableViewCell.identifier)
}
let noDataView = UIView().then {
$0.isUserInteractionEnabled = false
}
let imageView = UIImageView().then {
$0.image = UIImage(named: "emptyImg")
$0.contentMode = .scaleAspectFill
$0.isHidden = true
}
let noDataMainTitle = UILabel().then {
$0.text = "검색결과가 없습니다"
$0.textAlignment = .center
$0.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
$0.textColor = .black
$0.isHidden = true
}
let noDataSubTitle = UILabel().then {
$0.text = "검색을 통해 메이트를 추가해주세요"
$0.textAlignment = .center
$0.font = UIFont.systemFont(ofSize: 16)
$0.textColor = .lightgray
$0.isHidden = true
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupConstraints()
setupTableView()
setupSearchBar()
setupAddedMatesCollectionView()
setupNavigationBar()
fetchUsers()
updateNoDataView(isEmpty: true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.tintColor = .black
navigationItem.largeTitleDisplayMode = .never
}
func setupUI() {
view.addSubview(searchBar)
view.addSubview(addedMatesView)
addedMatesView.addSubview(addedMatesCollectionView)
view.addSubview(tableView)
view.addSubview(noDataView)
noDataView.addSubview(imageView)
noDataView.addSubview(noDataMainTitle)
noDataView.addSubview(noDataSubTitle)
view.backgroundColor = .white
}
func setupConstraints() {
searchBar.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide)
$0.leading.trailing.equalToSuperview().inset(16)
}
addedMatesView.snp.makeConstraints {
$0.top.equalTo(searchBar.snp.bottom)
$0.leading.trailing.equalToSuperview()
$0.height.equalTo(addedMates.isEmpty ? 0 : 40)
}
addedMatesCollectionView.snp.makeConstraints {
$0.edges.equalTo(addedMatesView)
}
tableView.snp.makeConstraints {
$0.top.equalTo(addedMatesView.snp.bottom)
$0.leading.trailing.bottom.equalTo(view)
}
noDataView.snp.makeConstraints {
$0.edges.equalTo(view)
}
imageView.snp.makeConstraints {
$0.centerX.equalTo(noDataView)
$0.centerY.equalTo(noDataView).offset(-40)
$0.width.height.equalTo(60)
}
noDataMainTitle.snp.makeConstraints {
$0.top.equalTo(imageView.snp.bottom).offset(20)
$0.leading.trailing.equalTo(noDataView).inset(20)
}
noDataSubTitle.snp.makeConstraints {
$0.top.equalTo(noDataMainTitle.snp.bottom).offset(10)
$0.leading.trailing.equalTo(noDataView).inset(20)
}
}
func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
}
func setupSearchBar() {
searchBar.delegate = self
}
func setupNavigationBar() {
let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneButtonTapped))
navigationItem.rightBarButtonItem = doneButton
}
@objc func doneButtonTapped() {
delegate?.didSelectMates(addedMates)
navigationController?.popViewController(animated: true)
}
func setupAddedMatesCollectionView() {
addedMatesCollectionView.delegate = self
addedMatesCollectionView.dataSource = self
}
func updateAddedMatesCollectionViewVisibility() {
addedMatesView.isHidden = addedMates.isEmpty
addedMatesView.snp.updateConstraints {
$0.height.equalTo(addedMates.isEmpty ? 0 : 40)
}
tableView.snp.updateConstraints {
$0.top.equalTo(addedMatesView.snp.bottom)
}
}
func fetchUsers() {
MateManager.shared.fetchUserSummaries { [weak self] result in
switch result {
case .success(let userSummaries):
self?.users = userSummaries
self?.filteredUsers = []
self?.updateNoDataView(isEmpty: true)
self?.tableView.reloadData()
case .failure(let error):
print("Error fetching users: \\(error)")
}
}
}
func searchFriends(with query: String) {
let filteredByName = users.filter { $0.displayName.lowercased().contains(query.lowercased()) }
let filteredByEmail = users.filter { $0.email.lowercased().contains(query.lowercased()) }
filteredUsers = Array(Set(filteredByName + filteredByEmail))
updateNoDataView(isEmpty: filteredUsers.isEmpty)
tableView.reloadData()
}
func updateNoDataView(isEmpty: Bool) {
noDataMainTitle.isHidden = !isEmpty
noDataSubTitle.isHidden = !isEmpty
imageView.isHidden = !isEmpty
}
}
extension MateViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredUsers.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: MateTableViewCell.identifier, for: indexPath) as? MateTableViewCell else {
return UITableViewCell()
}
let user = filteredUsers[indexPath.row]
cell.configure(with: user)
cell.delegate = self
cell.selectionStyle = .none
return cell
}
}
extension MateViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
filteredUsers = []
updateNoDataView(isEmpty: true)
} else {
searchFriends(with: searchText)
}
tableView.reloadData()
}
}
extension MateViewController: MateTableViewCellDelegate {
func didTapAddButton(for user: UserSummary) {
if let index = filteredUsers.firstIndex(where: { $0.uid == user.uid }) {
filteredUsers[index].isMate.toggle()
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
if filteredUsers[index].isMate {
addedMates.append(filteredUsers[index])
} else {
addedMates.removeAll { $0.uid == filteredUsers[index].uid }
}
addedMatesCollectionView.reloadData()
}
}
}
extension MateViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return addedMates.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddedMateCell.identifier, for: indexPath) as? AddedMateCell else {
return UICollectionViewCell()
}
let mate = addedMates[indexPath.row]
cell.configure(with: mate)
cell.delegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if collectionView == addedMatesCollectionView {
let user = addedMates[indexPath.row]
let labelWidth = user.displayName.size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .semibold)]).width
let buttonWidth: CGFloat = 50
let padding: CGFloat = 20
let totalWidth = labelWidth + buttonWidth + padding
return CGSize(width: totalWidth, height: 30)
}
return CGSize(width: 120, height: 30)
}
}
extension MateViewController: AddedMateCellDelegate {
func didTapRemoveButton(for user: UserSummary) {
if let index = addedMates.firstIndex(where: { $0.uid == user.uid }) {
addedMates.remove(at: index)
if let filteredIndex = filteredUsers.firstIndex(where: { $0.uid == user.uid }) {
filteredUsers[filteredIndex].isMate = false
tableView.reloadRows(at: [IndexPath(row: filteredIndex, section: 0)], with: .automatic)
}
addedMatesCollectionView.reloadData()
}
}
}
protocol MateViewControllerDelegate: AnyObject {
func didSelectMates(_ mates: [UserSummary])
}
먼저 메이트 목록을 가져올 메이트VC를 구현한다 이곳에는 서치바를 통해 검색할 수 있도록 하고, 메이트로 추가된 유저가 리스트업이 될 수 있도록 테이블 뷰를 구현하였다.
MateVC 상세 코드
그럼 이제 상세하게 구현 내용을 살펴보자
var selectedFriends: [UserSummary] = []
상단에 새로 적용된 데이터를 담을 수 있도록 변수에 넣어준다.
selectedFriends의 값은 inputVC 내 메이트 컬렉션 뷰에 들어갈 내용이다.
func fetchUserSummary(userId: String, completion: @escaping (UserSummary?) -> Void) {
let db = Firestore.firestore()
db.collection("users").document(userId).getDocument { document, error in
if let document = document, document.exists, let data = document.data() {
let userSummary = UserSummary(
uid: userId,
email: data["email"] as? String ?? "",
displayName: data["displayName"] as? String ?? "",
photoURL: data["photoURL"] as? String,
isMate: false
)
completion(userSummary)
} else {
completion(nil)
}
}
}
그리고 저장된 모델 내 데이터를 넣을 수 있도록 데이터를 패치하는 메서드를 구현한다.
그 이후
func loadSelectedFriends(pinLog: PinLog) {
let group = DispatchGroup()
selectedFriends.removeAll()
for userId in pinLog.attendeeIds {
group.enter()
fetchUserSummary(userId: userId) { [weak self] userSummary in
guard let self = self else {
group.leave()
return
}
if var userSummary = userSummary {
userSummary.isMate = true
self.selectedFriends.append(userSummary)
}
group.leave()
}
}
group.notify(queue: .main) {
self.mateCollectionView.reloadData()
self.updateMateCountButton()
}
}
구현된 메서드를 활용해 inpuVC에 값을 넣어줄 수 있도록 구현했다.
그리고 테이블 뷰 내
func configure(with user: UserSummary) {
self.user = user
nicknameLabel.text = user.displayName
if let photoURL = user.photoURL, let url = URL(string: photoURL) {
profileImageView.kf.setImage(with: url)
} else {
profileImageView.image = UIImage(named: "defaultProfileImage")
}
updateAddButton()
}
값을 넣을 수 있는 configure를 넣어준 뒤 로드하면 정상적으로 메이트 구현이 완료된다.
'◽️ Programming > T I L' 카테고리의 다른 글
[Project 일지] 여행 기록 앱 만들기 (8) - 앱 배포 및 reject 사유 수정 (0) | 2024.06.21 |
---|---|
[Project 일지] 여행 기록 앱 만들기 (7) - 차단 목록 만들기 및 데이터 차단 (0) | 2024.06.17 |
[Project 일지] 여행 기록 앱 만들기 (5) - sendSubviewToBack , 비동기 처리 (1) | 2024.06.07 |
[Project 일지] 여행 기록 앱 만들기 (4) - scrollViewDidScroll, SegmentControl (0) | 2024.06.05 |
[Project 일지] 여행 기록 앱 만들기 (3) - isExpanded , isUserInteractionEnabled (0) | 2024.06.03 |