[UITableView] 아무 데이터가 없을 때 나오는 화면,엠티뷰(empty view) 만들기

2022. 1. 16. 21:50UIKit, SwiftUI, H.I.G

iOS 개발을 하면, 가장 자주 만들어야 할 화면이 바로 테이블 뷰와 컬렉션 뷰일 거다.

테이블 뷰(TableView)와 컬렉션 뷰(CollectionView)는 모두 같은 형태의 데이터를 표출할 때 큰 틀만 만들어두고,
그 안에 들어가는 데이터의 세부 내용만 바꿔주는 식으로 사용한다는 점에서 두 뷰가 공통점을 가졌다.

즉, 다시 말하자면, 이 두 화면은 모두 1개 이상의 데이터가 존재할 때 그 데이터들을 반복해서 보여주기 위한 화면이라는 거다.

 

그렇다면, 만약에 데이터가 한 개도 없을 때는 어떤 화면이 나오게 될까?

당연히 아무런 데이터가 없기 때문에, 아래 왼쪽 사진처럼 빈 화면이 나오게 된다.
이렇게 빈 화면을 사용자에게 보여줄 수 있지만, 보통은 이 화면을 그대로 노출시키지는 않는다.

이 상황에, 설명 라벨이나 이미지, 버튼 등을 화면에 넣어 사용자에게 특정한 액션을 유도하도록 하는 화면을 엠티 뷰(EmptyView)라고 부른다.

이번에는 테이블 뷰와 컬렉션 뷰에서 이 엠티 뷰(EmptyView)를 구현하는 두 가지 방법에 대해 글로 다뤄보도록 하겠다 ^__^

왼쪽은 아무런 데이터가 없을 때, 표출되는 화면, 가운데와 오른쪽은 내가 만들었던 앱에서 모두 엠티뷰를 따로 만들어준 모습이다.

 

1. Data의 개수에 따라, DataSource에서 표출되는 Cell을 바꿔주는 방법


일단, 데이터가 있을 때와, 없을 때를 분기 처리하여, 표출되는 Cell 자체를 변경하는 방법부터 알아보자.

이럴 때는 Cell을 두 개나 만들어줘야 한다.

하나는 데이터가 있을 때 들어갈 Cell(틀의 형태), 또 다른 하나는 엠티 뷰에서 사용할 Cell인데, 나는 Empty 상황에서의 보여줄 Cell을 Xib 파일로 아래처럼 만들었다.

이제 테이블 뷰에서 데이터 수에 따라 표출하는 Cell View가 다르도록 분기 처리를 해줄 차례이다.

항상 해온 것처럼 생명주기 함수 부분에 셀을 register 해주자.
역시 두 개의 Cell을 모두 사용할 거기 때문에 여기서도 모두 register 해줘야 한다.

// MARK: - View Life Cycle
override func viewDidLoad() {
    super.viewDidLoad()
    
    groupEditTableView.register(GroupEditTableViewCell.nib(), forCellReuseIdentifier: Const.Xib.groupEditTableViewCell)
    groupEditTableView.register(EmptyGroupEditTableViewCell.nib(), forCellReuseIdentifier: Const.Xib.EmptyGroupEditTableViewCell)
    
    groupEditTableView.delegate = self
    groupEditTableView.dataSource = self
}

다음은 데이터 개수에 따라, 표출되는 Cell이 다르도록, DataSource 부분을 만져줘야 한다.

이때 중요한 점은 numberOfRowsInSection 부분에서 count(데이터의 개수)가 0일 때, 1을 return 해줘야 한다는 점이다!
(만약 0으로 return 하면... 원하는 empty cell을 표출할 수가 없음...)

count를 고려해 분기 처리를 해도 되고, isEmpty라는 속성을 사용해서 분기 처리를 해도 상관없다!

// MARK: - TableView DataSource
extension GroupEditViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let count = serverGroups?.groups.count
        if count == 0 {
            return 1
        } else {
            return count ?? 1
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if serverGroups?.groups.isEmpty == true {
            guard let serviceCell = tableView.dequeueReusableCell(withIdentifier: Const.Xib.EmptyGroupEditTableViewCell, for: indexPath) as? EmptyGroupEditTableViewCell else { return UITableViewCell() }
            return serviceCell
        } else {
            guard let serviceCell = tableView.dequeueReusableCell(withIdentifier: Const.Xib.groupEditTableViewCell, for: indexPath) as? GroupEditTableViewCell else { return UITableViewCell() }
            
            serviceCell.initData(title: serverGroups?.groups[indexPath.row].groupName ?? "")
            return serviceCell
        }
    }
}

 

