[TIL] 10.24

Junyoung_Hong·2023년 10월 24일
0

TIL_10월

목록 보기
15/20
post-thumbnail

1. ViewController에 Realtime Database CRUD 적용시키기

https://velog.io/@wnsdud0721/TIL-10.20

Firebase의 Realtime Database를 이용해서 CRUD를 구현해봤다. 기본으로 잡고 있던 구조체에 프로퍼티를 좀 더 추가하고, CURD를 수정한 후 다양한 상황에 적용을 시켜보자.

1-1. 구조체 변경

기본으로 잡고 있던 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

1-2. CRUD 수정

구조체의 프로퍼티가 수정되었으므로, CRUD 함수들도 수정을 하자. 가장 큰 변화는 refchild(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)
        }
    }
}

1-3. 초기화 항목에 Club 생성하기

이제 게시글 목록과 게시글 작성 페이지는 동일한 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)
    .
    .
}

2. 상황에 따른 화면 보여주기

DB에 게시글이 있다면 게시글을 보여주면 되지만, 게시글이 없다면 사용자에게 알림화면을 보여주어야 한다.

게시글이 없을 경우

게시글이 있는 경우

2-1. 처음 진행했던 방향

빈 게시글 알림 화면

우선 알림 화면을 보여주기 전에는 이렇게 화면을 보여주고 있었다.

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()에서 뷰 계층 구조를 변경하기 때문에 발생한 것이었다.

데이터 업데이트 적용X

뿐만 아니라, 작성 페이지에서 게시글을 작성하게 되면 게시글 목록 페이지에서는 알림화면이 없어지고 게시글을 보여주어야 하는데 그렇지 않았다.

그 이유는 각각의 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를 사용했기 때문에, 데이터가 제대로 연결이 되지 않았던 것이다.

2-2. 수정 방향

빈 게시글 알림 화면

알림 화면부터 수정을 해보자. 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)
    })
}

데이터 업데이트 적용X

이제 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)
    .
    .
}
profile
iOS 개발자를 향해 성장 중

0개의 댓글