Using DispatchGroup in onAppear - swiftui

What I am trying to achieve is:
1) fetch records from cloudkit (excluding assets)
2) randomly select 3 recordIDs from those fetched records
3) fetch the full 3 records from cloudkit (including assets).
The issue that I'm struggling with is making sure that 1, 2, & 3 begin and are completed one after the other.
//...
.onAppear(){
let group = DispatchGroup()
// MARK: - 1) fetch from CloudKit - excluding CKAsset
CloudKit.fetch() {(result) in
group.enter() // moving group.enter() above CloudKit.fetch() locks everything up
switch result {
case .success(let newItem):
self.array.append(newItem)
group.leave()
case .failure(let err):
print(err.localizedDescription)
}
}
// MARK: - 2) randomly select 3 records
group.wait() // doesn't actually wait as this is executed before group.enter()
self.array.shuffle()
let firstSelection = self.array[0].recordID!
let firstSelection = self.array[1].recordID!
let firstSelection = self.array[2].recordID!
// MARK: - 3) fetch recordID from CloudKit - includes CKAsset
CloudKit.fetchRecord(recordID: self.array[0]) {(result) in
switch result {
case .success(let newItem):
self.arrayRandomlySelected.append(newItem)
case .failure(let err):
print(err.localizedDescription)
}
}
// run for remaining 2 records and move to next view when done
}
I tried using DispatchGroup() but group.wait() gets called before group.enter(). Am I using DispatchGroup() correctly or should I be trying something else?

Related

If statement to allow single and multiple if options

