[UIKit] UITableView: Swipe Actions

Junyoung Park·2022년 12월 16일
0

UIKit

목록 보기
122/142
post-thumbnail
post-custom-banner

Swift: Custom TableView Swipe Actions (2021, Xcode 12, Swift 5) - iOS Development

UITableView: Swipe Actions

구현 목표

  • 테이블 뷰 커스텀 스와이프 액션 구현

구현 태스크

  • 커스텀 테이블 뷰 셀 구현
  • 커스텀 테이블 뷰 스와이프 액션 구현
  • 테이블 뷰 리로드 로직 구현

핵심 코드

    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let model = models[indexPath.row]
        
        let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, success in
            self?.models.remove(at: indexPath.row)
            self?.tableView.deleteRows(at: [indexPath], with: .automatic)
            success(true)
        }
        deleteAction.image = UIImage(systemName: "xmark")
        
        let favoriteAction = UIContextualAction(style: .normal, title: "Favorite") { [weak self] _, _, success in
            self?.models[indexPath.row].isFavorite.toggle()
            self?.tableView.reloadRows(at: [indexPath], with: .automatic)
            success(true)
        }
        favoriteAction.backgroundColor = model.isFavorite ? .systemBlue : .systemPink
        favoriteAction.image = UIImage(systemName: model.isFavorite ? "hand.thumbsdown.circle" : "hand.thumbsup.circle")
        
        return UISwipeActionsConfiguration(actions: [deleteAction, favoriteAction])
    }
  • 좌측/우측 방향의 스와이프 액션 커스텀 델리게이트 함수
  • UISwipeActionsConfigurationUIContextualAction의 집합으로 구성
  • UIContextualAction은 액션 스타일, 타이틀, 이미지, 컬러, 핸들러 등을 지원
  • 삭제 메소드라면 데이터 소스의 데이터 삭제 및 테이블 뷰의 행 삭제
  • 변경 메소드라면 데이터 소스 데이터 변경 및 테이블 뷰 리로드 행

소스 코드

import UIKit

final class SwipeActionTableViewController: UIViewController {
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        tableView.register(SwipeActionTableViewCell.self, forCellReuseIdentifier: SwipeActionTableViewCell.identifier)
        tableView.delegate = self
        tableView.dataSource = self
        return tableView
    }()
    private var models: [PokemonModel] = PokemonModel.stubs
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }
    
    private func setUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(tableView)
    }
}

extension SwipeActionTableViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let model = models[indexPath.row]
        
        let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, success in
            self?.models.remove(at: indexPath.row)
            self?.tableView.deleteRows(at: [indexPath], with: .automatic)
            success(true)
        }
        deleteAction.image = UIImage(systemName: "xmark")
        
        let favoriteAction = UIContextualAction(style: .normal, title: "Favorite") { [weak self] _, _, success in
            self?.models[indexPath.row].isFavorite.toggle()
            self?.tableView.reloadRows(at: [indexPath], with: .automatic)
            success(true)
        }
        favoriteAction.backgroundColor = model.isFavorite ? .systemBlue : .systemPink
        favoriteAction.image = UIImage(systemName: model.isFavorite ? "hand.thumbsdown.circle" : "hand.thumbsup.circle")
        
        return UISwipeActionsConfiguration(actions: [deleteAction, favoriteAction])
    }
    
    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let model = models[indexPath.row]
        
        let muteAction = UIContextualAction(style: .normal, title: "Mute") { [weak self] _, _, success in
            self?.models[indexPath.row].isMuted.toggle()
            self?.tableView.reloadRows(at: [indexPath], with: .automatic)
            success(true)
        }
        muteAction.backgroundColor = model.isMuted ? .systemOrange : .systemGreen
        muteAction.image = UIImage(systemName: model.isMuted ? "speaker.circle" : "speaker.slash.circle")
        return UISwipeActionsConfiguration(actions: [muteAction])
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

extension SwipeActionTableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return models.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: SwipeActionTableViewCell.identifier, for: indexPath) as? SwipeActionTableViewCell else { fatalError() }
        cell.configure(with: models[indexPath.row])
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return view.frame.height / 8
    }
}
  • 커스텀 스와이프 액션을 테이블 뷰의 델리게이트 함수를 통해 간단하게 구현
  • 테이블 뷰의 각 셀의 크기는 전체 디바이스 크기의 8분의 1로 구성, 즉 8개의 셀로 전체 뷰를 구성
import UIKit

final class SwipeActionTableViewCell: UITableViewCell {
    static let identifier = "SwipeActionTableViewCell"
    private let pokemonImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        return imageView
    }()
    private let pokemonNameLabel: UILabel = {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .headline)
        label.textAlignment = .left
        return label
    }()
    private let favoriteImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        return imageView
    }()
    private let muteImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        return imageView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let size = contentView.frame.size.height
        pokemonImageView.frame = CGRect(origin: .zero, size: .init(width: size, height: size))
        pokemonImageView.layer.cornerRadius = size / 2
        let otherSize = size / 2
        favoriteImageView.frame = CGRect(x: size + 10, y: (contentView.frame.height - otherSize) / 2, width: otherSize, height: otherSize)
        muteImageView.frame = CGRect(x: size + otherSize + 20, y: (contentView.frame.height - otherSize) / 2, width: otherSize, height: otherSize)
        pokemonNameLabel.frame = CGRect(x: size + otherSize + otherSize + 30, y: 0, width: contentView.frame.size.width - (size + otherSize + otherSize) - 30, height: size)
    }
    
    private func setUI() {
        contentView.addSubview(pokemonImageView)
        contentView.addSubview(pokemonNameLabel)
        contentView.addSubview(favoriteImageView)
        contentView.addSubview(muteImageView)
    }
    
    func configure(with model: PokemonModel) {
        favoriteImageView.image = UIImage(systemName: model.isFavorite ? "hand.thumbsdown.circle" : "hand.thumbsup.circle")?.withTintColor(model.isFavorite ? .systemBlue : .systemPink, renderingMode: .alwaysOriginal)
        muteImageView.image = UIImage(systemName: model.isMuted ? "speaker.circle" : "speaker.slash.circle")?.withTintColor(model.isMuted ? .systemOrange : .systemGreen, renderingMode: .alwaysOriginal)
        pokemonNameLabel.text = model.name
        
        guard let url = URL(string: model.imageURLString) else { return }
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            guard
                let data = data,
                let response = response as? HTTPURLResponse,
                response.statusCode >= 200 && response.statusCode < 400,
                error == nil else { return }
            DispatchQueue.main.async { [weak self] in
                self?.pokemonImageView.image = UIImage(data: data)
            }
        }
        .resume()
    }
}
  • configure 함수 단에서 들어오는 모델의 isMuted, isFavorite 등 불리언 값에 따라 현재 셀의 이미지를 변경

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글