/ IOS

[iOS] Swift 기본 (2)

5. 클래스와 구조체

클래스는 class로 정의하고, 구조체는 struct로 정의합니다.

  class Dog { 
    var name: String? 
    var age: Int? 
    func simpleDescription() -> String { 
      if let name = self.name { 
        return"🐶 \(name)" 
      } else { 
        return"🐶 No name" 
      } 
    } 
  }
  struct Coffee { 
    var name: String? 
    var size: String? 
    func simpleDescription() -> String { 
      if let name = self.name { 
        return"☕️ \(name)" 
      } else { 
        return"☕️ No name" 
      } 
    } 
  } 
  var myDog = Dog() myDog.name = "찡코" 
  myDog.age = print(myDog.simpleDescription()) // 🐶 찡코
  var myCoffee = Coffee() myCoffee.name = "아메리카노" 
  myCoffee.size = "Venti"print(myCoffee.simpleDescription()) // ☕️ 아메리카노

클래스는 상속이 가능합니다. 구조체는 불가능합니다.

  class Animal { 
    let numberOfLegs = 4 
  } 
  class Dog: Animal { 
    var name: String? 
    var age: Int? 
  } 
  var myDog = Dog() 
  print(myDog.numberOfLegs) // Animal 클래스로부터 상속받은 값 (4)

클래스는 참조Reference하고, 구조체는 복사Copy합니다.

  var dog1 = Dog()  // dog1은 새로 만들어진 Dog()를 참조합니다.
  var dog2 = dog1   // dog2는 dog1이 참조하는 Dog()를 똑같이 참조합니다. 
  dog1.name = "찡코"// dog1의 이름을 바꾸면 Dog()의 이름이 바뀌기 때문에,
  print(dog2.name)  // dog2의 이름을 가져와도 바뀐 이름("찡코")이 출력됩니다.
  var coffee1 = Coffee()   // coffee1은 새로 만들어진 Coffee() 그 자체입니다.
  var coffee2 = coffee1    // coffee2는 coffee1을 복사한 값 자체입니다. 
  coffee1.name = "아메리카노"// coffee1의 이름을 바꿔도 
  coffee2.name             // coffee2는 완전히 별개이기 때문에 이름이 바뀌지 않습니다. (nil)

생성자 (Initializer) 클래스와 구조체 모두 생성자를 가지고 있습니다. 생성자에서는 속성의 초깃값을 지정할 수 있습니다.

  class Dog { 
    var name: String? 
    var age: Int? 
    init() { 
      self.age = 0 } 
    } 
  }
  struct Coffee { 
    var name: String? 
    var size: String? 
    init() { 
        self.size = "Tall" 
      } 
  } 

만약 속성이 옵셔널이 아니라면 항상 초깃값을 가져야 합니다. 만약 옵셔널이 아닌 속성이 초깃값을 가지고 있지 않으면 컴파일 에러가 발생합니다.

  class Dog { 
    var name: String? 
    var age: Int// 컴파일 에러! 
  } 
  stored property 'age' without initial value prevents synthesized initializers

속성을 정의할 때 초깃값을 지정해 주는 방법과,

  class Dog { 
    var name: String? 
    var age: Int = 0// 속성을 정의할 때 초깃값 지정 
  } 

생성자에서 초깃값을 지정해주는 방법이 있습니다.

  class Dog { 
    var name: String? 
    var age: Int 
    init() { 
      self.age = 0// 생성자에서 초깃값 지정
    } 
  } 

생성자도 함수와 마찬가지로 파라미터를 받을 수 있습니다.

  class Dog { 
    var name: String? 
    var age: Int 
    init(name: String?, age: Int) { self.name = name self.age = age } 
  } 
  var myDog = Dog(name: "찡코", age: 3) 

