An Example of why I think Apple’s developer documentation sucks

for anyone thinking of complaining i’m being “mean” or “unfair”, I was being *nice* here. I could have compared MS’s OS automation and PowerShell documentation to Apple’s Automation and AppleScript documentation. But why beat a dead horse?

It’s no secret I really hate Apple’s dev. docs. Because they are bad. Objectively so, and in ways that hurt new people. For example, let’s say I want to put a button in my UI. I need to know things about that item right? Here’s what you get for a SwiftUI button, from the docs page for it.

Lots of info on creating a button. In fact, almost all of the information on buttons are about some variant of init() for a button, 25 entries in all. But how do I make the text in a button. Well, that’s not actually listed, but in the initial button page, there’s a link to Label, but it seems I don’t’ always need a label, just sometimes. That’s…not helpful. I guess I should use Label only when I have a title and an icon? Well the Label page sort of helps, but not really.

You know what would be handy here? Some simple sample code with images. Nope. Doesn’t exist.

Also what are a button’s properties? What are the methods that are unique to a button? Do they exist? Well they must, because there’s this whole buttonStyle thing, let’s take a look at that, maybe there’s a list of properties there with sample co…nope. Just one shown, .bordered, and no images. So if you don’t know what a .bordered button looks like already, yeah, you’re hosed.

Here’s the documentation on ButtonBorderShape. Well, that’s certainly useless to a new person. “A shape that is used to draw a button’s border.” Thanks Chad. Does ButtonRole list all the options for ButtonRole? No, of course not, what could possibly be the value in that? At least EditButton has a screenshot. Someone must have snuck that in.

Over and over, and mind you, this is just for a *button*, Apple’s dev docs are clearly aimed at experienced developers. Oh they have a “Learning SwiftUI section”, but again, who is it aimed at? It takes about ten seconds to realize that “New devs” are not the target of any Apple Docs. Seriously, this?

The MyApp structure conforms to the App protocol, and provides the content of the app and its behavior.

is not something aimed at beginners. Note: I was trained in how to write documentation for doing maintenance on aircraft for people where the sole qualification was “can read english at a 9th-grade level”, I am very familiar with writing documentation for new people.

Now, let’s take a look at the section for a WPF Button on Microsoft’s site: That is a short overview page with an actual screenshot and an explanation of what a button does. Okay, good. Then there’s a link to another Button page. What’s on that? Oh my, everything. No really, everything. Do you need to know how Button fits into WPF overall? It’s explicitly there. Do you want sample code for Hover, Click, and Release in both XAML and C#? Explicitly there. Could use more screenshots, but still. Constructors? Explicitly there, and there’s one, Button().

Fields? There. Properties? Oh my god, all of them. Methods? A huge list with basic explanations and links. There’s even an OnClick() method, what a weird name! An Event listing! If I…click…on the link to the Click event, there’s sample code, and a talk about how a button can be clicked with various things that aren’t mouse buttons, and even what other events can happen with Click and how they’re handled.

There’s a control gallery sample project I can download from GitHub, there’s a tutorial on creating a UI for Windows 10 apps that goes into great detail about what you’re seeing. It explains the obsession with Grid in WPF. Like the first lesson spends a LOT of time on getting you to understand the basic layout of a WPF or UWP app.

Is it easier or better than SwiftUI? That’s a useless question in this context. What is clear is that Microsoft’s Developer Support team is willing to spend a lot of time and effort on helping truly new folks become functional Windows Devs, time and effort that Apple has chosen not to spend. Probably because of some bullshitery about “We’re so intuitive you don’t need training” and “real devs don’t need good docs, just header files.”

I don’t know that for sure about Apple, but that’s damned sure how it feels and I can tell you that as a person who knows less about C# and WPF than I do about SwiftUI, I was able to get a functional app done in WPF and C# far faster than I ever have with SwiftUI (given I’ve never been able to complete a SwiftUI app and I have been able to complete a C# WPF app), and a lot of that was due to Microsoft actually giving a shit about new devs.

That can’t possibly bite Apple in the ass ever.

School Project Fun

So in my final semester for my 39-year bachelor’s, one of the things I have to do is build an SOS game. It’s a grid game where players try to spell SOS. A bit harder than Tic-Tac-Toe, but not too bad. We don’t have any restrictions on language, so I’m building it with SwiftUI, since the only thing I’m more familiar with is ASOC, and that seems mean to do to a prof who really only groks Java.

The project itself is at https://github.com/johncwelch/SOS-Swift, and so far, I have the basic window setup done, i.e. the controls for the stuff and some very basic unit tests. I’ll be adding to this post as I go along as a way of documenting some of the “fun” bits of SwiftUI, especially dealing with Apple’s “If you actually need docs, go write C# you loser” attitude towards proper documentation

Spare me the arguments, Apple’s Developer Docs are awful. Pages of how to init a control, nothing on the properties of said control, barely more on the methods. Let me put it this way, it is possible, without excessive work for someone basically new to C# to, using only MS’s official dev docs, build an application in C#. It’d be a bit of work, but the docs are complete and the sample code is solid. It would be impossible for someone to do the same with Swift(UI) using only Apple’s developer documentation. It’s awful, just bookmark Hacking With Swift, save yourself some pain.

Initial Setup

I’m not going to do a line by line thing here, just talk about some of the more interesting things. The first thing I did was lost that “default” VStack so that the HStack that all my game control bits are in would stretch the width of the window via the .frame(maxWidth: .infinity) property. Also, WHY DOES APPLE REFUSE TO DOCUMENT PROPERTIES WORTH A SHIT???

Another early thing I do, that I do to about every app I write is in the AppNameApp.swift file, I add in:

class AppDelegate: NSObject, NSApplicationDelegate {
     func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
          return true
     }
}

and then in the @main app struct I add: @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate, this way, when I close the application window, it auto-quits. Way handy.

Another thing I did was start taking code partitioning more seriously, so the only thing in my ContentView file are things that have to be there. Everything else, like radio button setup structs, my viewmodifiers, those are all in SOS_SwiftApp.swift. THAT file may get a bit long, but it will be nicely modular.

ViewModifiers are really handy, they’re basically stylesheets for things. For example, i have a few Text fields, that have really similar needs in terms of font, font weight, text selection enabled, and few more that have those same properties, but also need the exact same frame sizes. So, we create two structs:

struct basicTextModifier: ViewModifier {
     func body(content: Content) -> some View {
          return content
          .font(.body)
          .fontWeight(.bold)
          .frame(width: 120.0,height: 22.0,alignment: .leading)
          .textSelection(.enabled)
     }
}
struct basicTextModifierNoFrame: ViewModifier {
	func body(content: Content) -> some View {
		return content
			.font(.body)
			.fontWeight(.bold)
			.textSelection(.enabled)
	}
}

The difference is pretty easy to see, but those let me replace all the formatting code across multiple Text fields with .modifier(basicTextModifier()) and .modifier(basicTextModifierNoFrame()) which cleans up the code in ContentView nicely.

The work to create radio buttons in SwiftUI is a bit twee (although not as twee as pasting multiline text into a wordpress block. Jesus wept but that shit is incompetently done.) First you build the definition struct:

struct gameTypeRadioButtonView: View {
	var index: Int
	@Binding var selectedIndex: Int

	var body: some View {
		//in swiftUI ALL BUTTONS ARE BUTTONS
		Button(action: {
			selectedIndex = index
		}) {
			//this sets up the view for the basic button. You define it here once, then implement it in
			//the main view
			HStack {
				//literally build the buttons this way because fuck if I know, this UI by code shit is
				//stupid
				Image(systemName:  self.selectedIndex == self.index ? "largecircle.fill.circle" : "circle")
					.foregroundColor(.black)
				//set the label for each one
				if index == 1 {
					Text("Simple")
				} else if index == 2 {
					Text("General")
				}

			}
			//.padding(.leading, 20.0)
		}
	}
}

Then in ContentView, you implement them:

VStack(alignment: .leading) {
			    Text("Game Type:")
					.modifier(basicTextModifier())
					.accessibilityLabel("Game Type label")
					.accessibilityIdentifier("gameTypeLabel")
				    //this actually draws the buttons
				gameTypeRadioButtonView(index: 1, selectedIndex: $gameType)
				gameTypeRadioButtonView(index: 2, selectedIndex: $gameType)
					//
			}
			.padding(.leading, 20.0)

So it works, but it’s a bit weirder than other languages where you just define the radio group, then each button in that group does what it should. Pickers are similarly weird in that there’s one picker, and what it looks like is defined by the .pickerStyle”

Picker("Board Size", selection: $boardSize) {
	Text("3").tag(3)
	Text("4").tag(4)
	Text("5").tag(5)
	Text("6").tag(6)
	Text("7").tag(7)
	Text("8").tag(8)
	Text("9").tag(9)
	Text("10").tag(10)
}
//picker properties
//padding has to be separate for each dimensions
	.modifier(basicTextModifierNoFrame())
	.frame(width: 140.0,alignment: .center)
	.padding(.top,2)
	.accessibilityLabel("Board Size Dropdown")
	.accessibilityIdentifier("boardSizeDropdown")
	//makes it look like a dropdown list
	.pickerStyle(MenuPickerStyle())
	//this is how you initiate actions based on a change event
	.onChange(of: boardSize) {
	        boardSizeSelect(theSelection: boardSize)
	}

Obviously I’m using state vars for this stuff, which as we’ll see when I add in the grid, will make our live very easy by reducing the amount of work needed to resize the grid on the fly to essentially zero:

@State var gameType: Int = 1
@State var boardSize: Int = 3
@State var bluePlayerType: Int = 1
@State var redPlayerType: Int = 1
@State var currentPlayer: String = "Blue"

Unit Tests!

So right now, I do not have a lot of Unit Tests in my code. This is less “I hate unit tests” and more “I spend all my (coding) time doing things where Unit Tests are not a big deal or they create dependencies. Unit Tests in PowerShell and AppleScript are very, very, different than in things like Swift or C#. Not conceptually, but how you implement them. So for right now, they’re all very basic, like does the app launch, does this button do what it should, etc. Does the button even click?

I’m having to learn about Unit Tests on the fly, they’re kind of an opaque topic, and have a lot of “well, clearly you already know so why do you need documentation” infecting them. They’ll show up later.

The Game Grid

