Cannot get Haptic Feedback to work with Swift - swift

I'm a CS major in college who had an idea for an Apple Watch app that vibrates every time it reaches a certain time interval, for example the user picks intervals of 20s through a picker, taps "start" and the app starts a timer from 0 to 20 and once it reaches 20 I want the watch to vibrate and start counting up to 20 again until the user chooses to stop it. I want to use it for runners to have an idea of consistent paces to run. The problem is that I can't get UIImpactFeedbackGenerator to work and none of the other methods of haptic feedback either. I have no swift experience, only python and Java but I am basically done except for this haptic part. The one error I get is UIImpactFeedbackGenerator is out of scope where the generator is declared under SecondView. Thank you in advance! Here's my code:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}

WatchOs does have the play(_:) - method for notification.
try WKInterfaceDevice.current().play(.click)
for more information take a look at the documentation -- https://developer.apple.com/documentation/watchkit/wkinterfacedevice/1628128-play

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

How to allow timer to count down in background on Watch OS

Made a pretty simple timer app, but I've found that Apple Watch documentation is no where near as good as the phone. It's been difficult to find any answers on this.
Here is my content view. The seconds for the timer is kept in '''timeVal'''
struct ContentView: View {
#State var timeVal = 0.0
#State var timerScreenShow:Bool = false
var body: some View {
VStack {
HStack{
Spacer()
Text("\(self.timeVal.hourMinute)")
Spacer()
}
//various buttons to set timer
NavigationLink(
destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: Double(Int(self.timeVal)), initialTime: Int(self.timeVal)),
isActive: $timerScreenShow,
label: {
Text("Start")
}).background(Color.green).cornerRadius(22)
}
}
}
Then, here is the bit that keeps time:
struct TimerView: View {
#Binding var timerScreenShow:Bool
#State var timeVal:Double
let initialTime:Int
var body: some View {
if timeVal > -1 {
VStack {
ZStack {
Text("\(self.timeVal.hourMinuteSecond)").font(.system(size: 20))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
//Text("\(self.timeVal.hourMinuteSecond)")
ProgressBar(progress: Int(self.timeVal), initial: self.initialTime).frame(width: 90.0, height: 90.0)
}
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Cancel")
.foregroundColor(Color.red)
})
.padding(.top)
}
} else {
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
}
}
}
What do I need to do to keep the timer running when the watch app is closed?
Don't keep track of how much time is left on the counter. Instead, store the startTime = Date(). Use the timer only as a signal to update the UI. When the timer ticks, compute the time left as startAmount - (Date().timeInterval(since: startTime)). Store startAmount and startTime and Bool isTimerRunning in persistent storage such as #AppStorage. When the app restarts, pick up where you left off.

Is there a way to get my Apple Watch App to vibrate when the screen sleeps?