만약 상속받은 클래스라면 생성자에서 상위 클래스의 생성자를 호출해주어야 합니다. 만약 생성자의 파라미터가 상위 클래스의 파라미터와 같다면, override 키워드를 붙여주어야 합니다. super.init()은 클래스 속성들의 초깃값이 모두 설정 된 후에 해야 합니다. 그리고 나서부터 자기 자신에 대한 self 키워드를 사용할 수 있습니다.

  class Dog: Animal { 
  var name: String? 
  var age: Int
  overrideinit() { 
    self.age = 0// 초깃값 설정
    super.init() // 상위 클래스 생성자 호출
    print(self.simpleDescription()) // 여기서부터 `self` 접근 가능 
  } 
  func simpleDescription() -> String { 
    if let name = self.name { 
      return"🐶 \(name)" 
    } else { 
        return"🐶 No name" 
      } 
    } 
  } 

만약, 위 예시 코드를 아래처럼 바꿔서 super.init()을 하기 전에 self에 접근한다면 컴파일 에러가 발생합니다.

  overrideinit() { 
    self.age = print(self.simpleDescription()) // 컴파일 에러!
    super.init() 
  }  

error: use of ‘self’ in method call ‘simpleDescription’ before super.init initializes self deinit은 메모리에서 해제된 직후에 호출됩니다.

  class Dog { ...deinit { print("메모리에서 해제됨") } } 

속성 (Properties) 속성은 크게 두 가지로 나뉩니다. 값을 가지는 속성Stored Property과 계산되는 속성Computed Property 입니다. 쉽게 말하면 속성이 값 자체를 가지고 있는지, 혹은 어떠한 연산을 수행한 뒤 그 결과를 반환하는지의 차이입니다. 우리가 지금까지 정의하고 사용한 name, age와 같은 속성들은 모두 Stored Property입니다. Computed Property는 get, set을 사용해서 정의할 수 있습니다. set에서는 새로 설정될 값을 newValue라는 예약어를 통해 접근할 수 있습니다.

  struct Hex { 
    var decimal: Int? 
    var hexString: String? { 
      get { 
        if let decimal = self.decimal { 
          returnString(decimal, radix: 16) 
        } else { 
          return nil 
        } 
      } set { 
        if let newValue = newValue { 
          self.decimal = Int(newValue, radix: 16) 
        } else { 
          self.decimal = nil 
        } 
      } 
    } 
  } 
  var hex = Hex() 
  hex.decimal = 10 
  hex.hexString // "a" 
  hex.hexString = "b" 
  hex.decimal // 11

위 코드에서 hexString은 실제 값을 가지고 있지는 않지만, decimal로부터 값을 받아와 16진수 문자열로 만들어서 반환합니다. decimal은 Stored Property, hexString은 Computed Property입니다. 참고로, get만 정의할 경우에는 get 키워드를 생략할 수 있습니다. 이런 속성을 읽기 전용Read Only이라고 합니다.

  class Hex { ...var hexCode: String? { if let hex = self.hexString { return "0x" + hex } return nil } } 

get, set과 비슷한 willSet, didSet을 이용하면 속성에 값이 지정되기 직전과 직후에 원하는 코드를 실행할 수 있습니다.

  struct Hex { 
    var decimal: Int? { 
      willSet {
        print("\(self.decimal)에서 \(newValue)로 값이 바뀔 예정입니다.") 
      } didSet { 
        print("\(oldValue)에서 \(self.decimal)로 값이 바뀌었습니다.") 
      } 
    } 
  } 

마찬가지로, willSet에서는 새로운 값을 newValue로 얻어올 수 있고, didSet에서는 예전 값을 oldValue라는 예약어를 통해 얻어올 수 있습니다. willSet과 didSet은 일반적으로 어떤 속성의 값이 바뀌었을 때 UI를 업데이트하거나 특정 메서드를 호출하는 등의 역할을 할 때에 사용됩니다.

6. 튜플

