다음 gist는 앞서 소개한 Employee, Department의 to-many 관계의 KVO를 구현한 코드이다. 이 코드에는 Swift 상에서 KVO에 필요한 모든 코드와, to-many 관계에 대한 프록시를 사용하기 위한 모든 요건이 표현되어 있으니 참고가 되었으면 한다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: Employee | |
class Employee: NSObject | |
{ | |
@objc dynamic var salary:Int | |
var name: String | |
init(name:String, salary:Int) { | |
self.name = name | |
self.salary = salary | |
super.init() | |
} | |
} | |
// MARK: Department | |
class Department: NSObject | |
{ | |
@objc dynamic var totalSalary: Int = 0 | |
@objc var employees: [Employee] = [] | |
private var totalSalaryContext = 0 | |
/// 총 급여액을 업데이트한다. 새로 계산된 금액이 기존값과 다른 경우에만 | |
/// 실제로 업데이트한다. | |
func updateTotalSalary() { | |
let newTotal = employees.map{ $0.salary }.reduce(0, +) | |
if newTotal != totalSalary { | |
willChangeValue(for:\Department.totalSalary) | |
totalSalary = newTotal | |
didChangeValue(for:\Department.totalSalary) | |
} | |
} | |
// 수동 통지를 위해서 totalSalary에 대한 자동통지를 중단한다. | |
override func automaticallyNotifiesObservers(forKey key: String) -> Bool | |
{ | |
guard key == "totalSalary" | |
else { | |
return super.automaticallyNotifiesObservers(forKey: key) | |
} | |
return false | |
} | |
// MARK: collection accessors for employees | |
@objc(countOfEmployees) | |
func numberOfEmployees() -> Int { | |
return employees.count | |
} | |
@objc(objectInEmployeesAtIndex:) | |
func employee(at index:Int) -> NSObject { return employees[index] } | |
// MARK: collection mutators for employees | |
@objc(insertObject:inEmployeesAtIndex:) | |
func insertEmployee(_ employee:Employee, at index:Int) { | |
// 직원을 삽입할 때 salary에 대해 옵저빙한다. | |
employee.addObserver(self, forKeyPath: #keyPath(Employee.salary), | |
options: [.new], | |
context: &totalSalaryContext) | |
employees.insert(employee, at: index) | |
// 직원이 늘어나면 총급여는 증가하므로 업데이트한다. | |
updateTotalSalary() | |
} | |
@objc(removeObjectFromEmployeesAtIndex:) | |
func removeEmployee(at index:Int) { | |
// 직원을 제거하기 전에 해당 직원에 대한 salary 옵저빙을 그만둔다. | |
employees[index].removeObserver(self, forKeyPath: #keyPath(Employee.salary)) | |
employees.remove(at: index) | |
// 직원이 줄었으므로 총급여를 업데이트한다. | |
updateTotalSalary() | |
} | |
// MARK: observer method | |
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | |
guard context == &totalSalaryContext | |
else { | |
super.observeValue(forKeyPath: keyPath, | |
of: object, | |
change: change, | |
context: context) | |
} | |
// 개별직원의 salary가 변경되면 | |
// 총급여 역시 갱ㅇ신되어야 한다. | |
updateTotalSalary() | |
} | |
} | |
// MARK: Observer | |
class Foo: NSObject | |
{ | |
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | |
guard let key = keyPath, | |
key == #keyPath(Department.totalSalary) | |
else { | |
super.observeValue(forKeyPath: keyPath, | |
of: object, | |
change: change, | |
context: context) | |
return | |
} | |
guard let theObj = object as? Department | |
else { | |
return | |
} | |
NSLog("total salary has been changed \(theObj.totalSalary)") | |
} | |
} | |
let dept = Department() | |
let e1 = Employee(name: "e1", salary: 10) | |
let e2 = Employee(name: "e2", salary: 20) | |
let foo = Foo() | |
dept.addObserver(foo, forKeyPath: #keyPath(Department.totalSalary), | |
options: [.new], context: nil) | |
let eee = dept.mutableArrayValue(forKey: "employees") | |
NSLog("직원추가") | |
eee.add(e1) | |
NSLog("직원추가") | |
eee.add(e2) | |
NSLog("임금상승") | |
e1.salary = 30 | |
NSLog("직원해고") | |
eee.removeObject(at: 0) | |
print(dept.totalSalary) |
-
그렇기 때문에 Swift에서 옵저빙 가능한 프로퍼티는
@objc dynamic var
를 써서 선언하게 된다. 접근자가 런타임에 동적으로 결정되기 때문이다. ↩