- iOS 개발을 위해 Swift 문법을 공부하다보니, 낯선 개념들이 많이 보였습니다. 이들을 어느정도 정리해야할 필요성을 느껴 정리해둡니다.
- 참고1 : iOS 프로그래밍을 위한 스위프트 기초(https://www.boostcourse.org/mo122/joinLectures/38564)
- 참고2 : 애플 개발자 문서(https://developer.apple.com/documentation)
- 참고3 : Swift 5.5 번역(https://xho95.github.io/swift/programming/language/grammar/2017/02/28/The-Swift-Programming-Language.html)
- cf. 타언어와 유사하고 기초적인 문법들은 정리하지 않았습니다.
optional
nil값과 optional이란
nil
: 아예 값이 존재하지 않음을 의미- swift는 기본적으로 변수(상수)에 nil값을 보관하는 것을 허용하지 않는다.
- 따라서 이를 보관하기 위해 사용되는 것이 optional이다.
- optional은 말 그대로 변수의 값이 nil일 수도 있음을 의미한다.
enum Optional<Wrapped>: ExpressibleByNiliteral {
case none
case some(Wrapped)
}
- optional은 enum이다. '값이 존재하지 않음' 혹은 '특정 값을 wrap해놓은 값' 두가지를 가지는 것이다.
optional 사용
var optionalValue: Int? = 100
optionalValue = nil
?
: 기본 옵셔널. 변수의 타입 뒤에 표현하며, 변수가 nil값을 포함할 수도 있음을 의미한다.- 하지만 변수처럼 자유롭게 사용은 불가능하다. (
optionalValue = optionalValue + 1
과 같은 실행 불가능)
var implicitlyUnwrappedOptionalValue: Int! = 100
implicitlyUnwrappedOptionalValue = nil
!
: 암시적 추출 옵셔널. 변수의 타입 뒤에 표현하며, 변수가 nil값을 포함할 수도 있음을 의미한다.- 변수처럼 자유롭게 사용이 가능하다. (
optionalValue = optionalValue + 1
과 같은 실행 가능) - 다만 이는 nil값이 아닐 것임에 대한 확신이 있을 때 사용한다. nil값을 포함할 수는 있지만 값을 항상 추출하여 변수처럼 사용하기 때문에 nil값이 존재할 경우 런타임 에러가 발생한다.
optional unwrapping
var number:Int? = 20
print(number!)
- optional 변수를 unwrapping하여 사용할 수 있도록 한다.
- 만약 nil값이 존재하는 변수였을 경우 바로 런타임 에러가 발생한다.
- cf.
!
으로 처음으로 암시적 추출 옵셔널을 사용했을 경우 이를 수행하지 않아도 바로 변수처럼 사용이 가능하다.
optional binding
- 옵셔널에 들어있는 값을 확인하고 추출하는 것이다.
var myName: String? = nil
var yourName: String! = nil
if let name: String = myName {
print(name)
} else {
print("myName is nil")
}
if let name = myName, let friend = yourName {
print("\(name) and \(friend)")
}
- if-let 방식 : if문과 let을 바로 연이어 사용. 옵셔널 안에 값이 있는지 확인하고 있으면 해당 값을 꺼내온다.
- 값이 존재하지 않을 경우(nil) else문을 수행한다.
- 이렇게 설정한 상수는 if-let 구문 내부에서만 사용이 가능하다.
optional chaining
- optional값 내부에 연속해서 optional값이 존재할 때 유용하게 사용된다.
guardJob = man?.home?.guard?.job
- man, home, guard가 모두 optional값이라고 가정했을 때 위와 같은 방식으로 전개해 값을 가져올 수 있다.
- 하나씩 접근하다 중간에 nil값이 존재하면 접근을 멈추고 nil값을 반환한다.
guardJob = yagom?.home?.guard?.job ?? "슈퍼맨"
- nil값이 반환되려 할 때 특정값이 대신 반환되도록 할 수 있다. 위의 경우 nil값이 반환되려 할 경우 대신 "슈퍼맨"이 반환된다.
class 메서드
- swift에서
static 메서드
는 다른 프로그래밍 언어와 같이 인스턴스를 생성하지 않고 바로 사용할 수 있는 메서드이다. - 다만 이런
static 메서드
는 상속 시 재정의가 불가능하다. - 인스턴스를 생성하지 않고 바로 사용할 수 있지만 상속 시 재정의가 가능한 메서드를
class 메서드
라고 한다.
class func classMethod() {
print("type method - class")
}
- class 내부에서 위와 같이 작성하여 정의한다.
- cf. 구조체와의 차이 : 클레스는 참조타입(데이터를 전할 때 메모리 위치전달)이고 구조체는 값타입(데이터를 전할 때 값을 복사) / 클레스는 상속이 가능하지만 구조체는 불가능
enum
enum Weekday {
case mon
case tue
case wed
case thu, fri, sat, sun
}
- 유사한 종류의 여러 값을 한 곳에 모아 정의한 것.
- enum 자체가 하나의 데이터타입이 된다. (대문자 카멜케이스)
- 각 case는 고유의 값이 된다. (소문자 카멜케이스)
- 한 줄에 여러개도 정의될 수 있다.
var day: Weekday = Weekday.mon
day = .tue
- enum 타입과 케이스를 사용해 값을 정의할 수 있다.
- 타입이 정해진 변수라면 케이스만 표현해도 괜찮다.
클로저
{ (매개변수 목록) -> 반환타입 in
실행 코드
}
- 매개변수와 반환 값이 존재할 수 있는 실행가능한 코드 블럭
- 이름을 필요로 하지 않는다.
- 함수는 일종의 이름있는 클로저이다.
let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
return a + b
}
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
var calculated: Int
calculated = calculate(a: 50, b: 10, method: add)
- 이는 그대로 상수에 할당해 전달인자로 사용할 수 있다.
calculated = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
return left + right
}
- 후행클로저 : 클로저가 함수의 마지막 전달인자일 경우 이름을 생략한 후 외부에 클로저를 구현할 수 있다.
calculated = calculate(a: 10, b: 10, method: { (left: Int, right: Int) in
return left + right
})
- 이미 반환값이 정해져있는 경우 반환값 생략이 가능하다.
calculated = calculate(a: 10, b: 10, method: {
return $0 + $1
})
- 매개변수의 이름도 생략하고 $0,$1,$2,... 로 표현할 수 있다.
calculated = calculate(a: 10, b: 10) {
$0 + $1
}
- 반환값이 존재하는 클로저의 경우 마지막줄은 암시적으로 반환값으로 취급된다.
프로퍼티
- swift에는 일반적인 프로퍼티와 다르게 연산 프로퍼티라는 개념이 존재한다.
연산 프로퍼티
struct Person {
var koreanAge: Int = 0
var westernAge: Int {
get {
return koreanAge - 1
}
set(inputValue) {
koreanAge = inputValue + 1
}
}
var selfIntroduction: String {
get {
return "저는 \(koreanAge)살 입니다"
}
}
static var selfIntroduction: String {
return "사람입니다"
}
}
- 내부적으로
get
,set
을 가지며 저장 프로퍼티를 기반으로 연산하여 값을 저장 및 반환한다. - 읽기전용 연산 프로퍼티도 가능하며, 이 경우
get
블럭은 생략할 수 있다. - 프로퍼티는 구조체,클래스,열거형 내부에 구현가능 (열거형 내부에는 연산프로퍼티만)
프로퍼티 감시자
struct Money {
var currencyRate: Double = 1100 {
willSet(newRate) {
print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다")
}
didSet(oldRate) {
print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
}
}
var dollar: Double = 0 {
willSet {
print("\(dollar)달러에서 \(newValue)달러로 변경될 예정입니다")
}
didSet {
print("\(oldValue)달러에서 \(dollar)달러로 변경되었습니다")
}
}
}
- 특정 프로퍼티가 변경될때마다 특정 작업을 수행하도록 할 수 있다.
- cf. 연산 프로퍼티에는 프로퍼티 감시자를 사용할 수 없다.
캐스팅
class Person {
}
class Student: Person {
}
class UniversityStudent: Student {
}
- 예시에 사용할 클래스
업캐스팅
var mike: Person = UniversityStudent() as Person
- 부모클래스의 인스턴스로 사용하고 싶은 경우
as
를 활용해 업캐스팅을 진행할 수 있다.
다운캐스팅
var optionalCasted: Student?
optionalCasted = mike as? UniversityStudent
optionalCasted = Person() as? UniversityStudent // nil
var forcedCasted: Student
forcedCasted = mike as! UniversityStudent
as?
를 통해 자식클래스로 다운캐스팅을 진행할 수 있다.- 캐스팅을 할 수 없는 경우 nil값을 반환한다. (결과의 타입은 옵셔널이어야함)
as!
를 통해 강제적으로 다운캐스팅을 진행할 수 있다.- 캐스팅에 실패할 경우 바로 런타임오류가 발생한다.
assert
- 예상하는 조건의 검증을 위해 사용된다.
- 디버깅모드에서만 작동하며, 배포하는 애플리케이션에서는 제외된다.
var someInt: Int = 0
assert(someInt == 0, "someInt != 0")
- 조건에 부합할 경우 지나가고, 실패할 경우 동작이 중지되고 설정한 메세지를 출력한다.
guard
func functionWithGuard(age: Int?) {
guard let unwrappedAge = age,
unwrappedAge < 130,
unwrappedAge >= 0 else {
return
}
print("당신의 나이는 \(unwrappedAge)세입니다")
}
var count = 1
while true {
guard count < 3 else {
break
}
print(count)
count += 1
}
- 잘못된 값 전달 시 실행 구문을 바로 종료한다.
- else 블럭 내에 코드블럭을 종료하는 지시어(break,return 등)이 존재해야 한다.
guard let unwrapped: Int = someValue else {
return
}
unwrapped = 3
- 옵셔널바인딩에도 사용가능
- if-let 바인딩과는 다르게 외부에서도 사용이 가능하다는 장점이 있다.
프로토콜
protocol Talkable {
// 프로퍼티 요구
var topic: String { get set }
var language: String { get }
// 메서드 요구
@objc optional func talk()
// 이니셜라이저 요구
init(topic: String, language: String)
}
- java의 interface와 유사한 기능이다.
- 다만 protocol은 초기값 설정이 허용되지 않으며,
optional
이 붙어있는 메서드는 구현하지 않아도 되고, 정적멤버 선언이 가능하다는 차이점이 있다. (java의 interface는 초기값 설정이 가능하고, 모든 메서드를 구현해야하며, 정적멤버 선언이 불가능하다.) - 상속형태로 클래스에서 받아와 작성한다. 단, 클래스와는 다르게 다중 상속이 가능하다.
someAny is Talkable
if let someAny: Talkable = someAny as? Talkable {
someAny.talk()
}
is
와as
를 통해 인스턴스가 특정 프로토콜이 준수하는지 확인할 수도 있다.
익스텐션
- extension을 통해 특정 클래스(혹은 자료형)에 새로운 기능을 추가할 수 있다.
연산 프로퍼티 추가
extension Int {
var isEven: Bool {
return self % 2 == 0
}
var isOdd: Bool {
return self % 2 == 1
}
}
print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd) // true
print(2.isOdd) // false
메서드 추가
extension Int {
func multiply(by n: Int) -> Int {
return self * n
}
}
print(3.multiply(by: 2)) // 6
print(4.multiply(by: 5)) // 20
이니셜라이저 추가
extension String {
init(int: Int) {
self = "\(int)"
}
init(double: Double) {
self = "\(double)"
}
}
let stringFromInt: String = String(int: 100)
// "100"
Generic
제네릭 사용
class Name<T> {
}
struct Name<T> {
}
enum Name<T> {
}
func Name<T> {
}
- 제네릭 타입을 선언하여 내부에서 사용하는 타입을 일반화할 수 있다.
- 클래스, 구조체, 열거형, 함수에 적용할 수 있다.
- 이는 자바의 제네릭과 유사한 개념이라고 볼 수 있다.
타입 제약 (Type Constraints)
- 하지만 해당 타입은 컴파일 전에는 정확히 알 수 없다.
- 제네릭으로 들어올 타입에 제약을 걸어 들어올 수 있는 타입을 제한하는 것을 타입 제약이라고 한다.
- 이를 통해 어떤 타입이 들어올지 알 수 없는 제네릭에도 특정 작업을 수행할 수 있게 된다.
func makeSet<T: Comparable>(inputArray: [T]) -> [T] {
var result = [T]()
for element in inputArray {
if !result.contains(element) {
result.append(element)
}
}
return result.sorted()
}
- 예를 들어 위와 같은 함수의 경우,
!result.contains(element)
에서 Equatable 제약이 필요하고,result.sorted()
에서 Comparable 제약이 필요하다. 하지만 Comparable 제약을 만족하면 Equatable 제약도 만족하므로, Comparable 제약만 존재하면 된다. - 추가적으로 제약을 필요로 할 경우 함수 선언 부 뒤에
func makeSet<T: Comparable>(inputArray: [T]) -> [T] where T: FloatingPoint
와 같이 추가하면 된다.
'🍎 Apple > Swift' 카테고리의 다른 글
[Swift] 문자열 처리 (0) | 2022.01.22 |
---|---|
[Swift] NSCache와 NSDictionary의 차이점 (0) | 2022.01.22 |
[Swift] UserDefaults와 Codable, NSCoding (2) | 2022.01.01 |
[Swift] Dynamic Dispatch 줄이기 (0) | 2021.12.16 |
[Swift] Codable Protocol (2) | 2021.09.02 |