iOS 개발 swift로 연월(년, 월)만 설정할 수 있는 UIPickerView 커스텀 하기

Danna 다나·2021년 10월 31일
1

우당탕탕 해결기

목록 보기
7/7
post-thumbnail

생각보다 개발을 하다보면 pickerView를 쓸 일이 꽤 많은 것 같다.

그래서 보통 피커뷰로 날짜를 설정하려고 하면 iOS에서 기본으로 제공하는 UIDatePicker을 사용하는데, 예쁘고 간편하긴 하지만 커스텀이 제한적이라 실제로 서비스에서 사용해본 적은 많이 없는 것 같다.

대신 커스텀이 용이한 UIPickerView로 UIDatePicker를 흉내내는 방식의 구현을 자주 사용한다.

iOS 프로젝트 개발에서 생각보다 많을 것 같지만, 한글로 된 블로그 글이 몇 개 없는 것 같아 정리해보는 오늘의 주제는 아래처럼 정했다.

UIPickerView로 년, 월만 선택 가능한 피커 만들기

1. 텍스트필드 얹기

피커뷰를 뷰에 컴포넌트로 얹을 수도 있지만, 보통은 코드로 많이 생성한다.
(뇌피셜로는 뷰에 얹어서 isHidden 처리 해주는 것보다 코드로 설정해주는 게 훨씬 간단하기 때문일 것 같다)

피커뷰를 코드로 만들기 위해서는 뜬금없지만 뷰에 UITextField가 있는 상태여야 한다. 추후에 이 텍스트필드에 입력수단으로 피커뷰를 이용할 계획이다. (텍스트필드의 커서는 없애서 UIButton처럼 구동하게 할 생각이므로 같은 동작을 하는 버튼은 없어도 된다. 버튼 대신 텍스트필드를 얹자)

나는 스토리보드에서 yearTextField라는 텍스트필드를 만들고 IBOutlet을 연결해주었다.

@IBOutlet var yearTextField: UITextField!

2. 피커뷰 코드 쓰기

그 후에는 이제 생성한 텍스트필드의 입력 수단으로 이용될 피커뷰를 제작하면 되는데, 이때는 스토리보드에서 아무런 작업 없이 코드만 추가해주면 된다.

override func viewDidLoad() {
    super.viewDidLoad()

    createPickerView()
}

/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear
        
    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
}

extension FundManagerThinkVC: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2 /// 년, 월 두 가지 선택하는 피커뷰
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch component {
        case 0:
            return availableYear.count /// 연도의 아이템 개수
        case 1:
            return allMonth.count /// 월의 아이템 개수
        default:
            return 0
        }
    }
    
    /// 표출할 텍스트 (2020년, 2021년 / 1월, 2월, 3월, 4월 ... )
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch component {
        case 0:
            return "\(availableYear[row])년"
        case 1:
            return "\(allMonth[row])월"
        default:
            return ""
        }
    }
    
    /// 피커뷰에서 선택된 행을 처리할 수 있는 메서드
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        
        switch component {
        case 0:
            selectedYear = availableYear[row]
        case 1:
            selectedMonth = allMonth[row]
        default:
            break
        }
    }
}

이렇게 하면 텍스트필드를 클릭했을 때 피커뷰가 올라온다.

3. 툴바 코드 쓰기

하지만 확인 버튼이 없네..? 피커뷰는 어떻게 내리지?

그 처리를 위해 툴바를 추가해준다.
참고로 툴바는 입력창 위에 취소, 확인 버튼 있는 부분이다.

툴바 코드를 추가한 피커뷰 코드는 다음과 같다.

/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear
        
    /// 툴바 세팅
    let toolBar = UIToolbar()
    toolBar.sizeToFit()
    
    let btnDone = UIBarButtonItem(title: "확인", style: .done, target: self, action: #selector(onPickDone))
    let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    let btnCancel = UIBarButtonItem(title: "취소", style: .done, target: self, action: #selector(onPickCancel))
    toolBar.setItems([btnCancel , space , btnDone], animated: true)
    toolBar.isUserInteractionEnabled = true
        
    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
    yearTextField.inputAccessoryView = toolBar
}

// 피커뷰 > 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -> 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = "\(selectedYear)년"
    monthLabel.text = "\(selectedMonth)월"
    
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}
     
