Yeah, SwiftUI too dude!

SwiftUI is just not good at macOS apps

So because I’m stupid, (I know, shocker), I decided to do a thing Apple says is pretty easy: try to make a macOS app using SwiftUI. Setting aside beta issues, let me be clear on this:

  1. It’s not an iOS/iPadOS app that will run on macOS via Catalyst.
  2. It may never run on iOS/iPadOS. I don’t really care about those
  3. This is not some thing with a list picker and a nav view and hey, UI is done.

Here, a screenshot of the non-SwiftUI version:

There’s a lot going on here

So yeah. There’s a lot going on and for what it’s used for, a front end to managing my Nagios servers, it’s pretty good at it. It does thing things I need it to do. That large blank area at the bottom is for status stuff. Also, I don’t want to hear that the reason I had a hard time with this is because I haven’t been a Swift Dev since it was introduced. If the only way an environment is usable requires years of experience to do even simple things, that environment is a failure. Period.

So okay, this is pretty straightforward right? tabs, controls, done. I mean, just to mock up the UI.

Lol.

So after two weeks, spread across much longer, I can say a few things about SwiftUI.

  1. If you’re building a macOS app that is macOS – first or only, stay away from SwiftUI. Like it had the dysentery and was dragging your conestoga towards a school of piranha. It’s honestly awful, as we’ll see
  2. I’m actually really sure at this point, that SwiftUI is an attempt to bring CSS into Apple Device app UI design. In and of itself, that’s not awful. But the current implementation as of the GM 2 drop of Xcode is just awful for macOS. Again, see 1. above.

What I wanted to do was build two tabs and have some stuff in it. So let’s go through this. First, here’s the ContentView of a generic starting macOS project using SwiftUI:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This gets you this:

Yep. That’s a window.

So now, we want to add tabs. This requires a tab view. So we erase the text saying “Hello world” hit ye olde + button in Xcode and drag in a tab view.

This is my first complaint here: You drag a control into a code window, not onto the window of things you want. Like, I did try, and this may work correctly for i(Pad)OS apps, but for a macOS app, it’s full of fail. I tried to drag a button into a tab view, couldn’t do it. So basically, with SwiftUI, you’re not visually laying out your controls at all.

