본문 바로가기

iOS 앱 개발자 프로젝트

[iOS] picker 버튼을 dropdown UIView와 연결하기

며칠 전 만든 이미지 없는 picker 버튼 기억하시나요?

"아니, 전혀 모르겠는데?" →  이곳으로 

옵션 선택을 위한 picker btn

 

오늘은 망망대해에 홀로 떠있는 이 버튼 뷰에게 

'언제' '어디서' '어떻게' 모습을 드러내라! 하고 생명의 바람을 후! 넣어 줍니다. 


 

picker 버튼을 불러올 '중간다리', dropdown 만들기 

 

 

1. DropdownButtonViewDelegate 프로토콜: 두 개의 메서드 정의

  • didSelectOption(_ option: String): 옵션이 선택되었을 때 호출
  • dropdownButtonTapped(): 드롭다운 버튼이 눌렸을 호출
protocol DropdownButtonViewDelegate: AnyObject {
    func didSelectOption(_ option: String)
    func dropdownButtonTapped()
}

 

 

2. DropdownButtonView 클래스

  • DropdownButtonView 클래스는 커스텀 뷰로, 드롭다운 버튼과 옵션 목록을 포함한다.
class DropdownButtonView: UIView {

 

 

 

3. 변수 선언: delegate, button, options, tableView, isOpen

  • delegate: DropdownButtonViewDelegate 프로토콜을 따르는 델리게이트 객체 저장
  • button: 드롭다운 버튼을 정의 → 기본 설정, 타이틀, 색상, 폰트, 이미지 등을 설정
  • options: 드롭다운에 표시될 옵션들을 저장하는 배열
  • tableView: 옵션 목록을 표시할 UITableView 저장
  • isOpen: 드롭다운이 열려 있는지 여부를 나타내는 플래그
    weak var delegate: DropdownButtonViewDelegate?
    
    private let button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("최신순", for: .normal)
        button.setTitleColor(.challendarBlack60, for: .normal)
        button.titleLabel?.font = .pretendardMedium(size: 14)
        button.tintColor = .challendarGrey50
        button.setImage(UIImage(named: "arrowDown")?.resizeImage(to: CGSize(width: 16, height: 16)), for: .normal)
        button.semanticContentAttribute = .forceRightToLeft
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.challendarBlack60.cgColor
        button.layer.cornerRadius = 12
        return button
    }()
    
    private let options = ["기한임박", "등록순", "최신순"]
    private var tableView: UITableView!
    private var isOpen = false

 

 

4. 초기화 메서드: DropdownButtonView의 인스턴스 초기화

  • configureUI(): UI 요소들 설정
  • configureConstraint(): UI 요소들의 레이아웃 제약 조건을 설정
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureUI()
        configureConstraint()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

 

 

 

5. UI 구성

  • button: 버튼을 뷰에 추가하고, 버튼이 눌렸을 때 toggleDropdown 메서드가 호출되도록 설정
  • tableView: UITableView 초기화하고 델리게이트와 데이터 소스를 설정 → 초기 상태에서는 숨김(isHidden = true) 상태. 테이블 뷰의 스타일과 셀을 등록하고 마지막으로 테이블 뷰를 뷰에 추가했다.
    func configureUI() {
        addSubview(button)
        button.addTarget(self, action: #selector(toggleDropdown), for: .touchUpInside)
        
        tableView = UITableView()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.isHidden = true
        tableView.layer.cornerRadius = 5
        tableView.backgroundColor = .black
        tableView.separatorStyle = .none
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        addSubview(tableView)
    }

 

 

 

6. 제약 조건 설정: NSLayoutConstraint를 사용하여 버튼과 테이블 뷰의 제약 조건 설정

  • button: 버튼의 너비, 높이, leading, trailing, topAnchor, bottonAnchor 제약 조건 설정
  • tableView: 테이블 뷰의 topAncho, leading, trailing, 높이 제약 조건 설정
    func configureConstraint() {
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 69),
            button.heightAnchor.constraint(equalToConstant: 24),
            button.leadingAnchor.constraint(equalTo: leadingAnchor),
            button.trailingAnchor.constraint(equalTo: trailingAnchor),
            button.topAnchor.constraint(equalTo: topAnchor),
            button.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 5),
            tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
            tableView.heightAnchor.constraint(equalToConstant: 132)
        ])
    }

 

 

 

7. toggleDropdown 

  • 드롭다운 버튼이 눌렸을 호출 → isOpen 상태를 토글하고, 테이블 뷰의 숨김 상태를 토글. 델리게이트 메서드 dropdownButtonTapped() 호출.
    @objc private func toggleDropdown() {
        isOpen.toggle()
        tableView.isHidden = !isOpen
        delegate?.dropdownButtonTapped() // 드롭다운 버튼이 눌렸을 때 델리게이트 호출
    }

 

 

 

