07‐Jackie - molab-itp/content-2026-itp GitHub Wiki

what programming languages have you used? how much?

  • html/css/js (still beginner)

what is your favorite mobile app and why?

  • dooo0t ! such a cute way to edit your pics~

if you have a concept for a final project write a short description

  • not sure yet!

Week 03 — SwiftUI 1, Images

Homework: Week03/TenPrintTiles

How I approached learning Swift fundamentals

Coming from html/css/js, the thing I keep bumping into is that Swift wants to know the type of everything up front. In JS I can put anything in an array and move on. In Swift the array has one type and the compiler tells me about it immediately.

What has actually worked for me so far:

  • Reading the teacher's sample code before writing my own. Weeks 01 and 02 I started by opening the sample playground pages, running them, and changing one value at a time to see what moved. The playground showing results in the right-hand column made this fast — I could see what a line did without a print statement.
  • Building up from a sample rather than a blank file. My Week02 ascii image page started as the teacher's inter weave page and turned into my own thing a few edits at a time.
  • Getting the loop and the array right on paper first. Especially for the grid stuff — working out row and col from a single index is easier drawn than typed.

Still shaky on: optionals and when to use ! vs ?, closures ({ $0.count } syntax still reads as magic to me), and knowing which type a SwiftUI modifier expects.

Week 03 progress

This is the first week the work is a real Xcode app project instead of a playground, which was its own adjustment.

What I built: a My Emojis screen — a grid of my most used emojis (🥹😂🙂‍↔️🫶😊🥰🥲✋⚽️👵🏻🙃⚾️) that reshuffles into a new random arrangement every time you tap the Shuffle button.

It works the way the teacher's 10Print example does: build an array of random elements first, then draw the array. Every cell calls randomElement() on my emoji array, so 40 cells get filled from a list of 12 and the repeats fall wherever they fall.

let myEmojis = ["🥹", "😂", "🙂‍↔️", "🫶", "😊", "🥰",
                "🥲", "✋", "⚽️", "👵🏻", "🙃", "⚾️"]

func makeEmojis() -> [String] {
  var result: [String] = []
  for _ in 0..<(emojiCols * emojiRows) {
    result.append(myEmojis.randomElement()!)
  }
  return result
}

The array lives in @State, which is what makes Shuffle actually work — I learned that a plain var changes the value but nothing on screen moves.

The grid itself is a nested ForEach — rows in a VStack, cells in an HStack — and the screen sits in a TabView with .tabItem { Label(...) }, the multi view pattern from 03-About-Me.

What I learned the most from: I started this screen by copying a version that drew SF Symbols, expecting to just swap the names out for emoji. That did not work. Image(systemName:) only takes SF Symbol names — emoji are text, so the screen needs Text(emoji) with a big font instead.

Two follow-on differences I did not expect. Emoji carry their own color, so .foregroundStyle() does nothing to them, which means there is no color to randomize — the only random choice left is which emoji goes in each cell. That actually made my code shorter than the symbol version, because I did not need a struct to pair an emoji with a color. Just an array of strings.

Issues and errors

  • Spent a while stuck because I opened the whole 00-molab-2026-jht folder in Xcode instead of opening a .playground file directly. In folder mode the playgrounds show up in the sidebar but clicking them does nothing — no editor, no run button. A .playground is a bundle, not a file, so the folder browser can't open it. Plain .swift files in the same folder opened fine, which is what made it confusing.
  • The Image(systemName:) vs Text() mixup above — the copied screen compiled fine but showed nothing useful until I understood emoji are text, not symbols.
  • The simulator remembers which tab was selected between launches, so my screenshots kept coming out on the wrong tab. Deleting the app from the simulator resets it.

AI use

Per the course AI policy: I used Claude Code to scaffold the Xcode project and the view files, working from the teacher's samples. It also diagnosed the Xcode folder-vs-playground problem above, and explained why emoji need Text() rather than Image(systemName:). I did not use AI on the Part 0 TRY: exercises. Each source file names the sample its technique came from so I can walk through the code in class.


Week 04 — SwiftUI 2, Audio

Homework: Week04/MinuteGarden

Writing this up late, catching my wiki back up.

Week 04 progress

What I built: MinuteGarden, a one minute timer. Press Start and a plant gets added to the garden every 10 seconds while birds play underneath. At zero the birds stop and a chime rings. Three tabs: Timer, Sounds, About.

It does both halves of the assignment — time (Timer.publish counting down, from CountDownTimerView) and audio (AVAudioPlayer, from PlayAudioView). The Sounds tab is honestly PlayAudioView with the names changed, which I would rather say out loud than hide.

The garden is just one String. Adding a plant is:

garden = garden + plants.randomElement()!

