This is a very similar problem to one I had before (which no one could answer). I'm trying to create a dynamic list in which I can edit elements. As far as I can gather, the recommended way to do this is to have an EditView, with bindings, that's activated by a NavigationLink in the LIst.
So, I've done that. It appears to work at first, until I realised that each NavigationLink would only work once (is this a bug?). I can't think what I could have done wrong to cause that.
Then I thought perhaps I can switch to in-place editing by having the EditView in the List. I devised a theoretical way to do this, then tried it in my code. And at first it seemed to work great. However, if 'edit in place' is on, deleting the last element causes 'Fatal error: Index out of range'.
I've bundled my whole code into one file so you can just copy and paste into Xcode to try for yourself.
I'm starting to think that maybe XCode 11.3.1 is far from the finished article, yet.
import SwiftUI
struct EditView: View {
#Binding var person:Person
var body: some View {
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}.frame(width:200)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}.navigationBarTitle("Edit entry")
}
}
struct Person:Identifiable, Equatable{
var id:UUID
var name1:String
var name2:String
var isEditable:Bool
}
class PersonList: ObservableObject {
#Published var individuals = [Person]()// Array of Person structs
}
struct ContentView: View {
#ObservedObject var people = PersonList()// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
#State private var allowEditing:Bool = false
var elementCount:Int{
let c = people.individuals.count
return c
}
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.circle")
.font(.largeTitle)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.individuals.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.elementCount)")
}
Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.red, lineWidth: 2))
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List{
ForEach(people.individuals){individual in
HStack{
if self.allowEditing{
//Toggle to edit in place
Toggle(isOn: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!].isEditable){
Text("edit").font(.headline).foregroundColor(.green).opacity(individual.isEditable ? 1.0 : 0.4)
}.frame(width:100)
}
if individual.isEditable{
EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])
}
else{
NavigationLink(destination:EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])){
Text("\(individual.name1) \(individual.name2)")
.frame(width: 200, alignment: .leading)
.padding(.all, 3)
}// link
}
}
}.onDelete(perform: deleteRow)
}
}.navigationBarTitle("People List (\(elementCount))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.individuals.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
}
}
Can anyone shed any light on this? I can't find anything to help me.
UPDATE: Thanks to 'krjw' for pointing out the single use NavLink problem does not happen on a real device.
The 'last element delete' issue seems to be something to do with an active binding being present in the element's view.
Ok despite my comment I tried to get to a solution and I might found an acceptable one:
I had to remodel Person... The whole indices was the issue of course but I couldn't exactly find out when what happens. I even tried with a local #State which updates the view and then updates the array of the #ObservedObject...
here are some links which could help to further investigate though...
Swift UI detail remove
How do I set the toggle state in a foreach loop in SwiftUI
Also this link here shows how to update members of an observed array generically which is pretty cool!:
https://stackoverflow.com/a/57920136/5981293
struct EditView: View {
#ObservedObject var person: Person
var body: some View {
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}//.frame(width:200)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}.navigationBarTitle("Edit entry")
}
}
struct RowView: View {
#Binding var allowEditing: Bool
#ObservedObject var individual: Person
var body: some View {
HStack {
if self.allowEditing {
//Toggle to edit in place
Toggle(isOn: self.$individual.isEditable){
Text("edit").font(.headline).foregroundColor(.green).opacity(self.individual.isEditable ? 1.0 : 0.4)
}//.frame(width:100)
}
if self.individual.isEditable{
EditView(person: self.individual)
}
else{
NavigationLink(destination:EditView(person: self.individual)){
Text("\(self.individual.name1) \(self.individual.name2)")
//.frame(width: 200, alignment: .leading)
.padding(.all, 3)
}// link
}
}
}
}
class Person: ObservableObject, Identifiable {
#Published var id:UUID
#Published var name1:String
#Published var name2:String
#Published var isEditable:Bool
init(id: UUID, name1: String, name2: String, isEditable: Bool){
self.id = id
self.name1 = name1
self.name2 = name2
self.isEditable = isEditable
}
}
struct ContentView: View {
#State var people = [Person]()//try! ObservableArray<Person>(array: []).observeChildrenChanges(Person.self)// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
#State private var allowEditing:Bool = false
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.circle")
.font(.largeTitle)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.people.count)")
}
Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.red, lineWidth: 2))
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List {
ForEach(self.people) { person in
RowView(allowEditing: self.$allowEditing, individual: person)
}.onDelete(perform: deleteRow)
}
}.navigationBarTitle("People List (\(self.people.count))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.remove(atOffsets: offsets)
print(self.people.count)
}
}
I hope this helps!
Related
SwiftUI
[This is the code for the list and my button]
ForEach(items, id: \.self) { current in
VStack(alignment: .leading) {
Text("\(current.task)")
.padding(2)
Text("\(dateFormatter.string(from: current.date))")
Button(action: {},
label: {
Image(systemName:
"checkmark.rectangle.fill")
.resizable()
.frame(width: 50, height: 35)
})
}
Items is my array containing a string and a date. Here is the code for it:
#State var items:[Tasks] = [Tasks(task: "Test", date: Date())]
And here is Tasks:
struct Tasks: Hashable {
let task: String
let date: Date
}
This is my list view
I want to have a user be able to click a button under each list item and it will remove that list item. I am currently using the onDelete method but I would prefer to have a confirm button in each list item that would allow the user to remove that list item.
#State var counter = -1
I tried using a counter that would increase by 1 each time the ForEach loop ran and create a new variable inside of that ForEach loop equal to it. However I could not access the variable inside of the button to use as an index.
Using your current code structure, assuming each task: String is unique, you could just add this to your Button action:
Button(action: {
if let index = items.firstIndex(where: {$0.task == current.task}) {
items.remove(at: index)
}
},
label: {
Image(systemName: "checkmark.rectangle.fill")
.resizable()
.frame(width: 50, height: 35)
})
However, it would be more robust to add an id property to your Task, and
at the same time rename it, because Swift already defines Task as part of its async/await framework.
Here is the complete code I use to test my answer:
struct MyTask: Identifiable, Hashable { // <--- here
let id = UUID() // <--- here
let task: String
let date: Date
}
struct ContentView: View {
#State var items:[MyTask] = [
MyTask(task: "Test1", date: Date()),
MyTask(task: "Test2", date: Date().addingTimeInterval(60*60*24*44)),
MyTask(task: "Test3", date: Date().addingTimeInterval(60*60*24*88))
]
var body: some View {
ForEach(items) { current in // <--- here
VStack(alignment: .leading) {
Text(current.task).padding(2) // <--- here
Text(dateFormatter.string(from: current.date)) // <--- here
Button(action: {
// --- here
if let index = items.firstIndex(where: {$0.id == current.id}) {
items.remove(at: index)
}
},
label: {
Image(systemName: "checkmark.rectangle.fill").resizable()
.frame(width: 50, height: 35)
})
}
}
}
var dateFormatter: DateFormatter = {
let frmt = DateFormatter()
frmt.dateFormat = "yyyy-MMM-dd"
return frmt
}()
}
I have a variable "textEditorText" bound to the "TextEditor".
When the button "send to the player" is clicked, the entire content of the text editor is transferred to the "stringArray" array.
But in this array, all the text is contained as one element, and I need to split it into words.
For example, the sentence "one, two, three" needs to be turned into the elements "one", "two", "three
I'm trying to do this by passing the content from stringArray to stringArray2 but I'm getting the error messages
How can I overcome this situation?
Thank you.
import SwiftUI
struct ContentView: View {
#State var textEditorText:String = "one two three"
#State var stringArray:[String] = []
var stringArray2:[String] = stringArray.components(separatedBy: " ")
var body: some View {
NavigationView {
VStack{
TextEditor(text: $textEditorText)//binding to #State var textEditorText
.frame(height: 200)
.cornerRadius(10)
Button (action: {
textEditorText = ""
}, label: {
Text("clear the editor".uppercased())
.font(.headline)
//.foregroundColor(.white)
.padding(10)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
})
Button (action: {
if stringArray.isEmpty && !textEditorText.isEmpty {
stringArray.append(textEditorText)
}
}, label: {
Text("send to the player".uppercased())
.font(.headline)
.padding(10)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
//.background(stringArray.isEmpty ? Color.red : Color.blue)
.background(Color.blue)
.cornerRadius(10)
})
Button(action: {
stringArray.removeAll()
}, label: {
Text("clear the player".uppercased())
.padding(10)
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10))
.foregroundColor(.white)
.font(.headline)
.shadow(radius: 5)
})
ForEach(stringArray, id: \.self) { data in
Text(data)
}
.frame(maxWidth:.infinity, maxHeight: 30)
Spacer()
} //VStack
.padding(10)
.background(Color.gray.opacity(0.25))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There's a lot of unnecessary code in your question, but if I understand correctly what you're trying to do, you just need to change it to use
stringArray.append(contentsOf: )
rather than
stringArray.append()
for example:
if stringArray.isEmpty && !textEditorText.isEmpty {
stringArray.append(contentsOf: textEditorText.components(separatedBy: " "))
}
New to SwiftUI. Trying to get a JSON key:value array to update to the next random item when the user presses the button. Got it to load up just fine, but the button does nothing. Tried making a shuffle function, but couldn't find a way to pass in the new values to the Text areas. Also tried to make my decodedQuotes and quote variables into #State vars inside the View, but they initialize before self is available.
Could normally call touchesBegan and write a simple function in Storyboard. Is there something similar I could do here?
var decodedQuotes = Bundle.main.decode([Quote].self, from: "quotes.json")
// parses an array with "quote":"name" pairs
var quote = decodedQuotes.randomElement()!
struct QuoteView: View {
var body: some View {
Button(action:
// Need it to update the Text below with a new random item from quote
)
HStack {
VStack {
HStack(alignment: .center) {
Text(quote.quote)
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.black)
}
HStack {
Text("-\(quote.name)")
.foregroundColor(.black)
}
}
}
.frame(width: 300, height: 300, alignment: .center)
.background(Background(isHighlighted: true, shape: Rectangle()))
.foregroundColor(.blue)
.padding(4)
.cornerRadius(20)
}
}
You were on the right track with #State
struct Quote {
var quote : String
var name : String
}
var decodedQuotes = [Quote(quote: "test1", name: "name1"),
Quote(quote: "test2", name: "name2"),
Quote(quote: "test3", name: "name3"),]
struct QuoteView: View {
#State var quote : Quote? = decodedQuotes.randomElement()
var body: some View {
Button(action: {
quote = decodedQuotes.randomElement()
}) {
Text("New quote")
}
if let quote = quote {
HStack {
VStack {
HStack(alignment: .center) {
Text(quote.name)
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.black)
}
HStack {
Text("-\(quote.name)")
.foregroundColor(.black)
}
}
}
.frame(width: 300, height: 300, alignment: .center)
.foregroundColor(.blue)
.padding(4)
.cornerRadius(20)
}
}
}
Obviously, for testing, I just used an array of pre-made Quotes
If you wanted to, you could make decodedQuotes a #State property on the QuoteView as well and decode them in onAppear
I've also chosen to make quote an optional for now. I check to see if it's available by doing the if let quote = quote line. This should be a bit future-proof in case you start loading quotes from other places at some point.
I believe this is a better implementation in the current SwiftUI where the text actually changes within the button. I hope it helps>
import SwiftUI
struct Quote {
var quote : String
var name : String
}
var decodedQuotes = [Quote(quote: "Title 1", name: "Description 1."),
Quote(quote: "Title 2", name: "Second description."),
Quote(quote: "Title 3", name: "final item."),]
struct ContentView: View {
#State var quote : Quote? = decodedQuotes.randomElement()
var body: some View {
Button(action: {
quote = decodedQuotes.randomElement()
}) {
Text("New quote")
if let quote = quote {
HStack {
VStack {
VStack(alignment: .center) {
Text(quote.quote)
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.blue)
Text("-\(quote.name)")
.foregroundColor(.blue)
}
}
}
.frame(width: 300, height: 300, alignment: .center)
.foregroundColor(.blue)
.padding(4)
.cornerRadius(20)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
UI: https://imgur.com/a/0BbJBFc
I'm using the ForEach to iterate over an array of minerals in the example code, but I can't find a proper solution to loop the second array (mineral amount) right underneath the minerals.
In different project, I made it so far that the ForEach loops both arrays but for every mineral displays all amount for the Planet and for the second mineral all amount for the Planet and so on.
I did create a new struct with both arrays but without success. Adding a binding property also failed. I hope to learn a new swift method to achieve the desire look.
Data File
import Foundation
struct Planet: Identifiable {
var id = UUID()
var name: String
var minerals: [String]
var mineralAmount: [String]
}
let planetsData: [Planet] = [
Planet(name: "Jupiter", minerals: ["Iron", "Gold", "Copper"], mineralAmount: ["10K", "20K", "30K"]),
Planet(name: "Earth", minerals: ["Lithium", "Aluminium", "Quartz"], mineralAmount: ["40K", "50K", "60K"])
]
ContentView
import SwiftUI
struct ContentView: View {
var planet: Planet
var body: some View {
VStack() {
ForEach(planet.minerals, id: \.self) { item in
Text(item)
.font(.system(size: 22, weight: .regular))
.foregroundColor(.primary)
Text("amount to be added")
.font(.system(size: 22, weight: .regular))
.foregroundColor(.primary)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(planet: planetsData[0])
}
}
[1]: https://i.stack.imgur.com/4MDKs.png
You can achieve it this way:
ScrollView {
VStack (alignment: .leading) {
ForEach(0..<planet.minerals.count) { i in
HStack {
Circle()
.frame(width: 50, height: 50)
.foregroundColor(.black)
VStack (alignment: .leading) {
Text(planet.minerals[i])
.font(.system(size: 22, weight: .regular))
.foregroundColor(.primary)
Text(planet.mineralAmount[i])
.font(.system(size: 22, weight: .regular))
.foregroundColor(.secondary)
}
}
}
Spacer()
}
}
why don't you create a dictionary from both values
mineralsDic = [minerals: mineralAmount]
I of course know the syntax of dictionary but I just try to explain my idea + instead of making 2 loops you can make only one which has less complexity and better performance
look for some help on my study of the scrollView in swiftUI.
I have a scroll view that display the value of an array, base when the user tap on the different item of scroll view I want to display it on the textField below.
how can I pass the array value to the text field??
import SwiftUI
struct ContentView: View {
let post = ["TEST1 ","Test 2" , "Test 3","TEST4 ","Test 5" , "Test 6"]
var temp = ""
var body: some View {
VStack {
ScrollView(.horizontal, content: {
HStack(spacing: 100) {
ForEach(post, id: \.self){ item in
ZStack {
Rectangle().foregroundColor(.blue).frame(width: 190, height: 170, alignment: .center)
Text(item)
}.onTapGesture {
// pass the value off Scroll View to the text
debugPrint("\(item)")
}
}
}
.padding(.leading, 10)
})
.frame(height: 190)
Spacer()
Text("dispaly here array value selected")
Spacer()
}
}
}
thank for helping me...
The trick here is you need to #State temp when you need to assign to a #State value inside the view.
struct ContentView: View {
let post = ["TEST1 ","Test 2" , "Test 3","TEST4 ","Test 5" , "Test 6"]
#State private var temp = ""
var body: some View {
VStack {
ScrollView(.horizontal, content: {
HStack(spacing: 100) {
ForEach(post, id: \.self){ item in
ZStack {
Rectangle().foregroundColor(.blue).frame(width: 190, height: 170, alignment: .center)
Text(item)
}.onTapGesture {
// pass the value off Scroll View to the text
self.temp = item
}
}
}
.padding(.leading, 10)
})
.frame(height: 190)
Spacer()
Text( self.temp)
Spacer()
}
}
}