iOS Custom Calendar 직접 만들어보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


Custom Calendar 직접 만들어보기

일반적으로 우리가 달력을 구현할 때는 fsCalendar라는 라이브러리 사용을 많이 합니다.
사실 결론적으로 저도 프로젝트를 진행하면서 결국은 fsCalendar를 사용했지만 초반에는 커스텀으로 직접 달력을 구현해보려고 했었습니다.

그 당시에 만들었던 코드를 공유하고자 합니다. :)

우선 당시 제 프로젝트 구조는 아래와 같습니다.

  1. 뷰컨 위에 테이블뷰가 있다.
  2. 테이블 뷰 셀 안에 컬렉션뷰가 존재한다.
  3. 컬렉션뷰로 달력을 구현한다.

따라서 컬렉션 뷰 셀 코드는 아래와 같습니다.

import UIKit

class CalendarCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var dateLbl: UILabel!
}

셀 안에 컨테이너 뷰로 뷰 하나를 만들어놓고 해당 뷰 내부에 라벨 하나를 넣어주었습니다.
그리고 아래 코드는 테이블뷰 셀 안에서 직접 달력을 그려주는 코드들이 진행되어집니다.

class WeekTableViewCell: UITableViewCell {
    let now = Date()
    var cal = Calendar.current
    let dateFormatter = DateFormatter()
    var components = DateComponents()
    var weeks: [String] = ["일", "월", "화", "수", "목", "금", "토"]
    var days: [String] = []
    // 해당 월이 몇일까지 있는지
    var daysCountInMonth = 0
    // 시작일
    var weekdayAdding = 0   

    @IBOutlet weak var yearMonthLbl: UILabel!
    @IBOutlet weak var collectionView: UICollectionView!

    override func awakeFromNib() {
        super.awakeFromNib()

        initUI()
    }

    func initUI() {
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        self.collectionView.isScrollEnabled = false

        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0

        self.collectionView.collectionViewLayout = layout

        dateFormatter.dateFormat = "yyyy년 M월"
        components.year = cal.component(.year, from: now)
        components.month = cal.component(.month, from: now)
        components.day = 1

        self.calculation()
        self.collectionView.reloadData()
    }

    /*
     1 일요일 2 - 1  -> 0번 인덱스부터 1일 시작
     2 월요일 2 - 2  -> 1번 인덱스부터 1일 시작
     3 화요일 2 - 3  -> 2번 인덱스부터 1일 시작
     4 수요일 2 - 4  -> 3번 인덱스부터 1일 시작
     5 목요일 2 - 5  -> 4번 인덱스부터 1일 시작
     6 금요일 2 - 6  -> 5번 인덱스부터 1일 시작
     7 토요일 2 - 7  -> 6번 인덱스부터 1일 시작
     */
    func calculation() {
        let firstDayOfMonth = cal.date(from: components)
        // 해당 수로 변환 > 1은 일요일 ~ 7은 토요일
        let firstWeeakDay = cal.component(.weekday, from: firstDayOfMonth!)
        daysCountInMonth = cal.range(of: .day, in: .month, for: firstDayOfMonth!)!.count
        // 이 과정을 해주는 이유는
        // 예로 2020년 4월이라하면 4월 1일은 수요일, 수요일이 달의 첫 날이 됨
        // 수요일은 components의 4이기 때문에 collectionView에서 앞의 3일은 비울 필요가 있음
        // 따라서 index가 1일부터 시작할 수 있도록 해줌
        // 따라서 2-4해서 -2부터 시작하게 되어 정확히 3일후부터 1일이 시작
        weekdayAdding = 2 - firstWeeakDay

        self.yearMonthLbl.text = dateFormatter.string(from: firstDayOfMonth!)

        self.days.removeAll()

        for day in weekdayAdding...daysCountInMonth {
            // 1보다 작은경우는 비워줘야함
            if day < 1 {
                self.days.append("")
            } else {
                self.days.append(String(day))
            }
        }
    }

