IOS

[IOS] Notification을 이용하여 여러 클래스에서 데이터 받기

영화 정보 앱을 만드는 부스트코스 프로젝트에서 사용자가 선택한 정렬 기준에 맞게

영화 목록을 갱신하여 보여주는 기능을 구현하려고 했다.

 

사용자가 선택한 기준으로 정렬된 데이터가 담겨있는 URL에서 데이터를 가져오면 끝나는 일이었지만,

데이터를 요청한 클래스가 아닌 다른 클래스에도 가져온 데이터를 전달하여 동일한 정렬된 데이터로 갱신해야 했다.

 

이것을 해결한 방법을 간단하게 정리하려고 한다.


데이터를 요청하는 전역 메소드인 requestData 메소드를 살펴보자.

func requestData<T: Decodable>(_ sub: String, _ para: String, _ type: T.Type, _ noti: Notification.Name) {
    
    let session = URLSession(configuration: .default)
    
    guard let url = URL(string: "\(baseURL + sub)?\(para)") else { return }
   
    // url로부터 네트워킹을 통해 Data객체를 가져오는 작업 생성
    let dataTask = session.dataTask(with: url){ (data: Data?, resopnse: URLResponse?, error:Error?) in
        
        // 네트워킹에 실패했을 때 오류 처리
        if let error = error {
            print(error.localizedDescription)
            return
        }
        
        guard let data = data else { return }
        
        do {
            
            // 가져온 Data(JSON타입)객체를 디코딩
            let data = try JSONDecoder().decode(type.self, from: data)
            
            // NotificationCenter로 정보를 담은 Notification 발송
            NotificationCenter.default.post(name: noti, object: nil, userInfo: ["data":data])
            
            OperationQueue.main.addOperation {
                indicatorView.stopAnimating()
            }
        } catch(let err) {
            print(err.localizedDescription)
        }
    }
    
    indicatorView.startAnimating()

    dataTask.resume()
    
}

여러 클래스에서 데이터를 요청하는 일이 반복적으로 생기다보니 클래스마다 메소드를 구현하지 않고

제네릭을 이용하여 전역 메소드로 만들었다.

 

특정 클래스에서 데이터를 요청할 URL의 정보와 받을 데이터의 타입, 그리고 Notification.Name 타입의 객체를 인자로 넘겨준다.

이때 데이터의 타입은 Codable 프로토콜을 준수해야 한다.

 

그렇게 호출된 requestData 메소드는 인자로 받은 URL 정보를 가지고 특정 URL에

네트워크 통신을 통해 JSON 데이터를 가져오고 데이터를 클래스에서 요청한 타입의 객체로 변환한다.

 

변환까지 오류 없이 완료됐다면 이 데이터에 인자로 받은 noti(Notification.Name)라는

이름을 붙여서 NotificationCenter로 보낸다. 

 

이때 NotificationCenter는 받은 데이터의 이름을 보고 어떤 클래스에 전달할지 결정한다.

클래스에서 데이터를 받기 위해서는 특정한 이름을 가진 Notification을 받겠다고 NotificationCenter에 알려줘야 한다.

그러면 NotificationCenter는 그 이름을 가진 데이터를 받았을 때, 설정된 클래스에게 데이터를 보내게 된다.

override func viewDidLoad() {
        super.viewDidLoad()
        
        // NotificationCenter에 특정 알림에 대해 알림을 받는 뷰 컨트롤러 등록
        NotificationCenter.default.addObserver(self, selector: #selector(self.receivedData), name: DidReceiveMovieListNotification, object: nil)

위처럼 특정 이름의 Notification에 대한 옵저버 객체를 설정할 수 있다.

DidReceiveMovieListNotification이라는 이름을 갖는 Notification은 설정된 객체로 전달되고,

그에 대한 액션으로 receivedData를 호출한다.

 

특정 이름의 Notification에 대해 여러 클래스를 옵저버로 등록해 놓으면,

동일한 데이터를 등록된 모든 클래스에서 전달받을 수 있다.

 

@objc func receivedData(_ noti: Notification) {
        
        // NotificationCenter로부터 Notification을 받았을 때 실행되는 메소드
        // Notification을 보낸 스레드와 동일한 백그라운드 스레드에서 실행된다.
        
        guard let data = noti.userInfo?["data"] as? MovieList else {
            return
        }
        
        self.movies = data.movies
        
        // UI관련 작업은 메인 스레드로 보냄.
        OperationQueue.main.addOperation {
            self.tableView?.reloadData()
            self.navigationItem.title = naviTitle[data.order]
        }
    }

전달받은 Notification 객체의 데이터는 해당 Notification을 보낼 때 설정한 userInfo 프로퍼티의 Key로 접근할 수 있다.

이때 전달받은 Notification의 처리(즉, receivedData 메소드)는 메인 스레드가 아닌 다른 스레드에서 수행되기 때문에,

UI와 관련된 작업은 메인 스레드로 보내어 수행해야 한다.

 

 

위의 모든 과정을 요약하면 다음과 같다

  1. 1번, 2번 클래스를 옵저버 객체로 등록
  2. 1번 클래스에서 데이터 요청 (requestData 메소드 호출)
  3. NotificationCenter에 해당 데이터 전달
  4. NotificationCenter에서 1번, 2번 클래스에 Notification 전달
  5. 1번, 2번 클래스에서 받은 데이터 처리 (receivedData 메소드 호출)