본문 바로가기

학습 노트/iOS (2021)

062 ~ 069. Handling Date

Date


날짜를 처리하는 다양한 자료형이 존재한다.

Class Strcture
NSDate
NSCalendar
NSDateComponents
NSTimeZone
NSLocale
Date
Calendat
DateComponents
TimeZone
Locale

예전엔 클래스로만 제공했지만 현행 코코아터치프레임워크는 구조체로도 제공하고 있다.
또한 이러한 자료형들은 클래스 자료형의 이름에서 NS를 제외한 이름을 가지고 있다.

이 둘은 이름과 구현 방식만 다를 뿐, 사용법과 동작 방식은 동일하거나 유사하고,
대부분의 자료형들이 브릿징을 지원하기때문에 타입캐스팅을 활용해 자유롭게 전환할 수 있다.

Date()
결과

"Aug 27, 2021 at 12:47 PM"

Date 생성자를 사용하면 위와 같은 날짜를 얻을 수 있다.

let date = Date()
print(date)
결과

2021-08-27 03:49:01 +0000

이것을 상수에 저장하고 출력하면 위와 같은 결과를 확인 할 수 있다.
시뮬레이터의 기본 언어 설정은 영어이고, 따라서 Date 생성자의 결과또한 시뮬레이터의 설정에 따라 출력된다.

하지만 print로 출력된 결과는 조금 다른데,
지역 설정에 관계 없이 UTC기준시에 따라 출력된다.
출력되는 것 외에도 여러 값들을 표현하고있으며, 이러한 값들은 특정한 기준시나 달력과는 독립적인 값이다.
이때 기준이 되는 날짜를 ReferenceDate라고 한다.

Reference Date

2001-01-01 00:00:00 / UTC

ReferenceDate에 숫자를 더하면 이후의 날짜가 되고, 빼면 이전의 날짜가 된다.

var dt = Date(timeIntervalSinceReferenceDate: 3600)
print(dt)
결과

"Jan 1, 2001 at 10:00 AM"
"2001-01-01 01:00:00 +0000\n"

Date생성자의 timeIntervalSinceReferenceDate속성에 초단위의 숫자를 전달하면
DeferenceDate기준에서 그 만큼 지난 시간을 반환해 준다.

3600초는 1시간이므로 ReferenceDate기준 1시간이 지난 '2001년 1월 1일 1시'가 출력된다.
이전 날짜는 반대로 음수를 전달한다.

TimeInterval

위에서 한 시간을 전달할 때 '1'이 아닌 '3600'을 전달한 이유는 TimeInterval을 사용하기 때문이다.

let onesec = TimeInterval(1)
결과

1

timeInterval의 기본 단위는 '초'이고, 1을 전달하면 1초가 된다.

public typealias TimeInterval = Double

하지만 실제 선언은 Double 형식의 typalias로 선언되어있다.
따라서 초보다 작은 단위를 표현하는 것이 가능하다.

let oneMsec = TimeInterval(0.001)
let oneMin = TimeInterval(60)
let oneHour1 = TimeInterval(oneMin * 60)
let oneHour2 = TimeInterval(60 * 60)
let oneHour3 = TimeInterval(3600)
let oneDay = TimeInterval(oneHour1 * 24)
결과

0.001
60
3600
3600
3600
86400

위처럼 여러 응용을 통해 단위 시간을 표현할 수 있다.

  • Date()
    현재 날짜를 생성할 때 사용한다.
  • Date(timeInterval:since:)
    기준이 되는 날짜를 직접 지정할 때 사용한다.
  • Date(timeIntervalSince1970:)
    '1970년 1월 1일 00시 00분 00초'를 기준으로 새 시간을 생성할 때 사용한다.
    UNIX timestamp로 날짜를 만들어야 하는 경우 사용한다.
    API서버에서 전달한 날짜를 parsing할 때 주로 사용한다.
  • Date(timeIntervalSinceNow:)
    지금 시간을 기준으로 새 시간을 생성할 때 사용한다.
  • Date(timeIntervalSinceReferenceDate:)
    Referencetime 기준으로 새 시간을 생성할 때 사용한다.

지금을 기준으로 얼마간 지난 시간을 표현하기 위해선 Date(timeIntercalSinceNow) 생성자를 사용한다.

let onesec = TimeInterval(1)
let oneMsec = TimeInterval(0.001)
let oneMin = TimeInterval(60)
let oneHour1 = TimeInterval(oneMin * 60)
let oneHour2 = TimeInterval(60 * 60)
let oneHour3 = TimeInterval(3600)
let oneDay = TimeInterval(oneHour1 * 24)

Date(timeIntervalSinceNow: oneDay)
결과

"Aug 28, 2021 at 3:16 PM"

위처럼 원하는 timeInterval을 전달해 주면 현재 시간에서 전달된 시간 만큼 지난 시간을 반환하게 된다.

 

Calendar and Date Components


한국만 해도 양력과 음력 달력 두가지를 사용하고,
전 세계적으로 다양한 달력이 존재한다.

따라서 Date에 저장되는 값으 특정 달력과 연관있는 값이 아닌 독립적인 값이다.
따라서 이를 계산하거나 표시하고 싶다면 특정 달력과 연관지어 표현해야한다.

달려은 Calendar 구조체로 구현되어있고, 'Calendar.identifier' 열거형 안에 선언되어있다.

public enum Identifier {
	
	case gregorian
	
	case buddhist
	
	case chinese
	
	case coptic
	
	case ethiopicAmeteMihret
	
	case ethiopicAmeteAlem
	
	case hebrew
	
	case iso8601
	
	case indian
	
	case islamic
	
	case islamicCivil
	
	case japanese
	
	case persian
	
	case republicOfChina
	
	@available(macOS 10.10, iOS 8.0, *)
	case islamicTabular
	