    @IBAction func prevBtnClicked(_ sender: UIButton) {
        components.month = components.month! - 1
        self.calculation()
        self.collectionView.reloadData()
    }


    @IBAction func nextBtnClicked(_ sender: UIButton) {
        components.month = components.month! + 1
        self.calculation()
        self.collectionView.reloadData()
    }
}

extension WeekTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        switch section {
        case 0:
            // 요일 수는 고정
            return 7
        default:
            // 일의 수
            return self.days.count
        }
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCell", for: indexPath) as! CalendarCollectionViewCell

        switch indexPath.section {
        case 0:
            // 요일
            cell.dateLbl.text = weeks[indexPath.row]
        default:
            // 일
            cell.dateLbl.text = days[indexPath.row]
        }

        if indexPath.row % 7 == 0 {
            cell.dateLbl.textColor = .red
        } else if indexPath.row % 7 == 6 {
            cell.dateLbl.textColor = .blue
        } else {
            cell.dateLbl.textColor = .white
        }

        return cell
    }
}

iOS FSCalendar 써본것 모두다 싹다! 정리

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


FSCalendar

이번에 달력을 구현함에 있어서 FSCalendar라는 라이브러리를 사용한 경험이 있었습니다.
모든 기능을 다 써본것은 아니지만, 그래도 써본 모든 기능에 대해서 정리를 해보려 합니다!

기본 셋팅

여기서 중요한거… delegate들 꼭.. 꼭.. 가져오세요..
첨에 아니 이거 사용하려면 스택오버플로우에 이런 함수있다는데.. 대체 왜!! 왜 나는 안불러져 오는거지..?! 했는데

응~ FSCalendarDelegateAppearance 이거 채택 안했구요..ㅎ

꼭.. 꼭.. 잊지마세요!
저 같은 바보같은 행동 하지마세요.흑ㅎ극…

class CalendarViewController: UIViewController, FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance {
    @IBOutlet weak var calendar: FSCalendar!
    var selectedDate: Date = Date()

    override func viewDidLoad() {
        super.viewDidLoad()
        setCalendarUI()
    }

    // 오늘 날짜로 돌아오는 버튼 및 액션 
    @IBOutlet weak var currentBtn: UIButton!
    @IBAction func currentBtnClicked(_ sender: Any) {
        self.calendar.select(Date.Today())
    }

    // 현재 달로 돌아오기 위해서는 
    @IBAction func currentMonthBtnClicked(_ sender: Any) {
        self.calendar.setCurrentPage(Date(), animated: true)
    }

