본문 바로가기

학습 노트/Swift (2021)

135 ~ 139. Extension (익스텐션)

Extension (익스텐션)

익스텐션으로 확장할 수 있는 것은 클래스, 구조체, 열거형, 프로토콜이 있다.
익스텐션은 형식에 새로운 멤버를 추가하는 것은 아니다. 새로운 멤버는 별도의 코드로 구현하고,
형식과 연결해서 기존의 멤버들과 함께 사용할 수 있다.
따라서 따라서 형식 선언을 수정할 수 없는 경우에도 사용할 수 있다.
기본자료형들도 내부적으로 구조체로 구현되어 있기 때문에 기본자료형들을 수정할 수 없어도 익스텐션을 통해 멤버를 확장할 수 있다.

익스텐션으로 멤버를 추가하는 것은 가능하지만 기존의 멤버를 오버 라이딩하는 것은 불가능하다.
만약 오버라이딩이 필요하다면 상속을 통해 서브클래스화 해야 한다.

Syntax

extension Type {
    computedProperty
    computedTypeProperty
    instanceMethod
    typeMethod
    initializer
    subscript
    NestedType
}

extension TypeProtocol, ... {
    requirements
}

익스텐션은 새로운 형식을 생성하는 것이 아닌 기존의 형식을 확장하는 개념이기 때문에
extension 키워드 다음에 대상 형식의 이름을 작성한다.
내용에는 형식에 추가할 멤버들을 작성한다.
이때 작성할 수 있는 멤버들의 속성은 다음으로 제한된다.

  • computed property (계산 속성)
  • 인스턴스 메소드
  • 타입 메소드
  • 생성자 (클래스의 경우 'Convenience initializer/간편 생성자'로 제한)
  • 서브스크립트
  • nested type

두 번째 문법은 기존의 형식에 프로토콜을 추가할 때 사용한다.
이후 프로토콜 부분에서 다시 언급한다.

struct Size {
	var width = 0.0
	var height = 0.0
}

extension Size {
	var area: Double {
		return width * height
	}
}

let s = Size(width: 3, height: 7)
s.width
s.height
s.area
결과

3
7
21

확장된 area에서 대상인 Size 구조체에 접근하는 것도 자연스럽게 가능하다.
또한 Size로 생성된 인스턴스 s에서 확장된 area 속성에 접근하는 것도 가능하다.
이때 익스텐션에는 계산 속성만 추가할 수 있다는 것에 주의하자.

익스텐션으로 비교 연산을 구현해 보자.
비교 연산자는 equatable 프로토콜을 사용하며, 지금은 원본 구조체를 수정할 수 있으므로 익스텐션으로 구현할 필요는 없다.
하지만 원본 구조체를 수정할 수 없는 경우 다음과 같이 진행한다.

struct Size {
	var width = 0.0
	var height = 0.0
}

extension Size: Equatable {
	public static func == (lhs: Size, rhs: Size) -> Bool {
		return lhs.width == rhs.width && lhs.height == rhs.height
	}
}

대상이 Size가 Equatable 프로토콜을 채용할 것을 익스텐션으로 구현했다.
이는 프로토콜 추가 문법으로서 이후에 다시 언급한다.
가능하다는 것만 기억하자.

 

Adding Properties (속성 추가하기)

익스텐션에서는 계산 속성만 추가할 수 있다.
또한, 형식에 존재하는 속성을 오버 라이딩하는 것도 불가능하다.

Date 속성을 확장해서 연도를 반환하도록 하자.

extension Date {
	var year: Int {
		let cal = Calendar.current
		return cal.component(.year, from: self)
	}
}

let d = Date()
d.year
결과

"Jul 2, 2021 at 4:27 PM"
2021

Date에서 반환하는 형식 대신에 d.year에서는 연도만 별도로 출력하는 것을 볼 수 있다.
속성 내부에서 날짜 인스턴스에 접근할 때는 'self'로 접근한다는 것을 알아두자.

이번엔 월만 출력하는 익스텐션을 추가해 보자.

extension Date {
	var year: Int {
		let cal = Calendar.current
		return cal.component(.year, from: self)
	}
	var month: Int {
		let cal = Calendar.current
		return cal.component(.month , from: self)
	}
}

let d = Date()
d.year
d.month
결과

"Jul 2, 2021 at 4:32 PM"
2021
7

오늘의 날짜에서 월만 별도로 반환하는 익스텐션이 완성됐다.

날짜도 마찬가지로 구현할 수 있다.