While I like to bag on how weird SwiftUI can be and how execrable Apple’s documentation is, there are times SwiftUI is great. Like managing the game grid. This has to be a grid between 3×3 and 10×10 in size. So kind of a pain in the ass if you’re going to do this manually. Luckily, I’m not doing this manually. The grid is one state variable: @State var boardSize: Int = 3 and then the grid itself:

  HStack(alignment: .top) {
			Grid (horizontalSpacing: 0, verticalSpacing: 0){

				//the view (grid) will refresh if you change the state var gridSize,
				//but, you have to include the id: \.self for it to work right, because
				//of how swift handles this. Note, you don't use the id, 
				//this is just telling the view what's going on.

				//row foreach
				ForEach(0..<boardSize, id: \.self) { row in
					GridRow {
						//column foreach
						ForEach(0..<boardSize, id: \.self) {col in
								//put a rectangle in each grid space
								//the overlay is how you add text
								//the border is how you set up grid lines
								//the order is important. if foreground color comes after
								//overlay, it covers the overlay
							Rectangle()
								.foregroundColor(.teal)
								.overlay(Text("\(row),\(col)").fontWeight(.heavy))
								.border(Color.black)
						}
					}
				}
			}
		}

That’s the entire grid, and because I’m basing the size on the boardSize state var, whenever that’s changed in the picker, the grid is refreshed, and redrawn to the new size, and I do…nothing. The next step on the game is putting buttons in each grid, but before that, I wanted to add dynamic score fields.

Score Fields

There are two types of games: simple and general. For a simple game, it’s over whenever someone finally makes “SOS” or the board is filled. So there’s no score, it’s sudden death. For a general game, it goes until the board is filled, and whomever made the most SOS’s wins. So general needs a score, simple does not. Therefore, the score fields should only show up when the game type is general. State vars to the rescue. First, we create the ints for the score, @State var bluePlayerScore: Int = 0 and @State var redPlayerScore: Int = 0. Then we create an Hstack for the score that will live at the bottom of the column with the other respective player info, with a specific if statement:

 HStack(alignment: .center) {
					//in an if statement, we don't use the $varname, just the varname
					if gameType != 1 {
						Text("Blue Score")
						Text(String(bluePlayerScore))
						     .frame(width: 25, height: 22, alignment: .center)
					}
				}
				.frame(width: 105, height: 22, alignment: .leading)

So whenever the gameType radio button is set to simple, the score fields are not shown. When its set to general, they are:

I do zero “onClick()”-type coding, that’s all for free. Sometimes SwiftUI does not suck.

BUTTONS

Oh christ, this one wanted to kill me because the obvious solution of frame maxwidth and maxheight both set to infinity only half-worked, for the width. So stupid. So I need to be able to dynamically set the width and height of the button via the text fram, but how? Turns out, there is functionality that’s actually pretty easy, and designed to do just this kind of thing: GeometryReader. Basically, GeometryReader lets you get a bunch of well, geometric info from a view, and what is a grid cell but a view? Right. So i want to keep my rectangle for now, and put the button on top of it, and we put the entire thing inside GeometryReader{} in the inner loop that builds the grid:

 GeometryReader { gridCellSize in
		Rectangle()
			.foregroundColor(.teal)
			.overlay(Text("\(row),\(col)").fontWeight(.heavy))
			.border(Color.black)

		Button {

		} label: {
			Text("")
				.frame(width: gridCellSize.frame(in: .global).width,height: gridCellSize.frame(in: .global).height)
		}
	}

That does exactly what we need, no muss, no fuss. Next step, getting the buttons to cycle between values on a click, but that should be easy. Well, easier.

NARRATOR VOICE: It was NOT easier

Advanced Button Tedium

This is where I realized that in terms of SwiftUI documentation online, it’s all either “This is a button, isn’t it cooooool” or it’s all hyperadvanced shit for people who have been coding Swift since it first came out. The in-between stage is naught but crickets. As well, a lot of the descriptions of things are really clearly written for…not n00bs. The various explanations of how @State et al work? Dude, I just guessed and prayed.

Luckily, and I mean that, the folks in the Hacking With Swift forums and some very kind folks on Mastodon took way too much of their time to help out, and so I was able to dope it out. The gist of it is, when I click on a button, I need it to cycle between “”, “S” and “O”. I need it to do some other things, but those are pretty easy. This basic thing turned out to be shockingly hard, and really counterintuitive, since as near as I can tell, there’s no way to determine what order things happen in with SwiftUI outside of specific things like button clicks. Layout, on appear, on change, it’s all kind of “it happens when it happens”, which is really not okay.

I ended up making some core changes to my setup. First, we have our Cell class:

 @Observable
class Cell: Identifiable {
	let id = UUID()
	var title: String = ""
	var buttonDisabled: Bool = false
	var index: Int = 0
	var xCoord: Int = 0
	var yCoord: Int = 0
	var backCol: Color = .gray
	//var disabled: Bool = false
}

This sets up a few things like the title, the background color, etc. I don’t really understand why it has to be @Observable, only that it does. Okay. Also, I learned that if you don’t explicitly import Observation, SOME of Observable works, SOME does not. Again, THIS IS NOT OKAY. If I should import Observation to really use @Observable, then for Christ’s sake, throw build error and tell me. It’s two words, not a big deal to add to the top of the file.

Next we have our second observable class, Game, which creates an array of Cell, created by buildStructArray that by the time we’re done, creates the objects we attach to our grid of buttons. From my POV, there is a lot of magic here, but it works, so cool. I’m in IT, magic is a thing. Game Class:

 //class that is a collection of cells that are attached to the buttons in contentview
@Observable class Game {
	//this sets the size of the grid, passsed when creating the array
	var gridSize: Int {
		didSet {
			//builds the array based on the int passed in which is set in the picker
			gridCellArr = buildStructArray(theGridSize: gridSize)
		}
	}

	//create the array var
	var gridCellArr: [Cell] = []

	//initialization
	init(gridSize: Int) {
		self.gridSize = gridSize
		self.gridCellArr = buildStructArray(theGridSize: gridSize)
	}
}

buildStructArray:

 //this builds an array of Cell that gets attached to each button in the game. It's kind of important
//takes in the grid size set in the UI, and then returns an array of cells
func buildStructArray(theGridSize: Int) -> [Cell] {
	var myStructArray: [Cell] = []
	let arraySize = (theGridSize * theGridSize) - 1
	for i in 0...arraySize {
		myStructArray.append(Cell())
		myStructArray[i].index = i
		myStructArray[i].backCol = .gray
	}

	return myStructArray
}

That’s all set up initially in a state var for our 3×3 grid we have on launch: @State var theGame = Game(gridSize: 3)

Because of this, we also modified our picker:

 Picker("Board Size", selection: $theGame.gridSize) {
	    Text("3").tag(3)
	    Text("4").tag(4)
	    Text("5").tag(5)
	    Text("6").tag(6)
	    Text("7").tag(7)
	    Text("8").tag(8)
	    Text("9").tag(9)
	    Text("10").tag(10)
}
		//picker properties
		//padding has to be separate for each dimensions
		.modifier(basicTextModifierNoFrame())
		.frame(width: 140.0,alignment: .center)
		.padding(.top,2)
		.accessibilityLabel("Board Size Dropdown")
		.accessibilityIdentifier("boardSizeDropdown")
	    //makes it look like a dropdown list
		.pickerStyle(MenuPickerStyle())
		//this is how you initiate actions based on a change event
		//test func to show size of board based on selection
		.onChange(of: theGame.gridSize) {
        				    boardSizeSelect(theSelection: theGame.gridSize)
		}

boardSizeSelect(theSelection: theGame.gridSize)is still just a test function to validate the picker.

