섹션13: Networking, JSON Parsing, UITextField 날씨 앱 만들기(복습)

인생노잼시기·2021년 2월 9일
0

📱Udemy iOS

목록 보기
6/20

API를 활용해 JSON형식으로 된 날씨 정보를 가져오기
protocol을 extension해서 delegate패턴 사용하기

Clima

148강 api key를 발급받는 데 시간이 걸릴 수 있으므로 먼저 들어두자

SF Symbols

https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
애플에서 자동으로 크기와 정렬을 맞춰주는 아이콘을 제공
다크모드로 변경도 가능

  • tint(색) 커스터마이징하는 방법
    Assets.xcassets > +아이콘 누르고 Color Set> Appearances: Any, Light, Dark> 원하는 색 넣은 후> Name 변경


커스텀컬러 팔레트 만들려면 드래그 해주기

Appearance 다크모드

scalr images: PNGs or JPEGs

확대하면 픽셀이 깨짐

vector images, PDF images

확대해도 픽셀이 안깨짐
Assests.xcassets> background 변경> Resizing: Preserve Vector Data 체크 > Scales: Single Scale> Appearances: Any, Light, Dark
Main.Storyboard에서는 선택한 이미지사이즈가 고정되어서 깨져보이지만
시뮬레이터로 실행하면 안깨진다

  • 오류
    다크모드로 변경해도 설정한 배경화면이 보이지 않는데
    preserve vector data 체크를 풀면 확인이 가능하고
    시뮬레이터에서도 확인 가능하다(shift command a)


UITextField, UITextFieldDelegate

사용자의 텍스트를 입력받음
검색창으로 사용
Text Input Traits
Capitalization: Words
Return Key: Go
키보드: I/O > Keyboard > Toggle Software Keyboard (command K)

searchTextField.delegate = self

UITextField가 viewController(self)를 delegate(위임하다)해서
텍스트필드에 무슨 일이 발생하면 뷰컨트롤러가 알아챈다
뷰 컨트롤러가 위임받는다

textFieldShouldReturn
텍스트필드의 리턴버튼을 누를지 말지 delegate(뷰컨트롤러)한테 물어본다


import UIKit

class WeatherViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var conditionImageView: UIImageView!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var searchTextField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //텍스트필드가 뷰컨트롤러에 무슨 일이 일어났는지 알려준다
        searchTextField.delegate = self
    }

    @IBAction func searchPressed(_ sender: UIButton) {
        searchTextField.endEditing(true)    //키보드 숨기기
    }
    
    //키보드의 리턴을 사용해야하나요? 옙!
    //💥(_ textField: UITextField) 여기를 주의깊게 살펴보면,
    //화면에 텍스트필드가 여러개여도 텍스트필드마다 작업을 따로 설정해줄 필요가 없어진다💥
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        searchTextField.endEditing(true)    //키보드 숨기기
        return true
    }
    
    //shoud -> asking for permission
    //유효성검사에 유용하다
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        if textField.text != "" {
            return true
        } else {
            textField.placeholder = "Type something"
            return false
        }
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        
        searchTextField.text = ""   //텍스트필드 비우기
    }
}

https://developer.apple.com/documentation/uikit/uitextfielddelegate


Open Weather Map API Documentation

https://openweathermap.org/api
https://openweathermap.org/current
JSON data를 tree structure로 보려면
JSON Viewer Awesome를 크롬 extension을 다운받자(Pro로 바뀌었넴)
그냥 json 검색해서 아무거나 다운 받았다

크롬 익스텐션 말고
그냥 웹사이트 추천
https://jsonlint.com

Networking

    1. Create a URL
    1. Create a URLSession
    1. Give URLSession a task
    1. Start the task (hit the enter on the chrome)
//  ViewController.swift

import UIKit

class WeatherViewController: UIViewController, UITextFieldDelegate {
    
    var weatherManager = WeatherManager()
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        if let city  = searchTextField.text {
            weatherManager.fetchWeather(cityName: city)
        }
        
        searchTextField.text = ""   //텍스트필드 비우기
    }
}
//  WeatherManager.swift

import Foundation

struct WeatherManager {
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=나의에이피아이&units=metric"
    
    func fetchWeather(cityName: String) {
        let urlString = "\(weatherURL)&q=\(cityName)"
        performRequest(urlString: urlString)
    }
    
    func performRequest(urlString: String) {
        //1. create a URL
        if let url = URL(string: urlString) {
            
            //2. create a URLSession
            let session = URLSession(configuration: .default)   //브라우저
            
            //3. give the sessiona task
            let task = session.dataTask(with: url, completionHandler: handle(data:response:error:))
            
            //4. start the task
            task.resume()
        }
        
    }
    
    func handle(data: Data?, response: URLResponse?, error: Error?) {
        if error != nil {
            print(error!)
            return
        }
        
        if let safeData = data {
            let dataString = String(data: safeData, encoding: .utf8)
            print(dataString)
        }
    }
}