extension Date {
	var year: Int {
		let cal = Calendar.current
		return cal.component(.year, from: self)
	}
	var month: Int {
		let cal = Calendar.current
		return cal.component(.month , from: self)
	}
	var date: Int {
		let cal = Calendar.current
		return cal.component(.day, from: self)
	}
}

let d = Date()
d.year
d.month
d.date
결과

"Jul 2, 2021 at 4:35 PM"
2021
7
2

이번엔 Double에 radian과 degree 변환을 추가해 본다.

extension Double {
	var radian: Double {
		return (Double.pi * self) / 180.0
	}
	var degree: Double {
		return self * 180.0 / Double.pi
	}
}

let dv = 45.0
dv.radian
dv.radian.degree
결과

45
0.7853981633974483
45

각도를 라디안으로 변환하고,
라디안을 각도로 변환한다.
이때 각각의 익스텐션의 반환형이 모두 Double이므로 연달아 쓰는 것도 가능하다.

 

Adding Methods (메소드 추가)

익스텐션으로 메소드를 추가한다.
저장된 온도를 섬 씨와 화씨로 변환하는 익스텐션을 구현한다.
화씨의 섭씨 변환 공식은 다음과 같다.

(°F − 32) × 5/9 = °C

따라서 섬 씨의 화씨 변환 공식은 다음과 같다.

(°C − 32) × 9/5 = °F
extension Double {
	func rah() -> Double {
		return self * 9 / 5 + 32
	}
	func cel() -> Double {
		return (self - 32) * 5 / 9
	}
}

let t = 32.0
t.toC()
t.toC().toF()
결과

32
0
32

변환이 잘 되는 것을 확인할 수 있다.

이번엔 생성한 인스턴스 메소드를 활용해 타입메소드를 생성해 본다.
Double을 받아 인스턴스 메소드를 활용환 변환을 거친 뒤 다시 Doble을 반환한다.

extension Double {
	func toF() -> Double {
		return self * 9 / 5 + 32
	}
	func toC() -> Double {
		return (self - 32) * 5 / 9
	}
	
	static func convFtoC (from cel: Double) -> Double {
		return cel.toC()
	}
	static func convCtoF (from fah: Double) -> Double {
		return fah.toF()
	}
}

let t = 32.0
t.toC()
Double.convFtoC(from: 32)
결과

32
0
0

타입 메소드를 사용하지만 인스턴스 메소드와 같은 결과가 나오는 것을 볼 수 있다.

이번엔 날짜를 문자열로 바꾸는 메소드를 추가해 본다.

extension Date {
	func toString(format: String = "yyyyMMdd") -> String {
		let privateForm = DateFormatter()
		privateForm.dateFormat = format
		return privateForm.string(from: self)
	}
}

let td = Date()
td.toString()
결과

"Jul 2, 2021 at 5:42 PM"
"20210702"

파라미터를 비우고 메소드를 호출하게 되면 날짜값만 기본 포맷에 맞게 반환된다.

extension Date {
	func toString(format: String = "yyyyMMdd") -> String {
		let privateForm = DateFormatter()
		privateForm.dateFormat = format
		return privateForm.string(from: self)
	}
}

let td = Date()
td.toString(format: "dd")
결과

"Jul 2, 2021 at 5:44 PM"
02

파라미터에 원하는 포맷을 전달하면 해당 포맷으로 치환되어 반환한다.

이번엔 일정 길이의 무작위 문자열을 생성하는 메소드를 생성한다.

extension String {
	static func rnd(length: Int, character: String = "ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣabcdefghijklmnopqrstuvwxyz") -> String {
		var rndString = String()
		rndString.reserveCapacity(length)
		
		for _ in 0 ..< length {
			guard let char = character.randomElement() else {
				continue
			}

			rndString.append(char)
		}

		return rndString
	}
}

해당 메소드는 타입메소드이다.
파라미터로 길이와 기본 문자열이 전달된다.
처음으로 전달된 길이만큼의 문자열 공간을 확보하고,
반복문을 통해 기본 문자열에서 무작위의 문자를 뽑아 병합한다.
이후 완성된 문자열을 반환한다.

extension String {
	static func rnd(length: Int, character: String = "ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣabcdefghijklmnopqrstuvwxyz") -> String {
		var rndString = String()
		rndString.reserveCapacity(length)

		for _ in 0 ..< length {
			guard let char = character.randomElement() else {
				continue
			}
			
			rndString.append(char)
		}
		
		return rndString
	}
}

