简体   繁体   中英

Using property observers to modify UI components in Swift

I have created a subclass of a UICollectionViewCell that shows some information. I have one property in with type Weather. When an instance of that is set I want to update the cell. Is the approach below bad? I am thinking of that I may trigger the view to be created to early if I access the UI components before it is loaded. Or is that non sense and only applies to UIViewController (with regard to using view property to early)?

If this is bad, what would be the correct way?

var weather: Weather? {
        didSet {
            if let weather = weather {                            
                dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)

                // ... more code like this
            }
        }
    }

You may want an else clause, though, clearing the text field if weather was nil . Likewise, if you might update this from a background thread, you might want to dispatch that UI update back to the main thread.

Be aware that this observer is not called when you set weather in the cell's init (nor would be the @IBOutlet be configure at that point, anyway). So make sure that you're not relying upon that.

Also, if Weather is mutable, recognize that if you change the fromDate of the existing Weather object, this won't capture that. (If Weather was mutable, you'd really want to capture its changing properties via KVO, a delegate-protocol pattern, or what have you.) But if you make Weather immutable, you should be fine.


So, technically, that's the answer to the question, but this raises a few design considerations:

  1. One generally should strive to have different types loosely coupled, namely that one type should not be too reliant on the internal behavior of another. But here we have an observer within the cell which is dependent upon the mutability of Weather .

  2. This use of a stored property to store a model object within view is inadvisable. Cells are reused as they scroll offscreen, but you probably want a separate model that captures the relevant model objects, the controller then handles the providing of the appropriate model object to the view object (the cell) as needed.

Bottom line, it's not advisable to use a stored property for "model" information inside a "view".

You can tackle both of these considerations by writing code which makes it clear that you're only using this weather parameter solely for the purpose of updating UI controls, but not for the purposes of storing anything. So rather that a stored property, I would just use a method:

func updateWithWeather(weather: Weather?) {
    if let weather = weather {
        dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)

        // ... more code like this
    } else {
        dayLabel.text = nil

        // ... more code like this
    }
}

And this would probably only be called from within collectionView:cellForItemAtIndexPath: .

But, this makes it clear that you're just updating controls based upon the weather parameter, but not trying to do anything beyond that. And, coincidentally, the mutability of the weather object is now irrelevant, as it should be. And if the model changes, call reloadItemsAtIndexPaths: , which will trigger your collectionView:cellForItemAtIndexPath: to be called.

There are times where a stored property with didSet observer is a useful pattern. But this should be done only when the property is truly a property of view. For example, consider a custom view that draws some shape. You might have stored properties that specify, for example, the width and the color of the stroke to be used when drawing the path. Then, having stored properties for lineWidth and strokeColor might make sense, and then you might have a didSet that calls setNeedsDisplay() (which triggers the redrawing of the view).

So, the pattern you suggest does have practical applications, it's just that it should be limited to those situations where the property is truly a property of the view object.

I would use a property observer if I planned up updating the value during the users session. If this is a value that only gets updated when the user first loads, I would just simply call a method when my view is initially loaded.

If you use a property observer, you can give it an initial value when you define it so the data is there when the user needs it. Also, if you're updating the user interface, make sure you do it on the main queue.

 var weather: Weather = data {
        didSet {      
            dispatch_async(dispatch_get_main_queue(),{
                if let weather = weather {                            
                    dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)

                // ... more code like this
                }
            })
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM