iOS/Swift 기초문법

Swift 문법 기초 2편

soultreemk 2022. 3. 27. 13:20

 

<타입 캐스팅>

인스턴스의 타입을 확인하거나 어떠한 클래스의 인스턴스를 

해당 클래스 계층구조의 슈퍼 클래스나 서브 클래스로 취급하는 방법

 

class MediaItem {
    var name: String
    init(name: String){
        self.name = name
    }
}

class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

// library는 타입추론을 통해 super class인 MediaItem으로 형변환됨
let library = [
    Movie(name: "기생충", director: "봉준호"),
    Song(name: "Butter", artist: "BTS"),
    Movie(name: "올드보이", director: "박찬욱"),
    Song(name: "Wonderfull", artist: "Oasis"),
    Song(name: "Rain", artist: "이적")
]

var movieCount = 0
var songCount = 0

// is 연산자를 통해 인스턴스의 클래스 파악 - moviCount는 2, songCount는 3
for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
}

// as?  - 다운캐스팅 (옵셔널 값 반환)
// library 내 인스턴스 들은 Movie class or Song class
// Movie or Song으로 다운캐스팅이 될수 있으면 해당 내용을 출력
for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name"), dir. \(movie.director)")
    }
   else if let song = item as? Song {
        print("Song: \(song.name"), dir. \(song.artist)")
    }
}
// as!  - 강제 언래핑 (항상 성공할 것이라는 확신이 있을 때 사용)

 

< assert문  /  guard문 >
  • assert문 
    - 특정 조건을 체크, 조건이 성립되지 않으면 메세지와 함께 에러를 발생시킴
    - 디버깅 모드에서만 동작. 디버깅 중 조건의 검증을 위하여 사용
  • guard문
    - 함수 내부에서 조건식으로 뭔가를 검사하여 그 다음에 오는 코드를 실행할지 말지 결정
    - gaurd문에서 주어진 조건문이 거짓일 때 구문이 실행
    - eary exit: 특정 조건문을 만족하지 않으면 이후 코드를 실행하지 않도록 방어코드로 활용
                      (잘못된 값이 함수에 들어오지 않기 위한 방어)
// assert함수
var value = 0
assert(value == 0)

value = 2
assert(value == 0, "값이 0이 아닙니다") // 런타임 에러. console창에 "Assertion faild: 값이 0이 아닙니다" 출력

guard문을 사용하면 옵셔널 바인딩 된 상수를 조건문 범위 밖에서도 사용 가능

func guardTest(value: Int) {
    guard value == 0 else {return}
    print("안녕하세요")
}

guardTest(value: 2) // guard문에 막혀서 아무것도 출력되지 않음 (함수 종료)
guardTest(value: 0) // guard문을 통과해서 그 다음 구문인 "안녕하세요"가 출력됨

// guard문을 통한 옵셔널 바인딩
func guardTest2(value: Int?) {
    guard let value = value else {return} // value에 값이 들어가 있으면 옵셔널 바인딩 해서 value상수에 넣기
    print(value)
}

guardTest2(value: 2) // 2가 출력됨
guardTest2(value: nil) // guard문에 막혀서 함수가 종료

 

<프로토콜>

기본 형식

protocol SomeProtocol {
    <#requirements#>
}

protocol SomeProtocol2 {
    <#requirements#>
}
// 구조체가 두개의 프로토콜을 따르도록 지정
struct SomeStruct: SomeProtocol, SomeProtocol2 {

}
// 1. 프로퍼티 요구
// 특정 프로토콜을 재택한 타입이 어떤 프로퍼티를 구현해야 하는지 요구할 수 있음
protocol FirstProtocol {
    var name: Int {get set}  // 읽기,쓰기 가능
    var age: Int {get}       // 읽기 전용
}
// 프로토콜에서 "타입 프로퍼티"를 요구하려면 항상 static 키워드를 붙여야 함
protocol AnotherProtocol {
   static var someTypeProperty: Int { get set }
}

