KVC 집합 접근자/변경자 메소드 작성방법

키밸류 코딩의 집합 접근자/변경자 메소드

to-many 관계의 프로퍼티에 대한 조정은 키밸류 코딩에서 배열 프록시를 통해서 이루어진다고 했다. 이 때 개별 원소를 추가/삭제/교체하는 작업의 효율을 높이고, 각 동작에 대해서도 KVO 지원을 가능하게 하기 위해서 배열 프록시와 연계하여 동작할 수 있는 집합 메소드를 추가로 정의하는 것이 강력하게 권장된다. 이들 메소드들은 기본적으로 NSMutableArray의 기본적인 액세스 메소드들에 키 이름이 혼합된 형태로, 일정한 규칙에 의해 이름 지어진다. 단, 키 이름이 메소드 이름에 들어가기 때문에 메소드명이 고정되지 않았고, 따라서 NSKeyValueCoding 레퍼런스 상에서는 소개되지 않는다.

집합 접근자 및 변경자들은 기본적으로 NSMuatbleArray의 몇몇 메소드들의 이름에 키 이름을 추가하여 변형한 것이다. 집합메소드에서 키 이름은 <key>로 쓰여진다. 키 이름이 대문자로  시작해야하는 경우는 <Key>로 쓰였으니 잘 구분해서 사용해야 한다.

구분 Array 메소드 KVC 집합 메소드 비고
접근자 -count -countOf<Key> 필수
-objectAtIndex: -objectIn<Key>AtIndex: 둘 중 하나 필수
-objectsAtIndexes: -<key>AtIndexes:
-getObjects:range: -getObjectsIn<Key>:range: 선택적
변경자 -insertObject:atIndex: -insertObject:in<Key>AtIndex: 배열 변경이 필요한 경우에는 필수
-removeObjectAtIndex: -removeObjectFrom<Key>AtIndex:
-replaceObjectAtIndex:withObject: -replaceObjectIn<Key>AtIndex:withObject: 선택적

 

Swift 버전을 생각하기

Swift에서는 몇가지 고려해야 하는 부분이 있다.  (Swift4, Xcode9 버전 대응)

  1. KVC는 Objective-C 런타임의 기능이므로 이름 패턴 검색 정책 자체는 달라지는 점이 없다.
  2. 집합 메소드 프로퍼티는 모두 런타임에 이름을 통해서 액세스될 수 있어야 하므로 @objc를 통해서 런타임에 노출되어야 한다.
  3. insert... 를 제외하고는 메소드 이름을 지을 때,  ...AtIndex(_:)...(atIndex:)나 차이가 없다. 예를 들어 Swift 메소드명, objectInEmployeesAtIndex(_ index:)objectInEmployees(atIndex:) 모두 Objective-C에서는 -objectInEmployeesAtIndex:로 해석된다.
  4. 하지만 insertObject(_:...)insert(object:...)로 바꿔썼을 때 제대로 찾지 못한다.[^1] 즉, 파라미터 개수가 2개 이상인 경우는 명시적으로 밖으로 빼서 첫번째 파라미터를 숨겨야 한다. 이것은 Swift2 버전의 기본 메소드 이름 규칙인데, Objective-C 런타임에 적용된 이름 규칙이 오래전 버전인 것 같다.
  5. 이름 해석 규칙이 애매할 수도 있고, Xcode 버전이 올라감에 따라 다시 다른 규칙이 적용될지 모른다. 따라서 가장 좋은 방법은 @objc() 변경자에서 Objective-C 런타임상의 이름을 같이 써주는게 좋다. (그러면 Swift 이름은 뭐 어떻게 짓든 상관없다.)
  6. 그외 타입규칙
    1. 인덱스를 가리키는 NSUIntegerInt로 번역된다.
    2. Element의 타입은 Any, AnyObject가 아닌 소속 클래스 타입을 그대로 쓰면 된다. 다만 어떤 접근자이든 옵셔널타입을 쓰지는 않는다. 옵셔널 타입으로 파라미터나 리턴타입을 정의하면 올바른 메소드 이름이 아닌 것으로 간주되니 주의할 것.

다음 예에서 Foo 클래스의 words프로퍼티에 대한 집합 접근자/변경자는 다음과 같이 작성할 수 있다.

/// 프로퍼티 선언

/// Objective-C
@property (strong, nonatmoic) NSMutableArray<NSString *>* words;

/// Swift
/// setter에 의한 교체를 추적하려면 @objc dynamic var... 로 선언해야 하지만
/// 배열 프록시를 통해서 제어하는 경우, 굳이 해당 프로퍼티가 노출되지 않아도 된다.
var words:[String] = []