String.rnd(length: 6)
String.rnd(length: 8, character: "1234567890")
결과

"ㅇxdyㅗt"
"68510361"

길이만 전달하면 기본 문자열에서, 길이와 대상 문자열을 전달하면 해당 문자열에서 무작위 추출한다.
무작위이기 때문에 실행할 때마다 다른 문자열이 반환된다.

 

Adding Initializers (생성자 추가)

익스텐션으로 생성자를 추가한다.

swift에서 원하는 날짜를 생성하려면 calendar와 DateComponents를 사용해야 해서 구현이 복잡하다.
연, 월, 일을 파라미터로 받아 원하는 날짜를 생성하는 생성자를 익스텐션으로 추가한다.
이때, 생성할 수 없는 날짜가 존재할 수 있기 때문에 failable initializer를 활용한다.

extension Date {
	init?(year: Int, month: Int, day: Int) {
		let cal = Calendar.current
		var comp = DateComponents()
		
		comp.year = year
		comp.month = month
		comp.day = day
		
		guard let date = cal.date(from: comp) else {
			return nil
		}
		self = date
	}
}

상수 cal에 Calendar.current로 유저가 사용 중인 calendar를 불러온다.
변수 comp에 빈 DateComponents를 생성한다.

이후 연, 월, 일을 DateComponents의 year, month, day 속성에 저장한다.
저장된 comp를 cal에 대입해 반환한다.
만약 반환할 수 없다면 nil을 반환한다.

extension Date {
	init?(year: Int, month: Int, day: Int) {
		let cal = Calendar.current
		var comp = DateComponents()
		
		comp.year = year
		comp.month = month
		comp.day = day
		
		guard let date = cal.date(from: comp) else {
			return nil
		}
		self = date
	}
}

Date(year: 1999, month: 9, day: 30)
결과

"Sep 30, 1999 at 12:00 AM"

입력한 날짜로 초기화되었다.

UIColor 클래스에 새로운 생성자를 추가한다.
클래스에 생성자를 추가할 경우 반드시 간편 생성자로 구현해야 한다.

UIColor 클래스는 0.0~1.0까지의 값으로 컬로 코드를 받는다.
하지만 사용자들은 0~255까지의 RGB코드를 사용하는 것에 익숙하기 때문에
UIColor 클래스가 RGB코드를 받을 수 있도록 생성자를 추가해보자.