	@available(macOS 10.10, iOS 8.0, *)
	case islamicUmmAlQura
	
	public static func == (a: Calendar.Identifier, b: Calendar.Identifier) -> Bool
	
	public func hash(into hasher: inout Hasher)
	
	public var hashValue: Int { get }
}

위와 같이 여러 종류의 달력이 선언되어있다.

Calendar.Identifier.gregorian

Calendar.current
Calendar.autoupdatingCurrent

선언되어 있는 달력을 명시적으로 사용해도 되고,
current 속성이나 autoupdatingCurrent 속성을 사용해 사용자의 설정 그대로 사용하는 것도 가능하다.
이름 그대로 autoupdatinfCurrent 속성은 유저가 달력을 바꾸면 자동으로 업데이트 한다는 차이점이 있다.
단, 유저가 달력을 자주 바꾸는 경우가 드믈기 때문에 current 속성이 더 자주 이용된다.

DateComponents

let now = Date()
let calendar = Calendar.current

calendar.dateComponents(Set<Calendar.Component>, from: now)

다시 말하지만 now에 저장된 Date값은 독립적인 값이기 때문에 calendar의 도움을 받아 속성에 접근해야한다.
calendar의 dateComponents 메소드를 사용해 접근할 component와 Date값을 전달한다.

public enum Component {
	
	case era
	
	case year
	
	case month
	
	case day
	
	case hour
	
	case minute
	
	case second
	
	case weekday
	
	case weekdayOrdinal
	
	case quarter
	
	case weekOfMonth
	
	case weekOfYear
	
	case yearForWeekOfYear
	
	case nanosecond
	
	case calendar
	
	case timeZone
	
	public static func == (a: Calendar.Component, b: Calendar.Component) -> Bool
	public func hash(into hasher: inout Hasher)
	public var hashValue: Int { get }
}

dateComponents에는 위와 같이 연, 월, 일을 포함한 많은 컴포넌트가 선언되어있다.

let now = Date()
let calendar = Calendar.current

let components = calendar.dateComponents([.year, .month, .day], from: now)

components.year
components.month
components.day
결과

2021
8
27

여러개의 컴포넌트를 확인한다면 위처럼 dateComponents에 셋의 형태로 확인할 컴포넌트를 전달하면 되지만,

let year = calendar.component(.year, from: now)
year
결과

2021

굳이 그럴 필요가 없다면 componet(from:) 메소드를 사용해 접근한다.

timeInterval은 초단위로 값을 생성하기 때문에 특정 날짜를 만드는 것은 상당히 번거로운 일이다.
따라서 dateComponents를 원하는 값으로 채우고, Calendar가 제공하는 메소드를 사용해 만든다.

let calendar = Calendar.current
var customComponents = DateComponents()
customComponents.year = 2014
customComponents.month = 11
customComponents.day = 30

let custom = calendar.date(from: customComponents)
결과

"Nov 30, 2014 at 12:00 AM"

calendar와 새로 만들 component를 생성하고,
component에는 원하는 날짜를 저장한다.
이후 calendar에 존재하는 date(from:) 메소드를 사용해 날짜를 만들어 주면 끝이다.

Date Calculation

이번엔 calendar를 사용해 날짜계산을 해 본다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

2002년 월드컵 부터의 날짜를 계산하기 위해 월드컵 날짜를 저장한 데이터를 만든다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

let now = Date()

계산할 날짜는 오늘까지 이므로, Date 생성자를 사용해 현재의 날짜를 만든다.
이렇게 생성한 날짜는 시간을 포함하고 있는데, 시간을 포함한 경우와 포함하지 않는 경우의 계산 결과가 달라지게 된다.
따라서 시간을 포함할 것인지, 제외할 것인지를 결정해야한다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!
let now = Date()
let today = calendar.startOfDay(for: now)
결과

"Aug 27, 2021 at 4:35 PM"
"Aug 27, 2021 at 12:00 AM"

만약 오늘을 기준으로 시간을 제외한다면 calendar의 startOfDay 메소드를 사용해 오늘의 시작 시간을 받아와야한다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

let now = Date()
let today = calendar.startOfDay(for: now)

var customD = DateComponents()
customD.day = 100

다시 새로운 components를 만들고 해당 컴포넌트에 계산할 날짜를 저장한다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

let now = Date()
let today = calendar.startOfDay(for: now)

var customD = DateComponents()
customD.day = 100

calendar.date(byAdding: customD, to: now)
calendar.date(byAdding: customD, to: today)
결과

"Dec 5, 2021 at 6:07 PM"
"Dec 5, 2021 at 12:00 AM"

이후엔 calendar의 date(byAdding:to:) 메소드를 사용해 날짜를 계산한다.
또한, 앞서 말한 것과 같이 두 계산의 결과가 다른 것을 확인할 수 있다.
이 차이는 day가 아닌 다른 컴포넌트를 포함할 시에 더 명확히 확인할 수 있다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

let now = Date()
let today = calendar.startOfDay(for: now)

var customD = DateComponents()
customD.day = 100
customD.hour = 13

calendar.date(byAdding: customD, to: now)
calendar.date(byAdding: customD, to: today)
결과

"Dec 6, 2021 at 7:12 AM"
"Dec 5, 2021 at 1:00 PM"

 

위와 같이 영 다른 결과가 나타나게 된다.
이는 기준시에 따른 차이이므로 주의해야 할 필요가 있다.

extension Date {
	init?(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0, calendar: Calendar = .current) {
		var components = DateComponents()
		components.year = year
		components.month = month
		components.day = day
		components.hour = hour
		components.minute = minute
		components.day = day
		
		guard let date = calendar.date(from: components) else {
			return nil
		}
		
		self = date
	}
}