튜플Tuple은 어떠한 값들의 묶음입니다. 배열과 비슷한 개념이지만, 배열과는 다르게 길이가 고정되어 있습니다. 값에 접근할 때에도 [] 대신 .을 사용합니다.

  var coffeeInfo = ("아메리카노", 5100) 
  coffeeInfo.0// 아메리카노 
  coffeeInfo.1// 5100 
  coffeeInfo.1 = 5100

이 튜플의 파라미터에 이름을 붙일 수도 있습니다.

  var namedCoffeeInfo = (coffee: "아메리카노", price: 5100) 
  namedCoffeeInfo.coffee // 아메리카노 
  namedCoffeeInfo.price // 5100 
  namedCoffeeInfo.price = 5100

이렇게 보면, 앞서 살펴본 구조체와 비슷한데, 실제로도 간단한 자료형을 만들 때에는 구조체 대신 튜플을 사용하기도 합니다. 튜플의 타입 어노테이션은 이렇게 되어있습니다.

  var coffeeInfo: (String, Int) 
  var namedCoffeeInfo: (coffee: String, price: Int) 

튜플을 조금 응용하면, 아래와 같이 여러 변수에 값을 지정할 수도 있습니다.

  let (coffee, price) = ("아메리카노", 5100) 
  coffee // 아메리카노 
  price // 5100

튜플이 가진 값을 가지고 변수에 값을 지정할 때, 무시하고 싶은 값이 있다면 _ 키워드를 사용해서 할 수 있습니다. 아래 코드에서는 “라떼”라는 첫 번째 값을 무시합니다.

  let (_, latteSize, lattePrice) = ("라떼", "Venti", 5600) 
  latteSize // Venti 
  lattePrice // 5600

물론, 튜플을 반환하는 함수도 만들 수 있습니다.

  /// 커피 이름에 맞는 커피 가격 정보를 반환합니다. 일치하는 커피 이름이 없다면 `nil`을 반환합니다.////// - parameter name: 커피 이름////// - returns: 커피 이름과 가격 정보로 구성된 튜플을 반환합니다.
  func coffeeInfo(for name: String) -> (name: String, price: Int)? { 
    let coffeeInfoList: [(name: String, price: Int)] = [ ("아메리카노", 5100), ("라떼", 5600), ] 
      for coffeeInfo in coffeeInfoList { 
        if coffeeInfo.name == name { 
          return coffeeInfo 
        } 
      } 
    return nil 
  }
  coffeeInfo(for: "아메리카노")?.price // 5100 
  coffeeInfo(for: "에스프레소")?.price // nil
  let (_, lattePrice) = coffeeInfo(for: "라떼")! lattePrice // 5600 

7. Enum

열거라는 뜻을 가진 Enumeration에서 따온 용어입니다. 한글로 번역할 때에는 열거형이라는 말을 많이 사용합니다. 1월부터 12월까지를 enum으로 한 번 정의해보겠습니다.

  enum Month: Int { 
    case january = 1
    case february 
    case march 
    case april 
    case may 
    case june 
    case july 
    case august 
    case september 
    case october 
    case november 
    case december 
    func simpleDescription() -> String { 
      Switch self { 
        case .january: return"1월"
        case .february: return"2월"
        case .march: return"3월"
        case .april: return"4월"
        case .may: return"5월"
        case .june: return"6월"
        case .july: return"7월"
        case .august: return"8월"
        case .september: return"9월"
        case .october: return"10월"
        case .november: return"11월"
        case .december: return"12월" 
      } 
    } 
  } 
  let december = Month.december print(december.simpleDescription()) // 12월
  print(december.rawValue)            // 12

위 예시에서 작성한 Month는 Int를 원시값Raw Value으로 가지도록 정의되었습니다. 그렇기 때문에 각 케이스들은 1부터 12까지의 값을 가지고 있습니다. rawValue 속성이 바로 그 값을 나타내는데, 반대로 원시값을 가지고 Enum을 만들 수도 있습니다.

  let october = Month(rawValue: 10) print(october) // Optional(Month.october)

