안녕하세요. 엘림입니다🙇🏻‍♀️

Swift 공식 문서 정독하기 시리즈입니다!

제 스타일대로 정리했으니 추가적으로 더 필요한 정보는
공식문서 링크를 눌러 확인해주세용!

좀 더 편하게 보기위해 한국어로 번역된 사이트를 함께 확인했습니다!ㅎㅎ

자, 그럼 시작해볼까요

이 글은 공부하면서 작성한 글이기 때문에 잘못된 정보가 있을 수 있습니다.🥺
금방 잊어버릴... 미래의 저에게 다시 알려주기 위한 글이다보니
혹시라도 틀린 부분이 있다면, 댓글로 친절하게 알려주시길 부탁드립니다.🙏


프로퍼티

프로퍼티는 클래스, 구조체, 열겨형과 관련한 값입니다.

  • 저장 프로퍼티 : 값을 저장하고 있는 프로퍼티(클래스와 구조체에서만 사용 - 열거형에서는 타입 프로퍼티만 사용 가능)
  • 연산 프로퍼티 : 값을 저장하고 있지 않고, 특정하게 계산한 값을 반환해주는 프로퍼티

저장 프로퍼티

단순히 값을 저장하고 있는 프로퍼티입니다.

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 범위 값은 0, 1, 2 입니다.
rangeOfThreeItems.firstValue = 6
// 범위 값은 6, 7, 8 입니다.

상수 구조체의 인스턴스 저장 프로퍼티

구조체 자체를 상수로 선언하면, 그 구조체 내부의 인스턴스 프로퍼티를 변경할 수 없습니다.

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 범위 값은 0, 1, 2, 3 입니다.
rangeOfFourItems.firstValue = 6
// 에러 발생!

rangeOfFourItems는 상수(let)로 선언되었기 때문에 프로퍼티를 변경할 수 없습니다. 반면 구조체가 아니라 클래스는 let으로 선언하더라도 프로퍼티가 변경 가능합니다. 이유는 클래스 인스턴스는 참조 타입 이기 때문입니다.

lazy(지연) 프로퍼티

값이 처음으로 사용 되기 전에는 계산되지 않는 프로퍼티 입니다. (지연 프로퍼티는 반드시 변수(var)로 선언해야 합니다. 왜냐하면 상수는 초기화가 완료 되기전에 값을 가지고 있어야 하는 프로퍼티인데, 지연 프로퍼티는 처음 사용되기 전에는 값을 갖지 않는 프로퍼티이기 때문입니다.)

lazy 프로퍼티는 프로퍼티가 특정 요소에 의존적이어서 그 요소가 끝나기 전에 적절한 값을 알지 못하는 경우에 유용합니다. 또 복잡한 계산이나 부하가 많이 걸리는 작업을 지연 프로퍼티로 선언해 사용하면 실제 사용되기 전에는 실행되지 않아서 인스턴스의 초기화 시점에 복잡한 계산을 피할 수 있습니다.

class DataImporter {
    /*
        DataImporter는 외부 파일에서 데이터를 가져오는 클래스입니다.
         이 클래스는 초기화 하는데 매우 많은 시간이 소요된다고 가정하겠습니다.
     */
    var filename = "data.txt"
    // 데이터를 가져오는 기능의 구현이 이 부분에 구현돼 있다고 가정
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // 데이터를 관리하는 기능이 이 부분에 구현돼 있다고 가정
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 인스턴스는 이 시점에 생성돼 있지 않습니다

print(manager.importer.filename)
// the DataImporter 인스턴스가 생성되었습니다.
// "data.txt" 파일을 출력합니다.

DataManager라는 클래스는 데이터를 가져오는 DataImporter클래스를 갖고 있습니다. 그리고 이 DataImporter는 초기화시 많은 시간이 소요됩니다. 그래서 이 클래스를 지연 프로퍼티로 선언합니다. 이 프로퍼티는 인스턴스 manager를 생성하고 거기에 data를 넣어도, 그 시점에 DataImporter인스턴스는 생성돼 있지 않습니다. 다시 말하면 지연 프로퍼티로 선언해 놓았기 때문에 실제 그 프로퍼티를 사용하기 전에는 복잡하고 시간일 오래 소요되는 연산을 할 필요가 없다는 것입니다.
그리고 결국 manager.importer.filename가 실행돼 실제 importer 프로퍼티에 처음 접근할 때 비로소 importer인스턴스는 생성됩니다.

만약 지연 프로퍼티가 여러 스레드에서 사용되면 지연 프로퍼티가 한번만 실행되는 걸 보장하지 않습니다. 만약 지연 프로퍼티가 단일 스레드에서 사용되면 초기화는 한번만 하게 됩니다.

연산 프로퍼티

연산 프로퍼티는 실제 값을 저장하고 있는 것이 아니라, getter와 optional한 setter를 제공해 값을 탐색하고 간접적으로 다른 프로퍼티 값을 설정할 수 있는 방법을 제공합니다.

struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
                  size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// "square.origin is now at (10.0, 10.0)" 출력

연산 프로퍼티는 값을 직접 가지고 있는 것이 아니라, 연산을 통해서 구할수 있습니다.(GET)

인자 처리

앞의 코드에서는 Setter의 인자 이름을 아래와 같이 set(newCenter)라고 명시했지만, 만약 이렇게 (newCenter)라고 인자 이름을 지정하지 않으면 인자 기본 이름인 newValue를 사용할 수 있습니다.

struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY) // return 생략 가능
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

