DanielZanchi
DanielZanchi

Reputation: 2768

Change @IBOutlet from a subview

I'm trying to enable or disable an @IBOutlet UIButton Item of a toolbar from a UIView.
The button should get disabled when the array that I'm using in EraseView.Swift is empty.
I tried creating an instance of the view controller but it gives me the error (found nil while unwrapping):
in EraseView:

class EraseView: UIView {
    ...
    let editViewController = EditImageViewController()
    //array has item
    editViewController.undoEraseButton.enabled = true //here I get the error
    ...
}

I tried to put a global Bool that changed the value using it in EditImageViewController but it doesn't work:

var enableUndoButton =  false

class EditImageViewController: UIViewController {

   @IBOutlet weak var undoEraseButton: UIBarButtonItem!

    viewDidLoad() {
        undoEraseButton.enabled = enableUndoButton
    }
}

class EraseView: UIView {
    ...        
    //array has item
    enableUndoButton = true //here I get the error
    ...
}

I know it's simple but I can't let it work.
Here's the situation: enter image description here

Upvotes: 1

Views: 169

Answers (1)

Rob
Rob

Reputation: 437882

The root of the problem is the line that says:

let editViewController = EditImageViewController()

The EditImageViewController() says "ignore what the storyboard has already instantiated for me, but rather instantiate another view controller with no outlets hooked up and use that." Clearly, that's not what you want.

You need to provide some way for the EraseView to inform the existing view controller whether there was some change to its "is empty" state. And, ideally, you want to do this in a way that keeps these two classes loosely coupled. The EraseView should only be informing the view controller of the change of the "is empty" state, and the view controller should initiate the updating of the other subviews (i.e. the button). A view really shouldn't be updating another view's outlets.

There are two ways you might do that:

  1. Closure:

    You can give the EraseView a optional closure that it will call when it toggles from "empty" and "not empty":

    var emptyStateChanged: ((Bool) -> ())?
    

    Then it can call this when the state changes. E.g., when you delete the last item in the view, the EraseView can call that closure:

    emptyStateChanged?(true)
    

    Finally, for that to actually do anything, the view controller should supply the actual closure to enable and disable the button upon the state change:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        eraseView.emptyStateChanged = { [unowned self] isEmpty in 
            self.undoEraseButton.enabled = !isEmpty
        }
    }
    

    Note, I used unowned to avoid strong reference cycle.

  2. Delegate-protocol pattern:

    So you might define a protocol to do that:

    protocol EraseViewDelegate : class {
        func eraseViewIsEmpty(empty: Bool)
    }
    

    Then give the EraseView a delegate property:

    weak var delegate: EraseViewDelegate?
    

    Note, that's weak to avoid strong reference cycles. (And that's also why I defined the protocol to be a class protocol, so that I could make it weak here.)

    The EraseView would then call this delegate when the the view's "is empty" status changes. For example, when it becomes empty, it would inform its delegate accordingly:

    delegate?.eraseViewIsEmpty(true)
    

    Then, again, for this all to work, the view controller should (a) declare that is conforms to the protocol; (b) specify itself as the delegate of the EraseView; and (c) implement the eraseViewIsEmpty method, e.g.:

    class EditImageViewController: UIViewController, EraseViewDelegate {
    
        @IBOutlet weak var undoEraseButton: UIBarButtonItem!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            eraseView.delegate = self
        }
    
        func eraseViewIsEmpty(empty: Bool) {
            undoEraseButton.enabled = !empty
        }
    }
    

Both of these patterns keep the two classes loosely coupled, but allow the EraseView to inform its view controller of some event. It also eliminates the need for any global.

There are other approaches that could solve this problem, too, (e.g. notifications, KVN, etc.) but hopefully this illustrates the basic idea. Views should inform their view controller of any key events, and the view controller should take care of the updating of the other views.

Upvotes: 1

Related Questions