[udemy]Section13

서희찬·2022년 7월 5일
0

swift

목록 보기
16/17
post-thumbnail

이번 섹션에서는 Clima라는것을 만들어 볼 것인데
API를 가져와서 날씨를 알려주는 어플이다.
우리는 이번 시간에 아래를 배워볼 것이다.

  • How to create a dark-mode enabled app and use vector assets.
  • Learn to use the UITextField to get user input
  • Learn about Swift Protocols and the Delegate Design Pattern
  • Learn to work with APIs by making HTTP requests with URLSession
  • Parse JSON with the native JSONDecoder
  • Learn to use computed properties, closures and extensions.
  • Learn to use Core Location to get the GPS data

How to create a dark-mode enabled app and use vector assets.

image -> SF Symbols

https://developer.apple.com/design/human-interface-guidelines/foundations/sf-symbols

여기에 많은 이미지들이 구비되어 있다.
시스템에 다크모드가 있는데 이는 label Color를 적용해주면 알아서 검정 흰색으로 모드에 따라 바뀐다.
대신 시스템칼라하면 안바뀐다!

근데 우리는 그냥 검정->흰 보다 UI를 좀 더 보기 좋게 만들기 위해서 배경색이랑 같게 해주고 싶은데
이를 위해 Assets에서 Color를 추가해주자.


이렇게 색상을 추가해주어서 다크모드와 Light모드일때 원하는 색이 가능하다.

Single Scale, Resizing -> Vector 데이터로 변경할 수 있다.

그 후 Appearance에서 다크모드와 라이트모드 배경을 만들 수 있다.

이러고 나면 다크모드 시 배경과 색상변경이 가능하며, 벡터 assest을 사용하면 어떤 폰이든 깔끔하게 나오는것을 확인할 수 있다.

Learn to use the UITextField to get user input

    @IBAction func searchPressed(_ sender: UIButton) {
        print(searchTextField.text!)
    }
    
    // return 버튼으로 값 반환하게 만들기 
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        print(searchTextField.text!)
        return true
    }

이렇게 키보드에서 return 버튼을 누르면 값을 반환하게, 검색 버튼을 누르면 반환하게 2가지 케이스를 만들어줄 수 있다.
그런데... 이렇게 검색 후 키보드가 사라지지않고 유지되므로 키보드를 사라지게 만들어보자.

searchTextField.endEditing(true)

이 코드를 추가해주면 된다.

그리고 editing이 끝날 때 어떤 명령을 주고 싶다면

func textFieldDidEndEditing(_ textField: UITextField) {
        searchTextField.text = ""
    }

이 함수를 사용해서 안에 빈칸으로 넣게 만들 수 있다.

그 후

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        if textField.text != ""{
            return true
        } else {
            textField.placeholder = "Type Something"
            return false
        }
    }

이 코드를 추가하면 ..! 된다.

protocols

프로토콜은 아래와 같이 작성한다.

    protocol <#name#> {
        <#requirements#>
    }

https://medium.com/@jgj455/%EC%98%A4%EB%8A%98%EC%9D%98-swift-%EC%83%81%EC%8B%9D-protocol-f18c82571dad

프로토콜에 대해 잘 설명해주셨는데 정의만 가져오자면 프로토콜이란 특정 역할을 하기 위한 메소드, 프로퍼티, 기타 요구사항 등의 청사진이라고 보면된다 !

그래서 구조체나 클래스에 프로토콜을 주기 위해서는 strct name : protocol 방식으로 해주면 된다.
class는 처음은 슈퍼클래스 두셋부터 프로토콜을 반영한다.

The Delegate Design Pattern

(대리자 디자인 패턴...?)
https://tonyw.tistory.com/15

https://velog.io/@zooneon/Delegate-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

UITextField가 weatherViewCotroller한테 알리는걸 넣어주면 되는데
하나하나 다른 클래스가 있을때마다 우리는 그것을 예측하고 추가 할 수 없다.
이때 문제가 발생한다.
우리는 UITextField이를 최대한 재사용하기를 원한다.
그러나... 우리는 미래 사용자가 만드는 클래스를 알 수 없다.
그를 해결하기 위한 하나의 해결책이

