Unwrapped optional keeps refreshing / Working with optionals, arrays and randomElement() - swift

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
}
}

Related

SwiftUI: Why do I get the same pattern of random items in an array?

I'm working on my project and there's a section where you can test yourself on Japanese letters. You tap a button that plays a sound of a letter then you choose the right button out of three with the correct letter. there are 10 questions in total and it should always randomize, the letters but whenever you come back to the view, the first question is always the same. after the first one it randomizes the rest of the questions but the first question always has the same pattern of letters. What I want is getting a random pattern of letters for the first question every time you come back to the view. I'd appreciate any help.
here's the code of QuestionView:
import SwiftUI
struct HiraganaQuiz: View {
var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json")
#State private var correctAnswer = Int.random(in: 0...45)
#StateObject var soundplayer = Audio()
#State private var answer = ""
#State private var counter = 0
#State private var correctAnswerCounter = 0
#State private var showingAlert = false
#State private var alertMessage = ""
#State private var disabled = false
var body: some View {
ZStack {
Color.darkBackground
.ignoresSafeArea()
VStack {
Text("\(counter) / 10")
.padding(.top,40)
.foregroundColor(.white)
.font(.system(size:30).bold())
Text("Tap the speaker and choose the right letter")
Button {
soundplayer.playSounds(file: hiraganas[correctAnswer].voice1)
} label: {
Image(systemName: "speaker.wave.3.fill")
}
.font(.system(size:70))
height: 110)
HStack {
ForEach (0...2, id: \.self) { index in
Button {
letterTapped(index)
} label: {
Text(hiraganas[index].letter)
}
}
.disabled(disabe())
.foregroundColor(.white)
.font(.system(size: 35).bold())
Text("\(answer)")
.foregroundColor(.white)
.padding(.bottom,20)
.font(.system(size: 30))
Button {
resetTheGame()
} label: {
Text("Next")
}.buttonStyle(.plain)
.font(.system(size: 30).bold())
.frame(width: 200, height: 50)
}
}
.alert("⭐️ Well done ⭐️", isPresented: $showingAlert) {
Button("Retry", action: reset)
} message: {
Text(alertMessage)
}
} .onAppear { hiraganas = Bundle.main.decode("Hiragana.json").shuffled() }
}
I'm surprised this even compiles -- it should be giving you an error about trying to initialize state like this.
Instead, you can shuffle in the properties in the property initializer:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json").shuffled()
Update, to show the request for onAppear usage:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = []
var body: some View {
ZStack {
// body content
}.onAppear {
hiraganas = Bundle.main.decode("Hiragana.json").shuffled()
}
}
}

My TextField resets to "" on first pass through view but acts normal thereafter

I have created a simple swiftui search View that I use to dynamically search documents in firestore. Essentially, the user begins typing the name he wants to search for and once he get to 3 characters of the name, I execute a search against firebase for every character typed thereafter.
Here is my issue.
Initially, the user types his first 3 characters and the following occurs:
immediately the TextField he is typing in is reset to "".
the search does execute perfectly as i can tell from 'print()' statements, however, no search results are presented to the user.
So when the user begins typing his search value again, the results appear immediately.
After this odd sequence, all is well. The TextField shows the search string and resulting values perfectly.
Can you help me understand why the textfield goes blank after the first 3 characters are entered?
import SwiftUI
import Firebase
import FirebaseFirestore
import FirebaseAuth
struct PlayGroupSearchCoursesView: View {
#StateObject var playGroupViewModel = PlayGroupViewModel()
#StateObject var courseListViewModel = CourseListViewModel()
#State var masterPlayerList = [Player]()
#State var searchText = ""
#State var sval = ""
#State var isSearching = false
#State var activeSearchAttribute: Int = 0
#State var presentCourseAddedAlertSheet = false
#State var selectedCourseIdx: Int = 0
var body: some View {
NavigationView {
ScrollView {
HStack {
HStack {
TextField("Search", text: $searchText)
.textCase(.lowercase)
.autocapitalization(.none)
.padding(.leading, 24)
.textFieldStyle(.roundedBorder)
.onChange(of: searchText) { newValue in
print("search text changed to <\(newValue)>")
// we will only return 7 findings and not until they enter at least 3 chars
if newValue.count > 2 {
Task {
do {
try await self.courseListViewModel.reloadSearchCourses(searchText: newValue, nameOrCity: activeSearchAttribute)
} catch {
print(error)
}
}
}
}
}
.padding()
.background(Color(.systemGray5))
.cornerRadius(8)
.padding(.horizontal)
.onTapGesture{
isSearching = true
}
// overlay (could have used zstack) the icons into the search bar itself
.overlay() {
HStack{
Image(systemName: "magnifyingglass")
Spacer()
if isSearching{
Button(action: {
searchText = ""
isSearching = false
}, label: {
Image(systemName: "xmark.circle.fill")
.padding(.vertical)
})
}
}.padding(.horizontal, 32)
.foregroundColor(.gray)
}
//
if isSearching{
Button(action: {
isSearching = false
searchText = ""
}, label: {
Text("Cancel")
.padding(.trailing)
.padding(.leading, 0)
})
.transition(.move(edge: .trailing))
}
}
// these are the search attributes - allow the user to choose the search criteria
HStack{
// add buttons to change the search attribute (course, fullname, nickname)
Button(action: {
print ("set search attribute to fullname and here is searchtext: \(searchText)")
activeSearchAttribute = 0
searchText = ""
selectedCourseIdx = 0
}) {
Text("Name")
.padding(.all, 8)
.background((activeSearchAttribute == 0 ? .blue: .gray))
.foregroundColor(.white)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 15, trailing: 0))
}
.cornerRadius(6)
.disabled(false)
Button(action: {
print ("set search attribute to city")
activeSearchAttribute = 1
searchText = ""
selectedCourseIdx = 0
}) {
Text("City")
.padding(.all, 8)
.background(activeSearchAttribute == 1 ? .blue: .gray)
.foregroundColor(.white)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 15, trailing: 0))
}
.cornerRadius(6)
.padding(.leading, 15)
.disabled(false)
Spacer()
}
Here is my dynamic search function...
func reloadSearchCourses(searchText: String, nameOrCity: Int ) async throws {
var searchField: String = String()
if nameOrCity == 0 {
searchField = "name"
}
if nameOrCity == 1 {
searchField = "city"
}
self.searchCourses.removeAll()
let snapshot = try! await Firestore.firestore()
.collection("courses")
.whereField(searchField,isGreaterThanOrEqualTo: searchText)
.whereField(searchField,isLessThanOrEqualTo: searchText + "z")
.order(by: searchField) // alpha order - ascending is default
.limit(to: 7)
.getDocuments()
snapshot.documents.forEach { docsnap in
let docdata = try! docsnap.data(as: Course.self)
self.searchCourses.append(docdata!)
}
}