Delegate 부분도 건드려줘야 한다.

기존 테이블 뷰에서 적용했던 Swipe Action이라던지, 셀을 클릭할 때 수행하는 액션을 정의하는 didSelectRowAt 함수 부분 등에도 분기 처리를 해줘야 하기 때문이다.

엠티 뷰가 클릭이 된다거나, 스와이프가 된다거나 하면 안 되기 때문에 ^__^

// MARK: - TableView Delegate
extension GroupEditViewController: UITableViewDelegate {
    // Swipe Action
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        if serverGroups?.groups.isEmpty == true {
            return nil
        } else {
            let deleteAction = UIContextualAction(style: .normal, title: "삭제", handler: { (_ action, _ view, _ success) in
                self.makeCancelDeleteAlert(title: "그룹 삭제", message: "해당 그룹에 있던 명함은\n미분류 그룹으로 이동합니다.", cancelAction: { _ in
                    // 취소 눌렀을 때 액션이 들어갈 부분
                }, deleteAction: { _ in
                    self.groupDeleteWithAPI(
                        groupID: self.serverGroups?.groups[indexPath.row].groupID ?? 0,
                        defaultGroupId: self.unClass ?? 0)
                    self.groupEditTableView.reloadData()
                    NotificationCenter.default.post(name: Notification.Name.passDataToGroup, object: 0, userInfo: nil)
                })
            })
            deleteAction.backgroundColor = .red
            
            let swipeActions = UISwipeActionsConfiguration(actions: [deleteAction])
            swipeActions.performsFirstActionWithFullSwipe = false
            
            return swipeActions
        }
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if serverGroups?.groups.isEmpty == false {
            let nextVC = GroupNameEditBottomSheetViewController()
                .setTitle("그룹명 변경")
                .setHeight(184)
            nextVC.modalPresentationStyle = .overFullScreen
            nextVC.text = serverGroups?.groups[indexPath.row].groupName ?? ""
            nextVC.returnToGroupEditViewController = {
                self.groupListFetchWithAPI(userID: UserDefaults.standard.string(forKey: Const.UserDefaultsKey.userID) ?? "")
            }
            nextVC.nowGroup = serverGroups?.groups[indexPath.row]
            self.present(nextVC, animated: false, completion: nil)
        }
    }
}

 

여기까지가 DataSource의 분기 처리에 따라 표출되는 Cell을 변경해주는 방식이다.
Cell을 두 개 만들어줘야 하고, Delegate에서 분기 처리를 일일이 해줘야 한다는 점에서 조금.. 아니 많이 귀찮은 방법이다.

그래서, 사실 엠티 뷰(EmptyView)를 만들 때는 이 방법보다는 다음에 설명할 두 번째 방법을 더 많이 사용하게 될 거다. 계속 글을 읽어보자.

 

2. 테이블 뷰/컬렉션 뷰 자체에 뷰를 구현해주고, isHidden의 true, false를 조절하는 방법


이번에는 그냥 테이블 뷰나 컬렉션 뷰의 자체 background에다가 넣고 싶은 element(UILabel, UIButton, UIImage 등)를 집어넣고,
데이터가 있을 때는 isHidden을 true로, 데이터가 하나도 없을 때(empty view를 표출시켜야 할 때)는 false로 설정해 화면에 보이도록 하는 방법이다.

앞에 방법보다 훨씬 간단하고, 분기 처리도 적어 이 방법이 훨씬 편하다.

예시를 살펴보자.
아래 모습과 같이 이번에는 CollectionView안이지만, Cell과는 별도로 별도의 EmptyView를 만들어줄 거다.
그리고 EmptyView에 표출될 element들을 이 UIView안에 구현해주기만 하면 된다.

그리고 정말 단순하게, 데이터를 불러와서 담는 부분의 count값 혹은 isEmpty의 여부에 따라서,
테이블 뷰 혹은 컬렉션 뷰에 구현한 emptyView의 isHidden이 false인지, true인지를 분기 처리해주면 된다.

if self.frontCards?.count == 0 {
    self.emptyView.isHidden = false
} else {
    self.emptyView.isHidden = true
}

너무 허무...하지 않나.....?

아무튼 진짜 이게 전부다. 그래서 앞에서 말했듯이 1번 방법보다는 2번 방법을 주로 사용해서 엠티뷰(EmptyView)를 만든다고 한 거다!!

그래도, 결국 코딩은 본인이 직접 하는거니깐.
두 방법을 모두 익혀보고, 자신에게 맞는 방법을 잘 찾아 엠티뷰(EmptyView)를 구현하길 바란다.