how to add a toolbar divider for a three column view in swiftUI life cycle - macos

I am looking for a way to achieve a Toolbar for a three column layout like Mail.app. Also the Notes.app uses almost the same Toolbar with the only important difference between the two apps being that Notes.app looks like it's WindowStyle is a HiddenTitleBarWindowStyle and Mail.app looks like a Default|TitleBarWindowStyle.
Following should be true:
If the sidebar is collapsed there is a list and detail view
The dividing line that separates the list from the detail view goes all the way up through the toolbar. (This could be achieved with a HiddenTitleBarWindowStyle)
If the Title is too long to fit into the navigation list, the vertical dividing line will be broken: The list is still divided from the Detail View as before, but now the Toolbar looks like a DefaultWindowStyle with only a small Divider()-like line in the Toolbar.
Which combination of WindowStyle, WindowToolbarStyle and .toolbar configuration do I need to achieve this setup?
EDIT
I noticed that it is not possible to remove the divider line that Notes.app shows. I have not found a references to any such element in the docs though.
Code Example
I have distilled the problem down to a simple app with mostly toolbar content. In my original code I use two nested NavigationViews, while for the example I used only one NavigationView with two Lists. However the Toolbar results are the same.
Toolbar with DefaultWindowStyle
import SwiftUI
#main
struct ToolbarTestApp: App {
var body: some Scene {
WindowGroup {
ContentView(titleBarIsHidden: true)
}
.windowToolbarStyle(UnifiedWindowToolbarStyle())
.commands {
SidebarCommands()
}
}
}
struct ContentView: View {
#State var destination: String = "Toolbar Test"
#State var detail: String = ""
var body: some View {
NavigationView {
List {
Button(action: {self.destination = "Item with the identifier: 1"}, label: {
Text("Item 1")
})
.buttonStyle(DefaultButtonStyle())
Button(action: {self.destination = "Item 2"}, label: {
Text("Item 2")
})
.buttonStyle(DefaultButtonStyle())
}
.listStyle(SidebarListStyle())
List {
NavigationLink(
destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 1"},
label: {
Text("\(destination) – Detail 1")
})
NavigationLink(
destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 2"},
label: {
Text("\(destination) – Detail 2")
})
}
.listStyle(InsetListStyle())
Text("\(destination) – \(detail)")
}
.navigationTitle(destination)
.navigationSubtitle(detail)
.toolbar(id: "nav") {
ToolbarItem(id: "plus", placement: ToolbarItemPlacement.principal, showsByDefault: true) {
HStack {
Button(action: {print("pressed")}, label: {
Image(systemName: "plus.circle")
})
}
}
ToolbarItem(id: "spacer", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
HStack {
Spacer()
}
}
ToolbarItem(id: "sidebar.end", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
Button(action: {print("pressed")}, label: {
Image(systemName: "sidebar.right")
})
}
}
}
}
This example will result in a Toolbar that will never show the divider line that splits the whole Toolbar in two parts. Also the first ToolbarItem is positioned in the center of the Toolbar. I tried all ToolbarItemPlacement but none caused the item to move to the far left adjacent to the title.
Toolbar with HiddenTitleBarWindowStyle
#main
struct ToolbarTestApp: App {
var body: some Scene {
WindowGroup {
ContentViewForHiddenTitleBar()
}
.windowStyle(HiddenTitleBarWindowStyle()) // added hidden title style
.windowToolbarStyle(UnifiedWindowToolbarStyle())
.commands {
SidebarCommands()
}
}
}
struct ContentViewForHiddenTitleBar: View {
#State var destination: String = "Toolbar Test"
#State var detail: String = ""
var body: some View {
NavigationView {
List {
Button(action: {self.destination = "Item with the identifier: 1"}, label: {
Text("Item 1")
})
.buttonStyle(DefaultButtonStyle())
Button(action: {self.destination = "Item 2"}, label: {
Text("Item 2")
})
.buttonStyle(DefaultButtonStyle())
}
.listStyle(SidebarListStyle())
// add geometry reader to trim title width in toolbar
GeometryReader { geometry in
List {
NavigationLink(
destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 1"},
label: {
Text("\(destination) – Detail 1")
})
NavigationLink(
destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 2"},
label: {
Text("\(destination) – Detail 2")
})
}
// there is no title anymore so let's fake it.
.toolbar(id: "list navigation") {
ToolbarItem(id: "title", placement: ToolbarItemPlacement.navigation, showsByDefault: true) {
VStack(alignment: .leading) {
Text(destination)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(detail)
.font(.subheadline)
.opacity(0.6)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(width: geometry.size.width)
}
}
}
.listStyle(InsetListStyle())
Text("\(destination) – \(detail)")
}
.navigationTitle(destination)
.navigationSubtitle(detail)
.toolbar(id: "nav") {
// primary action will place the item next to the divider line.
ToolbarItem(id: "plus", placement: ToolbarItemPlacement.primaryAction, showsByDefault: true) {
HStack {
Button(action: {print("pressed")}, label: {
Image(systemName: "plus.circle")
})
}
}
ToolbarItem(id: "spacer", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
HStack {
Spacer()
}
}
ToolbarItem(id: "sidebar.end", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
Button(action: {print("pressed")}, label: {
Image(systemName: "sidebar.right")
})
}
}
}
}
This example will result in a Toolbar that will always show a full height divider. Even if the title is too long. Therefore a GeometryReader was added. This is fine until the sidebar will collapse. The placement of ToolbarItems will not be correct. Also when customising the Toolbar there would be the possibility to remove the title, which should not be possible.

The default title bar style is fine, you just need to attach your toolbar items to the subviews of your top NavigationView, e.g.:
var body: some View {
NavigationView {
List {
...
}
.listStyle(SidebarListStyle())
.toolbar {
ToolbarItem {
Button(action: { }, label: {
Image(systemName: "sidebar.right")
})
}
}
List {
...
}
.listStyle(InsetListStyle())
.toolbar {
ToolbarItem {
Button(action: { }, label: {
Image(systemName: "plus.circle")
})
}
}
Text("\(destination) – \(detail)")
}
.navigationTitle(destination)
.navigationSubtitle(detail)
}
I didn’t attach any toolbar items to the third column (the Text) but you can — just be sure to attach the same toolbar items to your DetailViews, as its toolbar will replace the Text’s toolbar when the user navigates. (if you’re not sure what I mean, just try it and you’ll quickly see what I’m talking about :)

Related

Placing the toggle sidebar button on the sidebar toolbar

I have a three column layout macOS application with the first being the sidebar. I have a button that toggles that enabling the user to hide the sidebar.
On Xcode and other macOS, the toggle sidebar button resides on the toolbar on top of the sidebar, and becomes part of the main toolbar when the sidebar is hidden.
For example, open sidebar on Xcode:
And when you hide the sidebar:
I have added the toolbar with the toggle sidebar to the view containing my sidebar, and another toolbar to the second column, but still toggle sidebar appears on the main toolbar, on top of the second column.
Am I missing anything? Here's the code:
// sidebar view, first of three columns
struct ContentView: View {
#State var selection: Set<Int> = [0]
var body: some View {
NavigationView {
List(selection: self.$selection) {
NavigationLink(destination: AllData()) {
Label("All Data", systemImage: "note.text")
}
.tag(0)
Label("Trash", systemImage: "trash")
}
.listStyle(SidebarListStyle())
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: toggleSidebar, label: {
Image(systemName: "sidebar.left") }).help("Toggle Sidebar")
}
}
}
}
func toggleSidebar() {
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
}
// second column view, with the rest of the toolbar
struct AllData: View {
var body: some View {
NavigationView {
List(filteredData) {
// list items here.
}
.toolbar {
ToolbarItem(placement: .automatic) {
Button("Press Me") {
print("Pressed")
}
}
ToolbarItem(placement: .automatic) {
Button("Press Me too") {
print("Pressed too")
}
}
}
}
}
}
If you use the .frame modifier on the sidebar then you are able to set constraints for minimum width, ideal width and maximum width.
I find a minWidth: 148 ensures the sidebar cannot be collapsed to the point where SwiftUI shuffles the toolbar button off to the trailing edge of the nav toolbar and behind (need to click) the double chevron expander.
So your code might look like this...
...
var body: some View {
NavigationView {
List(selection: self.$selection) {
NavigationLink(destination: AllData()) {
Label("All Data", systemImage: "note.text")
}
.tag(0)
Label("Trash", systemImage: "trash")
}
.listStyle(.sidebar) //<-- from iOS 14 and macOS 10.15
.frame(minWidth: 148, idealWidth: 160, maxWidth: 192, maxHeight: .infinity)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
toggleSidebar
}, label: {
Image(systemName: "sidebar.left")
})
.help("Toggle Sidebar")
}
}
}
}
...
PS. I note that the .primaryAction and .status modifiers both place the sidebar button in a similar position, although I've not tested this thoroughly or completed any research.
try this:
ToolbarItem(placement: .primaryAction) {

Customize the SwiftUI Form label layout for MacOS

I am running a macOS application that contains a Form with some elements on it. I love the default layout when using elements with labels (example: TextField and Picker) but am having trouble replicating that layout with a custom control. Specifically I want to have an HStack containing a Text/Label field (laid out under the other labels) and a button (even with the other fields above). Instead both elements are under the second part and not both.
I created a dummy project showing my issue. It's like the Form is a two column table and the labels are getting put in the first column and anything else goes in the second column. I did see this question, Swiftui Form Label alignment on macOS, but I was hoping with my below example there might be a better solution. I tried putting everything in a VStack but then the labels are left aligned and the text boxes start wherever. I also don't want to hardcode the width of the labels.
This is what I made as an example:
With the code:
import SwiftUI
struct ContentView: View {
#State var myName:String = "Kyra"
#State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(.black)
}
.padding()
}
}
I basically want the third section in the form to look like the previous two. I added a black background so it's more apparent. I want the label: to be even under My Name: and Pick Something: while the button is even with the text field itself and the picker above.
Thanks to anyone that can help me come up with an elegant solution
Update 1: Took #ChrisR's comment and attempted to use the style. I found that the ButtonStyle doesn't have a width like the ToggleStyle example. I then found this question, Get width of a view using in SwiftUI, and used the commonSize in Paul B's answer to set the alignment. It's set the width of my stack properly but can't set the alignment properly.
Image:
Code:
import SwiftUI
struct ContentView: View {
#State var myName:String = "Kyra"
#State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
#State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
.readSize { textSize in
commonSize = textSize
}
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
Update 2: I attempted ChrisP's answer "You want .readSize from your Button label, and get rid of the .frame" and ended up with both left aligned in the right column:
If I don't set commonSize or use 0 instead it moves both elements to the first column:
If I split up the elements by removing the label from the HStack I can get one on the first column and one on the second BUT then they're on two different lines.
With macOS 13 (Ventura), you can use LabeledContent to create a label with any view easily.
LabeledContent("Label:") {
Button("Button HERE") { ... }
}
You want .readSize from your Button label,
and get rid of the .frame, then it works :)
struct ContentView: View {
#State var myName:String = "Kyra"
#State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
#State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
.readSize { textSize in
commonSize = textSize
}
}
// .frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

