Ch.13 Gold: Support Multiple Windows

# 멀티 윈도우 지원하고 싱크 맞추기

  • LootLogger 가 여러 윈도우에서 열릴 수 있게 하고, 한 곳에서의 편집 사항이 다른 곳에서도 적용되게 하는 게 과제



# 프로젝트 설정에서 멀티윈도우 지원하기

  • Project settings - Deployment Info - Supports multiple windows

# 공유 ItemStore

  • 솔직히 맞게 한 건지는 모르겠는데...암튼 모든 scene 에서 item store 를 공유하게 해야 된대서 SceneDelegate 에서 itemStore 를 static 하게 하나 선언하고, 새로운 유저 인터페이스가 생성되거나 다시 로딩될 때 호출되는 scene(_:willConnectTo:options:) 메서드에서 이 static itemStore 를 주입하는 방식...!
    • 공식 문서를 보면 데이터 주입 등을 해당 메서드에서 하라고 하고 있다..
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    static let itemStore = ItemStore()  // static 으로 선언


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
     
        guard let _ = (scene as? UIWindowScene) else { return }
        
        // Access the ItemsViewController and set its item store
        let navController = window!.rootViewController as! UINavigationController
        let itemsController = navController.topViewController as! ItemsViewController
        itemsController.itemStore = SceneDelegate.itemStore
    }
}

# notification

  • 위에처럼 하니까 itemStore 를 모든 scene 에서 공유하기 때문에 다른 창을 켜던가 아님 다른 뷰에 넘어갔다 오면 모든 창에서 업데이트가 되어 있는데 화면 전환 없이는 한 창에서의 변화가 다른 창에서 즉각적으로 적용되지 않았다.

  • 그래서 변화가 있을 때마다 앱에서 공유 중인 NotificationCenter.default 를 통해 알리고, 알림을 받을 수 있도록 해야겠다고 생각했는데 여기서부터 멍청함과 가보자고 마인드의 환장의 콜라보...

# 구독

  • 일단 NotificaitonCenter.default 가 앱 단위로 하나씩 존재하기 때문에 하나의 앱에서 여러 window(scene) 을 허용한 현재의 경우 얘를 쓰면 되는데 멍청한 착각으로 서로 다른 앱 간의 알림을 가능케 하는 DistributedNotificationCenter 를 써야한다고 생각했다...절대 선언이 안돼서 이것 때문에 시간 순삭 굿...

  • 암튼 뭔가 한 윈도우에서 아이템 스토어에 변화가 생기면 다른 윈도우에서도 모두 뷰를 업데이트 해줘야 하므로 itemStoreDidUpdate 라는 Notification 을 새로 만들어주고, ItemsViewController 와 DetailViewController 에서 해당 notification 에 구독하도록 했다.

    • 뭔가 작업마다 다른 notification 을 만들어줄까 생각도 했는데(특히 Detail View 에서 아이템을 편집하는 경우), ItemStore 변화라는 키워드로 묶을 수 있기도 하고, 이 시점에는 이미 너무 많은 고통을 받아서 수정하기 싫어서 걍 넘어갔다...
  • ItemsViewController 에서는 알림이 오면 tableView.reloadData() 를 통해 화면을 업데이트했고 DetailViewController 에서는 모든 라벨을 다시 업데이트 하도록 했다.

    • 특정 행이 아닌 전체를 다 reload 한 이유는 이전에도 최애템 과제에서 비슷한 내용을 얘기한 적 있는데, 추가/삭제 작업의 경우 업데이트가 필요한 창에서는 아직 추가/삭제할 행이 생성되지 않아 프로그램이 터지기 때문!
extension Notification.Name {
    static let itemStoreDidUpdate = Notification.Name("itemStoreDidUpdate")
}

class ItemsViewController: UITableViewController {

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        navigationItem.leftBarButtonItem = editButtonItem
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(reloadData(for:)),
                                               name: .itemStoreDidUpdate,
                                               object: nil)
    }
    
    @objc private func reloadData(for notification: Notification) {
        if let notifyingController = notification.object as? UIViewController,
            self != notifyingController {
            tableView.reloadData()
        }
    }
}