let calendar = Calendar.current
let worldCup2002 = Date(year: 2002, month: 5, day: 31)!

let today = calendar.startOfDay(for: now)

var customD = DateComponents()

customD = calendar.dateComponents([.day], from: worldCup2002, to: today)
cusromD.day
결과

7028

월드컵 개최일은 시간에 해당하는 정보가 없으므로, 계산을 할 날짜도 시간아 없는 startOfDay메소드를 적용한 날짜를 사용한다.
두 날짜를 비교해 결과를 확인하기 위해서는 calendar의 dateComponents(from:to:)메소드를 사용하는데,
확인할 컴포넌트는 위와같이 셋으로 전달한다.

TimeZone

Date 형식은 기본 상태에서 UTC 기준시에 따라 처리된다.
한국 기준시는 Korean Standard Time으로 KST라고 부른다.
이 둘은 9시간의 차이가 발생한다.
따라서 이를 그대로 사용하는 것은 불편함을 초례할 수 있다.

print(TimeZone.knownTimeZoneIdentifiers.count)
결과

439

iOS에서 지원하는 시간대는 총 439개로,
한국 기준시의 이름은 Asia/Seoul 이다.
또한, 이를 별도로 설정하지 않는다면 iOS의 기본시를 사용한다.

let calendar = Calendar.current
var components = DateComponents()
components.year = 2014
components.month = 4
components.day = 16

이번엔 위처럼 이미 만들어진 Date를 한국 기준시로 바꿔보고 비교해 보도록 한다.

let calendar = Calendar.current
var components = DateComponents()
components.year = 2014
components.month = 4
components.day = 16
components.timeZone = TimeZone(identifier: "Asia/Seoul")
calendar.date(from: components)
components.timeZone = TimeZone(identifier: "America/Los_Angeles")
calendar.date(from: components)
결과

"Apr 16, 2014 at 12:00 AM"
"Apr 16, 2014 at 4:00 PM"

DateComponents의 timeZone 속성에 시간대를 할당하면 해당 시간대가 적용된 시간을 얻을 수 있다.

 

Date Picker


iOS 내에서 날짜를 선택할 때는 DatePicker를 사용한다.
비슷한 이름의 PickerView와 달리 데이터를 입력하는 코드를 직접 작성할 필요는 없다.
대신 DatePickerMode 라는 속성으로 표시할 데이터를 선택하고, 선택 이벤트는 TargetAction 방식으로 구현한다.

//
//  DatePickerViewController.swift
//  HandlingDate
//
//  Created by Martin.Q on 2021/08/27.
//

import UIKit

class DatePickerViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
	}
	
}

사용할 씬과 코드는 위와 같다.

라이브러리에서 Date Picker를 선택에 씬에 추가한다.

DatePicker는 사이즈를 조절할 수는 있지만 보통 너비는 화면에 꽉 채우고,
높이는 기본 높이를 사용한다.

DatePicker의 attribute inspector는 위와 같다.

가장 첫번째인 Preffered Style은 iOS 13.4에서 새롭게 추가됐고,
Automatic으로 설정하는 경우 Mode 속성에 따라 자동으로 레이아웃을 표시한다.

iOS 13.3 까지는 씬에 표시되는 것과 같이 Wheel 방식만 제공하지만,

iOS14 시뮬레이터에서는 다른 레이아웃을 보여주는 것을 확인 할 수 있다.
해당 레이아웃은 Compact Style이다.
단, Compact Style은 앱의 너비를 전부 채우지 못하기 때문에 제약오류가 발생할 수도 있어 주의해야 한다.

Inline Style은 달력과 함께 시간을 선택할 수 있는 레이아웃을 표시한다.

Deployment target이 iOS 13.4 버전 이상이라면 원하는 레이아웃을 선택해 사용해도 괜찮지만,
그 이하의 버전까지 지원해야 한다면 Wheel 스타일을 선택한다.

//
//  DatePickerViewController.swift
//  HandlingDate
//
//  Created by Martin.Q on 2021/08/27.
//

import UIKit

class DatePickerViewController: UIViewController {
    @IBOutlet weak var datePicker: UIDatePicker!
    

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

코드로 스타일을 지정하기 위해서는 코드에 outlet으로 연결해야한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	datePicker.preferredDatePickerStyle = .wheels
}

datePicker의 스타일을 설정하기 위해선 preferredDatePickerStyle 속성에 접근해야한다.
하지만 해당 속성 자체가 iOS 13.4 이상에서나 지원하는 속성이기 때문에 버전에 따라 분기해 줄 필요가 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
}

이렇게 수정하면 컴파일 에러는 사라진다.

Mode 속성은 DatePicker가 동작할 방식을 결정한다.


결과


각각의 Mode에 따른 레이아웃 변화는 위와 같다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
	
	datePicker.datePickerMode = .dateAndTime
}

코드로 설정할 때는 datePickerMode 속성으로 설정한다.

Locale 설정이 Default로 되어있으면 디바이스의 언어 설정을 따른다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
	
	datePicker.datePickerMode = .dateAndTime
	datePicker.locale = Locale(identifier: "ko_kr")
}

코드로 설정할 때는 locale 속성으로 설정한다.
전달하는 값은 Locale(identifier:) 생성자로 ko_kr과 같이 해당하는 locale을 생성해 전달한다.


결과


Interval 속성은 항목간의 간격을 설정한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
	
	datePicker.datePickerMode = .dateAndTime
	datePicker.locale = Locale(identifier: "ko_kr")
    datePicker.minuteInterval = 3
}

코드로는 minuteInterval 속성으로 설정하며, 1 ~ 30 사이의 60의 약수인 정수형태로 설정할 수 있다.
만약 60의 약수가 아니라면 1분으로 대체되어 사용된다.


결과


