프로젝트 기간: 2021년 7월 5일 ~ 7월 16일
with Coden, Namu, Nala
- Codable이 선언된 형태
typealias Codable = Decodable & Encodable
Decodable
또는Encodable
둘 중 하나의 프로토콜만 채택하는 경우 해당 타입이 특정 목적으로만 사용된다는 것을 명시할 수 있게 된다. (디코딩 또는 인코딩 목적으로만 사용된다!)Codable
을 채택하는 경우 인코딩과 디코딩이 모두 가능한 상태가 된다.- Codable은 Struct만 채택이 가능한 프로토콜이며 구조체 내부의 모든 프로퍼티들이 Codable을 준수하여야만 한다.
-
json에서의 키값과 struct의 프로퍼티명이 일치하지 않는 경우 유용한 프로토콜
-
이 프로토콜을 채택하는 enum은 이름을
CodingKeys
라고 해야 한다.➡️ 왜 이름이
CodingKeys
이어야 하는지에 대해 의문을 가졌었고 이에 대한 의논이 이루어졌던 링크를 얻을 수 있었다. -
CodingKeys는 Codable struct의 Nested Type으로써 UML에 작성해주었었는데 이는 적절치 못하다고 한다. Codable struct가 이 enum을 쓴다기보다는 Codable 자체에서 쓰이는 부분이라서! ➡️ UML 수정을 하면서 삭제
{
"data" : [
{
"filename" : "뭐시기뭐시기.json",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
- Asset에 데이터를 추가하는 경우 자동으로 생성되는 파일
- 데이터에 대한 데이터, 메타데이터 파일이다.
- 이 친구는 Model 타입을 따로 만들어줄 필요가 없다.
Protocol이 AnyObject를 상속하면 해당 프로토콜은 클래스만 채택할 수 있게 됩니다. 이는 `Object와 DelegateObject간 순환참조`가 발생하는 것을 막기위함입니다. 위에 제가 그린 UML에도 나와있듯이 SomeObject와 DelegateObject는 순환참조가 발생할 가능성이 존재합니다.(서로를 참조하는 화살표가 존재) 만약 Delegate 프로토콜을 구조체나 열거형도 채택할 수 있도록 AnyObject를 상속하지 않고 그냥 내버려 둔다면 DelegateObject가 구조체나 열거형으로 만들어질 수 있습니다. 이러면 SomeObject는 DelegateObject를 무조건 강한참조로 가지고 있을 수 밖에 없게 됩니다.(Delegate Object가 value type이므로 weak reference를 할 수 없게 됨) 이 때 반대로, DelegateObject가 SomeObject를 참조하지 않는다면 별 문제가 되지 않겠지만, 만약 프로퍼티나 지역변수로 SomeObject를 참조하게 된다면 `순환참조 문제`가 발생하게 됩니다. 이는 인스턴스가 메모리에서 해제되지 않는다는 것을 의미합니다. (DelegateObject가 지역변수로 단순히 특정 메소드 내에서만 SomeObject를 의존한다고 하더라도 순환참조 문제는 발생할 수 있다고 생각합니다. 해당 지역변수가 어떻게 활용될지 모르니까요.) 이 이유때문에 Delegate Protocol이 AnyObject를 상속받았다고 생각합니다. 😆 이렇게 함으로써 Delegate Object는 클래스(참조 타입)임이 보장되고, Object에서는 이 Delegate Object를 weak참조할 수 있게 되어 순환참조 문제를 미연에 방지할 수 있게 됩니다. - 관련 문서 https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html |
- TableViewController는 tableView 프로퍼티를 기본적으로 가지고 있으며 DataSource, Delegate 메소드에 대한 기본 구현이 제공되는 클래스이다. (따라서 원하는 기능을 제공하기 위해서는 해당 메소드들을
override
해주어야 한다. ) - Apple의 Tutorial에서는 대부분 TableViewController를 쓰지만 실제로 이 방식은 거의 사용하지 않는다.
- DataSource나 Delegate라는 책임에 대한 부분이 명시적으로 보이지 않아서 쓰지 않는 듯 하다.
- TableViewController를 쓰는 경우 화면을 전부 TableView로만 채워야 하는 단점도 존재한다.
https://developer.apple.com/documentation/uikit/uitableviewcell
-
기본 스타일 셀에는
basic
,right detail
,left detail
,subtitle
이 존재한다.
- 셀은 재사용된다. 화면에서 사라진 셀은 다음에 나타날 셀을 표현하기 위해 재사용된다. (매번 이니셜라이징 되지 않음)
- 이는 퍼포먼스를 위한 것이다. (뷰를 매번 새롭게 만드는 것은 오버헤드가 크다.)
오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간 · 메모리 등을 말한다. - 위키
- TableViewDataSource의
tableView(_:cellForRowAt:)
에서tableView
의dequeueReusableCell(withIdentifier:for:)
을 통해 재사용할 셀을 가져올 수 있다. - 재사용할 셀이 있다면 해당 셀을 가져다가 쓸 수 있으며, 여유 셀이 없는 경우 새로 만들게 된다.
-
in
UITableViewCell
기본 제공 스타일로 UITableViewCell을 사용하고자 하는 경우 내부 컴포넌트들을 정의하는 방식이 iOS 14를 기준으로 바뀌었다. 이전에는 cell의 imageView 등의 프로퍼티에 바로 접근하여 세팅하는 방식이었다면 이제는
defaultContentConfiguration()
을 사용해야 한다. -
zeddiOS 블로그 참고
-
뷰 컨트롤러간 데이터를 전달하는 방법에는 여러가지가 있다. 우리는 그 중
segue를 이용한 방식
과TableViewDelegate 내에서 다음 VC를 instantiate하는 방식
을 고민했었다. 각 방식의 장점은 다음과 같다.방식 이름 장점 segue(prepareFor) 스토리보드에서 화면의 흐름을 알 수 있다 storyboard.instantiate in TableViewDelegate 코드에서 흐름을 알 수 있다
디버깅에 용이하다➡️ 두 방식 모두 스토리보드에 의존적이다.
import UIKit
class ParsingManager {
//MARK: Singleton Instance
static let shared = ParsingManager()
//MARK: Initializer
private init() {}
}
//MARK:- Parse JSON
extension ParsingManager {
func parse<T: Codable>(from fileName: String, to type: T.Type) -> Result<T, ParsingError> {
guard let asset = NSDataAsset(name: fileName) else {
return .failure(.dataSetNotFound)
}
guard let data = try? JSONDecoder().decode(type, from: asset.data) else {
return .failure(.decodingFailed)
}
return .success(data)
}
}
-
제네릭을 사용하면 ''정의한 요구사항에 따르는 모든 타입''에 대응되는 ''유연하고 재사용 가능한 함수 및 타입''을 작성할 수 있다.
-
메타타입은 타입에 대한 타입이다. 메타타입의 인스턴스는 해당 타입 그 자체를 의미한다. 타입을 값처럼 표현할 수 있다.
-
호출하는 곳에서는
T
라는 타입이 무엇인지 알려주려면 최소한 T.Type은 넘겨줘야 한다.(또는 T 인스턴스를 넘겨서 인스턴스를 넘겨줌과 동시에 T 타입에 대해 알려줄 수도 있다.) -
T가 Codable을 준수하면 [T]도 Codable을 준수한다. 때문에 위의
parse(from:to:)
메소드를 호출하는 곳에서는 메타타입 부분에 Array 메타타입을 넘겨줄 수도 있다.struct SomeModel: Codable {...} ParsingManager.shared.parse(from: "", to: SomeModel.self) ParsingManager.shared.parse(from: "", to: [SomeModel].self)
- ViewController에 대한 Unit Test는 아키텍처에 따라 다를 수 있겠지만 MVC에서의 VC는 UnitTest보다는 UITest에 더 가깝다. (뷰의 라이프싸이클에 의존적이므로)
- Singleton Object는 별도의 변수에 할당하여 사용하지 않고 바로 사용하는 것이 일반적이다.
- json 포맷도 사실은 텍스트를 기반으로 한 포맷이다.
- 결론부터 말하자면 JSON 형식에 맞춰서 작성되었다면 텍스트 파일이라고 하더라도 파싱이 가능하다.
- JSON 형식과 파일의 포맷(확장자) 및 인코딩방식 모두 중요하다.
- JSON 형식에 관해 - 문법적인 부분이므로 당연히 맞춰주어야 한다.
- 파일의 포맷(확장자)에 관해 - Text Editor를 이용하여 만든 rtf파일은 파싱이 안되었다.(인코딩 방식을 UTF-8로 변경해봐도 안되었음)
- 인코딩 방식에 대해 - 텍스트 파일이여도 인코딩 방식이 다른 경우 파싱이 되지 않았다.('한국어 MacOS' 인코딩 기준)
-
Handling View Rotations부분에 대하여
- 방향에 대한 부분은 root VC만이 관리할 수 있다고 한다. 이 위에 쌓이는 VC들은 rootVC의 규칙을 따르는 것으로 보임(Navigation Controller를 사용하는 경우 root는 이 Navigation Controller가 됨)
→ 따라서 Navigation Controller에 쌓이는 VC들의 뷰 방향을 설정하려면 가장 먼저 내비게이션 컨트롤러의 뷰 방향 설정을 해줘야 함
Disable Rotation of UIViewController Embedded Into UINavigationController - Apps Developer Blog
- 위 링크대로 해주면 첫번째 VC의 방향은 세로로 고정이 된다.
- 근데 아래쪽 Navigation Controller에 대한 extension 작성 시 왜 get으로 한번 더 감싸줬는지 모르겠다. 안써줘도 될텐데..
- Navigation Controller에 VC가 올라올 때마다 Navigation Controller extension의 세 개 연산 프로퍼티들이 호출?되는 것으로 보인다.
class Yagom {
private(set) var age = 21
func updateYagomAge(to age: Int) {
if age <= 21 {
self.age = age
}
}
}
class Yagom2 {
private var _age = 21
var age: Int {
get {
return self._age - 1 //미국식 나이로 알려주기
}
set {
if newValue <= 21 {
self.age = newValue
}
}
}
}
위와같이 쓸거라면 get이 들어가는게 적절한데 위 링크에서는 왜 get을 넣었을까..Yagom2
클래스에 작성한 방식처럼 연산프로퍼티를 getter setter로써 쓴다면 get과 set이라는 것을 내부에서 나눠주는게 별로 이상하지 않은데, 링크 에서는 값을 얻어가는 부분만 있음에도 내부를 get으로 한번 더 감싼 부분이 이해가 가지 않았다.
"위와같이(Yagom2 클래스의age
연산 프로퍼티같이) 쓸거라면 get이 들어가는게 적절한데 왜 링크에서는 값을 얻는 부분만 있음에도 get을 넣었을까!" 실제로 get을 빼도 동작은 문제가 없었다.
아래는 의문을 가진 부분에 대한 코드이다.
extension UINavigationController {
override open var shouldAutorotate: Bool {
get {
if let visibleVC = visibleViewController {
return visibleVC.shouldAutorotate
}
return super.shouldAutorotate
}
}
override open var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation{
get {
if let visibleVC = visibleViewController {
return visibleVC.preferredInterfaceOrientationForPresentation
}
return super.preferredInterfaceOrientationForPresentation
}
}
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask{
get {
if let visibleVC = visibleViewController {
return visibleVC.supportedInterfaceOrientations
}
return super.supportedInterfaceOrientations
}
}
}
supportedInterfaceOrientations
이 각각의 VC에서 설정하는 것이라면
navigationControllerSupportedInterfaceOrientations(_:)
는 delegate가 한번에 설정해서 관리하는 것 같다.
import UIKit
//root view controller
class WorldsExpoViewController: UIViewController {
//MARK: LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
//MARK:- Fix Orientation
extension WorldsExpoViewController: UINavigationControllerDelegate {
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
if navigationController.visibleViewController == self {
return .portrait
} else {
return .all
}
}
}
//MARK:- Fix Orientation(수정한 버전)
extension WorldsExpoViewController: UINavigationControllerDelegate {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.portrait
}
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return navigationController.visibleViewController?.supportedInterfaceOrientations ?? .all
}
}
-
일단은 UINavigationControllerDelegate의 책임은 Root ViewController에 부여해주었다. 왜냐하면 Pop되지는 않을테니까. 뭔가 다른 좋은 방법이 있으려나 (일단 retain cycle은 미발생)
-
방향에 대한 protocol들을 만들어준 다음 모든 VC가 채택하게 하면
navigationControllerSupportedInterfaceOrientations(_:)
이 안에서는 조건문을 나누기가 편해지겠지? 프로토콜에 대한 것만 체크하면 될테니까.→ 아니면 위의 수정한 버전처럼 해줘도 될 듯.(모든 VC가 각자의
supportedInterfaceOrientations
설정 필요. 설정 안해두면 기본값 사용) -
VC가 내비게이션 컨트롤러에 올라올 때마다
navigationControllerSupportedInterfaceOrientations(_:)
가 호출되는 것으로 보인다.