본문 바로가기

학습 노트/iOS (2021)

208. Data Task

Data Task


URL 생성

DataTaskTableViewController.swift

guard let url = URL(string: booksUrlStr) else {
    fatalError("Invalid URL")
}

Network 작업은 항상 URL을 새로 생성하는 것으로 시작한다.
guard 문을 사용해 URL을 생성함과 동시에 오류처리를 함께 진행할 수 있다.

 

seesion 생성

DataTaskTableViewController.swift

 

let session = URLSession.shared

기본 session인 shared session을 생성한다.
shared session은 기본 설정값을 사용하며, Completion Handler를 통해 반환 값을 전달한다.

 

Task 생성

DataTaskTableViewController.swift

 

let task = session.dataTask(with: url) { data, response, error in

}

 

dataTask(with:completionHandler:)

 

Apple Developer Documentation

 

developer.apple.com

생성한 session을 사용해 data task를 생성한다.
이때 사용하는 dataTask 메서드의 첫 번째 파라미터로 URL과 URLRequest를 전달할 수 있고,
URLRequest는 Task 각각의 네트워크 설정을 변경해야 하는 경우 사용한다.

두 번째 파라미터인 completioHandler는 요청의 반환 데이터를 처리하는 방법에 따라 사용 여부가 결정된다.
completionHandler를 사용한다면 해당 파라미터로 closure를 전달하게 된다.
closure는 네트워크 요청이 완료된 시점에 한 번만 호출되고,
서버에서 전달된 binary data, 서버 응답 정보, error가 각각의 파라미터로 전달된다.
따라서 서버에서 전달된 파라미터들을 사용해 이를 처리하는 코드가 구현되게 된다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }
}

error가 반환된 경우 이를 화면에 표시하고 Task를 종료한다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }
}

서버에서는 자신의 상태를 숫자 형태의 응답 코드로 반환한다.
이 응답 코드를 확인하기 위해 HTTPURLResponse로 캐스팅하고,
실패하는 경우 확인할 수 없는 응답으로 Task를 종료한다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }

   guard (200...299).contains(response.statusCode) else {
       self.showErrorAlert(with: "\(response.statusCode)")
       return
   }
}

성공하는 경우의 응답 코드는 대게 200번대를 지니고 있다.
따라서 해당 응답 코드가 아닌 다른 코드를 반환하는 경우 이를 표시하고 Task를 종료한다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }

   guard (200...299).contains(response.statusCode) else {
       self.showErrorAlert(with: "\(response.statusCode)")
       return
   }

   guard let data = data else {
       fatalError("Invalid Data")
   }

}

서버에서 data가 제대로 전달됐는지 확인하고, 문제가 있다면 종료한다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }

   guard (200...299).contains(response.statusCode) else {
       self.showErrorAlert(with: "\(response.statusCode)")
       return
   }

   guard let data = data else {
       fatalError("Invalid Data")
   }

   do {
       let decoder = JSONDecoder()
       decoder.dateDecodingStrategy = .custom({ decoder in
           let container = try decoder.singleValueContainer()
           let dateStr = try container.decode(String.self)
           let formatter = ISO8601DateFormatter()

           formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]

           return formatter.date(from: dateStr)!
       })
       
       let booklist = try decoder.decode(BookList.self, from: data)
   } catch {
       self.showErrorAlert(with: error.localizedDescription)
   }
}

서버에서 반환 받는 데이터는 실습 기준으로 JSON 형식을 가지고 있다.
따라서 JSONDecoder를 설정한다.

Server
"2016-10-25T00:00:00"

Swift
"2016-10-25T00:00:00 Z"

반환되는 JSON 내의 날짜 형식이 ISO8601 형식이긴 하지만 문자열 마지막의 'Z'가 빠진 형태라 Swift의 방식과는 차이가 존재한다.
따라서 이를 해결하기 위해 날짜 디코딩 형식을 custom으로 변경하는 과정이 필요하다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }

   guard (200...299).contains(response.statusCode) else {
       self.showErrorAlert(with: "\(response.statusCode)")
       return
   }

   guard let data = data else {
       fatalError("Invalid Data")
   }

   do {
       let decoder = JSONDecoder()
       decoder.dateDecodingStrategy = .custom({ decoder in
           let container = try decoder.singleValueContainer()
           let dateStr = try container.decode(String.self)
           let formatter = ISO8601DateFormatter()

           formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]

           return formatter.date(from: dateStr)!
       })

       let booklist = try decoder.decode(BookList.self, from: data)

       if booklist.code == 200 {
           self.list = booklist.list

           DispatchQueue.main.async {
               self.tableView.reloadData()
           }
       } else {
           self.showErrorAlert(with: booklist.message ?? "Error")
       }
   } catch {
       self.showErrorAlert(with: error.localizedDescription)
   }
}