I own a Apple Watch series 3 and I made an app for it that vibrates every specified amount of seconds. I know haptics drain your battery but, regardless, its the purpose of my app. I tried to test it on my watch and I encountered a problem. When the screen is on the app functions like its supposed to but as soon as I put my wrist down and the screen sleeps, the app runs in the background but it doesn't vibrate like supposed to. Is there a way around this? Like I said I have a series 3 therefore I don't have access to the always on screen display function.
My app code is below:
import SwiftUI
struct ContentView: View {
#State var timerScreenShown = false
#State var timeVal = 10
var body: some View {
VStack{
Text("Select \(timeVal)s intervals").padding()
Picker(
selection: $timeVal,
label: Text("")){
ForEach(10...120, id: \.self) {
Text("\($0)")
}
}
NavigationLink(destination: SecondView(timerScreenShown: $timerScreenShown, timeVal: timeVal), isActive: $timerScreenShown, label: {Text("Go")})
}
}
}
struct SecondView: View{
#Binding var timerScreenShown:Bool
#State var timeVal = 10
#State var startVal = 0
var body: some View {
VStack{
if timeVal > 0 {
Text("Timer")
.font(.system(size: 14))
Text("\(startVal)")
.font(.system(size: 40))
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > 0{
self.startVal += 1
if self.timeVal == self.startVal{
WKInterfaceDevice.current().play(.failure)
self.startVal = 0
}
}
}
}
Text("seconds")
.font(.system(size: 14))
Button(action: {
self.timerScreenShown = false
}) {
Text("Cancel")
.foregroundColor(.red)
}
} else {
Button(action: {
self.timerScreenShown = false
}) {
Text("Done")
.foregroundColor(.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}

Getting Values from own PickerView

I'm new to Swift and I'm currently developing my own Timer Application for practice purposes. (I do it without storyboard)
Now I have the Problem that i made a View called "TimePickerView" (Code below), where I created my own Picker. Then I use that TimePickerView in another part of my Application with other Views (in a View). In that View I want to pick my time but I don't know how i can get the Values of the Picker (The Picker works by the way)
This is my TimePickerView
import SwiftUI
struct TimePickerView: View {
#State private var selectedTimeIndexSecond = 0
#State private var selectedTimeIndexMinutes = 0
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(minutes[selectedTimeIndexMinutes]) min : \(seconds[selectedTimeIndexSecond]) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
func getValues() -> (Int, Int) {
return (self.minutes[selectedTimeIndexMinutes] ,self.seconds[selectedTimeIndexSecond])
}
}
and that is the View I want to use my Picker, but I don't know how I get those values from the Picker:
struct SetTimerView: View {
#State var timepicker = TimePickerView()
var body: some View {
NavigationView{
VStack{
//Select the time
timepicker
//Timer variables (This doesn't work)
var timerTime = timepicker.getValues()
var minutes = timerTime.0
var seconds = timerTime.1
Spacer()
let valid : Bool = isValid(timerTime: minutes+seconds)
//Confirm the time
NavigationLink(
destination:
getRightView(
validBool: valid,
timerTime: minutes*60 + seconds),
label: {
ConfirmButtonView(buttonText: "Confirm")
});
Spacer()
}
}
}
func isValid(timerTime : Int) -> Bool {
if (timerTime == 0) {
return false
} else {
return true
}
}
#ViewBuilder func getRightView(validBool : Bool, timerTime : Int) -> some View{
if (validBool == true) {
TimerView(userTime: CGFloat(timerTime), name: "David", isActive: true)
} else {
UnvalidTimeView()
}
}
}
I think main problem is misunderstanding conceptions between data and views.
At first you need a model witch will override your data (create it in separate swift file):
import Foundation
class Time: ObservableObject {
#Published var selectedTimeIndexMinutes: Int = 0
#Published var selectedTimeIndexSecond: Int = 0
}
Pay attention on ObservableObject so that swiftUI can easily detect changes to it that trigger any active views to redraw.
Next I try to change the value of the model in the view
import SwiftUI
struct TimePickerView: View {
#EnvironmentObject var timeData: Time
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $timeData.selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $timeData.selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(timeData.selectedTimeIndexMinutes) min : \(timeData.selectedTimeIndexSecond) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
.environmentObject(Time())
}
}
Like you can see I don't using #Blinding, instead of it I connecting our Model with a View
On the next view I can see changes, I created a new one because your example have view that don't indicated here...
import SwiftUI
struct ReuseDataFromPicker: View {
#EnvironmentObject var timeData: Time
var body: some View {
VStack{
Text("You selected")
Text("\(timeData.selectedTimeIndexMinutes) min and \(timeData.selectedTimeIndexSecond) sec")
}
}
}
struct ReuseDataFromPicker_Previews: PreviewProvider {
static var previews: some View {
ReuseDataFromPicker()
.environmentObject(Time())
}
}
And collect all in a Content View
struct ContentView: View {
var body: some View {
TabView {
TimePickerView()
.tabItem {Label("Set Timer", systemImage: "clock.arrow.2.circlepath")}
ReuseDataFromPicker()
.tabItem {Label("Show Timer", systemImage: "hourglass")}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Time())
}
}
Like that you can easily change or reuse your data on any other views

Embedded navigationlinks

I am trying to develop an app to do a little cognitive testing
When opening the app there is a NavigationView{} with NavigationLink{}
So far so normal
One of the links
NavigationLink(destination: IntermediateCoreView()) { Text("Go to tests") }
Takes you to a 'prescreen for the tests, from which you can link to the 'test' its self
From the IntermediateCoreView.swift you can NavigationLink(destination: ContentView()) { Text("+ new test") }
Which works, you can take the test. At the end of the test (X seconds pass) and then it displays an alert that the user has run out of time and takes them back to the Intermediate CoreView
This is where it goes wrong as the IntermediateCoreView, 'Back' button in the NavigationView goes back to the 'test' view. Not back to the InitialView
I get that this is 'expected behaviour', the test is back from the screen its been sent to. Is there a way to override this?
To add a minimal example - the .app file:
import SwiftUI
#main
struct minRep2App: App {
var body: some Scene {
WindowGroup {
initialView()
}
}
}
Then the initial view controller:
import SwiftUI
struct initialView: View {
var body: some View {
NavigationView{
List{
NavigationLink(destination: ContentView()) {
Text("Go to CoRe tests")
}
}
}
}
}
struct initialView_Previews: PreviewProvider {
static var previews: some View {
initialView()
}
}
Lastly the test demo
import SwiftUI
struct ContentView: View {
#State private var timeRemaining = 50.00
#State private var showingAlert = false
#State var showIntermediate: Bool = false
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationLink(destination: initialView(), isActive: self.$showIntermediate)
{
EmptyView()
}
Text("Test goes here")
.padding()
HStack{
// Text("Score: \(score)")
Text("Time: \(timeRemaining)")
}.padding(.bottom, 10)
.onReceive(timer) { time in
if self.timeRemaining > 0.1 {
self.timeRemaining -= 1
}
if self.timeRemaining == 0.0 {
self.showingAlert = true
self.timer.upstream.connect().cancel()
}
}
.alert(isPresented: $showingAlert){
Alert(title: Text("Warning"), message: Text("Sorry your time is up!"), dismissButton: .default(Text("OK"), action: {
self.showIntermediate = true
})
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Change your ContentView to this:
import SwiftUI
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
#State private var timeRemaining = 50.00
#State private var showingAlert = false
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
Text("Test goes here")
.padding()
HStack{
// Text("Score: \(score)")
Text("Time: \(timeRemaining)")
}.padding(.bottom, 10)
.onReceive(timer) { time in
if self.timeRemaining > 0.1 {
self.timeRemaining -= 1
}
if self.timeRemaining == 0.0 {
self.showingAlert = true
self.timer.upstream.connect().cancel()
}
}
.alert(isPresented: $showingAlert){
Alert(title: Text("Warning"), message: Text("Sorry your time is up!"), dismissButton: .default(Text("OK"), action: {
self.presentationMode.wrappedValue.dismiss()
})
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This has nothing to do with the back button which will work fine. The problem is the logic in your alert caused you to instantiate a new, wholly different, InitialView. Think of it like webpages. If you navigate from one to the next, and then click on a link that goes to the first one, you have actually gone FORWARD to the first page, and if you click the back button you will get to the second page. Same thing here.
Instead, you should use #Environment(\.presentationMode) var presentationMode which, when you call self.presentationMode.wrappedValue.dismiss() will trigger you to go back to the view that presented the view that you are trying to navigate back from.