I have this simple situation:
struct User: Hashable, Identifiable {
var id: Int
var name: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
let bob = User(id: 1, name: "Bob")
let john = User(id: 2, name: "John")
let users = [bob, john]
struct ParentView: View {
#State private var user: User?
var body: some View {
VStack(spacing: 0) {
// Other elements in view...
Divider()
ChildPickerView(user: $user)
Spacer()
}
}
}
struct ChildPickerView: View {
#Binding var user: User?
var body: some View {
HStack {
Text("Selected : \(author?.name ?? "--")")
Picker(selection: $user, label: Text("Select a user")) {
ForEach(0..<users.count) {
Text(users[$0].name)
}
}
}
.padding()
}
}
When I select another value in the picker, the name in the Text() above isn't updated.
However, when tapping the picker again, there is a tick near the selected user, which means the value has actually been selected. Why is the text not updated then?
Thank you for your help
Type of selection and picker content id or tag should be the same. Find below fixed variant.
Tested with Xcode 12.1 / iOS 14.1.
VStack {
Text("Selected : \(user?.name ?? "--")")
Picker(selection: $user, label: Text("Select a user")) {
ForEach(users) {
Text($0.name).tag(Optional($0))
}
}
}
Related
I have 3 views. The issue I have is that the 1st one (a parent view of 2) is not changing when the 3rd one (a children of view 2) has updated a property in the array.
Let's use some code so it is easier to understand:
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
import SwiftUI
#main
struct ThisIsMessedUpApp: App {
var body: some Scene {
WindowGroup {
ItemsMainView()
}
}
}
import Foundation
import SwiftUI
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
HStack {
ForEach(self.items, id: \.id) { item in
Text(item.name ?? "nothing found")
Spacer()
Text(item.inStock.description)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString,
name: "Test #1",
inStock: false))
})
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}
Some of the Text are for debugging purposes.
If you run this code and change the second TextField's value to, say, "Test #2", you will see that you end up with an inconsistent UI state: ItemsMainView has "Test #1", whereas ItemView has "Test #2"
In Xcode 14.2, create a new project for ios or macos. Copy and paste the following code, replacing
the original ContentView.
Tell us if this code, tested on real ios 16.3 devices (not Previews), macCatalyst and MacOS 13.2 only, works for you.
struct ContentView: View {
var body: some View {
ItemsMainView()
}
}
public struct Item {
public let id: String
public var name: String?
public var inStock: Bool
}
struct ItemsMainView: View {
#State var items = [Item]()
var body: some View {
VStack {
Text("Item count is \(items.count)")
Divider()
VStack {
Text("ItemsMainView has:")
ForEach(items, id: \.id) { item in
HStack {
Text(item.name ?? "nothing found").foregroundColor(.red)
Spacer()
Text(item.inStock.description).foregroundColor(.red)
}
}
}
ItemsView(items: $items)
}
}
}
struct ItemsView: View {
#Binding var items: [Item]
var body: some View {
Button("Add new item (call made from ItemsView", action: {
self.items.append(Item(id: UUID().uuidString, name: "Test #1", inStock: false))
}).buttonStyle(.bordered)
VStack {
ForEach($items, id: \.id) { $item in
ItemView(item: $item)
}
}
}
}
struct ItemView: View {
#Binding var item: Item
#State var draftItemName: String = ""
var body: some View {
Text("ItemView has")
HStack {
TextField("TextField", text: $draftItemName)
.onSubmit {
item.name = draftItemName
}
Spacer()
Text(item.inStock.description)
}
.onAppear {
draftItemName = item.name ?? ""
}
}
}
I am trying to setup a picker, simple. I am successfully fetching an array of projects from firebase and populating the picker with the names of the projects. The problem that I am having is that I need to get the project id when I click the list but it's not doing anything after I click the option that I want. I tried to run it in a simulator and also on my iPhone and nothing happens after I make the selection. I am pretty sure I am not updating the picker and thus I am not updating the variable with the selected project id. I tried using the .onChange on the picker but nothing happens.
import SwiftUI
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: String = ""
var body: some View {
NavigationView {
VStack {
Picker("", selection: $selectedClient) {
ForEach(clients, id:\.self) {
Text($0.clientName)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Here is the picker populated with the foo data: picker
Your selection variable $selectedClient needs to have a type that matches the tagged value of each item in the picker.
As you're not specifying an explicit .tag for your text, the ForEach creates an implicit one using what it's using for tracking its loop, which in this case looks like it's a Client.
You can either change selectedClient to be a type of Client, or tag your displayed subview with the string value to populate selectedClient with, e.g.:
ForEach(clients, id: \.self) { client in
Text(client.clientName)
.tag(client.clientID)
}
Also, if each client has a unique ID, you're better off using that as ForEach's identifier than \.self. You can either specify id: \.clientID, etc., to use a single attribute – or you can add Identifiable conformance to Client and make sure that it has an id value that is guaranteed to be unique.
import SwiftUI
import Firebase
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: Client = Client(id: "", clientName: "Blank", timestamp: Timestamp(), ownerId: "", ownerUsername: "")
var body: some View {
NavigationView {
VStack {
Picker("d", selection: $selectedClient) {
ForEach(clients, id:\.id) { client in
Text(client.clientName)
.tag(client)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
Text(selectedClient.id ?? "")
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}
I'm playing with the new Sidebar that has come with SwiftUI 2 and the possibility to navigate in large screens with three columns. An example about how it works can be found here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-sidebar-for-ipados
It works fine, but I would like to go one step forward and make some options of my main menu that show the three columns but other options just two.
Here an example of some demo code.
import SwiftUI
struct ContentView: View {
var body: some View{
NavigationView{
List{
Section(header: Text("Three columns")){
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals",systemImage: "tortoise")
})
NavigationLink(
destination: ItemsView(),
label: {
Label("Animals 2",systemImage: "hare")
})
}
Section(header: Text("Two columns")){
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings",systemImage: "gear")
})
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label("Settings 2",systemImage: "gearshape")
})
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
ItemsView()
DetailView(animal: "default")
}
}
}
struct ItemsView: View{
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View{
List{
ForEach(animals, id: \.self){ animal in
NavigationLink(
destination: DetailView(animal: animal)){
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View{
var animal: String
var body: some View{
VStack{
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
If you run the code in, for example, an iPad Pro (12,9-inch) in landscape mode, you can see the tree columns. First the App Menu (sidebar). If you click on one of the first two options (animals, and animals 2), you can see a list of animals (second column) and when you click on some animal, you reach the third column (detail view).
However, I want to have only two columns when I click on the last two options of the menu (Settings and Settings 2). Any clue how to achieve it?
I've tried to hide that section if some of the first options in menu are not selected (with the selected parameter in NavigationLink), but without luck. It seems it is not possible (or I don't know) to know which option is selected in the sidebar.
Any idea is welcome!
It took me a few days to figure it out.
Test on different iPad device & multiple tasking mode, all works as expected.(iOS14+, haven't test on iOS13)
Minimal Example:
extension UISplitViewController {
open override func viewDidLoad() {
super.viewDidLoad()
self.show(.primary) // show sidebar, this is the key, toke me days to find this...
self.showsSecondaryOnlyButton = true
}
}
struct ContentView: View {
#State var column: Int = 3
var body: some View {
switch column {
case 3: // Triple Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
Text("Detail View")
}
default: // Double Column
NavigationView {
List {
HStack {
Text("Triple")
}
.onTapGesture {
column = 3
}
HStack {
Text("Double")
}
.onTapGesture {
column = 2
}
}
Text("Supplementary View")
}
}
}
}
My another answer: set sidebar default selected item.
Combine with this two solution, I have built an 2&3 column co-exist style's app.
SwiftUI, selecting a row in a List programmatically
Here is a solution which uses a custom ViewModifier. It's working on iOS 14.2, 15.0 and 15.2. Since you are using SidebarListStyle and Label I didn't test for prior versions.
Testproject:
enum Item: Hashable {
case animals, animals2, settings, settings2
static var threeColumns = [Item.animals, .animals2]
static var twoColumns = [Item.settings, .settings2]
var title: String {
switch self {
case .animals:
return "animals"
case .animals2:
return "animals2"
case .settings:
return "settings"
case .settings2:
return "settings2"
}
}
var systemImage: String {
switch self {
case .animals:
return "tortoise"
case .animals2:
return "hare"
case .settings:
return "gear"
case .settings2:
return "gearshape"
}
}
}
struct ContentView: View {
#State var selectedItem: Item?
var body: some View {
NavigationView {
List {
Section(header: Text("Three columns")) {
ForEach(Item.threeColumns, id: \.self) { item in
NavigationLink(tag: item, selection: $selectedItem) {
ItemsView()
} label: {
Label(item.title.capitalized, systemImage: item.systemImage)
}
}
}
Section(header: Text("Two columns")) {
ForEach(Item.twoColumns, id: \.self) { item in
NavigationLink(
destination: Text("I want to see here a single view, without detail"),
label: {
Label(item.title, systemImage: item.systemImage)
})
}
}
}
.listStyle(SidebarListStyle())
.navigationBarTitle("App Menu")
}
}
}
struct ItemsView: View {
let animals = ["Dog", "Cat", "Lion", "Squirrel"]
var body: some View {
List {
ForEach(animals, id: \.self) { animal in
NavigationLink(
destination: DetailView(animal: animal)) {
Text(animal)
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Animals")
}
}
struct DetailView: View {
var animal: String
var body: some View {
VStack {
Text("🐕")
.font(.title)
.padding()
Text(animal)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.sizeThatFits)
}
}
private struct ColumnModifier: ViewModifier {
let item: Item?
func body(content: Content) -> some View {
if item == .settings || item == .settings2 {
content
EmptyView()
} else {
content
EmptyView()
DetailView(animal: "default")
}
}
}
Suppose you want to create a triple-column view (with two sidebars and a detail view). Here is an example to show Projects in the first column, Files in second column and File Content in the last column.
The core step is to add three View()s in NavagationView { ... }.
import SwiftUI
struct MainView: View {
var body: some View {
ProjectsSidebar()
}
}
struct ProjectsSidebar: View {
var body: some View {
NavigationView {
List {
ForEach([1, 2, 3], id: \.self) { project_id in
VStack {
NavigationLink {
FilesSidebar(project_id: project_id)
.navigationTitle("Project \(project_id)")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.columns)
} label: {
Text("Project \(project_id)")
}
}
}
}
.listStyle(.sidebar)
.navigationTitle("Projects")
.navigationBarTitleDisplayMode(.inline)
FilesSidebar.DefaultView()
DetailView.DefaultView()
}
.navigationViewStyle(.columns)
}
}
struct FilesSidebar: View {
var project_id: Int
var body: some View {
List {
ForEach([1, 2, 3, 4], id: \.self) { file_id in
NavigationLink {
DetailView(project_id: project_id, file_id: file_id)
.navigationTitle("File")
.navigationBarTitleDisplayMode(.inline)
} label: {
Text("File \(file_id)")
}
}
}
.listStyle(.sidebar)
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a project.")
}
}
}
}
struct DetailView: View {
var project_id: Int
var file_id: Int
var body: some View {
VStack {
Text("Project \(project_id) - File \(file_id)")
}
}
struct DefaultView: View {
var body: some View {
VStack {
Text("Please select a file.")
}
}
}
}
first launch:
triple columns:
select project and file:
In a small sample SwiftUI app, I have a settings view that shows a couple of option selections, implemented as segmented controls. The text in these segmented controls visibly moves when an alert is presented or dismissed. Is there a way to get rid of this glitch?
Paste this in a Playground to reproduce:
import SwiftUI
import PlaygroundSupport
struct FlickeringSegmentsView: View {
#State var option = 0
#State var alerting = false
var body: some View {
VStack(alignment: .center, spacing: 120) {
Picker("options", selection: $option) {
Text("Option A").tag(0)
Text("Option B").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding(16)
Button(action: { self.alerting.toggle() },
label: { Text("Show Alert") }
)
.alert(isPresented: $alerting) {
Alert(title: Text("Alert"))
}
}
}
}
PlaygroundPage.current.setLiveView(FlickeringSegmentsView())
This issue is resolved in Xcode 12 beta using the included iOS 14 simulator (and hopefully stays that way).
I hope code below should help you:
public protocol SegmentedPickerViewElementTraits: Hashable {
var localizedText: String { get }
}
public struct SegmentedPickerView<Value, Data, ID, Label>: View
where
Value: SegmentedPickerViewElementTraits,
Data: RandomAccessCollection,
Data.Element == Value,
ID: Hashable,
Label: View {
public let data: Data
public let id: KeyPath<Data.Element, ID>
public let selection: Binding<Value>
public let label: Label
public init(data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<Value>,
label: Label) {
self.data = data
self.id = id
self.selection = selection
self.label = label
}
public var body: some View {
Picker(selection: selection, label: label) {
ForEach(data, id: id) {
Text($0.localizedText).tag($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
and lets modify your code:
enum Options: UInt8, CaseIterable {
case optionA
case optionB
}
extension Options: SegmentedPickerViewElementTraits {
var localizedText: String {
switch self {
case .optionA:
return "Option A"
case .optionB:
return "Option B"
}
}
}
struct FlickeringSegmentsView: View {
#State
var option: Options = .optionA
#State
var alerting = false
var body: some View {
VStack(alignment: .center, spacing: 120) {
SegmentedPickerView(
data: Options.allCases,
id: \.self,
selection: $option,
label: Text("options")
)
.padding(16)
Button(
action: { self.alerting.toggle() },
label: { Text("Show Alert") }
)
.alert(isPresented: $alerting) {
Alert(title: Text("Alert"))
}
}
}
}