Date는 처음에 표시할 날짜와 시간을 선택한다.
기본값인 Current Date는 현재의 날짜와 시간을 사용하고,
Custom을 사용하면 별도의 날짜와 시간으로 설정할 수 있다.

함께 있는 Minimum Date와 Maximum Date는 선택 범위를 설정한다.
이 때 Minimum Date는 반드시 Maximum Date보다 작아야한다.
Date와 관련된 속성들은 timer mode일 때는 사용되지 않는다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
	
	datePicker.datePickerMode = .dateAndTime
	datePicker.locale = Locale(identifier: "ko_kr")
	datePicker.minuteInterval = 3
	datePicker.date = Date()
	
}

datePicker의 date 속성은 Date 형식의 데이터를 전달해 설정한다.

datePicker.setDate(Date(), animated: true)

애니메이션을 적용하고 싶다면 위와 같은 형태로 설정한다.

datePicker.minimumDate = Date()
datePicker.maximumDate = Date()

범위는 minimumDate와 maximumDate 속성으로 설정할 수 있다.

이 외에도 DatePicker가 사용할 시간대와 캘린더를 설정할 수 있는데,
이는 코드로만 설정할 수 있다.

datePicker.calendar = Calendar.current
datePicker.timeZone = TimeZone.current

이 둘은 DatePicker 내의 같은 이름의 속성으로 설정한다.

DatePicker에서 원하는 항목을 선택하면 ValueChanged 이벤트가 발생하고,
이 이벤트를 처리하고자 한다면 Action을 연결해야 한다.

@IBAction func datePickerAction(_ sender: UIDatePicker) {
	print(sender.date)
	
}

Action으로 연결하고 sender는 UIDatePicker를 선택한다.
UIDatePicker가 반환하는 날짜를 받기 위해 sender의 date속성에 접근해 이를 출력하도록 했다.


결과


값을 잘 받아와 출력하는 것을 볼 수 있다.
DatePicker가 ValueChanged 이벤트를 호출하는 것은, 움직임을 멈추고 하나의 항목을 선택했을 시점이다.
또한 출력되는 데이터는 사용자에게 불친절한 데이터이기 때문에 DateFormatter를 사용해 친숙하게 바꿔 사용하게 된다.

 

Countdown Timer


//
//  CountDownTimerViewController.swift
//  HandlingDate
//
//  Created by Martin.Q on 2021/08/27.
//

import UIKit

class CountDownTimerViewController: UIViewController {
	@IBOutlet weak var label: UILabel!
	@IBOutlet weak var datePicker: UIDatePicker!
	@IBAction func datePickerAction(_ sender: UIDatePicker) {
	}
	@IBAction func StartAction(_ sender: Any) {
	}
	
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
		if #available(iOS 13.4, *) {
			datePicker.preferredDatePickerStyle = .wheels
		} else {
			// Fallback on earlier versions
		}
		datePicker.datePickerMode = .countDownTimer
	}
}

사용할 씬과 연결된 코드는 위와 같다.
datePicker의 스타일은 wheel로 고정하고, Mode는 countDownTimer로 설정했다.


결과


그리고 시뮬레이터를 확인해 보면 초기값이 0분으로 되어있고, 0분으로 설정하려 해도 선택되지 않는다.
countDownTimer의 최솟값은 1분이고, 사용할 수 있는 최대값은 23시간 59분이다.

override func viewDidLoad() {
	super.viewDidLoad()
	if #available(iOS 13.4, *) {
		datePicker.preferredDatePickerStyle = .wheels
	} else {
		// Fallback on earlier versions
	}
	datePicker.datePickerMode = .countDownTimer
	datePicker.countDownDuration = 300
}

초기값을 변경하고 싶다면 countDownDuration 속성에 초단위로 전달한다.


결과


다시말해 위에서 코드로 설정한 300은 5분이 된다.

@IBAction func StartAction(_ sender: Any) {
	label.text = "\(Int(datePicker.countDownDuration))"
}

이제는 DatePicker에서 선택한 값을 label에 표시한다.
countDownTimer는 이름과는 다르게 카운트다운을 직접 처리하지는 못한다.
따라서 기능 자체는 timer를 사용해 직접 구현해야한다.

@IBAction func StartAction(_ sender: Any) {
	label.text = "\(Int(datePicker.countDownDuration))"
	
	remain = Int(datePicker.countDownDuration)
}

var remain = 0

남은 시간을 저장할 변수를 만든 뒤, 시작 버튼을 누르면 countDownPicker의 값을 해당 변수에 저장한다.

@IBAction func StartAction(_ sender: Any) {
	label.text = "\(Int(datePicker.countDownDuration))"
	
	remain = Int(datePicker.countDownDuration)
	
	Timer.scheduledTimer(withTimeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void)
}

또한 타이머를 구현하기 위해 Timer 클래스의 scheduledTimer(withTimeInterval:repats:block:)메소드를 사용한다.

  • withTimerInterval
    반복 주기를 설정한다.
  • repeats
    반복 여부를 결정한다.
  • blcok
    실행할 코드를 전달한다.
@IBAction func StartAction(_ sender: Any) {
	label.text = "\(Int(datePicker.countDownDuration))"
	
	remain = Int(datePicker.countDownDuration)
	
	Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
		self.remain -= 1
		self.label.text = "\(self.remain)"
	}
}

따라서 1초마다 반복하며 남은 시간을 1씩 줄이도록 구현했다.

@IBAction func StartAction(_ sender: Any) {
	label.text = "\(Int(datePicker.countDownDuration))"
	
	remain = Int(datePicker.countDownDuration)
	
	Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
		self.remain -= 1
		self.label.text = "\(self.remain)"

		if self.remain == 0 {
			timer.invalidate()
			AudioServicesPlaySystemSound(1315)
		}
	}
}