I also had to modify the ForEach statements: ForEach(0..<theGame.gridSize, id: \.self) { for this change. On the upside, by setting the title to use theGame properties and setting the .frame option there, I no longer needed the Rectangle() code, so cool. New label and onAppear code:

 label: {
		//set the text of the button to be the title of the button
       Text(theGame.gridCellArr[myIndex].title)
                 //set the font of the button text to be system with a
                 //size of 36, a weight of heavy, and to be a serif font
                 .font(.system(size: 36, weight: .heavy, design: .serif))

				//this ensures the buttons are always the right size
				.frame(width: gridCellSize.frame(in: .global).width,height: gridCellSize.frame(in: .global).height, alignment: .center)
}
 //styles button. Since I only have to do this once, here, there's no
 //real point in building a separate button style
 //note that .background is necessary to avoid weird button display errors
 .foregroundStyle(buttonTextColor)
 //this allows the button color to change on commit
 .background(theGame.gridCellArr[myIndex].backCol)
 .border(Color.black)
 //once a move is committed, buttonDisabled is set to true, and the button is
 //disabled so it can't be used again
 .disabled(theGame.gridCellArr[myIndex].buttonDisabled)
 .onAppear(perform: {
			//this has each button set its own coordinates as it appears
			//which is IMPORTANT later on
			theGame.gridCellArr[myIndex].xCoord = col
			theGame.gridCellArr[myIndex].yCoord = row
})

The onAppear sets the coordinates of the button which we’ll need to calculate if SOS has been built. Also, we added .id(theGame.gridSize) to the outer ForEach loop, which insured that all buttons were completely redrawn when the grid size changed. Without that, it would only draw the “new” buttons and all the “old” ones would be blank, with coordinates of 0,0, which was not okay.

However, now I could start doing something besides dicking with buttons. Mind you, this shit took over a week to figure out. Have I mentioned Apple’s developer docs are shit? Because they are. Inexcusably so.

Next up: the beginnings of “Commit Move” and “New Game” is finally complete!

More On Unit Tests

Well not as such. Adding unit tests has been oddly difficult for two reasons, one my doing, one not. The my doing is i don’t write for unit tests. Most of my functions have multiple parameters in and tuples coming out. Some of them are on the long side. But as a sysadmin, most of my coding has been for my specific needs, an audience of one, and what I’m coding for is really straightforward. Also, most of my functions aren’t returning bools.

The other…I swear there is a disease in the Swift world where no one comprehends the needs of newer devs. Every example of Unit Tests in SwiftUI has been along two lines: Either buttons are the only thing one ever needs to test or everyone is evidently testing custom protocols that override every class in Swift(UI). Meanwhile, I’m out here trying to track a variable and there’s nothing on that.

Anywhere.

However, I was able to get some things working, like testing for control states after clicking a button. The clicking a button is just recording actions for the app (god, I LOVE the recording feature.) The the rest is:

  1. Create an accessibility label for the control you want to test
  2. use that in an XCTAsserttrue/false statement

for example, when I click on the “New Game” button, there’s a few controls that need to be enabled that may or may not be currently enabled depending on what’s going on. Here’s the test:

 func testNewGameButton() {
		let SOSApp = XCUIApplication()
		//launch application for button click test
		SOSApp.launch()
		let myCommitButton = SOSApp.buttons["commitButton"]
		let myGameTypeSimpleButton = SOSApp.buttons["gameTypeSimple"]
		let myGameTypeGeneralButton = SOSApp.buttons["gameTypeGeneral"]
		let myBluePlayerTypeHuman = SOSApp.buttons["Blue Player Human"]
		let myBluePlayerTypeComputer = SOSApp.buttons["Blue Player Computer"]
		let myRedPlayerTypeHuman = SOSApp.buttons["Red Player Human"]
		let myRedPlayerTypeComputer = SOSApp.buttons["Red Player Computer"]
		let myCurrentPlayer = SOSApp.staticTexts["Current Player"]
		//click the button
		SOSApp.windows["SwiftUI.ModifiedContent<SOS_Swift.ContentView, SwiftUI._FlexFrameLayout>-1-AppWindow-1"].buttons["New Game"].click()
		//states that should be a specific way after clicking new game:
		XCTAssertFalse(myCommitButton.isEnabled)
		XCTAssertTrue(myGameTypeSimpleButton.isEnabled)
		XCTAssertTrue(myGameTypeGeneralButton.isEnabled)
		XCTAssertTrue(myBluePlayerTypeHuman.isEnabled)
		XCTAssertTrue(myBluePlayerTypeComputer.isEnabled)
		XCTAssertTrue(myRedPlayerTypeHuman.isEnabled)
		XCTAssertTrue(myRedPlayerTypeComputer.isEnabled)
	}

It looks like there’s a lot going on but there really isn’t:

  1. Set SOSApp to XCUIApplication, namely the app we’re testing
  2. Launch SOSApp
  3. Set up vars for the various buttons (regular and radio alike, SwiftUI doesn’t care, they’re all buttons. That’s good and annoying in equal parts) and text fields (staticTexts in SwiftUI Unit Test-ese)
  4. There’s the statement that clicks the new game button. The name used, “New Game” is the label on the button, that doesn’t have an accessibilityLabel. If I create one for that button, then the accessibilityLabel, not the button label would be used in that statement
  5. then we just check for the enabled state of things.

Note that I don’t test for the staticText item. Because I’ve yet to figure out how to. Because in all the online tutorials, no one can be arsed to show something so simple, and before you ask, no, Apple’s documentation is of zero use here. But then, Apple’s documentation is always of zero use, so no surprise there.

Really, I’m going to talk about commit move et al soon. promise.

What Happens When You Click The Button?

As it turns out, to quote “Caligula”, everything and nothing. Well, more everything. Sort of. Meh, how many chances to you get to quote a Tinto Brass classic?

Anyway, when you click the button, first there’s a check that you’re not trying to cheat by setting the player type to computer and then clicking. Honestly, there’s exactly one way that can happen, which is the starting player, always Blue is set to computer and you click the button before you click “start game” which I’ll get into in a minute. (it’s a hack, but it works). The no cheating checks:

 if (currentPlayer == "Blue") && (bluePlayerType == 2) {
       return
}

if (currentPlayer == "Red") && (redPlayerType == 2) {
      return
}

if the conditions of either if are met, nothing happens when you click. Next, we set the index of the last button clicked. This happens twice here, and I really only need once, but for now, it stays, I’ll deal with that during the “make it pretty” sprint.

 lastButtonClickedIndex = theGame.gridCellArr[myIndex].index

Now we’re going to call the aptly named buttonClickStuff(for: theGame.gridCellArr[myIndex].index). myIndex is defined before we get to the button click as:

let myIndex = (row * theGame.gridSize) + col

buttonClickStuff() takes the title of the button that was clicked, which can be “”, “S”, or “O”, the game array itself, the current player, and an array of currently unused buttons, and it returns a tuple consisting of a string value for title, a bool for the new commit button status, and a string for the current player. the call looks like:

let theTuple = buttonClickStuff(for: theGame.gridCellArr[myIndex].index, theTitle: theGame.gridCellArr[myIndex].title, myArray: theGame, myCurrentPlayer: currentPlayer, myUnusedButtons: arrayUsedButtonsList)

Within buttonClickStuff(), there’s a few critical things going on. First, we make mutable vars of the Commit button status, the title, and the current player, since as passed, those values are not mutable and we’ll need to…mutable them.

var theCommitButtonStatus: Bool = false
var theCellTitle: String = ""
var theCurrentPlayer: String = ""

Next we have a switch-case that is used to set the title of the button. This function runs every time you click the button, so this is how we rotate between the three titles;

 switch theTitle {
		//button is current blank, click sets it to "S"
		case "":
			theCellTitle = "S"
			theCommitButtonStatus = false
			//disable all other enabled buttons but the one that you're clicking
			disableOtherButtonsDuringMove(myGridArray: myArray, currentButtonIndex: myIndex)
		//button is currently "S", click sets it to "O"
		case "S":
			theCellTitle = "O"
			theCommitButtonStatus = false
			//disable all other enabled buttons but the one that you're clicking
			disableOtherButtonsDuringMove(myGridArray: myArray, currentButtonIndex: myIndex)
		//button is currently "O", click sets it to ""
		case "O":
			theCellTitle = ""
			theCommitButtonStatus = true
			//enable the other buttons that should be enabled since the button you just clicked
			//is now blank and you can click the other buttons to change your move
			enableOtherButtonsDuringMove(myGridArray: myArray)
		default:
			print("Something went wrong, try restarting the app")
	}

As we want all the other buttons to be disabled if the title of the button being clicked is not “”, to prevent cheating, we call disableOtherButtonsDuringMove() and pass it the array we passed to buttonClickStuff() and the index of the button being clicked. That function looks like:

 func disableOtherButtonsDuringMove (myGridArray: Game, currentButtonIndex: Int) {
	//iterate through the array
	for i in 0..<myGridArray.gridCellArr.count {
		//look for buttons that are not the current button and are not already disabled
		if !myGridArray.gridCellArr[i].buttonDisabled && i != currentButtonIndex {
			myGridArray.gridCellArr[i].buttonDisabled = true
		}
	}
}

It’s a pretty brute force function, but it works and ensures that only the current button being clicked is usable if the title of that button is S or O. If clicking the button returns the title back to “”, the last none-default option in the switch-case, we call enableOtherButtonsDuringMove() and pass it the game array we passed into buttonClickStuff():

 func enableOtherButtonsDuringMove (myGridArray: Game){
	for i in 0..<myGridArray.gridCellArr.count {
		if myGridArray.gridCellArr[i].title == "" {
			myGridArray.gridCellArr[i].buttonDisabled = false
		}
	}
}

Again, a dead stupid function that works and is fast. And since Game is a class, and class is always passed by reference, it just works. W00t!

The default case prints an error to the console. I’ve yet to hit it.

Once we’re out of the switch-case, we change the current player var:

 if myCurrentPlayer == "Blue" {
		theCurrentPlayer = "Red"
} else {
		theCurrentPlayer = "Blue"
}

I am not a clever programmer, I am not a programmer at all, I’m a sysadmin who sometimes has to write code. The way I write code shows that, but sometimes folks need a reminder.

Finally, we build our return tuple and well, return it:

 let theReturnTuple = (myTitle: theCellTitle, myCommitButtonStatus: theCommitButtonStatus, myCurrentPlayer: theCurrentPlayer)
return theReturnTuple

This dumps us back into ContentView, where we set the title of the button to the title returned by the tuple. This can be done back in buttonClickStuff(), and probably will be in the “make it pretty” sprint.

 theGame.gridCellArr[myIndex].title = theTuple.myTitle

Next, we use the Commit button status to set buttonBlank, which either enables the “Commit Move” button (new title is S or O) or sets it to/keeps it disabled (title is “”):

 buttonBlank = theTuple.myCommitButtonStatus

And since the label of the button is also using the same title as what we set with the return from buttonClickStuff(), that’s set automagically by setting the title of the button to theTuple.myTitle

Next up: committing the move, and oh there is some shit happing with that.

Computer Players

Yes I know I said commit move, but I just finished all the code for computer players which has a massive effect on commit, so we deal with the first parts of that.

With computer players, there’s three scenarios:

  1. Blue Computer, Red Human
  2. Blue Human, Red Computer
  3. Blue Computer, Red Computer

Since Blue always goes first on launch or for a new game, I needed a way to set blue to computer that didn’t start as soon as I changed the radio button, because that would be so annoying. I didn’t need it for Red, since Red never makes the first move. This is also not an issue once a move has been made regardless because once that happens, the player type radio buttons are disabled. So for Blue, when you click the “Computer” radio button, two things happen. We show the Start Game button and we disable all the buttons on the grid and make sure they’re all blank. (again, this can ONLY happen when it’s a new game AND Blue is computer. We don’t care for Red, Red never starts a game):

.onChange(of: bluePlayerType) {
			//we only hide when BOTH players are human
			if (bluePlayerType == 1) && (redPlayerType == 1) {
					showStartGameButton = false
					//if they change blue back to human, re-enable all the buttons
					enableOtherButtonsDuringMove(myGridArray: theGame)
					//modifying this because we don't need the start game button for the red player
					//only the blue, since blue ALWAYS goes first in a new game.
			} else if (bluePlayerType == 2) {
					showStartGameButton = true
					disableAllButtonsForBlueComputerPlayerStart(myGridArray: theGame)
			}
}

and here’s disableAllButtonsForBlueComputerPlayerStart():

func disableAllButtonsForBlueComputerPlayerStart (myGridArray: Game) {
	for i in 0..<myGridArray.gridCellArr.count {
		myGridArray.gridCellArr[i].title = ""
		myGridArray.gridCellArr[i].buttonDisabled = true
	}
}

It’s a brute force function, but it works. enableOtherButtonsDuringMove() is similarly simple:

func enableOtherButtonsDuringMove (myGridArray: Game){
	for i in 0..<myGridArray.gridCellArr.count {
		if myGridArray.gridCellArr[i].title == "" {
			myGridArray.gridCellArr[i].buttonDisabled = false
		}
	}
}

Since all buttons have a title of “” when it’s called here, this works. (We also use this function in a few other places, so not changing it was important.

So now the Start Button. First, we check to see if it should even exist:

if showStartGameButton {

Now we set up a buttonTitle var that’s an empty string (which stands a high percentage of going away, since we ended up not using it, but that’s for the next sprint), and we set up our button code (the comments are important and save me from having to narrate the thing here:

 //since we are effectively starting the game, and we don't actually click the commit move
//button, we replicate much of that here
//disable things
gamePlayerTypeDisabled = true

//since this only works on the first move, which is always blue, there's no point in caring
//about red, but it literally takes half a line of code to set it up, so why not?
if ((currentPlayer == "Blue") && (bluePlayerType == 2)) || ((currentPlayer == "Red") && (redPlayerType == 2)) {

	let theStartGameTuple = startGame(myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)
buttonTitle = theStartGameTuple.myButtonTitle
lastButtonClickedIndex = theStartGameTuple.myButtonToClick
}

And what does startGame() do? Literally starts the first computer move:

func startGame(myUnusedButtons: [Int], myGridArray: Game, myCurrentPlayer: String, myArrayUsedMemberCountdown: Int) -> (myButtonTitle: String, myButtonToClick: Int){
	//create title array, shuffle it, and set the first element to be the new button title
	let buttonTitles = ["S","O"]
	//shuffle the array to get as random a result as we can
	let shuffledArray = buttonTitles.shuffled()
	//always use the first item in this array
	let buttonTitle = shuffledArray[0]
	//get the size of the unused button array
	let sizeOfUnusedButtons = myUnusedButtons.count
	//get a random number from 0 to last element of the array
	//get the Index of the button we want to click
	let buttonToClickIndex = Int.random(in: 0..<sizeOfUnusedButtons)
	//this is necessary to avoid some gnarly out of range errors. Basically, the button uses the raw number to determine which one is getting modified
	//so if the size of the array is say 6, then index 5 is a valid choice. But, if there's no button with an index of 5, BOOM. So this ensures the value
	//of the array at the index is what is used, not the raw index itself:
	//if index is [0,1,3,4,6,7], the value at index 5 is 7, but there's now no available button with an index of 5, so if we use 5, it's bad.
	//this prevents that.
	let buttonToClick = myUnusedButtons[buttonToClickIndex]
	//we'll need to set the title. we can ignore the commit button status for now, we're not using it
	//once we set the title, we call commitMove() once we exit this function
	//set the title
	myGridArray.gridCellArr[buttonToClick].title = buttonTitle
	//build the return. Note, we don't seem to use the button title we return, think about dumping it.
	let computerPlayerTuple = (myButtonTitle: buttonTitle, myButtonToClick: buttonToClick)
	//return the tuple
	return computerPlayerTuple
}

The array index thing in there drove me spare for a hot minute, but yeah, it’s working. Now, we have a button with a title, and the index that was “clicked”, the same as if we’d manually set a button to S or O and clicked the Commit Move button. So this first move runs along fairly simply, in that as this code only runs for the first move, we never have to check to see if the game is over/won:

 //so now we have a title and a button clicked, let's call commit move
let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

arrayUsedButtonsList = theCommitTuple.myUnusedButtonArray
arrayUsedMemberCountdown = theCommitTuple.myCountDownInt

//this is true if that commit move created an SOS
var SOSFlag = theCommitTuple.mySOSFlag
//used for incrementing scores in general game because you can have one move create multiple SOS's
var SOSCounter = theCommitTuple.mySOSCounter
//even though we didn't "click" commit move, we want the commit button to be disabled
buttonBlank = true
//once we start the game regardless of how, we don't need start game to be usable
//since we can't change the player type mid game anyway
disableStartGameButton = true
//no one has won, and general game and SOS, increment score. note a win is impossible
//for start button since it can only be used as the first move
if (gameType == 2) && (SOSFlag) {
    	var incrementScoreTuple = incrementScore(myCurrentPlayer: currentPlayer, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore, mySOSCounter: SOSCounter)
		redPlayerScore = incrementScoreTuple.myRedPlayerScore
		bluePlayerScore = incrementScoreTuple.myBluePlayerScore
}
//we don't check for winning game here, the button isn't enabled for that
//get is game over/player won flag

//change the player
currentPlayer = changePlayer(myCurrentPlayer: currentPlayer)

//if both players aren't computer, we want to bug out here. This button only gets clicked if
//both players are computer, so we can cheat with this:
//red is a human, bug out of this code
if (currentPlayer == "Red") && (redPlayerType == 1) {
		return
}

Okay, so we’ve handled the state where Blue is a computer and we clicked Start Game. There’s only one other condition we care about in this button code, and that is if Red is ALSO a computer, i.e. computer v. computer. This is where we have a while loop that is essentially a duplicate of a big chunk of the Commit Move button code. We literally check for either player being a computer at this point, because the current player will change. Then we do a lot, including re-running startGame():

 while !playerWon {
		//check for computer player. Since there's no real difference in the code other than the test,
		//we can collapse this into a single thing.
		//right now the else is there as a test statement, will be removed
		if ((currentPlayer == "Blue") && (bluePlayerType == 2)) || ((currentPlayer == "Red") && (redPlayerType == 2)) {

		//make the computer move
		let theStartGameTuple = startGame(myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)
		buttonTitle = theStartGameTuple.myButtonTitle
		lastButtonClickedIndex = theStartGameTuple.myButtonToClick

		//here's where we duplicate a lot of code, but we only do it once, so it's fine.
		//we can look at fixing it in the next sprint maybe.
		let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

		arrayUsedButtonsList = theCommitTuple.myUnusedButtonArray
		arrayUsedMemberCountdown = theCommitTuple.myCountDownInt
		SOSFlag = theCommitTuple.mySOSFlag
		SOSCounter = theCommitTuple.mySOSCounter
		buttonBlank = true
		disableStartGameButton = true

		if (gameType == 2) && (SOSFlag) {
				var incrementScoreTuple = incrementScore(myCurrentPlayer: currentPlayer, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore, mySOSCounter: SOSCounter)
				redPlayerScore = incrementScoreTuple.myRedPlayerScore
				bluePlayerScore = incrementScoreTuple.myBluePlayerScore
		}

		var gameOverTuple  = isGameOver(myArrayUsedMemberCountdown: arrayUsedMemberCountdown, myGameType: gameType, myGridArray: theGame, mySOSFlag: SOSFlag, myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore)

		gameWasDraw = gameOverTuple.myGameIsDraw
		generalGameWinner = gameOverTuple.myGeneralGameWinner
		playerWon = gameOverTuple.myGameIsOver

		if playerWon {
				return
		}

		if !playerWon {
				currentPlayer = changePlayer(myCurrentPlayer: currentPlayer)
		}

	} else if ((currentPlayer == "Blue") && (bluePlayerType == 1)) || ((currentPlayer == "Red") && (redPlayerType == 1))  {
			//print("Next player is \(currentPlayer) and is a human player")
	}
}

A lot of this is obvious, but some of it isn’t. For example, the real point of playerWon is to pop an alert that’s tied to the Commit Move button section (we’ll get there, calm down). incrementScore() looks like:

func incrementScore(myCurrentPlayer: String, myRedPlayerScore: Int, myBluePlayerScore: Int, mySOSCounter: Int) -> (myRedPlayerScore: Int, myBluePlayerScore: Int){
	var bluePlayerScore = myBluePlayerScore
	var redPlayerScore = myRedPlayerScore

	if myCurrentPlayer == "Blue" {
		//incrementing by number of SOS's created, not just by one no matter what
		bluePlayerScore = bluePlayerScore + mySOSCounter
	} else {
		redPlayerScore = redPlayerScore + mySOSCounter
	}

	let playerScoreTuple = (myRedPlayerScore: redPlayerScore, myBluePlayerScore: bluePlayerScore)
	return playerScoreTuple
}

This only gets called in a general game, where you have a score, and the first SOS doesn’t win. We do the counter thing (which we’ll see in Commit Move) because in a general game, one move can create up to 8 SOS’s (I tested). So it gets called, then returns all the scores that have been incremented. As a matter of practice, only one is incremented, but doing it this way is easier and lets me use one function with one set of calling parameters and one return tuple, which makes it easier on me and no penalty on play. The “easier on me” is really important here. I really love that I can return tuples, that has SAVED my sanity

Then we call isGameOver():

func isGameOver(myArrayUsedMemberCountdown: Int, myGameType: Int, myGridArray: Game, mySOSFlag: Bool, myRedPlayerScore: Int, myBluePlayerScore: Int) -> (myGameIsOver: Bool, myGameIsDraw: Bool, myGeneralGameWinner: String) {

	var gameIsOver: Bool = false
	var gameIsDraw: Bool = false
	//we'll need this lateer
	var gridCountdown = myArrayUsedMemberCountdown
	var gameWinner: String = ""

	//check to see if gridCountDown is zero. If it is, game is over no matter what
	if gridCountdown > 0 {
		//grid countdown is not zero, check for other things
		if myGameType == 1 {
			//simple game
			if mySOSFlag {
				//there's an SOS, game is over
				//grid countdown to zero
				//gridCountdown = 0
				//disable all the buttons
				for i in 0..<myGridArray.gridCellArr.count {
					myGridArray.gridCellArr[i].buttonDisabled = true
				}
				//set the game over flag
				gameIsOver = true
				gameIsDraw = false
			} else {
				//mySOSFlag is not true, game is not over
				gameIsOver = false
				gameIsDraw = false
			}
		//we may not have to do anything here for a general game, since the SOS flag being true and game over being false runs the
		//score increment outside of this function
		}
	} else if gridCountdown <= 0 {
		//0 or less the game is over regardless of winner or not or game type
		//disable the game board
		//also, you CAN'T win a general game until all the squares are filled
		for i in 0..<myGridArray.gridCellArr.count {
			myGridArray.gridCellArr[i].buttonDisabled = true
		}
		//simple game draw
		if myGameType == 1 {
			if mySOSFlag {
				//there's a winner, so not a draw
				gameIsOver = true
				gameIsDraw = false
			} else {
				//no winner, is a draw
				gameIsOver = true
				gameIsDraw = true
			}
		} else {
			//general game all squares are filled is different
			//regardless of who won, game is over
			gameIsOver = true
			//red won
			if myRedPlayerScore > myBluePlayerScore {
				gameWinner = "Red"
				gameIsDraw = false
				//blue won
			} else if myBluePlayerScore > myRedPlayerScore {
				gameWinner = "Blue"
				gameIsDraw = false
				//Draw/no one won
			} else {
				gameWinner = "Draw"
				gameIsDraw = true
			}
		}
	}
	let gameIsOverTuple = (myGameIsOver: gameIsOver, myGameIsDraw: gameIsDraw, myGeneralGameWinner: gameWinner)
	return gameIsOverTuple
} 

If playerWon is true, the alert pops, we’re done. If it’s not, keep going and run that while loop again. Also, if the game isn’t over, we need to change the player, aka changePlayer():

func changePlayer(myCurrentPlayer: String) -> String {
	var newPlayer: String = ""
	if myCurrentPlayer == "Blue" {
		newPlayer = "Red"
	} else if myCurrentPlayer == "Red" {
		newPlayer = "Blue"
	}
	return newPlayer
}

This is simple enough that I hope I don’t have to explain it. Because I am not going to. Finally, there’s the label, the accessibilityLabel, the disabled status and me being paranoid about game button states:

 label: {
	Text("Start Game")
}
.onAppear(perform: {
//enable the start game button if it is visible
disableStartGameButton = false
})
.disabled(disableStartGameButton)
.accessibilityLabel("startButton")

Okay, we should be good to go for Commit Move. Maybe. As Skeletor would say: UNTIL WE MEET AGAIN!

Commit Move

Oh god, this will be a long section, Commit Move is one of the core parts of the game. Not that anything so far has been short, but still…

So in a manual game, that is with at least one human player, to finish making a move, you have to click the “Commit Move” button. This solves a few issues, like how to know when someone has actually made the move they want. Requiring an explicit action was the easy way. For some values of easy that is. Honestly, while there is a lot of code tied to “commit move” the basic functionality is pretty easy.

Having an explicit “commit move” button also lets us avoid a lot of problems with this one line, the first in the button action code:

gamePlayerTypeDisabled = true

Once you click “Commit Move”, the first thing it does is disable the game type and player type radio buttons, so you can’t change game or player type mid-move. The only way to re-enable them is the “new game” button or changing the grid size.

Next we have the functionality for recording a move in a manual game. This involves some controls that didn’t exist when I first started this post (because they were a stretch goal) but I’m so far ahead on this project, may as well include them) The record button is in ContentView:

 HStack(alignment: .top) {
			//record game button
			Toggle("Record Game:", isOn: $recordGame)
				.toggleStyle(.switch)
				.modifier(basicTextModifierNoFrame())
				.padding(.trailing, 10.0)
				/*.onChange(of: recordGame) {
					print("Toggle State is: \(recordGame)")
				}*/
			//playback button
			Button {
				//do stuff
			} label: {
				Text("Playback")
			}
			.disabled(playbackDisabled)
}

It’s a basic toggle button that when turned on, sets recordGame to true. The playback button is disabled by default unless the record button is enabled, and a game has finished. Then it’s enabled if you want to replay the game. The record button is set to Off/False on application launch, pressing the New Game button, or changing the grid size:

 self.recordGame = false
playbackDisabled = true

Back to the “Commit Move” button action code. First it tests for recordGame being true. Then if it is, it calls the addRecordedMove function, passing it the gameMoveRecord struct array, the index of the button that was clicked and is being committed via commit move, the array of game buttons, and the name of the current player. The gameMoveRecord struct array is an array of moveRecord structs that each hold important data about a game move, namely the button index, the title of the button (S or O), the name of the player, and the color of the button:

struct moveRecord {
	var moveIndex: Int
	var moveTitle: String
	var movePlayer: String
	var moveColor: Color
}

The array itself is an @State var:

@State var gameMoveRecord = [moveRecord]()

And it keeps a record of the moves made in a recorded game. So when Commit Move is clicked, and recording is enabled, we run this:

if recordGame {
			gameMoveRecord = addRecordedMove(myGameRecord: gameMoveRecord, myCommittedButtonIndex: lastButtonClickedIndex, myGridArray: theGame, myCurrentPlayer: currentPlayer)
}

which calls, as we said, addRecordedMove():

func addRecordedMove(myGameRecord: [moveRecord], myCommittedButtonIndex: Int, myGridArray: Game, myCurrentPlayer: String) -> [moveRecord] {
	var tempGameRecord = myGameRecord
	let theMove = myGridArray.gridCellArr[myCommittedButtonIndex]
	let theTempMoveRecord = moveRecord(moveIndex: theMove.index, moveTitle: theMove.title, movePlayer: myCurrentPlayer, moveColor: theMove.backCol)
	tempGameRecord.append(theTempMoveRecord)
	return tempGameRecord
} 

That’s pretty straightforward: it creates a temp mutable version of the moveRecord array, a temp version of the commited move index (mostly to make the grid array reference easier to read.) It creates an instance of the moveRecord struct with all the current move data, and appends that onto theTempGameRecord which it then returns back to ContentView

Next, the Commit Move button action calls commitMove the function:

commitMove()

let theCommitTuple = commitMove(myCommittedButtonIndex: lastButtonClickedIndex, myUnusedButtons: arrayUsedButtonsList, myGridArray: theGame, myCurrentPlayer: currentPlayer, myArrayUsedMemberCountdown: arrayUsedMemberCountdown)

commitMove() in and of itself isn’t that long, but it does a lot, both the function parameters are pretty long:

func commitMove(myCommittedButtonIndex: Int, myUnusedButtons: [Int],myGridArray: Game, myCurrentPlayer: String, myArrayUsedMemberCountdown: Int) -> (myUnusedButtonArray: [Int], myCountDownInt: Int, mySOSFlag: Bool, mySOSCounter: Int) {

First, we create some mutable copies of the vars passed to the function:

var theTempArray = myUnusedButtons
var theTempCounter = myArrayUsedMemberCountdown

checkForSOS()

(Going to put this into sections for Commit Move, because this is SO long)

Next, we want to check for an “SOS” because if it’s a simple game, an SOS ends it, and if not, then we need to know to increment the score. So we call checkForSOS(), which does do a lot, because it’s kind of complicated. Remember, in a general game, you can have one move create multiple SOS’s, up to 8 in fact, (I checked) and there’s different patterns. An S can create an SOS in a spoke pattern from it. Two vertical directions, two horizontal, and four diagonal, since an S is always at the end of an SOS. So we have to check for all 8. An O, being in the middle, really only has four: one vertical, one horizontal, two diagonal.

So, depending on the letter placed, we can have anywhere from one to 8 SOS’s happening. Which means we need 8 checks for S and four for O. I decided, in the interest of just having the function be long, but simple, to check for all possibilities. It takes zero time on a human scale, and that allows it to be really straightforward, just a series of if statements all of which are checked depending on letter, eight for S, four for O. First, the call to checkForSOS()

let theSOSTuple = checkForSOS(myGridArray: myGridArray, myLastButtonClickedIndex: myCommittedButtonIndex, myGridSize: myGridArray.gridSize, myCurrentPlayer: myCurrentPlayer)

Here’s the actual def. for checkForSOS:

func checkForSOS(myGridArray: Game, myLastButtonClickedIndex: Int, myGridSize: Int, myCurrentPlayer: String) -> (mySOSFlag: Bool, mySOSCounter: Int)

Because we can have multiple SOS’s in a move, we have to count those:

var SOSCounter: Int = 0

Now, because of how letters can be placed on an edge button, we need to make sure we treat those differently than one placed dead in the middle. What you’ll notice is that I only check for right and bottom edges, and I have a leftmost and rightmost flag. The right and bottom edges are the only ones we have to calculate. The other two are either the x-coord being 0 (leftmost edge) or the y-coord is 0, (topmost edge), so we don’t have to calculate those values:

let buttonRightEdgeCheck = myGridSize - 1
let buttonBottomEdgeCheck = myGridSize - 1
var buttonLeftmostFlag: Bool = false
var buttonRightmostFlag: Bool = false

Next, we need a flag for the existence of an SOS:

var SOSFlag: Bool = false

Now, a switch statement to do the initial flag sets:

switch myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord {
		case 0:
			buttonLeftmostFlag = true
		case buttonRightEdgeCheck:
			buttonRightmostFlag = true
		default:
			buttonLeftmostFlag = false
			buttonRightmostFlag = false
}

Now we set up some other vars like the current title (I could directly refer to it, but it’s tedious do do that, and makes the code a bit hard to read) and the distance from the right and bottom edges of the button:

let theCurrentButtonTitle = myGridArray.gridCellArr[myLastButtonClickedIndex].title

let distanceFromRight = buttonRightEdgeCheck - myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord

let distanceFromBottom = buttonBottomEdgeCheck - myGridArray.gridCellArr[myLastButtonClickedIndex].yCoord

Again, the top and left edges are zero for one of the coordinates, those are easy to check inline. So we do.

First we do the S checks. To make things a bit easier for me (important!), I group the S checks. First we do the LTR (Left-To-Right) checks. Because of this, we need to make sure the buttonRightMost flag is false, (if it’s true, the button is on the far right edge, LTR is not valid), and then for LTR, we want to make sure the button is at least three buttons from the edge, since LTR, SOS is three buttons. Less than three, again, invalid. If both of those are true, then an SOS is possible, so we go ahead and check:

if theCurrentButtonTitle == "S" {
		if !buttonRightmostFlag {
			if distanceFromRight >= 2 {
				let nextCellIndex = myLastButtonClickedIndex + 1
				let secondCellIndex = myLastButtonClickedIndex + 2
				if (myGridArray.gridCellArr[nextCellIndex].title == "O") && (myGridArray.gridCellArr[secondCellIndex].title == "S") {
					setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: nextCellIndex, myThirdIndex: secondCellIndex, myGridArray: myGridArray)
					SOSFlag = true
					SOSCounter += 1
				}
				if distanceFromBottom >= 2 {
					let nextCellIndex = (myLastButtonClickedIndex) + (myGridSize + 1)
					let secondCellIndex = (nextCellIndex) + (myGridSize + 1)
					if (myGridArray.gridCellArr[nextCellIndex].title == "O") && (myGridArray.gridCellArr[secondCellIndex].title == "S") {
						setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: nextCellIndex, myThirdIndex: secondCellIndex, myGridArray: myGridArray)
						SOSFlag = true
						SOSCounter += 1
					}
				}

Of COURSE there’s another function in the function we called from a function. In this case, if there’s an SOS, we want to set the color of all three buttons to that of the player that made the call. I thought about a gradient for cases where it was already a color, but that was a really weirdly large amount of work to implement, so I didn’t. our next function is setSOSButtonColor()

setSOSButtonColor()

This does what it seems, sets the color for all three buttons in the SOS. Here’s the function definition:

func setSOSButtonColor(myCurrentPlayer: String, myFirstIndex: Int, mySecondIndex: Int, myThirdIndex: Int, myGridArray: Game)

And here’s the actual code:

func setSOSButtonColor(myCurrentPlayer: String, myFirstIndex: Int, mySecondIndex: Int, myThirdIndex: Int, myGridArray: Game) {
	if myCurrentPlayer == "Blue" {
		myGridArray.gridCellArr[myFirstIndex].backCol = .blue
		myGridArray.gridCellArr[mySecondIndex].backCol = .blue
		myGridArray.gridCellArr[myThirdIndex].backCol = .blue
	} else if myCurrentPlayer == "Red" {
		myGridArray.gridCellArr[myFirstIndex].backCol = .red
		myGridArray.gridCellArr[mySecondIndex].backCol = .red
		myGridArray.gridCellArr[myThirdIndex].backCol = .red
	}

}

It’s pretty simple: we pass it the three buttons we care about and the Game grid. If the current player is Blue, we set the background colors of the buttons to blue. If Red, red. Since Game is a class and is pass by reference, we don’t need a return, so it’s a void function. Now, back to checkForSOS()!

checkForSOS() cont’d

All the S checks work about the same. First, we make sure we can validly check for the SOS we want. Then because SOS is three buttons, we set up the second and third buttons (nextCellIndex and secondCellIndex.) Initially, I thought this was going to be a lot of tedious xCoord and yCoord comparisons, but I realized as I was working it out that I didn’t need any of those, just the indexes. For example, if I’m checking for a LTR horizontal, and the first button has an index of n, then the second button’s index is n + 1 and the third is n + 2. The rest were similarly simple:

 LTR Diag Down is (index) + (gridsize + 1)
 LTR Diag Up is (index) – (gridsize -1)
Vertical Up is index – gridsize
Vertical Down is index + gridsize
RTL Horizontal is index – 1
RTL Diag Down is (index) + (gridsize – 1)
RTL Diag Up is (index) – (gridsize + 1)

That is far more simple to implement than some “if x is increasing and y is decreasing” crap. Then if nextCellIndex is an O and secondCellIndex is an S, we have an SOS, call setSOSButtonColor(), set SOSFlag to true and increment SOSCounter.

I did this all as a series of IF statements because while i could have had yet another function (YAF), then I’d have a fairly complex function call, and within that function, the only “repetition” i’d save would be in calling setSOSButtonColor() and setting the SOSFlag to true and incrementing the SOSCounter. So 36 or so lines of code, but now I also am slinging function calls back and forth, which is resource overhead, and it makes tracking what’s happening if something goes wrong more tedious.

More importantly, it really doesn’t make the code significantly more readable or easier to debug, it doesn’t radically improve the speed of the function execution, and my setup code would be somewhat longer anyway…in other words, I didn’t see how doing that didn’t make anything better enough to justify the work, and it didn’t solve any actual problem.

So the S if chain does its thing, and then we get to the O if chain, which is a little different. Because O is in the middle, we only need four checks as there are only four unique combinations for an O since it is always in the middle, and we have to check on either side of the O. So the basic functionality is the same, we just tweak some things. For example, while an S has to have two squares between it and an edge, an O only needs one. An S in the corner can have three possible SOS’s, an O in the corner can have none. For S, we only care about boundary checking in one direction, for O we have to check in two. That kind of thing:

if theCurrentButtonTitle == "O" {
		 if (myGridArray.gridCellArr[myLastButtonClickedIndex].xCoord >= 1) && (distanceFromRight >= 1) {
			let rightAdjacentCellIndex = myLastButtonClickedIndex + 1
			let leftAdjacentCellInded = myLastButtonClickedIndex - 1
			if (myGridArray.gridCellArr[rightAdjacentCellIndex].title == "S") && (myGridArray.gridCellArr[leftAdjacentCellInded].title == "S") {
				setSOSButtonColor(myCurrentPlayer: myCurrentPlayer, myFirstIndex: myLastButtonClickedIndex, mySecondIndex: rightAdjacentCellIndex, myThirdIndex: leftAdjacentCellInded, myGridArray: myGridArray)
				SOSFlag = true
				SOSCounter += 1
		}
}

It’s similar to an S check. We set the index for the buttons on either side of the O, and if both are an S, then setSOSButton(), SOSFlag = true, SOSCounter increments. Once all the checks are done, we pack SOSFlag and SOSCounter into a tuple and return them to commitMove():

let myCheckForSOSTuple = (mySOSFlag: SOSFlag, mySOSCounter: SOSCounter)
return myCheckForSOSTuple

commitMove() cont’d

Signing PowerShell Scripts on macOS

During my PowerShell session at MacAdmins 2023, someone asked if you could sign a PowerShell script on macOS. Doing so on Windows is trivial and well-documented elsewhere, I won’t be talking about that here.

The quick answer: yes, you can. You need a code signing cert, for this I’m just using my cert from Xcode. The syntax is simple:

codesign --force --sign 'Developer ID Application: <your name>' -i '<bundle id for script>' '<path to script>/<powershell script>.ps1'

That will take care of the signing, and it is a valid sig:

codesign -dv --verbose=4 /<path to script>/<powershell script>.ps1
Executable=/<path to script>/<powershell script>.ps1
Identifier=<bundle id for script>
Format=generic
CodeDirectory v=20200 size=181 flags=0x0(none) hashes=1+2 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha1=<hash>
CandidateCDHashFull sha1=<hash>
CandidateCDHash sha256=<hash>
CandidateCDHashFull sha256=<hash>
Hash choices=sha1,sha256
CMSDigest=<digest>
CMSDigestType=2
Page size=none
Launch Constraints:
None
CDHash=<hash>
Signature size=9050
Authority=Developer ID Application: <name> (<team id>)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=<time stamp>
Info.plist=not bound
TeamIdentifier=796488VG95
Sealed Resources=none
Internal requirements count=2 size=236

Now, here’s where it can get weird. On Windows, the signature is embedded in the script itself at the bottom. On macOS, the sig is in the extended attributes of the script file:

ls -ale@
-rw-r--r--@ 1 <user> staff 23725 Jul 20 09:47 <powershell script>.ps1
com.apple.cs.CodeDirectory 145
com.apple.cs.CodeRequirements 236
com.apple.cs.CodeRequirements-1 181
com.apple.cs.CodeSignature 9050
com.apple.lastuseddate#PS 16

This means that if you move that script to some storage that strips those ea’s, you lose the signature. Also, you can’t notarize the script, which makes some form of sense given how notarization works. But yes, you can sign PowerShell scripts on macOS.

PowerShell version of Rich Trouton’s “Listing and downloading available macOS installers…” script

The original shell version is here: https://derflounder.wordpress.com/2023/06/30/listing-and-downloading-available-macos-installers-using-apples-softwareupdate-tool/#more-12084. The basics are simple: use the sofwareupdate command to display a list of available updates for a given Mac and let the user pick the one they want to download. This doesn’t install it, just downloads them.

Because I have an upcoming presentation at MacAdmins 2023 on PowerShell on the Mac, I wanted to do a PowerShell version of Rich’s shells script, mostly to see how it would work out. Turns out it worked pretty well, once I got past one hump, namely being able to create an array of items that allows for duplicates. Doing that with a hashtable isn’t possible and most of the workarounds are tedious as hell, so I took advantage of a custom feature in PowerShell: I created a custom class.

The entire script is at: https://github.com/johncwelch/List-and-Download-available-macOS-Updates, but we’ll do the tour here.

The first line is pretty basic, it sets up the command we’ll use to actually download the installer by version number, so that when we run the command, all we have to do is tack on the version:

$getFullInstallerByVersion = "/usr/sbin/softwareupdate --fetch-full-installer --full-installer-version " 

The next fourteen lines are where we define our custom class, which is an object with three string parameters: an index, a title, and a version:

class OSUpdate {
     [string]$index
     [string]$title
     [string]$version;

     OSUpdate(
          [string]$index,
          [string]$title,
          [string]$version
     ){
          $this.index = $index
          $this.title = $title
          $this.version = $version
     }
}

By doing this, we avoid having to work around hashtable issues or what have you, and we can take advantage of PowerShell’s object syntax.

Next we set up our two arraylists (think NSMutableArray), the first will hold the raw output from the software update command to list the available updates, the second will be an arraylist of our custom objects:

[System.Collections.ArrayList]$softareUpdateArrayList=@()
[System.Collections.ArrayList]$availableOSUpdates=@() 

Now we run the command to get the full list of installers, and shove it into the first arraylist:

$softareUpdateArrayList = /usr/sbin/softwareupdate --list-full-installers 

Then we remove the first two lines, because they’re not useful for our needs. It’s not the most elegant thing, but it works, and only two lines:

$softareUpdateArrayList.RemoveAt(0) 
$softareUpdateArrayList.RemoveAt(0)  

Now we iterate through that array and for each item we:

  • shove the index of the current entry into $theIndex as a string
  • remove the first two characters in the string (* ) via Substring(2)
  • we split each entry into a separate string array on the comma via Split(“,”) which creates a 5-item string array for the current entry
  • We take the first item of that string array, $OSVersionStringArray[0], and split it on the colon, which gives us another string array ($OSTitleTemp) with two items: the word “Title” and the actual name of the OS version with a leading space via Split(“:”)
  • We take the second item, the actual name, strip off the leading space with Substring(1) and put that into $OSTitle
  • We repeat the process for the title with the second item of the main array in our loop, $OSVersionStringArray[1], and shove just the version number into $OSVersion
  • We create a new OSUpdate Object called “$OSUpdateItem” consisting of the index ($theIndex), the title, $OSTitle, and the version ($OSVersion) of the current item
  • Finally we add that into the $availableOSUpdates arraylist, piping the output to Out-Null so you avoid PowerShell’s love of counting off iterations through an array
foreach ($installer in $softareUpdateArrayList  ) {
     #get the index of the item
     [string]$theIndex = $softareUpdateArrayList.IndexOf($installer)
     #remove the *<space> chars in each line
     $installer = $installer.Substring(2)
     #split on the comma to create a string array 
     $OSVersionStringArray = $installer.Split(",")
     
     #get just the title from the string array
     $OSTitleTemp = $OSVersionStringArray[0].Split(":")
     #grab just the title and delete the leading space
     $OSTitle =  $OSTitleTemp[1].Substring(1)

     #get just the version number from the string array
     $OSVersionTemp = $OSVersionStringArray[1].Split(":")
     #grab just the version and delete the leading space
     $OSVersion = $OSVersionTemp[1].Substring(1)
     
     #create new OSUpdate Object
     $OSUpdateItem = @([OSUpdate]::new($theIndex,$OSTitle ,$OSVersion))

     #add the item to the arraylist of updates, suppress index output
     $availableOSUpdates.Add($OSUpdateItem)|Out-Null
} 

Now, we list out the available updates and ask the user to enter the index of the desired update. We pipe the Write-Host lines to Out-Host, because otherwise, Read-Host will suppress all of it until you enter an index. That you can’t see because Read-Host is suppressing it. Because we’re using Out-Host, we specify newlines with `n in the Write-Host lines:

Write-Host "The available updates for this Mac are:`n" | Out-Host 

This next line sets up the headers for the list, colors them green via -ForegroundColor Green, (kind of a tradition in PowerShell when you’re displaying array contents with content names), and use escape characters to underline the words in the line. `e[4m starts the underlining, and `e[24m ends it:

Write-Host "`e[4mIndex`tTitle`t`tVersion`e[24m`n" -ForegroundColor Green| Out-Host 

Next, iterate through the arraylist of available updates, with each entry as a tab-delimited (`t) string:

foreach ($update in $availableOSUpdates) {
     $update.index + "`t" + $update.title + "`t" + $update.version | Out-Host  
} 

The Read-Host line to get the index number as an int:

[Int32]$desiredUpdate = Read-Host "Enter the index of the update you want to download" 

Get the object data from the array for that index:

$updateToFetch = $availableOSUpdates[$desiredUpdate] 

And finally, tell the user we’re downloading the installer, build the full command, and run the command via Invoke-Expression:

Write-Host "Downloading Installer"
#build the command
$theSoftwareUpdateCommand = $getFullInstallerByVersion + $updateToFetch.version
#run the command via Invoke-Expression
Invoke-Expression $theSoftwareUpdateCommand 

That’s the script, and it’s only about ten lines longer than Rich’s shell script, but thanks to PowerShell understanding more than “everything is text”, it’s a bit more readable, and we don’t need to do as much cat/tail/awk/build/grep, etc. If we want to say, add the size parameter, modifying the script for that would be pretty easy, since we’re not slashing strings but manipulating objects and arrays.

Resurrecting An Apple Watch

Recently, I had my Series 6 Apple Watch die spectacularly. I’d been out working in a friend’s garden, came in and was washing my hands with the watch on. It’s water-resistant right? Well, it may be that, but it does not appear to be water and soap resistant.

Suddenly (TMI warning: while I’m taking a leak) it decides to start trying to make emergency calls, and I can’t stop it. Like to where as I’m trying to pee, I’m dealing with emergency notifications texts going out, trying to stop it from calling 911, telling people I’m fine, telling 911 operators I’m fine, not peeing all over the bathroom…it put the FU in fun, tell you what.

So I get it shut off and when I put it on power, it just does the forlorn Apple icon blink.

Fuque

I go to the local Apple Store, and they confirm, it’s daid. So I get a new SE and just leave the corpse on top of my laser printer.

A few weeks later on a whim, I put it on power and it looks like it’s coming back for a few, but then it goes back to the logo. But for a few seconds, it was normal…a plan is hatched. I take it off power, and then go into Find My… and set it so that the second it comes back onto the internet, it gets wiped.

Wait a day or two, put it back on power, and boom, wiped. Wait a couple days, and go through the setup, and…it appears to be back. Updating it to watchOS 9.5.2 as I type this. I don’t know if this is a true fix or it’s going to freak out again, but for now, I have two Apple Watches, (Series 6 and an SE), that both seem to be working.

Hopefully, this may help someone. Standard idiot disclaimer: This will not do diddly for physical damage. If you wrecked your watch with a nail gun, this is not going to do a damned thing for you. But if it’s a badly corrupted OS/software issue, this process could help, so I put it out there for y’all.

Vision Pro Redux

So now that I’ve had some time to think about the VP, I think so much of the criticism (especially the radically stupid stuff) is based on some key bad points.

This is not a competitor for Oculus

or the PSVR or any of the low-end “pure” VR rigs. I mean, just in terms of hardware, you’re comparing Fisher-Price to AMG. Even the just-announced Meta Quest 3 isn’t even close to the resolution of the VP, much less the existing MQ 2. Also according to Tom’s Guide, there’s no eye tracking. So that’s an entire set of capabilities the MQ 3 doesn’t have at all. There’s the difference in sensors, sensor processing, on and on.

Which is fine, because they’re two devices that are only vaguely similar. A quick look at the Quest software store confirms it. It’s mostly game, and some workout stuff that’s basically a VR version of what the Wii was doing a decade ago, and some entertainment apps. None of that is bad, clearly Meta understands the customer base it wants, but there’s nothing *but* games/entertainment, so really, the Quest is a videogame console. The VP is not, well not just that, and a game that makes good use of the eye tracking could be amazing. Also, honestly, based on what I’ve seen, the graphics in Quest games are not all that. But it serves its customers well, which is kind of a point.

Other High End AR/VR headsets

So lets look at the space the VP actually lives in. If we look at the ThinkReality VRX, which retails for around $1300/$1400, we something that’s a bit better. It’s got a Snapdragon XR2+ CPU/SOC, somewhat better resolution than the MQ3, and weighs more than the VP (if the 1lb weight for the VP is correct.) The VRX has fewer sensors, and requires joysticks. The optics are solid, but less resolution than the VP. (estimating 3391×3391 for the VP if the numbers I’ve seen are right.) But the biggest advantage the VP has here is VisionOS.

The VRX is running a (assumedly) custom version of Android (as is the MQ 3). Android’s a capable OS, but it’s not designed for AR/VR the way VisionOS is. Having an OS that is specifically designed from the ground up for the device is a real advantage. Taking a general-purpose phone OS and hacking at it to run on a device it wasn’t designed for tends to lead to compromises that aren’t always great, as anyone who tried to use the really bad Phone versions of Windows in the pre-iPhone days can attest to. You don’t see a lot of people pining for Windows Mobile, there’s a reason. In addition, given how Apple seems to be doing things, because of the hardware commonalities, it looks like any application that can run natively on Apple Silicon should be at least functional on the VP. The demonstrated support for things like physical keyboards/trackpads and game controllers shows that.

The Microsoft HoloLens 2 is probably the closest competitor to the VP, but even it’s outclassed in terms of tech specs. Is it a good system, yes. Is it capable of what the VP should be able to do? Doubtful, the chipset in it, the Snapdragon 850 doesn’t compete well with the M2 at all. The HoloLens does at least have a wider range of apps than the MQ 3 does, but given the HoloLens’s focus, that makes sense.

I don’t want to come across as bagging on Qualcomm. They’re designing chips for a wide range of use by a wide range of customers. Apple is designing for itself, that’s a big advantage. Apple doesn’t have to make the compromises Qualcomm does. If Apple decides an SOC doesn’t need 5G, they just don’t put it on. Qualcomm’s major market is phones and similar, so everything it sells has to fit into that use requirement. Qualcomm makes good kit, but their needs are different than Apple’s and the product shows that.

I’m not saying don’t make comparisons but come on y’all, think first. The price difference between the VP and the MQ 3 in and of itself is just silly.

Use Cases

This is the big question isn’t it…not the price or the specs but what can you really use it for, who can benefit from it. Well, as it turns out, a lot of people can.

In industrial manufacturing/engineering, well, the keynote was literally showing examples of it. Everyone focused on Unity, but PTC (who make a lot of Model-Based-Engineering applications) being in the keynote was big. PTC and their Creo suite have a lot to do with manufacturing and design, and being able to work with others directly on a model before it goes into manufacturing in a virtual space would be huge, especially if things like DFM (Design for Manufacturing) are included. That would allow designers and engineers to test a component to see if it’s actually buildable as designed. If any errors show up, they could be fixed and retested in the virtual space well before it gets to the “make it real” stage.

In terms of Theatre (plays), set designers, costumers, directors, builders, actors, everyone could benefit. The ability to “build” the stage, and see what’s involved in the construction before you even start gathering materials, being able to set up blocking well before the set is built, doing test fits and dressings and see how it will look before cloth is committed? Not a small benefit. Doing full light and sound simulations with the “physical” set and the actual “people” would be another big benefit. Need to simulate how a full house would affect the sound? Way easier to do that with the VP. The cost of the VP would be an issue for a lot of theaters, but the benefits are clear. Same thing for film, all the issues of set design et al exist, and VP would be a massive help there.

Urban planning, traffic flow, on and on. The compute capability of the VP and the fact that it’s not some weird standalone thing that can’t work with other tools, but can integrate with macOS and iOS, and that its OS is designed for those things are critical features for the VP, and I’m hoping that as we get closer to release, we’ll see more vendors, Autodesk for one, tuning their apps for the VP.

Folks really need to not directly compare the VP with anything else out there, it’s currently in a class by itself.

Vision Pro Revives Bad Hot Takes Industry

No, really.

Okay, backstory for like the one person who doesn’t know: Apple today, 5 June 2023 announced their AR Goggles, the Vision Pro. VERY basic specs:

  • M2 and a new dedicated sensor processing chip, the R1
  • Internal display is 23M pixels, so over 4K
  • No joystick needed, uses hand motions
  • Has an external battery pack/can plug into external power, which looks dorky, but keeps you from wearing a battery on your head
  • Uses an iris scanner to unlock ala FaceID, has secure enclave
  • Display looks like a cross between Iron Man and Minority Report
  • 20+ sensors including IR and LIDAR
  • Announced Price is $3499

The rest, you can watch the WWDC ’23 Keynote for.

Yes, it’s expensive, although given other AR headsets, it’s not out of line. The Microsoft Hololens 2 for example, starts at $3500. I’d compare specs, but the Vision Pro doesn’t really have details yet other than the CPU/Sensor processing chip. The 850 is a good chipset but it’s not great, it’s a solidly mid SOC, and honestly, it gets smoked by the Apple M1, much less an M2. (https://cpu-benchmark.org/compare/qualcomm-snapdragon-850/apple-m1/)

That’s not to say the Hololens is a bad bit of kit. But it, like every other piece of hardware has its focus. The Vision Pro’s focus is a bit…wider.

Which is all well and good, but the TAKES. Oh my god, not sense the iPhone introduction in 2007 has there been this many bad takes. (Nothing as bad as “how will you use it with BBQ sauce on your fingers??” AS OPPOSED TO WHAT BBQ SAUCE DOES TO WEE TINY KEYS???) One of my favorites was basically:

Apple hasn’t gotten me to buy a watch, which is much cheaper, in ten years, how can it convince me to buy this?

Some idiot

It can’t. You’re never going to be an Apple customer. Move on.

Or the FOSSstans who will never buy a thing Apple sells anyways talking about how this is the END OF INNOVATION.

My brother in christ, you are complaining from outside a club you can’t get into, have no interest in trying to get into, and don’t really even know where the club is. You have no value at all to Apple. Or really, anyone else outside of your cult. It’s like a Pentacostal AG whining about the lack of Sunday School and Jesus at a Jewish temple.

“Well, i already have a VR headset that suits my needs perfectly, and is much cheaper”

Another Idiot

It’s not a VR headset, and the Oculus has nothing on this rig. Move on.

Over and over and the best part is are hoe many people who spent years arguing that Apple desktop and laptop prices shouldn’t be directly compared to a $500 PC because of massive spec differences are now using the same bad arguments as the wintel people used to. They all turned into 2000 Paul Thurrot.

Sigh

All that’s missing is a craptacular list from Don Resinger and an even more inane Rob Enderle article.

Still Flogging, Still Dead

A day or so ago, I was reminded of one of Apple’s stupidest actions ever, one that has left everything running macOS open to a DOS attack, a thing that no only will they refuse to fix, but one they defend, because it only affects user data, so an app that would rampantly mess up your ability to use files without deleting or harming them at all, but by modifying one piece of file metadata, make it hard, if not impossible to correctly use that file.

The post:

And this is why you don’t use file name metadata as your sole determinant for what a file is, does, or belongs to.

<stares at macOS where a script/app that yanked all the file name extensions from every file in your home directory would be a (sadly) extremely effective way to DOS you on your own computer because without filename, UTIs fail to work and the OS (and every app that didn’t exist prior to OS X) will not only not know what to fucking do with the file…>

My post on mastodon

It was a sort of quote tweet of another post by someone else talking about how having TLDs that end in .mov or .zip could be a bad idea. I don’t completely agree, we’ve had .com for a long time, but, to be fair, the days of actually using .com executables as a common thing are somewhat in the past.

However, it did remind me of how unless the dev takes steps to prevent it, if I want to force you to do a lot of work, like file restores, all I have to do is embed a…between 1 and 3-line script in an application that has a good reason to get access to things like your documents directory, your iCloud directory, OneDrive directory and similar, (not hard do do for any document-based application) and all that script has to do is delete all filename extensions from every file it can find, and well, unless you know what’s up, you’re…what’s the phrase…oh, right:

You’re Fucked

Now, if you have a decent backup/archiving system, and you can just completely restore all the contents of those directories, well, it’s still a lot of tedium, especially if you use cloud backups and aren’t in a location with lots of bandwidth, but at least then it’s fixable.

However, if you are not, or your backup strategy has…holes…then thanks to Apple’s complete reliance on file metadata, the filename extension, you have little chance of getting your files back to normal anytime soon. Here, a tour:

So here we have a pages file, it’s a homework file for a class I’m taking. If I want to get details reasonably easy, a bit of fun with AppleScript’s “info for” command and here’s what Script Debugger has for us:

That looks normal. We see the file name, with the extension (.pages), the kind is a “Pages Document”, and the UTI is “com.apple.iwork.pages.sffpages.” If we double-click on the file, it opens in Pages. If we use File -> Open in Pages, we can open the file. If we right-click the file and select “Open”, it opens in Pages. If we right-click the file and select “Open With”, Pages is the default. All is as it should be, right down to the icon in the Get Info Window:

All Is Well

But

What happens if we change the file name, specifically, if we remove the Pages extension?

Fail

First, the file icon changes to a generic Unix Command line utility icon. No, really:

Same file, only thing that changed was part of the file name. Just happened to be the “wrong” part. But surely, that’s not correct for everything, surely the OS still knows what kind of file it is…

Um…nope:

Pages won’t open it at all:

Gotta say that circled part is my favorite thing ever. And no, “Open With” doesn’t work. Unless you happen to know what that file is in terms of the “proper” file name extension, or can restore an earlier version, congrats, that file is lost to you. Even if you try to look at the raw elements of the file in say, BBEdit, which will show you the file structure:

The files in the metadata folder? Nope. Here’s what they have:

BuildVersionHistory.plist

<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd”&gt;
<plist version=”1.0″>
<array>
<string>Template: Blank (12.1)</string>
<string>M12.2.1-7035.0.161-2</string>
</array>
</plist>

DocumentIdentifier.plist:

EE9FEC88-81E3-4BA8-AA2E-C51C5E429CD8

Properties.plist

The only chance you have is the preview.jpg file, which shows you a preview of the first page of the document. That’s it. all the .iwa files are snappy-compressed files, and of course, the Archive Utility won’t touch them. BRILLIANT.

Now, it’s not just Apple mind you. Most what old-timers would call Cocoa apps have the same problem. Here, Pixelmator Pro.

File with extension:





Same file, no extension:





(If you try to open it with TextEdit, it does not work)

What happens if you try “Open With” and force Pixelmator Pro to try? Well

So that’s a fail. Look, I’m not really bagging on the Pixelmator folks right? They’re doing what Apple recommends. The problem is, this isn’t i(Pad)OS where there’s other ways to manage this and I can’t just arbitrarily mung the filename like this. At least with Pixelmator, if you open the file in BBEdit, the metadata.info file is a SQLite database file with the application name in there, so at least theoretically, if you know how to troll through SQLite files and you know what you’re looking for, you have a chance to fix it. iWork files (or really any file from Apple), lol, you’re screwed.

Oh and if you use the default view in the Finder, that hides file extensions? you can’t even tell the name was changed!

The only visual indication is the icon. But that’s not the most egregious example. Wonder what happens if you yank the extension off a Shortcuts file? Of course you do, you’ve read this far, WHY STOP THE AGONY NOW?

Just by removing the extension, POOF! IT’S A FOLDER.

By the way, to all you nerds who love to laugh at people who think you can make a jpeg into Word doc just by changing the filename extension? CRAP LIKE THIS IS WHY! PUSHING AND ADVOCATING FOR THIS SHIT IS WHY THAT HAPPENS! IT’S YOUR FAULT, 100%

But surely there’s an application that isn’t stupid. Why yes, yes there is. My favorite example, MICROSOFT WORD.

With the filename extension:





All as expected. But now, let us remove the extension…


okay, that’s expected, a generic icon. At least it’s a document icon, and not a unix executable or a gods-damned folder.



See, just like…wait, it’s still a Word file, even without an extension. That can’t be right…



But it is right. And the reason, for the youngin’s out there isn’t obvious, so let me explain.

If you look at the file info for either word version, you’re going to see two values that don’t exist for pages/shortcuts/pixelmator pro/”new” application files: file creator and file type. Back in the before (OS X/macOS) times, on a Mac, the filename extension truly was metadata. It was mostly there for sharing files with other platforms, like Windows that heavily relied on the filename extension. But the older Mac OS, didn’t care, because of those two values. If you hear old-timers talking about resource forks, that’s a lot of what they did: they told the OS and the Finder what kind of file a thing was. Wasn’t just for document files either:

No need for .exe or .app or what have you. Just a bit of metadata that existed outside of what you saw, so changing the name and/or filesystem didn’t change a file into a folder or even appear to.

Now, the implementation of this in the Classic Mac OS was anything but perfect. It created its own brand of magical thinking, i.e. “Rebuild the Desktop” was the classic version of “Repair Permissions”, but it was a way to ensure a file was what you thought it was regardless of name, and clearly it still works. It’s not the best thing right, I mean, prior to the Office XML files, the file type was “WDBN” and I think at one point, there was a “WD4N” or something. Having only four characters meant things got weird, but the concept of file types that aren’t solely reliant on something as trivially changed as the file name makes a lot of sense, and prevents the kind of DOS attack that every copy of macOS is pathetically vulnerable to.

Oh, and please, Office people, CC people, really, anyone who’s been building apps since before OS X, please keep using File Types and Creator Codes. I know it’s a pain in the ass, I really do, but the few times you need them, you need them and they will literally save your ass. Thank you for that extra work, it’s a genuine help, and we appreciate it.

Before the pedants feel the need to bother me with it, if I render someone unable to actually use their computer, to work on their files until they do a massive document restore, probably multiples given the defaults not showing you the extensions so you can’t even see the problem until you try to open your folder, I mean file, that is a Denial of Service attack. It is denying you the ability to use your computer and your files, it is most definitely a DOS attack. Or DoS for the overly case-sensitive.

Just because the OS and the Applications can sit there and quietly navel-gaze doesn’t mean there’s no problem. At that point, the difference between hundreds/thousands of files you can’t use and an erased hard drive is not as great as you wish to think, especially for folks who are not coders, are not sysadmins and just want to use their computer to do work, to get through a class, to write stories or make paintings or all the things computers are used for that aren’t sysadmin or coding. I know it’s shocking, but non-tech workers do “real” work too. Some of you may need some time to internalize that, but it needs to be done.

The sad thing, the absolutely sad thing is there’s a fix that’s almost trivial: let applications set UTIs within file EAs instead of deriving them from the filename or making people try to use file types and creator codes. Most of the work is already done, applications can set the UTIs they use/conform to, you can still associate UTIs with filename extensions/MIME types (which you’d need to do for files coming from non-Apple OS’s), none of that has to go away, nor should it. But give the app creating the file the ability to write the pertinent type identifier into the file, and not just have it derived from something so trivially changed. Hell, make it read/write in Standard Additions so people can automate changing it for specific files en masse outside of the somewhat kludgy “Get Info” window.

Adding this capability would cause effectively zero harm and remove a vector to cause harm, or at least make it harder to do that. All the mechanisms are in place, it would be a trivial API change, and no, unlike one fine young dingus claimed the last time I talked about this, it would not keep you from having your web browser be the default for JPEGs you wee dork. It would just mean that if your file lost the .jpg/.jpeg extension, your web browser would still know it’s a goddamned JPEG. Nerds are the worst people to move anything forward for anyone but themselves, STG, SMDH.

But this won’t happen, because the powers that be at Apple and everyone else think doing an entire home directory restore from the cloud is trivial because it’s not like anyone ever doesn’t have 1G bandwidth at their fingertips.

S I G H

Like so many other things, Apple can improve this, they just neither care nor want to. Pity.

But Y Tho?

As those of you who follow me on mastodon know, a while back, I had given up on my Apple Watch. I had actually put it in a drawer, because I had gotten so frustrated with the changes to the run tracking on it.

I run, but not because I’m any kind of runner. It’s a way to exercise, to get my cardio in, and during the pandemic, well, a pretty easy way to not get completely out of shape. I run anywhere between 3 & 5 miles on a given run, so I’m not like Adam Engst or Heather Kilborn, friends of mine who are Runners. I’m not a serious runner. So what I wanted out of my watch was simple shit. Distance, Elapsed Time, Calories, Heart Rate, Pace. That’s it. I know there’s all kinds of things you can track, but those are the things I care about and for years, that was what I saw.

Then in a recent WatchOS update, it all went to hell. Split times, lap times, a half-dozen stats that if I were a Serious Runner, I’d care about. And no matter how I tried I couldn’t get it to show me what I wanted. So I was like, fuck this shit, I’m out. I carry my phone anyway, so the watch was a convenience. And if I couldn’t use it for the one really “smart” thing I had it for, why bother with it?

Fortunately, someone on Mastodon (and I am blanking on who, my apologies) helped me find the secret settings. Wherein if you go to outdoor workout, tap on the settings widget, then do the counterintuitive thing of creating a custom workout, you can enter the what feels like 3m tall list of settings that you can turn off and on and eventually get what you want.

So much scrolling.

SO

MUCH

SCROLLING

But why? Why is this only on the watch, and not on the watch app on the iPhone, where this would be trivial? This is the kind of thing where whomever was designing this update at Apple decided most of the users are Serious Runners, and so would only want these settings on the watch. That’s the only theory that makes sense. I don’t have a problem with the settings being on the watch, I have a problem with the settings ONLY being on the watch and being unable to change them on the phone, which has an infinitely better UI for this. I have a problem with the settings being badly labeled on the phone, and at the end of a long list of things that have nothing to do with the basic display settings. I have a problem with Apple deciding only Serious Athletes use the watch.

If you make the defaults useful only to the high end, and completely painful to change, then you’re telling the vast majority of your customer base to go fuck off. May not be your intent, but that is in fact the message you’re sending. Maybe don’t do that? I know Apple sucks at communication, they have always sucked at communication, but maybe think about fixing this?