How to create an instruction prompt after pressing a button? - swiftui

I'm creating an instruction dialog that will start after pressing a button. I want the instruction to be displayed in the same view and just get replaced after a few seconds to display the next instruction.
The instructions are stored in a text array.
Any help will be greatly appreciated as I'm having trouble finding a solution
#State var showPrompt = false
#State var textToUpdate = ""
let instructionTo = ["hi", "bye"]
VStack {
Text(textToUpdate)
.frame(width: UIScreen.main.bounds.width - 80, height: 350)
Button(action: {
if(self.showPrompt == false)
{
self.showPrompt = true
}
else{
self.showPrompt = false
}
}) {
if(self.showPrompt == false)
{
Text("Start")
}
else
{
Text("Stop")
// some for loop that would update the text
// and have a delay of 5 seconds between each
// index in the array
}
}
}

you can add another State variable for the Text value.
here is a sample code, assuming that you want to start changing the text after tapping on the start button after 5 seconds. I just added an empty string to show an empty text at first.
struct TestView: View {
#State var showPrompt = false
#State var textIndex = 0
let instructionTo = ["", "hi", "bye"]
var body: some View {
VStack {
Text(instructionTo[textIndex])
.frame(width: UIScreen.main.bounds.width - 80, height: 350)
Button(action: {
if(self.showPrompt == false)
{
self.showPrompt = true
// update text value
self.updateText()
}
else{
self.showPrompt = false
}
}) {
if(self.showPrompt == false)
{
Text("Start")
}
else
{
Text("Stop")
}
}
}
}
func updateText() {
if self.textIndex < self.instructionTo.count - 1 {
// update text after 5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.textIndex += 1
self.updateText()
}
}
}
}

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.

SwiftUI - Why doesn't my .alert window change my state variable?

The incomprehensible thing is that if I set breakpoints when debugging, I get my desired result. But now to the problem:
I have a view, which on appearance should check if errors have appeared in previous calculations. If so, then the SelectionView() should be called when the error is confirmed. However, this does not happen. The alert window remains rigid, and you cannot continue using the app.
The function ErrorCheck() returns whether there is no error: 0 ; a warning: 1 ; or an error where you should jump back to the menu: 2.
So if there should be an error, then after confirming the Alert window, the window should close itself and you should be sent to the SelectionView.
If I set a breakpoint at the line where the state variable should be changed, then the alert window closes, and you are sent to the SelectionView.
import SwiftUI
struct NearfieldCalibrationView: View {
#StateObject var ErrorviewModel = ErrorViewModel()
var Speech = SpeechModel()
#State var showAlert = false
#State private var Menu: Int? = 0
#State var isActive = false
#ViewBuilder func getView() -> some View {
switch Menu {
case 1:
SelectionView().navigationBarTitle("").navigationBarHidden(true)
case 2:
WMLoadingMeasurementView(Flag: 5)
case 3:
WMLoadingMeasurementView(Flag: 7)
default:
Text("")
}
}
var body: some View {
ZStack {
BackgroundView()
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
VStack {
Spacer().frame(height: 20)
Text("Hello-StackOverFlow").font(Font.custom("Baro Plain", size: 20)).foregroundColor(.white)
Spacer().frame(height: 20)
Group {
NavigationLink(isActive: $isActive, destination: getView) {
Button(action: {
if SpeakerSetup.count != 2 {
self.Menu = 2
self.isActive = true
} else {
self.Menu = 3
self.isActive = true
}
}) {
Text("Start")
}.buttonStyle(OutlineButton())
.simultaneousGesture(TapGesture().onEnded { Speech.Speech(text: "", StopSpeak: true) })
}
Spacer().frame(height: 20)
}
}
}
.onAppear(perform: {
if ErrorviewModel.ErrorCheck() > 0 { showAlert = true }
else {
Speech.Speech(text:"Hello-StackOverflow",StopSpeak: false)
}
})
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
.alert(isPresented: $showAlert) { () -> Alert in
var button: Alert.Button
if ErrorviewModel.ErrorCheck() == 2 { button = Alert.Button.default(Text("zurück zum Menü")) {
self.Menu = 1; self.isActive = true }}
else { button = Alert.Button.default(Text("Ok")) {}}
return Alert(title: Text("Error"), message: Text(ErrorviewModel.ErrorList()), dismissButton: button)
}
}
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

