스위프트 공식 문서 정리 2

안준성·2022년 8월 6일

Swift

목록 보기
6/6

Swift Roadmap

image


Properties

  • 저장된 프로퍼티 : 값을 저장하고 있는 프로퍼티
  • 계산된 프로퍼티 : 값을 저장하고 있지 않고 특정하게 계산한 값을 반환해주는 프로퍼티
  • 프로퍼티 옵저버 : 값이 변할 때마다 모니터링할 수 있다.

Lazy stored properties

값이 처음으로 사용되기 전에는 계산되지 않는 프로퍼티.
프로퍼티가 특정 요소에 의존적이어서 그 요소가 끝나기 전에 적절한 값을 알지 못하는 경우 유용하다.

Computed properties

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

	struct Rect {
    	var a = 1
    	var center: Point {
        	get {
            	return Point(~)
            }
            set(new) {
            	a = new
            }
        }
    }

Property Observers

프로퍼티에는 새 값이 설정(set)될 때마다 이벤트를 감지할 수 있는 옵저버를 제공합니다.
프로퍼티 옵저버는 새 값이 이전 값과 같더라도 항상 호출됩니다.
계산된 프로퍼티는 setter에서 값의 변화를 감지할 수 있기 때문에 따로 옵저버를 정의할 필요가 없습니다.
프로퍼티에서는 다음 두가지 옵저버를 제공합니다.

willSet : 값이 저장되기 바로 직전에 호출 됨
didSet : 새 값이 저장되고 난 직후에 호출 됨.
willSet에서는 새 값의 파라미터 명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 newValue를 사용합니다.
didSet에서는 바뀌지 전의 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 oldValue를 사용합니다.

class Test {
	var value: Int = 0 {
    	willSet(newValue) {
        	print("\(value)\(newValue)로 업데이트 직전")
        }
        didSet {
        	print("before : \(oldValue), after: \(value)")
        }
    }
}

Type Properties

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

static 키워드를 이용해 사용합니다.
클래스에서는 staticclass 2가지 형태로 타입 프로퍼티를 선언할 수 있는데 이 두가지 경우의 차이는 서브클래스에서의 overriding 가능 여부이다.
class로 선언된 프로퍼티는 서브클래스에서 오버라이드 가능하다.

class Test {
	static var storedTypeProperty = "test"
    static var computedTypeProperty: Int {
    	set(){...}
        get(){...}
    }
    class var overrideable: Int {
    	return 107
    }
}

Closure

Shorthand Argument Names

	reversedNames = names.sorted(by: { $0 > $1 } )
    
    reversedNames = names.sorted(by: >)

Trailing Closures

	func test(closure: { 
    	// closure 본문
    } )
    
    test() { $0 > $1 }
    
    test { $0 > $1 }

Optional Chaining

	class Address {
    	var buildName: String?
    	var street: String?
    
        func build() -> String? {
            if let buildName = buildName, let street = street {
                return buildName
            } else if buildName != nil {
                return buildName
            } else {
                return nil
            }
        }
    }

Subscripts

class, struct, enum 들에서 collection, list, sequence 등의 property에 접근하는 subscripts를 정의할 수 있습니다.
설정하고 가져오는데 별도의 메소드 없이 index로 값을 설정하고 가져오기 위해 subscript를 사용합니다.
var num = [1,2,3] num[0] = 100도 이미 만들어져있는 subscript이다. 이처럼 subscript를 활용하면 내가 작성한 클래스에서도 인덱스로 값을 접근할 수 있다.

	struct Num {
    	let num = [1, 2, 3]
        subscript(i: Int) -> Int {
        	get { }
            set(newValue) { }
        }
    }
    
    let n = Num()
    print(n[0], n[1], n[2])

Type Casting

Checking Type

is 연산자를 통해 인스턴스 타입 확인 가능

	switch jason {
   case is UniversityStudent:
   	print("대학생")
   case is Student:
   	print("학생")
   case is Person:
   	print("사람")
   default:
   	print("노사람")
   }

Downcasting

as?as! 연산자를 이용해 특정 타입인지 체크
자식 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 인스턴스의 타입정보를 전환해줍니다.

func test(someone: Person) {
	if let university = someone as? UniversityStudent {
    	university.goToMT()
    } else if let student = someone as? Student {
    	student.goToSchool()
    } else if let person = someone as? Person {
    	person.breath()
    }
}

Any, AnyObject의 타입 캐스팅

Swift에서는 두가지 특별한 타입을 제공

  • Any : 합수 타입을 포함한 모든 타입
  • AnyObject : 모든 클래스 타입의 인스턴스
	let things = [Any]()
    
    things.append(0)
    things.append("hello")
    things.append((3.0, 5.0))
    things.append(Movie("~~"))
    things.append({ (name: String) -> String in "hello"})
    
    for thing in things {
    	switch thing {
        case let someInt as Int:
        	print(someInt)
        case let someString as String:
        	print(someString)
        case (x, y) as (Double, Double):
        	print(x, y)
        case let movie as Movie:
        	print(movie.name)
        case let stringConverter as (String) -> String:
        	print(stringConveter)
        default:
        	print("hi")
        }
    }