그렇게 반복하다가 남은 시간이 0이 되면,
invalidate() 메소드로 타이머를 종료하고,
AudioServicesPlaySystemSound 메소드를 통해 알림음을 재생한다.
알림음 재생을 위해서는 

import AudioToolbox

AudioToolbox를 반드시 import 해 줘야한다.


결과


Date Formatter


let now = Date()
print(now)
결과

2021-08-29 09:31:12 +0000

Date형식의 데이터를 그대로 출력하게 되면 위와 같은 형태로 출력된다.
이는 충분한 양의 데이터를 가지고 있지만, 실제 사용자에게 제공할 만한 형태는 아니다.
따라서 제공할 만한 형식의 문자열로 바꾸어야 하는데, 이때 사용하는 것이 DateFormatter이다.

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .medium

DateFormatter는 새로운 인스턴스를 만든 후 이미 선언되어있는 형식을 사용하거나,
원하는 형식을 직접 작성하여 적용할 수 있다.

이미 선언되어있는 형식을 사용하는 경우 날짜와 시간의 형식을 별도로 설정해 줘야한다.
full, long, medium, short, none의 다섯가지 선택지를 제공한다.
none을 사용할 경우 해당 부분은 문자열에 포함되지 않는다.
dateStyle과 timeStyle 모두 기본값이 none이기 때문에 둘 중 하나는 반드시 none이 아닌 값으로 설정해야한다.

var result = formatter.string(from: now)
print(result)
결과

Sunday, August 29, 2021 at 6:38:45 PM

이렇게 설정한 formatter의 string 메소드를 사용해 해당 형식의 문자열로 변환해 출력하면,
사용자에게 제공하기 적절한 형태의 데이터가 출력된다.

let now = Date()

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .medium
formatter.locale = Locale(identifier: "ko_kr")

var result = formatter.string(from: now)
print(result)
결과

2021년 8월 29일 일요일 오후 6:42:02

formatter의 언어설정을 해주지 않으면 기본 설정인 영문으로 변환된다.
적절한 언어를 locale 속성에 전달해 해당 언어로 변환하도록 할 수 있다.

formatter.string(for: now)

만약 formatter가 전달받는 파라미터가 optional이라면 string(from:)이 아닌 string(for:)메소드를 사용해야한다.

DateFormatter.localizedString(from: now, dateStyle: .full, timeStyle: .medium)

만약 DateFormatter를 반복적으로 사용할 필요가 없는 경우,
인스턴스를 생성하지 않고, 위와 같이 Class 함수를 사용할 수 있다.

Custom Format

직접 형식을 지정할 때는 UnicodeDateFormatPattern에 선언되어있는 문자를 사용해야한다.

Era
== en_US ==============
G          AD
GG         AD
GGG        AD
GGGG       Anno Domini
GGGGG      A
== ko_KR ==============
G          AD
GG         AD
GGG        AD
GGGG       서기
GGGGG      AD


Year
== en_US ==============
y          2021
yy         21
yyy        2021
yyyy       2021
yyyyy      02021
Y          2021
YY         21
YYY        2021
YYYY       2021
YYYYY      02021
u          2021
uu         21
uuu        2021
uuuu       2021
uuuuu      02021
U          2021
UU         21
UUU        2021
UUUU       2021
UUUUU      02021
== ko_KR ==============
y          2021년
yy         21년
yyy        2021년
yyyy       2021년
yyyyy      02021년
Y          2021년
YY         21년
YYY        2021년
YYYY       2021년
YYYYY      02021년
u          2021년
uu         21년
uuu        2021년
uuuu       2021년
uuuuu      02021년
U          2021년
UU         21년
UUU        2021년
UUUU       2021년
UUUUU      02021년


Quarter
== en_US ==============
q          3
qq         03
qqq        Q3
qqqq       3rd quarter
Q          3
QQ         03
QQQ        Q3
QQQQ       3rd quarter
== ko_KR ==============
q          3
qq         03
qqq        3분기
qqqq       제 3/4분기
Q          3
QQ         03
QQQ        3분기
QQQQ       제 3/4분기


Month
== en_US ==============
M          8
MM         08
MMM        Aug
MMMM       August
MMMMM      A
L          8
LL         08
LLL        Aug
LLLLL      A
LLLLL      A
== ko_KR ==============
M          8월
MM         08월
MMM        8월
MMMM       8월
MMMMM      8월
L          8월
LL         08월
LLL        8월
LLLLL      8월
LLLLL      8월


Day
== en_US ==============
d          29
dd         29
D          241
DD         241
DDD        241
F          5
== ko_KR ==============
d          29일
dd         29일
D          241
DD         241
DDD        241
F          5


Week Day
== en_US ==============
E          Sun
EE         Sun
EEE        Sun
EEEE       Sunday
EEEEE      S
e          Sun
ee         1
eee        Sun
eeee       Sunday
eeeee      S
c          Sun
cc         1
ccc        Sun
cccc       Sunday
ccccc      S
== ko_KR ==============
E          일
EE         일
EEE        일
EEEE       일요일
EEEEE      일
e          일
ee         1
eee        일
eeee       일요일
eeeee      일
c          일
cc         1
ccc        일
cccc       일요일
ccccc      일


Week
== en_US ==============
w          36
ww         36
W          5
== ko_KR ==============
w          36
ww         36
W          5


Period
== en_US ==============
a          PM
== ko_KR ==============
a          오후


Hour
== en_US ==============
h          6 PM
hh         6 PM
H          18
HH         18
== ko_KR ==============
h          오후 6시
hh         오후 6시
H          18시
HH         18시


Minute
== en_US ==============
m          56
mm         56
== ko_KR ==============
m          56
mm         56