I have an If statement dependent on a choice made in a google form. Everything currently works as needed BUT, the question choice allows them to choose ALL OPTIONS THAT APPLY. If more than one option is chosen, the script fails.
How it currently works - person fills out form, script grabs info and inputs to a document, gives specified message based on choice from question (cif and message in script below), then emails the correct recipient (chosen in form). It works beautifully if cif is submitted with one choice.
What do I add/edit to the script to allow for multiple choices, and therefore multiple messages input to the document, without having to write an If state for each scenario?
// Global variables
var docTemplate = "1EoBcz0BK4R5hm-q5pR68xnQnR8DlR56XzjxRrgsu4uE"; // *** replace with your template ID ***
var docName = "You got a High 5";
function onFormSubmit(e) { // add an onsubmit trigger
// Values come from the spreadsheet form
var observer = e.values[1]
var teacher = e.values[2]
var email = e.values[2]
var period = e.values[4]
var time = e.values[3]
var cif = e.values[5]
var comments = e.values[6]
var message;
if (cif == 'READ - Variety of texts') {
message = 'read quote'
}
else if (cif == 'WRITE - Paper or electronic') {
message = 'write quote'
}
else if (cif == 'THINK - Actively engaged students') {
message = 'think quote'
}
else if (cif == 'TALK - Purposeful discussion') {
message = 'talk quote'
}
else if (cif == 'MOVE - Students moving through class') {
message = 'move quote'
}
else if (cif == 'CIF not observed') {
message = 'CIF not observed'
}
// Get document template, copy it as a new temp doc, and save the Doc’s id
var copyId = DriveApp.getFileById(docTemplate)
.makeCopy(docName+' for '+teacher)
.getId();
// Open the temporary document
var copyDoc = DocumentApp.openById(copyId);
// Get the document’s body section
var copyBody = copyDoc.getActiveSection();
// Replace place holder keys,
copyBody.replaceText('keyobserver', observer);
copyBody.replaceText('keyperiod', period);
copyBody.replaceText('keytime', time);
copyBody.replaceText('keycif', cif);
copyBody.replaceText('keycomments', comments);
copyBody.replaceText('keymessage', message)
var todaysDate = Utilities.formatDate(new Date(), "GMT", "MM/dd/yyyy");
copyBody.replaceText('keyTodaysDate', todaysDate);
// Save and close the temporary document
copyDoc.saveAndClose();
// Convert temporary document to PDF by using the getAs blob conversion
var pdf = DriveApp.getFileById(copyId).getAs("application/pdf");
// Attach PDF and send the email
var subject = "High 5 - it matters.";
var body = "You got a High 5! See attached PDF. " +
"Please do not reply to this email. You will be asked to supply a response thorugh a link within the attached PDF. " +
"'Do all the good you can. By all the means you can. In all the ways you can. In all the places you can. At all the times you can. To all the people you can. As long as you ever can. -John Wesley'";
MailApp.sendEmail(email, subject, body, {htmlBody: body, attachments: pdf});
// Delete temp file
DriveApp.getFileById(copyId).setTrashed(true);
}
Having multiple selected answer will return a comma separated values in form of string.
For example:
e.values[5] can have:
'READ - Variety of texts, WRITE - Paper or electronic, THINK - Actively engaged students, TALK - Purposeful discussion, MOVE - Students moving through class, CIF not observed'
in one single string and using == will not work since it has different content versus the string you are comparing to. This causes all of your if statements to fail because non of them matches the string. This leads to an undefined variable message which causes error on replaceText() function.
To fix the issue replace this line of code:
var message;
if (cif == 'READ - Variety of texts') {
message = 'read quote'
}
else if (cif == 'WRITE - Paper or electronic') {
message = 'write quote'
}
else if (cif == 'THINK - Actively engaged students') {
message = 'think quote'
}
else if (cif == 'TALK - Purposeful discussion') {
message = 'talk quote'
}
else if (cif == 'MOVE - Students moving through class') {
message = 'move quote'
}
else if (cif == 'CIF not observed') {
message = 'CIF not observed'
}
with:
var message = [];
if (cif.match('READ - Variety of texts')) {
message.push('read quote');
}
if (cif.match('WRITE - Paper or electronic')) {
message.push('write quote');
}
if (cif.match('THINK - Actively engaged students')) {
message.push('think quote');
}
if (cif.match('TALK - Purposeful discussion')) {
message.push('talk quote');
}
if (cif.match('MOVE - Students moving through class')) {
message.push('move quote');
}
if (cif.match('CIF not observed')) {
message.push('CIF not observed')
}
and in your
copyBody.replaceText('keymessage', message);
just add join function to make message a comma separated values when the content is more than 1:
copyBody.replaceText('keymessage', message.join(',');
Example Usage:
Multiple CIF:
var cif = 'READ - Variety of texts, WRITE - Paper or electronic, THINK - Actively engaged students, TALK - Purposeful discussion, MOVE - Students moving through class, CIF not observed';
Output:
Single CIF:
var cif = 'WRITE - Paper or electronic';
Output:
References
Array.join()
String.match()

AUDIOKIT - Why is the AKOscillatorBank RELASE, DECAY, SUSTAIN, and ATTACK being updated long after I change their values?

I have a simple AudioKit app in the works.
I initialize an AKOsicillatorBANK at runtime and allow you to press a button to access a new viewCONTROLLER which has SLIDERS that when changed change the values of the oscillatorBANK properties [releaseDURATION, attackDURATION, decayDURATION, sustainLEVEL].
At the end of the slider's PANGESTURE [state == ended], I call a function that sets the oscillatorBANKS properties to their new values, which are stored in class variables.
I do not know why, but the changes only take effect some time (a few minutes) after the function is called. Does anyone know why this is?
ALSO PLEASE DO NOT COMMENT ON MY CAPITALIZATION STYLE. I PREFER THIS AS IT ALLOWS ME TO EMPHASIZE WHAT I BELIEVE TO BE IS IMPORTANT, WHICH CAN THEN HELP YOU SEE MY TRAIN OF THOUGHTS BETTER.
HERE IS THE CODE. Please note that I ONLY included the code for DECAY within the panGESTURE, because attack, release, and sustain code is all the same design:
// MAIN VIEW CONTROLLER CLASS, GLOBAL SCOPE, INITIALIZED WITH VIEW DID LOAD //
let bank = AKOscillatorBank()
var bankATTACK: Double = 0
var bankDECAY: Double = 0
var bankSUSTAIN: Double = 1
var bankRELEASE: Double = 0
func updateBANKWAVE() {
self.bank.rampDuration = 1
self.bank.attackDuration = self.bankATTACK
self.bank.decayDuration = self.bankDECAY
self.bank.sustainLevel = self.bankSUSTAIN
self.bank.releaseDuration = self.bankRELEASE
print("NEW BANK RAMP [\(self.bank.rampDuration)]")
print("NEW BANK ATTACK [\(self.bank.attackDuration)]")
print("NEW BANK DEC [\(self.bank.decayDuration)]")
print("NEW BANK SUS [\(self.bank.sustainLevel)]")
print("NEW BANK REL [\(self.bank.releaseDuration)]")
}
func prepareforSEGUE() {
if let sliderCONTROLLER = segue.destination as? SLIDERVIEWCONTROLLER {
sliderCONTROLLER.mainCONTROLLER = self
}
}
// SLIDER VIEW CONTROLLER CLASS //
if panGESTURE.state == changed {
// ... update a UILabel to reflect the value to be set ...
decayTEXT = String(format: "%.3f", (self.decayTRANSLATION.truncatingRemainder(dividingBy: 100.0)))
// ... update the MAIN controller variables ...
self.mainCONTROLLER.bankDECAY = Double(self.decayTRANSLATION.truncatingRemainder(dividingBy: 100.0))
// ... protect against values above 10 or below 0 ...
if decayTRANSLATION > 10 { decayVALUE.text = "10.000" ; decayTRANSLATION = 10.01 ; self.mainCONTROLLER.bankDECAY = 10 }
if decayTRANSLATION < 0 { decayVALUE.text = "0.000" ; decayTRANSLATION = -0.01 ; self.mainCONTROLLER.bankDECAY = 0 }
}
if panGESTURE.state == ended {
self.mainCONTROLLER.updateBANKWAVE()
}
By changing the oscillatorBANK.rampDURATION property to 0 instead of 1, I get instantaneous results. However, even though the release is being set to 1, the note can still be heard after 4 or 5 seconds... so...

How can I make a MicroBit MakeCode mass code creator?

I am working on a project for my town's Maker Faire. What I'm trying to do is have a Micro:Bit send a message through radio, where another one would pick it up and send it through another channel. Then another Micro:Bit would pick that up and so on and so forth. I have the code for the starting micro:bit that sends the first message, and the second micro:bit that receives the first one's message and sends it out again. Each new Micro:Bit bumps up the radio channels by one. Is there any way to do this automatically without having to manually bump it up for each new Micro:bit?
This is my code for the second Micro:Bit:
radio.onReceivedString(function (receivedString) {
radio.setGroup(1)
basic.showString(receivedString)
radio.setGroup(2)
radio.sendString(receivedString)
})
Thanks!
The challenge here is coming up with a way so that each micro:bit knows what its sequence number is on startup. If you're able to initialise each micro:bit with a unique sequence number (eg: 0, 1, 2, 3, 4, 5), then you can flash the same code on each micro:bit and just use the sequence number as an offset. ie: setGroup(sequenceNumber)... setGroup (sequenceNumber + 1).
In the case of the first micro:bit that will be groups 0 and 1 respectively, in the case of the second micro:bit that will be groups 1 and 2 respectively, and so on.
I can think of a few ways of having each micro:bit have its own unique sequence number on startup. One way you can do that is have them all set to 0 on startup, and then use the buttons on each micro:bit to change the sequence number. Something like this:
let sequenceNumber = 0;
input.onButtonPressed(Button.A, function () {
if (sequenceNumber > 0) sequenceNumber--;
})
input.onButtonPressed(Button.B, function () {
sequenceNumber++;
})
radio.onReceivedString(function (receivedString) {
radio.setGroup(sequenceNumber)
basic.showString(receivedString)
radio.setGroup(sequenceNumber + 1)
radio.sendString(receivedString)
})
The above strategy would require you to go around each micro:bit and manually set their sequence number after flashing it. If you flash it again, you'll have to repeat the process..
Another way to approach this is to have all micro:bits running the same program, except for one, which we'll refer to as the master. This master micro:bit will keep a list of all devices its seen (over radio on a preset group, eg: 0) and for every new micro:bit that requests a sequence number, it'll assign it a unique number and send it back. Each of the other micro:bits will startup in an initialization phase where it continuously requests a sequence number and polls until it's been assigned one by the master micro:bit.
Something like the following:
Master:
let masterGroupNumber = 0; // predetermined group number for master micro:bit
let currentSequenceNumber = 1;
let devices: { [key: string]: number } = {};
radio.setGroup(masterGroupNumber);
radio.onReceivedValue(function (name: string, value: number) {
if (name === "uid") {
// We're received a unique id. Assign it a sequence number
// if it has not already been assigned
let uid = value.toString();
if (!devices[uid])
devices[uid] = currentSequenceNumber++;
radio.sendValue(uid, devices[uid]);
}
})
All other micro:bits:
// Begin the program in the initialization phase,
// we're waiting to be assigned a sequence number
let masterGroupNumber = 0; // predetermined group number for master micro:bit
let sequenceNumber = 0;
let hasSequenceNumber = false;
radio.setGroup(masterGroupNumber);
let uniqueDeviceId = control.deviceSerialNumber();
radio.onReceivedValue(function (name: string, value: number) {
if (name === uniqueDeviceId.toString()) {
sequenceNumber = value;
hasSequenceNumber = true;
}
})
// Poll till we've received a sequence number
while (!hasSequenceNumber) {
// Broadcast our unique device id.
radio.sendValue("uid", uniqueDeviceId);
// Wait a litte
basic.pause(500);
}
// We've established communication with the master micro:bit
// and have received a sequence number
radio.onReceivedString(function (receivedString) {
radio.setGroup(sequenceNumber)
basic.showString(receivedString)
radio.setGroup(sequenceNumber + 1)
radio.sendString(receivedString)
})
There's probably a few other ways you could go about doing this, but I hope this gives you some ideas.
ps: I did not have a chance to test if this code works. Let me know how it goes :)

