Sesac Recap Improvements

Heedon Ham·2023년 8월 15일
0

iOS 이것 저것

목록 보기
11/17
post-thumbnail

1994년 영화 사탄탱고의 러닝타임은 7시간 19분이다. 극장 개봉 당시 휴식 시간으로 10분 인터미션을 세 번 준 것으로 들은 바 있는데 이번 과제가 아마 첫 번째 인터미션이 아닐가 싶다.
언제부터 과제가 휴식이랑 같은 의미였냐

회고라 하면 너무 무게감이 클 것 같고 한줄평 정도의 제출한 버전 대비 개선점을 살펴보자.


View의 identifier들을 굳이 매번 String Literal로 가져야 하는가?

작동하는 것에 초점을 둔다면 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를 상속한다.

따라서 우리는 UIViewControllerReusableViewProtocol을 채택하면 앞으로 생성하는 모든 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)
    }
}

동그라미가 동일하게 안나오네?

CornerRadius

cornerRadiu 설정은 이전 글에도 설명했지만 원의 반지름을 설정해서 원을 그리면 해당 영역 외부는 마스킹해서 가리는 작업을 나타낸다.

async 글에서 한 번 더 정리할 거지만 storyboard와 code load 순서에 따른 문제였다.간단하게 순서를 작성하면

  • app이 load되고 storyboard 파일 (xib 포함)이 먼저 메모리로 올라옴
  • frame으로 잡힌 device 크기에 따라 원으로 나타내려는 사각형의 실제 값이 적용될 값으로 잡힘
    (앱이 실행될 실제 디바이스 크기가 적용된 값 X)
    e.g.) iPhone 12 mini로 개발, 실제 실행되는 폰은 14 Pro Max
  • ViewController의 viewDidLoad에서 UI configuration 수행
  • configuration 과정에서 cornerRadiu로 사각형 길이 값의 1/2을 적용
  • 이후 사용자 눈에 보일때는 실제 수행될 디바이스의 크기에 맞는 사각형 크기로 나타남

differentCornerRadiusValue

왼쪽이 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로 반지름이 설정되어 원을 나타낸다.

cornerRadiusDispatchQueueMainAsync


전역변수로 UI 설정 값 가져다 활용

C언어 작성 때 활용하던 습관이 남은 듯하다.
차라리 struct의 type property로 활용해서 여러 곳에서 사용하지만 매번 길게 작성하기 귀찮은 값들을 따로 instance 생성 없이 바로 가져다 활용하자.

struct UISetting {
	static let customBackgroundColor = UIColor(red: 23/255, green: 56/255, blue: 14/266, alpha: 1)
    //...중략...
}

UserDefaults용 관리

데이터 관리를 위해 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: passing domainName, and calling the removeObjectForKey: method on each of its keys.


DarkMode 대응 안할거면 확실하게

DarkMode는 유저가 시스템으로 설정가능한 세팅이므로, 대응을 안할 것이면 모든 Color 설정을 DarkMode에도 동일하게 나타나도록 하거나, info.plis에서 설정을 변경하거나 하자.

ignoreDarkModeAppearanceInInfoPlist


data를 저장하는 타이밍, 에러 케이스 관리

고민하면서도 정답이 있을 수가 없음을 직감했지만 역시나 진리의 case by case 영역인 듯하다.
UserDefaults로만 data를 관리하는 상황에서는 어차피 local storage에 접근하는 것이기에 저장하는 타이밍과 횟수가 critical하게 영향을 주진 않는다.
서버로 데이터를 관리해도 read/write 상황과 해당 에러 케이스를 개발자가 최대한 고려해서 작성해도 QA 팀의 테스트에서 또 잡아주고, 유저 에러 발생 시 긴급 업데이트로 대처해야 하는 등 완벽한 대응이란 유토피아처럼 지향점이지 도달할 수 없음을 인정하고 가야할 듯하다.
미래에 어떤 상황이 올 지 알 수가 없으니 주어진 상황에서 필요한 작업을 해야겠다. Apple도 iTunes를 Music과 TV로 분리하면서 다 뜯어 고친거보면 필요하니까 그랬겠지.

profile
dev( iOS, React)

0개의 댓글