Second
== en_US ==============
s          19
ss         19
S          4
SS         44
SSS        446
SSSS       4460
SSSSS      44600
A          68239446
AA         68239446
AAA        68239446
AAAA       68239446
AAAAA      68239446
== ko_KR ==============
s          19
ss         19
S          4
SS         44
SSS        446
SSSS       4460
SSSSS      44600
A          68239446
AA         68239446
AAA        68239446
AAAA       68239446
AAAAA      68239446


Zone
== en_US ==============
z          GMT+9
zz         GMT+9
zzz        GMT+9
zzzz       Korean Standard Time
Z          +0900
ZZ         +0900
ZZZ        +0900
ZZZZ       GMT+09:00
ZZZZZ      +09:00
v          South Korea Time
vvvv       Korean Standard Time
V          krsel
VVVV       South Korea Time
== ko_KR ==============
z          GMT+9
zz         GMT+9
zzz        GMT+9
zzzz       대한민국 표준시
Z          +0900
ZZ         +0900
ZZZ        +0900
ZZZZ       GMT+09:00
ZZZZZ      +09:00
v          대한민국 시간
vvvv       대한민국 표준시
V          krsel
VVVV       대한민국 시간

문자의 종류와 결과는 위와 같다.

formatter.setLocalizedDateFormatFromTemplate("yyyyMMMMdE")

DateFormatter에 지역화되는 Date 양식을 만들기 위해서는 setLocalizedDateFormatFromTemplate 메소드를 사용한다.

let now = Date()
let formatter = DateFormatter()

formatter.setLocalizedDateFormatFromTemplate("yyyyMMMMdE")

formatter.locale = Locale(identifier: "ko-kr")
var result = formatter.string(from: now)
print(result)

formatter.locale = Locale(identifier: "en-us")
result = formatter.string(from: now)
print(result)
결과

일, 8월 29, 2021
Sun, August 29, 2021

변화를 보기 위해 한국과 미국으로 각각 변환하고, 이를 출력하면
언어만 다르게 나올 뿐, 출력 형식 자체는 동일하다.
이는 Date 형식이 Locale에 맞게 업데이트 되지 않았기 때문으로,
사용한 메소드는 단순히 전달 된 문자열을 현재 Locale에 적합한 포맷 문자열로 바꾸어 인스턴스에 저장한다.
따라서 Locale을 바꿨다고 저장되어있는 포맷문자열이 업데이트 되지는 않는다.
따라서 Locale을 바꾼 후 해당 메소드를 다시 호출해야할 필요가 있다.

let now = Date()
let formatter = DateFormatter()

formatter.locale = Locale(identifier: "ko-kr")
formatter.setLocalizedDateFormatFromTemplate("yyyyMMMMdE")
var result = formatter.string(from: now)
print(result)

formatter.locale = Locale(identifier: "en-us")
formatter.setLocalizedDateFormatFromTemplate("yyyyMMMMdE")
result = formatter.string(from: now)
print(result)
결과

2021년 8월 29일 (일)
Sun, August 29, 2021

이제는 Locale 설정에 맞는 형식으로 변환됐다.

print(formatter.dateFormat)
결과

Optional("EEE, MMMM d, yyyy")

실제 사용하는 문자열은 DateFormat 속성을 통해 확인할 수 있다.

formatter.dateFormat = "yyyyMMMMde"
print(formatter.string(from: now))
결과

2021August291

또한 해당 속성에 원하는 문자열을 설정할 수도 있다.
이는 Locale에 따르지 않고 고정된 형식으로 사용해야 하는 겨우 사용된다.

Relative Date Formatting

날짜는 기본적으로 년, 월, 일로 출력되는데, 현재를 기준으로 상대적인 표현으로 출력하는 방법또한 존재한다.

let now = Date()
let yesterday = now.addingTimeInterval(3600 * -24)
let tomorrow = now.addingTimeInterval(3600 * 24)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateStyle = .full
formatter.timeStyle = .none

print(formatter.string(from: now))
print(formatter.string(from: yesterday))
print(formatter.string(from: tomorrow))
결과

2021년 8월 29일 일요일
2021년 8월 28일 토요일
2021년 8월 30일 월요일

위와 같이 일반적인 형태로 출력되던 날짜들이

let now = Date()
let yesterday = now.addingTimeInterval(3600 * -24)
let tomorrow = now.addingTimeInterval(3600 * 24)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateStyle = .full
formatter.timeStyle = .none

formatter.doesRelativeDateFormatting = true

print(formatter.string(from: now))
print(formatter.string(from: yesterday))
print(formatter.string(from: tomorrow))
결과

오늘
어제
내일

doesRelativeDateFormatting 속성을 참으로 설정하면
위와 같이 상대적인 형태로 날짜를 표시한다.

해당 속성은 48시간 이내의 날짜를 그저께, 어제, 오늘, 내일, 모레로 변환해 표현한다.

Symbols

DateFormatter는 변환 과정에서 지정된 심볼을 사용한다.
기본 심볼을 주로 사용하지만 직접 지정하는 것이 가능하다.

let now = Date()
let weekdaySymbols = ["☀️", "🌕", "🔥", "💧", "🌲", "🥇", "🌏"]
let am = "🌅"
let pm = "🌇"

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .full

print(formatter.string(from: now))

formatter.amSymbol = am
formatter.pmSymbol = pm

print(formatter.string(from: now))
결과

Sunday, August 29, 2021 at 7:23:41 PM Korean Standard Time
Sunday, August 29, 2021 at 7:23:41 🌇 Korean Standard Time

사용할 이미지를 불러와 해당 심볼로 지정해주면 된다.

DateFormatter에서는 위와 같이 많은 항목에 심볼을 사용할 수 있도록 제공하고있다.

let now = Date()
let weekdaySymbols = ["☀️", "🌕", "🔥", "💧", "🌲", "🥇", "🌏"]
let am = "🌅"
let pm = "🌇"