The Delegate Pattern

이다.
애플은 이를 사용하는걸 매우 좋아한다.
아래 예시로 프로토콜 하나가 있다고 생각해보자.

protocol CanDealWithUITextFields{
	func textFieldDidBeginEditing()
}

이제 다시 생각해보자
재사용 가능한 UITextField가 있다. 이는 애플에서 제작한것잉다.
그들은 어떠한 다른 클래스에 대해서 모른다.
그리고 우리는 우르의 WeatherViewController가 있다.
위 둘은 서로 소통을 위해서 필요로하다.
이때 protocol을 UITextFieldDelegate라고 생각하자.

이때 UITextField는 delegate를 가지는데 이 delegate의 data type은 UITextFieldDelegate이다.

An Example of protocols and Delegates in practice

환자-cpr

protocol AdvancedLifeSupport {
    func performCPR()
}


class EmergencyCalHandler{
    var delegate: AdvancedLifeSupport?
    
    func asseseSitulation(){
        print("Can you tell me what happend?")
    }
    
    func medicalEmergency(){
        delegate?.performCPR()
    }
}

struct Paramedic: AdvancedLifeSupport{
    
    init(handler: EmergencyCalHandler) {
        handler.delegate = self
    }
    
    func performCPR() {
        print("The paramedic does chest compressions, 30 per second.")
    }
    
    
}

let emilio = EmergencyCalHandler()
let peto = Paramedic(handler: emilio)

emilio.asseseSitulation()
emilio.medicalEmergency()
protocol AdvancedLifeSupport {
    func performCPR()
}


class EmergencyCalHandler{
    var delegate: AdvancedLifeSupport?
    
    func asseseSitulation(){
        print("Can you tell me what happend?")
    }
    
    func medicalEmergency(){
        delegate?.performCPR()
    }
}

struct Paramedic: AdvancedLifeSupport{
    
    init(handler: EmergencyCalHandler) {
        handler.delegate = self
    }
    
    func performCPR() {
        print("The paramedic does chest compressions, 30 per second.")
    }
    
    
}


class Docotr:AdvancedLifeSupport {
    
    init(handler: EmergencyCalHandler){
        handler.delegate = self
    }
    
    func performCPR() {
        print("The doctor does chest compressions, 30 per second.")
    }
    
    func useSetthescope(){
        print("Listening for heart sounds")
    }
}

class Surgeon: Docotr{
    override func performCPR() {
        super.performCPR()
        print("Sings staying alive by the beegees")
    }
    
    func useElectricDrill(){
        print("Whirr...")
    }
}

let emilio = EmergencyCalHandler()
let angela = Surgeon(handler: emilio)

emilio.asseseSitulation()
emilio.medicalEmergency()

procols 과 delegate를 익히는 시간을 가졌다..
https://stackoverflow.com/questions/5431413/difference-between-protocol-and-delegates
에 차이점을 확인할 수 있다.
https://shark-sea.kr/entry/swift-delegate%ED%8C%A8%ED%84%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

protocol : 붕어빵 기계
이기에 기능을 전부다 구현하는것이 아니라 선언만 해둔다.

delegate : 알바생
이라보면 된다.
이 일바생은 메뉴얼대로 일을 처리한다.
그렇기에 protocol은 메뉴얼 delgate는 이 메뉴얼대로 일하는 알바생이라고 생각하자.

API : Application Programming Interface

이는 간단히 개발자와 api제공자사이의 계약서라고 보면된다.
우리는 날씨 어플을 제작할것이니!
https://openweathermap.org/current#name
의 api를 사용하자

import Foundation

struct WeatherManager {
    let weatherURL =
    "https://api.openweathermap.org/data/2.5/weather?appid={API}"
    
    
    func fetchWeather(cityName: String){
        let urlString = "\(weatherURL)&q=\(cityName)"
        print(urlString)
    }
    
}

