Table View
적용Navigation Controller
적용enum
의 associated value
를 이용한 활용성 및 재사용성 향상Numberformatter
의 사용지난 포스팅에서는 JSON 디코딩을 지원하는 타입을 만들어 보았습니다. 이번에는 디코딩된 JSON 데이터를 활용해서 Table View
를 만들어보겠습니다!
먼저 스토리보드에서 View Controller
에 Table View
를 올려 놓습니다.
화면에서 Table View
가 존재할 위치를 잡습니다.
Table View Cell
을 Table View
위에 올려둡니다.
Cell
을 선택(클릭)해서 Attributes Inspector
의 Reuse Identifier
의 이름을 설정해줍니다.
Cocoa Touch Class
형식으로 UIViewController
타입을 가진 파일을 하나 만듭니다. 초기 View Controller
를 Table View
로 만들고 싶을 경우에는 새로 만들지 않고 기존 파일을 활용하셔도 됩니다.생성한 ViewController
에 JSON 데이터를 디코딩하기 위한 코드를 추가합니다. Table View
를 연결할 IBOutlet
도 함께 만들어줄게요.
class ViewController: UIViewController {
// MARK: - Properties
@IBOutlet private weak var tableView: UITableView!
private var artworks: [Artwork] = []
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
let decodedResult: Result = ExpoJSONDecoder.decode(to: [Artwork].self,from: ExpoData.artworks)
switch decodedResult {
case .success(let result):
artworks = result
case .failure(let error):
debugPrint(error)
}
}
}
그리고 스토리보드에서 미리 만들어둔 View Controller
의 Identity Inspector
의 Class
란에 새로 만든 View Controller
의 이름을 작성합니다.
Connections Inspector
으로 이동해서 만들어준 IBOutlet
과 UI 요소로 만들어둔 Table View
를 이어줍니다.
여기에서 tableView
의 Cell
에 들어갈 내용이 Basic
, Right Detail
등 기본적으로 제공되는 스타일로 커버가 가능한 경우는 해당 스타일을 이용하시면 됩니다. 하지만 저처럼 그림과 제목, 짧은 설명과 이동 아이콘까지 여러 내용을 Cell
에 넣고 싶으시다면 UI 요소를 활용해 직접 Cell
을 디자인해보세요.
직접 Custom Cell
을 만들어주신 경우, Cocoa Touch Class
형식으로 UITableViewCell
타입을 가진 파일을 하나 만들고, 데이터를 넣어줄 요소들을 IBOutlet
으로 정의한 다음 스토리보드에서 Cell
의 Custom Class
를 지정해주고, 각 IBOutlet
을 UI 요소와 연결해주세요.
import UIKit
class ArtworkTableViewCell: UITableViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var shortDescriptionLabel: UILabel!
}
계속해서 TableView
와 함께 일해줄 delegate
와 dataSource
를 지정해주어야 하는데요, 사전 작업으로 UITableViewDelegate
와 UITableViewDataSource
프로토콜을 채택해주고 각 프로토콜의 요구사항을 충족시켜주어야 해요.
요구사항을 보니 UITableViewDataSource
프로토콜에 optional
로 표기되지 않은 두 가지 메서드를 구체화해주는 것이네요.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
code
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
code
}
첫 번째 메서드는 tableView(_:numbersOfRowsInSection:)
로, 한 섹션에 몇 개의 열(Row)가 들어갈 것인가를 알려주는 메서드에요. 두 번째 메서드인 tableView(_:cellForRowAt:)
은 tableView
의 각 열에 들어갈 셀(Cell
)의 데이터를 채워 UITableViewCell
타입으로 반환하는 메서드입니다. 작성된 코드를 보시면 어떤 내용인지 바로 이해가 되실거에요.
// MARK: - Table view data source
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return artworks.count // 디코딩된 데이터의 배열 요소 갯수를 열 개수로 반환
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Reuseable Cell 선언
let cell: ArtworkTableViewCell = tableView.dequeueReusableCell(
withIdentifier: "artworkCell",
for: indexPath
) as! ArtworkTableViewCell
// Reuseable Cell에 데이터 할당
cell.thumbnailImageView.image = UIImage(named: artworks[indexPath.row].imageName)
cell.titleLabel.text = artworks[indexPath.row].name
cell.shortDescriptionLabel.text = artworks[indexPath.row].shortDescription
return cell
}
}
이전 포스팅에서 다룬 적이 있지만 delegate
는 Table View
대신에 스크롤링 지원을 위해 셀의 높이를 계산한다든지 셀이 선택되는 등 테이블과의 상호작용을 다루어주고, dataSource
는 테이블에 데이터와 셀을 제공하는 역할을 수행합니다. 이 친구들은 두 가지 방법으로 지정해줄 수 있어요. 둘 중에 하나만 하셔도 됩니다.
Table View
를 구현하고자하는 View Controller
에 아래와 같은 코드를 작성합니다.override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Connections inspector
탭에서 dataSource
와 delegate
를 View Controller
에 연결합니다.이제 오토 레이아웃을 조금 적용해주면 아래와 같은 화면을 확인하실 수 있습니다. 세부 화면으로 이동할 때 데이터를 이동시키는 등 세부 화면으로의 이동 방법은 다음 포스팅에서 다루도록 하겠습니다~!
구조 상으로 Navigation Controller
를 적용해야 하는 경우가 있는데요, 아래와 같이 하시면 됩니다!
Navigation Controller
를 적용하고자 하는 View Controller
를 선택합니다.XCode
메뉴 바에서 Editor
→ Embed in
→ Navigation Controller
를 선택합니다.Navigation Controller
의 흐름 속에서 특정 화면에서는 Navigation Bar
가 보이지 않게끔 설정해야할 때가 있죠? 그럴 때는 코드를 통해 해결할 수 있는데요, 두 가지 방법을 알아보겠습니다.
setNavigationBarHidden(_:animated:)
메서드 이용Navigation Bar
를 숨기고 나타낼 때 애니메이션을 적용할 수 있습니다.override func viewWillAppear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(true, animated: true) // 뷰 컨트롤러가 나타날 때 숨기기
}
override func viewWillDisappear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(false, animated: true) // 뷰 컨트롤러가 사라질 때 나타내기
}
isNavigationBarHidden
프로퍼티 이용Navigation Bar
를 숨기고 나타낼 수 있습니다. 애니메이션이 필요 없는 경우에 사용합니다.override func viewWillAppear(_ animated: Bool) {
navigationController?.isNavigationBarHidden = true // 뷰 컨트롤러가 나타날 때 숨기기
}
override func viewWillDisappear(_ animated: Bool) {
navigationController?.isNavigationBarHidden = false // 뷰 컨트롤러가 사라질 때 나타내기
}
열거 타입에 연관 값을 이용하여 활용성과 재사용성을 향상시키는 사례를 살펴보겠습니다. 에러 타입을 정의하기 위해 열거 타입을 활용하는 경우, 아래와 같이 코드를 작성할 수 있습니다.
enum ExpoAppError: Error {
case invalidJSONFileName
case invalidJSONFormat
case foundNil
case numberFormattingFailed
case unknownError
}
extension ExpoAppError: CustomDebugStringConvertible {
var debugDescription: String {
switch self {
case .invalidJSONFileName:
return "📃 존재하지 않는 JSON 파일이에요. 파일 이름을 다시 확인해주세요!"
case .invalidJSONFormat:
return "📑 JSON 형식이 맞지 않아요. 데이터를 다시 확인해주세요."
case .foundNil:
return "😵 이 값은 nil이에요!"
case .numberFormattingFailed:
return "😅 숫자 형식 변환에 실패했어요! 숫자를 다시 확인해주세요."
case .unknownError:
return "𝙛 알 수 없는 에러가 발생했어요!"
}
}
}
그럼 이런 에러는 아래와 같이 반환하거나 던질(throw) 수 있습니다.
static func decode<Decoded>(
to type: Decoded.Type,
from jsonFileName: String
) -> Result<Decoded, ExpoAppError> where Decoded: Decodable {
var decodedResult: Decoded
guard let jsonData: NSDataAsset = NSDataAsset(name: jsonFileName) else {
return .failure(ExpoAppError.invalidJSONFileName) // 에러 반환
}
do {
decodedResult = try jsonDecoder.decode(Decoded.self, from: jsonData.data)
} catch {
return .failure(ExpoAppError.invalidJSONFormat) // 에러 반환
}
return .success(decodedResult)
}
이 경우, 유사한 에러이지만 발생 위치가 다를 경우 이를 특정하기 위해 새로운 에러 케이스를 작성해야 합니다. 하지만 열거 타입의 연관 값(associated value
)를 활용하면 에러와 관련된 값을 에러 메시지에 포함하여 나타낼 수 있으면서도 같은 케이스를 활용할 수 있게끔 할 수 있습니다. 적용은 아래와 같이 하면 됩니다.
enum ExpoAppError: Error, Equatable {
case invalidJSONFileName(String) // 연관값의 타입을 소괄호 내부에 명시
case invalidJSONFormat(String)
case foundNil(String)
case numberFormattingFailed(Int)
case unknownError(String)
}
extension ExpoAppError: CustomDebugStringConvertible {
var debugDescription: String {
switch self {
case .invalidJSONFileName(let fileName):
return "📃 존재하지 않는 JSON 파일이에요. 파일 이름을 다시 확인해주세요! 파일 이름: \(fileName)" // 연관 값 활용
case .invalidJSONFormat(let fileName):
return "📑 JSON 형식이 맞지 않아요. 데이터를 다시 확인해주세요. 파일 이름: \(fileName)"
case .foundNil(let valueName):
return "😵 이 값은 nil이에요! 값 이름: \(valueName)"
case .numberFormattingFailed(let number):
return "😅 숫자 형식 변환에 실패했어요! 숫자를 다시 확인해주세요. 입력한 숫자: \(number)"
case .unknownError(let location):
return "𝙛 알 수 없는 에러가 발생했어요! 발생 위치: \(location)"
}
}
}
static func decode<Decoded>(
to type: Decoded.Type,
from jsonFileName: String
) -> Result<Decoded, ExpoAppError> where Decoded: Decodable {
var decodedResult: Decoded
guard let jsonData: NSDataAsset = NSDataAsset(name: jsonFileName) else {
return .failure(ExpoAppError.invalidJSONFileName(jsonFileName)) // 입력한 파일 이름과 함께 반환
}
do {
decodedResult = try jsonDecoder.decode(Decoded.self, from: jsonData.data)
} catch {
return .failure(ExpoAppError.invalidJSONFormat(jsonFileName))
}
return .success(decodedResult)
}
멋지지 않나요? associated value
를 활용해서 열거 타입을 더 유용하게 활용해봅시다!
단위가 큰 십진수는 컴마를 통해 구분하는 경우가 많죠. 이런 형식을 지원해야할 경우에는 직접 로직을 만들 수도 있지만 이미 있는 NumberFormatter
타입을 이용해서 간단히 숫자 형식을 변환할 수 있습니다.
저는 메서드를 하나 만들어 보았는데요, 저처럼 메서드로 구성할 수도 있고 프로퍼티의 getter
를 통해 읽어올 때 형식을 설정해줄 수도 있습니다.
func formattedNumber(_ number: Int) -> Result<String, ExpoAppError> {
let numberFormatter: NumberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
guard let formatted: String = numberFormatter.string(from: NSNumber(value: number)) else {
return .failure(ExpoAppError.numberFormattingFailed(number))
}
return .success(formatted)
}
이제 NumberFormatter
를 활용해서 숫자를 더 알아보기 쉽게 나타낼 수 있게 되었습니다!