8. UITableViewDelegate & UITableViewDataSource (extension)

  • UITableViewDelegate 및 UITableViewDataSource 메서드:
    • numberOfRowsInSection(section:): 테이블 뷰의 섹션에 있는 행의 수를 반환 (여기서는 옵션의 개수)
    • cellForRowAt(indexPath:): 테이블 뷰의 각 행에 대한 셀을 설정 → 셀의 텍스트를 설정하고, 셀의 텍스트 색상과 배경 색상을 설정
    • didSelectRowAt(indexPath:): 테이블 뷰의 행이 선택되었을 호출. 선택 옵션을 버튼의 타이틀로 설정하고, 드롭다운을 닫고, 델리게이트 메서드 didSelectOption(option:) 호출
extension DropdownButtonView: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return options.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = options[indexPath.row]
        cell.textLabel?.textColor = .white
        cell.backgroundColor = .black
        cell.selectionStyle = .none
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let option = options[indexPath.row]
        button.setTitle(option, for: .normal)
        isOpen = false
        tableView.isHidden = true
        delegate?.didSelectOption(option)
    }
}

 

 

9. UIImage 확장: UIImage의 크기를 변경하는 메서드를 추가

  • resizeImage(to size:): 지정된 크기로 이미지를 리사이즈한다. 컨텍스트에서 이미지를 가져오고, 컨텍스트를 종료. 리사이즈된 이미지를 반환
extension UIImage {
    func resizeImage(to size: CGSize) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        draw(in: CGRect(origin: .zero, size: size))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage
    }
}

 

 

dropdown 최신순

 

 

여기까지 DropdownButtonView 컴포넌트 만들기 끝.

 

 

TodoCalendarViewController 에서 DropdownButtonView 를 연결하기 

 

 

 

1. DropdownButtonView 인스턴스를 dropdownButtonView 변수로 선언

class TodoCalendarViewController: BaseViewController  {
    private let periodBtnView = PeriodPickerButtonView() // 기간피커
    private let dropdownButtonView = DropdownButtonView() // 드롭다운 버튼 뷰
    // 나머지 변수들...
}

 

 

2. configureUI() 메서드에서 추가

  • view.addSubview(dropdownButtonView)를 통해 DropdownButtonView를 뷰 계층에 추가
  • 제약 조건 설정: dropdownButtonView.snp.makeConstraints를 사용하여 드롭다운 버튼의 위치와 크기를 설정. 위치는 make.top.equalToSuperview().offset(150)와 같이 값을 설정하여 화면에서 원하는 위치에 배치
override func configureUI() {
    super.configureUI()
    collectionView.snp.makeConstraints { make in
        make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
        make.leading.equalToSuperview().offset(16)
        make.trailing.equalToSuperview().offset(-16)
        make.bottom.equalToSuperview()
    }
    
    view.addSubview(periodBtnView) // 기간피커
    view.addSubview(dropdownButtonView) // 드롭다운 버튼 뷰 추가

    periodBtnView.snp.makeConstraints { make in
        make.width.equalTo(131)
        make.left.equalToSuperview().offset(16)
        make.top.equalToSuperview().offset(104)
    }
    
    dropdownButtonView.snp.makeConstraints { make in
        make.width.equalTo(69)
        make.height.equalTo(24)
        make.top.equalToSuperview().offset(150) // 적절한 위치로 조정
        make.left.equalToSuperview().offset(16)
    }
}

 

 

3. 델리게이트 설정

  • viewDidLoad() 메서드에서 dropdownButtonView.delegate = self를 통해 DropdownButtonView의 델리게이트를 TodoCalendarViewController로 설정 → 드롭다운 버튼에서 발생하는 이벤트를 컨트롤러에서 처리할 수 있다.
override func viewDidLoad() {
    filterTodoitems()
    configureCollectionView()
    super.viewDidLoad()
    configureFloatingButton()
    configureTitleNavigationBar(title: "월간")
    periodBtnView.delegate = self
    dropdownButtonView.delegate = self // 드롭다운 버튼 뷰의 델리게이트 설정
}

 

 

4. DropdownButtonViewDelegate 구현:  프로토콜을 구현하여 드롭다운 버튼 뷰의 델리게이트 메서드 정의

  • didSelectOption(_ option: String): 드롭다운에서 옵션이 선택되었을 때 호출. 선택된 옵션을 처리하는 로직을 여기에 작성
  • dropdownButtonTapped(): 드롭다운 버튼이 눌렸을 호출. 드롭다운 버튼이 눌렸을 때의 동작을 여기에 정의.
extension TodoCalendarViewController: DropdownButtonViewDelegate {
    func didSelectOption(_ option: String) {
        // 드롭다운에서 선택된 옵션을 처리합
        print("Selected option: \(option)")
    }

    func dropdownButtonTapped() {
        // 드롭다운 버튼이 눌렸을 때의 동작을 정의
        print("Dropdown button tapped")
    }
}

 

 


