I am trying to get a value from a picker and store it so it can be passed into the function...
The problem is that I try to pass the Picker value into a function as the action for a button, then covert it to an integer, but it just uses the default value, and it seems like the it is never reading the value from the picker
Here is my ContentView file for reference:
struct ContentView: View {
#State var repetitions: String
let directions : [String] = ["Left", "Right"]
var body: some View {
VStack {
Picker("Values", selection: $repetitions) {
ForEach((0...100), id: \.self) {
Text("\($0)")
}
}
Button(action: {choseDirection(reps: repetitions)}, label: { // value passed into function here
Text("Start Drill")
.frame(minWidth: 0, maxWidth: 200)
.padding()
.foregroundColor(.white)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
.background(Color.blue.cornerRadius(10))
)
})
}
}
func choseDirection(reps: String) {
let reps = Int(reps) ?? 1 // HERE always uses default value instead of value from picker
var count = 1
while count <= reps {
// does some action
}
}
}
So how can I change this to read the value, so it can be used in the function for the correct amount of repetitions
Inside the Picker, you're looping over 0...100. The ForEach automatically assigns a tag to each Int. The picker then uses the tag to keep track of the selection inside repetitions.
However, because repetitions is a String, it doesn't match with each selection (0...100)'s type (Int). As a result, repetitions never gets set. To fix, also make repetitions an Int.
struct ContentView: View {
#State var repetitions = 0 /// Int, not String
let directions: [String] = ["Left", "Right"]
var body: some View {
VStack {
Picker("Values", selection: $repetitions) {
ForEach((0...100), id: \.self) {
Text("\($0)")
}
}
Button(action: {
choseDirection(reps: repetitions) // value passed into function here
}) {
Text("Start Drill")
.frame(minWidth: 0, maxWidth: 200)
.padding()
.foregroundColor(.white)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
.background(Color.blue.cornerRadius(10))
)
}
}
}
func choseDirection(reps: Int) {
print("reps: \(reps)")
var count = 1
/// comment out for now, otherwise it's an infinite loop
// while count <= reps {
// does some action
// }
}
}
Related
I was wondering how you would approach making the following application work:
You have an array where you take a random element.
You make it multiply with a random number.
You have to input the answer and then whether or not you were right you get a score point.
Trying to make it work I stumbled upon the following problems:
When actually running the program every time the view was updated from typing something into the TextField, it caused the optional to keep getting unwrapped and therefor kept updating the random number which is not the idea behind the program.
How do I check whether or not the final result is correct? I can't use "myNum2" since it only exists inside the closure
Here's my code:
struct Calculate: View {
#State var answerVar = ""
#State var myNum = Int.random(in: 0...12)
let numArray = [2,3,4,5] // this array will be dynamically filled in real implementation
var body: some View {
VStack {
List {
Text("Calculate")
.font(.largeTitle)
HStack(alignment: .center) {
if let myNum2 = numArray.randomElement() {
Text("\(myNum) * \(myNum2) = ") // how can i make it so it doesn't reload every time i type an answer?
}
TextField("Answer", text: $answerVar)
.multilineTextAlignment(.trailing)
}
HStack {
if Int(answerVar) == myNum * myNum2 { //how can i reuse the randomly picked element from array without unwrapping again?
Text("Correct")
} else {
Text("Wrong")
}
Spacer()
Button(action: {
answerVar = ""
myNum = Int.random(in: 0...12)
}, label: {
Text("Submit")
.foregroundColor(.white)
.shadow(radius: 1)
.frame(width: 70, height: 40)
.background(.blue)
.cornerRadius(15)
.bold()
})
}
}
}
}
}
Move your logic to the button action and to a function to setupNewProblem(). Have the logic code change #State vars to represent the state of your problem. Use .onAppear() to set up the first problem. Have the button change between Submit and Next to control the submittal of an answer and to start a new problem.
struct Calculate: View {
#State var myNum = 0
#State var myNum2 = 0
#State var answerStr = ""
#State var answer = 0
#State var displayingProblem = false
#State var result = ""
let numArray = [2,3,4,5] // this array will be dynamically filled in real implementation
var body: some View {
VStack {
List {
Text("Calculate")
.font(.largeTitle)
HStack(alignment: .center) {
Text("\(myNum) * \(myNum2) = ")
TextField("Answer", text: $answerStr)
.multilineTextAlignment(.trailing)
}
HStack {
Text(result)
Spacer()
Button(action: {
if displayingProblem {
if let answer = Int(answerStr) {
if answer == myNum * myNum2 {
result = "Correct"
} else {
result = "Wrong"
}
displayingProblem.toggle()
}
else {
result = "Please input an integer"
}
}
else {
setupNewProblem()
}
}, label: {
Text(displayingProblem ? "Submit" : "Next")
.foregroundColor(.white)
.shadow(radius: 1)
.frame(width: 70, height: 40)
.background(displayingProblem ? .green : .blue)
.cornerRadius(15)
.bold()
})
}
}
}
.onAppear {
setupNewProblem()
}
}
func setupNewProblem() {
myNum = Int.random(in: 0...12)
myNum2 = numArray.randomElement()!
result = ""
answerStr = ""
displayingProblem = true
}
}
I have this code inside which I'm calling "makeView" function that returns a View, and in the makeView function I'm incrementing the variable "id" and passing it to the View, but when I do this it shows this error
"Updating a preview from SongsList_Previews in SongBook.app (2935) took more than 5 seconds."
And surprisingly when I comment out the line "self.id += 1" everything works.
I'm new to SwiftUI, so forgive me if I left out some information.
struct SongsList: View {
#State private var id: Int = 0
var body: some View {
VStack{
NavigationView {
List(songs) {
song in NavigationLink(
destination: SongMainView(song: song)) {
makeView(song: song)
}
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
.background(Color.orange.opacity(0.2))
.edgesIgnoringSafeArea(.all)
}
func makeView(song: Song) -> SongsIndexView {
self.id += 1;
return SongsIndexView(song: song, id: self.id)
}
}
The variable is marked with #State property wrapper.
#State private var id: Int = 0
Every time this value changes, SwiftUI tries to update the view.
func makeView(song: Song) -> SongsIndexView {
self.id += 1;
return SongsIndexView(song: song, id: self.id)
}
This causes an infinite loop like -
id = 1, updateView (but now id = 2 because of id += 1)
id = 2, updateView (but now id = 3 because of id += 1)
You should never mutate the state while view is being updated.
Right, I'm new to swift(ui) and have been experimenting - my current issue is to do with a basic card app.
I have generated a deck of card objects (in this case 10 items) and first displayed them all in a stack using a ForEach loop - no problem there.
I then went on to pull out one card from the array to display on it's own nd this is where everything comes to a crashing halt. I suspect it's something simple and obvious but how do I get a single card from my deck for display?
struct Card: Identifiable, Equatable{
let id = UUID()
var factor1: Int = 0
var factor2: Int = 0
var product: Int = 0
var possibleAnswers: [Int] = []
}
struct CardView: View{
let card: Card
var body: some View{
VStack {
VStack { // Actual Card
Spacer()
Text("\(card.factor1) X \(card.factor2)")
.font(.title)
.foregroundColor(.black)
// .bold()
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
.padding()
.frame(width: 300, height: 200)
.background(
Rectangle()
.foregroundColor(cardColor(of: card))
)
HStack {
Spacer()
ForEach(card.possibleAnswers, id:\.self) { answer in
Text("\(answer)")
.foregroundColor(.black)
.frame(width: 75, height: 50)
.background(
Rectangle()
.foregroundColor(cardColor(of: card))
)
Spacer()
}
}
.padding()
}
}
func cardColor(of card: Card) -> Color {
return Color(red:228/255, green: 229/255, blue: 229/255)
}
}
class Deck: ObservableObject {
#Published var numberCards = 10
#Published var cards = [Card]()
func buildDeck() {
cards.removeAll()
let cardCount = 1...self.numberCards
for _ in cardCount {
// Set up new card
var myCard = Card()
myCard.factor1 = .random(in: 1...12)
myCard.factor2 = .random(in: 1...12)
myCard.product = myCard.factor1 * myCard.factor2
myCard.possibleAnswers.append(myCard.product)
for _ in 0...2{
myCard.possibleAnswers.append(Int.random(in: 1..<145))
}
myCard.possibleAnswers.shuffle()
// add card to the deck
self.cards.append(myCard)
}
}
var count: Int {
return cards.count
}
}
struct ContentView: View {
#StateObject var deck = Deck()
var body: some View {
VStack {
Button(action: {
deck.buildDeck()
}) { // button text
Text("Click to add card. Current Number of cards in deck: \(deck.count)")
}
Spacer()
ZStack {
// ForEach(deck.cards) { card in
if let card = deck.cards[0] {
CardView(card: card)
}
// }
}
Spacer()
}
}
}
From the code provided, it looks like this error was caused by accessing an index that an array doesn't even have in the first place.
In this case, you're accessing the first element of cards while in buildDeck function you removed all the elements which may be the cause of the error while it's:
let card = deck.cards[0]
CardView(card: card)
You can try to use if let to make sure the element is present before accessing it.
if let card = deck.cards.first {
CardView(card: card)
}
I have three buttons, each button corresponds to a value in the data array. When user click a button, the corresponding value will be displayed in a sheet window. Here's my code:
struct TestView: View {
#State var data: [Int] = [100, 200, 300]
#State var presented: Bool = false
#State var index4edit: Int = 1
var body: some View {
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit-1])")
})
}
}
My problem is, the first time I click a button, no matter which button, it's always display the first data 100. The reason is that the data index4edit was not updated on the first click, it is always 1, but why? Thanks.
That's because sheet view is created on View initalization. When you change your State, your view won't be recreated because your State variable is not used in your view.
To solve it, just use your State variable somewhere in the code like this dummy example
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index) + (index4edit == 0 ? "" : "")) } //<<here is the State used
.frame(width: 30, height: 30)
.padding()
}
}
Now you view will rerender when the State changes, hence your sheet view will be rerendered.
You should set up index4edit to be the real (0-based) index, not index + 1. You should also use data.indices in the ForEach instead of manually inputting the range.
struct TestView: View {
#State private var data: [Int] = [100, 200, 300]
#State private var presented: Bool = false
#State private var index4edit: Int = 0
var body: some View {
HStack {
ForEach(data.indices) { index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit])")
})
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I tried to make multiple rows of 3 columns buttons in a VStack. It worked in this post, after I rewrote the solution to make the buttons to appear in a row of 3 columns, it didnt work anymore - when I click the 'Delete Button', the additional trash image will not appear on each button. Anything goes wrong here?
class SomeData: ObservableObject{
#Published var buttonObjects: [ButtonObject] = [ButtonObject(name: "tag1", isSelected: false),
ButtonObject(name: "tag2", isSelected: false), ButtonObject(name: "tag3", isSelected: false), ButtonObject(name: "tag4", isSelected: false)]
}
struct someData3: View {
#Environment(\.editMode) var mode
#ObservedObject var someData = SomeData()
#State var newButtonTitle = ""
#State var isEdit = false
var body: some View {
NavigationView{
// List{ // VStack
VStack{
VStack(alignment: .leading){
ForEach(0..<someData.buttonObjects.count/3+1) { row in // create number of rows
HStack{
ForEach(0..<3) { column in // create 3 columns
self.makeView(row: row, column: column)
}
}
}
}
HStack{
TextField("Enter new button name", text: $newButtonTitle){
let newObject = ButtonObject(name: self.newButtonTitle, isSelected: false)
self.someData.buttonObjects.append(newObject)
self.newButtonTitle = ""
}
}
Spacer()
HStack{
Text("isEdit is ")
Text(String(self.isEdit))
}
}
.navigationBarItems(leading: Button(action: {self.isEdit.toggle()}){Text("Delete Button")},
trailing: EditButton())
}
}
func makeView(row: Int, column: Int) -> some View{
let ind = row * 3 + column
return Group{
if ind<self.someData.buttonObjects.count {
Button(action: {
self.someData.buttonObjects[ind].isSelected = !self.someData.buttonObjects[ind].isSelected
print("Button pressed! buttonKeyName is: \(self.someData.buttonObjects[ind].name) Index is \(ind)")
print("bool is \(self.someData.buttonObjects[ind].isSelected)")
}) {
Text(self.someData.buttonObjects[ind].name)
}
.buttonStyle(GradientBackgroundStyle(isTapped: self.someData.buttonObjects[ind].isSelected))
.overlay(Group {
if self.isEdit {
ZStack {
Button(action: {self.deleteItem(ind: ind)}) {
Image(systemName: "trash")
.foregroundColor(.red).font(.title)
}.padding(.trailing, 40)
.alignmentGuide(.top) { $0[.bottom] }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) //topTrailing
}
}
)
.padding(.bottom, 20)
}
else{
EmptyView()
}
}
}
func deleteItem(ind: Int) {
let name = someData.buttonObjects[ind].name
print(" deleting ind \(ind), key: \(name)")
self.someData.buttonObjects.remove(at: ind)
}
}
You can add an explicit .id at the end of the ForEach - since you're using a static ForEach you may need to force refresh it.
In your previous post you used a dynamic ForEach (with the id parameter). That's why it worked.
ForEach(0 ..< someData.buttonObjects.count / 3 + 1) { row in // create number of rows
HStack {
ForEach(0 ..< 3) { column in // create 3 columns
self.makeView(row: row, column: column)
}
}
}
.id(UUID()) // <- add this line