/// 집합 액세스 메소드
/// 꼭 필요하지는 않다.
/// Swift의 경우 @objc 를 붙이지 않았다면 이들을 구현해서
/// 배열 프록시로 동작하게 할 수 있다.
/// Objective-C
- (NSUInteger)countOfWords { 
    return [_words count]; 
}
- (NSString*)objectInWordsAtIndex:(NSUInteger)index 
{
    return [_words objectAtIndex:index];
}

/// Swift
@objc func countOfWords() -> Int {
  return words.count
}

@objc func objectInWord(atIndex index: Int) -> String
{
  return words[index]
}

/// 변경자
/// Objective-C
- (void)insertObject:(NSString*)object inWordsAtIndex:(NSUInteger)index
{
    [_words insertObject:object atIndex:index];
}

- (void)removeObjectFromWordsAtIndex:(NSUInteger)index
{
   [_words removeAtIndex:index];
}

/// swift
@objc(insertObject:inWordsAtIndex:)
func insert(words: String, at index:Int) {
  words.insert(words, at:index)
}

@objc(removeFromWordsAtIndex:)
func remove(at index:Int) {
  words.remove(at:index)
}

참고자료

iOS에서 사용할 수 있는 애니메이션 구현 기법들에 대한 정리

애니메이션 구현 방법

iOS의 애니메이션 구현은 크게 두 가지로 나눌 수 있는데 하나는 UIKit의 애니메이션 API를 사용하는 것이고, 다른 하나는 코어 애니메이션을 사용하는 것이다. 오늘은 각각의 세부적인 구현보다는 각각의 API의 차이와 기본적인 사용방법에 대해서 살펴보도록 하겠다. iOS에서 사용할 수 있는 애니메이션 구현 기법들에 대한 정리 더보기

(Swift) Objctive-C와 Swift 타입의 브릿징

브릿징

Swift의 많은 기본 타입들은 Objective-C의 파운데이션 타입에 대응되는 것들이 있고 (e.g. String <-> NSString) 이렇게 카운터파트가 존재하는 타입들은 파운데이션타입으로 브릿징된다. 브릿징은 Objective-C 타입이 Swift 타입으로 행동하거나 혹은 반대로 Swift 타입이 Objective-C 타입처럼 행동하여 이 둘을 상호간에 바꿔쓸 수 있음을 말한다. 다시 말해 파운데이션 API를 사용하면서 NSString 을 사용해야 할 때 String을 쓰거나 반대로 Swift 함수/메소드 호출 시 String 을 써야 할 때 NSString을 쓰는 것이 허용된다는 말이다. 1

 Swift는 어떤 Objective-C 타입들을 자동으로 Swift 타입으로 변환하며, 그 반대의 동작도 자동으로 수행합니다. 이렇게 Objective-C와 Swift 사이에서 전환되는 타입들을 “bridged types”라 부릅니다. 예를 들어, Swift 코드에서 NSString 파라미터를 요구하는 Objective-C API에 String 타입의 값을 넘겨줄 수 있습니다. 또한 많은 코코아 프레임워크들은 API를 좀 더 Swift 스럽게 새로 정의했습니다. 예를 들어 NSCoderdecodeObjectOfClass(_:forKey:)는 Swift의 제네릭 타입을 통해 보다 강한 타입시그니처를 갖게 됐습니다.2

사실 이 브릿징은 Objective-C에 익숙하다면 낯선 개념은 아니다. 코코아에는 Objective-C가 아닌 C로 제작된 코어 파운데이션 프레임워크가 있고, 이미 NS* 클래스들은 CF* 타입들과 브릿징되고 있다.  (Swift) Objctive-C와 Swift 타입의 브릿징 더보기

(Swift) 에러 핸들링 기법

에러 핸들링은 프로그램 내에서 에러가 발생한 상황에 대해 대응하고 이를 복구하는 과정이다. Swift는 에러를 던지고, 캐치하고, 이전하며 조작할 수 있는 기능을 언어의 기본 기능으로 지원한다.

어떤 동작들은 항상 유용한 결과를 내놓거나 완전하게 실행되는 것이 보장되지 않는다. 결과가 없는 경우에 이를 표현할 수 있는 옵셔널 타입을 사용할 수도 있지만, 처리 자체가 실패하는 경우, 어떤 원인으로 실패하였는지를 알면 코드가 이에 적절히 대응할 수 있도록 할 수 있기에 그 원인을 하는 것은 유용하다.

예를 들어 디스크상의 파일로부터 데이터를 읽고 처리하는 작업을 생각해보자. 여기에는 작업이 실패할 수 있는 경우가 꽤 많다. 주어진 경로에 파일이 존재하지 않거나, 파일을 읽을 수 있는 권한이 없거나 혹은 파일이 호환되는 포맷으로 인코딩 되어 있지 않을 수도 있다. 이런 상황들을 구분하는 것은 프로그램으로 하여금 에러를 스스로 해결하거나, 자동으로 해결할 수 없는 에러에 대해서 사용자에게 보고할 수 있게끔 한다. (Swift) 에러 핸들링 기법 더보기

gcd in swift3

GCD in Swift3

iOS/OSX에서 동시성을 처리하기 위해서는 NSThread로부터 NSOpaerationQueue에 이르는 방법이 있었고, 시스템 레벨에서 모든 걸 관리해주는 GCD가 나오기에 이르렀다. 하지만 GCD는 libdispatch의 API를 그대로 사용했기 때문에 Swift에서는 가장 번거로운 문법을 이용해야 했었으나, Swift3에서 API를 전면적으로 개선했다.

dispatch_async

이전에는 동기/비동기 여부를 결정한 후 어떤 큐를 쓸 것인지를 결정하는 순서로 코드가 작성되었는데, 이제는 이 순서가 바뀌었다. 왜냐면 dispatch_async, dispatch_sync는 자유함수에서 큐의 메소드로 편입되었기 때문이다.

DispatchQueue.global(attributes:[.qosDefault]).async {
    // 백그라운드 작업
    DispatchQueue.main.async(execute: { /* ... */ })

}

큐의 속성은 생성시점에 지정될 수 있고, 이는 Swift의 OptionSet 프로토콜을 따르게끔 되어 있어서 순서대로 처리할지 동시에 처리할지, 메모리 관리 옵션은 어떻게 되는지, 서비스 퀄리티는 어떤지 등을 지정할 수 있다. 또한 서비스 퀄리티는 예전의 상수가 아닌 enum 케이스로 정의되었다.

  • DISPATCH_QUEUE_PRIORITY_HIGH >> .userInitiated
  • DISPATCH_QUEUE_PRIORITY_DEFAULT >> .default
  • DISPATCH_QUEUE_PRIORITY_LOW >> .utility
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND >> .background

메모리 및 활동성관리는 이번에 새로 도입되었는데, 큐를 비활성 상태로 시작할 수 있게끔한다.

work item

Swift로 GCD를 도입하면서 WorkItem을 새로 만들었다.

let wordItem = DispatchWorkItem(qos: .userInitiated, flags: .assignCurrentContext) {
    // ...
}
queue.async(execute: workItem)

이는 CGD의 작업을 단순히 크로저로 넘기는 것 이상으로 객체화하여 별도의 옵션을 세세하게 부여해서 보다 간단한 API를 통해서 사용하 수 있게 한다.

dispatch_once

dispatch_once는 싱글턴 등을 만들때 단 한 번만 초기화되는 인스턴스 같은 걸 만드려고 썼다. 하지만 Swift는 언어 레벨에서 초기화가 단 한 번만 이루어지도록 할 수 있으므로 이는 제거되었다. 아래 예제 코드들은 dispatch_once를 써서 만들던 부분이다.


// 공유 인스턴스 class Object { static let sharedInstance = Object() } // 전역 상수 let constant = Object() // 전역 변수. 선언시에 한 번만 초기화 된다. var variable: Object = { let variable = Object variable.doSomething() return variable }()

dispatch_time_t

dispatch_time_t는 간단하게 enum 타입으로 변경됐다.

let delay = DispatchTime.now() + .seconds(60)
DispatchQueue.main.after(when: delay) { 
    // ...
}

간단하게 단위별로 숫자값을 넣어서 만들 수 있음

  • .seconds(Int)
  • .milliseconds(Int)
  • .microseconds(Int)
  • .nanoseconds(Int)

dispatch_assert

특정 스레드에서만 코드를 실행하거나 실행하지 않기 위해서 체크하는 기능은 문법이 대체되었다.


let queue = DispatchQueue.global(attributes: .qosUserInitiated) let mainQueue = DispatchQueue.main mainQueue.async { dispatchPrecondition(condition: .notOnQueue(mainQueue)) // 이 코드는 실행되지 않음 } queue.async { dispatchPrecondition(condition: .onQueue(queue)) // 실행될 것임. }