Nested Types

열거형은 특정 구조체나 클래스의 기능을 처리하기 위해 자주 사용된다.
열거형, 클래스, 구조체를 그 타입 안에서 다시 정의할 수 있습니다.

struct BlackjackCard {
	enum Suit: Character {
    	case spades = "♠", hearts = "♡", diamonds = "♢"
    }
    
    enum Rank: Int {
    	case two = 2, three, four
        case jack, queen, king, ace
        
        struct Values {
        	let first: Int, second: Int?
        }
        var values: Values {
        	switch self {
            case .ace:
            	return Values(first: 1, second: 11)
            case .jack, .queen, .king:
            	return Values(first: 10, second: nil)
            default:
            	return Values(first: self.rawValue, second: nil)
            }
        }
    }
    
    let rank: Rank, suit: Suit
    var description: String {
    	var output = "suit is \(suit.rawValue),"
        output += " value is \(rank.values.first)"
        if let second = rank.values,second {
        	output += " or \(second)"
        }
        return output
    }
}

Extensions

  • 새로운 initializer 제공
  • subscript 정의
  • 중첩 타입의 선언과 사용
  • 특정 프로토콜을 따르는 타입 만드기

    NOTE
    익스텐션은 새 기능을 추가할 수는 있지만 override는 불가능 합니다.

하나의 익스텐션에서 현재 존재하는 타입에 한개 이상의 프로토콜을 따르도록 확장할 수 있습니다.

extension SomeType: protocol1, protocol2 { }

Computed Properties

extension Double {
	var km: Double { return self * 1_000.0 }
    var m: Double {return self }
    var mm: Double {return self / 1_000.0 }
}

let marathon = 42.km + 195.m // marathon = 42195.0

NOTE
익스텐션은 새 계산된 값은 추가할 수 있지만 새로운 저장 프로퍼티나 프로퍼티 옵저버는 추가할 수 없습니다.

Initializers

convenience initializer는 추가할 수 있지만
designated initializer나 deinitializer를 추가할 수는 없습니다.

struct Rect {
    var origin = Point()
    var size = Size()
}
extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

Protocols

프로토콜은 특정 기능 수행에 필수적인 요소를 정의한 blueprint이다.

변경 가능한 메소드 요구사항

mutating 키워드를 사용해 인스턴스에서 변경 가능하다는 것을 표시할 수 있습니다. 이 mutating 키워드는 값타입 형에만 사용합니다.

NOTE
프로토콜에 mutating을 명시한 경우 이 프로토콜을 따르는 클래스형을 구현할 떄는 메소드에 mutating을 명시하지 않아도 됩니다.

Initializer Requirements

프로토콜에서 필수로 구현해야 하는 이니셜라이저를 지정할 수 있습니다.
프로토콜에서 특정 이니셜라이저가 필요하다고 명시했기 떄문에 구현에서 해당 이니셜라이저에 required 키워드를 붙여줘야 합니다.

NOTE
클래스 타입에서 final로 선언된 것에는 required를 표시하지 않아도 됩니다. final로 선언되면 서브클래싱 되지 않기 때문입니다.

특정 프로토콜의 필수 이니셜라이저를 구현하고, 슈퍼클래스의 이니셜라이저를 서브클래싱하는 경우 이니셜라이저 앞에 required 키워드와 override 키워드를 적어줍니다.

protocol TestProtocol {
	init()
}

class TestClass {
	init() {...}
}

class SubClass: TestClass, TestProtocol {
	required override init() {...}
}

Delegation(위임)

delegation은 클래스 혹은 구조체 인스턴스에 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴 중 하나입니다.

protocol DiceGame {
	var dice: Dice {get}
    func play()
}

protocol DiceGameDelegate: AnyObject {
	func gameDidStart(_ game: DiceGame)
    ...
}

익스텐션을 이용해 프로토콜 따르게 하기

익스텐션을 사용하여 이미 존재하는 타입이 새 프로토콜을 따르게할 수 있습니다.

protocol Test { ... }

extension String: Test {
	...
}

조건적으로 프로토콜을 따르기

특정 조건을 만족시킬 때만 프로토콜을 따르도록 제한할 수 있습니다.
이 선언은 where절을 사용해 정의합니다.
아래 예제는 TextRepresentable을 따르는 Array중에 각 원소가 TextRepresentable인 경우에만 따르는 프로토콜을 정의합니다.

extension Array: TextRepresentable where Element: TextRepresentable {
	var textDescription: String {
    	return "hi"
    }
}

Collections of Protocol Types

프로토콜을 Array, Dictionary등 Collection 타입에 넣기위한 타입으로 사용할 수 있습니다.

let things: [TextRepresentable] = [game, d12]

for t in things {
	print(t.textDescription)
}

Array의 모든 객체는 TextRepresentable을 따르므로 textDescription 프로퍼티를 갖는다.

Checking for Protocol Conformance

