SwiftUI "Game over.Restart?" message pop-up - swiftui

I am new to SwiftUI and I am stuck here.
I have a "choose right flag game". Currently it can show you 3 flags and you have to choose the one that represents country named above. It keeps score of your game and number of rounds you've played( max is 20 ).
When you hit the 20 game sets all numbers to 0 and you start again.
I want to make it pop-up alert message with "Game over. Your result is (bad, average, good ( based on the amount of scores))!" and button "Restart".
I've made alert massage for every round, but simply "copy-paste" with some changes doesn't work.
How can I do it?
import SwiftUI
struct ContentView: View {
#State private var countries = [
"afghanistan",
"albania",.......//here goes list of countries
"zimbabwe"
].shuffled()
#State private var correctAnaswer = Int.random(in: 0...2)
#State private var score = 0
#State private var showingAlert = false
#State private var endGameAlert = false
#State private var alertTitle = ""
#State private var currentRound = 0
#State private var maxRound = 20
var body: some View {
NavigationView {
VStack{
ForEach((0...2), id:\.self) { number in
Image(self.countries[number])
.border(Color.black, width: 1)
.onTapGesture {
self.flagTapped(number)
}
}
Text("Your Score \(score), current round is \(currentRound) of \(maxRound) ")
Spacer()
}
.background(Image("background").resizable().scaledToFill().edgesIgnoringSafeArea(.all).blur(radius: 20))
.navigationBarTitle(Text(countries[correctAnaswer].uppercased()))
.alert(isPresented: $showingAlert) {
Alert(title: Text(alertTitle),
message:Text("Your score is \(score)"),
dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}
}
}
func flagTapped(_ tag: Int){
if currentRound <= 20 {
if tag == correctAnaswer {
score += 1
currentRound += 1
alertTitle = "Correct"
} else {
score -= 1
currentRound += 1
alertTitle = "Wrong"
}
showingAlert = true
}
else {
endGameAlert = true
score = 0
currentRound = 0
}
}
func askQuestion() {
countries.shuffle()
correctAnaswer = Int.random(in: 0...2)
}
}