and showing it is Text(garden). No grid, no row and column arithmetic.

What I learned the most from: I built a bigger version first — MinuteGardenPlus — with an @Observable class shared across pages with .environment(), a Track struct, selectable ambience, and a ProgressView driven by TimelineView(.animation). All Week04 material, and it worked. I dropped it anyway, because it was much more machinery than the assignment needed and I could not confidently explain every line of it. It is still in git history at commit 5eed76a.

The thing that decision taught me: @State only means each page owns its own stuff and nothing is shared. So TimerView and SoundsView each have their own player, and if you start the timer and then hit Play on the Sounds tab you get two sounds at once. That bug is the explanation of what @Observable + .environment is for.

Issues and errors

  • Bundle.main.path(...)! force unwraps, so a misspelled file name crashes the app instead of printing an error. The sample does this too, but it was the first place I had to look when it died on Start.
  • The timer ticks whether or not the countdown is running — .onReceive fires every second no matter what. Without if isRunning && timeRemaining > 0 at the top of the block, the number goes negative.
  • One AVAudioPlayer only holds one sound. Reassigning it for the chime drops the looping birds, so the birds have to be stopped first or it gets weird.
  • timeRemaining % 10 == 0 grows a plant at 50, 40, 30, 20, 10 and 0 — that is 6 plants in a 60 second session, not 5. Zero counts.
  • #Preview crashes with no explanation if a view reads something out of @Environment and the preview does not supply it. Not an issue in the version I kept, but it bit me repeatedly in MinuteGardenPlus.

Hours: [fill in]

AI use

Used Claude Code to scaffold the project from the teacher's samples and to talk through whether the @Observable version was worth keeping. Each file names the sample its technique came from.


Week 05 — Local Data 1, Sensors

Homework: Week05/TurnBack

[screenshot - need to run it on my phone first]

Week 05 progress

What I built: TurnBack, a navigation app that runs on the gyroscope. Press Mark while facing the way you came in, and the arrow keeps pointing back that way however you turn. Three pages browsed from a list: Heading, Level, About.

The demo I incorporated is @AppStorage, from 05-AppStorageDemo. Four emoji buttons 📍 🏠 🚗 🥾 pick what the marked spot is called, and the pick survives relaunch.

The thing I got wrong at the start: I thought "gyroscope" meant I could build a compass. It does not. The gyroscope only knows how far the phone has spun since the sensor started — it has no idea where north is. North needs the magnetometer, which we have not covered. So the arrow is relative to wherever I was standing when I pressed Mark, and it has to be marked again every launch.

That turned into the most interesting decision in the app. I am saving the label with @AppStorage but deliberately not saving the marked direction, even though saving a Double would be one line. A bearing restored from yesterday would be meaningless, because the sensor picks a brand new zero every time it starts. So: what can be saved and what is worth saving are two different questions.

The maths is just subtraction. detector.yaw is how far the phone has spun, in radians; subtract the yaw I saved when I pressed Mark, and turn the arrow by the difference with .rotationEffect — the same trick the needle uses in NeedleSeismometer.

var turnAngle: Double {
  var diff = detector.yaw - markedYaw
  if diff > Double.pi { diff = diff - 2 * Double.pi }
  if diff < -Double.pi { diff = diff + 2 * Double.pi }
  return diff
}

Those two ifs matter: yaw wraps around at -pi and pi, so without them the difference can come out as more than half a turn and the arrow swings the wrong way round.

What I changed in the sample code. MotionDetector.swift, DoubleExtension.swift, BubbleLevel.swift and OrientationDataView.swift are copied from 05-BubbleLevel. Two changes, both commented in the files:

  1. Added yaw to MotionDetector — the sample tracks roll, pitch and z acceleration, but not yaw, which is the one thing my app actually needs.
  2. Took start() / stop() out of LevelView, because my page list starts and stops the detector for the whole app the way SeismometerBrowser does.

The bubble level earns its place rather than being decoration: yaw only reads true with the phone held flat, face up. Tilt it upright and roll, pitch and yaw start mixing into each other.

Issues and errors

  • Nothing moves in the simulator. There is no gyroscope in it, so yaw sits at 0 and the arrow never turns. Both class samples are listed under "Device Required" and now I know why. Layout can be checked in the simulator; behavior cannot.
  • Leaving start() / stop() in LevelView as well as the page list killed the sensor every time I navigated back to the list. Start and stop belong in exactly one place.
  • Tried to build straight to my phone and got "The developer disk image could not be mounted on this device" twice — I think because the phone was locked. Still to sort out: [ ] run it on the phone from Xcode.
  • Still to verify on the phone: which way the arrow turns. I worked out the sign by reasoning about the phone lying face up, but that is the kind of thing you confirm by standing up and turning around, not on paper. If it tracks my turn instead of pointing back, it is a one character fix (flip the minus in turnAngle).

