궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Udemy iOS

[Udemy] 섹션13 : Protocols, Networking, Delegate 패턴, JSON Parsing, UITextField 날씨 앱

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

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

 

다크 모드

레이블 색상

Main.Stroyboard에서 inspector의 tint의 색상을 Label Color로 정해주게 되면 라이트 모드일 때는 검은색으로, 다크 모드일 때는 하얀색으로 색상의 컬러가 변하게 된다.

 

사용자가 지정한 Custom Color 일 때는 Assets에서 Light 모드일 때와 Dark 모드 일 때의 색을 정해줄 수 있다.

배경 이미지 

단순히 텍스트 컬러만 바꾸는 것이 아닌 background의 image도 변경할 수 있다. 백터 이미지나 pdf이미지도 사용이 가능하다.

pdf파일을 assets파일의 background에 drop하면 된다.

  • Inspector > Scale > Single
  • Appearance > Any, Light, Dark

다크 모드로 변경해도 설정한 배경화면이 보이지 않으면 preserve vector data 체크를 풀면 확인 가능.  +  + A

 

TIP
 
 

백터 이미지를 사용하면 좋은 점
jpegpng 같은 파일을 Assets에 추가하여 확대해보면 이미지가 픽셀화 된 것을 볼 수 있는데, 화면이 커질수록 픽셀이 깨진다. 하지만 pdf파일 형식으로 이미지를 가져오게 되면 이미지를 픽셀화 하지 않고 각각의 위치를 계산하기 때문에 픽셀이 깨지지 않는다. MainStoryboard에서는 크기가 고정되어있어서 픽셀이 깨져 보이지만, 실제로 시뮬레이터를 돌려보면 깨지지 않는 것을 확인할 수 있다.

 

VC연결

1. textFieldVC에 연결하고 시뮬레이터를 실행한다.

2. textField 분을 터치해서 키보드를 확인할 수 있다. 만약 소프트웨어 키보드가 올라오지 않는다면 아래 키를 누르면 된다.

I/O > Keyboard > Toggle Software Keyboard ( + K)

import UIKit

class WeatherViewController: UIViewController {

    @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()
        // Do any additional setup after loading the view.
    }


    @IBAction func searchPressed(_ sender: UIButton) {
        print(searchTextField.text!)
    }
}

UITextFieldDelegate

 

Apple Developer Documentation - UITextFieldDelegate

UITExtFieldDelegate에 대해 자세히 알아보기

 

텍스트 필드에 Korea를 입력하고 키보드 이동 버튼을 누르면 아무런 반응이 없는데, 이동 버튼을 눌렀을 때 반응을 나타나게 하기 위해선 UITextFieldDelegate를 사용해야 한다. 

 

Delegate는 쉽게 말해서 ViewController에게 전달하는 것이다. 무엇을? 사용자가 어떤 행동을 하고 있는지를.

 

"사용자가 지금 텍스트를 입력하고 있어!", "텍스트 입력을 멈췄어!", "사용자가 다른 곳을 탭 하려고 해!" 하는 것처럼 말이다.

 

1. UIViewController에 쉼표를 하고 UITextFieldDelegate를 추가한다.

class WeatherViewController: UIViewController, UITextFieldDelegate {
}

2.  viewDidLoad 내부에 searchTextField.delegate = self를 넣어준다. 여기서 selfviewController를 뜻한다.

textFieldShouldReturn

3. 이 둘을 수행할 수 있게 해 주는 것이 textFieldShouldReturn 메서드이다. "사용자가 키보드에서 return키를 눌렀다!"라고 VC에 전달한다는 의미!

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        print(searchTextField.text!)
        return true  // shouldReturn 타입이 bool 이기 때문에 bool 타입을 반환해준다.
    }

TIP
 
 

키보드의 리턴을 사용해야 합니까?
(_ textField: UITextField) 이곳을 주의 깊게 살펴보면, 화면에 텍스트 필드가 여러 개여도 텍스트 필드마다 작업을 따로 설정해줄 필요가 없어진다.

 

키보드 닫기

키보드가 아닌 다른 화면 어딘가를 탭 했을 때 키보드가 닫히게 할 수 있다.

 

 

SearchPressed 내부에 endEditing 추가

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        searchTextField.endEditing(true)
        print(searchTextField.text!)
        return true
    }
    
    @IBAction func searchPressed(_ sender: UIButton) {
        searchTextField.endEditing(true)
        print(searchTextField.text!)
    }