DropdownButtonView 코드 전체 ▽ 

import UIKit

protocol DropdownButtonViewDelegate: AnyObject {
    func didSelectOption(_ option: String)
    func dropdownButtonTapped()
}

class DropdownButtonView: UIView {
    
    weak var delegate: DropdownButtonViewDelegate?
    
    private let button: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("최신순", for: .normal)
        button.setTitleColor(.challendarBlack60, for: .normal)
        button.titleLabel?.font = .pretendardMedium(size: 14)
        button.tintColor = .challendarGrey50
        button.setImage(UIImage(named: "arrowDown")?.resizeImage(to: CGSize(width: 16, height: 16)), for: .normal)
        button.semanticContentAttribute = .forceRightToLeft
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.challendarBlack60.cgColor
        button.layer.cornerRadius = 12
        return button
    }()
    
    private let options = ["기한임박", "등록순", "최신순"]
    private var tableView: UITableView!
    private var isOpen = false
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureUI()
        configureConstraint()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureUI() {
        addSubview(button)
        button.addTarget(self, action: #selector(toggleDropdown), for: .touchUpInside)
        
        tableView = UITableView()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.isHidden = true
        tableView.layer.cornerRadius = 5
        tableView.backgroundColor = .black
        tableView.separatorStyle = .none
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        addSubview(tableView)
    }
    
    func configureConstraint() {
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.widthAnchor.constraint(equalToConstant: 69),
            button.heightAnchor.constraint(equalToConstant: 24),
            button.leadingAnchor.constraint(equalTo: leadingAnchor),
            button.trailingAnchor.constraint(equalTo: trailingAnchor),
            button.topAnchor.constraint(equalTo: topAnchor),
            button.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 5),
            tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
            tableView.heightAnchor.constraint(equalToConstant: 132)
        ])
    }
    
    @objc private func toggleDropdown() {
        isOpen.toggle()
        tableView.isHidden = !isOpen
        delegate?.dropdownButtonTapped() // 드롭다운 버튼이 눌렸을 때 델리게이트 호출
    }
}

extension DropdownButtonView: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return options.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = options[indexPath.row]
        cell.textLabel?.textColor = .white
        cell.backgroundColor = .black
        cell.selectionStyle = .none
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let option = options[indexPath.row]
        button.setTitle(option, for: .normal)
        isOpen = false
        tableView.isHidden = true
        delegate?.didSelectOption(option)
    }
}

extension UIImage {
    func resizeImage(to size: CGSize) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        draw(in: CGRect(origin: .zero, size: size))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage
    }
}

 

 

 

TodoCalendarViewController에서 dropdownBtn 과 periodPicker 연결 코드 전제 ▽

class TodoCalendarViewController: BaseViewController  {
    private let periodBtnView = PeriodPickerButtonView() // 기간피커
    private let dropdownButtonView = DropdownButtonView() // 드롭다운 버튼 뷰
    private var todoItems: [Todo] = CoreDataManager.shared.fetchTodos()
    private var completedTodo : [Todo] = []
    private var inCompletedTodo: [Todo] = []
    var days : [Day] = []
    var changedMonth : Date?
    var currentDate : Date?
    private var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        filterTodoitems()
        configureCollectionView()
        super.viewDidLoad()
        configureFloatingButton()
        configureTitleNavigationBar(title: "월간")
        periodBtnView.delegate = self
        dropdownButtonView.delegate = self // 드롭다운 버튼 뷰의 델리게이트 설정
    }
    
    override func configureUI() {
        super.configureUI()
        collectionView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
            make.leading.equalToSuperview().offset(16)
            make.trailing.equalToSuperview().offset(-16)
            make.bottom.equalToSuperview()
        }
        
        view.addSubview(periodBtnView) // 기간피커
        view.addSubview(dropdownButtonView) // 드롭다운 버튼 뷰 추가

        periodBtnView.snp.makeConstraints { make in
            make.width.equalTo(131)
            make.left.equalToSuperview().offset(16)
            make.top.equalToSuperview().offset(104)
        }
        
        dropdownButtonView.snp.makeConstraints { make in
            make.width.equalTo(69)
            make.height.equalTo(24)
            make.top.equalToSuperview().offset(150) // 적절한 위치로 조정
            make.left.equalToSuperview().offset(16)
        }
    }
}

extension TodoCalendarViewController: DropdownButtonViewDelegate {
    func didSelectOption(_ option: String) {
        // 드롭다운에서 선택된 옵션을 처리합니다.
        print("Selected option: \(option)")
    }

    func dropdownButtonTapped() {
        // 드롭다운 버튼이 눌렸을 때의 동작을 정의합니다.
        print("Dropdown button tapped")
    }
}

 

 

 

 

만들어둔 picker를 dropdown btn과 연결