Month(rawValue:)의 반환값이 옵셔널인 이유는, Enum에서 정의되지 않은 원시값을 가지고 생성할 경우 nil을 반환하기 때문입니다.

  Month(rawValue: 13) // nil

일반적으로 Enum은 Int만을 원시값으로 가질 수 있다고 생각합니다. 다른 프로그래밍 언어에서는 모두 그렇기 때문입니다. 하지만, Swift의 Enum은 조금 독특합니다. 아래 예시는 String을 원시값으로 가지는 Enum입니다.

  enum IssueState: String { case open = "open"case closed = "closed" } 

만약 어떤 API의 응답에서 내려주는 state의 값이 open 또는 closed라면, if-else 없이도 IssueState(rawValue:)를 사용해서 Enum을 생성할 수 있습니다. Enum은 원시값을 가지지 않을 수도 있습니다. 원시값을 가져야 할 필요가 없다면 굳이 만들지 않아도 됩니다.

  enum Spoon { 
    case dirt 
    case bronze 
    case silver 
    case gold 
    func simpleDescription() -> String { switch self { 
      case .dirt: return"흙수저"
      case .bronze: return"동수저"
      case .silver: return"은수저"
      case .gold: return"금수저" } 
    } 
  } 

Enum을 예측할 수 있다면 Enum의 이름을 생략할 수 있습니다.

  let spoon: Spoon = .gold // 변수에 타입 어노테이션이 있기 때문에 생략 가능
  func doSomething(with spoon: Spoon) { /* ... */}  
  doSomething(with: .silver) // 함수 정의에 타입 어노테이션이 있기 때문에 생략 가능

연관 값(Associated Values)을 가지는 Enum

Enum은 연관 값Associated Values을 가질 수 있습니다. 아래 예시는 어떤 API에 대한 에러를 정의한 것인데요. invalidParameter 케이스는 필드 이름과 메시지를 가지도록 정의되었습니다.

  enum NetworkError { case invalidParameter(String, String) case timeout } 
  let error: NetworkError = .invalidParameter("email", "이메일 형식이 올바르지 않습니다.") 

이 값을 꺼내올 수 있는 방법으로는 if-case 또는 switch를 활용하는 방법이 있습니다.

  if case .invalidParameter(let field, let message) = error {
    print(field) // email
    print(message) // 이메일 형식이 올바르지 않습니다. 
  } switch error { 
    case .invalidParameter(let field, let message): print(field) // email
    print(message) // 이메일 형식이 올바르지 않습니다.default: break 
  } 
  /*응용하기: NetworkError에 message라는 읽기 전용 속성을 추가하고, 에러에 대한 명확한 메시지를 반환하도록 만들어봅시다. 더 나아가서, 있을법한 다른 에러에 대한 경우도 추가해봅시다.*/

사실, 옵셔널은 Enum입니다. 실제 구조도 다음과 같습니다.

  public enum Optional<Wrapped> { case none case some(Wrapped) }

옵셔널이 왜 ‘값’과 ‘없는 값’을 포함하고 있다고 설명했는지, 그리고 왜 ‘감싸다’라는 표현을 사용했는지 이해가 되십니까? 옵셔널은 Enum이기 때문에, 아래와 같은 구문도 사용할 수 있습니다.

  let age: Int? = 20
  switch age { 
    case .none: // `nil`인 경우print("나이 정보가 없습니다.") 
    case .some(let x) where x < 20: print("청소년") 
    case .some(let x) where x < 65: print("성인") 
    default: print("어르신") 
  }

8. 프로토콜

프로토콜Protocol은 인터페이스입니다. 최소한으로 가져야 할 속성이나 메서드를 정의합니다. 구현은 하지 않습니다. 진짜로 정의만 합니다.

  /// 전송가능한 인터페이스를 정의합니다.
  protocol Sendable { var from: String? { get } var to: String { get } func send() } 