변환이 정상적으로 완료되면  tableView에 연결된 데이터 배열에 저장하고,
서버의 상태 값을 확인해 tableView를 업데이트한다.
상태가 정상(실습에선 200)이 아니거나 실패하는 경우 이를 표시하도록 구현했다.

DataTaskTableViewController.swift

let task = session.dataTask(with: url) { data, response, error in
   if let error = error {
       self.showErrorAlert(with: error.localizedDescription)
       return
   }

   guard let response = response as? HTTPURLResponse else {
       self.showErrorAlert(with: "Invalid Response")
       return
   }

   guard (200...299).contains(response.statusCode) else {
       self.showErrorAlert(with: "\(response.statusCode)")
       return
   }

   guard let data = data else {
       fatalError("Invalid Data")
   }

   do {
       let decoder = JSONDecoder()
       decoder.dateDecodingStrategy = .custom({ decoder in
           let container = try decoder.singleValueContainer()
           let dateStr = try container.decode(String.self)
           let formatter = ISO8601DateFormatter()

           formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime]

           return formatter.date(from: dateStr)!
       })

       let booklist = try decoder.decode(BookList.self, from: data)

       if booklist.code == 200 {
           self.list = booklist.list

           DispatchQueue.main.async {
               self.tableView.reloadData()
           }
       } else {
           self.showErrorAlert(with: booklist.message ?? "Error")
       }
   } catch {
       self.showErrorAlert(with: error.localizedDescription)
   }
}

task.resume()

완성된 Task는 바로 동작을 시작하지 않는다.
따라서 resume 메서드를 호출해 동작할 수 있도록 한다.

 

tableViewCell에 표시하기

DataTaskTableViewController.swift

extension DataTaskTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
	   let target = list[indexPath.row]
	   cell.textLabel?.text = target.title
      
      return cell
   }
}

tableView는 최종적으로 저장된 list의 원소 수만큼 Cell을 생성하며,
원소의 데이터를 표시한다.

 

새로운 View에 전송받은 데이터 표시하기

BookDetailTableViewController.swift

class BookDetailTableViewController: UITableViewController {
   @IBOutlet weak var titleLabel: UILabel!
   @IBOutlet weak var descLabel: UILabel!
   @IBOutlet weak var dateLabel: UILabel!
   
   lazy var formatter: DateFormatter = {
      let f = DateFormatter()
      f.dateStyle = .long
      f.timeStyle = .none
      return f
   }()
   
   var book: Book?
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
	   titleLabel.text = book?.title
	   descLabel.text = book?.desc
	   dateLabel.text = formatter.string(from: book!.date)
   }
   
   override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
      guard indexPath.row == 3 else {
         return nil
      }
      
      return indexPath
   }
   
   override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      tableView.deselectRow(at: indexPath, animated: true)
      
      if indexPath.row == 3 {
         if let link = book?.link, let url = URL(string: link) {
             if UIApplication.shared.canOpenURL(url) {
                 UIApplication.shared.open(url, options: [:], completionHandler: nil)
             }
         }
      }
   }
}

데이터를 표시할 View는 BookDetailTableViewController로,
서버에서 전송받은 데이터를 그대로 사용하기 때문에 정의했던 Book 형식으로 데이터를 받게 된다.

그중 함께 전달받은 link는 터치하면 사파리로 링크의 내용을 확인할 수 있도록
바인딩한 후 이를 사파리로 전달해 확인할 수 있도록 구현한다.

DataTaskTableViewController.swift

   override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      if let destVC = segue.destination as? BookDetailTableViewController {
         if let cell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: cell) {
            destVC.book = list[indexPath.row]
         }
      }
   }

prepare 메서드를 구현해 BookDetailTableViewController로 이동하는 경우
Table에 표시했던 list를 해당 View의 Book 형식의 변수에 전달하도록 구현한다.

 

결과


'학습 노트 > iOS (2021)' 카테고리의 다른 글

210. SessionConfiguration  (0) 2022.08.23
209. URL Session Delegate  (0) 2022.08.23
207. URL Loading System  (0) 2022.08.20
205~ 206. JSON  (0) 2022.07.13
204. App Transport Security (ATS)  (0) 2022.07.08