1994년 영화 사탄탱고의 러닝타임은 7시간 19분이다. 극장 개봉 당시 휴식 시간으로 10분 인터미션을 세 번 준 것으로 들은 바 있는데 이번 과제가 아마 첫 번째 인터미션이 아닐가 싶다.
언제부터 과제가 휴식이랑 같은 의미였냐
회고라 하면 너무 무게감이 클 것 같고 한줄평 정도의 제출한 버전 대비 개선점을 살펴보자.
작동하는 것에 초점을 둔다면 ViewController에 type property로 각자 identifier를 작성하고 활용해도 전혀 문제없다. 또한 identifier를 View의 이름과 다르게 설정해야 하는 이유가 명확하다면 따로 작성해서 활용해도 될 것이다.
하지만 대부분의 경우, 관리 용이성과 재사용 측면에서 View의 이름과 동일하게 identifier를 활용해서 혼란의 가능성을 줄이려 한다.
지금처럼 매번 직접 작성하는 것보다 identifier를 각 view의 자동 생성된 type property처럼 만들 수 있지는 않을까?
이 때 protocol을 활용해볼 수 있다.
protocol ReusableViewProtocol {
static var identifier: String { get }
}
ReusableViewProtocol
을 채택/conform하는 모든 view들은 identifier를 구현해야 한다.
생각을 조금 해보면 scene을 구성하는 모든 ViewController는 세부 타입이 어떻게 되는 일단 UIViewController
를 상속한다.
따라서 우리는 UIViewController
가 ReusableViewProtocol
을 채택하면 앞으로 생성하는 모든 ViewController들은 마치 원래 identifier property가 있었던 것처럼 활용할 수 있다.
UIViewController
가 채택하려면 identifier property를 구현해야 한다. 우리는 재사용과 관리 용이를 위해 각 ViewController의 이름과 동일하도록 설정했다.
init(describing:)
Creates a string representing the given value.
Use this initializer to convert an instance of any type to its preferred representation as a String instance. The initializer creates the string representation of instance in one of the following ways, depending on its protocol conformance:
extension UIViewController: ReusableViewProtocol {
static var identifier: String {
return String(describing: self)
}
}
cornerRadiu
설정은 이전 글에도 설명했지만 원의 반지름을 설정해서 원을 그리면 해당 영역 외부는 마스킹해서 가리는 작업을 나타낸다.
async 글에서 한 번 더 정리할 거지만 storyboard와 code load 순서에 따른 문제였다.간단하게 순서를 작성하면
viewDidLoad
에서 UI configuration 수행cornerRadiu
로 사각형 길이 값의 1/2을 적용왼쪽이 iPhone 13 mini, 오른쪽은 iPhone 14 Pro Max이다. 같은 1/2 적용임에도 디바이스가 달라지면 다른 원 마스킹을 보여준다. 따라서 이를 해결하기 위해 비동기 처리를 활용한다.
//...UI 설정 중략...
DispatchQueue.main.async {
self.coverImageView.cornerRadius = self.coverImageView.frame.width/2
}
cornerRadius
로 사각형 길이 값을 화면에 보이기 직전에 설정이 아니라 다른 sync 코드를 모두 적용 후에 main thread에서 작동한다. 이러면 device에 맞게 각자 길이의 1/2로 반지름이 설정되어 원을 나타낸다.
C언어 작성 때 활용하던 습관이 남은 듯하다.
차라리 struct의 type property로 활용해서 여러 곳에서 사용하지만 매번 길게 작성하기 귀찮은 값들을 따로 instance 생성 없이 바로 가져다 활용하자.
struct UISetting {
static let customBackgroundColor = UIColor(red: 23/255, green: 56/255, blue: 14/266, alpha: 1)
//...중략...
}
데이터 관리를 위해 single instance만 활용해서 DataManager로 관리하듯, UserDefault
작업용 UserDefaultManager를 만들어 사용하자. 하나 더 나아가면 dataManager가 데이터를 관리하기 위한 목적이므로 UserDefaultManager의 instance는 DataManager 안에서만 활용하자.
ViewController들은 DataManager와 데이터를 주고 받고, DataManager는 필요한 데이터를 저장하고 가져오기 위해 UserDefaultManager에게 해당 작업을 요청한다. UserDefaultManager는 UserDefaults 관련 작업만 수행한 뒤 결과를 DataManager에게 전달한다.
이렇게 하면 UserDefaults
에 저장하는 key값을 String literal로 갖고 있지 않고 enum case로 한번에 관리할 수 있다.
class UserDefaultManager {
let userDefault = UserDefaults.standard
enum Key: String {
case name
case level
}
//UserDefaults: save & load 작업
//computed property 활용, get과 set으로 save & load 대응
var name: String {
get {
return userDefault.string(forKey: Key.name.rawValue)
}
set {
userDefault.set(newValue, forKey: Key.name.rawValue)
}
}
//remove whole UserDefaults Data
func removeWholeUserDefaultsData() {
if let bundleId = Bundle.main.bundleIdentifier {
userDefault.removePersistentDomain(forName: bundleId)
}
}
}
class DataManager {
static let shared = DataManager()
private init() {}
private let userDefaultManager = UserDefaultManager()
func getName() -> String {
let name = userDefaultManager.name
return name.isEmpty ? "default" : name
}
func saveName(newName: String) {
userDefaultManager.name = newName
}
}
class ViewController: UIViewController {
//...중략...
func getNameFromUserDefault() {
nameLabel.text = dataManager.getName()
}
@IBAction func saveNameButtonTapped(_ sender: UIButton) {
if let text = textField.text {
dataManager.saveName(newName: text)
}
}
}
예시 코드로는 단순 String type 데이터를 저장하고 불러오는 작업이지만 만약 struct나 class의 instance 등 규모가 커질 수록 dataManager 내부에서 데이터 관리보다 UserDefaults
관련 코드가 더 많아져 singleton manager의 목적에 부합하지 않는다.
목적에 맞는 모델과 View와의 분리, 이를 위해 중복되고 비슷한 영역의 코드들은 재분리하는 과정을 고려하자.
참고) UserDefaults 데이터 전체 삭제
removePersistentDomainForName:
Removes the contents of the specified persistent domain from the user’s defaults.
Calling this method is equivalent to initializing a user defaults object with
initWithSuiteName:
passingdomainName
, and calling theremoveObjectForKey:
method on each of its keys.
DarkMode는 유저가 시스템으로 설정가능한 세팅이므로, 대응을 안할 것이면 모든 Color 설정을 DarkMode에도 동일하게 나타나도록 하거나, info.plis
에서 설정을 변경하거나 하자.
고민하면서도 정답이 있을 수가 없음을 직감했지만 역시나 진리의 case by case 영역인 듯하다.
UserDefaults
로만 data를 관리하는 상황에서는 어차피 local storage에 접근하는 것이기에 저장하는 타이밍과 횟수가 critical하게 영향을 주진 않는다.
서버로 데이터를 관리해도 read/write 상황과 해당 에러 케이스를 개발자가 최대한 고려해서 작성해도 QA 팀의 테스트에서 또 잡아주고, 유저 에러 발생 시 긴급 업데이트로 대처해야 하는 등 완벽한 대응이란 유토피아처럼 지향점이지 도달할 수 없음을 인정하고 가야할 듯하다.
미래에 어떤 상황이 올 지 알 수가 없으니 주어진 상황에서 필요한 작업을 해야겠다. Apple도 iTunes를 Music과 TV로 분리하면서 다 뜯어 고친거보면 필요하니까 그랬겠지.