Why is `didSet` property observer called multiple times in this example with Swift 4.1, but only once with Swift 4.0?

Hi,

I’ve noticed this strange behavior in Swift 4.1 (different than it was in Swift 4.0), but I don’t understand why is that happening and I would appreciate if someone could explain it to me, because I’m not sure if that’s a bug or a feature? :)

Playground example code:

import PlaygroundSupport
import UIKit

PlaygroundPage.current.needsIndefiniteExecution = true

public protocol Item {
    var id: String { get }
}
public protocol Section {
    var items: [Item] { get set }
}
public protocol ViewModel {
    var sections: [Section] { get set }
}

public struct BasicItem: Item {
    public let id: String
}
public struct BasicSection: Section {
    public var items: [Item]
}
public struct BasicViewModel: ViewModel {
    public var sections: [Section]
    init(sections: [Section]) {
        self.sections = sections
    }
    
    init() {
        sections = [BasicSection(items: [
            BasicItem(id: "Item1"),
            BasicItem(id: "Item2"),
            BasicItem(id: "Item3")
            ])
        ]
    }
}

open class TableViewController: UITableViewController {
    open var viewModel: ViewModel! {
        didSet {
            print("didSet")
        }
    }
    open override func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.sections.count
    }
    open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.sections[section].items.count
    }
    open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}
class CustomTVC: TableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad")
        viewModel = BasicViewModel()
    }
}

let tvc = CustomTVC(style: .plain)
PlaygroundPage.current.liveView = tvc

My observations:

As you can see here, there are a few simple protocols and structs conforming to those, which are later used in a UITableViewController subclass.

If I run this code with Swift 4.0, print("didSet") will be called only once, while in Swift 4.1 it will be called multiple times, although I obviously set this property only once in viewDidLoad.

With a breakpoint in this line I could see that didSet is called once from viewDidLoad (expected) and several times from tableView(_:numberOfRowsInSection:) which was internally calling materializeForSet and viewModel.setter (unexpected).

Part which I don’t understand is why accessing a read only property on the viewModel actually ends up mutating it and calling the setter under the hood?

This problem does not happen (didSet is called only once) if any of these is true:

  • ViewModel protocol is defined as ViewModel: class { ... } so no structs can be used for it
  • ViewModel and Section protocols define their properties like { get } instead of { get set }
  • using just a simple structs with the same properties (without protocols)

I’ve tried to find some connection in the release notes for Swift 4.1 but unfortunately, I couldn’t relate this problem to anything I saw there.

Have you filed a bug about this yet?

Not surprised this happened, there is something going wrong with value type semantics right now. FYI

SR-7257
SR-7220

Time to update the bug list :)

Thanks for the feedback @Joe_Groff and @anthonylatsis.

I’ve filled a bug for this: SR-7335

2 Likes
Terms of Service

Privacy Policy

Cookie Policy