How to delete mass records using Map/reduce script?

I have created a Map/Reduce script which will fetch customer invoices and delete it. If I am creating saved search in UI based on the below criteria, it shows 4 million records. Now, if I run the script, execution stops before completing the "getInputData" stage as maximum storage limit of this stage is 200Mb. So, I want to fetch first 4000 records out of 4 million and execute it and schedule the script for every 15 mins. Here is the code of first stage (getInputData) -
var count=0;
var counter=0;
var result=[];
var testSearch = search.create({
type: 'customrecord1',
filters: [ 'custrecord_date_created', 'notonorafter', 'sta​rtO​fLa​stM​ont​h' ],
columns: [ 'internalid' ]
});
do{
var resultSearch = testSearch.run().getRange({
start : count,
end : count+1000
});
for(var arr=0;arr<resultSearch.length;arr++){
result.push(resultSearch[arr]);
}
counter = count+counter;
}while(resultSearch.length >= 1000 && counter != 4000);
return result;
During creating the saved search, it is taking long time, is there any work around where we can filter first 4000 records during saved search creation?
Why not a custom mass update?
It would be a 5-10 line script that grabs the internal id and record type of the current record in the criteria of the mass update then deletes the record.
I believe this is what search.runPaged() and pagedData.fetch() is for.
search.runPaged runs the current search and returns summary information about paginated results - it does not give you the result set or save the search.
pagedData.fetch retrieves the data within the specified page range.
If you are intent on the Map/Reduce you can just return your created search. Netsuite will run it and pass each line to the next phase. You can even use a saved search where you limit the number of lines and then in your summarize phase re-trigger the script if there's anything left to do.
The 4k record syntax though is:
var toDelete = [];
search.run().each(function(r){
toDelete.push(r.id);
return toDelete.length < 4000;
});
return toDelete;
finally I normally do this as scheduled mass update. It will tend to interfere less with any production scheduled and map/reduce scripts.
/**
* #NApiVersion 2.x
* #NScriptType MassUpdateScript
*/
define(["N/log", "N/record"], function (log, record) {
function each(params) {
try {
record.delete({
type: params.type,
id: params.id
});
log.audit({ title: 'deleted ' + params.type + ' ' + params.id, details: '' });
}
catch (e) {
log.error({ title: 'deleting: ' + params.type + ' ' + params.id, details: (e.message || e.toString()) + (e.getStackTrace ? (' \n \n' + e.getStackTrace().join(' \n')) : '') });
}
}
return {
each:each
};
});