extension UIColor {
	convenience init(red: Int, green: Int, blue: Int) {
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

UIColor의 생성자는 red, green, blue와 투명도를 결정하는 0.0~1.0의 alpha로 이루어진다.
파라미터로 전달하는 값을 255로 나누어 0.0~1.0의 값으로 변환하고 이를 사용해 초기화를 진행한다.

extension UIColor {
	convenience init(red: Int, green: Int, blue: Int) {
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

UIColor(red: 0, green: 255, blue: 255)
결과

r 0.0 g 1.0 b 1.0 a 1.0

RGB코드로 UIColor를 초기화할 수 있게 됐다.

익스텐션으로 생성자를 만드는 데에는 이유가 있다.

struct Size {
	var width = 0.0
	var height = 0.0
}

이전에 정리한 대로 구조체에서 모든 멤버가 기본값을 가지고 별도의 생성자를 가지지 않을 때 기본 생성자와 memberwise 생성자가 제공된다.
하지만 별도의 생성자가 필요해져 이를 구현했을 경우

struct Size {
	var width = 0.0
	var height = 0.0
	
	init(value: Double) {
		width = value
		height = value
	}
}

Size()
결과

//error

기본 생성자와 memberwise 생성자를 사용할 수 없어 이를 추가로 구현해야 한다는 번거로움이 생긴다.
이런 경우 익스텐션으로 생성자를 생성하게 된다면

struct Size {
	var width = 0.0
	var height = 0.0
}
	
extension Size {
	init(value: Double) {
		width = value
		height = value
	}
}

Size(value: 77)
Size()
Size(widht: 12, height: 34)
결과

Size(width: 77.0, height: 77.0)
Size(width: 0.0, height: 0.0)
Size(width: 12.0, height: 34.0)

새로 작성한 생성자와 기본 생성자, memberwise 생성자까지 그대로 사용할 수 있다.

연습

위에서 작성한 코드는 UIColor 코드는 범위의 유효성을 생각하지 않는 코드이다.
이를 개선해본다.

extension UIColor {
	convenience init(red: Int, green: Int, blue: Int) {
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

해당 간편 생성자는 Int 형식의 red, green, blue를 파라미터로 가지고,
해당 파라미터들을 0.0~1.0 사이의 값으로 변환한 뒤, UIColor의 생성자에 전달한다. 이때 alpha의 기본값은 1.0으로 전달한다.

따라서 범위의 유효성을 확인할 파라미터는 red, green, blue 3가지이다.
각각의 값에 대해 비교 연산자를 사용하여 구현할 수도 있지만, 코드의 가독성을 위해 반복문을 사용하도록 한다.

extension UIColor {
	convenience init(red: Int, green: Int, blue: Int) {
		let param = [red, green, blue]
		
		for value in param {
		
		}
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

세 파라미터의 값들을 배열로 받고, 배열의 요소만큼 반복하도록 for-in문을 작성했다.
배열의 요소인 value에 대하여 조건에 따라 분기하도록 해야 하는데,
여러 개의 조건으로 분기하기보다는 switch문을 사용해 가독성을 높이도록 했다.

extension UIColor {
	convenience init(red: Int, green: Int, blue: Int) {
		let param = [red, green, blue]
		
		for value in param {
			switch value {
			case 0...255:
				continue
			default:
				return nil
			}
		}
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

이렇게 되면 0~255 사이의 값들에 대해서는 아무 일 없이 진행되어 결국 생성자를 호출하지만
범위 밖의 값이 인식될 경우 default로 분기해 즉시 nil을 반환한다.
이때 nil을 반환할 수 있도록 생성자를 failable 생성자로 바꿔준다.

extension UIColor {
	convenience init?(red: Int, green: Int, blue: Int) {
		let param = [red, green, blue]
		
		for value in param {
			switch value {
			case 0...255:
				continue
			default:
				return nil
			}
		}
		self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
	}
}

UIColor(red: 0, green: 255, blue: 255)
UIColor(red: 0, green: 300, blue: 255)
결과

r 0.0 g 1.0 b 1.0 a 1.0
nil

의도한 대로 동작하는 것을 확인할 수 있다.

+

실습 진행한 코드를 조금 더 개선했다.

extension UIColor {
	convenience init?(red: Int, green: Int, blue: Int) {
		let param = [red, green, blue]
		let condition : ((Int)) -> Bool = {
			$0 < 0 || $0 > 255
		}
		
		if param.contains(where: condition) == true {
			return nil
		}
		else {
			self.init(red: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: 1.0)
		}
	}
}

UIColor(red: 0, green: 255, blue: 255)

 

원하는 범위를 클로저로 생성한 뒤,
contains(where:)에 전달해 해당 범위에 맞게 판단하도록 했다.
괜히 꼬는 느낌이라 '적절한 값이 아니면'이라는 조건에 매칭 되면 true로 반환되는 부분은 그대로 뒀다.

 

Adding Subscripts (서브스크립트 추가)

문자열 인스턴스는 원칙적으로 문자열 index를 서브스크립트로 전달할 수 없다.
이를 가능하게 하고, 해당 문자를 반환하도록 구현해 본다.

extension String {
	subscript(idx: Int) -> String? {
		guard (0..<count).contains(idx) else {
			return nil
		}
		let target = index(startIndex, offsetBy: idx)
		return String(self[target])
	}
}

저장된 문자열의 길이와 비교해 인덱스가 범위 내에 존재하면 해당 인덱스의 문자를 반환한다.

extension String {
	subscript(idx: Int) -> String? {
		guard (0..<count).contains(idx) else {
			return nil
		}
		let target = index(startIndex, offsetBy: idx)
		return String(self[target])
	}
}

let str = "hello"
str[2]
str[9]
결과

l
nil

저장된 hello의 세 번째 인덱스에 해당되는 l이 반환됐다.
범위를 벗어난 경우 nil이 반환된다.

서브스크립트로 전달할 수 있는 것은 범위와 인덱스뿐만이 아니다.
다음엔 날짜 component를 반환하는 서브스크립트를 생성한다.

extension Date {
	subscript(component: Calendar.Component) -> Int? {
		let cal = Calendar.current
		return cal.component(component, from: self)
	}
}

let d = Date()
d[.year]
결과

2021

날짜에서 파라미터로 전달된 컴포넌트를 출한 다음 이를 반환한다.
.year 컴포넌트를 전달했으므로 연도를 반환한다.

 


Log

2021.09.12.
블로그 이전으로 인한 글 옮김 및 수정