Firebase의 Realtime Database를 이용해서 CRUD를 구현해봤다. 기본으로 잡고 있던 구조체에 프로퍼티를 좀 더 추가하고, CURD를 수정한 후 다양한 상황에 적용을 시켜보자.
기본으로 잡고 있던 NoticeBoard
구조체를 다음과 같이 변경했다.
struct NoticeBoard: Codable, Identifier {
let id: String
let rootUser: UserSummary
let createDate: Date
let clubID: String
var title: String
var content: String
var imageList: [String]
var commentCount: String
}
게시글은 다양하게 생기기 때문에 Indentifier도 추가했다. 이 프로토콜을 채택한 타입은 고유 개채를 구분하기 위해서 비교 알고리즘에 이 id를 사용하게 되고 id가 다르다면 서로 다른 대상이 되는 것이며 값이 다르더라고 id가 같으면 동일한 개체로 구분되게 된다. 프로퍼티에 이미 id가 있기 때문에 적용을 바로 시켰다.
https://developer.apple.com/documentation/swift/identifiable
구조체의 프로퍼티가 수정되었으므로, CRUD 함수들도 수정을 하자. 가장 큰 변화는 ref에 child(noticeBoard.clubID)
가 추가 되었다는 점이다. 게시판이 작성되고 있는 모임의 id를 통해서 게시판의 위치를 파악하기 때문이다.
이 부분을 추가하지 않게 되면, A 모임에서 작성한 게시판이 B 모임의 게시판에서 똑같이 보여지게 된다. 왜냐하면 A 모임에서 작성을 했는지, B 모임에서 작성을 했는지 구별 할 수 있는 조건이 없기 때문이다.
class FirebaseManager {
weak var delegate: FirebaseManagerDelegate?
var noticeBoards: [NoticeBoard] = []
// MARK: - 데이터 저장
func saveNoticeBoard(noticeBoard: NoticeBoard, completion: ((Bool) -> Void)? = nil) {
let ref = Database.database().reference().child("noticeBoards").child(noticeBoard.clubID).child(noticeBoard.id)
// UserSummary
let userSummaryDict: [String: Any?] = [
"id": noticeBoard.rootUser.id,
"profileImage": noticeBoard.rootUser.profileImageURL,
"nickName": noticeBoard.rootUser.nickName,
"description": noticeBoard.rootUser.description
]
// NoticeBoard
let noticeBoardDict: [String: Any] = [
"id": noticeBoard.id,
"clubID": noticeBoard.clubID,
"rootUser": userSummaryDict,
"title": noticeBoard.title,
"content": noticeBoard.content,
"createDate": noticeBoard.createDate.dateToString,
"imageList": noticeBoard.imageList,
"commentCount": noticeBoard.commentCount
]
ref.setValue(noticeBoardDict) { error, _ in
if let error = error {
print("Error saving notice board: \(error)")
completion?(false)
}
else {
print("Successfully saved notice board.")
completion?(true)
}
}
}
// MARK: - 데이터 생성
// 임시 작성 유저 정보
let currentUser = UserSummary(id: "currentUser", profileImageURL: nil, nickName: "파이브 아이즈", description: "This is the current user.")
func createNoticeBoard(title: String, content: String, clubID: String, completion: ((Bool) -> Void)? = nil) {
let ref = Database.database().reference().child("noticeBoards").child(clubID)
let newNoticeBoardID = ref.childByAutoId().key ?? ""
let createDate = Date()
let newNoticeBoard = NoticeBoard(id: newNoticeBoardID, rootUser: currentUser, createDate: createDate, clubID: clubID, title: title, content: content, imageList: [], commentCount: "0")
self.saveNoticeBoard(noticeBoard: newNoticeBoard) { success in
if success {
self.noticeBoards.insert(newNoticeBoard, at: 0)
self.delegate?.reloadData()
}
}
}
// MARK: - 데이터 읽기
func readNoticeBoard(clubID: String, completion: ((Bool) -> Void)? = nil) {
let ref = Database.database().reference().child("noticeBoards").child(clubID)
ref.getData(completion: { (error, snapshot) in
var newNoticeBoards: [NoticeBoard] = []
if let error = error {
print("Error getting data: \(error)")
completion?(false)
return
}
guard let value = snapshot?.value as? [String: Any] else {
self.delegate?.reloadData()
completion?(false)
return
}
for (_, item) in value {
if let itemDict = item as? [String: Any],
let id = itemDict["id"] as? String,
let clubID = itemDict["clubID"] as? String,
let rootUserDict = itemDict["rootUser"] as? [String: Any],
let rootUserId = rootUserDict["id"] as? String,
let rootUserNickName = rootUserDict["nickName"] as? String,
let title = itemDict["title"] as? String,
let content = itemDict["content"] as? String,
let createDateStr = itemDict["createDate"] as? String,
let createDate = createDateStr.toDate,
let commentCount = itemDict["commentCount"] as? String
{
let profileImageString = rootUserDict["profileImage"] as? String
let rootUser = UserSummary(id: rootUserId, profileImageURL: profileImageString, nickName: rootUserNickName, description: rootUserDict["description"] as? String)
let imageList = itemDict["imageList"] as? [String] ?? []
let noticeBoard = NoticeBoard(id: id, rootUser: rootUser, createDate: createDate, clubID: clubID, title: title, content: content, imageList: imageList, commentCount: commentCount)
newNoticeBoards.append(noticeBoard)
}
}
self.noticeBoards = newNoticeBoards.sorted(by: { $0.createDate > $1.createDate })
self.delegate?.reloadData()
completion?(true)
})
}
// MARK: - 데이터 업데이트
func updateNoticeBoard(at index: Int, title newTitle: String, content newContent: String) {
if index >= 0, index < self.noticeBoards.count {
var updatedNoticeBoard = self.noticeBoards[index]
updatedNoticeBoard.title = newTitle
updatedNoticeBoard.content = newContent
self.saveNoticeBoard(noticeBoard: updatedNoticeBoard) { success in
if success {
self.noticeBoards[index] = updatedNoticeBoard
self.delegate?.reloadData()
}
}
}
}
// MARK: - 데이터 삭제
func deleteNoticeBoard(at index: Int, completion: ((Bool) -> Void)? = nil) {
if index >= 0, index < self.noticeBoards.count {
let noticeBoardID = self.noticeBoards[index].id
let ref = Database.database().reference().child("noticeBoards").child(noticeBoards[index].clubID).child(noticeBoardID)
ref.removeValue { error, _ in
if let error = error {
print("Error deleting notice board: \(error)")
completion?(false)
}
else {
print("Successfully deleted notice board.")
self.noticeBoards.remove(at: index)
self.delegate?.reloadData()
completion?(true)
}
}
}
else {
completion?(false)
}
}
}
이제 게시글 목록과 게시글 작성 페이지는 동일한 ClubID를 가져야 한다. init안에 club을 넣어주는 방향으로 진행을 하자.
class NoticeBoardViewController: UIViewController {
var club: Club
init(club: Club) {
self.club = club
super.init(nibName: nil, bundle: nil)
}
}
class CreateNoticeBoardViewController: UIViewController {
var club: Club
init(club: Club) {
self.club = club
super.init(nibName: nil, bundle: nil)
}
}
그리고 상위 페이지에서 동일한 club을 넣어준다.
class NoticeMeetingController: TabmanViewController {
.
.
let titleVC = NoticeBoardViewController(club: club)
.
.
let createNoticeBoardVC = CreateNoticeBoardViewController(club: club)
.
.
}
DB에 게시글이 있다면 게시글을 보여주면 되지만, 게시글이 없다면 사용자에게 알림화면을 보여주어야 한다.
우선 알림 화면을 보여주기 전에는 이렇게 화면을 보여주고 있었다.
override func loadView() {
view = noticeBoardView
}
View와 ViewController를 분리시켰기에, ViewController의 View를 내가 만든 noticeBoardView로 지정한 것이다.
그래서 처음에는 FirebaseManager 클래스에 있는 noticeBoards 배열에 값의 여부에 따라 view를 변화시키려고 했다.
override func loadView() {
if firebaseManager.noticeBoards.isEmpty {
view = noticeBoardEmptyView
} else {
view = noticeBoardView
}
}
이렇게 진행을 했더니, Unexpected subviews라는 오류가 발생했었다. Unexpected subviews 오류 메시지는 UIKit이나 다른 관련된 라이브러리에서 특정 뷰의 하위 뷰 구조에 예기치 않은 변경이 생겼을 때 발생하는 오류이다. loadView()
에서 뷰 계층 구조를 변경하기 때문에 발생한 것이었다.
뿐만 아니라, 작성 페이지에서 게시글을 작성하게 되면 게시글 목록 페이지에서는 알림화면이 없어지고 게시글을 보여주어야 하는데 그렇지 않았다.
그 이유는 각각의 ViewController에서 FirebaseManager를 인스턴스화 시켜서 사용했기 때문이다.
class NoticeBoardViewController: UIViewController {
private let noticeBoardView = NoticeBoardView()
private let noticeBoardEmptyView = NoticeBoardEmptyView()
private let firebaseManager = FirebaseManager()
.
.
.
}
class CreateNoticeBoardViewController: UIViewController {
private let createNoticeBoardView = CreateNoticeBoardView()
private let firebaseManager = FirebaseManager()
.
.
.
}
즉, 처음 모임 화면에서 같은 FirebaseManager를 가져와서 사용을 해야 하는데 다른 FirebaseManager를 사용했기 때문에, 데이터가 제대로 연결이 되지 않았던 것이다.
알림 화면부터 수정을 해보자. view를 직접 바꾸는 방법은 옳지 않기 때문에, addSubview()를 이용해서 view를 추가하는 방향으로 생각을 했다.
FirebaseManager에서 데이터 작업이 끝났을 때 불려지기 때문에 함수로 만들었다. 그리고 view에 대한 constraints도 적용했다.
private func selectView() {
if firebaseManager.noticeBoards.isEmpty {
noticeBoardView.removeFromSuperview()
if noticeBoardEmptyView.superview == nil {
view.addSubview(noticeBoardEmptyView)
setupView(for: noticeBoardEmptyView)
}
}
else {
noticeBoardEmptyView.removeFromSuperview()
if noticeBoardView.superview == nil {
view.addSubview(noticeBoardView)
setupView(for: noticeBoardView)
}
}
}
private func setupView(for subView: UIView) {
subView.snp.makeConstraints { make in
make.top.leading.trailing.bottom.equalToSuperview()
}
}
removeFromSuperview()
는 UIView의 인스턴스 메서드로, 호출된 뷰를 슈퍼뷰에서 제거한다.
firebaseManager.noticeBoards 배열이 비어 있을 때 noticeBoardView를 화면에서 제거하고, noticeBoardEmptyView를 화면에 추가하는 데 사용된다. 반대로 배열에 데이터가 있을 때는 noticeBoardEmptyView를 화면에서 제거하고, noticeBoardView를 화면에 추가한다.
그 다음, 이 함수를 전에 만들었던 Delegate 함수 안에 넣었다.
extension NoticeBoardViewController: FirebaseManagerDelegate {
func reloadData() {
selectView()
noticeBoardView.noticeBoardTableView.reloadData()
}
}
Delegate 함수인 reloadData()
를 CRUD의 completion이 true(성공)일때만 불렀다.
func readNoticeBoard(clubID: String, completion: ((Bool) -> Void)? = nil) {
let ref = Database.database().reference().child("noticeBoards").child(clubID)
ref.getData(completion: { (error, snapshot) in
var newNoticeBoards: [NoticeBoard] = []
if let error = error {
print("Error getting data: \(error)")
completion?(false)
return
}
guard let value = snapshot?.value as? [String: Any] else {
completion?(false)
return
}
for (_, item) in value {
if let itemDict = item as? [String: Any],
.
.
{
.
.
}
}
self.delegate?.reloadData()
completion?(true)
})
}
그런데 계속 noticeBoardEmptyView가 보여지지 않아서 BreakPoint를 찍어서 디버깅을 해보니, 애초에 게시글이 없다는 것은 guard let value = snapshot?.value as? [String: Any]
에서 value가 없다는 뜻이다. 즉, 이 부분에서도 reloadData()
를 불러와야했던 것이다.
func readNoticeBoard(clubID: String, completion: ((Bool) -> Void)? = nil) {
let ref = Database.database().reference().child("noticeBoards").child(clubID)
ref.getData(completion: { (error, snapshot) in
var newNoticeBoards: [NoticeBoard] = []
if let error = error {
print("Error getting data: \(error)")
completion?(false)
return
}
guard let value = snapshot?.value as? [String: Any] else {
self.delegate?.reloadData()
completion?(false)
return
}
for (_, item) in value {
if let itemDict = item as? [String: Any],
.
.
{
.
.
}
}
self.delegate?.reloadData()
completion?(true)
})
}
이제 FirebaseManager를 동일하게 사용할 수 있도록 수정하자. 위에서 Club을 init에 넣은 것처럼 FirebaseManager도 init에 넣자. 게시글 목록과 게시글 작성의 상위 페이지인 모임화면에서 FirebaseManager를 인스턴스화 시킨다음에 화면이동 할 때, 적용시키면 된다.
class NoticeBoardViewController: UIViewController {
var firebaseManager: FirebaseManager
var club: Club
init(club: Club, firebaseManager: FirebaseManager) {
self.club = club
self.firebaseManager = firebaseManager
super.init(nibName: nil, bundle: nil)
}
}
class CreateNoticeBoardViewController: UIViewController {
var firebaseManager: FirebaseManager
var club: Club
init(club: Club, firebaseManager: FirebaseManager) {
self.club = club
self.firebaseManager = firebaseManager
super.init(nibName: nil, bundle: nil)
}
상위 페이지에서 동일한 firebaseManager를 넣어준다.
class NoticeMeetingController: TabmanViewController {
private let firebaseManager = FirebaseManager()
.
.
let titleVC = NoticeBoardViewController(club: club, firebaseManager: firebaseManager)
.
.
let createNoticeBoardVC = CreateNoticeBoardViewController(club: club, firebaseManager: firebaseManager)
.
.
}