Hours: [fill in]

Plans and questions

  • Measure the drift. A gyroscope adds up rotation over time, so small errors accumulate. Mark a direction, walk a loop around the floor, come back to the same spot and see how far off the arrow is. That is a measurement, not a guess.
  • Why does this app share one MotionDetector across pages when MinuteGarden deliberately gave each page its own player? I think two pages reading the same live sensor is exactly the case @Observable + .environment exists for — would like to check that reasoning in class.
  • MotionDetector uses Timer.scheduledTimer and asks the sensor for its current value. Week04 used Timer.publish(...).autoconnect() with .onReceive. Both make something happen every so often — when is each the right one?

AI use

Per the course AI policy: I used Claude Code to scaffold the Xcode project, copy in the sample files, and write the Heading page. It is also what pushed back on my original "compass" idea and explained that the gyroscope cannot find north, which changed the whole concept. Every file names the sample its technique came from, and the sensor reasoning is something I still need to confirm by walking around with the phone rather than taking on faith.


Week 06 — Local Data 2, Files

Homework: Week06/TurnBack

Week 06 progress

What I built: TurnBack again, plus My Walks, a drift log saved as a JSON file in the app's Documents folder. The saving option I picked is Codable + JSON, from 06-ImageEditDemoJSON.

Last week I worked out that the marked direction is not worth saving, because the sensor picks a new zero every launch. This week's answer to "so what is worth saving?" is how wrong the arrow gets. Mark a direction, walk a loop, come back to the same spot, face the way I marked, and tap I'm back. Whatever the arrow reads at that moment is the gyroscope's drift. Each walk is saved with the place, the date, the minutes I was gone and the degrees off.

The saving follows the sample's three pieces:

  • DriftEntry — one walk, an Identifiable, Codable struct (the sample's ItemModel)
  • Model — the list of entries; loads itself from the file and saves itself back (the sample's Model + ModelJSON, merged into one file)
  • Document — an @Observable class shared with .environment, which saves after every change, so there is no Save button to forget

SaveLoadJSON.swift is copied from the sample unchanged. I use its generic saveJSON / loadJSON without having written the <T: Encodable> part myself.

struct DriftEntry: Identifiable, Codable {
  var id = UUID()
  var emoji = ""
  var name = ""
  var date = Date()
  var minutes = 0.0
  var driftDegrees = 0.0
}

UX pass. I also redesigned both screens with a designer's eye:

  • Renamed Heading → Way Back and Drift Log → My Walks: say what it's for, not which sensor it uses
  • One main action per step. Before marking: pick the place, then one big Mark this direction button. While walking: the arrow and one big I'm back - save walk button. The place picker hides mid-walk
  • Touch targets: every button is at least 44 pt tall (Apple's minimum), and the main buttons are 56 pt and full width in the thumb zone
  • Words, not raw numbers: "12° to your right" instead of "+012°"
  • The answer first: My Walks opens with the average drift in big type
  • Destructive actions are small and far away. Start over sits well below I'm back, and Clear all walks needs two taps (done with just @State)

Issues and errors

  • Every #Preview now needs .environment(Document()) as well as the detector — same crash-with-no-explanation as Weeks 04 and 05.
  • The first launch prints loadJSON NO file at filePath. That's expected, not a bug: there is no file until the first save.
  • The simulator can test saving but not drift. Every walk reads 0° because there's no gyroscope. The saving test is: save a walk, stop the app, run it again, and the walk should still be in the list. [confirm I did this]
  • [fill in anything else I hit]

Hours: [fill in]

Plans and questions

  • Run it on my phone and log real walks, a short one and a long one. Does drift grow with time or with how much I turn?
    • Walk 1 (Oct 14, on my iPhone): 3° off after a walk of under a minute. It works on the phone, saves, and shows up in My Walks.
    • Next: a longer walk to compare. [fill in]
  • If I add a property to DriftEntry later, what happens to walks already saved in the old file? (I think they stop loading and the list comes up empty.)
  • Why save after every change instead of when the app closes?
  • Final project direction: turn TurnBack into a "find my car" app for underground garages, where GPS drops out. Save a photo of the level sign (Week 07 Photo), the level and a note, and later share the spot with someone (Weeks 09–10). Real GPS and compass would need Core Location, which we haven't covered.

AI use

Per the course AI policy: I used Claude Code to brainstorm options for saving data, pick the drift log idea (it came from my own Week 05 questions), scaffold the new files from 06-ImageEditDemoJSON, and do the UX review of both screens. Every file names the sample its technique came from, and Week06/CLAUDE.md in my repo lists exactly what's new this week so I can walk through it in class.