class DetailViewController: UIViewController, UITextFieldDelegate {

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(updateLabels),
                                               name: .itemStoreDidUpdate,
                                               object: nil)
    }
    
    @objc private func updateLabels(for notification: Notification) {
        if let notifyingController = notification.object as? UIViewController,
           self != notifyingController {
            updateLablesToMatchItem()
        }
    }
    
    private func updateLablesToMatchItem() {
        nameField.text = item.name
        serialNumberField.text = item.serialNumber
        valueField.text = numberFormatter.string(from: NSNumber(value: item.valueInDollars))
        dateLabel.text = dateFormatter.string(from: item.dateCreated)
    }
}

# 알림

  • 변화가 있었음을 알릴 부분은 총 4군데로, itemsView 에서 아이템을 추가/삭제/이동하는 경우와 detailView 에서 내용을 편집하는 경우였다. 각 경우에 모든 작업이 끝나고 알림을 보내도록 했다. 정확한 이유는 모르겠는데 작업 중간에 알림을 보내면 어쨌든 현재 편집 중이던 윈도우도 해당 알림을 받아서 이 과정에서 꼬이는 것 같았다...나중에 다른 이유로 알림을 받았을 때 내가 보낸 알림인 경우에는 (이미 뷰가 업데이트 되었으므로) 뷰를 업데이트 하지 않도록 하는 처리를 했는데, 이렇게 했더니 작업 시작, 중간, 끝 어느 시점에서 알림을 보내든 상관이 없었다.
class ItemsViewController: UITableViewController {
    
    var itemStore: ItemStore!
    
    @IBAction func addNewItem(_ sender: UIBarButtonItem) {
        let newItem = itemStore.createItem()
        
        if let index = itemStore.allItems.firstIndex(of: newItem) {
            let indexPath = IndexPath(row: index, section: 0)
            
            tableView.insertRows(at: [indexPath], with: .automatic)
            // 알림
            NotificationCenter.default.post(name: .itemStoreDidUpdate,
                                            object: self)
        }
    }
    
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

        if editingStyle == .delete {
            let item = itemStore.allItems[indexPath.row]
            itemStore.removeItem(item)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            // 알림
            NotificationCenter.default.post(name: .itemStoreDidUpdate,
                                            object: self)
        }
    }
    
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

        itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
        
        // 알림
        NotificationCenter.default.post(name: .itemStoreDidUpdate,
                                        object: self)
    }
}

class DetailViewController: UIViewController, UITextFieldDelegate {
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        view.endEditing(true)
        
        item.name = nameField.text ?? ""
        item.serialNumber = serialNumberField.text ?? ""
        
        if let valueText = valueField.text,
           let value = numberFormatter.number(from: valueText) {
            item.valueInDollars = value.intValue
        } else {
            item.valueInDollars = 0
        }
        
        // 알림
        NotificationCenter.default.post(name: .itemStoreDidUpdate,
                                        object: self)
    }
}

  • 그리고 현재 알림을 보내는 view controller 를 notification object 로 설정했는데, 앞서 말했듯이 실제 편집이 최초로 발생해서 굳이 뷰를 또 업데이트할 필요 없는 윈도우도 알림을 받기 때문에 reloadData() 메서드가 실행되면서 애니메이션이 씹혔다. 그래서 이를 방지하기 위해 알림을 보낸 view controller 가 이를 받은 view controller 가 다른 경우에만 뷰를 업데이트하도록 했다.
    • 여기서 또 구독하면 뭘 리턴받는지 몰라서 고통받았다...Notification 객체를 받는다....
    @objc private func reloadData(for notification: Notification) {
        if let notifyingController = notification.object as? UIViewController,
            self != notifyingController {
            tableView.reloadData()
        }
    }

나를 살린 블로그

좋은 웹페이지 즐겨찾기