클래스와 구조체에 프로토콜을 적용Conform시킬 수 있습니다. 프로토콜을 적용하면, 프로토콜에서 정의한 속성와 메서드를 모두 구현해야 합니다.

  struct Mail: Sendable { 
    var from: String? 
    var to: String
    func send() { 
      print("Send a mail from \(self.from) to \(self.to)") 
    } 
  } 
  struct Feedback: Sendable { 
    var from: String? { return nil// 피드백은 무조건 익명으로 보냅니다. 
  } 
  var to: String
  func send() { print("Send a feedback to \(self.to)") } } 

프로토콜은 마치 추상클래스처럼 사용될 수 있습니다.

  func sendAnything(_ sendable: Sendable) { 
    sendable.send() 
  } 
  let mail = Mail(from: "ac85kr@gmail.com", to: "gksxowns93@ohcomon.com") 
  sendAnything(mail) 
  let feedback = Feedback(from: "ac85kr@gmail.com") 
  sendAnything(feedback) 

sendAnything() 함수는 Sendable 타입을 파라미터로 받습니다. Mail와 Feedback은 엄연히 다른 타입이지만, 모두 Sendable을 따르고 있으므로 sendAnything() 함수에 전달될 수 있습니다. 그리고, Sendable에서는 send() 함수를 정의하고 있기 때문에 호출이 가능합니다. 프로토콜은 또다른 프로토콜을 따를 수 있습니다.

  protocol Messagable { var message: String? { get } } protocol Sendable: Messagable { ... } 

Sendable은 Messagable을 기본적으로 따르는 프로토콜입니다. 따라서, Sendable을 적용하려면 var message: String? { get }을 정의해주어야 합니다.

Any는 모든 타입에 대응합니다. AnyObject는 모든 객체Object에 대응합니다.

  let anyNumber: Any = 10
  let anyString: Any = "Hi"
  let anyInstance: AnyObject = Dog() 

Any와 AnyObject는 프로토콜입니다. Swift에서 사용 가능한 모든 타입은 Any를 따르도록 설계되었고, 모든 클래스들에는 AnyObject 프로토콜이 적용되어있습니다.

타입 캐스팅 (Type Casting) anyNumber에 10을 넣었다고 해서 anyNumber가 Int는 아닙니다. ‘Any 프로토콜을 따르는 어떤 값’이기 때문입니다.

  anyNumber + 1// 컴파일 에러!

이럴 때에는 as를 이용해서 다운 캐스팅Down Casting을 해야 합니다. Any는 Int보다 더 큰 범위이기 때문에, 작은 범위로 줄인다고 하여 ‘다운 캐스팅’입니다. Any는 Int 뿐만 아니라 String과 같은 전혀 엉뚱한 타입도 포함되어 있기 때문에 무조건 Int로 변환되지 않습니다. 따라서 as?를 사용해서 옵셔널을 취해야 합니다.

  let number: Int? = anyNumber as? Int

옵셔널이기 때문에, 옵셔널 바인딩 문법도 사용할 수 있습니다. 실제로 이렇게 사용하는 경우가 굉장히 많습니다.

  if let number = anyNumber as? Int { print(number + 1) } 

타입 검사 타입 캐스팅까지는 필요 없고, 만약 어떤 값이 특정한 타입인지를 검사할 때에는 is를 사용할 수 있습니다.

  print(anyNumber isInt)    // true
  print(anyNumber isAny)    // true
  print(anyNumber isString) // false
  print(anyString isString) // true

Swift 주요 프로토콜 Swift에는 기본적으로 제공하는 기초적인 프로토콜들이 있습니다. 알아두면 개발할 때 굉장히 유용하게 사용할 수 있습니다. CustomStringConvertible 자기 자신을 표현하는 문자열을 정의합니다. print(), String() 또는 “()”에서 사용될 때의 값입니다. CustomStringConvertible의 정의는 아래와 같이 생겼습니다.

  public protocol CustomStringConvertible { /// A textual representation of `self`.
    public var description: String { get } 
  } 