https (ATS policy)

The resource could not be loaded
because the App Transport Security policy requires the use of a secure connection

closures, anonymous function: 클로저, 익명함수

클로저: 이름이 없는 익명함수, self-contained한 함수로 매개변수로 넘길 수 있다, 리턴값으로 받을 수 있다

다양한 형태로 축약이 가능한데, 간결성과 가독성을 고려해야한다.
simplicity간결성 vs readibility가독성

func functionName(parameter: parameterType) -> returnType {
	return output
}

func calculator(n1: Int, n2: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(n1, n2)    //call
}
// func add(no1: Int, no2: Int) -> Int {
//     return no1 + no2
// }

// // 클로저로 변환
// { (no1: Int, no2: Int) -> Int in
//     return no1 + no2
// }

// data type inference 데이터 타입 추론
// n1, n2, return type의 데이터 타입 생략 가능
// calculator(n1: 2, n2: 3, operation: { (no1, no2)  in no1 + no2 })

// $0: 첫번째 파라미터, $1: 두번째 파라미터
// calculator(n1: 2, n2: 3, operation: { $0 + $1 })

// trailing closure
let result = calculator(n1: 2, n2: 3) { $0 + $1 }
print(result)

calculator(n1: 2, n2: 3, operation: { (no1: Int, no2: Int) -> Int in
    return no1 + no2
})

// 함수를 매개변수로 사용할 때
func multiply(no1: Int, no2: Int) -> Int {
    return no1 * no2
}
calculator(n1: 2, n2: 3, operation: multiply)
// map reduce filter
// stringfy
let array = [1, 2, 3, 4, 5]

print(array.map{"\($0)"})
//위의 코드에 클로저 적용하기 
//handle함수를 task가 실행되면 실행되도록 클로저 형식으로 나타내기
            //3. give the sessiona task
            let task = session.dataTask(with: url) { data, response, error in
                if error != nil {
                    print(error!)
                    return
                }
                
                if let safeData = data {
                    let dataString = String(data: safeData, encoding: .utf8)
                    
                }
            }

https://developer.apple.com/documentation/swift/array/3017522-map
https://docs.swift.org/swift-book/LanguageGuide/Closures.html

클로저 안에서 클래스 안에 있는 메서드, 변수를 호출하려면 self키워드를 써야 한다

stored, computed property

computed property는 계산에 따라 값이 바뀌기 때문에 var 키워드를 써줘야 한다

struct myModel {
	//stored property
	let myName: String
    let myAge: Int
    
    //computed property
    var myGeneration: String {
    	switch myAge:
        case 0...19:
        	return "어린이"
        case 20...:
        	return "어른"
    }
}

JSON Parser

//WeatherManager.swift

import Foundation

struct WeatherManager {
            //3. give the sessiona task
            let task = session.dataTask(with: url) { data, response, error in
                if error != nil {
                    print(error!)
                    return
                }
                
                //💥JSON파싱 눈여겨보기
                if let safeData = data {
                    //let dataString = String(data: safeData, encoding: .utf8)
                    parseJSON(weatherData: safeData)
                }
            }
        }
        
    }
    
    func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            print(decodedData.name)
            print(decodedData.main.temp)
            print(decodedData.weather[0].description)
        } catch {
            print(error)
        }
    }
}
//WeatherData.swift
//JSON 데이터를 파싱하기 위해서

import Foundation

//{
//    "weather": [{
//        "id": 801,
//        "main": "Clouds",
//        "description": "few clouds",
//        "icon": "02d"
//    }],
//    "main": {
//        "temp": 19.76
//    },
//    "name": "London"
//}

struct WeatherData: Decodable {
    let name: String
    let main: Main
    let weather: [Weather]
}

struct Main: Decodable {
    let temp: Double   //JSON데이터와 프로퍼티의 이름이 정확히 일치해야한다
}

struct Weather: Decodable {
    let description: String
    let id: Int
}

WeatherManager: 데이터를 가져오기 위한 메서드를 사용하는 공간
WeatherData: JSON 파싱하기 위해 필요한 데이터
WeatherModel: 화면에 뿌리기 위해 필요한 데이터

//WeatherManager.swift
    func parseJSON(weatherData: Data) -> WeatherModel? {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            let id = decodedData.weather[0].id
            let temp = decodedData.main.temp
            let name = decodedData.name
            
            let weather = WeatherModel(conditionId: id, cityName: name, temperature: temp)
            return weather
            
        } catch {
            print(error)
            return nil
        }
    }
//  WeatherModel.swift

import Foundation

struct WeatherModel {
    //Stored Property
    let conditionId: Int
    let cityName: String
    let temperature: Double
    