macOS SwiftUI Table with contextMenu

Using SwiftUI's new Table container, how can I add a context menu that appears when Control-clicking a row?
I can add the contextMenu modifier to the content of the TableColumn, but then I will have to add it to each individual column. And it only works above the specific text, not on the entire row:
I tried adding the modifier to the TableColumn itself, but it shows a compile error:
Value of type 'TableColumn<RowValue, Never, Text, Text>' has no member 'contextMenu'
Here's what I have in terms of source code, with the contextMenu modifier in the content of the TableColumn:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)])
private var items: FetchedResults<Item>
#State
private var sortOrder = [KeyPathComparator(\Item.name)]
#State
private var selection = Set<Item.ID>()
var body: some View {
NavigationView {
Table(items, selection: $selection, sortOrder: $items.sortDescriptors) {
TableColumn("Column 1") {
Text("Item at \($0.name!)")
.contextMenu {
Button(action: {}) { Text("Action 1") }
Divider()
Button(action: {}) { Text("Action 2") }
Button(action: {}) { Text("Action 3") }
}
}
TableColumn("Column 2") {
Text($0.id.debugDescription)
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
if selection.isEmpty {
Text("Select an item")
} else if selection.count == 1 {
Text("Selected \(items.first(where: { $0.id == selection.first! })!.id.debugDescription)")
} else {
Text("Selected \(selection.count)")
}
}
}
}
So, how can I add a context menu to the entire row inside the Table?
You can make cell view with the following extensions and use it for each column cell, then it will be clickable in any row place
Text("Item at \($0.name!)")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) // << this !!
.contentShape(Rectangle()) // << this !!
.contextMenu {
Button(action: {}) { Text("Action 1") }
Divider()
Button(action: {}) { Text("Action 2") }
Button(action: {}) { Text("Action 3") }
}

