일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- human interface guidelines
- Modality
- contentInset
- Failed to register bundle identifier
- 부트캠프
- IOS
- Structures and Classes
- 책후기
- SWIFT
- Codegen
- Mock
- delegation
- roundingMode
- @available
- 스타트업주니어로살아남기
- NumberFormatter
- viewcontroller
- 독후감
- NotificationCenter
- SWIFTUI
- 독서후기
- 야곰아카데미
- mvvm
- Info.plist
- xcode
- UIResponder
- Navigation
- 아이폰
- 스위프트
- View Life Cycle
- Today
- Total
호댕의 iOS 개발
[TWL] 21. 12. 06 ~ 21. 12. 10 (JSON, TableView, AutoLayout) 본문
[TWL] 21. 12. 06 ~ 21. 12. 10 (JSON, TableView, AutoLayout)
호르댕댕댕 2021. 12. 10. 23:44방학에 푹 쉬고 다시 시작하는 첫 주이다. 방학 때 공부를 많이 못한 것은 아쉽기도 하지만 다시 체력을 보충하고 다시 공부에 전념할 수 있도록 마인드 셋팅을 할 수 있었다는 것에 대해선 만족한다.
이번 주는 iOS 개발을 하면서 중요한 'TableView'에 대해 학습했다.
아직 어려운 부분이 많지만 팀원들과 함께 공부하며 TableView의 기본적인 메서드와 호출 순서에 대해 어느정도 감을 잡을 수 있었다.
스스로도 계속 적용해보려고 노력하면 익숙해지겠지...?
21.12.06
TableView
- dataSource -> 데이터를 전달
- delegate -> 테이블 뷰가 수행하는 행동, 모습을 전달
- didSelectRowAt 메서드를 통해 어떤 섹션에 어떤 로우에 이벤트를 받았는지 알 수 있다.
이벤트가 발생할 때만 호출된다.
TableView에게 데이터를 전해줄 수 있도록 하는 약속을 프로토콜을 통해 수행하게 된다.
UITableViewDataSource
예를 들어 연락처에서 ㄱㄴㄷㄹ 순으로 나눠져 있는 것: Section 섹션 내 한 줄: Row
테이블뷰가 전체 섹션에 대해 물어봄 -> func tableView(UITableView, numberOfRowsInSection: Int) -> Int 화면에 뭘 보여줘야 하나 물어봄 -> func tableView(UITableView, cellForRowAt: IndexPath) → UITableView
테이블뷰의 경우 dataSource가 없다면 화면에 아무 것도 나오지 않게 된다. (화면이 보여지기 직전에 datasource를 불러와서 화면을 구성하게 된다. 따라서 여기서 복잡한 셋팅이나 데이터 가공을 최대한 하지 않는 것이 바람직하다)
Cell의 기본 스타일
참고: H.I.G table
- default: optional image가 row의 왼편에 있고, 그 옆에 title이 위치해있다. 추가 정보가 없을 때 유용함
- subtitle: title과 subtitle이 왼쪽에 정렬되어 있다. row가 비슷할 때 유용함(subtitle을 통해 구분)
- value1 (RightDetail): 왼쪽에 title, 오른쪽에 subtitle이 동일한 라인에 위치해있다.
- value2 (LeftDetail): 오른쪽에 title, 왼쪽에 subtitle이 있다.
Cell의 content 모양이 위 스타일에 따라 정해진다. 만약 위 4가지 스타일 중 원하는 모양이 없다면 custom을 통해 직접 정해주면 된다.
JSON
Javascript Object Notation의 줄인 말이다.
그렇다면 Javascript에서 사용되는 것 같은 JSON을 왜 배워야 하는 것일까? 컴퓨터의 데이터는 전부 0과 1로 구성되어 있으며, 이를 다른 컴퓨터로 전달하기 위해선 서로 0, 1을 해석하는 방법이 동일해야 한다. 앱을 끄면 인스턴스가 메모리에서 할당 해제되기 때문에 만약 데이터를 저장하거나, 어디로 전달하고 싶다면 인스턴스를 특정 포맷으로 변환시켜야 하는 것이다.
이때 주고 받는 데이터를 사람도 알아보기 쉽도록 하는 것이 바로 JSON이다!!
이전에는 XML을 사용했다. HTML과 유사한 형태인데 컴퓨터가 읽기에는 빠르지만 사람이 읽기에는 가독성이 떨어져 현재는 JSON을 표준처럼 다양하게 사용하고 있다.
Codable
JSON 객체에서 데이터 타입의 인스턴스를 디코딩/인코딩 할 수 있도록 하는 프로토콜이다. 만약 인코딩만 하고 싶다면 Encodable / 디코딩만 하고 싶다면 Decodable을 사용할 수도 있다. Codable은 인코딩과 디코딩 둘을 한 번에 지원하는 프로토콜이다. Codable만 채택해주면 자동으로 Encodable과 Decodable을 충족하게 된다.
Codable 을 채택하면 기본적인 데이터 포맷과 커스텀 인코더, 디코더가 제공하는 모든 포맷을 정렬할 수 있다.
예를 들어 PropertyListEncoder와 JSONEncoder 둘 다 사용해서 인코딩할 수 있다. 이는 심지어 JSON이나 property List로 다룰 수 있는 코드가 없더라도 가능하다.
Key의 이름 바꾸기
JSON 데이터의 이름과 다른 이름으로 데이터를 가져오려면 어떻게 해야할까?
struct ExpositionItem: Codable {
let name: String
let imageName: String
let shortDescription: String
let description: String
enum CodingKeys: String, CodingKey {
case name
case imageName = "image_name"
case shortDescription = "short_desc"
case description = "desc"
}
}
위 코드처럼 CodingKey 프로토콜을 채택해주면 된다. 또한 원래 이름을 String으로 적어줘야하기 때문에 String도 채택해줘야 한다.
이는 중첩된 형태로도 사용할 수 있다.
struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case additionalInfo
}
enum AdditionalInfoKeys: String, CodingKey {
case elevation
}
}
이렇게 해주고 let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo) 이렇게 nestedContainer 메서드를 사용하면 중첩된 형태처럼 사용할 수 있다.
Contents.JSON
처음에 프로젝트에서 expo_asset 파일을 받았을 때 Contents.JSON 파일의 역할이 무엇인지 알지 못했다. 하지만 차차가 설명도 잘 해주시고, 직접 Xcode의 Asset에 넣어보니 해당 파일의 역할이 무엇인지 알 수 있었다. asset catalog에 파일을 넣어줄 때 필요한 정보를 가지고 있으며 이 파일을 Asset에 넣어주게되면 자동으로 정보를 인식했다.
asset 별로 필요한 Key가 따로 정해져 있었다. 이는 Asset Catalog Format Reference에서 확인할 수 있었다.
image Set type idiom에서 universal을 선택하게 되면 기기, 플랫폼에 관계없이 전부 이미지를 사용할 수 있었다.
추가적으로 Asset catalog 파일을 추가하면 Terminal에 create mode ~~ 로 나왔다. (JSON, Image에 관계없이 mode를 생성했다고 나왔다)
21.12.07
NSDataAsset
정의: Asset catalog에 저장된 data set type 객체 Asset catalog에서 data에 해당하는 것들을 초기화할 수 있다. (image는 들어오지 않는다)
init?(name: NSDataAssetName)
초기화를 할 때에는 위 코드처럼 init을 해주면 된다. NSDataAssetName의 경우 String 타입으로 typealias를 통해 따로 이름을 만들어준 것이다. 초기화를 하게 되면 Assets에서 파일을 찾을 수 없는 상황이 존재하기 때문에 옵셔널로 값이 나오게 된다.
data 파일에 접근할 때에는 data 프로퍼티를 활용하여 접근할 수 있다.
import UIKit
enum JSONParser<Item: Decodable> {
static func decode(fileName: String) throws -> Item {
guard let jsonData = NSDataAsset(name: fileName) else {
throw ParsingError.fileNotFound
}
let jsonDecoder = JSONDecoder()
let decodedData = try jsonDecoder.decode(Item.self, from: jsonData.data)
return decodedData
}
}
- 여기서 고민했던 점
- 제네릭 타입의 이름을 어떻게 짓는 것이 좋을까?
- Item이 적합하다고 생각했는데 이미 ExpositionItem이라는 파일이 존재하여 Item으로 제네릭 타입의 이름을 짓는 것이 적합할지 고민.
- decode 함수의 매개변수 타입을 고민
- 공식문서에서도 NSDataAssetName으로 String 타입을 사용하고 있어 String 타입으로 매개변수를 받을 수 있도록 작성
decode 메서드
decode 메서드는 이름 그대로 JSON 객체로 부터 지정한 타입의 값을 반환해주는 메서드이다.
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
- type: 변환해주고 싶은 타입을 작성해준다. 프로젝트에선 JSON 객체에 맞춰 미리 만들어놓은 Exposition과 ExpositionItem을 넣어줄 수 있도록 제네릭의 타입을 넣어줬다.
- from data: 변환하고 싶은 데이터를 작성해준다. image를 제외한 data에 해당하는 데이터만 가져오면 되기 때문에 옵셔널 바인딩 해준 jsonData의 data 프로퍼티 값을 작성해줬다.
TableView Reuse Identifier
Table의 각 프로토타입 셀마다 String으로 할당해주게 된다.
즉, 스타일이 서로 다른 셀은 각각의 Reuse Identifier를 가지게 된다.
테이블 뷰는 내부의 큐에 이미 만들어진 cell을 보관한다. 만약 큐가 요청한 타입의 셀을 가지고 있다면 테이블 뷰는 셀을 반환한다. 만약 그렇지 않다면 새로운 셀을 스토리 보드의 프로토타입 셀을 사용해 만들어 낸다.
Reusing cell은 스크롤을 할 때 메모리 할당을 최소화하여 퍼포먼스를 향상시킨다.
TableView의 Accessary View
- none: 어떤 Accessary View도 없는 상태로 기본 값이다.
- disclosureIndicator: Accessary View를 탭하면 새로운 컨텐츠를 보여주고 싶을 때 사용한다.
- detailButton: 이걸 탭하면 row에 대한 정보를 확인해야 할 때 사용한다.
- detailDisclosureButton: 정보와 disclosure 컨트롤 둘 다 필요할 때 사용한다.
- checkmakr: 체크 표시를 보여주고 싶을 때 사용한다. 이 타입의 경우 터치를 추적하진 않는다.
21.12.09
# 객사오 4장 역할과 책임
책임: 객체지향 설계의 가장 중요한 재료로, 객체가 외부에 제공해 줄 수 있는 정보와 외부에 제공해줄 수 있는 서비스의 목록이다.역할: 책임의 집합이다.
- 역할의 대체 가능성은 행위 호환성을 의미하고, 행위 호환성은 동일한 책임의 수행을 의미한다.
# TableView 메서드 호출
- register() 메서드의 경우 처음 테이블 뷰를 구성할 때 한 번만 호출된다.
- 그 후 numberOfSections() 메서드가 호출된다.
- tableView(_:, numberOfRowsInSection:) 메서드는 먼저 마지막 row가 호출된 후 처음 row부터 차례로 호출된다.
- 재사용되는 cell은 기존 셀이 가지고 있던 이미지, 데이터를 그대로 가지고 들어가게 된다. 따라서 prepareForReuse 메서드가 반드시 필요하다.
<더 찾아볼 내용>
- willDisplayCell() 메서드: cell이 display 되기 전 항상 호출된다.
- cellForRowAt은 셀을 재사용할 때 사용하게 되는데 prefetch 된 경우 호출이 안 될 수 있다.
# Unit Test에서 어떤 오류를 뱉는지 확인하는 방법
기존의 XCTAssertThrowsError의 경우 Error를 throw하는지만 확인할 수 있었다.
func test_assetCatalog에_파일이_없는_경우_오류를_반환하는지() {
XCTAssertThrowsError(try JSONParser<ExpositionItem>.decode(fileName: "nonExist")) { error in
XCTAssertEqual(error as? ParsingError, ParsingError.fileNotFound)
}
}
위처럼 후행 클로저를 사용하게 되면 XCTAssertEqual을 활용하여 특정 에러를 throw하는지 확인할 수 있다.
# 프로젝트를 하며 새롭게 알게 된 내용
- 오토레이아웃에서 constraint to margin 체크를 풀지 않고 superView와 constraint를 설정해준 후, 체크를 해제해준다면, 자동으로 safe area로 조건이 걸리게 된다.
- cmd + shift + k -> clean build 단축키
- View의 Viewcontroller 선택후 size inspector를 fixed가 아닌 freedom으로 선택하면 사이즈를 늘릴 수 있다.
- textView는 작성이 되기 때문에 editable을 체크 해제해야 한다.
- main 스토리보드의 이름을 변경하게 된다면 변경해야 하는 곳
- Deployment Info → Main interface 수정
- Info.plist Scene Configuration - Application Session Role - Item 0 - Storyboard name 수정
- Main storyboard file base name 수정
- NavigationController를 추가하면 자동으로 tableView를 생성한다.
- tableView가 Navigation 방식을 자주 사용하기 때문에 자동으로 생성한다고 생각함.
- View에 기본적으로 Navigation이 옵셔널로 되어 있다.
- self.navigationController?.navigationBar.isHidden = true로 하면 네비게이션 바가 사라짐.하지만 이후 navigationController가 계속 사라지게 된다. 단 viewDidLoad에 설정을 해놓으면 다른 화면으로 이동했다가 다시 돌아오면 다시 navigationBar가 생기게 된다.
- titleLable.numberOfLines = 0 → 줄 수 제한없이 줄바꿈을 해줌
- contentMode
- scaleToFill: 이미지 뷰에 비율을 고려하지 않고 꽉 차게 들어간다.
- scaleAspectFit: 이미지 뷰에 이미지의 비율을 고려하여 들어간다.
- scaleAspectFill: 이미지 뷰에 이미지의 일부가 꽉 차게 들어간다.
- UITableViewController에는 delegate와 datasource 프로토콜이 채택되어 있다. 따라서 따로 프로토콜을 채택하지 않아도 된다.
- defaultContentConfigure
var content = cell.defaultContentConfiguration()
content.image = UIImage(named: expositionItem.imageName)
content.text = expositionItem.name
content.secondaryText = expositionItem.shortDescription
cell.contentConfiguration = content
# 의존성 주입
- 생성자 주입: 프로퍼티 주입 & 메서드 주입에 비해 단점이 없는 방식데이터를 받을 타입에 initializer를 생성한다.
init?(coder: NSCoder, expositionItem: ExpositionItem) {
self.expositionItem = [expositionItem]
super.init(coder: coder)
}
required init?(coder: NSCoder) {
self.expositionItem = []
super.init(coder: coder)
}
let expositionItemViewController = expositionItemStoryboard.instantiateViewController(identifier: "expositionItem") { coder in
return ExpositionItemViewController(coder: coder, expositionItem: selectedItem)
}
- 프로퍼티 주입의 단점
- 해당 프로퍼티가 외부에 공개된다.
- 해당 프로퍼티의 타입이 옵셔널이나 의미없는 기본값을 가지게 된다.
- 메서드 주입의 단점
- 해당 프로퍼티의 타입이 옵셔널이나 의미없는 기본값을 가지게 된다.
21.12.10
AutoLayout
오늘은 야곰닷넷에서 지금까지 들었던 내용을 포함하여 오토레이아웃 정복하기 강의를 정주행했다. (아직 다 듣진 못했다 😅)
CHCR
- Compression Resistance: 줄어들지 않으려고 버티는 힘
- Contents Hugging: 늘어나지 않으려고 버티는 힘
CHCR의 기준이 되는 것은 바로 Instrinsic content size이다.
- view(UIView & NSView)는 안에 뭐가 들어올지 모르기 때문에 고유 콘텐츠 사이즈가 존재하지 않는다.
- Labels, Buttons, Switches, text fields: 위치만 정해주면 되고 크기는 따로 정해주지 않아도 된다. → 자신의 사이즈는 추측할 수 있다.
- text view나 image view는 컨텐츠의 사이즈가 너무 다양하다.
- slider는 높이만 정의되어 있다.
Stack View
- axis: 가로(horizontal), 세로(vertical) → 스택 뷰의 종류가 2가지로 구분됨.
- distribution: 사이즈에 대한 이야기 → 스택뷰에 들어갈 때 사이즈 분배에 대한 것.
- fill - Stack View 전체를 채우게 됨 (내부 요소를 최대한 늘리게 됨) → hugging priority가 중요 stack view의 방향에 따라 채워짐.
- fillEqually - 요소를 전부 동등한 사이즈로 채우게 됨
- fillProportionally - 자신의 콘텐츠 사이즈 비율대로 채워지게 됨.
- equalSpacing - fill과 유사한데 여기서 동일한 spacing을 가져가게 됨.
- equalCentering - 각 뷰의 센터를 가상으로 연결한 사이 거리가 동일함.
- alignment: 위치에 관한 이야기 (스택 뷰 axis의 반대 방향을 결정함)
- fill - 전체를 정렬을 하지 않고 채우게 됨.
- leading - 앞에 맞춰서 정렬이 되고 콘텐츠 사이즈의 비율대로 정렬이 됨. (vertical)
- top - 위에 맞춰서 정렬이 되고 콘텐츠 사이즈 비율대로 정렬이 됨. (horizontal)
- firstBaseline - 첫째 텍스트의 아랫 줄에 맞춰 정렬
- center - 가운데 정렬
- trailing - 뒤에 맞춰 정렬이 되고 컨텐츠 사이즈의 비율대로 정렬 (vertical)
- bottom - 밑에 맞춰서 정렬이 되고 콘텐츠 사이즈 비율대로 정렬이 됨. (horizontal)
- lastBaseline - 마지막 텍스트의 아랫 줄에 맞춰 정렬
- spacing: Stack view의 요소 사이 간격
Code로 Constraints 주기
- Layout Anchors
button.translatesAutoresizingMaskIntoConstraints = false //오토레이아웃을 컨스트레인트로 자동으로 변경할 것인지 물어봄. 현재는 constraints를 사용해서 유명무실한 기능
let safeArea = view.safeAreaLayoutGuide // safe area의 영역을 알려주는 guide
// constraint가 NSLayoutContraint 객체를 생성해줌.
button.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 16).isActive = true // isActive를 하면 앵커를 만들자마자 활성화
button.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -16).isActive = true // 오른쪽에서 들어오기 때문에 constant를 음수로 설정!! 꼭 기억하자
let safeButtomAnchor = button.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
safeButtomAnchor.isActive = true
safeButtomAnchor.priority = .defaultHigh // .init(999)이렇게 숫자를 넣어줄 수도 있다.
let viewBottomAnchor = button.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -20)
viewBottomAnchor.isActive = true
오른쪽으로, 아래쪽으로 constant를 주면 양수 / 왼쪽으로, 위쪽으로 constant를 주면 음수로 작성했다. constraint를 주고 .isActive를 설정해주면 바로 constraint가 설정된다. 이를 설정하지 않으면 Result of call to 'constraint(equalTo:constant:)' is unused 이런 warning이 뜨고 constraint를 사용하지 않았다고 나온다.
- NSLayoutConstraint Class
let safeArea = view.safeAreaLayoutGuide
let leading = NSLayoutConstraint(item: button,
attribute: .leading,
relatedBy: .equal,
toItem: safeArea,
attribute: .leading,
multiplier: 1,
constant: 16)
// leading.isActive = true
let trailing = NSLayoutConstraint(item: button,
attribute: .trailing,
relatedBy: .equal,
toItem: safeArea,
attribute: .trailing,
multiplier: 1,
constant: -16)
//.leading과 .bottom으로 비교를 해도 컴파일 오류가 안나고 crash가 발생함. anchor의 경우 컴파일 오류가 발생함.
let bottomSafeArea = NSLayoutConstraint(item: button,
attribute: .bottom,
relatedBy: .equal,
toItem: safeArea,
attribute: .bottom,
multiplier: 1,
constant: -16)
let bottomView = NSLayoutConstraint(item: button,
attribute: .bottom,
relatedBy: .lessThanOrEqual,
toItem: view,
attribute: .bottom,
multiplier: 1,
constant: -20)
bottomView.priority = .defaultHigh
NSLayoutConstraint.activate([leading, trailing, bottomSafeArea, bottomView])
NSLayoutConstraint를 사용하면 잘못된 constraint를 설정해도 컴파일 오류가 발생하지 않는다 Layout anchors를 설정하는 것보다 구체적으로 layout을 설정할 수 있다. 각각 .isActive를 해주지 않고 맨 밑 코드처럼 배열로 묶어서 한번에 activate 설정을 할 수 있다.
프로젝트 리팩토링
JSONParser의 타입 및 구현 방법 고민
이번 JSON 데이터를 parsing하는 타입을 모델에 넣어놨었다. 또한 당시 인스턴스를 따로 생성하지 않고 바로 타입에 접근하여 decode 메서드를 호출하기 위해 열거형을 사용했다.
하지만 리뷰어인 찰리가 JSONParser의 경우 단순히 데이터를 parsing하는 기능만 수행하기 때문에, 이는 명확히 Util이라고 말씀해주셨다. 또한 static으로 타입 메서드를 만들 경우 사용여부와는 관계 없이 항상 메모리에 살아있기 때문에 필요할 때만 인스턴스를 생성하는 것이 좋지 않냐고 말씀해주셨다.
그래서 JSONParser는 상속이나 참조가 필요하지 않다고 생각해서 구조체로 구현했고 decode 메서드를 사용하는 ViewController에 직접 인스턴스를 생성하는 방법으로 수정을 했다.
반드시 필요한 경우가 아니라면 static의 사용은 지양해도 좋을 것 같다.
Outlet을 강제 언래핑하면 안 좋은 것일까?
IBOutlet 변수가 UI 요소에 연결되지 않은 경우, 또는 View가 Load되지 않은 경우에 IBOutlet 변수에 접근하려고 한다면 nil이 할당될 가능성이 존재한다. 참고 : https://cocoacasts.com/should-outlets-be-optionals-or-implicitly-unwrapped-optionals
이 경우 앱이 크래쉬가 나기 때문에 강제 언래핑을 주의해야 한다. (역시 강제 언래핑은 최대한 지양하는 것이 좋은 것 같다 🥲)