    //Computed Property (변하기 때문에 var로 선언)
    var conditionName: String {
        switch conditionId {
            case 200...232:
                return "cloud.bolt"
            case 300...321:
                return "cloud.drizzle"
            case 500...531:
                return "cloud.rain"
            case 600...622:
                return "cloud.snow"
            case 701...781:
                return "cloud.fog"
            case 800:
                return "sun.max"
            case 801...804:
                return "cloud.bolt"
            default:
                return "cloud"
        }
    }
    
    var temperatureString: String {
        return String(format: "%.1f", temperature)
    }
}

Parameter Names

  • external parameter
  • internal parameter

delegate pattern과 parameter names를 사용해 화면에 데이터 뿌리기

WeatherManager를 통해서 세션을 연결하고
WeatherData를 통해 데이터를 파싱하고
WeatherModel에 이쁘게 데이터를 저장했는데...
💥WeatherModel에 저장한 데이터를 뷰컨트롤러에 띄워야 한다

프로토콜을 만들 때는 textField의 파라미터 이름과 같은 형식으로 만드는 것이 일반적이다.
어떻게 하면 좋을까?

//  WeatherManager.swift
//  프로토콜 만들기

protocol WeatherManagerDelegate {
    func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel)
}

struct WeatherManager {
    //delegate 만들기
    var delegate: WeatherManagerDelegate?
    
    func performRequest(urlString: String) {
        
                
                if let safeData = data {
                    //let dataString = String(data: safeData, encoding: .utf8)
                    if let weather = self.parseJSON(weatherData: safeData) {
                        //여기서 만약 보여줄 뷰컨트롤러의 인스턴스를 생성했으면 그 뷰컨트롤러에서만 WeatherManager를 사용할 수 있기 때문에 delegate 패턴을 사용한다
                        delegate?.didUpdateWeather(self, weather: weather)
                    }
                }
            }
        }
        
    }
}
//  ViewController.swift
import UIKit

class WeatherViewController: UIViewController, WeatherManagerDelegate {	//상속받기
	//구현하기
    func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel) {
        print(weather.cityName)
    }
    
    var weatherManager = WeatherManager()
    
    override func viewDidLoad() {
        //위임하기
        weatherManager.delegate = self
    }
   
}

DispatchQueue

UILabel.text must be used from main thread only

디스패치큐 클로저를 사용하는 이유

UI(뷰컨트롤러에 있는 Lable)에 data를 넣으려고 했을 때
data를 가져오는 과정에서 (session task과 parseJSON 실행해야 함)
세션 안에서 API로 네트워킹 및 데이터 파싱을 클로저로 받아 콜백함수(completion handler)를 사용했기 때문에 이걸 다 기다리면 시간이 오래걸리기 때문이다.

    func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel) {
        DispatchQueue.main.async {
            self.temperatureLabel.text = weather.temperatureString
            self.conditionImageView.image = UIImage(systemName: weather.conditionName)
            self.cityLabel.text = weather.cityName
        }
    }

https://developer.apple.com/documentation/foundation/thread

Xcode 사용법

  • 코드 placeholder에서 enter키를 치면
    알아서 trailing closure 형태로 변환해준다
  • 디버그 콘솔이 안보일 때
    view > Debug Area > Activate Console (shift command c)
  • 예를 들어 for을 입력하면 자동으로 확장시켜주는 code snippet 만드는 방법
    코드 클릭하고 우클릭> Create Code Snippet
    • paceholder 추가하는 방법 <# placeholder #>
    • 이제 mark를 입력하고 엔터를 치면 생성한 코드 스니펫을 사용할 수 있다.

개발 영단어

  • API (일종의 계약)
      1. create software: Apple Developer Document 사용할 때
    • 2-1. interact with an external system: 틴더에서 페이스북 친구목록을 가져올 때
    • 2-2. 앱에서 API Docs(Client)에서 정해진 방식으로 Query로 request하면 API Provider(Web Server)에서 data로 response한다. -> 네트워킹

영단어

  • fudge: 캔디의 일종, 꾸미다, 속이다
    fudge our code
  • proprietary: 소유권
  • trigger: trigger a method
  • bleep: 병원에서 쓰는 호출 장치, 삐삐같은 거
  • cardiac arrest: 심정지
  • resuscitate: 소생시키다
  • paramedic: 낙하산 부대 군의관
  • Kelvin: 온도 단위
  • Celsius섭씨C, Fahrenheit화씨F
  • boil down: 졸이다, boil down a function into a data type
    (Int, Int) -> Int
  • convoluted: 뒤얽힌
  • one down two to go: 한 놈 잡았고, 두 놈 남았다
  • hone in: ~에 집중하다, 기술을 연마하다. hone in on accurate data
  • stub: 메서드, add protocol stubs 프로토콜을 추가하면 추가해야 하는 메서드
  • fiat currency: 실물 화폐의 반댓말, 중앙 은행의 신용에 의해 유통되는 지폐(신용지폐), 예를 들어 USD
profile
인생노잼

1개의 댓글