포스트

Clima (5)

파라미터 수정과 프로토콜

현재 이렇게 함수가 되어있는데, _ 를 사용하여 value만 입력하게 해보자

1
2
3
func didUpdateWeather(weather : WeatherModel) {
        print(weather.temperature)
    }

그리고 이 함수가 근본적으로 어디서 왔는지? 를 함수안의 변수에 명시를 해준다.

이 함수는 잘 생각 해보면?

1
2
3
protocol WeatherManagerDelegate{
    func didUpdateWeather(weather : WeatherModel)
}

이 프로토콜에서 왔다!

그리고 이 대리자(delegate)를 발생시킨 개체는?

1
2
3
4
5
struct WeatherManager {

    // 기타 코드 생략.
    
    var delegate : WeatherManagerDelegate?

여기서 발생시켰다.

즉 우리는 다시 didupdateWeather로 돌아가서 파라미터에 그 delegate를 발생시킨 개체, 즉 정체성을 부여해준다.

1
2
3
func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel) {
        print(weather.temperature)
   }

함수의 내역을 수정했으니 다시 프로토콜로 돌아가서 변수를 다음과 같이 바꿔주자!

지금은 역순으로 프로토콜의 구성을 해보았다.

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

즉 프로토콜을 구현할때는 다음과 같이 한다.

1
2
3
4
protocol myProtocol {

    func myFunc (_ delegate발생시킨개체, 일반적으로 사용하는 매개변수와 타입)
}

이것을 생각해서 위의 프로토콜과 내용을 다시 정리한다면?

  • weatherManager:WeatherManager : delegate 발생시킨 개체
    • var delegate : WeatherManagerDelegate?
  • weather : WeatherModel : 실제로 우리가 사용할 변수, 타입

이렇게 되는 것이다.

구글링을 좀 해보니 일반적으로는 이런식으로 프로토콜을 사용하여 코드를 구현하는것 같다.

  1. 프로토콜 구현

  2. 일을시킬 컨트롤러에 delegate 선언 (우리는 구조체로 했다.)

  3. 일을할 컨트롤러를 대리자로 설정

  4. 할 일을 지시

  5. 지시 받은 동작을 대리자에서 대신 처리하기


그렇다면 위의 순서를 현재까지 내가 한 내용에 접목을 시켜 보자

물론 우리는 공부를 하고 있는 입장이라 역순으로 갔지만, 이렇게 대입을 해보는것도 나쁘지 않다는 생각이 들었다.

  1. 프로토콜 구현
1
2
3
protocol WeatherManagerDelegate{
    func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel)
}
  1. 일을시킬 컨트롤러에 delegate 선언 (우리는 구조체로 했다.)
1
2
3
struct WeatherManager {
    var delegate : WeatherManagerDelegate?
}    
  1. 일을할 컨트롤러를 대리자로 설정
1
2
3
4
// WeatherViewController
 override func viewDidLoad() {
    weatherManager.delegate = self
 }
  1. 할 일을 지시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func performRequest(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 { // 에러가 발생하면 출력
                    print(error!)
                    return // 여기선 return을 사용하면 함수를 빠져나가 아무것도 하지말라는 것이다
                }
                
                if let safeData = data {
                    if let weather = self.parseJSON(weatherData: safeData) {
                        self.delegate?.didUpdateWeather(self, weather: weather)
                        
                    }
                }
            }
            
            task.resume()
        }
    }
  1. 지시 받은 동작을 대리자에서 대신 처리하기
    1
    2
    3
    
    func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel) {
         print(weather.temperature)
     }
    

이렇게 되는 것이다!

이렇게 정리를 하고 나니 뭔가 좀 전체적인 흐름이 이해가 갔다!

현재 정리한 부분에 미리 self로 하나를 적어놨는데 그부분부터 시작하겠다.

다시 돌아가서,

1
2
3
4
5
if let safeData = data {
     if let weather = self.parseJSON(weatherData: safeData) {
             self.delegate?.didUpdateWeather(weather: weather)
              }
     }

다음과 같이 에러가 난다

왜냐 우린 프로토콜을 다음과 같이 정의 했기 때문이다

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

현재 weatherManager가 빠져있는 걸 알 수 있다. fix를 눌러보면?

위와 같이 된다.

현재 WeatherManager에서 작성하고 있으므로, 그자신이니까 self로 해주자!

에러가 수정되었다.

에러를 다룰 함수 만들기

1
2
3
4
5
protocol WeatherManagerDelegate{
    func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel)
    func didFailWithError (error: Error)
}

다음과 같이 프로토콜에 함수를 하나 만들어주자!

이 함수는

1
2
3
4
if error != nil { 
            print(error!)
             return
            }

이 부분을 다룰 목적으로 만들어졌다.

print대신 delegate를 써서 위임을 해보도록 하자.

1
2
3
4
if error != nil { // 에러가 발생하면 출력
               self.delegate?.didFailWithError(error: error!)
                return // 여기선 return을 사용하면 함수를 빠져나가 아무것도 하지말라는 것이다
                }

