본문 바로가기

학습 노트/Swift (2021)

173 ~ 174. Advanced Topic

Availiability Condition

API 가용성을 확인하는 방법이다.
새로운 OS가 출시되면 새로운 기능을 위한 API가 추가된다.
매번 최신 API를 사용한다면 좋겠지만 현실적으로 불가능한 경우가 많다.

새 애플리케이션을 생성하면 배포 타깃은 자동으로 최신 버전으로 지정된다.
이 상태로 배포하게 되면 해당 버전보다 낮은 상태의 기기들은 이 앱을 사용할 수 없다.
따라서 최신버전 보다는 2~3 정도 낮은 버전을 타깃으로 설정한다.

class ViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		// Do any additional setup after loading the view.
		
		navigationController?.navigationBar.prefersLargeTitles = true
	}
}

해당 코드는 네비게이션 바에서 LargeTitle을 활성화하는 코드이다.
해당 코드는 현재 빌드 타겟인 14.4 버전에서 문제없이 동작하지만,

빌드 타겟을 10.0으로 설정하면 해당 코드를 비롯, 자동으로 작성됐던 코드들 까지 상당수가 에러를 표시한다.

오류가 난 코드의 개발 문서를 확인해 보자.

해당 속성인 predersLargeTitles는 iOS11.0 이상, Mac Catalyst 13.0 이상에서 사용 가능하다고 설명되어 있다.
해당 속성은 iOS11.0에서 새롭게 추가된 속성으로 배포 버전이 10.0으로 설정된 지금은 사용할 수 없는 속성이다.

컴파일러에서 에러를 조금 더 자세히 확인해 보자.

첫번째 자동 수정 추천에 '#available'을 선택하게 되면 Available Condition 기능이 추가된다.

class ViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		// Do any additional setup after loading the view.
		
		if #available(iOS 11.0, *) {
			navigationController?.navigationBar.prefersLargeTitles = true
		} else {
			// Fallback on earlier versions
		}
	}
}

이렇게 새로운 if문이 추가되는데,
이 중 '#available(iOS 11.0, *)'이 vailable condition이다.

Syntax

if #available(OS VersionOS Version, *) {

} else {

}

while #available(OS Version, *) {

}

guard #available(OS Version, *) else {
    return
}

availablility condition은 if문, while문, guard문에서 사용한다.
'#available'키워드로 시작해서 괄호 안에 OS 버전과 이름을 작성한다.
보통은 하나의 버전을 작성하지만 경우에 따라 복수를 나열할 수도 있다.
OS 이름으로 사용할 수 있는 것은 iOS, macOS, tvOS, watchOS 네 가지로 제한된다.
이 중 iOS와 macOS는 익스텐션을 선언하는 것이 가능하고, 버전을 작성할 때는 숫자로 작성하고 최소 버전을 나타낸다.
마지막 '*'은 생략할 수 없다.

따라서 위에 작성된 if 블록 내의 코드는 iOS 11 이상에서 실행될 수 있다.
else 블록에선 지원하지 않는 기능이라는 안내 메세지나 이전 버전을 지원하는 별도의 기능을 작성하기도 한다.

컴파일 타임이 아닌 런타임에 버전을 확인하며, 논리 연산자를 활용해 다음과 같이 두 개 이상의 컨디션을 결합하는 것은 허용되지 않는다.

class ViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		// Do any additional setup after loading the view.
		
		if #available(iOS 11.0, *) && #available(macOS 12.0, *) {
			navigationController?.navigationBar.prefersLargeTitles = true
		} else {
			// Fallback on earlier versions
		}
	}
}
결과

//error

이 경우 다음과 같이 괄호 안에 나열하거나 ','로 작성해야 한다.

class ViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		// Do any additional setup after loading the view.
		
		if #available(iOS 11.0, macOS 12.0, *) {
			navigationController?.navigationBar.prefersLargeTitles = true
		} else {
			// Fallback on earlier versions
		}
	}
}

 

Metatype

Metatype은 값의 타입을 표현하는 타입이다.
Metatype이라는 이름을 이루는 Meta(Metadata/메타데이터)는

사진의 EXIF같이 여러 부가정보들을 말한다.
이렇게 데이터를 설명하는 데이터를 메타데이터라고 부른다.
따라서 Metatype은 타입을 설명하는 타입이 된다.

func checkType(of value: Any) {
	let typeOfValue = type(of: value)
	
	print("\(value) => \(typeOfValue)")
}