어떤 타입이 특정 프로토콜을 따르는지 다음과 같은 방법으로 확인할 수 있습니다.

  • is 연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있습니다.
  • as?는 특정 프로토콜 타입을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트를 하게 되고 따르지 않는 경우는 nil을 반환합니다.
  • as!는 강제로 특정 프로토콜을 따르도록 정의합니다.

Optional Protocol Requirements

프로토콜을 선언하면서 필수구현이 아닌 선택적 구현 조건을 정의할 수 있습니다.
이 프로토콜의 정의를 위해서 @objc 키워드를 프로토콜 앞에 붙이고,
개별 함수 혹은 프로퍼티에는 @objc optional 키워드를 붙입니다.
@objc 프로토콜은 클래스 타입에서만 채용될 수 있습니다.

@objc protocol Test {
	@objc optional func add() -> Int
    @objc optional var add2(): Int {get}
}

class Counter {
	var count = 0
    var dataSource: Test?
    func increment() {
    	if let amount = dataSource.add?() {
        	count += amount
        }
    }
}

프로토콜 익스텐션에 제약 추가

프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있습니다.
이 선언에는 where절을 사용합니다.
다음은 Collection 엘리먼트가 Equatable인 경우에만 적용되는 test() 메소드를 구현한 예입니다.

extension Collection where Element: Equatable {
	func test() -> Bool {
    	for element in self {
        	if element != self.first {
            	return false
            }
        }
        return true
    }
}

각 배열의 엘리먼트는 모두 Equatable 프로토콜을 따르기 때문에 test()를 호출할 수 있습니다.


Generics

func swap<T>(_ a: inout T, _ b: inout T) {
	let temp = a
    a = b
    b = tmp
}

inout을 쓰면 인자로 들어온 값을 참조하여 실제 값을 조작합니다.
파라미터 이름은(a: T) T 고정이 아닌 단일문자 대문자로 작성. 파라미터 간의 관계가 Key, Value면 a: K, b: V 이런식으로.

Generic Types

Swift에서는 제네릭 함수 뿐만 아니라 제네릭 타입도 정의할 수 있습니다.

struct Stack<Element> {
	var items = [Element]()
    mutating func push(_ item: Element) {
    	items.append(item)
    }
    mutating func pop() -> Element {
    	return items.removeLast()
    }
}

쥐기네

제네릭 타입의 확장

extension을 이용해 제네릭 타입을 확장할 수 있습니다. 이 때 원래 선언한 파라미터 이름을 사용합니다.

extension Stack {
	var topItem: Element? {
    	return items.isEmpty ? nil : items[items.count - 1]
    }
}

Type Constraints(타입 제한)

Swift의 Dictionary 타입은 key값을 사용합니다. 이 때 key는 유일한 값이어야 하기 때문에 hashable이라는 프로토콜을 반드시 따라야합니다.
이와 같이 특정 타입이 반드시 어떤 프로토콜을 따라야 하는 경우가 있습니다.
이에 제네릭에서는 특정 클래스를 상속하거나 특정 프로토콜을 따르거나 합성하도록 명시할 수 있습니다.

func findIndex<T>(of value: T, in array:[T] -> Int? {
	for (i, v) in array.enumerated() {
    	if v == value {
        	return i
        }
    }
    return nil
}

위 코드는 에러가 발생합니다.
이유는 v == value에서 두 값을 비교할 때 ==를 사용하므로 두 값은 반드시 Equatable 프로토콜을 따라야 하기 때문입니다.
따라서 위 코드는 이렇게 수정되어야 합니다.

func findIndex<T: Equatable>(of value: T, in array:[T] - > Int? {
	for (i, v) in array.enumerated() {
    	if v == value {
        	return index
        }
    }
    return nil
}

Associated Types

연관타입은 프로토콜의 일부분으로 타입에 placeholder 이름을 부여합니다. 다시 말해 특정 타입을 동적으로 지정할 수 있게 해줍니다.

protocol Container {
	associatedtype Item
    mutating func append(_ item: Item)
    ...
}

associatedtype을 사용하면 Item은 어떤 타입도 될 수 있습니다.

struct Test: Container {
	typealias Item = Int
    mutating func append(_ itme: Int) {
    	self.push(item)
    }
    ...
}

이처럼 프로토콜을 지키는 구조체 등에서 typealias를 사용해 Item의 타입을 지정해주어 함수등에서 사용할 수 있습니다.


Access Control

접근제어는 다른 소스파일이나 모듈에서 특정 코드의 접근을 제한하는 것입니다.
접근제어는 클래스, 구조체, 열거형 등 개별 타입에도 적용할 수 있고,
그 타입에 속한 프로퍼티, 메소드, initializer, subscript에도 적용할 수 있습니다.
Swift에서는 기본 접근레벨을 제공하여 사실 단일타겟의 앱에서는 특별히 접근레벨을 명시하지 않아도 됩니다.


고급 연산자


참조 : Swift 공식문서
Swift 공식문서 번역본

profile
안녕하세요

0개의 댓글