Can not update UI while making GMSMarkers - swift

Function which initialize GMSMarkers:
func makeMarkers (progressHandler: ((String) -> Void)? = nil) {
progressHandler?("Инициализируем маркеры...")
for device in self.devices {
progressHandler?("Инициализируем маркеры: для устройства \(device.name!)...")
if let position = self.positions.findById(device.positionId) {
if let marker = DeviceMarker(device, at: position) {
self.deviceMarkers.append(marker)
}
}
}
for geofence in self.geofences {
progressHandler?("Инициализируем маркеры: для геометки \(geofence.name!)...")
if let marker = GeofenceMarker(geofence) {
self.geofenceMarkers.append(marker)
}
}
self.locationManager.requestLocation()
progressHandler?("Инициализируем маркеры: для пользователя \(self.loggedUser.name!)...")
if let userLocation = self.locationManager.location {
self.userMarker = UserMarker(self.loggedUser, at: userLocation)
} else {
self.userMarker = UserMarker(self.loggedUser, at: CLLocation(latitude: 48, longitude: 44))
}
progressHandler?("Маркеры инициализированы...")
}
As you can see I want inform user about progress of this operation using progressHandler.
But during execution of that function I can not see any of that messages.
Snipped where I call function makeMarkers:
...
DB.geofences.server.getAll() { geofences in
self.statusLabel.text = "Получили геометки \(geofences.count)..."
DB.devices.server.getAll() { devices in
self.statusLabel.text = "Получили устройства \(devices.count)..."
DB.positions.server.getAll() { positions in
self.statusLabel.text = "Получили позиции \(positions.count)..."
DB.devices.client.insert(devices)
self.statusLabel.text = "Записали устройства..."
DB.positions.client.insert(positions)
self.statusLabel.text = "Записали позиции..."
DB.geofences.client.insert(geofences)
self.statusLabel.text = "Записали геометки..."
MapManager.shared.makeMarkers() { status in
self.statusLabel.text = status
}
...
And if I change view messages using print() all messages will be shown.
So, question is: why UI changes are blocked during execution of my function and how get rid of that?
UPDATE: Sure, I do everything in main thread.
UPDATE 2: Code snippet where I'm calling my function updated. Last message which I can see before map controller shows is "Получили устройства...", it is strange, because I'm sure I get positions as well

Well, that occurs because UI updates happen in the main thread. But when you change your label's text this change does not happen immediately. Instead, all UI changes get scheduled and only get rendered when the app finishes the current pass through the event loop.
Since you are performing time-consuming tasks in the same main loop, it becomes blocked and changes won't happen until those tasks are finished and your function returns.
You may check these articles to get more detailed explanation on how event loops work: Run Loops, Main event loop
I suggest you move all time-consuming tasks (such as database queries) to a background thread and call the main thread from there when you actually want to update your UILabel.

Related

CodeMirror - Catch Pick event for show hints

I am trying to run a function when the "pick" object from CodeMirror's show hints pick function is triggered.
I have tried (among a lot of other options):
CodeMirror.commands.pick = function (cm) {
console.log(cm)
}
CodeMirror.on("pick", function (cm) { console.log(cm) } );
var completion = cm.state.completionActive;
console.log('handling the completion object:', completion);
var pick = completion.pick;
completion.pick = function (data, i) {
var completion = data.list[i];
console.log('picked', data, completion);
pick.apply(this, arguments);
}
In the documentation on codemirror it is described as:
"pick" (completion)
Fired when a completion is picked. Passed the completion value (string or object).
and I can see in the code for the show hints that this is triggered:
CodeMirror.signal(data, "pick", completion);
But how do I monitor that signal and react to it.

addUIInterruptionMonitor is not getting called on macOS

I want to test my macOS application. It uses your Macbook's camera, and want to handle this in my UITest. However I cannot get it working. Here is my NOT working code. This code triggers to notification, and I'm presented an alert to allow access to my camera, but the closure is not getting called. Thanks fo any help.
There are many solutions for iOS, but I need it on macOS.
let alertHandler = addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
if alert.buttons.matching(identifier: "OK").count > 0 {
alert.buttons["OK"].click()
self.app.click()
return true
} else {
return false
}
}
XCTAssertTrue(startButton.waitForExistence(timeout: 1.0))
startButton.click()
XCTAssertTrue(recordButton.waitForExistence(timeout: 20.0))
recordButton.click()
wait(for: 8)
recordButton.click()
removeUIInterruptionMonitor(alertHandler)
}
I managed to make interruption monitor work on macOS by adding an extra interaction after the interaction that triggers the system dialog (be it camera access or else). So in your example I would add an action after startButton.click() (if that is what triggers the camera access dialog).
Example:
func testCamera() {
let alertHandler = addUIInterruptionMonitor(withDescription: "Camera Permission Alert") { (alert) -> Bool in
if alert.buttons.matching(identifier: "OK").count > 0 {
alert.buttons["OK"].click()
self.app.click()
return true
} else {
return false
}
}
useCameraButton.click()
// try to interact with the app by clicking on the app's window
app.windows.first().click()
// at this point the handler should intercept the system interruption
// and blocks further execution until handler does return
// try to use the camera again
useCameraButton.click()
removeUIInterruptionMonitor(alertHandler)
}
Hint about this behaviour in Apple's documentation:
When an alert or other modal UI is an expected part of the
test workflow, don't write a UI interruption monitor. The test won’t
use the monitor because the modal UI isn’t blocking the test. A UI
test only tries its UI interruption monitors if the elements it needs
to interact with to complete the test are blocked by an interruption
from an unrelated UI.
https://developer.apple.com/documentation/xctest/xctestcase/handling_ui_interruptions

How to guarantee one function runs after another in the Grand Central Dispatch?

Basically, I want to upload some images before I run other functions that rely on the image being uploaded. I think I may have a misunderstanding of what GCD is/how the threads work. I want function 1 and 2 to happen AFTER I upload the images. They are both quick to execute but rely heavily on the upload images to be complete. Maybe I shouldn't use GCD (as I want to implement a waiting indicator)? I just can't seem to get this to execute properly
if goToHome {
DispatchQueue.global().async {
DispatchQueue.main.sync {
self.uploadImages() // Uploads the images, takes a good amount of time to execute
function1()
function2()
}
}
Functions 1 and 2 keep running before the upload images get completed as they take much less time to execute.
The basic pattern in Swift is to do work such as uploading on a background thread, then call a completion function on the main thread and continue work based on whether or not your upload finished successfully.
Generally you'll call back onto the main thread in case you need to do something with the user interface such as setting a progress indicator (which has to happen on the main thread).
So something like this:
func uploadInBackground(_ images: [Image], completion: #escaping (_ success: Bool) -> Void) {
DispatchQueue.global(qos: .background).async {
var success = true
// upload the images but stop on any error
for image in images {
success = uploadImage(image) // upload your images somehow
guard success else { break }
}
DispatchQueue.main.async {
completion(success)
}
}
}
func mainThreadUploader() {
let images = [Image(), Image()] // get your images from somewhere
// we are on the main thread where UI operations are ok, so:
startProgressIndicator()
uploadInBackground(images) { success in
// this callback is called on the main thread, so:
stopProgressIndicator()
guard success else { return }
// images uploaded ok, so proceed with functions that depend on
// the upload(s) completing successfully:
function1()
function2()
}
}
Despite running the upload image function in the main queue, the upload image function itself is running operations in a background queue. To fix this, possible strategies would be:
use a completion handler with the image upload, likely the easiest to implement depending on the implementation of the self.uploadImages() function
make the image uploading happen on the main thread, which is likely hard to implement and inadvisable
use a dispatch group, which I personally have less experience with but is an option

How to force UI Display to update in swift?

I'm trying to get the View Controller to update while I'm in a loop that calls a function that does tons of Label updates. But the update to the screen doesn't happen until I've finished the loop.
Is there a way to force a UI update before continuing? Something like forcing data to be written to a disk.
code for the button to start the loop is something like this:
while (match.currentInnings == currentInnings)
{
playSingleBall()
if (gameover == true)
{
return
}
}
After playSingleBall, I'd like to force a UI update before continuing.
Thanks.
Call it in Dispatch Queue:
Swift 3.x
DispatchQueue.main.async {
updateYourUI()
}
Swift 2.x
dispatch_async(dispatch_get_main_queue()) {
updateYourUI()
}

RxJs Observable with infinite scroll OR how to combine Observables

I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>