checkType 함수는 값을 파라미터로 받아서 type(of:) 함수로 타입을 받은 다음,
값과 타입을 출력한다.

func checkType(of value: Any) {
	let typeOfValue = type(of: value)
	
	print("\(value) => \(typeOfValue)")
}

//-----

let name = "Jane Doe"
checkType(of: name)

let age = 0
checkType(of: age)
결과

Jane Doe => String
0 => Int

따라서 각각의 데이터들에 대해 위와 같은 결과가 출력된다.
각자 저장된 데이터의 타입이 어떤 타입인지 이름을 출력해 주고 있다.

func type<T, Metatype>(of value: T) -> Metatype

개발 문서를 확인해 보면 위와 같이 반환값이 dynamic type이고,

The dynamic type, which is a metatype instance.

더 아래에는 '다이나믹 타입은 메타 타입의 인스턴스이다.'라고 설명하고 있다.

Dynamic Type & Static Type (스테틱 타입)

func checkType(of value: Any) {
	let typeOfValue = type(of: value)
	
	print("\(value) => \(typeOfValue)")
}

우선 기존에 작성했던 코드에서 typeOfValue에 저장 된 타입은 다이내믹 타입이다.

typeOfValue를 확인해 보면 'Any.Type'으로 표시되는 것을 확인할 수 있다.
위와 같이 기존의 타입 뒤에 '.Type'을 붙이면 메타 타입의 타입이 된다.
즉, 메타타입의 타입은 'Any.Type'이지만 이를 출력하면 'String'으로 출력되는 것이다.
이렇게 런타임에 확인된 타입을 다이내믹 타입이라고 부른다.
스테틱 타입은 컴파일 타임에 사용되는 타입이다.

func checkType(of value: Any) {
	let typeOfValue = type(of: value)
	
	print("\(value) => \(typeOfValue)")
}

//-----

let name = "Jane Doe"
checkType(of: name)

let age = 0
checkType(of: age)

다시 말해 name 상수의 다이나믹 타입은 String이고, 스테틱 타입은 String이다.
name 상수에서 호출하는 checkType의 파라미터인 value의 다이내믹 타입은 String이고, 스테틱 타입은 Any이다.
age 상수의 다이나믹 타입과 스테틱 타입은 모두 Int이다.
age 상수에서 호출하는 checkType의 파라미터인 value의 다이내믹 타입은 Int이고, 스테틱 타입은 Any이다.

만약 타입이 클래스, 구조체, 열거형이라면 위와 같이 '.Type'을 붙이고,
프로토콜이라면 '.Protocol'을 붙인다.

타입이 프로토콜이거나 프로토콜 composition이라면 Existential Metatype이라고 부르고,
이외의 경우엔 Concrete Metatype이라고 부른다.

Metatype 활용

현재는 전혀 이해할 수 없으나 미리 정리한다.

class MyCell: UITableViewCell {

}

class CellRegistrationViewController: UIViewController {
	
	let tableView = UITableView()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tableView.register(cellClass: AnyClass?, forCellReuseIdentifier: String)
	}
}

cellClass는 optional AnyClass이다.
이 AnyClass가 메타타입이다.

typealias AnyClass = AnyObject.Type

AnyClass의 개발 문서를 확인해 보면 AnyObject.Type의 typealias로 선언되어 있다.
따라서 해당 파라미터에는 등록된 Cell의 메타 타입을 전달해야 한다.

처음 선언한 MyCell을 전달하기 위해서

class MyCell: UITableViewCell {

}

class CellRegistrationViewController: UIViewController {
	
	let tableView = UITableView()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tableView.register(MyCell, forCellReuseIdentifier: "cell")
	}
}
결과

//error

위와 같이 타입의 이름만 전달하는 걸로 메타 타입이 전달되진 않는다.
그렇다고 배운 대로 '.Type'을 추가하면

class MyCell: UITableViewCell {

}

class CellRegistrationViewController: UIViewController {
	
	let tableView = UITableView()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tableView.register(MyCell.Type, forCellReuseIdentifier: "cell")
	}
}
결과

//error

여전히 에러를 표시한다.

class MyCell: UITableViewCell {

}

class CellRegistrationViewController: UIViewController {
	
	let tableView = UITableView()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		tableView.register(MyCell.self, forCellReuseIdentifier: "cell")
	}
}

메타 타입을 파라미터로 전달하기 위해선 '.self'로 전달해야 한다.
인스턴스 메서드에서 사용하던 self와는 다르게 메타 타입 인스턴스를 전달하는 속성으로 취급된다.