textFieldDidBeginEditing

First Responder가 된 직후 호출하는 인스턴스

textFieldDidEndEditing

4. textField에 텍스트를 입력 후 이동 또는 서치 아이콘을 클릭했을 때 키보드가 닫히는 것까지는 했는데, "사용자가 텍스트 작성을 중지했다!"라고 뷰 컨트롤러에 전달했을 때 실행하는 부분을 textFieldDidEndEding 메서드를 사용하여 지정해줄 수 있다.

// 키보드의 return키 또는 search icon을 탭했을때 textField가 clear됨

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

textFieldShouldEndEditing

5. 유효성 검사에 유용한 메서드이다. 예를 들어 텍스트 필드가 empty가 아니라면 search 될 수 있도록 하고, empty라면 placeholder가 그대로 남아 있도록 해줄 수 있다. textFieldShouldEndEditing 메서드를 사용한다.

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        if textField.text != "" {
            return true
        } else {
            textField.placeholder = "찾으려는 도시를 입력해주세요."
            return false //입력되지 않았기 때문에 키보드를 숨기지 않는다.
        }
    }

API

API? Application Programming Interface (API)

쉽게 말해서 API는 개발자와 API Provider(공급자) 간의 Contract(계약)이라고 보면 된다.

 

날씨 앱을 기준으로 보자면, 앱에 도시명을 입력 API Provider에게 전달해당 도시의 날씨를 개발자에게 전달이라고 보면 된다.

Open Weather Map API Documentation

OpenWeather 이 사이트로 가서 우선 회원가입을 하고 API를 직접 사용해야 한다.

TIP
 
 

JSON data를 tree structure로 보려면 JSON Viewer Awesome를 크롬 extension을 다운로드하면 편하지만, https://jsonlint.com 이 웹사이트를 더 추천함.

 

API 호출 방법

api.openweathermap.org/data/2.5/weather?q={city name}&appid={API key}
//API key가 활성화되는데 까지는 10분에서 하루의 시간이 걸릴 수 있다.

서울의 날씨를 가져와 본다면 아래와 같은데, JSON의 temp를 보면 290으로 표시되어있다. 이것은 켈빈이라는 단위이므로 이것은 섭씨로 바꿔줄 수 있다.

{
	"coord": {
		"lon": 126.9778,
		"lat": 37.5683
	},
	"weather": [{
		"id": 741,
		"main": "Fog",
		"description": "fog",
		"icon": "50n"
	}],
	"base": "stations",
	"main": {
		"temp": 290.47,
		"feels_like": 290.71,
		"temp_min": 288.38,
		"temp_max": 291.81,
		"pressure": 1009,
		"humidity": 94
	},
	"visibility": 2500,
	"wind": {
		"speed": 2.06,
		"deg": 320
	},
	"clouds": {
		"all": 4
	},
	"dt": 1632940392,
	"sys": {
		"type": 1,
		"id": 8105,
		"country": "KR",
		"sunrise": 1632950780,
		"sunset": 1632993472
	},
	"timezone": 32400,
	"id": 1835848,
	"name": "Seoul",
	"cod": 200
}

측정단위를 변경하려면 Units of measurement 탭에서 확인 가능하다.

나의 경우엔 섭씨를 사용할 것이기 때문에 metric units를 선택하여 아래 주소를 tree structure로 변경하여 확인했다.

api.openweathermap.org/data/2.5/weather?q=Seoul&appid=내API입력&units=metric

변경 전, 변경 후

Search 또는 Return키 터치

이제 Search아이콘 또는 Return키를 눌렀을 때 TextField에 입력한 도시가 결과로 보이도록 해줘야 한다. 이것을 수행하는 곳은 textFieldDidEndEding 메서드이다.

 

위에서 TextField의 text를 ""로 초기화했었는데 이것을 수행하기 전에 다음과 같이 진행하면 된다.

 

Xcode > New File > Swift > WeatherManager

 

weatherURL에서 도시를 검색할 때 쿼리인 q= 를 입력했었는데 그것을 TextField에서 검색한 텍스트로 넣어줄 것이기 때문에 아래와 같이 코드를 변경해 줄 수 있다.

//WeatherManag.swift

import Foundation

struct WeatherManager {
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=내API입력&units=metric"
    
    func fetchWeather(cityName: String) {
        let urlString = "\(weatherURL)&q=\(cityName)"
        print(urlString) //확인을 위한 출력
    }
}