Form Currency Input

I'm looking for help inputting currency via a form. I would like to have:
Allow entry of a single decimal / comma currency separator
Limit entry to 2 digits past the decimal
Allow value editing
Limit maximum digits entered
There is a similar question asked 6 years ago at Limiting user input to a valid decimal number in Swift, but all the answers appear to be using viewDidLoad. I don't think that is feasible from within a form entry. If it is feasible, please show me how. Thanks
Form {
...
// Enter entry amount
Section {
TextField("Enter Amount > " + curr, text: $moneyS)
.keyboardType(.decimalPad)
}
// select entry save or cancel
Section {
Button(action: {
self.showingAlert.toggle()
...
Following the idea to achieve this, not covering all your points but I think is enough to procede by yourself:
class Test2Model: ObservableObject {
#Published var currLimit: Double = 4.0
#Published var digitLimit: Int = 2
func getCurr(str: String) -> String{
guard let currInserted = Double(str)
else {
return String(currLimit)
}
if currInserted <= currLimit {
return String(currLimit)
}
return str
}
}
struct Test2View: View {
#ObservedObject private var test2Model = Test2Model()
#State private var moneyS: String = ""
var body: some View {
Form {
// Enter entry amount
Section {
TextField("Enter Amount > " + String(test2Model.currLimit), text: $moneyS)
.keyboardType(.decimalPad)
.onChange(of: moneyS, perform: { value in
guard let decimals = moneyS.components(separatedBy:".").last else {return}
if decimals.count > test2Model.digitLimit {
moneyS.removeLast()
}
})
}
// select entry save or cancel
Section {
Button(action: {
moneyS = test2Model.getCurr(str: moneyS)
}, label: {
Text("Check")
})
}
}
}
}
struct Test2View_Previews: PreviewProvider {
static var previews: some View {
Test2View()
}
}
you could try something like this:
struct ContentView: View {
let maxDigits = 6
let maxDecimals = 2
let allowedCharacters = CharacterSet.decimalDigits.union(CharacterSet(charactersIn: NumberFormatter().decimalSeparator))
#State var money: Double?
#State var moneyText = ""
var body: some View {
VStack (spacing: 30) {
Spacer()
TextField("enter a number", text: $moneyText)
.padding(.horizontal, 20)
.keyboardType(.decimalPad)
.onChange(of: moneyText) {
// to prevent pasting non-valid text
let txt = $0.filter { ".0123456789".contains($0) }
if allowed(txt) {
money = Double(txt)
moneyText = txt
} else {
moneyText = String(txt.dropLast())
}
}
Text("Money value is: \(money ?? 0)")
Spacer()
}
}
func allowed(_ txt: String) -> Bool {
let str = txt.trimmingCharacters(in: .whitespacesAndNewlines)
switch str.components(separatedBy: ".").count - 1 {
case 0:
if str.count > maxDigits {
return false
}
return allowedCharacters.isSuperset(of: CharacterSet(charactersIn: str))
case 1:
if str.starts(with: ".") && (str.count - 1) > maxDecimals {
return false
}
let parts = str.split(separator: ".")
if parts.count == 2 && (parts[1].count > maxDecimals) || (str.count - 1) > maxDigits {
return false
}
return allowedCharacters.isSuperset(of: CharacterSet(charactersIn: str))
default:
return false
}
}
}

How to display an image for one second? SWIFTUI

I want to show an image for one second when the player achieve the goal. I thought about putting an alert but it will slow down the game. I just want the image to stay for one second at the top of the screen and disappear until the next achievement.
Example code is below:
var TapNumber = 0
func ScoreUp() {
TapNumber += 1
if TapNumber == 100 {
showImage()
}
}
func showImage() {
// this is the function I want to create but. I do not know how
show image("YouEarnedAPointImage") for one second
}
Here is a demo of possible approach
struct DemoShowImage1Sec: View {
#State private var showingImage = false
var body: some View {
ZStack {
VStack {
Text("Main Content")
Button("Simulate") { self.showImage() }
}
if showingImage {
Image(systemName: "gift.fill")
.resizable()
.frame(width: 100, height: 100)
.background(Color.yellow)
}
}
}
private func showImage() {
self.showingImage = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showingImage = false
}
}
}