이런 방식으로 city를 가져와서 그 도시의 날씨 정보를 알 수 있는 페이지를 가져올 수 있따.

이 과정을 프로그래밍에서 네트워킹이라고 한다
나의 앱이 api를 통하 web Server와 이야기하기 때문이다.

query를 전달하면 web server 데이터로 응답한다.
이를 네트워킹이라고 한다.

  • STEP 1 : Create a URL
  • STEP 2 : Create a URLSession
  • STEP 3 : Give URLSession
  • STEP 4 : Start the task

이 코드를 추가해보자

import Foundation

struct WeatherManager {
    let weatherURL =
    "https://api.openweathermap.org/data/2.5/weather?appid=d932ef89f1585a36bfea388c8e5e608f"
    
    
    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 session a 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?) -> Void {
        if error != nil { // error 체크
            print(error!)
            return
        }
        
        if let safeData = data{
            let dataString = String(data: safeData, encoding: .utf8)
            print(dataString)
        }
    }
    
    
}

이와 같이 web에 있는 정보를 긁어올 수 있다.

만약 1022에러가 뜨면 http -> https로 바꿔주면된다.
completionHandler를 사용하는 방법과
taks object를 통해 메서드를 불러 인터넷에서 데이터를 가져오는것을 배웠다.
https://velog.io/@wannabe_eung/Swift-CompeltionHandler%EB%A5%BC-%EC%98%88%EC%8B%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
클로져를 공부하고 오자..

Closures

클로져는 스위프트의 꽃일 만큼 무조건!!
익히고 가야하는 문법이다.
사실 함수 또한 클로져인데

이와 같이 이름 있는 클로져는 함수라고 부르고
이름 없는 익명함수를 클로져라고 보통 부른다

그러니깐 둘다 클로져인데 보통은 클로져라고하면 익명함수를 부르는거다.
(func을 안적는다.)

클로져에 대해
https://babbab2.tistory.com/81
자세히 정리되어있다.

import UIKit


// 이름있는 클로져
func calculator (n1: Int, n2 :Int,operation : (Int,Int)->Int)->Int{
    
    return operation(n1,n2)
}

//func add(no1:Int, no2:Int)->Int{
//    return no1+no2
//}

func multiply(no1:Int, no2:Int)->Int{
    return no1 * no2
}

calculator(n1: 2, n2: 3,operation: multiply)

이를 클로져를 쓰면

import UIKit

func calculator (n1: Int, n2 :Int,operation : (Int,Int)->Int)->Int{
    
    return operation(n1,n2)
}

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

이와같이 한번에 작성할 수 있다.

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

이렇게 줄이는게 가능해서
더 못줄일거같지만.. 더 줄일 수 있다
파라미터 네임도,,줄이면

let reuslt = calculator(n1: 2, n2: 3){$0*$1}

trailing clousre
이는 꼬리 클로저, 후행클로져이다.
https://admd13.tistory.com/45

매개변수 지정시
단축인자 $0~이 사용가능하다.

이 클로져는 map함수를 위해서..! 중요하다!
아! 클로져는 python의 lamda함수와 비슷한 개념이라고 생각하면 될거같다.

map함수를 보자


오... 파이썬이랑 같다..!?
addOne을

import UIKit

let array = [6,2,3,9,4,1]


// array.map({(n1) in n1+1})
이렇게 해서
array.map{$0 + 1}) 

완전 줄일 수 있다..
이 클로져를 사용하면 코드를 좀 더 쉽게 읽을 수 있ㄷ는디.. 잘 모르겟따 하핫

쨌든 많이 중요하다고 하니깐 익히자 익명함수

JSON Decoding

배운 클로져를 적용해주자 !

import Foundation

struct WeatherManager {
    let weatherURL =
    "https://api.openweathermap.org/data/2.5/weather?appid=d932ef89f1585a36bfea388c8e5e608f"
    
    
    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 session a task
            
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil { // error 체크
                    print(error!)
                    return
                }
                
