tabItem vs. Tab() — how to support iOS 17 and 18?

Hi,

I'm adding tabs to the iOS version of my multiplatform app using TabView. I want the individual tabs to have names and icons. In iOS 17 and below, I have to do this using:

tabContent().tabItem { 
	Label(titleKey, systemImage: systemImage)
}

but this is deprecated, so in iOS 18 I would like to use the new version:

Tab(titleKey, image: systemImage) {
		content()
}

It would be annoying to have to have the two cases for each individual tab, so I'm trying to abstract it into a custom SwiftUI view like this:

var body: some View {
			if #available(iOS 18.0, *) {
				Tab(titleKey, image: systemImage) {
					content()
				}
			} else {
				content().tabItem { 
					Label(titleKey, systemImage: systemImage)
				}
		}
	}

There's a bit more to the custom view because I also have cases for iPad and macOS where I just have the views next to each other without tabs, but that's not really relevant to the question other than providing further motivation for abstracting this.

However, with this code, I get the error:

'buildExpression' is unavailable: this expression does not conform to 'View'

on the Tab line, because Tab isn't a view, and it can only be used directly inside a TabView.

For now at least, I can just use tabItem on all iOS versions and it works, but I'd prefer not to in case it is removed some time soon. I do want to support iOS 17 because that's what my iPad runs. Is there any clean way to do this?

Answered by darkpaw in 840192022

I'm not sure you should try to do this, for a couple of reasons. Look at some simple code that's written for iOS 17 and 18:

if #available(iOS 18, *) {
	TabView(selection: $selectedTab) {
		Tab("Watch Now", systemImage: "play", value: .watchNow) {
			WatchNowView()
		}
	}
} else {
	TabView(selection: $selectedTab) {
		MenuView()
			.tabItem {
				Label("Menu", systemImage: "list.dash")
			}
		OrderView()
			.tabItem {
				Label("Order", systemImage: "square.and.pencil")
			}
	}
}

First reason: The iOS 18 format has a specific Tab object which has its own title and image in the Tab item itself, whereas iOS 17's tabItems force you to create a Label instead. That would be difficult to work around.

Second reason: Tab contains a View, whereas tabItem is a modifier on a View (just as .bold() is a modifier on a Text view). It's the opposite way round, making it difficult again.

It might be cleaner to simply do something like this, and you can remove the unnecessary code when you no longer support iOS 17:

if #available(iOS 18, *) {
	MyTabs(selectedTab: $selectedTab)
} else {
	MyTabs_iOS17(selectedTab: $selectedTab)
}

...

struct MyTabs: View {
	@Binding var selectedTab: Tabs
	var body: some View {
		if #available(iOS 18, *) {
			TabView(selection: $selectedTab) {
				Tab("Watch Now", systemImage: "play", value: .watchNow) {
					WatchNowView()
				}
			}
		}
	}
}

struct MyTabs_iOS17: View {
	@Binding var selectedTab: Tabs
	var body: some View {
		TabView(selection: $selectedTab) {
			MenuView()
				.tabItem {
					Label("Menu", systemImage: "list.dash")
				}

			OrderView()
				.tabItem {
					Label("Order", systemImage: "square.and.pencil")
				}
		}
	}
}

There are ways to do this, but as I've mentioned, you'll be trying to fit A into B and C into D, and will have to write a chunk of code that makes that happen. It will likely be easier to bite the bullet and write the two separate sections.

Accepted Answer

I'm not sure you should try to do this, for a couple of reasons. Look at some simple code that's written for iOS 17 and 18:

if #available(iOS 18, *) {
	TabView(selection: $selectedTab) {
		Tab("Watch Now", systemImage: "play", value: .watchNow) {
			WatchNowView()
		}
	}
} else {
	TabView(selection: $selectedTab) {
		MenuView()
			.tabItem {
				Label("Menu", systemImage: "list.dash")
			}
		OrderView()
			.tabItem {
				Label("Order", systemImage: "square.and.pencil")
			}
	}
}

First reason: The iOS 18 format has a specific Tab object which has its own title and image in the Tab item itself, whereas iOS 17's tabItems force you to create a Label instead. That would be difficult to work around.

Second reason: Tab contains a View, whereas tabItem is a modifier on a View (just as .bold() is a modifier on a Text view). It's the opposite way round, making it difficult again.

It might be cleaner to simply do something like this, and you can remove the unnecessary code when you no longer support iOS 17:

if #available(iOS 18, *) {
	MyTabs(selectedTab: $selectedTab)
} else {
	MyTabs_iOS17(selectedTab: $selectedTab)
}

...

struct MyTabs: View {
	@Binding var selectedTab: Tabs
	var body: some View {
		if #available(iOS 18, *) {
			TabView(selection: $selectedTab) {
				Tab("Watch Now", systemImage: "play", value: .watchNow) {
					WatchNowView()
				}
			}
		}
	}
}

struct MyTabs_iOS17: View {
	@Binding var selectedTab: Tabs
	var body: some View {
		TabView(selection: $selectedTab) {
			MenuView()
				.tabItem {
					Label("Menu", systemImage: "list.dash")
				}

			OrderView()
				.tabItem {
					Label("Order", systemImage: "square.and.pencil")
				}
		}
	}
}

There are ways to do this, but as I've mentioned, you'll be trying to fit A into B and C into D, and will have to write a chunk of code that makes that happen. It will likely be easier to bite the bullet and write the two separate sections.

tabItem vs. Tab() — how to support iOS 17 and 18?
 
 
Q