// 예시) struct가 프로토콜을 준수
protocol FullyNames {
    var fullName: String { get set }
    func printFullName()  // 몸통부는 없어도 되지만 매개변수의 기본값 지정은 불가
}
// FullyNames 프로토콜이 요구하는 사항을 모두 충족하면 에러메시지 사라짐
struct Person: FullyNames {
    var fullName: String
    func printFullName() {
        print(fullName)
    }
}
// 2. initialize 요구
// 특정 프로토콜을 재택한 타입이 생성자도 준수하도록 요구 가능
protocol SomeProtocol {
    init(someParameter: Int)
}

protocol SomeProtocol2 {
    init()
}
// struct에서는 생성자 요구사항을 준수할때 required 식별자 필요X
// class에서는 생성자 요구사항을 준수할때 required 식별자가 필요
class SomeClass: SomeProtocol2 {
    required init() {
        
    }
}

 

< extension >

 - swift의 강력한 기능 중 하나

 - 기존의 클래스, 구조체, 열거형,  프로토콜에 새로운 기능을 추가하는 기능

1) 타입 프로퍼티 / 연산 인스턴스 프로퍼티

2) 타입 메서드 / 인스턴스 메서드

3) 이니셜라이저

4) 서브스크립트

5) 중첩 타입

6) 특정 프로토콜을 준수하도록 기능 추가

    * 새로운 기능을 추가할수는 있으나 기존 기능을 오버라이딩 하는건 불가

 

(저장 프로퍼티나 감시자는 추가 X)

// 예시1) int형 타입에 연산 프로퍼티 추가
extension Int {
    var isEven: Bool { // 짝수인지 판단
        return self % 2 == 0
    }
    var isOdd: Bool {
        return self % 2 == 1
    }
}

var number = 3
number.isOdd  // true
number.isEven // false



// 예시2) 메소드 기능 추가 : 문자열을 int형으로 바꿔주는 기능
extension String {
    func convertToInt() -> Int? {
        return Int(self)
    }
}
var string = "0"
string.convertToInt()  // "0"문자열이 int형 0 으로 변환됨

 

< 열거형 >

- 연관성이 있는 값들을 모아놓은 것

 (ex. 봄/여름/가을/겨울 -> 계절)

enum CompassPoint {
    case north
    case south
    case east
    case west
    // case north, south, east, west 이렇게도 사용 가능
}

var direction = CompassPoint.east
// 값 변경 시 .만 입력해도 관련 변수들 등장
// 컴파일러가 direction변수는 CompassPoint열거형 타입이라는 것을 추론하기 때문
// 열거형 이름을 생략하고 내부 항목 이름만으로도 사용 가능
direction = .west

// 활용예시) switch함수
switch direction {
case .north:
    print("north")
case .south:
    print("south")
case .east:
    print("east")
case .west:
    print("west")
}
// 각 항목이 특정 타입의 값을 원시값으로 갖도록 지정 가능
// CompassPoint2 열거형의 각 항목이 string형의 초기값을 갖도록 지정
enum CompassPoint2: String {
    case north = "북"
    case south = "남"
    case east = "동"
    case west = "서"
}

var direction2 = CompassPoint2.north

// 원시값을 가져올땐 rawValue사용
switch direction2 {
case .north:
    print(direction2.rawValue)
case .south:
    print(direction2.rawValue)
case .east:
    print(direction2.rawValue)
case .west:
    print(direction2.rawValue)
}
// 각 항목이 특정 타입의 값을 원시값으로 갖도록 지정 가능
// CompassPoint2 열거형의 각 항목이 string형의 초기값을 갖도록 지정
enum CompassPoint2: String {
    case north = "북"
    case south = "남"
    case east = "동"
    case west = "서"
}