읽기 전용 연산 프로퍼티

getter만 있고 setter를 제공하지 않는 계산된 프로퍼티를 읽기전용 계산된 프로퍼티라고 합니다. 즉, 읽기전용 계산된 프로퍼티는 반드시 반환 값을 제공하고 다른 값을 지정할 수는 없는 프로퍼티 입니다.
(계산된 프로퍼티는 읽기전용(read-only)이라 하더라도 계산 값에 따라 값이 변할 수 있기 때문에 var로 선언합니다.)

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// "the volume of fourByFiveByTwo is 40.0" 출력

get키워드와 중괄호를 제거하여 선언을 단순화할 수 있습니다 .

프로퍼티 옵저버

프로퍼티에는 새 값이 설정 될 때마다 이 이벤트를 감지할 수 있는 옵저버를 제공합니다. 프로퍼티 옵저버는 새 값이 이전 값과 같더라도 항상 호출됩니다.

다음 위치에 프로퍼티 옵저버를 추가할 수 있습니다.

  • 사용자가 정의한 저장 프로퍼티
  • 상속받은 저장 프로퍼티
  • 상속받은 연산 프로퍼티

서브클래스의 프로퍼티에 옵저버를 정의할 수도 있으며, 연산 프로퍼티는 setter에서 변화를 감지하기 때문에 따로 옵저버를 정의할 필요가 없습니다.(lazy var에서는 사용할 수 없습니다.)

프로퍼티 옵저버는 다음 중 하나 또는 둘 다 정의할 수 있습니다.

  • willSet : 값이 저장되기 바로 직전에 호출 됨
    (새 값의 파라미터명을 지정할 수 있음. 지정하지 않으면 기본 값으로 newValue 사용)
  • didSet : 새 값이 저장되고 난 직후에 호출 됨
    (바뀌기 전의 값의 파라미터명을 지정할 수 있음. 지정하지 않으면 기본 값으로 oldValue 사용)

슈퍼클래스의 프로퍼티 옵저버는 슈퍼클래스의 초기자(initializer)가 호출 된 후, 서브클래스 이니셜라이저에 속성이 설정될때 실행됩니다.(서브클래스 슈퍼클래스에서 프로퍼티를 변경하는 것도 마찬가지로 수퍼클래스의 초기자가 호출된 후 옵저버가 실행됩니다.)

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

만약 in-out 파라미터로 선언된 함수의 인자에 프로퍼티를 넘기면 willSet과 didSet이 항상 실행됩니다. 이유는 in-out 파라미터이기 때문에 프로퍼티가 항상 복사(copy)되기 때문입니다. 이 in-out 파라미터의 프로퍼티는 항상 원래 값에 새 값을 다시 덮어쓰게 됩니다.

Property Wrappers

Property Wrapper는 반복되는 로직들을 프로퍼티 자체에 연결할 수 있습니다.
프로퍼티가 저장되는 방식을 관리하는 코드와, 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가하는 것이지요.
예를 들어 스레드 안정성 검사를 제공하거나, 기본 데이터를 데이터베이스에 저장하는 속성이 있는 경우 모두 프로퍼티에 해당 코드를 작성해야 합니다. 하지만 Property Wrappers를 사용하면, 이를 정의할 때 관리하는 코드를 한 번 작성하여 다음 여러 프로퍼티에 적용하여 사용할 수 있습니다. 즉 관리 코드의 재사용이 가능해집니다.

아래 코드에 height와 width 프로퍼티는 중복된 내용을 가지고 있습니다.

struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

Property Wrappers를 정의하려면, 속성을 정의하는 구조체나 열거형, 클래스를 wrappedValue로 만듭니다.

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

