swiftuikit

How to synchronize button image changes in different controllers?


I have a view controller with a mute/unmute button. I also have a second view controller with the same mute/unmute button. If I click the button in the second controller view, the button image changes. And also at this moment I need to change the image in the first view controller. But how to do this? I need to call some action in the first view controller from the second view controller, like this:

class: 1VC {
    func changeButton() {
    }
}

class: 2VC {
    let controller = FirstVC()
    controller.changeButton()
}

Or is there another way? I pass to second view controller like this:

self.present(2VC(), animated: false, completion: nil)

real code:

SceneDelegate:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = (scene as? UIWindowScene) {
            
            let viewController = FirstController()
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = viewController
            self.window = window
            window.makeKeyAndVisible()
        }
    }

}

FirstController:

class FirstController: UIViewController, UICollectionViewDelegate {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        let detailController = SecondController()
        detailController.index = indexPath.row + 1
        detailController.modalPresentationStyle = .overFullScreen
        self.present(detailController, animated: false)

    }

}

SecondController:

class SecondController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Solution

  • You need to think about your app in terms of state and view; there is some state in your app (muted) and there are controls that change/show this state. SwiftUI makes it much easier to bind state to views, but of course you can do it in UIKit too.

    Your proposed pseudo-code, apart from not working (as Duncan C pointed out) also tightly couples the two view controllers. This adds to the complexity of your app and will make it harder to maintain and more likely to have bugs or undesired behaviour.

    Separating state and view makes it easier to decouple view controllers.

    First, create some object (typically known as a model) to hold the state.

    class PlaybackModel {
    
        var muted: Boolean = false
    }
    

    Then you need to share a single instance of this of this class between your two view controllers.

    Add a property var playbackModel: PlaybackModel? to your view controllers and make sure it is set

    For example:

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
        var window: UIWindow?
        var playbackModel = PlaybackModel()
    
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
            if let windowScene = (scene as? UIWindowScene) {
                
                let viewController = FirstController()
                viewController.playbackModel = self.playbackModel
                let window = UIWindow(windowScene: windowScene)
                window.rootViewController = viewController
                self.window = window
                window.makeKeyAndVisible()
            }
        }
    
    }
    
    class FirstController: UIViewController, UICollectionViewDelegate {
    
        var playbackModel: PlaybackModel?
    
        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    
            let detailController = SecondController()
            detailController.index = indexPath.row + 1
            detailController.modalPresentationStyle = .overFullScreen
            detailController.playbackModel = self.playbackModel
            self.present(detailController, animated: false)
    
        }
    
        func buttonTapped() {
            self.playbackModel?.muted.toggle()
        }
    }
    

    Then you need to decide how the model can let other objects know that the property has changed. Again, SwiftUI makes this very easy via @Observable. In UIKit you could use NSNotificationCenter or Combine.

    Here is an approach using Combine:

    First, add a CurrentValueSubject to your model, and send a value when muted changes:

    import Combine 
    
    class PlaybackModel {
        var muted: Boolean = false {
            didSet {
                self.mutedSubject.send(muted)
            }
        }
        var mutedSubject = CurrentValueSubject(false)
    }
    

    Note that my simple module sets the initial value of muted to false - You might need something more sophisticated that gets the initial value from preferences etc.

    Then in your view controllers you can subscribe to this subject:

    import Combine
    
    class SomeViewController: UIViewController {
    
        var playbackModel: PlaybackModel?
        var storage: Set<AnyCancellable>
    
        viewWillAppear(_ animated: Boolean) {
            self.playbackModel?.mutedSubject
                .receive(on: RunLoop.main)
                .sink { muted in
                    // Do something with the new value of muted
                }
                .store(in: &storage)
            super.viewWillAppear(animated)
        }
       
        viewWillDisappear(_ animated: Boolean) {
            self.storage.removeAll()
            super.viewWillDisappear(animated)
        }