And this is what the code looks like (in the interest of space, I’m going to not include the content view preview code, it doesn’t change:

struct ContentView: View {
	var body: some View {
		TabView(selection: /*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Selection@*/.constant(1)/*@END_MENU_TOKEN@*/) {
			/*@START_MENU_TOKEN@*/Text("Tab Content 1").tabItem { Text("Tab Label 1") }.tag(1)/*@END_MENU_TOKEN@*/
			/*@START_MENU_TOKEN@*/Text("Tab Content 2").tabItem { Text("Tab Label 2") }.tag(2)/*@END_MENU_TOKEN@*/
		}
	}
}

Fugly, but it gets us this:

Okay, so far so good

Now already, this is weird. The content of the tabs occurs outside/before the tabItem itself. The label of the tab happens after the tabItem. Sort of. So let’s reformat this a bit, also so it looks more like what you see in examples:

struct ContentView: View {
	@State private var selection = 2
	var body: some View {
		TabView(selection:$selection) {
			Text("Tab Content 1")
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

This gives us the same thing we saw before. A window with two tabs, a single string in each tab, and the tabs themselves have names. I added a line, the “@State” line, so that the first tab, with the .tag(1) property is the start tab.

But this is really weird right? First, the content, the strings “Tab Content 1” and “Tab Content 2” aren’t “in” the .tabItems. They’re before it. Whaaa? This right here is the most counterintuitive shit ever. The content of the .tabItem should be inside the .tabItem block, not outside. The name of the tab should be a property, just like the tag, i.e. .label or some such. Instead, it’s just a text string. Maybe a picture too.

So then, what if you want to put a text field with a label? Oh, then it gets stupid. So first, keeping with “content of thing before <thing>” which took me a WHILE to get, (I kept trying to put it IN the .tabItem, I mean, what kind of IDIOT would think that’s where it goes?) So now, we have this:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			Text("Text Field Label")
			TextField("Text Field", text: .constant(""))
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gets us the field and the label on top of each other, because this was designed for iOS. Also the text field stretches the width of the window. It looks like this:

Sigh

Actually, that’s not what happened. SwiftUI added a third tab and now it’s all stupid. No warning. What the hell? Well, first, we can try adding a horizontal stack (read “row”) and putting things in that. Sure. let’s try that. According to Apple et al, you just cmd-click the thing you want in the HStack, select “embed in HSTack, and oh look, it also put the .tabItem in the HStack too, which was completely not what we wanted. So NOW, we put our text field inside the HStack, the next line down from the label, and put in a .frame property one line below the TextField declaration to limit the width of the textField because this is not iOS and I don’t want a gigantic text field on a 4K screen and we get closer:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			HStack {
				Text("Text Field Label")
				TextField("Server URL", text: .constant(""))
					.frame(width:96,height:22)
			}
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

which gets us:

Okay, getting there…

But we want this on the left side of our window, and at the top. So the first is pretty easy. We add a spacer, which shoves it all over to the left, and then we add in some padding so it’s not at the edge of the window. Like seriously, this is some hardcore iOS shit you have to work around. Also, why can’t I just say “hey, in this horizontal row, I would like everything to be aligned left.” That’s a great idea, so of course you can do that. LOL! Nope, in an HStack, you can only adjust the vertical alignment of the things inside it. Because that’s how everyone thinks of aligning horizontal things in a horizontal structure. Vertically.

Sigh.

So now we have:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			HStack {
				Text("Text Field Label")
				TextField("Server URL", text: .constant(""))
					.frame(width:96,height:22)
			Spacer()
			}
			.padding(.leading)
			
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gives us:

Closer…

So now, what if we want two rows? Another HStack under the other one right?

Oh honey, no, it’s not that simple. That gets you a third tab. But go ahead and try it. Right, so now what? Well, now we embed this in a VStack, (Column) which will also let us handle our vertical positioning via a Spacer on that:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack{
				HStack {
					Text("Text Field Label 1")
					TextField("Text Field 1", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
			Spacer()
			}
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

and we get:

Closer….

Of course you’re thinking “why not just set the VStack so everything in it is at the top?” Well, that would be logical, and also have nothing to do with VStack alignment options, which are of course, left, right, and centered. Or leading/trailing/centered. Because when I’m looking at a vertical structure, I only care about horizontal alignment.

Sigh.

So now we just shove another HStack in and we’re set…sort of:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack{
				HStack {
					Text("Text Field Label 1")
					TextField("Text Field 1", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
				HStack {
					Text("Label 2")
					TextField("Text Field 2", text: .constant(""))
						.frame(width:96,height:22)
					Spacer()
				}
				.padding(.leading)
				
			Spacer()
			}
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which gets us:

Hrm…

I mean, it’s not bad, but the field alignment is kind of messed up. So how do we fix that? I mean, we could add in some padding statements, but that’s a bit much, especially as we may want to have more stuff in the window.

So to do that, we’re going to get medieval on this thing, by which I mean, we’re basically going to have to build a damned table to hold things. (Seriously, I’m having flashbacks to GoLive CyberStudio here.) Because that’s so much better than the old way.

So what we end up with are two VStacks in an HStack in a VStack. To get everything lined up correctly and with the right amount of margin from the left side of the window, we have (Comments added for clarity):

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
					.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

Which looks like:

Alllmost

That’s really close. but the labels and the text field alignment is kind of muffed. You might think “this is where that vertical alignment thing in the HStack comes into play!” You would think that, but you’re wrong. These are two VStacks in the HStack, remember? The way I ended up solving it was to add some padding to the bottom of the one label, so they separate out better:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
							//this separates the labels enough so they seem to
							//line up with the text fields better
							.padding(.bottom)
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
						.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	}
}

and from this, we get:

Hey, that looks right!

Oh, so if you want your window to be the right size, you add a .frame property to the bottom of the view block:

struct ContentView: View {
	@State private var selection = 1
	var body: some View {
		TabView(selection:$selection) {
			VStack {
				HStack {
					//the alignment statement here sets both labels to be left-aligned
					VStack(alignment: .leading) {
						Text("Text Field Label 1")
							//this separates the labels enough so they seem to
							//line up with the text fields better
							.padding(.bottom)
						Text("Label 2")
					}
					//this padding statment creates space between the edge of the
					//window and the text
						.padding(.leading)
					//this alignment property may not be strictly needed
					VStack(alignment: .leading) {
						TextField("Text Field 1", text: .constant(""))
							.frame(width:96,height:22)
						TextField("Text Field 2", text: .constant(""))
							.frame(width:96,height:22)
					}
				// this is the spacer for the HStack so it's all on the left
				Spacer()
				}
				//this is the spacer for the main VStack so it's at the top
				Spacer()
			}
			//this padding property gives us some space between the tabs and the stuff
			//in the main VStack
			.padding(.top)
				
			.tabItem{
				Text("Tab Label 1")
			}
			.tag(1)
			
			Text("Tab Content 2")
			.tabItem{
				Text("Tab Label 2")
			}
			.tag(2)
		}
	//set the window size here. Beats me why
	.frame(width:706,height:651)
	}
}

And now we have a window that starts life in a precise way:

This is…this is a lot of weird-assed work to get the very simplistic results I wanted. And that’s only on one tab. I have to redo that for every tab. Literally all that code I added is to set up two labels and two text fields, that aren’t doing anything.

I freely admit that my n00bness probably caused a lot of this. But, it was not helped by nothing about using SwiftUI with macOS apps. All of Apple’s documentation and every gods-forsaken screen shot I could find were all for iOS. This may be the longest bit of anything using SwiftUI for macOS available on the internet, and that is sad. Shamefully so. Keep in mind, my end goal has a lot of UI elements. So this should be awful at some point. But I may keep banging on it, because god knows, no one else is, and even documentation from a n00b is better than nothing.

If Apple wants people to use this for macOS dev, and not just i(Pad)OS -> macOS via Catalyst, then they have to, to be blunt, pull their goddamned heads out of their asses on this. Start creating documentation and tutorials and sample code that actually works for macOS targets in and of themselves. Or just admit that SwiftUI is not a good choice for starting on macOS, and that they don’t plan to care about that. That would at least be honest.

As well, there’s some choices here that are just baffling. I refer to SwiftUI as CSS for native UI design, but even CSS isn’t as bad as some of this. Having me put all the contents for a tab before the .tabItem block and not oh, in the .tabItem block is pretty damned weird, and completely counterintuitive.

Forcing macOS devs to specify control location the way you would on i(Pad)OS is way more work than it needs to be. Let me drop a damned button on a window and give me the shell code for that.

But for now, outside of idle curiosity, I cannot see why a macOS dev would ever want to use SwiftUI, it’s just awful.

(If you feel a need to tell me what you think about this, I’m @bynkii on twitter. If you feel the need to tell me i’m stupid for not immediately falling to my knees and loving SwiftUI, you may want to not actually tell me that. It’d be a waste of time for both of us. If you want to give me some pointers from a macOS perspective, DEAR GOD PLEASE!!!!)

Advertisements