I'm adding a bunch of annotations to a map and as the user moves and pans around to different countries I remove the annotations and add in some more. The problem I'm facing is the new annotations don't show until I've interacted with the map, either tap, pinch, pan or zoom.
I've tried placing the map.addAnnotations() into a DispatchQueue but that didn't work and I'm also offsetting the built method loadNewCountry(country: String) into a dispatchGroup. None of these are working!
Note: I've got several thousand annotations of varying types so loading them all in memory won't work for older devices :)
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
checkIfLoadNewCountry()
}
func checkIfLoadNewCountry() {
let visible = map.centerCoordinate
geocode(latitude: visible.latitude, longitude: visible.longitude) { placemark, error in
if let error = error {
print("\(error)")
return
} else if let placemark = placemark?.first {
if let isoCountry = placemark.isoCountryCode?.lowercased() {
self.loadNewCountry(with: isoCountry)
}
}
}
}
func loadNewCountry(with country: String) {
let annotationsArray = [
self.viewModel1.array,
self.viewModel2.array,
self.viewModel3.array
] as [[MKAnnotation]]
let annotations = map.annotations
autoreleasepool {
annotations.forEach {
if !($0 is CustomAnnotationOne), !($0 is CustomAnnotationTwo) {
self.map.removeAnnotation($0)
}
}
}
let group = DispatchGroup()
let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)
group.enter()
queue.async {
self.viewModel1.load(country: country)
group.leave()
}
group.enter()
queue.async {
self.viewModel2.load(country: country)
group.leave()
}
group.wait()
DispatchQueue.main.async {
for annoArray in annotationsArray {
self.map.addAnnotations(annoArray)
}
}
}
The key issue is that the code is initializing the [[MKAnnotation]] with the current view model results, then starting the load of the view models models for a new country, and then adding the old view model annotations to the map view.
Instead, grab the [[MKAnnotation]] after the reloading is done:
func loadNewCountry(with country: String) {
let annotations = map.annotations
annotations
.filter { !($0 is CustomAnnotationOne || $0 is CustomAnnotationTwo || $0 is MKUserLocation) }
.forEach { map.removeAnnotation($0) }
let group = DispatchGroup()
let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)
queue.async(group: group) {
self.viewModel1.load(country: country)
}
queue.async(group: group) {
self.viewModel2.load(country: country)
}
group.notify(queue: .main) {
let annotationsArrays: [[MKAnnotation]] = [
self.viewModel1.array,
self.viewModel2.array,
self.viewModel3.array
]
for annotations in annotationsArrays {
self.map.addAnnotations(annotations)
}
}
}
Unrelated to the problem at hand, I have also:
simplified the DispatchGroup group syntax;
eliminated the wait as you should never block the main thread;
eliminated the unnecessary autoreleasepool;
added MKUserLocation to the types of annotations to exclude (even if you're not showing the user location right now, you might at some future date) ... you never want to manually remove MKUserLocation or else you can get weird UX;
renamed annotationArrays to make it clear that you’re dealing with an array of arrays.
As an aside, the above raises thread-safety concerns. You appear to be updating your view models on a background queue. If you are interacting with these view models elsewhere, make sure to synchronize your access. And, besides, the motivating idea of “view models” (as opposed to a “presenter” pattern, for example) is that you hook them up so that they inform the view of changes themselves.
So, you might consider:
Give the view models asynchronous startLoad methods;
Give the view models some mechanism to inform the view (on the main queue) of changes when a load is done (whether observers, delegate protocol, closures, etc.).
Make sure the view models synchronize interaction with their properties (e.g., array).
E.g., let us imagine that the view model is updating the view via closures:
typealias AnnotationBlock = ([MKAnnotation]) -> Void
protocol CountryLoader {
var didAdd: AnnotationBlock? { get set }
var didRemove: AnnotationBlock? { get set }
}
class ViewModel1: CountryLoader {
var array: [CustomAnnotationX] = []
var didAdd: AnnotationBlock?
var didRemove: AnnotationBlock?
func startLoad(country: String, completion: (() -> Void)? = nil) {
DispatchQueue.global().async {
let newArray: [CustomAnnotationX] = ... // computationally expensive load process here (on background queue)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.didRemove?(self.array) // tell view what was removed
self.array = newArray // update model on main queue
self.didAdd?(newArray) // tell view what was added
completion?() // tell caller that we're done
}
}
}
}
That is a thread-safe implementation that abstracts the view and view controller from any of the complicated asynchronous processes. Then the view controller needs to configure the view model:
class ViewController: UIViewController {
#IBOutlet weak var map: MKMapView!
let viewModel1 = ViewModel1()
let viewModel2 = ViewModel2()
let viewModel3 = ViewModel3()
override func viewDidLoad() {
super.viewDidLoad()
configureViewModels()
}
func configureViewModels() {
viewModel1.didRemove = { [weak self] annotations in
self?.map?.removeAnnotations(annotations)
}
viewModel1.didAdd = { [weak self] annotations in
self?.map?.addAnnotations(annotations)
}
...
}
}
Then, the “reload for country” becomes:
func loadNewCountry(with country: String) {
viewModel1.startLoad(country: country)
viewModel2.startLoad(country: country)
viewModel3.startLoad(country: country)
}
Or
func loadNewCountry(with country: String) {
showLoadingIndicator()
let group = DispatchGroup()
group.enter()
viewModel1.startLoad(country: country) {
group.leave()
}
group.enter()
viewModel2.startLoad(country: country) {
group.leave()
}
group.enter()
viewModel3.startLoad(country: country) {
group.leave()
}
group.notify(queue: .main) { [weak self] in
self?.hideLoadingIndicator()
}
}
Now that’s just one pattern. The implementation details could vary wildly, based upon how you have implemented your view model. But the idea is that you should:
make sure the view model is thread-safe;
abstract the complicated threading logic out of the view and keep it in the view model; and
have some process whereby the view model informs the view of the relevant changes.
Related
I got a problem. I got a collectionview which is binded to a winPinataActions PublishSubject<[Object]>(). Initially, when loading collectionview everything is fine, it displays as it has to the objects, however when the pull to refresh action changes the publishSubject data the UI is not updated, it still gets the old content of the PublishSubject.
Here is how I bind the collectionView :
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
}
func configureCollectionView() {
/..../
viewModel.winPinataActions
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}.disposed(by: bag)
viewModel.getPinataActions()
}
#objc func handleRefreshControl() {
viewModel.getPinataActions()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.collectionView.refreshControl?.endRefreshing()
}
}
}
This is my viewModel class:
class WinPinatasViewModel {
let winPinataActions = PublishSubject<[WinPinatasAction]>()
func getPinataActions() {
guard let ssoId = UserDefaultsStore.ssoId() else {
return
}
NetworkEngine.shared.gamificationNetwork.getUserWinPinataActions(subject: winPinataActions, ssoID: ssoId)
}
}
And my NetworkEngine getuserPinataActions method:
func getUserWinPinataActions(subject winPinatasActions: PublishSubject<[WinPinatasAction]>, ssoID: String) {
//...//
let actions = try decoder.decode([WinPinatasAction].self, from: jsonData)
winPinatasActions.onNext(actions)
winPinatasActions.onCompleted()
//...//
}
When the pull to refresh action is done, the handleRefreshControl() method is called. Also While debugging I could see that after pullToRefresh action the new data is received inside my NetworkEngine method and both .onNext()and onCompleted() are called. But when I scroll through the collectionView the data the cell items are from the old array, not the one new one. Could you help me please? What am I doing wrong?
The problem here is that you are sending a completed event to the Subject but then expecting it to be able to send other events after that. The Observable contract specifies that once an Observable (or Subject in this case) sends a completed event, it will never send any more events under any circumstances.
Instead of passing a Subject into getUserWinPinataActions you should be returning an Observable from the function.
This is closer to what you should have:
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
let viewModel = WinPinatasViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.refreshControl!.rx.controlEvent(.valueChanged)
.startWith(())
.flatMapLatest { [viewModel] in
viewModel.getPinataActions()
}
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}
.disposed(by: bag)
}
}
class WinPinatasViewModel {
func getPinataActions() -> Observable<[WinPinatasAction]> {
guard let ssoId = UserDefaultsStore.ssoId() else {
return .empty()
}
return GamificationNetwork.shared.getUserWinPinataActions(ssoID: ssoId)
}
}
class GamificationNetwork {
static let shared = GamificationNetwork()
func getUserWinPinataActions(ssoID: String) -> Observable<[WinPinatasAction]> {
Observable.create { observer in
let jsonData = Data() // get jsonData somehow
let actions = try! decoder.decode([WinPinatasAction].self, from: jsonData)
observer.onNext(actions)
observer.onCompleted()
return Disposables.create { /* cancelation code, if any */ }
}
}
}
Remember:
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use... In production code you may find that you rarely use the IObserver interface and subject types... The IObservable interface is the dominant type that you will be exposed to for representing a sequence of data in motion, and therefore will comprise the core concern for most of your work with Rx...
-- Intro to Rx
If you find yourself reaching for a Subject to solve a problem, you are probably doing something wrong.
Also, this article might help: Integrating RxSwift Into Your Brain and Code Base
I have the following, but it completes using a closure. The question is whether the code will cause a memory cycle without the use of [weak self].
class ViewModel {
init() {}
var completion: ((Users) -> Void)?
func downloadFiles() {
guard let url = URL(string: "https://reqres.in/api/users?page=2") else {return}
let task = URLSession.shared.dataTask(
with: url,
completionHandler: { data, response, _ in // add [weak self]
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else { return }
if let decoded = try? JSONDecoder().decode(Users.self, from: data) {
self.completion?(decoded)
}
})
task.resume()
}
}
using the following viewcontroller
class ViewController: UIViewController {
private let viewModel: ViewModel
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.completion = {
print($0)
}
viewModel.downloadFiles()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and model
struct Users: Decodable {
let page: Int
let per_page: Int
let total: Int
let total_pages: Int
let data: [UserData]
}
struct UserData: Decodable {
let id: Int
let email: String
let first_name: String
let last_name: String
let avatar: String
}
So should [weak self] be used, and if not why not. This is a minimum example, and as such the memory debugger doesn't help me out here, but I want to know for the general case.
You need to use [weak self]. There are times when you won't end up with a memory leak not using [weak self], but you can't rely on that. Suppose your request takes too long to load, and the user clicks the back button. Your ViewController will wan to deinitalize, and clear itself from memory, but it can't. It can't because it has a strong reference to your ViewModel which is still downloading and has a strong reference there also. So basically you end up with a memory leak, that is your ViewController remains in memory, which you can't clear anymore.
In your code URLSessionDataTask completion block holds reference to view model. This means that it won't be released until task completes. There is no memory leak because you don't have situation where object A holds object B and object B holds object A.
However, we can ask a question: Do you need results of your data download when view controller is released and thus should be view model. If not you should go with [weak self] for view model
As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM
How is one supposed to aggregate data when using Grand Central Dispatch's ConcurrentPerform()?
I am doing what is in the code below, but resultDictionary seems to lose all its data when the notify() block ends. Thus all I get is an empty dictionary that is returned from the function.
I am not sure why this is happening, because when I print or set a breakpoint I can see there is something in the resultDictionary before the block ends.
let getCVPDispatchQueue = DispatchQueue(label: "blarg",
qos: .userInitiated,
attributes: .concurrent)
let getCVPDispatchGroup = DispatchGroup()
var resultDictionary = dataIDToSRLParticleDictionary()
getCVPDispatchQueue.async { [weak self] in
guard let self = self else { return }
DispatchQueue.concurrentPerform(iterations: self.dataArray.count) { [weak self] (index) in
guard let self = self else { return }
let data = self.dataArray[index]
getCVPDispatchGroup.enter()
let theResult = data.runPartcleFilterForClosestParticleAndMaybeStopAudio()
switch theResult {
case .success(let CVParticle):
// If there was a CVP found, add it to the set.
if let theCVParticle = CVParticle {
self.dataIDsToCVPDictionary.addTodataIDToCVPDict(key: data.ID,
value: theCVParticle)
}
case .failure(let error):
os_log(.error, log: self.logger, "rundatasProcessing error: %s", error.localizedDescription)
self._isActive = false
}
getCVPDispatchGroup.leave()
}
getCVPDispatchGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
print("DONE with \(self.dataIDsToCVPDictionary.getDictionary.count)")
resultDictionary = self.dataIDsToCVPDictionary.getDictionary
print("resultDictionary has \(self.dataIDsToCVPDictionary.getDictionary.count)")
}
}
print("Before Return with \(resultDictionary.count)")
return resultDictionary
}
Not sure if this will help, but this is simple class I made to made accessing the dictionary thread safe.
class DATASynchronizedIDToParticleDictionary {
var unsafeDictionary: DATAIDToDATAParticleDictionary = DATAIDToDATAParticleDictionary()
let accessQueue = DispatchQueue(label: "blarg2",
qos: .userInitiated,
attributes: .concurrent)
var getDictionary: DATAIDToDATAParticleDictionary {
get {
var dictionaryCopy: DATAIDToDATAParticleDictionary!
accessQueue.sync {
dictionaryCopy = unsafeDictionary
}
return dictionaryCopy
}
}
func addToDATAIDToCVPDict(key: String, value: DATAParticle) {
accessQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self.unsafeDictionary[key] = value
}
}
func clearDictionary() {
accessQueue.async(flags: .barrier) { [weak self] in
guard let self = self else { return }
self.unsafeDictionary.removeAll()
}
}
}
You said:
I am doing what is in the code below, but resultDictionary seems to lose all its data when the notify() block ends. Thus all I get is an empty dictionary that is returned from the function.
The issue is that you’re trying to return a value that is calculated asynchronously. You likely want to shift to a completion block pattern.
As an aside, the dispatch group is not necessary. Somewhat ironically, the concurrentPerform is synchronous (i.e. it doesn’t proceed until the parallelized for loop is finished). So there’s no point in using notify if you know that you won’t get to the line after the concurrentPerform until all the iterations are done.
I’d also discourage having the concurrentPerform loop update properties. It exposes you to a variety of problems. E.g. what if the main thread was interacting with that object at the same time? Sure, you can synchronize your access, but it may be incomplete. It’s probably safer to have it update local variables only, and have the caller do the property update in its completion handler block. Obviously, you can go ahead and update properties (esp if you want to update your UI to reflect the in-flight progress), but it adds an additional wrinkle to the code that might not be necessary. Below, I’ve assumed it wasn’t necessary.
Also, while I appreciate the intent behind all of these [weak self] references, they’re really not needed, especially in your synchronization class DATASynchronizedIDToParticleDictionary. We often use weak references to avoid strong reference cycles. But if you don’t have strong references, they just add overhead unless you have some other compelling need.
OK, so let’s dive into the code.
First, I’d retire the specialized DATASynchronizedIDToParticleDictionary with a general-purpose generic:
class SynchronizedDictionary<Key: Hashable, Value> {
private var _dictionary: [Key: Value]
private let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".dictionary", qos: .userInitiated, attributes: .concurrent)
init(_ dictionary: [Key: Value] = [:]) {
_dictionary = dictionary
}
var dictionary: [Key: Value] {
queue.sync { _dictionary }
}
subscript(key: Key) -> Value? {
get { queue.sync { _dictionary[key] } }
set { queue.async(flags: .barrier) { self._dictionary[key] = newValue } }
}
func removeAll() {
queue.async(flags: .barrier) {
self._dictionary.removeAll()
}
}
}
Note, I’ve removed the unnecessary weak references. I’ve also renamed addToDATAIDToCVPDict and clearDictionary with a more natural subscript operator and a removeAll method that more closely mirrors the interface of the underlying Dictionary type. It results in more natural looking code. (And because this is a generic, we can use it for any dictionary that needs this sort of low level synchronization.)
Anyway, you can now declare a synchronized rendition of the dictionary like so:
let particles = SynchronizedDictionary(dataIDToSRLParticleDictionary())
And when I want to update the dictionary with some value, you can do:
particles[data.ID] = theCVParticle
And when I want retrieve actual underlying, wrapped dictionary, I can do:
let finalResult = particles.dictionary
While we’re at it, since we might want to keep track of an array of errors that needs to be synchronized, I might add an array equivalent type:
class SynchronizedArray<Value> {
private var _array: [Value]
private let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".array", qos: .userInitiated, attributes: .concurrent)
init(_ dictionary: [Value] = []) {
_array = dictionary
}
var array: [Value] {
queue.sync { _array }
}
subscript(index: Int) -> Value {
get { queue.sync { _array[index] } }
set { queue.async(flags: .barrier) { self._array[index] = newValue } }
}
func append(_ value: Value) {
queue.async(flags: .barrier) {
self._array.append(value)
}
}
func removeAll() {
queue.async(flags: .barrier) {
self._array.removeAll()
}
}
}
We can now turn our attention to the main routine. So rather than returning a value, we’ll instead give it an #escaping completion handler. And, as discussed above, we’d retire the unnecessary dispatch group:
func calculateAllClosestParticles(completion: #escaping ([String: CVParticle], [Error]) -> Void) {
let queue = DispatchQueue(label: "blarg", qos: .userInitiated, attributes: .concurrent)
let particles = SynchronizedDictionary(dataIDToSRLParticleDictionary())
let errors = SynchronizedArray<Error>()
queue.async {
DispatchQueue.concurrentPerform(iterations: self.dataArray.count) { index in
let data = self.dataArray[index]
let result = data.runPartcleFilterForClosestParticleAndMaybeStopAudio()
switch result {
case .success(let cvParticle):
// If there was a CVP found, add it to the set.
if let cvParticle = cvParticle {
particles[data.ID] = cvParticle
}
case .failure(let error):
errors.append(error)
}
}
DispatchQueue.main.async {
completion(particles.dictionary, errors.array)
}
}
}
Now, I don’t know what the right types were for the dictionary, so you might need to adjust the parameters of the completion. And you didn’t provide the rest of the routines, so I may have some details wrong here. But don’t get lost in the details, but just note the scrupulous avoidance of properties within the concurrentPerform and the passing of the results back in the completion handler.
You’d call it like so:
calculateAllClosestParticles { dictionary, errors in
guard errors.isEmpty else { return }
// you can access the dictionary and updating the model and UI here
self.someProperty = dictionary
self.tableView.reloadData()
}
// but don't try to access the dictionary here, because the asynchronous code hasn't finished yet
//
FWIW, while I used the reader-writer pattern you did in your example, in my experience, NSLock is actually more performant for quick synchronizations, especially when you are using concurrentPerform that might tie up all of the cores on your CPU, e.g.
class SynchronizedDictionary<Key: Hashable, Value> {
private var _dictionary: [Key: Value]
private let lock = NSLock()
init(_ dictionary: [Key: Value] = [:]) {
_dictionary = dictionary
}
var dictionary: [Key: Value] {
lock.synchronized { _dictionary }
}
subscript(key: Key) -> Value? {
get { lock.synchronized { _dictionary[key] } }
set { lock.synchronized { _dictionary[key] = newValue } }
}
func removeAll() {
lock.synchronized {
_dictionary.removeAll()
}
}
}
Where
extension NSLocking {
func synchronized<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}
}
Bottom line, you don’t want to force context switches for synchronization if you don’t have to.
When doing concurrent perform, if you have many dataPoints and if the time required by each call to runPartcleFilterForClosestParticleAndMaybeStopAudio is modest, you might want to consider “striding”, doing several datapoint in each iteration. It’s beyond the scope of this question, but just a FYI.
Not exactly sure what I did, but I moved the
resultDictionary = self.dataIDsToCVPDictionary.getDictionary
outside the first async block and that seem to allowed the data to be retained/remain for the function return.
Working in Swift3; I've got a pretty expensive operation running in a loop iterating through stuff and building it into an array that on return would be used as the content for an NSTableView.
I wanted a modal sheet showing progress for this so people don't think the app is frozen. By googling, looking around in here and not a small amount of trial and error I've managed to implement my progressbar and have it show progress adequately as the loop progresses.
The problem right now? Even though the sheet (implemented as an NSAlert, the progress bar is in the accesory view) works exactly as expected, the whole thing returns before the loop is finished.
Here's the code, hoping somebody can tell me what am I doing wrong:
class ProgressBar: NSAlert {
var progressBar = NSProgressIndicator()
var totalItems: Double = 0
var countItems: Double = 0
override init() {
progressBar.isIndeterminate = false
progressBar.style = .barStyle
super.init()
self.messageText = ""
self.informativeText = "Loading..."
self.accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
self.accessoryView?.addSubview(progressBar)
self.layout()
self.accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
self.addButton(withTitle: "")
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
self.beginSheetModal(for: ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window!, completionHandler: nil)
}
}
static var allUTIs: [SWDAContentItem] = {
var wrappedUtis: [SWDAContentItem] = []
let utis = LSWrappers.UTType.copyAllUTIs()
let a = ProgressBar()
a.totalItems = Double(utis.keys.count)
a.progressBar.maxValue = a.totalItems
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
a.countItems += 1.0
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
Thread.sleep(forTimeInterval:0.0001)
DispatchQueue.main.async {
a.progressBar.doubleValue = a.countItems
if (a.countItems >= a.totalItems && a.totalItems != 0) {
ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window?.endSheet(a.window)
}
}
}
}
Swift.print("We'll return now...")
return wrappedUtis // This returns before the loop is finished.
}()
In short, you're returning wrappedUtis before the asynchronous code has had a chance to finish. You cannot have the initialization closure return a value if the update process itself is happening asynchronously.
You clearly successfully diagnosed a performance problem in the initialization of allUTIs, and while doing this asynchronously is prudent, you shouldn't be doing that in that initialization block of the allUTIs property. Move this code that initiates the update of allUTIs into a separate function.
Looking at ProgressBar, it's really an alert, so I'd call it ProgressAlert to make that clear, but expose the necessary methods to update the NSProgressIndicator within that alert:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Increment progress bar in this alert.
func increment(by value: Double) {
progressBar.increment(by: value)
}
/// Set/get `maxValue` for the progress bar in this alert
var maxValue: Double {
get {
return progressBar.maxValue
}
set {
progressBar.maxValue = newValue
}
}
}
Note, this doesn't present the UI. That's the job of whomever presented it.
Then, rather than initiating this asynchronous population in the initialization closure (because initialization should always be synchronous), create a separate routine to populate it:
var allUTIs: [SWDAContentItem]?
private func populateAllUTIs(in window: NSWindow, completionHandler: #escaping () -> Void) {
let progressAlert = ProgressAlert()
progressAlert.beginSheetModal(for: window, completionHandler: nil)
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
progressAlert.maxValue = Double(utis.keys.count)
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async { [weak progressAlert] in
progressAlert?.increment(by: 1)
}
}
DispatchQueue.main.async { [weak self, weak window] in
self?.allUTIs = wrappedUtis
window?.endSheet(progressAlert.window)
completionHandler()
}
}
}
Now, you declared allUTIs to be static, so you can tweak the above to do that, too, but it seems like it's more appropriate to make it an instance variable.
Anyway, you can then populate that array with something like:
populateAllUTIs(in: view.window!) {
// do something
print("done")
}
Below, you said:
In practice, this means allUTIs is only actually initiated when the appropriate TabViewItem is selected for the first time (which is why I initialize it with a closure like that). So, I'm not really sure how to refactor this, or where should I move the actual initialization. Please keep in mind that I'm pretty much a newbie; this is my first Swift (also Cocoa) project, and I've been learning both for a couple of weeks.
If you want to instantiate this when the tab is selected, then hook into the child view controllers viewDidLoad. Or you can do it in the tab view controller's tabView(_:didSelect:)
But if the population of allUTIs is so slow, are you sure you want to do this lazily? Why not trigger this instantiation sooner, so that there's less likely to be a delay when the user selects that tab. In that case, you might trigger it the tab view controller's own viewDidLoad, so that the tab that needs those UTIs is more likely to have them.
So, if I were considering a more radical redesign, I might first change my model object to further isolate its update process from any specific UI, but rather to simply return (and update) a Progress object.
class Model {
var allUTIs: [SWDAContentItem]?
func startUTIRetrieval(completionHandler: (() -> Void)? = nil) -> Progress {
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
let progress = Progress(totalUnitCount: Int64(utis.keys.count))
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async {
progress.completedUnitCount += 1
}
}
DispatchQueue.main.async { [weak self] in
self?.allUTIs = wrappedUtis
completionHandler?()
}
}
return progress
}
}
Then, I might have the tab bar controller instantiate this and share the progress with whatever view controller needed it:
class TabViewController: NSTabViewController {
var model: Model!
var progress: Progress?
override func viewDidLoad() {
super.viewDidLoad()
model = Model()
progress = model.startUTIRetrieval()
tabView.delegate = self
}
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
super.tabView(tabView, didSelect: tabViewItem)
if let item = tabViewItem, let controller = childViewControllers[tabView.indexOfTabViewItem(item)] as? ViewController {
controller.progress = progress
}
}
}
Then the view controller could observe this Progress object, to figure out whether it needs to update its UI to reflect this:
class ViewController: NSViewController {
weak var progress: Progress? { didSet { startObserving() } }
weak var progressAlert: ProgressAlert?
private var observerContext = 0
private func startObserving() {
guard let progress = progress, progress.completedUnitCount < progress.totalUnitCount else { return }
let alert = ProgressAlert()
alert.beginSheetModal(for: view.window!)
progressAlert = alert
progress.addObserver(self, forKeyPath: "fractionCompleted", context: &observerContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let progress = object as? Progress, context == &observerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
dispatchPrecondition(condition: .onQueue(.main))
if progress.completedUnitCount < progress.totalUnitCount {
progressAlert?.doubleValue = progress.fractionCompleted * 100
} else {
progress.removeObserver(self, forKeyPath: "fractionCompleted")
view.window?.endSheet(progressAlert!.window)
}
}
deinit {
progress?.removeObserver(self, forKeyPath: "fractionCompleted")
}
}
And, in this case, the ProgressAlert only would worry about doubleValue:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width: 290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Set/get `maxValue` for the progress bar in this alert
var doubleValue: Double {
get {
return progressBar.doubleValue
}
set {
progressBar.doubleValue = newValue
}
}
}
I must note, though, that if these UTIs are only needed for that one tab, it raises the question as to whether you should be using a NSAlert based UI at all. The alert blocks the whole window, and you may want to block interaction with only that one tab.