                if let safeData = data{
                    let dataString = String(data: safeData, encoding: .utf8)
                    print(dataString)
                }
            }
            
            //4. Start the task
            task.resume()
        }
    }
    
    
}

훨 간단해졌다.
그리고 우리가 가져올 데이터는 JSON형식인데
이는 뭘까?

JSON : JavaScript Object Notation

데이터 교환 방식인데 이에 대한 자세한 설명이 아래 블로그에 있다!
https://velog.io/@surim014/JSON%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

그러면 json데이터를 가져와보자

WeatherManager.swift
    func parseJSON(weatherData: Data){
        let decoder = JSONDecoder()
        do{
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            print(decodedData.main.temp)
        }catch{
            print(error)
        }
        
    }
WeatherData.swift

import Foundation

struct WeatherData: Decodable{
    let name: String
    let main: Main 
}

struct Main: Decodable{
    let temp:Double
    
}

이와 같이 미리 데이터에 구조를 만들어주고 불러와야지 메니저파일에서 에러가 안뜬다.

이제 Weather의 0번째 요소의 Descirption
즉 weater[0].description은 어떻게 가져올까?

import Foundation

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

struct Main: Decodable{
    let temp:Double
}

struct Weather: Decodable{
    let description : String
}
print(decodedData.weather[0].description)

이와 같이 가져오는것이 가능하다.

이제 날씨에 따라
화면의 날씨아이콘이 바뀌는 기능을 구현하자
각 날씨마다 id를 가지고 있고
아이디별로 날씨아이콘이 다르다.
이 id를 받아 switch문을 써서 아이디별로 날씨가 다른게 구현한 코드는 아래와 같다.

    func parseJSON(weatherData: Data){
        let decoder = JSONDecoder()
        do{
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            let id = decodedData.weather[0].id
            print(getConditionName(weatherId: id))
        }catch{
            print(error)
        }
        
    }
    func getConditionName(weatherId : Int) -> String{
        switch weatherId {
                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"
                }
    }
     

이를 model에 WeatherMdoel.swift에 몰아넣어주면 아래와 같다.

import Foundation

struct WeatherModel{
    let conditionId: Int
    let cityName: String
    let temperature : Double
    
    // 기온 소수점 한자리까지 가져오기
    var temperatureString: String{
        return String(format: "%.1f",temperature)
    }
    // 날씨 컨디션 코드에 따라 이름가져오기
    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"
                }
    }
    
}

typealiases

기존에 선언되어 있는 유형에 새로운 유형의 별칭을 사용함으로써
코드를 더 읽기, 이해하기 쉽도록 명확하게 만드는 문법이다.

https://ginjo.tistory.com/20

이를 통해
delegate디자인 패턴으로 변경 하였다.

parameter Names

func myFunc(external internal:Type){
	print(internal)
}

이 함수에서 external에 underscore를 넣는경우가 있다
이는

함수 호출시 넘기는 파라미터 앞에 라벨을 생략하기 위해 _을 사용하는 것이다.

자세한 설명은
https://medium.com/@codenamehong/swift-underscore-90dcbec5072f
을 참고하자

import Foundation
import CoreLocation

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

struct WeatherManager {
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e72ca729af228beabd5d20e3b7749713&units=metric"
    
    var delegate: WeatherManagerDelegate?
    
    func fetchWeather(cityName: String) {
        let urlString = "\(weatherURL)&q=\(cityName)"
        performRequest(with: urlString)
    }
    
    func fetchWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
        let urlString = "\(weatherURL)&lat=\(latitude)&lon=\(longitude)"
        performRequest(with: urlString)
    }
    
    //with
    func performRequest(with urlString: String) {
        if let url = URL(string: urlString) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    self.delegate?.didFailWithError(error: error!)
                    return
                }
                if let safeData = data {
                    if let weather = self.parseJSON(safeData) {
                        self.delegate?.didUpdateWeather(self, weather: weather)
                    }
                }
            }
            task.resume()
        }
    }
    
    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 {
            delegate?.didFailWithError(error: error)
            return nil
        }
    }
    
    
    
}