Turn Button into Picker

I have this code, where there are three buttons which have values stored in each of them.
When I press the Add button, the values add up and appear on the list. However, I am not able to check which values are being selected.
How can I make the button look like a picker, so that when the user clicks the button A, B, or C, it will show with a checkmark that it has been selected and when the user taps the Add button, the value of the selected button and "gp" should show up on the list? Also, the checkmark should disappear once the Add button is selected so that the user can select another list.
Such as:
If A and B are selected, the list should look like:
A = 2.0, B = 5.0, gp = 7.0.
If A and C are selected, the list should look like:
A= 2.0, C = 7.0, gp = 9.0.
I have tried using Picker and other methods, however, I couldn't get through. I have found this as the best solution. However, I am not able to put a checkmark on the buttons and not able to show the selected values on the list.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var a = 2.0
#State var b = 5.0
#State var c = 7.0
//var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
#State var name = String()
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(gp)", amount: Double(gp)))
self.gp = 0.0
}
func stepcount() {
count += 1
}
var body: some View {
HStack {
Button(action: { gp += a }) {
Text("A =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += b }) {
Text("B =") .frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
Button(action: { gp += c }) {
Text("C =").frame(width: 70, height: 15)
.foregroundColor(.yellow)
}
.background(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.black)
}
HStack(spacing: 15) {
Button(
String(format: ""),
action: {
print("pay for the shift is ")
gp += loadpay
}
)
Button(
action: {
addNewToDo()
stepcount()
},
label: { Text("Add")}
)
}
Form {
ForEach(self.taskStore.tasks) { task in
Text(task.toDoItem)
}
}
}
}
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
The issue is you are trying to turn a Button into something it is not. You can create your own view that responds to a tap and keeps its state so it knows whether it is currently selected or not. An example is this:
struct MultiPickerView: View {
#State private var selectA = false
#State private var selectB = false
#State private var selectC = false
let A = 2.0
let B = 5.0
let C = 7.0
var gp: Double {
(selectA ? A : 0) + (selectB ? B : 0) + (selectC ? C : 0)
}
var body: some View {
VStack {
HStack {
SelectionButton(title: "A", selection: $selectA)
SelectionButton(title: "B", selection: $selectB)
SelectionButton(title: "C", selection: $selectC)
}
.foregroundColor(.blue)
Text(gp.description)
.padding()
}
}
}
struct SelectionButton: View {
let title: String
#Binding var selection: Bool
var body: some View {
HStack {
Image(systemName: selection ? "checkmark.circle" : "circle")
Text(title)
}
.padding()
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(Color.blue, lineWidth: 4)
)
.padding()
.onTapGesture {
selection.toggle()
}
}
}
You could make it more adaptable by using an Array that has a struct that keeps the value and selected state, and run it though a ForEach, but this is the basics of the logic.

SwiftUI - using variable count within ForEach