let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .full

print(formatter.string(from: now))

formatter.amSymbol = am
formatter.pmSymbol = pm

print(formatter.string(from: now))

formatter.weekdaySymbols = weekdaySymbols
print(formatter.string(from: now))
결과

Sunday, August 29, 2021 at 7:23:41 PM Korean Standard Time
Sunday, August 29, 2021 at 7:23:41 🌇 Korean Standard Time
☀️, August 29, 2021 at 7:23:41 🌇 Korean Standard Time

여러 속성들도 동일하게 작동한다.

Date String Parsing

문자열로 저장되어있는 날짜를 Date 형식으로 parsing해 본다.
이 때는 DateFormatter의 dateFormat 형식으로 날짜의 형식을 정확히 지정해야한다.

let str = "2017-09-02T09:30:00Z"
let formatter = DateFormatter()

formatter.dateFormat = "yyyy-MM-ddTHH:mm:ssZ"

if let date = formatter.date(from: str) {
	formatter.dateStyle = .full
	formatter.timeStyle = .full
	print(formatter.string(from: date))
} else {
	print("Invalid")
}

 

결과

Invalid

date(from:)메소드를 통해 문자열을 date형식으로 바꾸는 경우 optional에 대비해 value binding 형식으로 구현한다.
DateFormatter에 문자열의 date 형식을 정확히 전달했지만 조건문에 의해 Invalid를 출력하고 있다.

문자열 str에 저장된 날짜는 ISO8601 표준 문자열에 해당한다.
해당 문자열엔 T와 Z같은 구분 문자가 포함되는데, 해당 구분문자가 dateFormat에 구분 없이 저장되었다.
이러한 구분문자는 작은따옴표로 감싸 표현해야한다.

let str = "2017-09-02T09:30:00Z"
let formatter = DateFormatter()

formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"

if let date = formatter.date(from: str) {
	formatter.dateStyle = .full
	formatter.timeStyle = .full
	print(formatter.string(from: date))
} else {
	print("Invalid")
}
결과

Saturday, September 2, 2017 at 9:30:00 AM Korean Standard Time

따라서 형식을 위와 같이 수정해 주면 정상적으로 변환된다.

이렇게 Date 형식을 정확히 하고, 오탈자가 없도록 주의해서 작성해야한다.

 

ISO8601 DateFormatter


ISO8601 DateFormatter는 iOS에서 날씨를 parsing할 때 주로 사용한다.
날짜를 문자열로 바꿀 때는 string(from:)메소드를, 문자열을 날짜로 바꿀 때는 date(from:)메소드를 사용한다.

let str = "2017-09-02T09:30:00Z"
let formatter = ISO8601DateFormatter()