let jsonData = """
{
	"id": 1717,
	"title": "173 ~ 174. Advanced Topic",
	"description": "typeOfValue를 확인해 보면 'Any.Type'으로 표시되는 것을 확인할 수 있다. 위와 같이 기존의 타입 뒤에 '.Type'을 붙이면 메타타입의 타입이 된다. 즉, 메타타입의 타입은 'Any.Type'이지만 이를 출력하면 'String'으로 출력되는 것이다. 이렇게 런타임에 확인 된 타입을 다이나믹 타입이라고 부른다. 스테틱 타입은 컴파일타임에 사용되는 타입이다."
}
""".data(using: .utf8)!


struct Book: Codable {
	let id: Int
	let title: String
	let description: String
}


class JSONDecodingViewController: UIViewController {
	
	override func viewDidLoad() {
		super.viewDidLoad()
		
		do {
			let decoder = JSONDecoder()
			
			try decoder.decode(type: Decodable.Protocol, from: Data)
		} catch {
			
		}
	}
}
try decoder.decode(type: Decodable.Protocol, from: Data)

위의 코드에서 JSON 디코더에서 제공하는 decoder 메소드의 첫 번째 type 파라미터도 '.Protocol'인 것으로 보아 메타 타입을 요구하고 있다.
이곳에는 Decodable을 구현하고 있는 타입의 메타 타입을 전달해야 하고, 따라서 위 코드에선 Book 인스턴스의 메타타입을 전달해야 한다.

try decoder.decode(Book.self, from: jsonData)

따라서 완성되는 코드는 위와 같이 Book.self와 jsonData를 전달해야 한다.

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

extension UIViewController {

}

제네릭과 메타타입을 사용하면 새로운 인스턴스를 생성하는 코드를 효율적으로 작성할 수 있다.

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

extension UIViewController {
	func instantivateViewController<VC: UIViewController>(ofType type: VC)
}
func instantivateViewController<VC: UIViewController>(ofType type: VC)

파라미터 타입을 위와 같이 선언하면 파라미터로 ViewController의 인스턴스를 전달해야 한다.
이 경우 메타 타입이 필요하므로 다음과 같이 수정한다.

func instantivateViewController<VC: UIViewController>(ofType type: VC.Type)

 

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

extension UIViewController {
	func instantivateViewController<VC: UIViewController>(ofType type: VC.Type) -> VC? {
		let vcClassName = String(describing: type)
	}
}

메타 타입은 위와 같이 String의 생성사를 사용해서 문자열로 변환할 수 있다.
vcClassName과 같은 storyboard ID를 가진 viewController가 있다면

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

extension UIViewController {
	func instantivateViewController<VC: UIViewController>(ofType type: VC.Type) -> VC? {
		let vcClassName = String(describing: type)
		return storyboard?.instantiateViewController(withIdentifier: vcClassName) as? VC
	}
}

실제 타입으로 타입 캐스팅하고, 이를 반환한다.
이후 메서드를 호출할 때는

class GenericFactoryViewController: UIViewController {
	override func viewDidLoad() {
		super.viewDidLoad()
		let vc = instantivateViewController(ofType: JSONDecodingViewController)
	}
}

extension UIViewController {
	func instantivateViewController<VC: UIViewController>(ofType type: VC.Type) -> VC? {
		let vcClassName = String(describing: type)
		return storyboard?.instantiateViewController(withIdentifier: vcClassName) as? VC
	}
}
let vc = instantivateViewController(ofType: JSONDecodingViewController)

위와 같이 클래스의 이름만 전달하는 것이 아닌,

let vc = instantivateViewController(ofType: JSONDecodingViewController.self)

'.self'로 전달해줘야 한다.

이렇게 되면 원하는 형식으로 바로 저장되기 때문에 별도의 타입 캐스팅이 필요가 없어진다.
이 경우 문자열로 직접 전달할 수도 있지만 오타의 여지없이 그대로 받아 전달할 수 있다는 장점이 있다.

func duplicateCurrentViewController() -> UIViewController? {
	let vcType = type(of: self)
}

이번엔 현재 보고 있는 뷰 컨트롤러를 복제하는 메소드이다.
type(of:)가 반환하는 것은 메타 타입의 인스턴스이다.
이는 타입 이름 대신 사용할 수 있다.

func duplicateCurrentViewController() -> UIViewController? {
	let vcType = type(of: self)
	return vcType.init()
}

따라서 상수 통해 생성자 호출은 물론, 타입의 type property에 접근하는 것도 가능하다.

 


Log

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