I want to basically make didSelectRow like UITableView in SwiftUI.
This is the code:
struct ContentView: View {
var testData = [Foo(name: "1"),
Foo(name: "2"),
Foo(name: "3"),
Foo(name: "4"),
Foo(name: "5")]
#State var selected: Foo?
var body: some View {
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
}
}
I was thought if I click the cell then selected should contains the selected value, but it's not. The selected has no value. And the cell not clickable.
So I added a Button.
NavigationView {
VStack {
List(testData, id: \.name, selection: $selected) { foo in
HStack {
Text(foo.name)
Button("Test") {
print("\(foo) is selected.")
print(selected?.name)
}
}
}.navigationBarTitle("Selected \(selected?.name ?? "")")
Button("Check:") {
print(selected?.name)
}
}
Now, click works, but actually foo is the one I want there's no need selected why selection of the List is here.
Not sure anything I missed. Should the Button is necessary for the List "didSelectRow"? thanks!
EDIT
After a bit more investigation, my current conclusion is:
For single selections, no need call List(.. selection:). But you have to use Button or OnTapGesture for clickable.
List(.. selection:) is only for edit mode, which is multiple selection, as you can see the selection: needs a set. My example should be
#State var selected: Set<Foo>?
On iOS selection works in Edit mode by design
/// Creates a list with the given content that supports selecting multiple
/// rows.
///
>> /// On iOS and tvOS, you must explicitly put the list into edit mode for
>> /// the selection to apply.
///
/// - Parameters:
/// - selection: A binding to a set that identifies selected rows.
/// - content: The content of the list.
#available(watchOS, unavailable)
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
so you need either add EditButton somewhere, or activate edit mode programmatically, like
List(selection: $selection) {
// ... other code
}
.environment(\.editMode, .constant(.active)) // eg. persistent edit mode
Update: Here is some demo of default SwiftUI List selection
struct DemoView: View {
#State private var selection: Set<Int>?
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
List(selection: $selection) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
}.onDelete(perform: {_ in})
}
.environment(\.editMode, .constant(.active))
}
}
Related
Scenario: I want to handle the selection event whilst harvesting the selected item via a Picker.
This is in reference to an introduction discussion to the Picker Proxy.
This is what I have so far. I can't get the event handler to fire/activate/run doSomething().
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { _ in
VStack {
Text("PickerView")
.font(.headline)
.foregroundColor(.gray)
.padding(.top, 10)
Picker("test", selection: Binding(get: { "" }, set: { _ in
doSomething()
})) {
Text("Hello").id("1")
Text("Uncle").id("2")
Text("Ric").id("3")
}.labelsHidden()
}.background(RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white).shadow(radius: 1))
}
.padding()
}
func doSomething() {
print("Hello Something!")
}
}
Note:
I don't know what to do with the get{} so I put a null string there to satisfy the compiler.
I tried evaluating the closure parameter (via print(*closure parameter*)) but got no value, so I placed a _ placeholder to satisfy the compiler.
How to I harvest the selection?
The closure parameter doesn't appear to work here; hence the placeholder. There aren't many examples to follow.
Here is a possible solution:
struct ContentView: View {
// extract picker values for easier access
private let items = ["Hello", "Uncle", "Ric"]
// store the currently selected value
#State private var selection = 0
// custom binding for the `selection`
var binding: Binding<Int> {
.init(get: {
selection
}, set: {
selection = $0
doSomething() // call another function after the `selection` is set
})
}
var body: some View {
Picker("test", selection: binding) {
ForEach(0 ..< items.count) { index in // use `ForEach` to quickly generate picker values
Text(items[index])
.tag(index) // use `tag` instead of `id`
}
}
.labelsHidden()
}
func doSomething() {
print("Hello Something!")
}
}
Discussion:
You can use onChange to trigger a side effect as the result of a value changing, such as an Environment key or a Binding...
(Apple doc.)
struct ContentView: View {
#State
private var selection = 0
let items = ["Hello", "Uncle", "Ric"]
var body: some View {
VStack {
Picker("", selection: $selection) {
ForEach(0..<items.count) {
Text(items[$0])
.tag($0)
}
}
.onChange(of: selection) { _ in
print("Hello Something!")
}
}
}
}
This is my model
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}
This is the view that will be displayed with a Picker. The list will be populated by an outside source but I simplified it for this example.
struct TypeSelectionView: View {
#State private var selected = 0
let testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
var body: some View {
VStack {
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(0 ..< testList.count) {
Text(testList[$0].name)
}
}.labelsHidden()
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(testList) {type in
Text(type.name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
The first Picker is correctly changing the display of the Text view on the page when the Picker changes but the second Picker does not. Is their a way to make the second Picker do the do the same thing where as you change the Picker the Text view will update accordingly
or is the first Picker the way you should always go when making Pickers in SwiftUI?
The reason your second Picker doesn't work is that the values returned by the Picker correspond to the id of the items. In the case of your second Picker, those are String.
You can apply a .tag() to each item, and then the Picker will return that. For example, if you added an explicit tag it would work:
Text(type.name).tag(testList.firstIndex(where: { $0.id == type.id })!)
Alternatively, if you changed your id values to be Int and the id values corresponded to the position in the array, it would work.
Because of the difficulties of implementing a tag, it is easy to see why many developers choose to just iterate on 0 ..< testList.count.
Ok so this my first ever answer for a stack overflow question, I'm quite a newbie myself but hopefully I can be of some help.
The code when placed in to Xcode shows two pickers whose initial values are name1 but when you change the first picker the second picker and the text displaying the selected type change accordingly, but because both pickers share the same source of truth #State private var selected = 0, changing this will have unintended side effects.
import SwiftUI
struct TypeSelectionView: View {
#State private var selected = 0
#State var testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
#State var priorityTypes = ["low", "medium", "high", "critical"]
var body: some View {
VStack {
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}
The use case is pretty simple. I have a List of places, and each corresponds to a geofence. I need to enable navigation in that particular row(s) whose geofence the user is inside. And the list is dynamic and is fetched from an API. There are similar question on Stackoverflow, but those address only static lists.
I tried using a dictionary of bools, but I am not able to make it work.
This is a simplified mock code:
struct ListView: View {
#State private var navActive: [UUID: Bool] = [:]
var body: some View {
List (viewModel.allItems, id: \.id) { item in
NavigationLink(destination: DetailView(item), isActive: $navActive[item.id]) {
Text(item.name)
.onTapGesture {
if currentLocation.isInside(item.geofence) { navActive[item.id] = true }
}
}
}
}
}
I get this error: Cannot convert value of type 'Binding<Bool?>' to expected argument type 'Binding<Bool>'
on the NavigationLink isActive argument.
Note: I've populated the navActive dictionary with key-value pairs on receiving allItems with an onRecieve modifier
Here is a possible approach for your use-case
struct ListView: View {
// ... your view model defined here
#State private var selectedItem: UUID? = nil
var body: some View {
List (viewModel.allItems, id: \.id) { item in
Text(item.name)
.onTapGesture {
self.selectedItem = item.id
}
.background(
NavigationLink(destination: DetailView(item), tag: item.id,
selection: $selectedItem) { EmptyView() }
.buttonStyle(PlainButtonStyle())
)
}
}
}
I have a SwiftUI ScrollView with an HStack and a ForEach inside of it. The ForEach is built off of a Published variable from an ObservableObject so that as items are added/removed/set it will automatically update in the view. However, I'm running into multiple problems:
If the array starts out empty and items are then added it will not show them.
If the array has some items in it I can add one item and it will show that, but adding more will not.
If I just have an HStack with a ForEach neither of the above problems occur. As soon as it's in a ScrollView I run into the problems.
Below is code that can be pasted into the Xcode SwiftUI Playground to demonstrate the problem. At the bottom you can uncomment/comment different lines to see the two different problems.
If you uncomment problem 1 and then click either of the buttons you'll see just the HStack updating, but not the HStack in the ScrollView even though you see init print statements for those items.
If you uncomment problem 2 and then click either of the buttons you should see that after a second click the the ScrollView updates, but if you keep on clicking it will not update - even though just the HStack will keep updating and init print statements are output for the ScrollView items.
import SwiftUI
import PlaygroundSupport
import Combine
final class Testing: ObservableObject {
#Published var items: [String] = []
init() {}
init(items: [String]) {
self.items = items
}
}
struct SVItem: View {
var value: String
init(value: String) {
print("init SVItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
struct HSItem: View {
var value: String
init(value: String) {
print("init HSItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
public struct PlaygroundRootView: View {
#EnvironmentObject var testing: Testing
public init() {}
public var body: some View {
VStack{
Text("ScrollView")
ScrollView(.horizontal) {
HStack() {
ForEach(self.testing.items, id: \.self) { value in
SVItem(value: value)
}
}
.background(Color.red)
}
.frame(height: 50)
.background(Color.blue)
Spacer()
Text("HStack")
HStack {
ForEach(self.testing.items, id: \.self) { value in
HSItem(value: value)
}
}
.frame(height: 30)
.background(Color.red)
Spacer()
Button(action: {
print("APPEND button")
self.testing.items.append("A")
}, label: { Text("APPEND ITEM") })
Spacer()
Button(action: {
print("SET button")
self.testing.items = ["A", "B", "C"]
}, label: { Text("SET ITEMS") })
Spacer()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
// problem 1
rootView: PlaygroundRootView().environmentObject(Testing())
// problem 2
// rootView: PlaygroundRootView().environmentObject(Testing(items: ["1", "2", "3"]))
)
Is this a bug? Am I missing something? I'm new to iOS development..I did try wrapping the actual items setting/appending in the DispatchQueue.main.async, but that didn't do anything.
Also, maybe unrelated, but if you click the buttons enough the app seems to crash.
Just ran into the same issue. Solved with empty array check & invisible HStack
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
if (self.items.count == 0) {
HStack{
Spacer()
}
}
}
It is known behaviour of ScrollView with observed empty containers - it needs something (initial content) to calculate initial size, so the following solves your code behaviour
#Published var items: [String] = [""]
In general, in such scenarios I prefer to store in array some "easy-detectable initial value", which is removed when first "real model value" appeared and added again, when last disappears. Hope this would be helpful.
For better readability and also because the answer didn't work for me. I'd suggest #TheLegend27 answer to be slightly modified like this:
if self.items.count != 0 {
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
}
}
Using Xcode 11.0, Beta 5.
I have a List driven from an array of model objects in an observed view model. Each item in the List has a NavigationLink with a destination detail view/view model that accepts the model as an argument.
The user can tap a bar button above the list to add a new item which is added to the view model's array and the List is therefore reloaded with the new item displayed.
The issue I cannot solve is how to select that new item in the list and therefore display the detail view without needing the user to select it manually. (This is an iPad app with split screen view, hence the reason to want to select it)
I've tried using a NavigationLink programatically, but can't seem to get anything to work. I looked at the selection argument for the List but that also requires the list to be in edit mode, so that's no good.
Any suggestions are very welcome!
The following solution uses the selection attribute of NavigationLink. One problem here is that only items currently visible get rendered, so selecting a row further down does nothing.
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var title: String
var content: String
}
struct ItemOverview: View {
let item: Item
var body: some View {
Text(item.title)
}
}
struct ItemDetailsView: View {
let item: Item
var body: some View {
VStack{
Text(item.title).font(.headline)
Divider()
Text(item.content)
Spacer()
}
}
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
struct ListSelectionView: View {
#State var items: [Item] = [
Item(title: "Hello", content: "Content World"),
Item(title: "Hey", content: "Content Du"),
]
#State var selection: UUID? = nil
func createItem() {
let newItem = Item(title: randomString(length: 3), content: randomString(length: 10))
self.selection = newItem.id
self.items.append(newItem)
}
var body: some View {
VStack{
NavigationView{
List(items) { item in
NavigationLink(destination: ItemDetailsView(item: item), tag: item.id, selection: self.$selection, label: {
Text(item.title)
})
}
}
Button(action: {
self.createItem()
}) { Text("Add and select new item") }
Divider()
Text("Current selection2: \(String(selection?.uuidString ?? "not set"))")
}
}
}
A second problem is that changing the $selection makes Modifying state during view update appear.
Third problem is that after manual selection the shading stays on the same item until again changed by hand.
Result
Programmatic selection is not really usable for now if you want to select a link not initialized yet (not visible?).
Further ideas
One might look into tags a little bit more.
Another option could be paging where all items of the current page are visible.
One could also use list selection and show details based on that.