역시나 클로저 안이기에 self를 작성해주자.

그리고 또한 parseJSON에서도 catch 구문이 바로 에러가 발생할때인데, 이때도 delegate에 위임을 하도록 해보자.

1
2
3
4
catch {
       delegate?.didFailWithError(error: error)
        return nil
        }

Weather뷰 컨트롤러에서 에러가 치고있다

우리가 만든 WeatherManagerDelegate 프로토콜을 준수하는데

함수가 새로 생겼는데 현재 여기엔 없으니 프로토콜을 준수하지 않아 발생하는 에러이다.

이제 에러가 발생하지 않게 함수를 만들어 주자.

1
2
3
func didFailWithError(error: Error) {
        print(error)
    }

가장 쉬운방법은 이렇게 에러를 프린트 하는 것이다.

하지만 어떤 오류가 발생하느냐에 따라 그걸 사용자에게 알려줘야할 필요가 있다!

여기선 네트워크 에러와, JSON Decoder 작동 실패 이렇게 두가지로 나뉜다.

실시간 데이터 업데이트 해보기 (dispatchQueue)

이제 뷰 컨트롤러로 돌아가서 데이터를 업데이트할 준비를 해보자.

실제로 데이터 업데이트를 담당할 함수는 이녀석이다.

1
2
3
func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel) {
        print(weather.temperature)
    }

우리는 여태 작동확인을 위해 print를 사용해 콘솔로 출력을 하였지만,

이번에는 label 바꾸도록 해보자!

1
2
3
 func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel) {
        temperatureLabel.text = weather.temperatureString
    }

이렇게 작성해주었다.

그러면 바뀌는지 확인을 해보자

?? 안된다.. 그리고 팅겨버렸다.

팅겼다는것 뭔가 충돌이 발생했다는건데 뭐가 문제일까

분명히 temperature 값을 프린트 할때는 잘만 넘어갔는데??

console에러도 엄청 길고, 새로운 창도 뜬다.

일단 뷰컨트롤러에 뜬 저 보라색 메세지 부터 확인을 해보자.

UiLabel은 반드시 메인스레드에서만 사용하라고 나온다.

현재는 관련된 documents가 삭제가 되었나보다…

강의의 내용을 좀 번역해서 정리를 해본다면,

네트워킹같이 오래걸리는 일은 종종 background에서 실행이 된다. 그래서 보통 UI를 막지 않는다.

그렇지 않으면 유져가 어떤 버튼을 눌러 조작을 하여 앱과 상호작용을 하기 전에 과정이 끝날때 까지 기다린다.

그러면 해당 어플은 뭔가 정지된 것 처럼 보일것이다.

삭제되기전 documents의 문제가 되는 상황은 바로 이거였다. 그리고 글의 원문도 같이 적는다

Updating UI from a Completion Handler

Long-running tasks such as networking are often executed in the background, and provide a completion handler to signal completion. Attempting to read or update the UI from a completion handler may cause problems.

1
2
3
4
5
6
7
let task = URLSession.shared.dataTask(with: url){ (data, response, error) in
    if let data = data {
        self.label.text = "\(data.count) bytes downloaded."
        // Error : label updated on background thread
    }
}
task.resume()

이 문제애 대한 솔루션은 이거였다.

Solution

Dispatch the call to update the label text to the main thread

1
2
3
4
5
6
7
8
let task = URLSession.shared.dataTask(with: url){ (data, response, error) in
    if let data = data {
        DispatchQueue.main.async { // Correct
        self.label.text = "\(data.count) bytes downloaded."
        }
    }
}
task.resume()

즉, 요소를 업데이트 할 경우, 백그라운드에서 사용자 인터페이스를 업데이트 하기 위해 Main thread를 호출해야 한다는 것이다.

그렇다면 삭제된 Docs이긴 하지만 해당내용을 바탕으로 수정을 해보자!

1
2
3
4
5
func didUpdateWeather(_ weatherManager:WeatherManager, weather : WeatherModel) {
        DispatchQueue.main.async{
            self.temperatureLabel.text = weather.temperatureString
        }
    }

이제 다시 실행해보자.

아주 잘 된다.

아까 에러났을때 화면을 터치해보니, 즉 클릭을 해보니 온도가 바뀌었는데, 위의 내용을 알고나니 왜 그랬나 이해가 조금은 간다.

이 기세를 이어나가 우린 이제 기상조건을 id값으로 받고, swich-case문을 통해 어떤 symbol 이 리턴 되는지 알고있다.

이미지도 바뀌게 해보자!

self.conditionImageView.image = UIImage(systemName: weather.conditionName)

이부분을 추가해 주었다.

imageView의 이미지는 UIImage type이어야 하는데,

이때 우리가 만든 conditionName이 바로 systemName에 들어가는 항목이다!

그럼 이제 도시명을 바꾸면 되지만!

처음에 나오는 현재 장치의 위치에 따라 다르게 나오기 할것이기 때문에 잠시 보류하도록 하겠다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.