Select the first item on a list and highlight it when the application launches

When the app first launches in macOS (Big Sur), it populates a list with the items saved by the user. When the user clicks on an item on that list, a second view opens up displaying the contents of that item.
Is there a way to select the first item on that list, as if the user clicked it, and display the second view when the app launches? Furthermore, if I delete an item on the list, I can't go back and select the first item on the list and displaying the second view for that item, or if I create new item, same applies, can't select it.
I have tried looking at answers here, like this, and this, and looked and tried code from a variety of places, but I can't get this to work.
So, using the code answered on my previous question, here's how the bare bones app looks like:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(destination: NoteView(note: note)) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.onAppear() {
print(data.notes.count)
}
}
}
I have tried adding #State var selection: Int? in AllNotes and then changing the list to
List(data.notes, selection: $selection)
and trying with that, but I can't get it to select anything.
Sorry, newbie here on SwiftUI and trying to learn.
Thank you!
You were close. Table view with selection is more about selecting item inside table view, but you need to select NavigationLink to be opened
There's an other initializer to which does exactly what you need. To selection you pass current selected item. To tag you pass current list item, if it's the same as selection, NavigationLink will open
Also you need to store selectedNoteId instead of selectedNote, because this value wouldn't change after your update note properties
Here I'm setting selectedNoteId to first item in onAppear. You had to use DispatchQueue.main.async hack here, probably a NavigationLink bug
To track items when they get removed you can use onChange modifier, this will be called each time passed value is not the same as in previous render
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
Not sure what's with #AppStorage("notes"), it shouldn't work because this annotation only applied to simple types. If you wanna store your items in user defaults you had to do it by hand.
After removing it, you were missing #Published, that's why it wasn't updating in my case. If AppStorage could work, it may work without #Published
final class DataModel: ObservableObject {
#Published
public var notes: [NoteItem] = [
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
]
}