    func setCalendarUI() {
        // delegate, dataSource 
        self.calendar.delegate = self
        self.calendar.dataSource = self
        
        // calendar locale > 한국으로 설정 
        self.calendar.locale = Locale(identifier: "ko_KR")
        
        // 상단 요일을 한글로 변경 
        self.calendar.calendarWeekdayView.weekdayLabels[0].text = "일"
        self.calendar.calendarWeekdayView.weekdayLabels[1].text = "월"
        self.calendar.calendarWeekdayView.weekdayLabels[2].text = "화"
        self.calendar.calendarWeekdayView.weekdayLabels[3].text = "수"
        self.calendar.calendarWeekdayView.weekdayLabels[4].text = "목"
        self.calendar.calendarWeekdayView.weekdayLabels[5].text = "금"
        self.calendar.calendarWeekdayView.weekdayLabels[6].text = "토"
        
        // 월~일 글자 폰트 및 사이즈 지정
        self.calendar.appearance.weekdayFont = UIFont.SpoqaHanSans(type: .Regular, size: 14)
        // 숫자들 글자 폰트 및 사이즈 지정
        self.calendar.appearance.titleFont = UIFont.SpoqaHanSans(type: .Regular, size: 16)
        
        // 캘린더 스크롤 가능하게 지정
        self.calendar.scrollEnabled = true
        // 캘린더 스크롤 방향 지정 
        self.calendar.scrollDirection = .horizontal
        
        // Header dateFormat, 년도, 월 폰트(사이즈)와 색, 가운데 정렬
        self.calendar.appearance.headerDateFormat = "YYYY년 MM월"
        self.calendar.appearance.headerTitleFont = UIFont.SpoqaHanSans(type: .Bold, size: 20)
        self.calendar.appearance.headerTitleColor = UIColor(named: "FFFFFF")?.withAlphaComponent(0.9)
        self.calendar.appearance.headerTitleAlignment = .center
        
        // 요일 글자 색
        self.calendar.appearance.weekdayTextColor = UIColor(named: "F5F5F5")?.withAlphaComponent(0.2)
        
        // 캘린더 높이 지정 
        self.calendar.headerHeight = 68        
        // 캘린더의 cornerRadius 지정 
        self.calendar.layer.cornerRadius = 10
        
        // 양옆 년도, 월 지우기
        self.calendar.appearance.headerMinimumDissolvedAlpha = 0.0
    
        // 달에 유효하지 않은 날짜의 색 지정 
        self.calendar.appearance.titlePlaceholderColor = UIColor.white.withAlphaComponent(0.2)
        // 평일 날짜 색
        self.calendar.appearance.titleDefaultColor = UIColor.white.withAlphaComponent(0.5)
        // 달에 유효하지않은 날짜 지우기 
        self.calendar.placeholderType = .none
        
        // 캘린더 숫자와 subtitle간의 간격 조정 
        self.calendar.appearance.subtitleOffset = CGPoint(x: 0, y: 4)
        
        self.calendar.select(selectedDate)
    }

    // 날짜를 선택했을 때 할일을 지정 
    func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
        self.navigationController?.popViewController(animated: true)
        self.delegate?.dateUpdated(date: date.key)
    }

    // 선택된 날짜의 채워진 색상 지정 
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, fillSelectionColorFor date: Date) -> UIColor? {
        return appearance.selectionColor
    }

    // 선택된 날짜 테두리 색상
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, borderSelectionColorFor date: Date) -> UIColor? {
        return UIColor.white.withAlphaComponent(1.0)
    }
    
    // 모든 날짜의 채워진 색상 지정 
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, fillDefaultColorFor date: Date) -> UIColor? {
        return UIColor.white
    }
    
    // title의 디폴트 색상 
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, titleDefaultColorFor date: Date) -> UIColor? {
        return UIColor.white.withAlphaComponent(0.5)
    }
    
    // subtitle의 디폴트 색상 
    func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, subtitleDefaultColorFor date: Date) -> UIColor? {
        return UIColor.white.withAlphaComponent(0.5)
    }
    
    // 원하는 날짜 아래에 subtitle 지정 
    // 오늘 날짜에 오늘이라는 글자를 추가해보았다
    func calendar(_ calendar: FSCalendar, subtitleFor date: Date) -> String? {
        switch dateFormatter.string(from: date) {
        case dateFormatter.string(from: Date()):
            return "오늘"
        default:
            return nil
        }
    }

    // 날짜의 글씨 자체를 오늘로 바꾸기 
    func calendar(_ calendar: FSCalendar, titleFor date: Date) -> String? {
        switch dateFormatter.string(from: date) {
        case dateFormatter.string(from: Date()):
            return "오늘"
        default:
            return nil
        }
    }
}

이 외에도 커스텀할 수 있는 다양한 요소들이 있습니다.
함수를 호출해서 사용하는 방법은 여러가지이니 각자 취향에 맞게 사용하면 좋을 것 같습니다 :)

Xcode Error, Unable to boot device because it cannot be located on disk

|

개인적인 연습 내용을 정리한 글입니다.
더 좋은 방법이 있거나, 잘못된 부분이 있으면 편하게 의견 주세요. :)


Unable to boot device because it cannot be located on disk