// 1) 원시값을 가지고 열거형 반환하기
let direction3 = CompassPoint2(rawValue: "남")
// direction3 상수에 south가 들어감

// 2) 열거형 항목에 연관값 추가
enum PhoneError {
    case unKnown
    case batteryLow(String)
}

let error = PhoneError.batteryLow("배터리가 곧 방전됨")
// batteryLow("배터리가 곧 방전됨")  출력


// 3) 연관값만 추출하려면 switch문 활용
switch error {
case .batteryLow(let message):
    print(message)
    
case .unKnown:
    print("알 수 없는 에러입니다.")
}
// "배터리가 곧 방전됨" 만 출력됨

 

<  옵셔널 체이닝 >

옵셔널에 속해 있는 nil 일지도 모르는 프로퍼티, 메서드, 서브스크립션 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정

- 옵셔널에 값이 있으면 프로퍼티, 메소드 등 호출 O

- 옵셔널이 nil이면 프로퍼티, 메소드는 nil 반환

struct Developer {
    let name: String
}

struct Company {
    let name: String
    let developer: Developer? // 옵셔널값
}

var company = Company(name:"gunter", developer: nil)
print(company.developer) // nil 출력

var developer = Developer(name: "yang")
var company2 = Company(name: "gunter", developer: developer)
print(company2.developer) // optional에 감싸진 developer 출력
print(company2.developer.name)
// 에러 발생: optional에 감싸져있기 때문에 내부프로퍼티 name에 접근 불가 -> 옵셔널을 벗겨야함(옵셔널바인딩 or 옵셔널 체이닝)

// 옵셔널 바인딩 말고 옵셔널 체이닝을 통해 값 벗겨내기
print(company2.developer?.name) // optional에 감싸진 yang
print(company2.developer!.name) // yang (강제언래핑 하므로)

 

< try catch 문 >

- 프로그램 내에서 에러가 발생한 상황에 대해 대응하고 복구하는 과정

- swift에서는 런타임 에러 발생한 경우 이를 처리하기 위한 4가지 일급클래스를 제공

1)  발생(throwing)

2) 감지(catching)

3) 전파(propagation)

4) 조작(manipulating)

 

- swift에서 error는 Error프로토콜을 따르는 타입의 값으로 표현됨

- Error프로토콜은 요구사항이 없는 빈 프로토콜이지만 오류를 표현하기 위해서는 이 프로토콜을 채택해야 함

// 오류가 발생했을 때 정상적으로 실행할 수 없다는 것을 나타내기 위해 throw 구문 사용
// 에러가 발생할 것 같은 지점에 throw를 던져 에러를 발생시킴
throw PhoneError.batteryLow(batteryLevel: 20)

 

  • 오류 처리 방법

1) 함수에서 발생한 오류를 해당 함수를 호출한 코드에 전파

2) do-catch 구문

3) try?를 통해 오류를 optional값 으로 처리 

4) 오류가 발생하지 않을것이라고 확신

// 오류가 던져진 것에 대비하여, 던져진 오류를 처리하기 위한 대비 코드
// 오류 처리 4가지 방법

// 1) 함수에서 발생한 오류를 해당 함수를 호출한 코드에 전파
// 오류가 발생할 수 있음을 나타내기 위해 함수(매개변수) 뒤에 throws
func checkPhoneBatteryStatus(batteryLevel: Int) throws -> String {
    // 오류가 throwing 되면 String 변수 반환
    guard batteryLevel != -1 else { throw PhoneError.unknown }
    guard batteryLevel <= 20 else { throw PhoneError.batteryLow(batteryLevel: 20)}
    
    return "배터리 상태가 정상입니다."
}
// 2) do-catch문으로 에러를 처리
/*
do {
 try 오류 발생 가능 코드
} catch 오류 패턴 {
 처리 코드
}
*/

