SwiftUI: Index out of range when deleting TextField() but not Text() - swiftui

The code below throws an index out of range error when deleting TextField() but not when deleting Text().
Here is the full error: Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
Section{
// Text(infoList[item].title) //<-- this doesn't throw error when deleted
TextField(infoList[item].title, text: $infoList[item].title)
}
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}

#Asperi's response about creating a dynamic container was correct. I just had to tweak a bit to make it compatible with Identifiable. Final code below:
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
EditorView(container: self.$infoList, index: item, text: infoList[item].title)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}
struct EditorView : View {
var container: Binding<[Information]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = Information(id: UUID(), title: text)
})
}
}

Related

Unwrapping Optional Entity

Let's say that Item is a CoreData entity:
struct ItemDetailView: View {
#Binding var item: Item?
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding($item.data)! )
}else{
Text(item!.data!)
}
}
}
Error: Value of optional type 'Item?' must be unwrapped to refer to member 'data' of wrapped base type 'Item'
Edit All the code:
import SwiftUI
import CoreData
struct ItemDetailView: View {
#Binding var item: Item?
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding($item.data)! )
}else{
Text(item!.data!)
}
}
}
struct ItemEditorView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) private var dismiss
let isNew: Bool
#State var isEditing: Bool = false
#State var item: Item?
#Binding var newItem: Item?
var body: some View {
if isNew {
}
NavigationView{
ItemDetailView(item: isNew ? $newItem : $item, isEditing: $isEditing)
.toolbar {
ToolbarItem {
Button(isNew ? "Add" : (isEditing ? "Done" : "Edit")) {
//TBI
if isNew {
}
}
}
ToolbarItem(placement:.cancellationAction){
Button("Cancel"){
dismiss()
}
}
}
.navigationTitle("Item Editor")
}
}
}
struct ItemsListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.data, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State var presentNewItemEditorView = false
#State var newItem: Item?
var body: some View {
NavigationView {
VStack{
Text(newItem?.data ?? "nil")
List {
ForEach(items){ item in
NavigationLink(item.data!, destination: ItemEditorView(isNew: false, item:item, newItem: $newItem))
}
}
}
.fullScreenCover(isPresented: $presentNewItemEditorView, content: {
ItemEditorView(isNew: true, isEditing: true, newItem: $newItem)
})
.navigationTitle("Main")
.toolbar {
ToolbarItem {
Button("New goal"){
presentNewItemEditorView = true
}
}
}
.task {
newItem = Item(context: viewContext)
newItem!.data = "New item text"
}
}
}
}
I it unclear what are you trying to achieve. Here are options that will compile.
Value type
struct Item {
var data: String
}
struct ItemDetailView: View {
#Binding var item: Item
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField( "Name", text: Binding(projectedValue: $item.data) )
}else{
Text(item.data)
}
}
}
Reference type and value type
class Item {
var data: String? = ""
}
struct ItemDetailView: View {
#Binding var item: Item? // Has no sense, because #Binding does't work with clsses!
#Binding var isEditing: Bool
var body: some View{
if isEditing {
TextField("Name", text: Binding(get: {
item!.data!
}, set: { value in
item?.data = value
}))
} else {
Text(item!.data!)
}
}
}
Implementation by Conny Wals sugested by #vadian
import CoreData
class EditViewModel {
/// **new** instance of item to edit. Don't edit the item, if any, that we pass to VM
var item: Item4
/// Context to use in the isolated changes
let context: NSManagedObjectContext
/// Pass an item if you want to edit one, or nil to generate one
init(item: Item4? = nil){
self.context = PersistenceController.shared.childViewContext()
if let item = item {
self.item = PersistenceController.shared.copyForEditing(of: item, in: context)
} else {
self.item = PersistenceController.shared.newTemporaryInstance(in: context)
}
}
func persist(){
PersistenceController.shared.persist(item)
}
}
import SwiftUI
struct EditItemView: View {
#State var viewModel: EditViewModel
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack{
TextField( "Item Name", text: Binding(get: {viewModel.item.name ?? ""}, set: {viewModel.item.name = $0}))
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
.navigationTitle("Edit Item")
.navigationBarItems(leading: Button("Cancel"){
dismiss()
}, trailing: Button("Save"){
viewModel.persist()
dismiss()
})
}
}
}
struct ItemsView04: View {
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item3.name, ascending: true)],
animation: .default)
private var items: FetchedResults<Item4>
#State var editItemViewIsPresent = false
var body: some View {
NavigationView{
List {
ForEach(items){ item in
NavigationLink(item.name ?? ""){
EditItemView(viewModel: EditViewModel(item: item))
}
}
}
.toolbar {
ToolbarItem {
Button {
editItemViewIsPresent = true
}label:{
Text("+")
}
}
}
.fullScreenCover(isPresented: $editItemViewIsPresent){
EditItemView(viewModel: EditViewModel())
}
}
}
}

