** Apple의 비동기(순서대로 수행되지 않는) API
=> URLSession이라는 클래스를 제공
1. Request(요청)와 Response(응답)
=> Request는 URL객체를 통해서 요청하는 방식과 URLRequest객체를 이용하는 방식 모두 제공
=> Response는 설정된 Task 의 Completion Hanlder 형태로 response를 받거나 RULSessionDelegate를 통해 쩡된 메소드를 호출하는 형태를 응답을 처리
간단한 응답은 Completion Handler를 이용하지만 앱이 백그라운드로 진입한 경우에도 응답을 받고자 하면 URLSessionDelegate를 이용
2. URLSession 종류
=> Default Session : 기본적인 세센으로 디스크 기반 캐싱을 이용
=> Ephemeral Session : 캐싱을 이용하지 않음
=> Background Session : 앱이 종료된 후에도 통신하고자 할 때 이용
백그라운드에서도 작업을 수행하고자 하는 경우에는 Info.plist 파일에 백그라운드 모드를 활성화 시켜야 한다.
3. 세션 생성
1) 공용 세션
let session = URLSession.shared : 존재하는 세션일경우
2) 직접 생성
let config = URLSessionConfiguration.default
let session = URLSession(configuration:config)
4. URLSessionTask
=> 세션을 이용해서 request를 하고 응답을 받고자 하는 경우 사용
1. 종류
DataTask : 일반적인 Task
DownloadTask : 파일의 형태로 변환시켜서 다운로드 받는 Task
UploadTask : 파일의 형태로 변환시켜서 업로드하는 Task
2) 메소드
suspend()
resume()
cancel()
3) 세션을 이용해서 Task 생성
func dataTask(with url:URL, completionHandler:@escaping(Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask
func dataTask(with request:URLRequest, completionHandler:@escaping(Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask
func downloadTask(with url:URL, completionHandler:@escaping(Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDownloadTask
func uploadTask(with request:URLRequest, fromFile fileURL: URL) -> URLSessionUploadTask
4) 예시
let url = URL(string:웹 주소)
let urlRequest = URLRequest(url:url!)
let session = URLSession.shared
let task = session.dataTask(url:url, completionHandler:{(data, response, error) -> Void in
수행할 내용
})
let task = session.dataTask(url:url){(data, response, error) -> Void in
수행할 내용
}
=> @escaping
함수에서 함수나 클로저를 매개변수로 받는 경우 함수 내에서 매개변수로 받은 함수나 클로저를 다른 변수에 대입해서 호출할 수 있도록 하는 예약어
메소드를 호출하는 입장에서는 @escaping 은 무시해도 됩니다.
5.URLSession을 이용한 GET 과 POST 방식 요청
=>GET 과 POST(웹 서버에서의 parameter(query string) 전송 방식 - method)
GET: 파라미터를 URL 뒤에 붙여서 전송, 자동 재전송 요청, 파라미커가 노출이 되고 길이에 제한
POST: 파라미터를 header에 숨겨서 전송, 파라미터가 보이지 않고 길이에 ㅔ한이 없음, 자동 재전송 요청이 안됨 - 네트워크가 비할성화 상태에서 활성화 상태로 변경되도 재전송되지 않는다.
파라미터에 password가 있거나 textarea, file 이 있을 때는 POST 방식으로 전송해야 한다.
6.실습
=>http://cyberadam.cafe24.com/movie/list 에서 문자열 데이터 가져오기 - GET 방식
파라미터는 page 이고 자료형은 정수
=>http://img.hani.co.kr/imgdb/resize/2018/0518/00502318_20180518.JPG 에서 이미지 다운로드 받기
=>http://cyberadam.cafe24.com/member/login 에서 문자열 데이터 가져오기 - POST 방식
파라미터는 email 과 pw
1) 비동기 다운로드에 이용한 ViewController 클래스를 상속받는 클래스를 생성 - AsyncRequest
2) Main.storyboard 파일에 ViewController를 추가하고 Class 속성 과 Storyboard ID 속성을 설정
3) RootViewController 에서 AsyncRequest를 출력할 수 있도록 코드를 추가
=> viewDidLoad 메소드에 추가
titles.append("비동기 다운로드")
subtitles.append("URLSession을 이용한 비동기 다운로드")
=> 셀을 선택했을 때 호출되는 메소드에 작성
else if indexPath.row == 3{
let asyncViewController = self.storyboard?.instantiateViewController(withIdentifier: "AsyncViewController") as! AsyncViewController
self.navigationController?.pushViewController(asyncViewController, animated: true)
}
4)AsyncViewController 의 viewDidLoad 메소드에 GET 방식으로 요청한 문자열 다운로드 받아서 출력하기
=>request
url: http://cyberadam.cafe24.com/movie/list
parameter: page - Integer
=>response
count: 데이터 개수
list: 데이터
import UIKit
class AsyncViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//비동기 방식으로 문자열 다운로드 - GET
//1. 세션 생성
let session = URLSession.shared
//2. Task 생성
let task = session.dataTask(with: URL(string: "http://cyberadam.cafe24.com/movie/list?page\(1)")!, completionHandler: {(data:Data?, response:URLResponse?, error:Error?) -> Void in
//에러가 발생하면
if error != nil{
NSLog("다운로드 에러:\(error!.localizedDescription)")
}else{
//NSLog("다운로드 받은 데이터:\(data!)")
//문자열로 변환해서 출력
let str = String(bytes: data!, encoding: .utf8)
NSLog("다운로드 받은 데이터:\(str!)")
let result = try! JSONSerialization.jsonObject(with: data!, options: []) as! NSDictionary
NSLog("다운로드 받은 데이터:\(result.description)")
}
})
//3. task 실행
task.resume()
}
}
5) AsyncViewController 의 viewDidLoad 메소드에 GET 방식으로 요청한 이미지를 다운로드 받아서 이미지 뷰에 출력하기
=>G UI 시스템에서는 화면 출력을 Main Thread에서만 할 수 있도록 제한을 합니다.
여러 스레드에서 화면 출력을 하게되면 서로 출력을 할려고 해서 원하는 출력이 되지 않을 가능성이 있기 때문입니다.
=> Apple System 에서는 메인 스레드에게 작업을 시킬 때는 아래 코드를 이용
OperationQueue.main.addOperation{ 수행할 내용 }
//이미지 뷰를 생성해서 배치
//1. 전체 화면 크기 가져오기
let frame = UIScreen.main.bounds
//2. UIImageView 객체 생성
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width/2, height: frame.size.height/2))
//3. 위치 설정
imageView.center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
//4. 현재 뷰 위에 추가
self.view.addSubview(imageView)
let imageTask = session.dataTask(with: URL(string: "http://img.hani.co.kr/imgdb/resize/2018/0518/00502318_20180518.JPG")!){
(data:Data?, response:URLResponse?, error:Error?) -> Void in
//다운로드 받은 이미지를 이미지를 생성
let image = UIImage(data: data!)
//imageView.image = image
OperationQueue.main.addOperation {
imageView.image = image
}
}
imageTask.resume()
6) POST 방식으로 파라미터 전송
=> http://cyberadam.cafe24.com/member/login
=> 파라미터는 email, pw
=> POST 방식으로 전송할 때는 URLRequest를 이용해야 하고 URLRequest의 httpMethod에 "POST"를 설정하고 파라미터는 이름=값&이름=값 의 형태로 URLRequest의 httpBody에 Data로 변환해서 대입하면 된다.
//POST 방식으로 전송하기 위한 URLRequest를 생성하고 POST를 설정
let url = URL(string:"http://cyberadam.cafe24.com/member/login")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
//파라미터 생성
let parameter = "email=root&pw=1234"
//파라미터 설정
request.httpBody = parameter.data(using: .utf8)
let task2 = session.dataTask(with: request){
(data:Data?, response:URLResponse?, error:Error?) -> Void in
let result = String(data: data!, encoding: .utf8)
NSLog(result!)
}
task2.resume()
** 외부라이브러리 의존성 설정 관리자
=> 여러가지 관리자가 있는데 가장 많이 사용되는 것이 cocoapods
=> 의존성 관리자를 사용하는 이유
애플리케이션을 개발하다 보면 모든 로직을 직접 개발하기는 쉽지 않고 Native Method호출 등을 직접 하는 것은 어렵기 때문에 3rd Party개발자들이 만든 라이브러리를 이용해서 개발을 많이 한다.
파이썬이나 자바, R, node, Linux 진영에서는 3rd Party 개발자들이 만든 라이브러리들을 관리해주는 repostory를 만들고 이를 다운로드 받아서 사용을 한다.
Mac에서 프로그램을 설치할 때 Homebrew를 이용한다.
기본 라이브러리 이외의 라이브러리를 사용할 때 주의할 점은 라이브러리에 종속될 수 있다는 점
개발을 할 때는 외부 라이브러리를 가져다 쓰지만 개발이 끝나면 외부 라이브러리 사용을 배재할 수 있도록 수정하는 것이 좋다.
iOS에서는 정책이 자주 바껴서 외부 라이브러리가 reject되는 경우가 발생할 수 있다.
1. cocoa pod 설치 - 터미널에서 작성
=> 처음 한번만 하면 된다.
=> Xcode는 Xcode를 설치될 때 Git을 같이 설치한다.
Git 버전이 오래되면 cocoa pod이 설치가 안된다.
=> Xcode를 최신버전으로 업데이트하고 설치
1) 다운로드
sudo gem install cocoapods
=> sudo는 관리자 명령어여서 관리자 비밀번호를 알아야 한다. (컴터 처음 접속시 비번)
![]() |
![]() 다운로드 완료 |
2) 설치
pod setup
![]() 설치 완료 |
2. cocoa pods 설정
=> cocoa pods의 설정은 Project 단위
=> 외부 라이브러리를 다운로드 받아서 사용할 수 있도록 해주는 것은 target 단위
=> 작업의 단위는 project지만 실행과 배포의 단위는 target(안드로이드는 module)
=> 터미널에서 프로젝트 디렉토리까지 경로를 이동 : change directory(cd)
상위 디렉토리로 이동 : cd .. (중간에 공백)
cd 경로
=> pod init 명령을 입력
프로젝트에 Podfile이 생성된다.
=> Podfile을 열어서 모듈단위로 필요한 라이브러리 설정
pod '라이브러리이름'
=> 라이브러리 설치
pod install
=> 프로젝트를 닫고 프로젝트 디렉토리에생성된 프로젝트 이름.xcworkspace 파일을 실행해서 작업
** Alamofire 라이브러리
=> URLSession과 URLRequest객체를 간소화해서 사용하기 편리하게 만든 라이브러리
1. 설치
1) 터미털을 열어서 cd 명령을 이용해서 프로젝트 파일이 있는 디렉토리로 이동
=> 제대로 이동했는지 확인
ls(현재 디렉토리에 있는 파일을 전부 출력)
프로젝트이름.xcodeproj 파일이 보이는지 확인
![]() |
2) pod init 명령을 실행해서 cocoa pods 프로젝트로 변경
![]() |
![]() 파일 하나가 생성된다. |
3) 새로 생성된 Podfile을 열어서 (텍스트 편집기로 열기) 라이브러리의 의존성 설정
pod 'Alamofire' 를 원하는 target에 작성
![]() 위처럼 작성하고 저장 |
4) pod install 명령어를 터미널에서 실행
![]() |
![]() 이 파일이 생성되면 설치가 완료된 것이다. |
5) 프로젝트를 닫고 새로 만들어진 파일 (확장자가 xcworkspace)을 열어서 작업
2. Alamofire 사용 : 화면 출력하는 코드를 작성하면 Main Thread에서 동작
1) 기본적인 문자열 다운로드
AF.request("요청 경로", method=.전송방식, parameter:[파라미터]){
request.response{
response in
수행할 내용 - response.data! 가 다운로드 받은 데이터
}
})
AF.request("요청 경로", method=.전송방식, parameter:[파라미터]){
request.responseString{
response in
수행할 내용 - response.value! 가 다운로드 받은 데이터
}
})
2) 실습
=> AsyncViewController.swift 파일에 Alamofire를 import
import UIKit
import Alamofire
class AsyncViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Alamofire 를 이용한 다운로드
let r = AF.request("https://httpbin.org/get", method:.get, parameters: nil)
r.response {
response in
let msg = String(bytes: response.data!, encoding: .utf8)
NSLog(msg!)
}
=> import에서 에러가 나면 프로젝트를 닫고 다시 열어서 수행
3) json 가져오기
r.responseJson{
response in response.value 가 파싱된 결과로 형변환해서 사용하면 돤다.
}
3. 이미지를 비동기적으로 다운로드 받는 라이브러리 - Nuke
=> iOS에서는 Alamofire와 Nuke 라이브러리를 많이 이용한다.
** ITEM 다운로드 받아서 테이블 뷰에 출력하기
URL : http://cyberadam.cafe24.com/item/list
데이터 구성 : JSON
result : Bool, 데이터 가져오기 성공 여부, 성공하면 true 실패하면 false
count : Integer, 전체 데이터 개수
list : Array(itemid-정수, itemname-문자열, price-정수, description:문자열, pictureurl:문자열, updatedate:문자열)
=> 이미지는 http://cyberadam.cafe24.com/img/pictureurl 을 입력하면 확인 가능
=> 서버의 데이터를 마지막 업데이트 한 시각: http://cyberadam.cafe24.com/item/date
=> 셀의 왼쪽에는 pictureurl를 출력하고 textLabel에는 itemname 을 출력하고 detailTextLabel에는 description 과 price를 출력
=>cocoa pods 까지 설치가 되었다고 가정하고 시작
1. Project를 생성(Target을 생성할 수 도 있고 UITableViewController로부터 상속받는 클래스를 생성)
2. Project 나 Target을 추가한 경우 수행
1) Project를 생성한 경우만 수행 : Project만든 디렉토리로 터미널의 경로를 이동
2) 터미널에 pod init 명령을 실행해서 Podfile 이 생성되는지 확인
3) 생성된 Podfile을 열어서 모듈 안에 의존성을 설정
=> Alamofire(웹 서버와의 통신을 편리하게 해주는 라이브러리 - 필수, https://github.com/Alamofire/Alamofire) 와 Nuke(이미지를 비동기적으로 다운로드 받는 라이브러리, https://github.com/kean/Nuke) 라이브러리의 의존성을 추가
pod 'Alamofire'
pod 'Nuke'
4) 터미널에 pod install 명령을 실행해서 라이브러리를 설치하고 프로젝트 파일을 다시 생성
5) Xcode를 종료하고 새로 만들어진 xcworkspace 파일을 실행
3. UITableView로 부터 상속받는 클래스를 생성(ItemListViewController)
=>프로젝트를 만들거나 Target을 추가한 경우는 기존 ViewController.swift 파일을 제거
4.Main.stroyboard 파일에 TableViewController를 추가하고 Class 속성 과 Storyboard ID 속성을 수정
=>프로젝트를 만들거나 Target을 추가한 경우는 추가한 TableViewController를 시작 뷰 컨트롤러로 생성 - 다섯번째 Inspector에서 is Initial View Controller를 체크
=>Navigation Controller를 추가 : [Editor] - [Embed In] - [Navigation Controller]
5. RootViewController에서 ItemListViewController를 출력하는 코드를 작성
=> 프로젝트를 만든거나 Target을 추가한 경우는 할 필요없다.
1) viewDidLoad 메소드에 설명을 추가
titles.append("ITEM Open API 활용")
subtitles.append("Alamofire를 이용한 비동기 요청 처리")
2)셀을 선택했을 때 호출되는 메소드에 추가
else if indexPath.row == 4{
let itemListViewController = self.storyboard?.instantiateViewController(withIdentifier: "ItemListViewController") as! ItemListViewController
self.navigationController?.pushViewController(itemListViewController, animated: true)
}
6. 셀의 모양이 원하는 출력 모양이 아닌 경우 UITableViewCell 로 부터 상속받는 클래스를 생성
7. Main.storyboard 파일의 TableView안의 Cell의 Class 속성과 Identifier를 변경
8. 셀을 디자인하고 필요한 변수와 메소드를 연결
9. 파싱한 결과를 저장하기 위한 자료구조를 생성 - Item
=> 새로운 swift 파일을 생성해서 작성
=> class(참조를 저장 - 참조를 복사, 복사를 하면 2개의 내용 및 참조가 동일, Call By Reference)와 Struct(값을 저장 - 값을 복사, 복사를 하면 2개의 내용은 같지만 참조는 달라짐, Call By Value)의 차이
import Foundation
import UIKit
struct Item{
var itemid : Int!
var itemname : String!
var price : Int!
var description : String!
var pictureurl : String!
//이미지를 다운로드 받아서 저장할 프로퍼티
var image : UIImage!
}
10. ItemListViewController.swift 파일에 테이블 뷰에 출력할 데이터 배열을 생성
class ItemListViewController: UITableViewController {
//테이블 뷰에 출력할 데이터 배열 - 파싱 결과 저장
//var list = Array<Item>() //기존 방식
//Closure를 이용한 지연 생성
//지연 생성 - 인스턴스가 생성될 때 생성하지 않고 처음으로 사용될 때 생성하는 객체 지향의 프로퍼티 생성 방식
lazy var list : [Item] = {
var imsi = [Item]()
return imsi
}()
11. ItemListViewController.swift 파일에 테이블 뷰 출력 관련 메소드를 재정의
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "cell"
var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil{
cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}
//하나의 행에 해당하는 데이터를 찾아오기
let item = list[indexPath.row]
//세부 내용 출력
cell!.imageView!.image = item.image
cell!.textLabel!.text = item.itemname
cell!.detailTextLabel!.text = "\(item.description!) \(item.price!)원"
return cell!
}
12. ItemListViewController.swift 파일의 viewDidLoad 메소드에 Alamofire를 이용해서데이터를 다운로드 받고 파싱해서 배열에 저장하고 테이블 뷰에 출력하는 코드를 작성
1) Alamofire import
import Alamofire
2) viewDidLoad 메소드에 작성
override func viewDidLoad() {
super.viewDidLoad()
self.title = "ITEM"
//다운로드 받을 주소
let url = "http://cyberadam.cafe24.com/item/list"
//JSON 데이터 가져오기
//headers 는 일반 파라미터가 아닌 파라미터가 있을 때 사용
//{(파라미터) -> 결과형 in 내용}
//{파라미터 in 내용}
//파라미터가 1개인 경우는 () 생략 가능
//리턴 타입이 Void 인 경우 -> 결과형 생략 가능
let request = AF.request(url, method:.get, encoding:JSONEncoding.default, headers:[:])
//요청을 전송하고 응답을 받아오기
request.responseJSON{
(response) -> Void in
//가져온 데이터는 response.value
//여기까지 작성하고 실행했는데 에러가 나면 URL을 확인하고 URLdㅣ http로 시작하면 Info.plist 파일에 ATS 설정이 되어 있는지 확인
//결과 데이터가 JSON이 맞는지 확인
//NSLog("\(response.value!)")
//시작이 { 이므로 NSDictionary로 변환
//[String : Any] 도 NSDictionary 이다
if let jsonObject = response.value as? [String:Any]{
//list 키의 데이터를 배열로 가져오기
let list = jsonObject["list"] as! NSArray
//배열 순회
for index in 0...(list.count-1){
//배열안의 객체를 인덱스를 이용해서 가져오기
let item = list[index] as! NSDictionary
//파싱한 결과를 저장하기 위한 객체를 생성
var obj = Item(
itemid: (item["itemid"] as! NSNumber).intValue,
itemname: item["itemname"] as! String,
price: (item["price"] as! NSNumber).intValue,
description: item["description"] as! String,
pictureurl: item["pictureurl"] as! String,
image: nil
)
//pictureurl을 이용해서 이미지 다운로드
let url:URL! = URL(string: "http://cyberadam.cafe24.com/img/\(obj.pictureurl!)")
let imageData = try! Data(contentsOf: url)
//다운로드 받은 이미지를 설정
obj.image = UIImage(data: imageData)
//배열에 추가
self.list.append(obj)
}
//테이블 뷰를 재출력
self.tableView.reloadData()
}
}
}
13. 현재 애플리케이션의 문제점
=> 이미지를 동기적으로 다운로드 받기 때문에 이미지를 전부 다운로드 받기 전에는 테이블 뷰를 재출력하지 못함
=> 이런 경우 일단 기본 이미지를 출력해두고 이미지가 다운로드 되면 이미지뷰만 재출력하도록 해야 하는데 이러한 작업을 편리하게 수행하도록 해주는 라이브러리가 Nuke
=> ImageLoadingOption을 이용해서 placeholder 나 애니메이션을 설정하고 Nuke의 loadImage 메소드를 이용해서 이미지를 다운로드 받아서 출력
1) 기본적으로 보여질 이미지를 프로젝트에 추가 : car00.jpg
2) Nuke를 import
import Nuke
3) Cell을 출력하는 메소드 수정
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "cell"
var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil{
cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
}
//하나의 행에 해당하는 데이터를 찾아오기
let item = list[indexPath.row]
//세부 내용 출력
//cell!.imageView!.image = item.image
//Nuke를 이용해서 이미지 출력
DispatchQueue.main.async(execute: {() -> Void in
//다운로드 받을 주소 생성
let url = URL(string:"http://cyberadam.cafe24.com/img/\(item.pictureurl!)")
//옵션 설정
let options = ImageLoadingOptions(placeholder: UIImage(named:"car00.jpg"), transition: .fadeIn(duration: 10))
//Nuke 설정
Nuke.loadImage(with: url!, options: options, into:cell!.imageView!)
})
cell!.textLabel!.text = item.itemname
cell!.detailTextLabel!.text = "\(item.description!) \(item.price!)원"
return cell!
}