이것 저것 삽질을 하던 중, 진짜 삽질을 했다.
파인더에서 뭐가 엄청 지저분해보이길래.. 그냥 삭제를 했다.

파일을 찾는데, 이거 삭제하고 다시 빌드를 하면 생기겠지.. 하는 안일한 생각과 함께 ..

그랬더니 Unable to boot device because it cannot be located on disk 이런 에러가 두두등장

검색해보니, 프로젝트를 실행해서 시뮬레이터를 재생하려는데 해당 시뮬레이터가 지정된 위치에 없을때 발생하는것이란다.
고로 내가 시뮬레이터들을 모조리 삭제했다는 것을 의미하는 것 같은데.. 맞는 것 같았다 .ㅎ…

해결방법은 간단하다.

  1. 현재 열려있는 Xcode와 시뮬레이터를 모두 종료시킨다.
  2. 터미널을 키고 아래 명령어를 적어준다.
xcrun simctl erase all

그러면 에러 해결 완료!

iOS SCPageControl 사용해보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


SCPageControl 사용해보기

이번엔 페이지 컨트롤의 라이브러리인 SCPageControl를 사용하는 방법에 대해 정리해보겠습니당

방법은 엄청 간단합니다.

class HomeTableViewCell: UITableViewCell {
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var sc: SCPageControlView!

    override func awakeFromNib() {
        super.awakeFromNib()
        // 페이지컨트롤에 필요한 페이지 수/페이지가 시작할 위치/현재 보여지는 페이지일때 페이지컨트롤의 색/기본 색 지정 
        sc.set_view(2, current: 0, current_color: UIColor(named: "7A7BDA")!, disable_color: nil)
        // 페이지 컨트롤 스타일 지정 
        sc.scp_style = .SCJAFlatBar
    }
}

extension HomeTableViewCell: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        sc.scroll_did(scrollView)
    }
}

이때 여기서 가장 중요한 포인트는 코드에 있지않습니다.

일반적으로 생각해보면 페이지컨트롤이 컬렉션뷰 안에 존재할것같다고 생각할수도 있습니다.
그렇지만 그렇지 않습니다. 컬렉션뷰 바깥에 같은 동일선상에 존재하고 있어야 합니다.

이렇게 말이죠!

iOS MKMagneticProgress를 사용해 circleProgressBar 만들어보기

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


반원 짜리 모양을 가진 프로그래스 뷰를 만들고 싶었습니다.
앞서 헀던 방식으로 직접 뷰를 그릴수도 있지만, 이번에는 라이브러리를 통해서 만들어보려고 합니다.

MKMagneticProgress를 사용해 circleProgressBar 만들어보기

MKMagneticProgress

import MKMagneticProgress

class HomeTableViewCell: UITableViewCell {
    @IBOutlet weak var circleProgressBar: MKMagneticProgress!

    override func awakeFromNib() {
        super.awakeFromNib()

        setProgressViewUI()
    }
    
    func setProgressViewUI() {
        circleProgressBar.setProgress(progress: 0)

        circleProgressBar.progressShapeColor = UIColor(named: "7A7BDA")!
        circleProgressBar.backgroundShapeColor = (UIColor(named: "FFFFFF")?.withAlphaComponent(0.1))!
        circleProgressBar.titleColor = UIColor.red
        circleProgressBar.percentColor = UIColor.white
        
        
        circleProgressBar.lineWidth = 16
        circleProgressBar.orientation = .bottom
        circleProgressBar.lineCap = .round
    }
}

무척 간단합니다.
여기서 중요한 점은 setProgress에서 progress로 받는 숫자는 0~1사이의 숫자입니다.

전체를 1로 보고 그 사이의 숫자들을 표현해주고 있는 것을 의미합니다.
따라서 해당하는 progress에 유의미한 그래프를 보여주고 싶다면 반드시 0과 1사이의 숫자로 표현해서 보여줘야함을 잊지말아야 합니다!