SwiftUI Picker in a Form doesn't show the selected row - ios

I am trying to have a Picker that shows which option is currently selected.
Try out the following code which correctly selects the right option but the picker does not show which option is selected:
import SwiftUI
struct ContentView: View {
#State var selectedIndex: Int = 0
let strings: [String] = {
var strings: [String] = []
for i in 0..<10 {
strings.append("\(i)")
}
return strings
}()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selectedIndex,
label: Text("Selected string: \(strings[selectedIndex])")) {
ForEach(0..<strings.count) {
Text(self.strings[$0]).tag($0)
}
}
}
}
.navigationBarTitle("Form Picker",
displayMode: NavigationBarItem.TitleDisplayMode.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone know what could be wrong? It's observed using Xcode 11.1 and iOS 13.1

I created the simple picker I call "ListPicker" which should fit the bill. I've written it so it works well in a Form; if you need it outside of a Form you will have to tinker with it. If you see any way to improve the code, please add a comment; this is still a learning experience for all of us.
// MARK: - LIST PICKER (PUBLIC)
struct ListPicker<Content: View>: View {
#Binding var selectedItem: Int
var label: () -> Content
var data: [Any]
var selectedLabel: String {
selectedItem >= 0 ? "\(data[selectedItem])" : ""
}
var body: some View {
NavigationLink(destination: ListPickerContent(selectedItem: self.$selectedItem, data: self.data)) {
ListPickerLabel(label: self.label, value: "\(self.selectedLabel)")
}
}
}
// MARK: - INTERNAL
private struct ListPickerLabel<Content: View>: View {
let label: () -> Content
let value: String
var body: some View {
HStack(alignment: .center) {
self.label()
Spacer()
Text(value)
.padding(.leading, 8)
}
}
}
private struct ListPickerContentItem: View {
let label: String
let index: Int
let isSelected: Bool
var body: some View {
HStack {
Text(label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}.background(Color.white) // so the entire row is selectable
}
}
private struct ListPickerContent: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedItem: Int
var data: [Any]
var body: some View {
List {
ForEach(0..<data.count) { index in
ListPickerContentItem(label: "\(self.data[index])", index: index, isSelected: index == self.selectedItem).onTapGesture {
self.selectedItem = index
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
Then you can use it like this:
#State var selectedCar: Int = 0
let cars = ["Jaguar", "Audi", "BMW", "Land Rover"]
Form {
ListPicker(
selectedItem: self.$selectedCar,
label: {
Text("Cars")
},
data: self.cars
)
}

Related

How to select only one checkbox in foreach in SwiftUI?

I am trying to show checkbox in foreach loop but when i click on any one of them all are selected.
How can we separate them.
struct Screen: View {
#State private var checked = true
var data = ["1","2", "3"]
var body: some View {
ForEach( data.indices, id:\.self ) { item in
HStack {
CheckBoxView(checked: $checked)
Text(data[item])
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
Thank You for help.
The easiest way is to use an array of state variables:
struct Screen: View {
#State private var checked: [Bool] = [true, true, true]
var data = ["1","2", "3"]
var body: some View {
VStack {
ForEach( data.indices, id:\.self ) { index in
HStack {
CheckBoxView(checked: $checked[index])
Text(data[index])
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
}
However, I personally do not like this solution, because the state array is not dynamic in size to your data. With this initialization your state array is always the same size as your data.
struct Screen: View {
var data = ["1","2", "3"]
#State private var checked: [Bool]
init() {
_checked = State(initialValue: [Bool](repeating: false, count: data.count))
}
var body: some View {
VStack {
ForEach( data.indices, id:\.self ) { index in
HStack {
CheckBoxView(checked: $checked[index])
Text(data[index])
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
var body: some View {
Image(systemName: checked ? "checkmark.square.fill" : "square")
.foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
.onTapGesture {
self.checked.toggle()
}
}
}
}
Here is an answer that will allow you to check one or more boxes, and keep track of which values were selected. It uses an .onChanged to keep track of the actual value that has been selected as the check box itself is just UI:
struct Screen: View {
var data = ["1","2","3"]
#State var selectedItems: Set<String> = [] // Use a Set to keep track of multiple check boxes
#State var selectedItems = "" // Use a String to keep track of only one.
var body: some View {
List {
ForEach( data, id:\.self ) { item in
CheckBoxRow(title:item, selectedItems: $selectedItems, isSelected: selectedItems.contains(item))
}
}
}
}
struct CheckBoxRow: View {
var title: String
#Binding var selectedItems: Set<String>
#State var isSelected: Bool
var body: some View {
GeometryReader { geometry in
HStack {
CheckBoxView(checked: $isSelected, title: title)
.onChange(of: isSelected) { _ in
if isSelected {
selectedItems.insert(title)// or
selectedItems = title
} else {
selectedItems.remove(title)// or
selectedItems = ""
}
}
}
}
}
}
struct CheckBoxView: View {
#Binding var checked: Bool
#State var title: String
var body: some View {
HStack {
Image(systemName: (checked ? "checkmark.square" : "square"))
Text(title)
.padding(.leading)
}
.onTapGesture {
self.checked.toggle()
}
}
}

swiftui subview reappear after click the back button and update state data

Very strange behavior.
Click the back button on the subpage (Subview) to return to the main page (ContentView). However, the subpage (Subview) automatically opens again. Why?
import SwiftUI
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.onAppear {
update()
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
struct Subview: View {
var count : Binding<Int>
var body: some View {
Text("sub")
.onAppear {
count.wrappedValue += 1
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NavigationLink should always be inside a NavigationView. If you put it in the toolbar or some other place, you might run into weird issues.
Instead, use the init(destination:isActive:label:) initializer. Then set the presentingNextPage property to true when you want to present the next page.
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
#State var presentingNextPage = false
var body: some View {
NavigationView {
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
/// placeholder navigation link
NavigationLink(destination: Subview(count: $count), isActive: $presentingNextPage) {
EmptyView()
}
}
.onAppear {
self.update()
}
.navigationTitle("a")
.toolbar{
ToolbarItem(placement: .navigationBarTrailing) {
Button("sub") {
presentingNextPage = true /// set to true
}
}
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
Result:
Put "onAppear{...}" on the NavigationView not the List, like this:
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
.onAppear { // <---
update()
}
}

SwiftUI is it possible to pass range of binding in ForEach?

I'd like to pass a range of an array in a model inside ForEach.
I recreated an example:
import SwiftUI
class TheModel: ObservableObject {
#Published var list: [Int] = [1,2,3,4,5,6,7,8,9,10]
}
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { item in
SubView(subList: $model.list[0..<5]) <-- error if I put a range
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var body: some View {
HStack {
ForEach (subList, id:\.self) { item in
Text("\(item)")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
The work around
I found is to pass all the list and perform the range inside the subView. But I'd like don't do this because the array is very big:
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { i in
SubView(subList: $model.list, number: i, dimension: 5)
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var number: Int
var dimension: Int
var body: some View {
HStack {
ForEach (subList[number*dimension..<dimension*(number+1)].indices, id:\.self) { idx in
Button(action: {
subList[idx] += 1
print(subList)
}, label: {
Text("num: \(subList[idx])")
})
}
}
}
}
I would pass the model to the subview since it is a class and will be passed by reference and then pass the range as a separate parameter.
Here is my new implementation of SubView
struct SubView: View {
var model: TheModel
var range: Range<Int>
var body: some View {
HStack {
ForEach (model.list[range].indices, id:\.self) { idx in
HStack {
Button(action: {
model.list[idx] += 1
print(model.list)
}, label: {
Text("num: \(model.list[idx])")
})
}
}
}
}
}
Note that I added indices to the ForEach header to make sure we access the array using an index and not with a value from the array.
The calling view would then look like
var body: some View {
VStack {
SubView(model: model, range: (0..<5))
Text("\(model.list.map(String.init).joined(separator: "-"))")
}
The extra Text is just there for testing purposes

SwiftUI: Sheet cannot show correct values in first time

I found strange behavior in SwiftUI.
The sheet shows empty text when I tap a list column first time.
It seems correct after second time.
Would you help me?
import SwiftUI
let fruits: [String] = [
"Apple",
"Banana",
"Orange",
]
struct ContentView: View {
#State var isShowintSheet = false
#State var selected: String = ""
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
isShowintSheet = true
}) {
Text(fruit)
}
}
}
.sheet(isPresented: $isShowintSheet, content: {
Text(selected)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
list
first tap
after second tap
Use .sheet(item:) instead. Here is fixed code.
Verified with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State var selected: String?
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
}) {
Text(fruit)
}
}
}
.sheet(item: $selected, content: { item in
Text(item)
})
}
}
extension String: Identifiable {
public var id: String { self }
}
Thank you, Omid.
I changed my code from Asperi's code using #State like this.
import SwiftUI
struct Fruit: Identifiable, Hashable {
var name: String
var id = UUID()
}
let fruits: [Fruit] = [
Fruit(name: "Apple"),
Fruit(name: "Banana"),
Fruit(name: "Orange"),
]
struct ContentView: View {
#State var selected: Fruit?
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
}) {
Text(fruit.name)
}
}
}
.sheet(item: $selected, content: { item in
Text(item.name)
})
}
}

List reload animation glitches

So I have a list that changes when user fill in search keyword, and when there is no result, all the cells collapse and somehow they would fly over to the first section which looks ugly. Is there an error in my code or is this an expected SwiftUI behavior? Thanks.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel(photoLibraryService: PhotoLibraryService.shared)
var body: some View {
NavigationView {
List {
Section {
TextField("Enter Album Name", text: $viewModel.searchText)
}
Section {
if viewModel.libraryAlbums.count > 0 {
ForEach(viewModel.libraryAlbums) { libraryAlbum -> Text in
let title = libraryAlbum.assetCollection.localizedTitle ?? "Album"
return Text(title)
}
}
}
}.listStyle(GroupedListStyle())
.navigationBarTitle(
Text("Albums")
).navigationBarItems(trailing: Button("Add Album", action: {
PhotoLibraryService.shared.createAlbum(withTitle: "New Album \(Int.random(in: 1...100))")
}))
}.animation(.default)
}
}
1) you have to use some debouncing to reduce the needs to refresh the list, while typing in the search field
2) disable animation of rows
The second is the hardest part. the trick is to force recreate some View by setting its id.
Here is code of simple app (to be able to test this ideas)
import SwiftUI
import Combine
class Model: ObservableObject {
#Published var text: String = ""
#Published var debouncedText: String = ""
#Published var data = ["art", "audience", "association", "attitude", "ambition", "assistance", "awareness", "apartment", "artisan", "airport", "atmosphere", "actor", "army", "attention", "agreement", "application", "agency", "article", "affair", "apple", "argument", "analysis", "appearance", "assumption", "arrival", "assistant", "addition", "accident", "appointment", "advice", "ability", "alcohol", "anxiety", "ad", "activity"].map(DataRow.init)
var filtered: [DataRow] {
data.filter { (row) -> Bool in
row.txt.lowercased().hasPrefix(debouncedText.lowercased())
}
}
var id: UUID {
UUID()
}
private var store = Set<AnyCancellable>()
init(delay: Double) {
$text
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.sink { [weak self] (s) in
self?.debouncedText = s
}.store(in: &store)
}
}
struct DataRow: Identifiable {
let id = UUID()
let txt: String
init(_ txt: String) {
self.txt = txt
}
}
struct ContentView: View {
#ObservedObject var search = Model(delay: 0.5)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("filter", text: $search.text)
.padding(.vertical)
.padding(.horizontal)
List(search.filtered) { (e) in
Text(e.txt)
}.id(search.id)
}.navigationBarTitle("Navigation")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and i am happy with the result

Resources