if let date = formatter.date(from: str) {
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

2017-09-02T09:30:00Z

이전에 일반 dateFormatter를 사용했던 것과 달리,
ISO8601형식의 문자열을 받았음에도 정상적으로 데이터를 parsing하고있다.

let str = "2017-09-02T"
let formatter = ISO8601DateFormatter()

if let date = formatter.date(from: str) {
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

invalid

하지만 문자열에서 시간 부분을 지우면 정상적으로 parsing하지 못한다.
이렇게 같은 ISO8601 형식의 문자열이라도 정보가 누락되어있으면 별도로 형식을 지정해 줄 필요가있다.

let str = "2017-09-02T"

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withYear, .withMonth, .withDay]

if let date = formatter.date(from: str) {
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

00020107

포멧옵션은 formatOptions 속성에서 지정할 수 있다.
formatOptions 속성은 ISO8601 dateFormatter.Options 구조체이며, optionset 프로토콜을 채용한다.
이 안에는 with로 시작하는 옵션들이 선언되어있고, 원하는 옵션을 선택하면 된다.

위처럼 연, 월, 일을 fotmatOptions에서 지정해 주면 parsing은 되지만 의미를 알 수 없는 결과가 출력된다.
이렇게 된 이유는 str저장된 문자열에 포함된 '-'을 인식할 수 없고, 따라서 가장 앞의 2017만 parsing되고 나머지는 누락된다.

let str = "2017-09-02T"

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withYear, .withMonth, .withDay, .withDashSeparatorInDate]

if let date = formatter.date(from: str) {
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

2017-09-02

추가적으로 withDashSeparatorInDate 옵션을 추가해 주면, '-'을 인식하게 되고, 정상적으로 parsing한다.

let str = "2017-09-02T"

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]

if let date = formatter.date(from: str) {
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

2017-09-02

위에서 적용한 네가지 옵션은 withFullDate로 치환할 수도 있다.

let str = "2017-09-02T"
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]

if let date = formatter.date(from: str) {
	formatter.formatOptions = [.withInternetDateTime]
	print(formatter.string(from: date))
} else {
	print("invalid")
}
결과

2017-09-02T00:00:00Z

또한 웹서버와 통신할 때에는 withInternetDateTime 옵션을 사용해
ISO8601과 RFC3339 표준을 사용하는 웹서버와 오류없이 날짜를 주고받을 수 있다.
출력하기 직전 해당 옵션을 formatter에 적용하게되면 자동으로 시간을 추가하게 된다.

 

DateIntervalFormatter


let startDate = Date()
let endDate = startDate.addingTimeInterval(3600 * 24 * 30)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateStyle = .long
formatter.timeStyle = .short

print("\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))")
결과

2021년 8월 29일 오후 10:19 - 2021년 9월 28일 오후 10:19

위의 코드는 날짜의 범위를 출력하는 코드이다.
지금도 잘 작동하고, 결과에도 문제가 없지만 DateIntervalFormatter를 사용한다면 조금 더 간단하게 구현할 수 있다.

let startDate = Date()
let endDate = startDate.addingTimeInterval(3600 * 24 * 30)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateStyle = .long
formatter.timeStyle = .short

print("\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))")

let intervalformatter = DateIntervalFormatter()
intervalformatter.locale = Locale(identifier: "ko_KR")
intervalformatter.dateStyle = .long
intervalformatter.timeStyle = .short

print(intervalformatter.string(from: startDate, to: endDate))
결과

2021년 8월 29일 오후 10:19 - 2021년 9월 28일 오후 10:19
2021. 8. 29. 오후 10:19 ~ 2021. 9. 28. 오후 10:19

dateIntervalFormatter를 사용하고,
string(from:to:)메소드를 사용해 date를 문자열로 바꾸어 출력했다.
출력부가 상당히 단순화 됐다.

출력된 날짜에서 '~'은 Locale 설정에 따라 가장 적합한 문자로 사용된다.
동일한 옵션을 적용했음에도 결과는 사뭇 다른 것이 확인된다.

이번엔 포맷 문자열을 전달해 보도록한다.

let startDate = Date()
let endDate = startDate.addingTimeInterval(3600 * 24 * 30)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateStyle = .long
formatter.timeStyle = .short

print("\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))")

let intervalformatter = DateIntervalFormatter()
intervalformatter.locale = Locale(identifier: "ko_KR")
//intervalformatter.dateStyle = .long
//intervalformatter.timeStyle = .short

intervalformatter.dateTemplate = "yyyyMMMdE"

print(intervalformatter.string(from: startDate, to: endDate))
결과

2021년 8월 29일 오후 10:27 - 2021년 9월 28일 오후 10:27
2021년 8월 29일 (일) ~ 9월 28일 (화)

지정한 포맷문자열 또한 Locale 설정에 맞게 자동으로 적용된다.
또한 이렇게 사용했을 경우 중복되는 날짜는 자동으로 생략된다.

 

DateComponentsFormatter


DateComponentsFormatter는 날짜와 시간 사이의 간격을 문자열로 바꿀 때 사용한다.
사용법은 다른 Formatter와 크게 다르지 않다.

let startDate = Date()
let endDate = startDate.addingTimeInterval(3600 * 24 * 30)

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full

if let date = formatter.string(from: startDate, to: endDate) {
	print(date)
}
결과

4 weeks, 2 days

DateComponentsFormatter는 옵션을 선택할 때 unitStyle속성을 사용한다.
해당 속성은 열거형이다.

Full로 설정하고 결과를 확인해보면 두 날짜 사이의 기간을 계산해 출력하고있다.

var component = DateComponents()
component.hour = 1
component.minute = 30

if let result = formatter.string(from: component) {
	print(result)
}
결과

1 hour, 30 minutes

컴포넌트를 생성해 전달한 시간도 자동으로 변환해 출력한다.

이번엔 '분'만 표현하도록 수정해 본다.

var component = DateComponents()
component.hour = 1
component.minute = 30

formatter.allowedUnits = [.minute]

if let result = formatter.string(from: component) {
	print(result)
}
결과

90 minutes

allowedUnits 속성에 minute를 지정한 후 출력하면 분단위로 변환된 시간이 출력된다.

var component = DateComponents()
component.hour = 1
component.minute = 30

formatter.maximumUnitCount = 1

if let result = formatter.string(from: component) {
	print(result)
}
결과

2 hours

maximumUnitCount 속성은 사용할 수 있는 유닛의 수를 제한한다.
위와 같이 1을 사용할 경우 가장 작은 단위의 유닛들부터 삭제되고, 이 때 반올림 한다.

var component = DateComponents()
component.hour = 1
component.minute = 30

formatter.includesTimeRemainingPhrase = true

if let result = formatter.string(from: component) {
	print(result)
}
결과

1 hour, 30 minutes remaining

includesTimeRemainingPhrase속성은 남은 시간의 형태로 표현한다.

var component = DateComponents()
component.hour = 1
component.minute = 30

formatter.includesTimeRemainingPhrase = true
formatter.includesApproximationPhrase = true

if let result = formatter.string(from: component) {
	print(result)
}

 

결과

About 1 hour, 30 minutes remaining

includesApproximationPhrase 속성을 true로 설정하면 시간 앞에 'About'이 추가된다.
이렇게 두개의 속성을 활용하면 네비게이션 등에서 도착 예정 시간을 쉽게 구성할 수 있다.
단, Locale 설정에 영향받지 않고 항상 영어로만 표시된다는 단점이 존재한다.

var component = DateComponents()
component.day = 0
component.hour = 1
component.minute = 0
component.second = 7

formatter.unitsStyle = .positional

if let result = formatter.string(from: component) {
	print(result)
}
결과

1:00:07

이번엔 component의 속성을 추가하고, formatter의 unitStyle 속성을 positional로 변경했다.
출력 결과를 확인하면 값이 0인 day는 출력되지 않았다. 반면 같은 0인 minute은 출력되었다.
0을 취급하는 방식은 zeroFormattingBehavior 속성으로 설정한다.
별도로 설정하지 않은 기본값인 상태에선 가장 앞의 0은 삭제하고 앞뒤로 0이 아닌 값이 존재하는 경우엔 유지한다.

var component = DateComponents()
component.day = 0
component.hour = 1
component.minute = 0
component.second = 7

formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad

if let result = formatter.string(from: component) {
	print(result)
}
결과

0d 01:00:07

해당 속성을 pad로 변경하자 0으로 설정된 day도 삭제되지 않고 출력된다.

var component = DateComponents()
component.day = 0
component.hour = 1
component.minute = 0
component.second = 7

formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .dropAll

if let result = formatter.string(from: component) {
	print(result)
}
결과

1h 7s

dropAll로 설정하자 0으로 설정된 모든 값이 삭제되고, 이에 따라 각각의 컴포넌트를 구별해 표시하게된다.