RACSubject migration to ReactiveCocoa 5 (Swift 3)

I have to migrate an application from ReactiveCocoa 4 to ReactiveCocoa 5 (due to Swift 3 migration)
The old implementation uses some RACSubject instances for triggering (performOperationSubject.sendNext) an operation and for handling (didOperationSubject.subscribeNext) the result
internal class MyClass {
internal var performOperationSubject: RACSubject = RACSubject()
internal var didOperationSubject: RACSubject = RACSubject()
internal overide init() {
super.init()
self.performOperationSubject.subscribeNext { [weak self](_) in
guard let strongSelf = self else { return }
strongSelf.didOperationSubject.sendNext(result)
}
}
and when the MyClass instance is used
myClassInstance.didOperationSubject.subscribeNext { ... }
myClassInstance.performOperationSubject.sendNext(value)
Unfortunately the RACSubject is no more present in ReactiveCocoa 5 (ReactiveSwift)
How can I replace the RACSubject in this context?
You would use pipe which gives you an input observer and an output signal instead of using a RACSubject for both input and output. The example from the ReactiveSwift docs looks like this:
let (signal, observer) = Signal<String, NoError>.pipe()
signal
.map { string in string.uppercased() }
.observeValues { value in print(value) }
observer.send(value: "a") // Prints A
observer.send(value: "b") // Prints B
observer.send(value: "c") // Prints C
Using Signal.pipe instead of RACSubject:
Since the Signal type, like RACSubject, is always “hot”, there is a special class method for creating a controllable signal. The Signal.pipe method can replace the use of subjects, and expresses intent better by separating the observing API from the sending API.
To use a pipe, set up observers on the signal as desired, then send values to the sink:
let (signal, observer) = Signal<String, NoError>.pipe()
signal.observeValue(value in
// use value
})
observer.send(value: "the value")