// 피커뷰 > 취소 클릭
@objc func onPickCancel() {
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

부가 설명을 하자면,
확인 버튼, 취소 버튼을 설정하는 부분에서
let btnDone = UIBarButtonItem(title: "확인", style: .done, target: self, action: #selector(onPickDone))
이 코드에서 title은 버튼 텍스트를,
style은 버튼의 형태, 코드에서 .을 눌러보면 모든 옵션을 볼 수 있는데 done과 plain이 있다
그리고 action: #selector()에서는 버튼이 눌렸을 때 어떤 액션을 취할 것인지 @objc func를 넣을 수 있는 형태로 되어 있다.
여기서는 onPickDone이라는 메서드를 정의했고,

// 피커뷰 > 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -> 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = "\(selectedYear)년"
    monthLabel.text = "\(selectedMonth)월"
    
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

이러한 액션들을 하고 있다.
다른 부분은 마음대로 써도 되지만, textField.resignFirstResponder() 부분은 꼭 써줘야 확인을 눌렀을 때 피커뷰가 사라진다.

같은 원리로 취소 버튼도 만들어주면 된다.

4. 날짜 계산

사실 2번까지만 해도 피커뷰를 띄울 수는 있지만,
조건이 하나 더 붙은 상황이라고 가정해보자.

2020년 1월부터 현재까지만 선택 가능하게 해주세요

라는 조건이 붙었을 때 우리는 아 날짜 계산을 해야겠다라고 생각하게 된다.

계산은 오늘 날짜를 계산해주는 Date()라는 걸 활용할 예정이다.

var availableYear: [Int] = []
var allMonth: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
var selectedYear = 0
var selectedMonth = 0
var todayYear = "0"
var todayMonth = "0"

/// 가능한 날짜 설정
func setAvailableDate() {
	/// 선택 가능한 연도 설정
    let formatterYear = DateFormatter()
    formatterYear.dateFormat = "yyyy"
    todayYear = formatterYear.string(from: Date())
        
    for i in 2020...Int(todayYear)! {
        availableYear.append(i)
    }
    
    /// 선택 가능한 달 설정
    let formatterMonth = DateFormatter()
    formatterMonth.dateFormat = "MM"
    todayMonth = formatterMonth.string(from: Date())
        
    selectedYear = Int(todayYear)!
    selectedMonth = Int(todayMonth)!
}

5. 미래 날짜 선택 막기

마지막으로 해줘야 할 것이 미래 날짜를 선택했을 때 막는 기능이다.
생각보다 간단하다.
기본으로 제공되는 pickerView.selectRow()를 이용하면 된다.
그러면 우리가 원하는 행으로 피커 휠을 돌릴 수 있다.

pickerView didselectRow에 다음 코드를 추가해주었다.

if (Int(todayYear) == selectedYear && Int(todayMonth)! < selectedMonth) {
        pickerView.selectRow(Int(todayMonth)!-1, inComponent: 1, animated: true)
        selectedMonth = Int(todayMonth)!
}

여기서 맨 처음 인자로는 돌릴 행(몇 월인지 - Int(todayMonth)!-1)을 inComponent에는 돌릴 열(이 코드에서 년도면 0, 월이면 1)을, 마지막으로는 돌아가는 애니메이션을 넣을지 말지를 설정해주면 된다.

그러면 만약 오늘이 2021년 10월이라고 할 때, 사용자가 2021년 11월을 선택하면 2021년 10월로 자동으로 돌려주게 된다.

부록) 전체 코드

var availableYear: [Int] = []
var allMonth: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
var selectedYear = 0
var selectedMonth = 0
var todayYear = "0"
var todayMonth = "0"

@IBOutlet var yearTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()
    
    setAvailableDate()
    createPickerView()
}

/// 피커뷰 생성
func createPickerView() {
    /// 피커 세팅
    let pickerView = UIPickerView()
    pickerView.delegate = self
    pickerView.dataSource = self
    yearTextField.tintColor = .clear
        
    /// 툴바 세팅
    let toolBar = UIToolbar()
    toolBar.sizeToFit()
    
    let btnDone = UIBarButtonItem(title: "확인", style: .done, target: self, action: #selector(onPickDone))
    let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    let btnCancel = UIBarButtonItem(title: "취소", style: .done, target: self, action: #selector(onPickCancel))
    toolBar.setItems([btnCancel , space , btnDone], animated: true)
    toolBar.isUserInteractionEnabled = true
        
    /// 텍스트필드 입력 수단 연결
    yearTextField.inputView = pickerView
    yearTextField.inputAccessoryView = toolBar
}

// 피커뷰 > 확인 클릭
@objc func onPickDone() {
    /// 확인 눌렀을 때 액션 정의 -> 아래 코드에서는 라벨 텍스트 업데이트
    yearLabel.text = "\(selectedYear)년"
    monthLabel.text = "\(selectedMonth)월"
    
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}
     
// 피커뷰 > 취소 클릭
@objc func onPickCancel() {
    yearTextField.resignFirstResponder() /// 피커뷰 내림
}

/// 가능한 날짜 설정
func setAvailableDate() {
	/// 선택 가능한 연도 설정
    let formatterYear = DateFormatter()
    formatterYear.dateFormat = "yyyy"
    todayYear = formatterYear.string(from: Date())
        
    for i in 2020...Int(todayYear)! {
        availableYear.append(i)
    }
    
    /// 선택 가능한 달 설정
    let formatterMonth = DateFormatter()
    formatterMonth.dateFormat = "MM"
    todayMonth = formatterMonth.string(from: Date())
        
    selectedYear = Int(todayYear)!
    selectedMonth = Int(todayMonth)!
}

extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch component {
        case 0:
            return availableYear.count
        case 1:
            return allMonth.count
        default:
            return 0
        }
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch component {
        case 0:
            return "\(availableYear[row])년"
        case 1:
            return "\(allMonth[row])월"
        default:
            return ""
        }
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        
        switch component {
        case 0:
            selectedYear = availableYear[row]
        case 1:
            selectedMonth = allMonth[row]
        default:
            break
        }
        
        if (Int(todayYear) == selectedYear && Int(todayMonth)! < selectedMonth) {
            pickerView.selectRow(Int(todayMonth)!-1, inComponent: 1, animated: true)
            selectedMonth = Int(todayMonth)!
        }
    }
}
profile
요즘은 https://welcometodannas.tistory.com/에 더 많은 글을 씁니다.

0개의 댓글