오늘은 월요일. 지난 주에 과제의 기본 구현을 완성했지만, 도전 구현 과제를 위해 Clean Architecture 구현을 위해 공부하는 시간을 가졌다. MVVM에 대해서 이해하는 것에도 꽤 많은 시간이 걸렸듯이, 이번에도 시간이 제법 걸릴 것이라 생각했다. MVVM은 어떤 방식으로 설계를 해나가야할 지에 대해서는 이해하는데 오래 걸리지 않았지만, 이를 MVC 패턴으로 구현한 프로젝트를 리팩토링하는 작업을 진행하는 것에 있어서 시간이 오래걸렸다. 또한, 새 프로젝트를 시작한다고 가정했을 때, MVC를 거치지않고 MVVM으로 바로 코드를 구현하는 것도 아직은 시간이 꽤 걸릴 것 같다. 그러나 MVVM과 달리 Clean Architecutre에 대해서는 아직 감도 제대로 잡히지 않는다. "구조를 계층화하고 관심사를 분리하는 것" 너무 추상적이라 도무지 이해를 하기가 힘들다. 조금 더 찾아보고 구조화되어있는 실제 코드도 참고하여 분석하는 시간을 가져봐야겠다.
다음으로는, 지난 주에 정리하려고 했던 내용 중 시간이 부족하여 기록하지 못한 필수 구현의 마지막 문제를 정리하고자 한다.
요구사항은 이러하다. 사용자가 마지막으로 본 화면 정보를 CoreData에 저장한다. 앱을 재시작하면 환율 리스트 화면(ExchangeRateViewcontroller), 환율 계산기 화면(CalculatorViewController) 중 마지막으로 본 화면으로 이동한다.
데이터를 저장하기 위해서는 CoreData에 Entity를 추가해야 한다. 사용하는 Entity명은 정보를 저장하고있기에 Information으로 정했으며 Attributes는 다음과 같다.
code: String? (통화코드를 저장하기 위해)
page: String (리스트,계산기 화면인지를 저장하기 위해)
constant: Int16 (덮어쓰기를 위한 상수)
Entity를 생성했으면, 이제 CoreData의 Entity에 데이터를 저장하고 불러오는 로직을 구현해야 한다.
먼저, 데이터를 저장하는 로직을 구현한다. 기존에 사용하던 방식과 마찬가지로 fetchRequest와 predicate를 사용하여 조건에 부합하는 데이터를 찾는다. 우리는 데이터를 덮어쓰기 위해 사용할 것이기 때문에 constant가 1인 데이터를 찾는다.
let fetchRequest = Information.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "constant == 1")
do {
let informations = try self.container.viewContext.fetch(fetchRequest)
let information = informations.first ?? Information(context: self.container.viewContext)
}
위를 통해, 앱을 처음 실행하여 CoreData에 데이터가 하나도 없을 경우와 데이터가 존재할 경우 받아오는 로직을 구현했다. 이제, 데이터를 새로 쓰거나(앱 최초 실행) 데이터를 덮어쓰기한 후 저장하면 된다.
information.constant = 1
information.code = code
information.page = page
try self.container.viewContext.save()
저장되어있는 데이터를 불러오는 메서드도 구현해야 한다. 데이터를 저장하는 방식과 마찬가지로 fetchRequest와 predicate를 이용하여 constant가 1인 데이터를 찾아서 return하도록 구현하면 된다.
func fetchData() -> Information? {
let fetchRequest = Information.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "constant == 1")
do {
let informations = try self.container.viewContext.fetch(fetchRequest)
return informations.first
} catch {
print("데이터 로딩 실패")
return nil
}
}
saveInfo와 fetchData는 공통된 로직이 존재한다. fetchRequest를 생성하여 predicate로 조건을 주입하는 방식이 두 메서드에서 동일하게 나타나고 있는데, 이를 하나로 합칠 수는 없을까 ?
이제 InformationManager를 통해 구현한 메서드를 사용할 차례이다. 본인은 AppDelegate와 SceneDelegate 중 SceneDelegate를 이용하여 구현했으며, 선정에 이유는 없다. 그저, 지금까지 작성한 코드들은 대개 SceneDelegate 상에서 이뤄졌기 때문이다.
그렇다면 앱을 실행했을 때 페이지로 연결되는 로직과 앱이 종료되기 전 데이터를 저장하는 로직은 어디서 실행되어야 하는가?
본인은 willConnectTo(앱이 실행될 때), sceneDidEnterBackground(앱 밖으로 나갈 때)에 각각 로직을 구현했다.
구현된 로직을 정리한는 순서는 "저장 -> 불러와서 표시"로 진행하겠다.
willConnectTo에 사용될 데이터를 저장하는 로직을 수행해야 한다. 데이터를 저장하기 위해서 InformationManager의 saveInfo 메서드를 사용해야 한다. 이를 위해 infoManager를 생성하고, 현재 표시되고 있는 화면을 확인하기 위해 navigation을 생성하여 topViewController가 CalculatorViewController라면 svaeInfo의 code에 calculatorVC.viewModel.item.code를 page에 calculator를 넣어서 저장해준다. 아닐 경우에는 code에 nil을 넣고 page에 exchangeRate를 넣고 저장한다.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let infoManager = InformationManager(container: appDelegate.persistentContainer)
let navigation = window?.rootViewController as! UINavigationController
if let calculatorVC = navigation.topViewController as? CalculatorViewController {
infoManager.saveInfo(code: calculatorVC.viewModel.item.code, page: "calculator")
} else {
infoManager.saveInfo(code: nil, page: "exchangeRate")
}
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
저장하는 로직까지 구현이 끝났으니 이제 앱에 접근했을 때 화면을 변경하는 로직을 구현하면 된다. 먼저, sceneDidEnterBackground와 마찬가지로 appDelegate와 연결해주고 이번에는 CoreData의 저장된 Info정보에 접근하기 위해 infoManager와 History정보(rate에 관한 정보)에 접근하기 위해 HistoryManager를 생성해준다. 또한, 생성된 infoManager의 fetchData를 통해 현재 저장되어있는 데이터를 info에 받아준다.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let infoManager = InformationManager(container: appDelegate.persistentContainer)
let historyManager = HistoryManager(container: appDelegate.persistentContainer)
let info = infoManager.fetchData()
여기까지 데이터를 받아왔으면 이제 끝이다. 이를 이용해서 조건을 충족하면 생성한 navigationController에 pushViewController를 해주면된다. 이를 위해 navigationController를 하나 생성해준다. default로 진행되는 rootViewController는 ExchangeRateViewController로 둔다.
조건은 다음과 같다. info의 page가 calculator라면 code에 info의 code를 저장하고 history에는 historyManager의 fetchData를 통해 데이터를 받아준다.(rate를 사용하기 위함)
이를 사용하여 calculatorVM과 calculatorVC를 생성하여 push해주면 된다.
if info?.page == "calculator", let code = info?.code {
if let history = historyManager.fetchData(code: code) {
let exchangeRate = ExchangeRate(code: code, rate: history.rate)
let calculatorVM = CalculatorViewModel(item: exchangeRate)
let calculatorVC = CalculatorViewController(viewModel: calculatorVM)
navigationController.pushViewController(calculatorVC, animated: true)
}
}
window.rootViewController = navigationController