[새싹 iOS] 4주차

임승섭·2023년 8월 12일
0

새싹 iOS

목록 보기
10/45

Alamofire + SwiftyJSON

  • Alamofire is an HTTP networking library written in Swift.
  • SwiftyJSON makes it easy to deal with JSON data in Swift.
  1. package 다운로드
    • project - Package Dependencies
  1. import Alamofire, import SwiftyJSON
  1. Work with Alamofire

    let url = "http://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=\(APIKey.boxOfficeKey)&targetDt=20120101"
    
    AF.request(url, method: .get).validate().responseJSON { response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            print("JSON: \(json)")
        case .failure(let error):
            print(error)
        }
    }

status code .success 범위

  • 에러가 났을 때 (statusCode != 200)
    .failure(let error)에서 print(error)
    에러 내용을 확인할 수 있긴 하지만,
    json 형태로 출력해서 더 자세한 내용을 확인할 수 있다
    AF.request(url, method: .get, headers: header)
        .validate(statusCode: 200...500)	// 200 ~ 500 사이를 모두 success라고 한다
        .responseJSON{ response in
        switch response.result {
        case .success(let value) :
            let json = JSON(
            print(json)
  • 주의해야 할 점은, 이 때 들어온 json은 statusCode가 200이 아닐 때의 값이 들어올 수 있기 때문에
    무작정 성공 케이스의 json 형태만 보고 접근하면 안된다.

    • 에러 내용을 확인하고 끝내야 한다

      case .success(let value):
              let json = JSON(value)
                print("JSON: \(json)")
      
              let statusCode = response.response?.statusCode ?? 500
      
              // 성공(200)인 경우 작업 시작
              if (statusCode == 200) {

ATS (App Transport Security)

  • 앱이 서버에 전송하는 데이터에 대한 보안을 설정한다
  • HTTPS의 경우 데이터 패킷을 암호화해서 전송하기 때문에 보안상 안정하다
  • 그래서 http로 연결하려고 하면 애플에서 block을 해버림
  • 해결책
    1. 뒤에 s 붙여서 https 만든다 -> 추가 인증서가 필요한 경우가 있따
    2. Info -> App Transport Security Settings
      -> Allow Arbitrary Loads -> YES

http 특성

비연결성 (connectionless)

  • 클라이언트와 한 번 연결을 맺고, 요청에 대한 응답이 완료되면 연결이 끊긴다
  • 서버 입장에서 많은 클라이언트와 연결을 유지해야 하는 리소스가 발생하지 않기 때문에 더 많은 연결이 가능해진다
  • 동일한 클라이언트에 대해 매번 새로운 연결을 해야 하기 때문에 연결에 대한 오버헤드가 발생한다

무상태성 (stateless)

  • 서버가 클라이언트를 식별할 수 없다
  • 서버에게 자주 요청을 하더라고 서버 입장에서 같은 사용자인지 알 수 없다
  • 따라서 매번 새로운 인증이 필요하다
  • 쿠키, 세션, 토큰 등의 방법으로 클라이언트를 기억하기도 한다

.gitignore

  • API 통신에 필요한 키 등 중요한 키 값들을 무작정 깃에다 올리면 위험하다
  • 해결책
    1. 키가 써있는 파일은 깃에 안올리고 로컬에서만 관리한다 -> 사실상 불가능
    2. 키만 하나의 파일에다가 다 써두고, 그 파일은 매번 커밋할 때 빼준다 -> 별로
    3. 키만 써있는 파일을 깃에서 track되지 않는 파일로 처리한다 -> 굿
      a. .gitignore 파일 생성
      b. 커밋하기 싫은 파일명 써주고 .gitignore 커밋

      c. 커밋하기 싫은 파일 생성

      d. 파일을 생성했는데도, 네비게이터 영역에 A 마크가 표시되지 않고,
      커밋 체크리스트에도 아예 포함되지 않는다

Active Indicator View

  • 서버에 데이터를 요청하고 응답받는데까지 걸리는 시간을 사용자가 인지할 수 있도록 로딩바 또는 인디케이터를 보여주어야 한다

Storyboard

  • Active Indicator View
    • 바로 table view 위에 올리려고 하면 올라가지 않는다
    • 일단 table view 밖에 위치시키고, size inspector 영역에서 위치 조절해서 안으로 넣어준다
    • vertical / horizontal constraint 잡아서 가운데에 위치시킨다

Code

  1. 맨 처음(서버 통신 전)에는 보일 필요가 없다

    override func viewDidLoad() {
            super.viewDidLoad()
    
            indicatorView.isHidden = true
    }
  1. 서버에 요청 들어가자마자 보이게 하고, 빙글빙글 시작

    func callRequest(_ query: String) {
    
            // 요청 들어간다
            indicatorView.startAnimating()
            indicatorView.isHidden = false
  1. 응답 성공적으로 받았으면 다시 숨기고, 빙글빙글 끝

                case .success(let value):
                    let json = JSON(value)
                    print("JSON: \(json)")
    
                    /* 데이터 들어온걸로 할거 하고 */
    
                    // 이제 숨겨
                    self.indicatorView.stopAnimating()
                    self.indicatorView.isHidden = true
                    self.movieTableView.reloadData()

API Manager

  • 매번 view controller 파일에서 네트워크 통신을 하면,
    코드가 너무 길어지고 복잡해진다.
  • APIManager 파일을 하나 만들고, api를 호출하는 코드를 여기서 작성한다
  • 호출이 완료되고 받은 데이터로 작업하는 일은 completionHandler를 이용한다 (@escaping 사용)
  • 최대한 코드를 이쁘게 정리하는 방법

Code

  1. URL+Extension.swift

    • 카카오에서 제공하는 api를 이용할 때, 여러 api 링크의 앞은 동일하므로 baseURL로 저장해둔다

      //  URL+Extension.swift
      //  0807sesac
      //
      //  Created by 임승섭 on 2023/08/11.
      //
      
      import Foundation
      
      // URL은 구조체이기 때문에 확장 가능
      extension URL {
          static let baseURL = "https://dapi.kakao.com/v2/search/"
      
          static func makeEndPointString(_ endpoint: String) -> String {
              return baseURL + endpoint
          }
      }
  1. EndPoint.swift

    • 원하는 분야(?)의 api 주소를 뒤에 붙여줄 때, enum으로 처리한다

    • 기존에 만들어뒀던 타입 메서드를 활용한다

      //  EndPoint.swift
      //  0807sesac
      //
      //  Created by 임승섭 on 2023/08/11.
      //
      
      import Foundation
      
      enum Endpoint {
          case blog
          case cafe
          case video
      
          var requestURL: String {
              switch self {
              case .blog: return URL.makeEndPointString("blog?query=??")
                  //return URL.baseURL + "blog?query=??"
              case .cafe: return URL.makeEndPointString("cafe?query=??")
              case .video: return URL.makeEndPointString("vclip?query=")
              }
      
          }
      }
  1. KakaoAPIManager.swift

    • api 호출하는 코드를 따로 파일을 만들어서 작성한다

      //  KakaoAPIManager.swift
      //  0807sesac
      //
      //  Created by 임승섭 on 2023/08/11.
      //
      
      import Foundation
      import Alamofire
      import SwiftyJSON
      
      class KakaoAPIManager {
      
          // 인스턴스 생성은 shared 하나만 할 수 있게 막아둠
          static let shared = KakaoAPIManager()
          private init() {}
      
          let header: HTTPHeaders = ["Authorization": APIKey.kakaoKey]
      
          // 어떤 분야(type), 필요한 문자열(검색)(query), 데이터로 할 일(completionHandler)
          func callRequest(type: Endpoint, query: String, completionHandler: @escaping (JSON) -> () ) {
      
              // 문자열은 반드시 encoding 해서 query에 넣어준다
              guard let text = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return }
      
              // 분야에 대한 url까지 requestURL을 통해 호출하고
              // 뒤에 필요한 문자열을 붙여준다 -> 최종 url
              let url = type.requestURL + text
      
              AF.request(url, method: .get, headers: header)
                  .validate()
                  .responseJSON { response in
                  switch response.result {
                  case .success(let value):
                      let json = JSON(value)
      
                      // json을 잘 받아왔으면, 이제 그거가지고 할 일은 얘를 호출하는 곳에서 진행한다 -> completionHandler
                      completionHandler(json)
      
                  case .failure(let error):
                      print(error)
                  }
              }   
          }
      }
  2. VideoViewController.swift

    • 기존에 api 요청했던 함수에서는 APIManager에서 만든 callRequest 함수를 호출하고,

    • 매개변수로 들어가는 클로저에 원하는 코드를 작성한다

      func callRequest(query: String, page: Int) {
              KakaoAPIManager.shared.callRequest(type: .video, query: query) { json in
      
                  /* json으로 할 일 작성 */
      
              }
      }

Identifier

  • 화면을 전환하거나 외부에서 셀을 사용할 때, withIdentifier에 대한 값을 문자열로 적어야 한다
  • 이걸 항상 literal한 문자열로 적고 있으면 오타가 날 가능성도 있고,
    코드가 좀 정제된 느낌이 들지 않는다
  • 이걸 정리하는 방법에 정답이 있는 건 아니지만, 최대한 깔끔하게 쓰는게 좋다

과제 코드

  • enum으로 모든 클래스를 케이스로 정리하였다.

  • identifier는 모두 클래스 이름 그대로 썼기 때문에 String rawValue로 뽑으면
    literal한 문자열을 얻을 수 있다.

    // About.swift
    
    enum Identifier: String {
        case SelectViewController
        case SelectCollectionViewCell
        case DetailSelectViewController
        case MainViewController
        case SettingViewController
        case SettingTableViewCell
        case ModifyNameViewController
        case ChangeTamaViewController
        case DetailChangeTamaViewController
    }
    // rawValue로 뽑아서 사용했다
    let vc = self.storyboard?.instantiateViewController(withIdentifier: Identifier.SelectViewController.rawValue) as! SelectViewController

수업 코드

  • 프로토콜의 타입 연산 프로퍼티를 사용한다

  • 타입 저장 프로퍼티는 그때마다 값을 업데이트해주어야 하는 로직이 필요하기 때문에 연산 프로퍼티를 사용했다

    //  ReusableViewProtocol.swift
    
    import Foundation
    import UIKit
    	
    	
    protocol ReusableViewProtocol {
        static var identifier: String { get }
    }
    	
    	
    extension UIViewController: ReusableViewProtocol {
        static var identifier: String {
            return String(describing: self)
        }
    }
    	
    extension UITableViewCell: ReusableViewProtocol {
        static var identifier: String {
            return String(describing: self)
        }
    }
    // 타입 연산 프로퍼티를 실행시켜서 문자열을 뽑는다
    guard let cell = tableView.dequeueReusableCell(withIdentifier: VideoTableViewCell.identifier ) as? VideoTableViewCell else { return UITableViewCell() }

UserDefaultsHelper

  • 과제를 하면서 데이터를 저장할 때 UserDefaults를 이용하였다.
  • 여러 값에 대한 데이터를 저장하다보니 코드에 Userdefaults.standard가 너무 많아서 보기 안좋았다
  • 클래스를 하나 만들고, 연산 프로퍼티를 이용한다
//  UserDefaultsHelper.swift

import Foundation

class UserDefaultsHelper {
    
    static let standard = UserDefaultsHelper()
   
    private init() {}   // 생성자에 외부에서 접근하지 못하게 한다
    // -> 외부에서 이 클래스에 대한 인스턴스 생성 불가능
    
    let userDefaults = UserDefaults.standard
   
    enum Key: String {
        case nickname, age
    }
    
    
    var nickname: String {
        get {
            userDefaults.string(forKey: Key.nickname.rawValue) ?? "대장"
        }
        set {
            userDefaults.set(newValue, forKey: Key.nickname.rawValue)
        }
    }
    
    var age: Int {
        get {
            return userDefaults.integer(forKey: Key.age.rawValue)
        }
        set {
            userDefaults.set(newValue, forKey: Key.age.rawValue)
        }
    }
}
  • 사실 위 방법으로 하면, enum의 case가 많아질수록
    연산 프로퍼티를 많이 만들어야 하고, 그럼 또 중복되는 코드가 많아진다.
  • 이를 더 간단하게 줄이기 위해 Property Wrapper를 사용한다

Sync Async Main Global

간단하게만 정리

  • sync : 지금 일 끝날 때까지 다음 일 못함
  • async : 지금 일 하는 동안 다른 일 가능
  • main (serial) : 닭벼슬. 주로 UI 관련
  • global (concurrent) : 다른 알바생. 주로 네트워크 관련

간단하게만 정리

  • main.sync : 여태까지 계속 하던거
  • main.async : 닭벼슬이 자기 일 매니저에게 맡기고 뒤에 일 먼저 한다
    • 근데 어차피 자기가 결국 해야해서 일 순서만 바뀌는 결과
  • global.sync : 닭벼슬이 자기 일 매니저에게 맡기고 다른 일 한다
    • 근데 닭벼슬 일 끝날때까지 맡긴 일을 할 수가 없어.
    • 결국 일 다 끝나고 닭벼슬이 하는 거랑 다른게 없어
    • 그래서 그냥 main.sync로 보내버려
    • 그래서 잘 안쓴다
  • global.async : 닭벼슬이 자기 일 매니저에게 맡기고 뒤에 일 먼저 한다
    • 닭벼슬이 일하고 있는 동안 맡겼던 일 작업 가능
    • 그래서 다 동시에 뚝딱뚝딱 진행된다
    1. 빠르다 2. 언제 끝날지 모른다

Example

원 모양의 이미지뷰


// imageView의 모양을 원으로 만들기 위해 cornerRadius를 조정해준다
// (1) 해당 이미지뷰(정사각형)의 width의 절반만큼 cornerRadius 값을 주면 된다
// (2) 현재 스토리보드 상에서 이미지뷰의 width는 기기 사이즈에 대한 비율로 잡아져 있다
// 당연히 순서는 (2)가 끝나고 (1)로 가야 한다

/* 일반적인 생각 */
@IBOutlet var imageView: UIImageView!

override func viewDidLoad() {
	super.viewDidLoad()

	imageView.layer.cornerRadius = imageView.frame.width / 2
}
// 요렇게 하면 (2)보다 (1)이 먼저 실행되어 버리고, 이 때 코드 상의 frame은 스토리보드 기준으로 임의로 잡아버린다
// 따라서 다른 기기로 앱을 실행시켜도 스토리보드에서 14pro로 작업했으면 모든 크기가 14pro 기준으로 잡히기 때문에
// 완벽한 원을 확인할 수 없다


/* DispatchQueue.main.async 사용*/
@IBOutlet var imageView: UIImageView!

override func viewDidLoad() {
	super.viewDidLoad()
	
	DispatchQueue.main.async {
    	self.imageView.layer.cornerRadius
        	= self.imageView.frame.width / 2
    }
}
// main.async로 (1)을 뒤로 보내버린다
// 굿

이미지 받아오는 중 화면 멈춤

  • 용량이 큰 이미지(NASA)를 받아올 때, 시간이 걸리기 때문에 해당 시간동안 화면 상에서 아무것도 못하는 상황이 발생한다
    • 버튼도 못눌러, 글씨도 못써, ...
    • UI Freezing
  • 따라서 해당 시간에도 다른 일을 할 수 있도록 코드를 작성해야 한다
@IBAction func buttonClicked(_ sender: UIButton) {
	// 1. url -> 2. data -> 3. image
    // 이 중 2가 시간이 오래걸리기 때문에, 2 3을 다른 알바생한테 던진다
    // 그래서 1 끝나고도 화면 만질 수 있도록 한다
    // 주의해야 할 점은, image는 UI 관련이기 때문에 다시 main으로 돌려줘야 한다
    
    let url = URL(string: "https://api.nasa.gov/assets/img/general/apod.jpg")!
    
    DispatchQueue.global().async { // 매니저에게 애들한테 골고루 나눠주라고 함, 그리고 애들한테는 바로 다음 작업 시작하라고 함
        let data = try! Data(contentsOf: url)
            
        // 다시 메인에다가 넣어줘야 해 (UI 관련된거)
        DispatchQueue.main.async {
            self.firstImageView.image = UIImage(data: data)
        }        
    } 
}

1개의 댓글

comment-user-thumbnail
2023년 8월 12일

훌륭한 글 감사드립니다.

답글 달기