WeatherViewController에서 weatherManager를 초기화하여 호출해주고, textFieldDidEndEditing 메서드에 텍스트 필드에 입력된 텍스트를 도시명으로 가져와서 API를 호출하려고 한다.

 

만약 let city = searchTextField.text를 바로 가져오게 된다면 searchTextField는 옵셔널 이기 때문에 래핑을 해줘야 한다.

//WeatherViewController.swift


class WeatherViewController: UIViewController, UITextFieldDelegate {

         ...
         
    var weatherManager = WeatherManager()
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        if let city = searchTextField.text {
            weatherManager.fetchWeather(cityName: city)
        }
        searchTextField.text = ""
    }
}

디버깅

Networking

Swift에서 네트워킹 작업을 수행하려면 조건이 있다.

첫 번째  Create a URL 

두 번째  Create a URLSession

세 번째  Give URL Session a task

네 번째  Start the task (hit the enter on the chrome)

//WeatherManager.swift

struct WeatherManager {
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=내API&units=metric"
    
    func fetchWeather(cityName: String) {
        let urlString = "\(weatherURL)&q=\(cityName)"
        performRequest(urlString: urlString) //performRequest 메서드 호출
    }


    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 URL Session a task (세션에 작업을 부여)
            let task = session.dataTask(with: url, completionHandler: (Data?, URLResponse?, Error?) -> Void)
            //4. Start the task (hit the enter on the chrome)
            task.resume()
    }

3번은 잠깐 보류하고 4번부터 보면, 왜 Start가 아닌 resume을 썼을까?

 

그 이유는 "새로 초기화된 작업은 일시 중단된 상태에서 시작되므로 이 메서드를 호출하여 작업을 시작해야 하기 때문" 이다.

 

그럼 보류해뒀던 3번의 completionHandler 부분도 처리해야 한다.

 

Handler?

프로그래밍에서는 시간이 많이 걸리는 작업들이 있다. 

 

인터넷에서 데이터를 다운로드해서 가져올 때 URL로 이동하여 데이터를 수집하고 다시 돌아와야 하는데, 개인의 인터넷 속도에 따라 1초 또는 몇 분의 시간이 소요될 수 있다. 개발자들은 항상 위와 같은 문제에 부딪힌다.

 

여기서 completionHandler에 대해 알아두는 것이 좋은데 간단히 말해서 "어떠한 일이 끝났을 때 진행할 업무를 담당" 한다고 생각하면 된다.

 

3번을 보면 completionHandler가 매개변수처럼 보이지만, 함수의 출력에 대해서 꺾쇠 출력이 Void 즉, 실제로 출력이 없다. 확실히 함수임을 알 수 있다. (함수를 만들어줘야 함)

            let task = session.dataTask(with: url, completionHandler: handle(data:response:error:))           
            //4. Start the task (hit the enter on the chrome)
            task.resume()
        }
    } 
    func handle(data: Data?, response: URLResponse?, error: Error?) -> Void {   
    }
}

handle이라는 함수를 만들고 completionHandler에 이 함수를 삽입해줬다.

 

차근차근 handle 함수를 풀어보자.

1. error

만약 errornil과 같지 않다면, 무슨 error인지 출력하고 모든 것을 중지해라 (return)

 

만약 return 뒤에 아무것도 오지 않는다면 "모든 기능을 중지 해라" 라는 의미이다.

func handle(data: Data?, response: URLResponse?, error: Error?){
   if error != nil {
      print(error!) //error는 옵셔널 상태이기때문에 강제 언래핑
      return
   }
}

2. data

error에서 오류가 없다면 실행된다.

 

옵셔널바인딩을 사용하여 safeData를 생성한다. 실제로 Data는 문자열로 쉽게 출력이 가능하지만 safeData를 출력하기 위해서는 문자열로 변환해야한다.

func handle(data: Data?, response: URLResponse?, error: Error?) -> Void {
        if error != nil {
            print(error!)
            return
        }
        if let safeData = data {
            let dataString = String(data: safeData, encoding: .utf8)
            print(dataString)
        }
}

이렇게 까지 작성하고 시뮬레이터를 실행해보면 JSON Data가 출력되는 것을 확인할 수 있다.

 

 

----------  문법  ----------

 UITextField, UITextFieldDelegate

UITextField에 대해 자세히 알아보기

Swift : UITextField

Protocols

클로저

클로저에 대해 자세히 알아보기

Swift : 클로저 및 고차함수(map, filter, reduce)

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.