// checkPhoneBatteryStatus메소드가 오류를 발생시켜도 catch문이 오류를 잡아 적절한 예외 처리 가능
do {
    try checkPhoneBatteryStatus(batteryLevel: -1)
} catch PhoneError.unknown {
    print("알 수 없는 에러입니다.")
} catch PhoneError.batteryLow(let batteryLevel) {
    print("배터리 전원 부족, 남은 배터리: \(batteryLevel)")
} catch {
    print("그 외 오류 발생: \(error)")
}
// 2) try? 옵셔널 값 변경
let status = try? checkPhoneBatteryStatus(batteryLevel: -1)
print(status) // 메소드가 unknown을 반환하므로 status는 nil

// 메소드가 에러를 던져주지 않는다면 optional값이 출력됨
let status2 = try? checkPhoneBatteryStatus(batteryLevel: 30)
print(status2)
// Optional("배터리 상태가 정상입니다.")


// try!  :  오류가 절대 발생하지 않을것이라고 확신할때
// 메소드가 오류를 발생하면 런타임에러, 프로그램 종료되므로 유의해서 사용
let status3 = try! checkPhoneBatteryStatus(batteryLevel: 30)

 

< 클로저 >

- 코드에서 전달 및 사용할 수 있는 독립 기능 블록이며, 일급 객체의 역할을 할 수 있음

  * 일급객체란? 전달 인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며 함수의 반환값이 될 수도 있음

- 보통 unnamed closure를 칭함

- unnamed라서 func표현식 사용

- closure head와 closure body를 in 키워드로 구분

let hello = { () -> () in
    print("hello")
    
}
// 클로저 호출
hello()


let hello2 = { (name: String) -> String in
    return "Hello, \(name)"
}

// 전달인자 레이블을 넣지 않고 바로 파라미터 값을 전달해야함
hello2(name: "Gunter") // 에러
hello2("Gunter")


// 클로저를 함수 파라미터로 전달
func doSomething(closure:() -> ()) {
    closure()
}

doSomething(closure: { () -> () in
    print("hello")
})


// 클로저를 함수 반환값으로 사용
func doSomething2() -> () -> () {
    return { () -> () in
        print("hello4")
    }
}

doSomething2()()

- 클로저를 간단하게 표현하는 법

// 후행 클로저를 통해 읽기 쉽게 변환
// int형 3개를 매개변수로 받고 int형을 반환하는 클로저
func doSomething(closure: (Int, Int, Int) -> Int) {
    closure(1,2,3)
}

doSomething(closure: { (a, b, c) in
    return a+b+c
})

doSomething(closure: {
    return $0+$1+$2
})

// 단일 return문은 return도 생략 가능
doSomething() {
    $0+$1+$2
}
// 단 하나의 클로저만 매개변수로 전달하면 ()도 생략 가능
doSomething {
    $0+$1+$2
}

 

< 고차함수 >

swift가 일급객체 함수라서 가능한 기능 중 하나

- 다른 함수를 전달 인자로 받거나, 함수 실행 결과를 함수로 반환하는 함수

- map, filter, reduce

// 1) map : 컨테이너 내부의 기존 데이터를 변형하여 새로운 컨테이너 생성
let numbers = [0,1,2,3]
let mapArray = numbers.map { (number) -> Int in
    return number * 2
}
print(mapArray) // [0,2,4,6]

// 2) filter : 컨테이너 내부의 데이터를 걸러서 새로운 컨테이너로 추출
let intArray = [10,5,20,13,4]
let filterArray = intArray.filter{ $0 > 5 }
print(filterArray) // [10,20,13]

// 3) reduce : 컨테이너 내부 데이터를 하나로 통합
// result: 누적 값, element:  각 요소
let someArray = [1,2,3,4,5]
let reduceResult = someArray.reduce(0) { // 괄호안은 result의 초기값
    (result: Int, element: Int) -> Int in
    print("\(result) + \(element)")
    return result + element
}
/*
 0+1
 1+2
 3+3
 6+4
 10+5