Thursday, May 19, 2022

A Metaphor for Iterative Design: A Livecode Project to Demonstrate the Sierpinski Gasket

I had the good fortune to be invited by the students and faculty of the Learning Sciences and Mathematics and Science Education doctoral programs at Boğaziçi University in Turkey to participate in a seminar on design-based research (DBR). DBR has become a particularly important research approach in the field of learning, design, and technology. I decided to use my 30 minute presentation on May 11, 2022 to focus on the importance of being a designer when engaging in DBR. That is, I think it important to take on the identity of a designer. When this happens, you take on a different view of the world that will influence and support your research.

The Importance of Iteration in Design

An important aspect of all design, including DBR, is iteration. The everyday definition of iteration is simply to repeat a process or procedure. But, I prefer the mathematical definition of iteration because I think it is closer to how iteration needs to be practiced in DBR. According to Wikipedia, iteration in mathematics is "the process of iterating a function, i.e. applying a function repeatedly, using the output from one iteration as the input to the next."

The key idea here is that the output of one iteration is used (at least partially) as the input of the next iteration. I think this distinction is important for DBR because it means you revise your intervention based on what you just learned from trying it out. Implementing this principle well leads to profound insights which a designer can use in the revision process. In this process you stop, reflect, and make explicit - by writing down - what you learned. This is followed by making a conjecture that if the design is revised in such and such a way, then something better will happen the next time the design is tested (e.g. more learning, more motivation, etc.) If no improvement is found, then that conjecture is refuted and a new conjecture must be constructed. All of this is being documented along the way in DBR. These conjectures should be based in the formulation and evolution of a local or "humble" theory generated by the designer/researcher. This is the heart of the DBR process. 

A Fun Example of Iteration

Rather than just talk about this on May 11, I thought I'd show a simple mathematical example as both a literal (it really shows iteration in mathematics) and metaphorical example (within DBR) of iteration. Consider the following:

  1. On a sheet of paper, draw three dots anywhere you like. These become the vertices of a triangle. I've drawn these in red in the figure below.
  2. Pick a random spot on the paper and draw another dot. I've drawn this in black on the left side of the figure below.
  3. Pick one of the three red dots at random. Let's say the bottom-right red dot was chosen.
  4. Draw a dot at the midpoint of the black dot and the randomly chosen red dot.
  5. Repeat steps 3 and 4 using the previously drawn black dot for step 4.

What do you think you will get after lots of dots, say 1000, 5000, or 10,000?

Some people guess that the piece of paper will just get filled with dots. Others guess that the triangle formed by the red dots will just fill up. Another guess is that a circular pattern will emerge.

The actual result is quite surprising. Here is a short video of a LiveCode project I built to demonstrate this example:

