본문 바로가기
한눈가계부/캘린더

popoverPresentationController 사용하여 연도 + 월을 선택하기

by 밤새는 탐험가89 2025. 9. 17.
728x90
SMALL

 

Swift로 구현하는 팝오버(Popover) 날짜 선택기

 이번 글에서는 가계부 앱의 핵심 기능 중 하나인 '월별 날짜 선택' 기능을 개선한 방법에 대해 공유하려 합니다. 기존에는 스크롤을 통해 월을 이동하는 방식이었지만, 사용자 경험을 개선하고자 팝오버(Popover) 형태로 연도와 월을 한눈에 보고 선택할 수 있는 기능을 추가했습니다.

 

이 기능을 구현하는 데 사용된 핵심 기술은 UIPopoverPresentationController입니다. 일반적인 modal 방식과 달리, 팝오버는 기존 화면을 유지한 채 특정 뷰 위에 작은 뷰를 띄워 내용을 보여줄 수 있어, 가볍고 직관적인 UI를 제공합니다.


1. 팝오버 뷰 컨트롤러: DatePickerViewController

먼저, 날짜를 선택하는 팝오버 화면을 담당할 DatePickerViewController를 살펴보겠습니다. 이 뷰 컨트롤러는 연도를 표시하는 UILabel과 UIImageView로 구성된 상단 뷰, 그리고 12개의 월을 표시하는 UICollectionView로 이루어져 있습니다.

 

class DatePickerViewController: UIViewController {

    // MARK: - Properties & Closure
    
    private var currentYear: Int = 0
    private var currentMonth: Int = 0
    
    var selectedDate: ((Int, Int) -> Void)?
    
    // MARK: - UI Elements
    
    private let yearLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont(name: "Ownglyph_daelong-Rg", size: 20) ?? .systemFont(ofSize: 20, weight: .regular)
        label.textColor = .label
        label.textAlignment = .center
        return label
    }()
    
    private let previousImage: UIImageView = {
        let imageView = UIImageView(image: UIImage(systemName: "arrow.backward"))
        imageView.tintColor = .label
        imageView.isUserInteractionEnabled = true
        return imageView
    }()
    
    private let followingImage: UIImageView = {
        let imageView = UIImageView(image: UIImage(systemName: "arrow.forward"))
        imageView.tintColor = .label
        imageView.isUserInteractionEnabled = true
        return imageView
    }()
    
    private var dateCollectionView: UICollectionView!
    
    // MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.currentYear = Calendar.current.component(.year, from: Date())
        configureUI()
        setupAction()
    }
}

 

  • UI 구성: UIStackView를 활용하여 연도와 화살표 이미지를 깔끔하게 배치했습니다. UICollectionView는 12개의 월을 표시하는 역할을 합니다.
  • 클로저(selectedDate): 이 클로저는 날짜 선택 시 호출되는 콜백 함수입니다. 선택된 연도와 월을 상위 뷰 컨트롤러(캘린더 뷰)로 전달하는 중요한 역할을 합니다.

 

 

컬렉션 뷰 설정

UICollectionView는 UICollectionViewDelegateFlowLayoutUICollectionViewDataSource 프로토콜을 채택하여 월별 셀을 구성합니다. 4열로 나누어 widthPerItem을 계산하는 로직이 인상적입니다.

 

didSelectItemAt: 셀 선택 시, selectedDate 클로저를 통해 선택된 연도와 월을 전달하고, dismiss를 호출하여 팝오버를 닫습니다.

// UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let interItemSpacing: CGFloat = 8
    let totalSpacing = interItemSpacing * 3 // 3개의 간격
    let availableWidth = collectionView.bounds.width - totalSpacing
    let widthPerItem = availableWidth / 4
    
    return CGSize(width: widthPerItem, height: widthPerItem - 8)
}

// UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 12
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "monthCell", for: indexPath)
    let monthLabel = UILabel(frame: cell.contentView.bounds)
    monthLabel.text = "\(indexPath.item + 1)"
    // ... UI 설정
    cell.contentView.addSubview(monthLabel)
    return cell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let selectedMonth = indexPath.item + 1
    self.currentMonth = selectedMonth
    selectedDate?(currentYear, currentMonth) // 클로저 호출
    dismiss(animated: true)
}

2. 팝오버 표시하기: PopoverPresentationController

이제 DatePickerViewController를 캘린더 뷰에서 팝오버 형태로 띄워봅시다. UITapGestureRecognizer를 통해 월을 표시하는 UILabel이 탭되었을 때, didTappedMonthLabel() 메서드가 호출됩니다.

didTappedMonthLabel() 메서드

@objc private func didTappedMonthLabel() {
    let datePickerVC = DatePickerViewController()
    
    // 팝오버 스타일 설정
    datePickerVC.modalPresentationStyle = .popover
    datePickerVC.preferredContentSize = CGSize(width: 250, height: 200)

    // 날짜 선택 클로저 구현
    datePickerVC.selectedDate = { [weak self] year, month in
        guard let self = self else { return }
        let dateComponents = DateComponents(year: year, month: month)
        guard let selectedDate = Calendar.current.date(from: dateComponents) else { return }
        
        self.calendarViewModel.currentMonth = selectedDate
        self.calendarViewModel.setupInitialData(with: selectedDate)
        self.monthLabel.text = self.formattedMonth(for: self.calendarViewModel.currentMonth)
    }

    // UIPopoverPresentationController 설정
    if let popover = datePickerVC.popoverPresentationController {
        popover.sourceView = monthLabel // 팝오버가 시작될 뷰 (monthLabel)
        popover.permittedArrowDirections = [.up, .down] // 화살표 방향
        popover.delegate = self // 델리게이트 지정
    }
    
    present(datePickerVC, animated: true)
}

 

 

  • modalPresentationStyle = .popover:
    • 뷰 컨트롤러를 팝오버 스타일로 띄우겠다고 선언하는 가장 중요한 코드입니다.
  • preferredContentSize:
    • 팝오버 뷰의 크기를 설정합니다.
  • popover.sourceView:
    • 팝오버가 어디에서 나올지 기준이 되는 뷰를 지정합니다. 여기서는 monthLabel이 팝오버를 띄우는 기준이 됩니다.
  • popover.permittedArrowDirections:
    • 팝오버가 가리키는 화살표의 방향을 설정합니다.
  • popover.delegate:
    • 팝오버의 동작을 제어하기 위해 델리게이트를 지정합니다. UIPopoverPresentationControllerDelegate를 채택하면 아이폰에서 팝오버가 전체 화면으로 표시되는 것을 막을 수 있습니다.

3. 마무리: 델리게이트 메서드 구현

아이폰에서는 팝오버가 기본적으로 전체 화면으로 표시됩니다. 이를 방지하고 팝오버 스타일을 유지하려면 UIPopoverPresentationControllerDelegate를 채택하고 다음 델리게이트 메서드를 구현해야 합니다.

extension YourViewController: UIPopoverPresentationControllerDelegate {
    func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
        return .none
    }
}

 

 

return .none을 통해 '어떤 환경에서든' 팝오버를 전체 화면으로 변환하지 않도록 설정하여 아이폰에서도 팝오버 스타일을 유지할 수 있습니다.

이번 개선을 통해 가계부 앱 사용자는 월을 직관적으로 선택하고, 더 편리하게 데이터를 관리할 수 있게 되었습니다. 팝오버 기능은 날짜 선택 외에도 다양한 상황에서 깔끔한 UI를 구현하는 데 유용하게 활용될 수 있습니다.

728x90
LIST