실제로 적용시…

  struct Dog: CustomStringConvertible { 
    var name: Stringvar description: String { 
      return"🐶 \(self.name)" 
    } 
  } 
  let dog = Dog(name: "찡코") 
  print(dog) // 🐶 찡코

ExpressibleBy 우리는 지금까지 10은 Int, “Hi”는 String이라고 ‘당연하게’ 인지하고 있었습니다. 하지만, 엄밀히 하자면 10은 원래 Int(10)으로 선언되어야 하고, “Hi”는 String(“Hi”)로 선언되어야 합니다. Int와 String 모두 생성자를 가지는 구조체이기 때문입니다. 이렇게, 생성자를 사용하지 않고도 생성할 수 있게 만드는 것을 리터럴Literal이라고 합니다. 직역하면 ‘문자 그대로’라는 뜻입니다. 아래 코드는 문자 그대로 10, 문자 그대로 “Hi”, 문자 그대로 배열이고 딕셔너리입니다.

  let number = 10
  let string = "Hi"
  let array = ["a", "b", "c"] 
  let dictionary = [ "key1": "value1", "key2": "value2", ] 

이 리터럴을 가능하게 해주는 프로토콜이 바로 ExpressibleByXXXLiteral 입니다. Int는 ExpressibleByIntegerLiteral을, String은 ExpressibleByStringLiteral을, Array는 ExpressibleByArrayLiteral을, Dictionary는 ExpressibleByDictionaryLiteral 프로토콜을 따르고 있습니다. 각 프로토콜은 리터럴 값을 받는 생성자를 정의하고 있습니다.

  struct DollarConverter: ExpressibleByIntegerLiteral { 
    type alias IntegerLiteralType = Int
    let price = 1_177
    var dollars: Int init(integerLiteral value: IntegerLiteralType) { self.dollars = value * self.price } 
  } 
  let converter: DollarConverter = 100 converter.dollars // 117700

Tip: typealias는 C의 typedef와 같습니다. typealias MyInt = Int라고 하면, 새로 생성된 MyInt는 Int와 완전히 동일한 타입입니다. 프로토콜에서도 typealias를 정의할 수 있습니다.

Tip: 1177은 가독성을 위해 1_177로 쓸 수 있습니다. 12_345는 12345랑 같아요. 1234_5도 12345와 같습니다. 분명히 구조체를 만들었는데, ExpressibleByIntegerLiteral을 적용하니까 = 100과 같은 문법을 사용할 수 있게 되었습니다.

  let oddEvenFilter: OddEvenFilter = [1, 3, 5, 2, 7, 4] 
  oddEvenFilter.odds  // [1, 3, 5, 7] 
  oddEvenFilter.evens // [2, 4] 

9. 익스텐션

Swift에서는 이미 정의된 타입에 새로운 속성이나 메서드를 추가할 수 있습니다. 익스텐션Extension이라는 기능인데, extension 키워드를 사용해서 정의할 수 있습니다.

  extension String { 
  var length: Int { return self.characters.count } 
    func reversed() -> String { 
      return self.characters.reversed().map { String($0) }.joined(separator: "") 
    } 
  } 
  let str = "안녕하세요" str.length // 5 str.reversed() // 요세하녕안
  //응용하기: 거꾸로 된 문자열을 반환하는 대신에, 자기 자신을 거꾸로 바꿔버리는 reverse().. 힌트: mutating 키워드와 self 

여기까지 Swift 언어의 기본 및 사용방법에 대해 알아보았으며, 다음부터는 코르도바를 활용한 블루투스 프로젝트 생성 예제에 대해 포스트 해보겠습니다. 감사합니다.

XCODE, SWIFT, IOS