이렇게 수정하고 에러 뜨는 곳도 알려주게 수정해주면 더 좋은 코드가 된다.

DispatchQueue

그냥가져오면 에러가 뜨는데
그를 위해 DispatchQueue를 사용하면된다.

https://seons-dev.tistory.com/entry/Swift-DispatchQueue%EB%9E%80-GCD-Grand-Central-Dispatch
여기에 dispatchqueue에 관해 설명이 아주자세히 되어있다
병렬적으로 실행해주어 바로가져올 수 있다

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

이렇게 하고나면 이미지와 온도를 가져와 ui변경이 가능하다.

Extensions

확장은 기존 클래스, 구조체 또는 열거형 타입에 새로운 기능을 추가하는것이다.
기존 소스 코드에서 접근하지 못하는 타입들을 확장하는 능력이다.

  • 계산 속성과 계산 정적 속성 추가
  • 인스턴스 메소드와 타입 메소드 정의
  • 새로운 이니셜라이저 제공
  • 서브스크립트 정의
  • 새로운 중첩 타입 정의와 사용
  • 기존 타입에 프로토콜 적용하기
    (출처 : http://minsone.github.io/mac/ios/swift-extensions-summary)
    위에 확장에 관한 자세한 설명참고하자

이 배운걸 통해 vc를 리팩토링해주자

단축키 설정


이렇게 단축키도 만들 수 있고 placeholder 만들고 싶으면 <#place holder#>방식으로 적어주면 된다.

GPS DATA

gps에 필요한 것들을 추가해주고,

이와 같이 plist수정을 해준다.

그러면 뭐 권한요청도 뜬다.

gps 가져오는거 정리잘해놓은 블로그다.
1. https://fomaios.tistory.com/entry/iOS-%EC%95%B1%EC%82%AC%EC%9A%A9%EC%A4%91-%EC%9C%84%EC%B9%98-%EA%B6%8C%ED%95%9C-%EB%B0%9B%EC%95%84%EC%98%A4%EA%B3%A0-%ED%98%84%EC%9E%AC-%EC%9C%84%EC%B9%98-%EC%95%8C%EC%95%84%EB%82%B4%EA%B8%B0-Request-Location-Authorization-Detect-Current-Location

  1. https://42kchoi.tistory.com/299

완성코드이다.


import UIKit
import CoreLocation

class WeatherViewController: UIViewController {
    
    @IBOutlet weak var conditionImageView: UIImageView!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var searchTextField: UITextField!
    
    var weatherManager = WeatherManager()
    let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization() //권한요청
        locationManager.requestLocation()
        
        weatherManager.delegate = self
        searchTextField.delegate = self
    }

}

//MARK: - UITextFieldDelegate

extension WeatherViewController: UITextFieldDelegate {
    
    @IBAction func searchPressed(_ sender: UIButton) {
        searchTextField.endEditing(true)
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        searchTextField.endEditing(true)
        return true
    }
    
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        if textField.text != "" {
            return true
        } else {
            textField.placeholder = "Type something"
            return false
        }
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        
        if let city = searchTextField.text {
            weatherManager.fetchWeather(cityName: city)
        }
        
        searchTextField.text = ""
        
    }
}

//MARK: - WeatherManagerDelegate


extension WeatherViewController: WeatherManagerDelegate {
    
    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
        }
    }
    
    func didFailWithError(error: Error) {
        print(error)
    }
}

//MARK: - CLLocationManagerDelegate


extension WeatherViewController: CLLocationManagerDelegate {
    
    @IBAction func locationPressed(_ sender: UIButton) {
        locationManager.requestLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            locationManager.stopUpdatingLocation()
            let lat = location.coordinate.latitude
            let lon = location.coordinate.longitude
            weatherManager.fetchWeather(latitude: lat, longitude: lon)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(error)
    }
}

이렇게 gps추가하고 lat, lon값을 전달해 내 위치의 날시정보를 가져 올 수 있다.

profile
부족한 실력을 엉덩이 힘으로 채워나가는 개발자 서희찬입니다 :)

0개의 댓글