(I don't know about you, but I find the erasing of the dots almost as mesmerizing as the emergence of the design.)

Here is the result drawn with 5000 small dots:

This is a special fractal known as the Sierpiński Gasket. It infinitely repeats the triangular shape. That is, if we were able to keep drawing an infinite number of dots with an ever-increasing fine point, the shape would continue to repeat itself to infinity no matter how much we zoomed in or magnified any portion of the figure. It is a most surprising result from such a simple set of rules.

But, my point in the seminar was not to teach anyone about fractals, but rather to use this example in an entertaining way to make some important points, metaphorical though they might be to process of iteration within DBR:

  • The process of iteration, though a seemingly simple idea, leads to insights of otherwise hidden patterns or deep structures for how to revise the design of a learning intervention.
  • The process of iteration can bring order and understanding to the otherwise complex activity of designing for learning.
  • Despite the apparent sense of order, there is still an infinite range of possible outcomes for our designs. That is, the experience of any one person when they use one's design will be unique for that person. 
I could probably come up with lots of more metaphorical lessons given that I never "met-a-phor I didn't like," but like all metaphors, there are limits before the metaphor breaks down. Obviously, iteration within DBR is not a mathematical function, but I hoped this example would pique the audience's curiosity and interest in the principles above.

Building the Project in LiveCode

This is actually a project I built about 30 years ago using HyperCard, the ancestor of LiveCode. My original HyperCard program harnessed the graphics tools to actually draw dots on the screen. That approach still works in LiveCode, but it is frustratingly slow. So, I redesigned the approach to copy and paste the graphical object of a black dot within a loop that follows the rules above.

Here's the LiveCode interface:

On another card titled "resources" I created two graphic objects for the two black dots used in the program (medium and small) using the oval graphic tool:

The three red dots are also drawn with the oval graphic tool and are titled "triangle1," "triangle2," and "triangle3". The "Start" button contains the code to draw the fractal:
1:  global gp,gx,gy,bx,beginy,x,y,  
2:  global varLoops, varPointName  
4:  on mouseUp  
5:    put 0 into field "counter"  
6:    put field "loops" into varLoops  
9:    wait 1 second  
11:    //Game Board  
12:    --choose first "starting point" at random   
13:    put the random of 1000 into x   
14:    put the random of 500 into y   
15:    copy graphic varPointName on card "resources" to this card  
16:    hide it  
18:    set the location of it to x,y  
19:    show it  
20:    put x into gx   
21:    put y into gy   
23:    //Play the Game  
24:    Repeat with i = 1 to varLoops  
25:     if i < 101 then  
26:       put i into field "counter"  
27:     else  
28:       if (i/100)-trunc(i/100)=0 then put i into field "counter"  
30:     end if  
32:     --choose one of the three "game points" at random   
33:     put the random of 3 into gp   
34:     if gp = 2 then   
35:       put item 1 of the location of graphic "triangle2" into bx  
36:       put item 2 of the location of graphic "triangle2" into beginy  
37:     end if   
39:     if gp = 1 then   
40:       put item 1 of the location of graphic "triangle1" into bx  
41:       put item 2 of the location of graphic "triangle1" into beginy  
42:     end if   
44:     if gp = 3 then   
45:       put item 1 of the location of graphic "triangle3" into bx  
46:       put item 2 of the location of graphic "triangle3" into beginy  
48:     end if   
50:     --draw the midpoint of the "starting point" and "game point"   
51:     put (bx+gx)/2 into x   
52:     put (beginy+gy)/2 into y   
53:     put round (x) into x   
54:     put round (y) into y   
55:     copy graphic varPointName on card "resources" to this card  
56:     set the name of it to varPointName&i  
57:     set the location of it to x,y  
59:     wait 1 ticks  
60:     --make the midpoint the new "start point" and repeat   
61:     put x into gx   
62:     put y into gy   
64:    end repeat   
66:  end mouseUp  

Here are brief explanations of key lines of code:

Lines 6 takes the number the user enters and stores this in the variable "varLoops." This determines how many dots will drawn on the screen.

Lines 11-21 draw the first black dot at a random point on the screen. The coordinates of this dot's location are stored in the local variables gx and gy.

Lines 24-64 is the repeating loop that does all of the work.

Lines 24-28 count up the number of dots drawn so far and displays this on the screen. The act of updating the field with this number can slow the whole program down considerably, so I figured out some code to only show the numbers 1-100, then increments of 100 thereafter.

Line 33 chooses a number from 1 to 3 at random. Lines 34-48 takes this random number and chooses one of the three red dots accordingly.

Lines 51-57 figure out the midpoint of the randomly chosen red dot and the last black dot and put a new black dot at that location. Line 55 copies and pastes the black dot from the card "resources" to this card, then moves it to the coordinates of the midpoint. Notice, by the way, that line 56 sets the name of this newly copied and pasted graphic object as "varPointName&i". The local variable i is chosen at the start of the repeating loop in line 24 as the counter for the loop. The "&" is the concatenation symbol which is used to "join" two sets of characters. So, the 56th dot copied and pasted will have the name "varPointName56." This is important because the "Erase" button will use these names to erase each dot in sequence.

Line 59 slows down the drawing of the dots just a little. A "tick" simply 1/60 of a second. I wanted the fractal to "reveal itself" quickly, but not too quickly, so as to keep the audience wondering what was being revealed. Slowing down the drawing of the dots further might be a good idea, depending on the audience and context.

Notice that lines 35-36, 40-41, and 45-46 refer to the location of each of the three vertices. I made each vertex "grabbable" so that I could move each anywhere I wanted on the screen. Here is the simple code embedded in each red dot that makes this possible:

on mousedown
   grab me
end mousedown

Finally, the "Erase" button simply deletes all of the graphics objects containing each of the dots. Here is the code for the button "Erase:"

1:  global varLoops, varPointName  
3:  on mouseup  
4:    put 0 into field "counter"  
6:    repeat with i = 1 to varLoops  
7:     put varPointName&i into varName  
8:     delete graphic varName  
9:    end repeat  
11:    delete graphic varPointName  
13:  end mouseup  

Lines 6-9 repeat for as many dots were drawn on the screen, except for the very first dot. Each is erased at line 8. Recall that i is the counter for the repeat statement, so if there are 100 dots, dot 56 will get erased on the 56th loop. The dot at that loop would be named "varPointName56" at that point in line 7, which is then stored in the local variable varName. Line 8 commits the deed and deletes that graphical dot. What makes the resulting animation mesmerizing, I think, is that the dots disappear in the same order in which they first appeared.

Final Thoughts

There are lots of possible "next projects" using the above code as a foundation. What would appear if I use 4, 5, or 6 vertices instead of 3? I'm sure the answer is already known and published somewhere by a mathematician and all I have to do is Google it. But, it is fun to "play mathematician" occasionally to find out for myself. I would also like to explore further the idea of using the polygon tool to create this figure. Then, I could play "connect the dots," perhaps with a random color for each line segment. That sounds like a nice blending of mathematics and art.

I end with thanking the students and faculty of the Learning Sciences and Mathematics and Science Education doctoral programs at Boğaziçi University in Turkey for inviting me to share some thoughts about design-based research. It gave me a good excuse to do some computer programming in LiveCode for fun.

Saturday, April 16, 2022

I Made a Wordle Cheating Program (by Mistake)

I recently was a guest speaker in an educational game design class at Old Dominion University. In preparation, I thought I'd check out Wordle given that it has been very popular recently. The "official" Wordle site is at the New York Times and it only provides a new word once a day. However, here is a good online version where you can play unlimited games.

The game's simplicity intrigued me - the graphic design just involves letters and simple colored boxes with a very simple user interface. It's quite a contrast when compared to other popular online games involving 3D graphics and virtual worlds. According to Wikipedia, Wordle was developed by Josh Wardle who eventually sold the game to the New York Times for "an undisclosed seven-figure sum." Hmm, I said to myself. I once created a word game called "Crack the Code." Maybe I have a seven-figure sum game I need to release to the world. Heck, I'd settle for a six-figure sum sale. As it turns out, I did "release this to the world" back in 2013 in a blog post. Unfortunately, my game is not much fun to play. How do I know? Well, when I asked people to play it and asked them how they liked it, they replied "Sorry, Lloyd, it's just not very fun to play." Pretty solid evaluation data there. The question of "What makes a game fun to play?" is actually a very interesting and profound question. So, it's worth thinking about why Wordle is so captivating.

Interestingly, even though I have much interest in games, I do not identify myself as a "gamer," mainly because I'm legitimately afraid of getting addicted and I just don't have time for that. Well, after playing Wordle for a short time, I was hooked. As I played it, it was clear to me that the first and second words you enter at the start of the game are very important. It turns out there are lots of web sites and videos that propose the best words to use. I've settled on "audio" as my favorite first word given that it contains so many vowels. Other favorites are "radio" and "audit." Here's an interesting video that provides some interesting statistics and advice for choosing a first word - bottomline: don't pick "fuzzy."

As I continued to play the game, I began to wonder how to determine the best way for myself to choose the first and second words. Given that the game is based on text processing, I thought this would be a perfect LiveCode project. So, last Sunday afternoon, I spent about two hours to quickly produce such a LiveCode program. A day or two later I thought this would be a good program to share with others on this blog, so I spent another two hours or so revising the user interface so others could use it. (As is typical, the time spent on the user design interface takes as long - and usually longer - than the programming for the software itself.)

Here's a screen shot of the interface:

I'll explain how this works in a moment, but the important point I want to make is my sole purpose in creating this app was only to gather statistics on the best words to choose at the beginning of the game. However, it turns out that my app also is the perfect tool to cheat at Wordle! The reason is that my program continues to whittle down the possible word matches as you go. Consequently, I can pretty much just choose a word at random at each turn of the game and win. As someone quickly asked me when I told them about this, doesn't that take the fun out of the game? The answer is yes. Of course, for me, I continue to find it quite exhilarating to see my little program chug along to beat the game.

Obviously, one needs a good list of five letter words from which to search for good and bad matches. I used the word list available at this site:

However, I really need to find a more complete word list because after using my app to play Wordle, I found my list was missing these common words: torte, motel, moron, spank.

Ok, here's a short video of me using the program as I played a round of Wordle.

The main code that makes this all work can be found in the "Search Word List" button. Here's the code:

1:  on mouseup  
2:    put empty into field "matches"  
4:    put the number of lines in field "word list" on card "resources" into L  
6:    repeat with i = 1 to L //word list loop  
7:     put false into varSkipWord  
8:     put line i of field "word list" on card "resources" into varWord  
10:     //Check first for matched letters in correct spot  
11:     put line 1 of field "first" into varFirstMatch  
12:     if field "first" is not empty then  
13:       if char 1 of varWord <> varFirstMatch then next repeat  
14:     end if  
15:     put line 1 of field "second" into varSecondMatch  
16:     if field "second" is not empty then  
17:       if char 2 of varWord <> varSecondMatch then next repeat  
18:     end if  
19:     put line 1 of field "third" into varThirdMatch  
20:     if field "third" is not empty then  
21:       if char 3 of varWord <> varThirdMatch then next repeat  
22:     end if  
23:     put line 1 of field "fourth" into varFourthMatch  
24:     if field "fourth" is not empty then  
25:       if char 4 of varWord <> varFourthMatch then next repeat  
26:     end if  
27:     put line 1 of field "fifth" into varFifthMatch  
28:     if field "fifth" is not empty then  
29:       if char 5 of varWord <> varFifthMatch then next repeat  
30:     end if  
32:     //Second, check for letters to exclude  
33:     put the number of lines in field "exclude" into M  
34:     repeat with j = 1 to M //use excluded letters loop  
35:       put line j of field "exclude" into varLetterToExclude  
36:       if varWord contains varLetterToExclude then   
37:        put true into varSkipWord  
38:        exit repeat //no need to keep looking, just exit out  
39:       end if  
40:     end repeat //END use excluded letters loop  
42:     //Third, check for letters to include  
43:     put the number of lines in field "include" into N  
44:     repeat with k=1 to N //use included letters loop  
45:       put line k of field "include" into varLetterToInclude  
46:       if varLetterToInclude is not in varWord then  
47:        put true into varSkipWord  
48:        exit repeat //no need to keep looking  
49:       end if  
50:     end repeat //END use included letters loop  
52:     //Fourth and last, check for matched letters but not yet in the right spot  
53:     put line 1 of field "notfirst" into varNotFirstMatch  
54:     if field "notfirst" is not empty then  
55:       if char 1 of varWord = varNotFirstMatch then next repeat  
56:     end if  
57:     put line 1 of field "notsecond" into varNotSecondMatch  
58:     if field "notsecond" is not empty then  
59:       if char 2 of varWord = varNotSecondMatch then next repeat  
60:     end if  
61:     put line 1 of field "notthird" into varNotThirdMatch  
62:     if field "notthird" is not empty then  
63:       if char 3 of varWord = varNotThirdMatch then next repeat  
64:     end if  
65:     put line 1 of field "notfourth" into varNotFourthMatch  
66:     if field "notfourth" is not empty then  
67:       if char 4 of varWord = varNotFourthMatch then next repeat  
68:     end if  
69:     put line 1 of field "notfifth" into varNotFifthMatch  
70:     if field "notfifth" is not empty then  
71:       if char 5 of varWord = varNotFifthMatch then next repeat  
72:     end if  
74:     if varSkipWord is false then put varWord&return after field "matches"  
75:    end repeat //END word list loop  
77:    put the number of lines in field "matches"&space&"matches" into field "matches label"  
79:    //Compute Statistics  
80:    put the number of lines in field "matches" into varMatchesCount  
81:    put line 1 of field "guess" into varGuess  
82:    put the number of lines in field "matches" into varNumerator  
83:    put the number of lines in field "word list" on card "resources"into varDenominator  
84:    put varGuess&comma&varMatchesCount&"("&round((varNumerator/varDenominator) * 100,3)&"%"&")"&return after field "statistics"  
86:  end mouseup  

I put a comment at the start of each important block of code. The key variable is varSkipWord. This variable will determine whether or not each word in the list of over 2500 words should be skipped. If skipped, the word does not match any still available word to try in the next guess. In line 7, I set this to false, meaning that unless something tells the computer otherwise, keep the word as a possible match - that is, don't skip it. At various places in the code, you'll see the code "put true into varSkipWord," such as in line 37. If varSkipWord remains false by the time the code reaches line 74, then the word is added to field "matches," which shows all of the words that are viable possibilities for the next guess.

In conclusion, this little project was perfect for LiveCode. Too bad I ruined my own Wordle game experience. I hope I don't ruin it for you.

Sunday, December 20, 2020

My Toothbrush Time App: Nearing Completion

I solved the problem I mentioned in my last post about the interval timer not working properly when the app is paused in the middle of a timing session. I completely overhauled the programming for this and the app's been working fine since. I barely remember how the original approach worked. I remember I used some (in hindsight) whacky system where I was subtracting interval seconds from the total seconds elapsed plus the amount of time paused, or something like that. It was yet another example of me programming by the seat of my pants without thinking it through ahead of time. But honestly, that's the way I like to program. I have a firm goal in mind and I just "have at it" until I reach the goal. I find this an exhilarating quest. Usually things work out fine, but occasionally - as in this case - they don't. A reasonable trade-off as far as I'm concerned.

The new approach is a simple one that occurred to me on a long walk as I was pondering the problem. It uses the the time on the clock as the starting point for figuring out when each new interval should end. So, if the next next interval is 20 seconds long and it begins when the clock reads 0:30, obviously the alert should sound at the 0:50 mark. If the person pauses the timer, the display is frozen until the Restart button is pressed. I still had to do some transposing of time based on the theSeconds function, but the logic was sound.

I had also mentioned in my previous post that the vibrate alert option seemed to really throw things off. It turned out that an extra second was added to the current interval every time the vibrate option was used. Given that the pattern of adding one and only one extra second never changed, I decided to just add the hack of subtracting a second when the vibrate option was used. I accomplished this with a variable I created named varIntervalFudge (in my mind fudge = hack).

Yeah, programming a hack rather than tracing down the actual problem is the lazy way to do things. But hey, I'm a busy guy.

Here is a review of the programming engine for this app. MainEventLoop, as its name suggests, is the main programming loop that allows the timer to run while the user is making choices, such as starting, stopping, and resetting the timer:

1:  on MainEventLoop  
2:    if varRunMainLoop is true then  
3:     lock screen  
4:     LloydStopWatch  
5:     unlock screen  
6:    end if  
7:    put the pendingMessages into tMessages   
8:    repeat for each line aLine in tMessages   
9:     if item 3 of aLine is "mainEventLoop" then cancel item 1 of aLine   
10:    end repeat    
11:    send "mainEventLoop" to me in 50 milliseconds  
12:  end MainEventLoop  

Line 4 triggers the procedure LloydStopWatch to run every 50 ms. The LloydStopWatch procedure contains the programming that operates the timer and checks on the intervals:

1:  on LloydStopWatch    
2:    put ((varIminutes1*60) + varIseconds1 - varIntervalFudge) into varInterval1  
3:    put ((varIminutes2*60) + varIseconds2 - varIntervalFudge) into varInterval2  
4:    put ((varIminutes3*60) + varIseconds3 - varIntervalFudge) into varInterval3  
5:    put ((varIminutes4*60) + varIseconds4 - varIntervalFudge) into varInterval4  
6:    if varIntervalTurn =1 then put varInterval1 into varIntervalCurrent  
7:    if varIntervalTurn =2 then put varInterval2 into varIntervalCurrent  
8:    if varIntervalTurn =3 then put varInterval3 into varIntervalCurrent  
9:    if varIntervalTurn =4 then put varInterval4 into varIntervalCurrent  
11:    put the seconds into varSecondsEnd    
12:    put (varSecondsEnd - varSecondsBegin) + varTimeElapsedPrevious into varTimeElapsed //Modified December 6, 2020  
13:    put varSecondsEnd - varSecondsBeginInterval into varTimeElapsedInterval  
15:    //Display the time elapsed  
16:    put trunc(varTimeElapsed/60) into varDisplayMinutes  
17:    put (varTimeElapsed)-(varDisplayMinutes*60) into varDisplaySeconds  
18:    if the length of varDisplaySeconds < 2 then put "0"&varDisplaySeconds into varDisplaySeconds  
19:    put varDisplayMinutes&":"&varDisplaySeconds into line 1 of field "my time"  
21:    if varIntervalCurrent = 0 then exit LloydStopWatch //Only when happens when all internal fields are 0  
22:    if varIntervalTurn = 1 and varIntervalCheck = true then //December 6, 2020  
23:     put varInterval1 + varTimeElapsed into varTimeOfNextAlert  
24:    end if  
25:    if varIntervalTurn = 2 and varIntervalCheck = true then //December 6, 2020  
26:     put varInterval2 + varTimeElapsed into varTimeOfNextAlert  
27:    end if  
28:    if varIntervalTurn = 3 and varIntervalCheck = true then //December 6, 2020  
29:     put varInterval3 + varTimeElapsed into varTimeOfNextAlert  
30:    end if  
31:    if varIntervalTurn = 4 and varIntervalCheck = true then //December 6, 2020  
32:     put varInterval4 + varTimeElapsed into varTimeOfNextAlert  
33:    end if  
34:    put false into varIntervalCheck  
36:    if varTimeElapsed = varTimeOfNextAlert then  
37:     if varIntervalTurn = 1 and varChime1 <> "vibrate" then play specialFolderPath("engine")&varChime1  
38:     if varIntervalTurn = 1 and varChime1 = "vibrate" then mobileVibrate 2  
39:     if varIntervalTurn = 2 and varChime2 <> "vibrate" then play specialFolderPath("engine")&varChime2  
40:     if varIntervalTurn = 2 and varChime2 = "vibrate" then mobileVibrate 2  
41:     if varIntervalTurn = 3 and varChime3 <> "vibrate" then play specialFolderPath("engine")&varChime3  
42:     if varIntervalTurn = 3 and varChime3 = "vibrate" then mobileVibrate 2  
43:     if varIntervalTurn = 4 and varChime4 <> "vibrate" then play specialFolderPath("engine")&varChime4  
44:     if varIntervalTurn = 4 and varChime4 = "vibrate" then mobileVibrate 2  
46:     //Hack when vibrate alert is used - December 6, 2020  
47:     //Reduce the next interval time by 1 second  
48:     put 0 into varIntervalFudge  
49:     if varIntervalTurn = 1 and varChime1 = "vibrate" then put 1 into varIntervalFudge  
50:     if varIntervalTurn = 2 and varChime2 = "vibrate" then put 1 into varIntervalFudge  
51:     if varIntervalTurn = 3 and varChime3 = "vibrate" then put 1 into varIntervalFudge  
52:     if varIntervalTurn = 4 and varChime4 = "vibrate" then put 1 into varIntervalFudge  
54:     add 1 to varIntervalTurn  
55:     //Next 3 lines account for when either interval 2 or 3 are empty; if interval 2 is empty, then code assumes interval 3 is empty (even if isn't)  
56:     if varInterval2 = 0 then put 1 into varIntervalTurn  
57:     if varIntervalTurn = 3 and varInterval3 = 0 then put 1 into varIntervalTurn  
58:     if varIntervalTurn = 4 and varInterval4 = 0 then put 1 into varIntervalTurn  
59:     if varIntervalTurn > 4 then put 1 into varIntervalTurn     
61:     put true into varIntervalCheck  
62:    end if  
64:  end LloydStopWatch   

Note the use of the variable varIntervalFudge when accounting for the vibrate alert.

Option to Save the Current Settings

One nice little feature I added was giving the user the option to save the current settings. This is really helpful if you have a main use for the app, such as teeth brushing, and want to revert to those settings after using the app to do other things. The solution to this is similar to the idea of a "cookie." The idea is to save a small data file on the iPhone that can be retrieved later. Here's a link to a good tutorial on how to set up something like this:

Here is the code for saving the current settings located in the button "Save current settings":

1:  on mousedown  
2:    put empty into field "appdata" on card "data"  
3:    Put "Interval Data" into line 1 of field "appdata" on card "data"  
4:    put "Interval 1,"&field "IMinutes1"&comma&field "ISeconds1"&comma&field "chime1" into line 2 of field "appdata" on card "data"  
5:    put "Interval 2,"&field "IMinutes2"&comma&field "ISeconds2"&comma&field "chime2" into line 3 of field "appdata" on card "data"  
6:    put "Interval 3,"&field "IMinutes3"&comma&field "ISeconds3"&comma&field "chime3" into line 4 of field "appdata" on card "data"  
7:    put "Interval 4,"&field "IMinutes4"&comma&field "ISeconds4"&comma&field "chime4" into line 5 of field "appdata" on card "data"  
9:    //Activate the following line to actually save the data  
10:    put field "appdata" on card "data" into URL ("file:data.txt")  
11:    wait 1 second  
12:    show image "checkmark"  
13:    wait 1 second  
14:    hide image "checkmark"  
15:  end mousedown  

Lines 4-7 put the data for each of the four intervals into the text field "appdata" on card "data."

Line 10 saves everything in field "appdata" into a text file named "data.txt"

I decided to give the user feedback that the file was saved by displaying a very small graphic checkmark. It looks better if there is a pause for one second before it is displayed. I then hide the checkmark after another second.

Here is the code in the button "Revert to saved settings" that retrieves the settings data:

1:  on mousedown  
3:    put empty into field "appdata" on card "data"  
4:    set the defaultFolder to specialFolderPath("Documents")  
5:    put URL ("file:data.txt") into field "appdata" on card "data"  
7:    if field "appdata" on card "data" is empty then  
8:     put "No saved settings" into field "notes"  
9:     put "0" into field ISeconds1  
10:     put "0" into field ISeconds2  
11:     put "0" into field ISeconds3  
12:     put "0" into field ISeconds4  
13:     put "0" into field IMinutes1  
14:     put "0" into field IMinutes2  
15:     put "0" into field IMinutes3  
16:     put "0" into field IMinutes4  
17:     put "Chimes" into field "chime1"  
18:     put "Chimes" into field "chime2"  
19:     put "Chimes" into field "chime3"  
20:     put "Chimes" into field "chime4"  
21:     put "/sounds for toothbrush app/chimes.wav" into varChime1  
22:     put "/sounds for toothbrush app/chimes.wav" into varChime2  
23:     put "/sounds for toothbrush app/chimes.wav" into varChime3  
24:     put "/sounds for toothbrush app/chimes.wav" into varChime4  
25:    else  
26:     put item 2 of line 2 of field "appdata" on card "data" into field IMinutes1  
27:     put item 3 of line 2 of field "appdata" on card "data" into field ISeconds1  
28:     put item 4 of line 2 of field "appdata" on card "data" into field "Chime1"  
30:     put item 2 of line 3 of field "appdata" on card "data" into field IMinutes2  
31:     put item 3 of line 3 of field "appdata" on card "data" into field ISeconds2  
32:     put item 4 of line 3 of field "appdata" on card "data" into field "Chime2"  
34:     put item 2 of line 4 of field "appdata" on card "data" into field IMinutes3  
35:     put item 3 of line 4 of field "appdata" on card "data" into field ISeconds3  
36:     put item 4 of line 4 of field "appdata" on card "data" into field "Chime3"  
38:     put item 2 of line 5 of field "appdata" on card "data" into field IMinutes4  
39:     put item 3 of line 5 of field "appdata" on card "data" into field ISeconds4  
40:     put item 4 of line 5 of field "appdata" on card "data" into field "Chime4"  
42:     repeat with i = 1 to 4  
43:       put "chime"&i into varI  
44:       if field varI is "Vibrate" then put "vibrate" into varChimeLocal  
45:       if field varI is "Applause" then put "/sounds for toothbrush app/applause.wav" into varChimeLocal  
46:       if field varI is "Bell" then put "/sounds for toothbrush app/bell.wav" into varChimeLocal  
47:       if field varI is "Bicycle Bell" then put "/sounds for toothbrush app/bicycle_bell.wav" into varChimeLocal  
48:       if field varI is "Chimes" then put "/sounds for toothbrush app/chimes.wav" into varChimeLocal  
49:       if field varI is "Ding" then put "/sounds for toothbrush app/ding.wav" into varChimeLocal  
50:       if field varI is "Laser" then put "/sounds for toothbrush app/laser_x.wav" into varChimeLocal  
51:       if field varI is "Oops" then put "/sounds for toothbrush app/oops.wav" into varChimeLocal  
52:       if field varI is "Slide Whistle" then put "/sounds for toothbrush app/slide_whistle_up.wav" into varChimeLocal  
53:       if field varI is "Tada" then put "/sounds for toothbrush app/tada2.wav" into varChimeLocal  
54:       if field varI is "Trumpet" then put "/sounds for toothbrush app/trumpet_x.wav" into varChimeLocal  
56:       if i = 1 then put varChimeLocal into varChime1  
57:       if i = 2 then put varChimeLocal into varChime2  
58:       if i = 3 then put varChimeLocal into varChime3  
59:       if i = 4 then put varChimeLocal into varChime4  
61:     end repeat  
62:    end if  
64:  end mousedown  

Yeah, there is quite a bit to this script, but that's because I had to take into account the chance that no settings had yet been saved. Line 5 is the key line. It takes whatever data are saved and puts it back into the text field "appdata" on card "data." From there, I can set all of the intervals to the saved settings properly.

Playing Alerts When the iPhone is Locked

Unlike tooth brushing, many other tasks may take quite a long time. The example that comes to mind is giving a 20 or 30 minute presentation where I'd like to have (most likely vibrate) alerts with about three or five minutes to go, then with a minute left, and then a final alert with 15 seconds to go so I can get off the stage for the next person. It is easy to see that the iPhone will likely automatically lock during task like this. I have my iPhone auto-lock after five minutes if not used. I want to be assured that the alerts will come whether the phone is locked or not. Unfortunately, right now although the timer keeps working, the alerts never sound.

I did a search on the LiveCode forums about this and found what should be the solution, the command iphoneSetAudioCategory. Here is a good web page with information about this command:

The problem should be solved just by adding the command iphoneSetAudioCategory "playback" to my code. Unfortunately, it isn't working.  I'll continue playing around with it. If I don't have any luck soon, I'll reach out to the LiveCode community in the forums.

Creating an Home Icon for the App

Despite my lack of graphic design skills - something I've written about frequently in this blog - I still enjoy the challenge of graphic design. Here is the design I've come up with for the app's icon on the iPhone home screen:

Though trite, a clock face is obviously synonymous with time. The four different colored squares give the clock face a distinctive look while also symbolizing the four opportunities to set four different times with unique alert sounds. I created this icon - and I'm almost embarrassed to admit it - in PowerPoint. Of all of the graphic tools out there, I find those in PowerPoint to be the most intuitive and simple to use. It's then easy to copy and paste the graphical object into Photoshop for exporting as a PNG file. My motto is always to use the tools that work for me and get the job done.

I also started toying around with some better names for this app other than "Lloyd's Toothbrush Timer App." Brushing my teeth was just the inspiration for the need for this app after all. I have a very bad track record of naming my apps, hence the names "Lloyd's Video Analysis Tool" and "Lloyd's Q Sort Tool." So, yes, I may just wind up calling this something like "Lloyd's Interval Timer App."

But, in an effort to force some extra drops of creativity, I thought I might name this the "This, Then That Timer." I played around with the alliteration of the four Ts in an earlier version of the app's icon, but none of those looked very good. Still, I think this name has potential. Of course, the boring - but descriptive - name of "Multiple Interval Timer" might win the day.

Just a Few Remaining Tasks

Very little is left on my punch list for this app other than solving the auto-lock issue just described. Here is the short list of the remaining tasks:

  • Continue to improve the graphical interface.

  • Improve the timer display to also include the number of seconds until when the next alert will be sounded. A "countdown" displays like this seems like a good idea to me.

So, look for at least one more blog post about this project.

Saturday, December 12, 2020

My Toothbrush Timer App: On My iPhone and Working (Well Enough for Now)

I've made much progress on my app in the past week. In fact, for the past few days I've been brushing my teeth with the help of my iPhone sitting on the edge of the bathroom sink running my app. It works great, just like I had hoped. I can "set it, start it, then forget it" as I start brushing. 

I like to set it to provide some gentle chimes after 50 seconds to let me know the first minute is just about up. Ten seconds later, at the one minute mark, I set it to ring a soft bell to let me it is time to switch "levels" of brushing from bottom to top. Chimes again alert me 50 seconds later to let me know that I have just 10 more to brush. It's actually quite satisfying to hear the final "tada!" alert just as the toothbrush automatically turns itself off. I also find it great for my one minute of mouthwash swishing immediately after brushing. 

Of course, with progress comes many lessons learned. This post contains a few highlights of what I learned over the past week.

Trouble Getting Any Sounds to Play on the iPhone Simulator or the Real iPhone

For a few days, I was baffled by the fact that although the app would play the sound alerts on my Macintosh during development, they would not play on either the iPhone simulator or later when I figured out how to transfer the app to my iPhone. At first, I just figured it was a problem with the simulator, so I ignored the problem. But, when the sounds failed to play on the iPhone itself, I knew something was wrong. I had been consulting this LiveCode tutorial:

The problem turned out to be that I had failed to provide the full path to the sounds. I had put the sounds in a folder on my Macintosh called "Sounds for Toothbrush App." This folder is at the same level as the app. I used the "copy files" option in the standalone settings, just as the tutorial described. I didn't realize that the path shown in that window had to match the code exactly. Here's a screenshot of the copy files window:

Here was the code I was using:

play specialFolderPath("engine") & "/bell.wav"

The sounds only started to play once I changed the code to the following:

play specialFolderPath("engine") & "/sounds for toothbrush app/bell.wav"

Note: The tutorial uses "resource" instead of "engine" for the code, but I had used "engine" in a previous app I made back in 2013, so I switched to it instead. Not sure if it makes a difference.

Testing the App on an Actual iPhone

I had been dreading this part because I knew it would require me to crawl back into the very frustrating world of the Apple Development Center. I vaguely recall needing all sorts of certificates and something called a provisioning profile. I also recall having to tell Apple the identifying information for the iPhone I wanted to test the app out on. I figured this was going to be a dark day.

Fortunately, I kept good notes the last time I created an iPhone app back in 2013. I found these notes on my Macintosh, dusted them off (metaphorically speaking), and began to follow them blindly. Lo and behold, they pretty much guided me through the entire process. There was one trouble spot. In my old notes (which, of course, were new at the time I wrote them), I got to a point where I had written "Not sure what to do next! So, I check my old notes..." Remember, these "old notes" were old back in 2013. So, I had to figure all that out. But, I updated all of these notes as I went. Here is a link to those updated notes - maybe someone out there in the blogosphere will find them useful. (Beware: This are written only well enough for me to fully understand. All bets are off if you will find them useful.) But, really, I figure I'll be the one needing these notes again when I likely make my next iPhone app seven or eight years from now.

Interestingly, I first tried getting my app to work on an old iPhone SE running an up-to-date iOS, but I had no luck. I then tried to get the app on my iPhone 6S and it worked fine. I think the problem has to do with the age of the phone and the version of Xcode I'm using. 

Updating the App's Interface

I also took some time to update the graphical design of the app. My goal was just to reduce some of its ugliness. Here's a screenshot of the current app:

Current Problems to Solve

After much testing, it's clear that the interval timer is not working properly when the app is paused in the middle of a timing session. I had mentioned this problem in an earlier blog post. At that time, I thought the problem was minor - I called is likely a "rounding error" problem. But, it's much more serious than that. Also, when I use the vibrate alert instead of an audible sound, the timer really gets screwed up. So, I realize I have to completely overhaul the programming of the intervals. I have some ideas of how to go about this, but I'll save that for a future post.

For now, I at least have an app on my iPhone that helps me brush my teeth very evenly. 

Wednesday, November 25, 2020

My Toothbrush Timer App: Early Struggles in Going Mobile

LiveCode is an amazing computer programming language and software development environment. The apps you make with it can be run on Macintosh computers, Windows computers, Apple mobile (iOS via iPhone or iPad), and Android. The only thing it hasn't yet figured out completely is online web apps, though the promise of LiveCode's ability to export to HTML5 remains very exciting (though mostly yet unfulfilled).

With that opening lovefest of a paragraph behind me, I now turn to some frustrations. To be fair, the frustrations aren't really with LiveCode, but mainly with Apple and how the Apple Computer Company makes compliance with its software application standards a moving target. 

The first step in taking my toothbrush interval timer to a mobile platform, specifically the iPhone, is to test the app in Apple's iPhone simulator, which is a part of the Xcode development software. To get that to work, one has to have all of the "stars" aligned, with the stars in this case being a metaphor for using the right version of LiveCode with the right version of Xcode and with the right version of MacOS on my laptop. In the end, I did get it all to work, but only after many hours spent downloading different versions of LiveCode, different versions of Xcode, and finally biting the bullet and updating my MacOS. And even then, I had to resort to searching the LiveCode user forums for a last bit of advice to make it all work. I detail my journey here with the hope that it might help others (or most likely just me in a few months time because I will have forgotten all of this by then).

The Challenging in Getting "Everyone to Play Nice" Together

In the end, here are the system and software specifications that are "playing together" well:

  • MacOS version: 10.15.2
  • Xcode version: 11.4
  • LiveCode version: 9.6.0

But, they didn't at first. 

Let's start at the beginning. I had not made a mobile app since I made my little game Lunar Hotel Shuttle, the inaugural project for this blog. This app first appeared in the app store on November 28, 2012. Yes, that's almost exactly eight years ago, so yes, it's been a long time since I made a mobile app. Due to a very faded memory of that experience I knew that all of my software had to be aligned to work, so I crossed my fingers as I clicked the "test" button in LiveCode after choosing iOS as my standalone application setting. If it all worked, then just clicking the "test" button would automatically launch Xcode's simulator followed by LiveCode packaging and bundling the app for mobile, then exporting the app to the simulator for testing. But, did I mention this all only happens if everything is aligned properly? Right, sorry to repeat myself. Not surprisingly, it didn't work.

No worries, I thought. I quickly found this handy dandy reference page on the LiveCode web site showing all of the different configurations one could use:

My Mac's OS was Catalina 10.15.1 and my version of Xcode was 11.1.1. So, I downloaded LiveCode 9.6.0 DP-3 since that was the version of LiveCode that seemed to align with the MacOS and Xcode. That's when the frustration really began. I began exploring one rabbit hole after another, though I did learn a lot along the way. LiveCode 9.6.0 DP-3 could not be "verified" by Apple. After some Googling, I learned a way to bypass Mac's verification process:

LiveCode opened, but I still had no luck getting the iPhone simulator to work. So, I went ahead and downloaded the LiveCode 9.6.0 (this is the "stable" version, not the "developer preview" version). Alas, I was greeted with a message from LiveCode (that kept popping up dozens of times) that I had to upgrade my MacOS to 10.15.2 for my version of Xcode to work. I am always loathe to update my operating system given some very bad past experiences. But, I decided to do it anyway. Well, the only option my Macintosh's update button was giving me was a major upgrade to MacOS Big Sur and no way was I going to do that. So, after a little more Googling, I found Apple's MacOS download page and did the upgrade. Remember, all of these software downloads and installations take time, particularly given my unusually slow Internet speed here in rural Georgia. Plus, I do have a day job, so I can only work on this other "hobby" periodically throughout the day. So, the hours are ticking by. But, as I finished, my confidence now was very high for my next test.

And Again It Didn't Work

At this point, if I had rum in the house I would have started drinking. But, I need to back up a little bit because it almost worked, and more importantly, this was not the first time it had almost worked. That is, the iPhone simulator did launch and LiveCode was giving me messages that it was dutifully creating all of the needed "bundles." But the toothpaste timer app just never appeared on the simulated iPhone. Within about 10 seconds or so, a message popped up from LiveCode asking me if I wanted to keep trying given how long it was taking for the app to load. I pressed "yes" and the message popped up again about 10 seconds later. I eventually said no. When I got this far in some of my previous testing, I just assumed that the stars were still not aligned. That, it turns out, was a faulty assumption. It was at this point I turned to the LiveCode forums and the very helpful community of LiveCode programmers.

LiveCode Forums to the Rescue

I quick found this forum thread in the "iOS Deployment" topic area:

A LiveCode developer named Panos offered the advice of opening the simulator first in Xcode and to wait until the simulated iPhone shows the home screen. Then, go back to LiveCode and try to test the app. Well, that worked! Here are the step to do just that:

  • Right-click on the Xcode app icon in the Macintosh's dock.
  • Choose: "Open Developer tool > Simulator"
  • Make sure the simulated iPhone boots all the way to the home screen.
  • Then, and only then, choose to "test" your app on the simulator from within LiveCode.

Here's a screen shot:

Yeah, pretty ugly, but the most important thing is that the app export and open successfully in the simulator.

A key point to be made and emphasized here is all of the right software was aligned the first time I saw the simulator launch. It was just that the simulated phone wasn't "turned on" yet. Maybe if I kept clicking "yes" to all of those message it would eventually have worked, but who knows. I'm just happy I now know what to do in order to move on.

Time, Finally, to Begin Revising the App for Mobile

Yes. I am finally ready to start the work to revise my app for the mobile platform.

I'll end by including some links to two other useful web pages:

A final good "lesson learned" worth sharing is to search the LiveCode Forums earlier in the troubleshooting process.

Monday, November 23, 2020

My Toothbrush Timer: On the Cusp of Going Mobile

A couple of days ago I had a few extra minutes in the morning to work on my app. I thought I would quickly add the option to pause the timer. This sounded simple enough and I figured I could add this feature in 15 or 20 minutes. Well, about three hours later I had it more or less figured out. You just never know where app development will take you.

The concept sounds simple enough - just stop everything with one click and restart it all with another click. But, this proved to be a more thornier challenge than it would seem at first glance. It stems from the fact that I'm using LiveCode's function the seconds as the way to keep the timer and the intervals accurate. (I explained how the seconds works in LiveCode in my previous blog posting. I recommend reading that first before continuing here.)

Here's a short explanation:

When the timer begins it saves the current time (in seconds) in a variable. For simplicity sake, let's just say that the seconds is equal to 0. The app then continually checks the current time and puts those seconds into the display. When the Stop button is pressed, the current time at that moment is saved into another variable and the timer is stopped. Of course, the seconds - time, in other words - just keeps going. When the Start button is pressed to start the timer up again I can't just restart the timer based the seconds at that moment because the time elapsed includes the time that the timer was paused. So, I have to subtract the paused time from the total time and only show the "running" time in the display. Here's the solution I programmed into the app:

0 seconds: Start the timer

50 seconds: Pause the timer. At this point, 0:50 remains on the display.

150 seconds: Start the timer back up. 150-50 is 100 seconds. So the display needs to show the following:

Current Time (150) - Original Starting Time (0) - Total Time Paused (100) = Total timer seconds to be displayed

And, every time the user pressed the Stop button, the total time paused must be increased accordingly.

All I can say is that I couldn't get it to work for about three hours. Somewhere my logic was failing me. In the end, I think the problem was that I had this algorithm sitting in the wrong place, namely inside the LloydStopWatch procedure I explained in my previous post. After I moved it and a few more tweaks, things seemed to work, more or less. I say "more or less" because there is still a small bug. Somehow one second is sometimes added to the total time elapsed. The problem is minor, but annoying. Here's what can happen. Imagine the timer with two intervals, each two seconds apart. The first interval is signaled with a bell and the second with chimes. So, the pattern is a timer going off every two seconds, first with a bell and then with chimes. So, an alert should be given only when an even number appears on the display. However, sometimes when pause is pressed, the alert sounds moves to odd numbers instead, but at two second intervals. It seems to resemble a kind of "rounding" error, but I can't find the source. So, after those three hours, I decided to just "let it go."

Displaying the Time Correctly

I made one other improvement to the app, namely showing the timer not just in seconds, but in minutes and seconds using the conventional display of Minutes:Seconds. This was a fun challenge to program. So, consider that a total of 147 seconds have gone by. What should the display read? A quick mental calculation results in 2 minutes and 27 seconds, or 02:27. That same mental calculation is shown in this code:

1:    put trunc(varTimeElapsed/60) into varDisplayMinutes  
2:    put (varTimeElapsed)-(varDisplayMinutes*60) into varDisplaySeconds  
3:    if the length of varDisplaySeconds < 2 then put "0"&varDisplaySeconds into varDisplaySeconds  
4:    put varDisplayMinutes&":"&varDisplaySeconds into line 1 of field "my time"  

In line 1, we divide the total seconds elapsed (varTimeElapsed) by 60. Dividing 147 by 60 gives you 2.45. Well, we just care first about the number of minutes. The function trunc truncates, or chops off, any decimal that remains. So, we are left with just two. That goes into the variable varDisplayMinutes.

Now we have to figure out the remaining number of seconds. All we need to do is take the total seconds elapsed (again, varTimeElapsed) and subtract from it the number minutes we just calculated.  Of course, we have to convert those minutes back into seconds first (varDisplayMinutes*60). The result is line 2: 147-120 = 27. We put the 27 into the variable varDisplaySeconds.

Lines 4 pretties everything up before displaying the time elapsed in the field "my time." Using the concatenation symbol "&" we stitch together minutes, a colon in the middle, followed by the seconds.

Adding a Fourth Interval Option and Alert Sound Options

In my last blog post, I alluded to the fact that having at least four intervals is preferred. So, this version of the app added that as well. I also had some fun finding and adding a menu of alert sounds. I downloaded several sound effects using the .wav sound format from this web site:

Here's a screen shot of the current version of the app paused after 97 seconds:

One More Important Step to Go

The app works well enough for me to brush my teeth, except for one thing. I don't want to put my laptop on the bathroom sink. So, the obvious next step is to "go mobile." This will require quite a few changes to the app's interface, such as replacing all of the text input fields with selector wheels. 

I also will need to move the app to my iPhone for testing. I am not looking forward to this. It will first require me to relearn how to create a "provisioning profile" in my Apple Developer Account using my iPhone's serial number and then relearning how to get the app from my computer to the iPhone. I've done it before and I'm sure I can do it again. But, it's been a few years and my memory of doing it isn't very pleasant.

Thursday, November 19, 2020

My Toothbrush Timer: Adding Initial Functionality

I've made progress on this app over the past few weeks, despite only being able to work on it now and then. As usual, my progress in building the app is outpacing my blogging about its construction. So, this blog post is at least the first of two in an effort to catch up. Here I give a report of the design of the app as of about two weeks ago.

Here is a screenshot of the app while running:

One obvious thing I need to work on is showing the time elapsed in the format of minutes:seconds. Using the example of brushing my teeth for 2 minutes, these intervals alert me half-way through (to switch from lower to upper teeth), then a warning 50 seconds later that I only have 10 seconds left, followed by the 2 minute bell. Really, this last alert is not necessary because the toothbrush I use just turns off automatically at 2 minutes. If I wanted to switch between lower and upper every 30 seconds, then just one interval setting of 30 would be needed. In the example of giving myself alerts while making a 12-minute conference presentation - a typical time limit - I would likely add alerts at intervals of 6 minutes (half-way point), 4 minutes (the 10-minute mark), 1 minute (just one minute remaining), and 45 seconds (to give myself 15 seconds to thank everyone and get off the stage). Of course, that would require four intervals, not three, so I would need to add one more interval to the app. (Hint hint: That's a clue about what's coming up in next blog post.)

Telling the Time in LiveCode with the Seconds

Having an accurate timer is fundamental to many applications. The best way to develop a timer in LiveCode is with the function the Seconds. My timer app starts by checking to see what the time is now in seconds. As I write this, it is 7:48:11 am on Wednesday, November 18, 2020. The idea is to make that the starting time when the Start button is pressed. Then, all timers are checking to see how much time has passed since then. So, if the first timer is set to go off 30 seconds later, the time will need to be 7:48:41. But if you think about it, this standard format for telling time is based on a past starting point using an arbitrary system we call the Julian calendar. Today's date and time uses as its starting point the likewise artibrary big "changover" from Before the Common eEa (BCE) to the Common Era (CE), which is just the secular version of BC and AD. The key word there is "arbitrary." Really, any starting point would do as long as we all recognize and accept it as the starting point. In LiveCode, the seconds is based on a similar starting point called the eon, defined in the LiveCode dictionary as follows: 
LiveCode uses midnight, January 1, 1970 as the start of the eon. Date and time functions are computed from that date.
So, the function the seconds gives you the current time in seconds since since very arbitrary starting point. As I write this sentence, the current time, in seconds, according to LiveCode is 1605704460. But now, as I write this sentence the time is 1605704492. So, if you subtract the first from the second you get 32 seconds. That interval is the basis for how my app works.

Doing Two (or More) Things at Once

The important thing to note is that while the timer is running, the app is constantly checking to see if the user presses any buttons. In contrast, many apps work by waiting until the user presses a button to perform a task. The app does the task, then waits for the user to press another button. If the task takes a long time to complete, the user has to wait until the task is completed before any other buttons can be pressed. Doing one thing at a time versus doing multiple things at the same time may sound like a subtle difference, but it requires a special programming technique to pull this off.

Here is the code for the app's engine called MainEventLoop, a procedure I wrote to run everything in the background while the user chooses timing intervals and clicks buttons to stop and stop the app's timer:

1:  on MainEventLoop  
2:    if varRunMainLoop is true then  
3:     lock screen  
4:     put the seconds into varSecondsEnd  
5:     LloydStopWatch  
6:     unlock screen  
7:    end if  
8:    put the pendingMessages into tMessages   
9:    repeat for each line aLine in tMessages   
10:     if item 3 of aLine is "mainEventLoop" then cancel item 1 of aLine   
11:    end repeat    
12:    send "mainEventLoop" to me in 50 milliseconds  
13:  end MainEventLoop  
15:  on LloydStopWatch    
      The magical code goes here
16:  end LloydStopWatch  

A procedure called MainEventLoop just runs and runs by looping forever while the app is running. This works by having it "call itself" in line 12 in a very short amount of time (i.e. 50 milliseconds). I know, that's a very weird idea. It's usually very hard for new programmers to grasp. I know it was for me. In that short amount of time, it completes any assigned task while "watching" to see if the user does something, such as clicking a button. There is nothing special about 50 ms. It could be called in one second, but 50 ms is plenty of time for the app to do its work.

Another procedure, LloydStopWatch, has all of the code that actually computes how many seconds have transpired since starting the timer. This code also keeps track of the intervals and knows when play a chime at the end of each interval. Note that line 5 is where the LloydStopWatch procedure is triggered, but only if the variable varRunMainLoop is true. This variable controls whether the timer is active. If the user clicks the Start button it becomes true. If the user clicks the Stop button, it switches to false.

So, how does one get MainEventLoop to start running. The answer is in the code for the button "Start:"

1:  on mouseup  
2:    put 0 into varTimeCurrentLoop  
3:    put 0 into varIntervalTotal  
4:    put 1 into varIntervalTurn  
5:    put false into varRestartInterval  
6:    put empty into field "notes"  
7:    put empty into field "Interval Totals"  
8:    put empty into varTimeLog  
9:    put field "Iminutes1" into varIminutes1  
10:    put field "Iseconds1" into varIseconds1  
11:    put field "Iminutes2" into varIminutes2  
12:    put field "Iseconds2" into varIseconds2  
13:    put field "Iminutes3" into varIminutes3  
14:    put field "Iseconds3" into varIseconds3  
15:    put true into varRunMainLoop  
16:    put the seconds into varSecondsBegin  
17:    put varSecondsBegin into varSecondsBeginInterval  
18:    MainEventLoop  
19:  end mouseup  

Lines 2-18 initiate all of the main fields and variables, essentially getting them ready for the start of a new timing session. Line 15 sets the variable varRunMainLoop to true and line 18 initiates the MainEventLoop procedure.

Lines 9-14 takes whatever interval values the user had entered and passes them on to unique variables for each for use within the LloydStopWatch procedure.

Line 16 essentially checks the current time and becomes the moment in time from which the time elapsed is figured. This value is held by the variable varSecondsBegin. Line 4 in the MainEventLoop procedure continually checks the current time, puts it in a variable named varSecondsEnd, then calls the LloydStopWatch procedure. The time elapsed in seconds is simply the difference between these two values. This simple subtraction problem is happening every 50 ms.

Interestingly, I first put the line "put the seconds into varSecondsEnd" into the LloydStopWatch procedure. But, I experienced some weirdness in the operation of the app. After the app had been running for about 10 minutes, the app started skipping a bunch of seconds which, of course, ruined the reliability of the app. I really couldn't figure out why. I tried all sorts of programming hacks, but none worked. So, in a bit of desperation, I moved this line to the MainEventLoop procedure and it solved the problem. All I can say I had a hunch it would work. I think the problem had something to do with the recursive nature of this MainEventLoop procedure. I've experienced weirdness in this coding technique in previous apps. I actually asked for help in one of the LiveCode forums a couple of years ago about this. Lines 8-11 and the lock/unlock screen code were suggested to me by an expert LiveCode user which solved the problem for me back then. So, I've continued to use this code anytime I use this looping technique. In short, you start building up a backlog of calls for the MainEventLoop procedure and eventually all hell breaks loose. Lines 8-11 clear out all previous calls for the procedure.

Finally, the timer stops when the user presses the "Stop" button - here is the code for the Stop button:

1:  on mouseup  
2:    put false into varRunMainLoop  
3:  end mouseup  

By changing the value of varRunMainLoop to false, it stops the loop within MainEventLoop that had been triggering the LloydStopWatch procedure.

Short List of Things to Do

Actually, the list is quite long, but here are some of the obvious revisions and updates that are needed:

  • Add a fourth timer interval
  • Change the elapsed time format
  • Allow the user to pause the timer
  • Add a choice of alert sounds
  • Add a mobile interface design, including spinner wheels to select time intervals and a vibrate option for alerts