First change your flagTapped(_) into something like this:
func flagTapped(_ tag: Int){
if tag == correctAnaswer {
score += 1
alertTitle = "Correct"
} else {
score -= 1
alertTitle = "Wrong"
}
endGameAlert = currentRound == maxRound
// score = 0 // reset this at the "Restart" call
// currentRound = 0 // reset this at the "Restart" call
if currentRound < maxRound {
currentRound += 1
}
showingAlert = true
}
Then you can check for endGameAlert in the .alert:
.alert(isPresented: $showingAlert) {
if endGameAlert {
return Alert(title: Text("Game over"),
message:Text("Your result is <>!"),
dismissButton: .default(Text("Restart")){
self.resetGame() // reset game here
})
} else {
return Alert(title: Text(alertTitle),
message:Text("Your score is \(score)"),
dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}
}

Related

SwiftUI: Custom binding that get value from #StateObject property is set back to old value after StateObject property change

I'm trying to implement a passcode view in iOS. I was following the guide here.
I'm trying to improve it a bit so it allows me to create a passcode by enter same passcode twice. I added a "state" property to the #StateObject and want to clear entered passcode after user input the passcode first time.
Here is my current code:
LockScreenModel.swift
====================
import Foundation
class LockScreenModel: ObservableObject {
#Published var pin: String = ""
#Published var showPin = false
#Published var isDisabled = false
#Published var state = LockScreenState.normal
}
enum LockScreenState: String, CaseIterable {
case new
case verify
case normal
case remove
}
====================
LockScreen.swift
====================
import SwiftUI
struct LockScreen: View {
#StateObject var lockScreenModel = LockScreenModel()
let initialState: LockScreenState
var handler: (String, LockScreenState, (Bool) -> Void) -> Void
var body: some View {
VStack(spacing: 40) {
Text(NSLocalizedString("lock.label.\(lockScreenModel.state.rawValue)", comment: "")).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
.onAppear(perform: {lockScreenModel.state = initialState})
.onDisappear(perform: {
lockScreenModel.pin = ""
lockScreenModel.showPin = false
lockScreenModel.isDisabled = false
lockScreenModel.state = .normal
})
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<6) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 30, weight: .thin, design: .default))
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { lockScreenModel.pin }, set: { newValue in
if newValue.last?.isWholeNumber == true {
lockScreenModel.pin = newValue
}
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(lockScreenModel.isDisabled)
}
private var showPinStack: some View {
HStack {
Spacer()
if !lockScreenModel.pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
lockScreenModel.showPin.toggle()
}, label: {
lockScreenModel.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
guard !lockScreenModel.pin.isEmpty else {
lockScreenModel.showPin = false
return
}
if lockScreenModel.pin.count == 6 {
lockScreenModel.isDisabled = true
handler(lockScreenModel.pin, lockScreenModel.state) { isSuccess in
if isSuccess && lockScreenModel.state == .new {
lockScreenModel.state = .verify
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
} else if !isSuccess {
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if lockScreenModel.pin.count > 6 {
lockScreenModel.pin = String(lockScreenModel.pin.prefix(6))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= lockScreenModel.pin.count {
return "circle"
}
if lockScreenModel.showPin {
return lockScreenModel.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
====================
The problem is the line lockScreenModel.state = .verify. If I include this line, the passcode TextField won't get cleared, but if I remove this line, the passcode TextField is cleared.
If I add a breakpoint in set method of boundPin, I can see after set pin to empty and state to verify, the set method of boundPin is called with newValue of the old pin which I have no idea why. If I only set pin to empty but don't set state to verify, that set method of boundPin won't get called which confuse me even more. I can't figure out which caused this strange behavior.

Capture previous value from textfield in SwiftUI

I am creating a basic manual counter to keep track of visitors, however, I am struggling to find out how I can capture and present the previous value entered by the user alongside the current number.
When I tap on the counter (0) (aka editednumber), a box appears and the user is asked to enter a number, I want to be able to save the number entered by the user, so when the user taps the counter again to enter a new number, the screen will show the previous number entered as well as the current number entered.
The previous number will of course be overwritten, every time a new number is entered, but regardless, I would like the previous number and new number to appear.
Example:
User enters the number 10, this will show as current_guests/editednumber which is fine, but if I tap to enter a new number 12, only the last entered number (10) is showing.
I want the view to show both the old (10) (stored into the previous_editednumber variable) and current (12) number (editednumber).
My code is as following:
// testView.swift
import SwiftUI
struct testView: View {
#State var current_guests:Int = 0
#State var denied_guests:Int = 0
#State var total_guests:Int = 0
#State var editednumber:Int = 0
#State var previous_editednumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(total_guests)")
Text("current_guests: \(current_guests)")
Text("editednumber: \(editednumber)")
Text("previous_editednumber:\(previous_editednumber)")
Button("\(current_guests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
TextField("Number", value: $editednumber, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
// perform calculations based on input
if (editednumber >= total_guests) {
current_guests = editednumber
total_guests = editednumber + total_guests
}
if (editednumber < total_guests) {
current_guests = editednumber
total_guests = total_guests - current_guests
}
})
Button("Cancel", role: .cancel, action: {})
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
current_guests += 1
total_guests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
denied_guests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
current_guests -= 1
if (current_guests <= 0) {
current_guests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
You can create an in-between #State that takes its initial value from editednumber then returns the newValue when the user clicks "Ok".
struct AlertBody: View{
#State var newValue: Int
let onAccept: (Int) -> Void
let onCancel: () -> Void
init(initialValue: Int, onAccept: #escaping (Int) -> Void, onCancel: #escaping () -> Void){
self._newValue = State(initialValue: initialValue)
self.onAccept = onAccept
self.onCancel = onCancel
}
var body: some View{
TextField("Number", value: $newValue, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
onAccept(newValue)
})
Button("Cancel", role: .cancel, action: onCancel)
}
}
Then you can replace the content in the alert
struct HistoryView: View {
#State private var currentGuests:Int = 0
#State private var deniedGuests:Int = 0
#State private var totalGuests:Int = 0
#State private var editedNumber:Int = 0
#State private var previousEditedNumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(totalGuests)")
Text("current_guests: \(currentGuests)")
Text("editednumber: \(editedNumber)")
Text("previous_editednumber:\(previousEditedNumber)")
Button("\(currentGuests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
AlertBody(initialValue: editedNumber) { newValue in
//Assign the previous number
previousEditedNumber = editedNumber
//Assign the newValue
editedNumber = newValue
//Your previous logic
if (editedNumber >= totalGuests) {
currentGuests = editedNumber
totalGuests = editedNumber + totalGuests
}
if (editedNumber < totalGuests) {
currentGuests = editedNumber
totalGuests = totalGuests - currentGuests
}
} onCancel: {
print("cancelled alert")
}
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
currentGuests += 1
totalGuests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
deniedGuests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
currentGuests -= 1
if (currentGuests <= 0) {
currentGuests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}

Making data persist in Swift

I'm sorry if this is a naive question, but I need help getting this form to persist in core data. The variables are declared in the data model as strings. I simply cannot get this to cooperate with me. Also, the var wisconsin: String = "" is there because I can't call this view in my NavigationView without it throwing an error.
import SwiftUI
struct WisconsinToolOld: View {
//Variable
var wisconsin: String = ""
#Environment(\.managedObjectContext) private var viewContext
#State var saveInterval: Int = 5
var rateOptions = ["<12", ">12"]
#State var rate = ""
var body: some View {
List {
Section(header: Text("Spontaneous Respirations after 10 Minutes")) {
HStack {
Text("Respiratory Rate")
Spacer()
Picker("Rate", selection: $rate, content: {
ForEach(rateOptions, id: \.self, content: { rate in
Text(rate)
})
})
.pickerStyle(.segmented)
}
Section(header: Text("Result")) {
HStack {
Text("Raw Points")
Spacer()
Text("\(WisconsinToolInterpretation())")
}
}.navigationTitle("Wisconsin Tool")
}
}
func saveTool() {
do {
let wisconsin = Wisconsin(context: viewContext)
wisconsin.rate = rate
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
func WisconsinToolInterpretation() -> Int {
var points = 0
if rate == "<12" {
points += 3
}
else {
points += 1
}
return points
}
}

SwiftUICharts are not redrawn when given new data

I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}

How to animate switching from one Text to another with swift ui?

I have an array of strings. For example ["Car", "Boat", "Van"]
How do I anime the changing of the text in the array (which can contain more strings) so that it switches from Car to Boat to Van by blurring transition? And so that it continually loops this?
I already have an idea on how to animate, but I was stuck in switching the text.
I have asked the question on why the text does not switch over here -> Why does the size animate and not the text with this SwiftUI view?
But I thought It might be better to write a separate question on how to actually switch the text.
Here is a possible solution animating text based on an array. I have used Asperis transition idea from this solution here
struct ContentView: View {
var array = ["First", "Second", "Third"]
#State var shortString = true
#State var currentIndex : Int = 0
#State var firstString : String = ""
#State var secondString : String = ""
var body: some View {
VStack {
if shortString {
Text(firstString).font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
if !shortString {
Text(secondString).font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
}
.animation(.default)
.onAppear {
firstString = array[0]
secondString = array[1]
let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
if (shortString) {
if currentIndex == array.count - 1 {
self.secondString = array[0]
currentIndex = 0
}
else {
self.secondString = array[currentIndex+1]
currentIndex += 1
}
}
else {
if currentIndex == array.count - 1 {
self.firstString = array[0]
currentIndex = 0
}
else {
self.firstString = array[currentIndex+1]
currentIndex += 1
}
}
shortString.toggle()
}
}
}
}
I have already selected #davidev answer. But based on his answer this was what I have implemented. Cheers 🍺
struct ContentView: View {
var array = ["First", "Second", "Third"]
#State var currentIndex : Int = 0
#State var firstString : String = ""
#State var timer: Timer? = nil
#State var isBlurred = false
var body: some View {
VStack {
Text(firstString).blur(radius: isBlurred ? 6 : 0)
}.onAppear {
self.timer = newTimer
}
}
var newTimer: Timer {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { v in
let rndTime = [0.5, 0.3, 0.7, 1.0].randomElement()! // I wanted a random time up to 1 second.
v.invalidate()
currentIndex += 1
if currentIndex == array.count { currentIndex = 0 }
DispatchQueue.main.asyncAfter(deadline: .now() + rndTime) {
self.isBlurred.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.isBlurred.toggle()
firstString = array[currentIndex]
self.timer = newTimer
}
}
}
}
}