SwiftUI onTapGesture displays new view

I am using SwiftUI to make my own Pizza delivery application. I am now stuck cause I do not know how I could display the same image view (including a pizza's description) related to his own index from the array. Instead of the lines {self.images print("ok")}, I would like that each time the user tap on an image from the array it displays the same image with a short text description. Is the description of my issue clear enough? I would be extremely grateful if anyone could help me out with this. Thanks a lot for reading it. This is the code:
HStack {
ForEach(images, id: \.id) { post in
ForEach(0..<1) { _ in
ImageView(postImages: post)
}
}
Spacer()
}
.onTapGesture {
self.images
print("ok")
}
.navigationBarTitle("Choose your Pizza")
.padding()
Assuming you have a struct for your Item, you need to conform it to Identifiable:
struct Item: Identifiable {
var id: String { name } // needed for `Identifiable`
let name: String
let imageName: String
let description: String
}
Then in your main view:
struct ContentView: View {
let items = [
Item(name: "item1", imageName: "circle", description: "some description of item 1"),
Item(name: "item2", imageName: "circle", description: "some description of item 2"),
Item(name: "item3", imageName: "circle", description: "some description of item 3"),
]
#State var selectedItem: Item? // <- track the selected item
var body: some View {
NavigationView {
HStack {
ForEach(items, id: \.id) { item in
ImageView(imageName: item.imageName)
.onTapGesture {
self.selectedItem = item // select the tapped item
}
}
Spacer()
}
.navigationBarTitle("Choose your Pizza")
.padding()
}
.sheet(item: $selectedItem) { item in // show a new sheet if selectedItem is not `nil`
DetailView(item: item)
}
}
}
If you have a custom view for your image:
struct ImageView: View {
let imageName: String
var body: some View {
Image(systemName: imageName)
}
}
you can create a detailed view for your item (with item description etc):
struct DetailView: View {
let item: Item
var body: some View {
VStack {
Text(item.name)
Image(systemName: item.imageName)
Text(item.description)
}
}
}
EDIT
Here is a different approach using the same View to display an image or an image with its description:
struct ContentView: View {
#State var items = [
Item(name: "item1", imageName: "circle", description: "some description of item 1"),
Item(name: "item2", imageName: "circle", description: "some description of item 2"),
Item(name: "item3", imageName: "circle", description: "some description of item 3"),
]
var body: some View {
NavigationView {
HStack {
ForEach(items, id: \.self) { item in
DetailView(item: item)
}
Spacer()
}
.navigationBarTitle("Choose your Pizza")
.padding()
}
}
}
struct DetailView: View {
#State var showDescription = false
let item: Item
var body: some View {
VStack {
Text(item.name)
Image(systemName: item.imageName)
if showDescription {
Text(item.description)
}
}
.onTapGesture {
self.showDescription.toggle()
}
}
}
and conform Item to Hashable:
struct Item: Hashable
or instead of:
ForEach(items, id: \.self)
specify the id explicitly:
ForEach(items, id: \.name)
I also took a stab at it, for what it's worth.
(BTW #pawello2222's answer is also 100% correct. There is more than one way to achieve what you described)
I first created a pizza menu (using placeholder images):
Main pizza list view
Upon selecting an item from the menu, you are taken to the chosen pizza detail:
Chosen pizza detail view
I'm using NavigationView and NavigationLink to navigate between views.
Here's the full source code (for best results run in Swift Playgrounds):
import PlaygroundSupport // For running in Swift Playground only
import SwiftUI
// The main pizza listing page (view)
struct PizzaList: View {
// Array of pizza images
let pizzaImages = [
Image(systemName: "circle.fill"),
Image(systemName: "square.fill"),
Image(systemName: "triangle.fill")
]
// Array of pizza descriptions
let pizzaDescriptions = [
"Circular Pizza",
"Square Pizza",
"Triangle Pizza"
]
var body: some View {
// Wrap in navigation view to be able to switch between "pages"
NavigationView {
VStack {
ForEach(0 ..< pizzaImages.count) { index in
// Wrap in navigation link to provide a link to the next "page"
NavigationLink(
// This is the contents of the chosen pizza page
destination: PizzaDetail(
pizzaImage: self.pizzaImages[index],
description: self.pizzaDescriptions[index]
)
) {
// This is an item on the main list
PizzaRow(
pizzaImage: self.pizzaImages[index],
description: self.pizzaDescriptions[index]
)
}
}
}
.navigationBarTitle("Available Pizzas")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
// Layout for each pizza item on the list
struct PizzaRow: View {
let pizzaImage: Image
let description: String
var body: some View {
HStack {
Spacer()
pizzaImage
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.orange)
Spacer()
Text(self.description)
Spacer()
}
.padding()
}
}
// Layout for the chosen pizza page
struct PizzaDetail: View {
let pizzaImage: Image
let description: String
var body: some View {
VStack {
Spacer()
Text(description)
.font(.title)
Spacer()
pizzaImage
.resizable()
.aspectRatio(contentMode: .fit)
Spacer()
}
.padding()
.navigationBarTitle("Your Pizza Choice")
}
}
// For Playgrounds only
PlaygroundPage.current.setLiveView(PizzaList())

Resources