You asked:
I wonder how to eliminate of using self
inside the DispatchQueue
.
In SE-0269, adopted in Swift 5.3, they introduced a pattern where if you include self
in the capture list, it eliminates the need for all the self
references in the closure:
func loadAllClasses() {
DispatchQueue.global(qos: .background).async { [self] in // note [self] capture list
classVM.fetchAllClasses(id: id) { (classes, error) in
DispatchQueue.main.async {
if error != nil {
showAlert(message: "try again", title: "Error")
}
if let classes = classes {
classesList = classes
classesCollectionView.reloadData()
}
}
}
}
}
As an aside, fetchAllClasses
would appear to already be asynchronous, so the dispatch to the global queue is unnecessary:
func loadAllClasses() {
classVM.fetchAllClasses(id: id) { [self] (classes, error) in // note [self] capture list
DispatchQueue.main.async {
if error != nil {
showAlert(message: "try again", title: "Error")
}
if let classes = classes {
classesList = classes
classesCollectionView.reloadData()
}
}
}
}
As a good practice, we are supposed to use self
only in the init()
?
No, you use self
wherever it eliminates ambiguity or where you need to make potential strong reference cycles clear. Do not shy away from using self
references.
But the intuition, to eliminate unnecessary self
references is good, because where not needed, it ends up being syntactic noise. And the whole point of requiring self
references inside a closure is undermined if your broader codebase sprinkles self
references all over the place.
As an aside, above I illustrate that where you are willing to capture self
, you can use [self] in
syntax to make one’s code more succinct, eliminate lots of unnecessary self
references inside the closure.
That having been said, we would generally want to use [weak self]
reference in this case. Sure, as vadian suggested, there might not be any strong reference cycle risk. But it is a question of the desired lifecycle of the object in question. For example, let us assume for a second that fetchAllClasses
could conceivably be very slow. Let us further assume that it is possible that the user might want to dismiss the view controller in question.
In that scenario, do you really want to keep the view controller alive for the sake of completing fetchAllClasses
, whose sole purpose is to update a collection view that has been dismissed?!? Probably not.
Many of us would use [weak self]
when calling a potentially slow, asynchronous process:
func loadAllClasses() {
classVM.fetchAllClasses(id: id) { [weak self] (classes, error) in // note `[weak self]` so we don't keep strong reference to VC longer than we need
DispatchQueue.main.async {
guard let self = self else { return } // if view controller has been dismissed, no further action is needed
guard error == nil, let classes = classes else { // handle error and unwrap classes in one step
self.showAlert(message: "try again", title: "Error")
return
}
self.classesList = classes // otherwise, proceed as normal
self.classesCollectionView.reloadData()
}
}
}
Yes, this has re-introduced the self
references that we removed above, but this is a good pattern for asynchronous requests, in general: We never let some network request designed to update a view controller prevent that view controller from being deallocated when the user dismisses it.
A further refinement of this above pattern would be to design fetchAllClasses
to be cancelable, and then in the view controller’s deinit
, cancel any pending network requests, if any. That is beyond the scope of this question, but the idea is that, not only should we not retain the view controller longer than necessary, but we should cancel pending requests, too. This deinit
cancelation pattern, though, only works if you used a weak
reference in the closure.