적용하기 위해서는 만든 프로퍼티 래퍼의 이름을 속성 앞에 작성합니다.

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

속성에 래퍼를 적용하면, 컴파일러는 래퍼에 대한 저장소를 제공하는 코드와 래퍼를 통해 속성에 대한 접근을 제공하는 코드를 합성합니다.(프로퍼티 래퍼는 랩핑된 값을 저장하는 역할을 하므로, 이에 대한 합성 코드가 없습니다.)

초기 값 설정

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"

ZeroRectangle의 height와 width의 초기 값이 없는 경우 init()을 이용하여 초기화 됩니다.

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

초기 값이 있는 경우 init(wrappedValue: Int)을 이용하여 초기화 됩니다.

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
print(mixedRectangle.width)
// Prints "1"
// Prints "2"

mixedRectangle.height = 20
mixedRectangle.width = 20

print(mixedRectangle.height)
print(mixedRectangle.width)
// Prints "12"
// Prints "9"

원한다면 init(wrappedValue: Int, maximum: Int) 등 사용자화한 이니셜 라이저로 초기화가 가능합니다.

값 투영

property wrapper는 wrapped value에 다른 프로퍼티를 추가할 수 있는 기능이 있습니다.
이는 projectedValue라는 값을 만들어서 사용할 수 있습니다.
(이름을 변경해서 해봤는데 컴파일이 안 되더라고요. 혹시 다른 이름으로도 가능하다면 댓글 남겨주세요. 다만, 값의 타입은 자유로운 것으로 보입니다!😁)
즉, wrappedValue 뿐만 아니라 추가적인 하나의 Value를 더 가질 수 있는 것이죠.

@propertyWrapper
struct SmallNumber {
    private var number = 0
    var projectedValue = false
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"

enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

지역변수와 전역변수

앞서 소개한 계산된 프로퍼티와 프로퍼티 옵저버 기능은 전역변수와 지역변수 모두에서 이용 가능합니다. 전역 변수란 함수, 메소드, 클로저 혹은 타입 컨텍스트 밖에 정의된 변수이고 지역 변수는 그 안에 선언된 변수를 말합니다.

전역 상수와 변수는 지연 저장 프로퍼티(Lazy Stored Properties)와 같이 지연 계산(lazy computed) 됩니다. 하지만 지연 저장 프로퍼티와 다르게 lazy키워드를 붙일 필요 없습니다.

또한 property wrapper는 지역 변수에는 적용할 수 있지만, 전역 변수나 연산 프로퍼티에는 적용할 수 없습니다.

타입 프로퍼티

인스턴스 프로퍼티는 특정 인스턴스에 속한 프로퍼티를 말합니다. 이 프로퍼티는 새로운 인스턴스가 생성될 때마다 새로운 프로퍼티도 같이 생성됩니다. 타입 프로퍼티는 특정 타입에 속한 프로퍼티로 그 타입에 해당하는 단 하나의 프로퍼티만 생성됩니다. 이 타입 프로퍼티는 특정 타입의 모든 인스턴스에 공통으로 사용되는 값을 정의할때 유용합니다.

인스턴스 프로퍼티와는 다르게 타입 프로퍼티는 항상 초기값을 지정해서 사용해야 합니다. 왜냐하면 타입 자체에는 초기자(Initializer)가 없어 초기화 할 곳이 없기 때문입니다.
또한 lazy 속성처럼 느리게 초기화 되지만, 따로 지정할 필요는 없습니다. 추가로 여러 스레드에서 동시에 접근하는 경우에도 한번만 초기화 되도록 되어있습니다.

타입 프로퍼티 구문

타입 프로퍼티를 선언을 위해서는 static 키워드를 사용합니다. 클래스에서는 static과 class 이렇게 2가지 형태로 타입 프로퍼티를 선언할 수 있는데 class로 선언된 프로퍼티는 서브클래스에서 오버라이드 가능합니다

struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

위 예제에서는 읽기 전용으로 연산프로퍼티를 만들었지만, 쓰기-읽기가 모두 가능한 연산 프로퍼티로도 만들 수 있습니다.

타입 프로퍼티의 접근과 설정

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // cap the new audio level to the threshold level
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // store this as the new overall maximum input level
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

currentLevel의 didSet안에서 currentLevel에 값을 할당하는 것은 didSet을 반복호출하지 않습니다.


오늘도 스위프트 공식문서를 정리해보았군욥~
다음편도 힘내보겠습니다!

감사합니다🙇🏻‍♀️

profile
한달 차 iOS 개발자입니다🐥

0개의 댓글