Anniversary Counter

스토리보드 구성


Dday를 나타내는 디데이, 날짜 이미지 뷰를 포함해 뷰로 틀을 고정한 뒤 스택뷰로 잡았다.

Main View Controller

import UIKit

class MainViewController: UIViewController {

  @IBOutlet weak var datePicker: UIDatePicker!
  @IBOutlet var dDayLabels: [UILabel]!
  @IBOutlet var dDayImageViews: [UIImageView]!
  @IBOutlet var targetDates: [UILabel]!
  @IBOutlet var capsuleViews: [UIView]!
  
  var dDayManager = DdayManager()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    dDayManager.load()
    
    //Calendar
    if #available(iOS 14.0, *) {
      datePicker.preferredDatePickerStyle = .inline
    }
    
    //dDayCapule UI
    for (idx, capsule) in capsuleViews.enumerated() {
      capsule.layer.cornerRadius = capsule.frame.width / 8
      capsule.clipsToBounds = true
      
      let tapGesture = DdayViewTapGesture(target: self, action: #selector(dDaySetup(sender:)))
      tapGesture.idx = idx
      capsule.addGestureRecognizer(tapGesture)
    }
    
    updateUI()
  }
  
  @objc func dDaySetup(sender: DdayViewTapGesture) {
    guard let idx = sender.idx else { return }
    let alert = UIAlertController(title: "디데이설정", message: nil, preferredStyle: .alert)
    
    alert.addTextField { afterDay in
      afterDay.placeholder = "숫자만 입력하세요"
      afterDay.keyboardType = .numberPad
    }
    
    alert.addTextField { color in
      color.placeholder = "#FFFFFF"
      color.text = "#"
    }
    
    let ok = UIAlertAction(title: "확인", style: .default) { _ in
      let afterDay = alert.textFields?[0].text ?? ""
      let color = alert.textFields?[1].text ?? ""
      
      if let intAfterDay = Int(afterDay) {
        self.dDayManager.ddays[idx].dday = intAfterDay
      }
      
      if color.count == 7 {
        self.dDayManager.ddays[idx].backgroundColor = color
      }
      
      self.updateUI()
      
    }
    alert.addAction(ok)
    
    let cancel = UIAlertAction(title: "취소", style: .cancel, handler: nil)
    alert.addAction(cancel)
    
    present(alert, animated: true, completion: nil)
    
  }
  
  //UI Reset
  func updateUI() {
    //dDayImageView UI
    for (imageView, dday) in zip(dDayImageViews, dDayManager.ddays) {
      let ddayColor = dday.backgroundColor
      imageView.backgroundColor = UIColor(hexString: ddayColor)
    }
    
    //dDayLabel UI
    for (dDaylabel, dday) in zip(dDayLabels, dDayManager.ddays) {
      dDaylabel.text = dday.ddayText
    }
    
    //dDayTargetDates UI
    for (dDayTarget, dday) in zip(targetDates, dDayManager.ddays) {
      dDayTarget.text = dday.targetDateText
    }
    dDayManager.save()
  }
  
  
  //MARK: - IBActions
  @IBAction func changedDatePicker(_ sender: UIDatePicker) {
    for idx in dDayManager.ddays.indices {
      dDayManager.ddays[idx].targetDate = sender.date
    }
    updateUI()
  }
  
}

class DdayViewTapGesture: UITapGestureRecognizer {
  var idx: Int?
}
  • 하위 뷰들을 감싸고 있는 상위 뷰는 subviews로 접근할 수 있는 것을 알았지만 데이터 모델과 함께 활용하기 위해서는 플랫하게 만들고 타입 캐스팅도 해야해서 생각보다 번거로웠다. 개체 많지 않으니 아웃렛 콜렉션으로 만들었다.
  • 디디에 표시 뷰의 각 항목을 탭하면 Dday 기준일과 배경색을 바꿀 수 있다. 커스텀 뷰를 isHidden을 통해 표시할까 했는데 정확히 어떻게 하는 지 몰라 공부가 정확히 되면 하기로 하고 AlertController 텍스트 필드를 활용해보았다. \

Dday Model

import Foundation

struct DDay: Codable {
  var targetDate: Date
  var dday: Int
  var backgroundColor: String
  
  var targetDateText: String {
    let remainDay = Date(timeInterval: Double(dday * 60 * 60 * 24), since: targetDate)
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy년\nMM월dd일"
    return formatter.string(from: remainDay)
  }
  
  var ddayText: String {
    return dday > 0 ? "D+\(dday)" : "D-\(dday)"
  }
}
  • DDay모델은 기준일과 디데이일, 백그라운드 컬러를 속성으로 갖는다.

DDay Manager

import Foundation

class DdayManager {
  var ddays: [DDay] = []
  
  init () {
    load()
    
    if ddays.isEmpty {
      ddays = [
        DDay(targetDate: Date(), dday: 100 , backgroundColor: "#EC007F"),
        DDay(targetDate: Date(), dday: 200, backgroundColor: "#EC007F"),
        DDay(targetDate: Date(), dday: 300, backgroundColor: "#EC007F"),
        DDay(targetDate: Date(), dday: 400, backgroundColor: "#EC007F")
      ]
    }
  }
  
  func save() {
    do {
      let ddaysData = try JSONEncoder().encode(ddays)
      UserDefaults.standard.set(ddaysData, forKey: "\(DdayManager.self)")
    } catch {
      print(error.localizedDescription)
    }
  }
  
  func load() {
    do {
      if let data = UserDefaults.standard.data(forKey: "\(DdayManager.self)") {
        let decoded = try JSONDecoder().decode([DDay].self, from: data)
        ddays = decoded
      }
    } catch {
      print(error.localizedDescription)
    }
  }
  
}
  • 특별한 건 없다. 초기화, 로드, 저장을 관리함

UIColor Hex init

import UIKit

///HEX 값으로 UIColor를 생성하는 생성자 오버로딩
///패턴은 #부터 시작해서 총 7글자여야 한다.
///예 #EC007F (대소문자는 상관하지 않음)
extension UIColor {
  convenience init(hexString: String, alpha: CGFloat = 1.0) {
    var validate = hexString
    if validate.first != "#"  {
      validate = "#000000"
    }
    
    var cutting = validate.map{$0}
    cutting.removeFirst()
    
    let r = String(cutting[0...1])
    let g = String(cutting[2...3])
    let b = String(cutting[4...5])
    
    let redInt = CGFloat(Int(r, radix: 16) ?? 0) / 255.0
    let greenInt = CGFloat(Int(g, radix: 16) ?? 0) / 255.0
    let blueInt = CGFloat(Int(b, radix: 16) ?? 0) / 255.0
    
    self.init(red: redInt, green: greenInt, blue: blueInt, alpha: alpha)
  }
}

Dday 인스턴스를 생성할 때 백그라운드 컬러를 핵사 코드로 생성하고 싶어서 따로 생성자를 만듦

GitHub

https://github.com/urijan44/SeSAC_AnniversaryCounter