활동 요약

  • 주간 TIL 정리
  • 만국박람회 프로젝트 Step 2 진행
    • Table View 적용
    • Navigation Controller 적용
    • 에러 타입 enumassociated value를 이용한 활용성 및 재사용성 향상
    • Numberformatter의 사용

학습 내용

JSON 데이터로 Table View 만들기

지난 포스팅에서는 JSON 디코딩을 지원하는 타입을 만들어 보았습니다. 이번에는 디코딩된 JSON 데이터를 활용해서 Table View를 만들어보겠습니다!

Table View와 Cell UI 구성

  • 먼저 스토리보드에서 View ControllerTable View를 올려 놓습니다.

  • 화면에서 Table View가 존재할 위치를 잡습니다.

  • Table View CellTable View 위에 올려둡니다.

  • Cell을 선택(클릭)해서 Attributes InspectorReuse Identifier의 이름을 설정해줍니다.

Table View의 코드를 작성하기 위한 타입 생성 및 관련 코드 작성

  • Cocoa Touch Class 형식으로 UIViewController 타입을 가진 파일을 하나 만듭니다. 초기 View ControllerTable 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)
      }
    }
}

Outlet 연결

그리고 스토리보드에서 미리 만들어둔 View ControllerIdentity InspectorClass란에 새로 만든 View Controller의 이름을 작성합니다.

Connections Inspector으로 이동해서 만들어준 IBOutlet과 UI 요소로 만들어둔 Table View를 이어줍니다.

Custom Cell 세부 UI 구성

여기에서 tableViewCell에 들어갈 내용이 Basic, Right Detail 등 기본적으로 제공되는 스타일로 커버가 가능한 경우는 해당 스타일을 이용하시면 됩니다. 하지만 저처럼 그림과 제목, 짧은 설명과 이동 아이콘까지 여러 내용을 Cell에 넣고 싶으시다면 UI 요소를 활용해 직접 Cell을 디자인해보세요.

Custom Cell 타입 생성 및 관련 Outlets 작성

직접 Custom Cell을 만들어주신 경우, Cocoa Touch Class 형식으로 UITableViewCell 타입을 가진 파일을 하나 만들고, 데이터를 넣어줄 요소들을 IBOutlet으로 정의한 다음 스토리보드에서 CellCustom Class를 지정해주고, 각 IBOutlet을 UI 요소와 연결해주세요.

import UIKit

class ArtworkTableViewCell: UITableViewCell {
  @IBOutlet weak var thumbnailImageView: UIImageView!
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var shortDescriptionLabel: UILabel!
}

Table View delegate 및 dataSource 지정을 위한 프로토콜 채택

계속해서 TableView와 함께 일해줄 delegatedataSource를 지정해주어야 하는데요, 사전 작업으로 UITableViewDelegateUITableViewDataSource 프로토콜을 채택해주고 각 프로토콜의 요구사항을 충족시켜주어야 해요.

요구사항을 보니 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 및 dataSource 지정

이전 포스팅에서 다룬 적이 있지만 delegateTable View 대신에 스크롤링 지원을 위해 셀의 높이를 계산한다든지 셀이 선택되는 등 테이블과의 상호작용을 다루어주고, dataSource는 테이블에 데이터와 셀을 제공하는 역할을 수행합니다. 이 친구들은 두 가지 방법으로 지정해줄 수 있어요. 둘 중에 하나만 하셔도 됩니다.

  1. 코드를 이용한 방법
    Table View를 구현하고자하는 View Controller에 아래와 같은 코드를 작성합니다.
override func viewDidLoad() {
  super.viewDidLoad()
  tableView.delegate = self
  tableView.dataSource = self
}
  1. 스토리보드를 이용한 방법
  • 스토리보드의 뷰 컨트롤러에서 테이블을 선택합니다.
  • 아래 화면과 같이 Connections inspector 탭에서 dataSourcedelegateView Controller에 연결합니다.

확인

이제 오토 레이아웃을 조금 적용해주면 아래와 같은 화면을 확인하실 수 있습니다. 세부 화면으로 이동할 때 데이터를 이동시키는 등 세부 화면으로의 이동 방법은 다음 포스팅에서 다루도록 하겠습니다~!

구조 상으로 Navigation Controller를 적용해야 하는 경우가 있는데요, 아래와 같이 하시면 됩니다!

  1. 스토리보드에서 Navigation Controller를 적용하고자 하는 View Controller를 선택합니다.
  2. XCode 메뉴 바에서 EditorEmbed inNavigation Controller를 선택합니다.
  3. 적용되었습니다!

특정 View Controller에서 Navigation Bar 숨기기

Navigation Controller의 흐름 속에서 특정 화면에서는 Navigation Bar가 보이지 않게끔 설정해야할 때가 있죠? 그럴 때는 코드를 통해 해결할 수 있는데요, 두 가지 방법을 알아보겠습니다.

  1. setNavigationBarHidden(_:animated:) 메서드 이용
  • 이 메서드를 이용하면 Navigation Bar를 숨기고 나타낼 때 애니메이션을 적용할 수 있습니다.
override func viewWillAppear(_ animated: Bool) {
  navigationController?.setNavigationBarHidden(true, animated: true) // 뷰 컨트롤러가 나타날 때 숨기기
}

override func viewWillDisappear(_ animated: Bool) {
  navigationController?.setNavigationBarHidden(false, animated: true) // 뷰 컨트롤러가 사라질 때 나타내기
}
  1. isNavigationBarHidden 프로퍼티 이용
  • 이 프로퍼티를 이용하면 애니메이션을 이용할 수 없지만 Navigation Bar를 숨기고 나타낼 수 있습니다. 애니메이션이 필요 없는 경우에 사용합니다.
override func viewWillAppear(_ animated: Bool) {
  navigationController?.isNavigationBarHidden = true // 뷰 컨트롤러가 나타날 때 숨기기
}

override func viewWillDisappear(_ animated: Bool) {
  navigationController?.isNavigationBarHidden = false // 뷰 컨트롤러가 사라질 때 나타내기
}

Enumeration의 associated value를 이용한 활용성 및 재사용성 향상

열거 타입에 연관 값을 이용하여 활용성과 재사용성을 향상시키는 사례를 살펴보겠습니다. 에러 타입을 정의하기 위해 열거 타입을 활용하는 경우, 아래와 같이 코드를 작성할 수 있습니다.

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를 활용해서 열거 타입을 더 유용하게 활용해봅시다!

Number Formatter 활용하여 숫자에 컴마 추가하기

단위가 큰 십진수는 컴마를 통해 구분하는 경우가 많죠. 이런 형식을 지원해야할 경우에는 직접 로직을 만들 수도 있지만 이미 있는 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를 활용해서 숫자를 더 알아보기 쉽게 나타낼 수 있게 되었습니다!

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글