I have a view created through a ForEach loop which needs to take a variable count within the ForEach itself i.e. I need the app to react to a dynamic count and change the UI accoridngly.
Here is the view I am trying to modify:
struct AnimatedTabSelector: View {
let buttonDimensions: CGFloat
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
HStack {
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
ForEach(1..<tabBarViewModel.activeFormIndex + 1) { _ in
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.green)
}
Circle().frame(
width: buttonDimensions,
height: buttonDimensions)
.foregroundColor(
tabBarViewModel.activeForm.loginFormViewModel.colorScheme
)
ForEach(1..<tabBarViewModel.loginForms.count - tabBarViewModel.activeFormIndex) { _ in
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.red)
Spacer().frame(maxWidth: buttonDimensions).frame(height: 20)
.background(Color.blue)
}
Spacer().frame(maxWidth: .infinity).frame(height: 20)
.background(Color.gray)
}
}
}
And the viewModel I am observing:
class TabBarViewModel: ObservableObject, TabBarCompatible {
var loginForms: [LoginForm]
#Published var activeForm: LoginForm
#Published var activeFormIndex = 0
private var cancellables = Set<AnyCancellable>()
init(loginForms: [LoginForm]) {
self.loginForms = loginForms
self.activeForm = loginForms[0] /// First form is always active to begin
setUpPublisher()
}
func setUpPublisher() {
for i in 0..<loginForms.count {
loginForms[i].loginFormViewModel.$isActive.sink { isActive in
if isActive {
self.activeForm = self.loginForms[i]
self.activeFormIndex = i
}
}
.store(in: &cancellables)
}
}
}
And finally the loginFormViewModel:
class LoginFormViewModel: ObservableObject {
#Published var isActive: Bool
let name: String
let icon: Image
let colorScheme: Color
init(isActive: Bool = false, name: String, icon: Image, colorScheme: Color) {
self.isActive = isActive
self.name = name
self.icon = icon
self.colorScheme = colorScheme
}
}
Basically, a button on the login form itself sets its viewModel's isActive property to true. We listen for this in TabBarViewModel and set the activeFormIndex accordingly. This index is then used in the ForEach loop. Essentially, depending on the index selected, I need to generate more or less spacers in the AnimatedTabSelector view.
However, whilst the activeIndex variable is being correctly updated, the ForEach does not seem to react.
Update:
The AnimatedTabSelector is declared as part of this overall view:
struct TabIconsView: View {
struct Constants {
static let buttonDimensions: CGFloat = 50
static let buttonIconSize: CGFloat = 25
static let activeButtonColot = Color.white
static let disabledButtonColor = Color.init(white: 0.8)
struct Animation {
static let stiffness: CGFloat = 330
static let damping: CGFloat = 22
static let velocity: CGFloat = 7
}
}
#ObservedObject var tabBarViewModel: TabBarViewModel
var body: some View {
ZStack {
AnimatedTabSelector(
buttonDimensions: Constants.buttonDimensions,
tabBarViewModel: tabBarViewModel)
HStack {
Spacer()
ForEach(tabBarViewModel.loginForms) { loginForm in
Button(action: {
loginForm.loginFormViewModel.isActive = true
}) {
loginForm.loginFormViewModel.icon
.font(.system(size: Constants.buttonIconSize))
.foregroundColor(
tabBarViewModel.activeForm.id == loginForm.id ? Constants.activeButtonColot : Constants.disabledButtonColor
)
}
.frame(width: Constants.buttonDimensions, height: Constants.buttonDimensions)
Spacer()
}
}
}
.animation(Animation.interpolatingSpring(
stiffness: Constants.Animation.stiffness,
damping: Constants.Animation.damping,
initialVelocity: Constants.Animation.velocity)
)
}
}
UPDATE:
I tried another way by adding another published to the AnimatedTabSelector itself to check that values are indeed being updated accordingly. So at the end of the HStack in this view I added:
.onAppear {
tabBarViewModel.$activeFormIndex.sink { index in
self.preCircleSpacers = index + 1
self.postCircleSpacers = tabBarViewModel.loginForms.count - index
}
.store(in: &cancellables)
}
And of course I added the following variables to this view:
#State var preCircleSpacers = 1
#State var postCircleSpacers = 6
#State var cancellables = Set<AnyCancellable>()
Then in the ForEach loops I changed to:
ForEach(1..<preCircleSpacers)
and
ForEach(1..<postCircleSpacers)
respectively.
I added a break point in the new publisher declaration and it is indeed being updated with the expected figures. But the view is still failing to reflect the change in values
OK so I seem to have found a solution - what I am presuming is that ForEach containing a range does not update dynamically in the same way that ForEach containing an array of objects does.
So rather than:
ForEach(0..<tabBarViewModel.loginForms.count)
etc.
I changed it to:
ForEach(tabBarViewModel.loginForms.prefix(tabBarViewModel.activeFormIndex))
This way I still iterate the required number of times but the ForEach updates dynamically with the correct number of items in the array

Storing value from a picker and passing it into a function

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
// }
}
}