SwiftUI - Binding var updates the original value only the first time, and then it doesn't update anymore

I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.

SwiftUI Picker Item multilines

How do I do text wrapping?
Here is my code:
struct ContentView: View {
private let items: [String] = [
"OneLineLongggggggggggggggggggggggggggggggggggggggggg",
"TwoLinesLonggggggggggggg\nLongggggggggggggggg",
"ThreeLinesLonggggggggggggg\nLongggggggggggggggg\nLongggggggggggggggg"
]
#State private var text: String = ""
var body: some View {
VStack {
Picker("Select Text", selection: self.$text) {
ForEach(self.items, id: \.self) {
Text($0)
.tag($0)
}
}
Text("select: \(self.text)")
}
}
}
Here is the result:
If I add fixSize then the elements run over each other:
struct ContentView: View {
private let items: [String] = [
"OneLineLongggggggggggggggggggggggggggggggggggggggggg",
"TwoLinesLonggggggggggggg\nLongggggggggggggggg",
"ThreeLinesLonggggggggggggg\nLongggggggggggggggg\nLongggggggggggggggg"
]
#State private var text: String = ""
var body: some View {
VStack {
Picker("Select Text", selection: self.$text) {
ForEach(self.items, id: \.self) {
Text($0)
.tag($0)
.fixedSize(horizontal: false, vertical: true)
}
}
Text("select: \(self.text)")
}
}
}
Here is the result:
Please tell me in which direction to look for the answer?
Please use .lineLimit(any number of max lines you want) with text.
In this case you can use
Text($0)
.tag($0)
.lineLimit(3)

SwiftUI Catch the Picker value from another view

I need to add searchbar and nil value to picker. I want to create a custom picker and use it everywhere to avoid code repetition. I was able to create a picker using this and this code, but I could not find how to capture the value I chose in another view.
This code helps me search inside the picker.
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.autocapitalizationType = .none
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
}
This is the custom picker I created.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#State var item: Item? = nil
#State var searchText: String = ""
var filteredItems: [Item] {
items.filter {
searchText.isEmpty ? true : $0[keyPath: text].lowercased().localizedStandardContains(searchText.lowercased())
}
}
var body: some View {
Picker(selection: $item, label: Text(title)) {
if needSearchBar {
SearchBar(text: $searchText, placeholder: "Search")
.padding(.horizontal, -12)
}
if needOptional {
Text("[none]").tag(nil as Item?)
.foregroundColor(.red)
}
ForEach(filteredItems, id: \.self) { item in
Text(item[keyPath: text])
.tag(item as Item?)
}
}
.onAppear{
searchText = ""
}
}
}
Usage
Country Model Example
struct Country: Codable, Identifiable, Hashable {
let id = UUID()
let name: String
enum CodingKeys: String, CodingKey {
case id, name
}
}
in ContentView
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true)
}
How do I catch the selected Item(optional) in ContentView?
Use #Binding to bind your item.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#Binding var item: Item? // <--Here
#State var searchText: String = ""
//-----Other code---------//
And use #State here in content view for getting selected value.
struct ContentView: View {
#State private var selectedItem: Country? //<-- Here
private let countryArray: [Country] = [.init(name: "A"), .init(name: "B")]
var body: some View {
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true, item: $selectedItem) // <-- Here
if let selectedItem = selectedItem {
Text(selectedItem.name)
}
}
}
}

SwiftUI NavigationLink isActive not working with dynamic Lists

Using a data-structure for isActive bools for dynamic NavigationLinks to enable a programmatic pop-back doesn't work. Tapping the Pop Back in the Destination should pop the view back to the root DynamicList, but it doesn't work.
import SwiftUI
struct DynamicList: View {
#ObservedObject var listViewModel = ListViewModel()
var body: some View {
NavigationView {
List {
ForEach(0..<self.listViewModel.cities.count, id: \.self) { index in
NavigationLink(
destination: Destination(isActive: $listViewModel.isActiveItems[index], city: $listViewModel.cities[index]),
isActive: $listViewModel.isActiveItems[index],
label: {
Text(listViewModel.cities[index])
})
}
}
.navigationBarTitle(Text("Locations"))
.navigationBarItems(trailing: Button(action: {
self.addRow()
}) {
Image(systemName: "plus")
})
}
}
private func addRow() {
self.listViewModel.addCity(city: ["New York", "London", "Moskau", "Sydney"].randomElement() ?? "")
}
}
class ListViewModel: ObservableObject {
#Published var cities: [String] = []
#Published var isActiveItems: [Bool] = []
func addCity(city: String){
cities.append(city)
isActiveItems.append(false)
}
}
struct Destination: View {
#Binding var isActive: Bool
#Binding var city: String
var body: some View {
Text(city)
Button(action: {
isActive.toggle()
}, label: {
Text("Pop Back")
})
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}