Pushing A New WatchKit Controller Immediately After Popping Another Always Fails - ios

OK. It's easy enough to do this in classic iOS, but WatchKit doesn't give any blocks/closures, and there isn't a choice between with/without animation.
I have a root controller that has a list of options. Touching one of the options (on either the watch or the phone) will close any currently open controller (popToRootController), then immediately, push a new one.
More or less, like so:
self.popToRootController()
self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)
The problem is that there isn't enough time between the calls, and there's no lambda for me to execute a semaphore or push the controller.
If I step through with the debugger, it happens, no problem.
If I just hit "run," it no work.
This is what is known as a "heisenbug".
I guess I could do a one-shot timer, but that seems to be such a hideous hack that it may actually cause a disruption of The Force.
Any better ideas? What am I missing?
I know there's a TON of answers for iOS. They don't do me a whole lot of good, here.

Well, I succumbed to The Dark Side, and did the timer hack. It works. I need to give it around 0.4 seconds per open controller.
Here's an approximation of what I did:
self.popToRootController()
let _ = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.timerCallback(_:)), userInfo: nil, repeats: false)
func timerCallback(_ timer: Timer) {
if let timerIndex = timer.userInfo as? Int {
if 0 <= timerIndex {
DispatchQueue.main.async {self.pushController(withName: "IKANHAZCHEEZEBURGR", context: nil)}
}
}
}
UPDATE: I do want to mention that, even though this "solves" my issue, the issue that this issue is even an issue is an issue. My design was bad, and I am redesigning the basic navigation. I'll be using a page-based approach, instead of this hierarchical design.
On general principle, if I need to hack to make it work, I'm usually better off doing it a different way.

Related

iOS/Swift - Lifecycle methods causing network request collisions

In my application, I have a home screen. Anytime this screen is loaded, it needs to make a network request (currently using Alamofire 5.2) to fetch the latest data needed to display. I am running into this issue that I believe has to do with my implementation of view lifecycles, but I am not sure how to get around it to achieve my desired effect.
Scenario 1
override func viewDidLoad() {
NotificationCenter.default.addObserver(
self,
selector: #selector(wokeUp),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
#objc func wokeUp() {
pageLoaded()
}
func pageLoaded(){
// network requests made here
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}
Here I am registering the observer within viewDidLoad. The reason I need this is many of our users will not close the application. We've found on more than a few occasions that they will let the phone sleep while the application is open, so we need to make this request when the phone is woken up and immediately back at this screen. the didBecomeActiveNotification seems to be what takes care of that.
Scenario 2
override func viewDidAppear(_ animated: Bool) {
pageLoaded() // same network request as example above
}
We also need to call this request within viewDidAppear, as there are quite a few flows where the user is brought back to this home page from another view in the application, and it's important that the request is made here as well (what the user does in the other flows has an impact of what shows up here, so we have to make sure it's updated).
The problem is that what I am finding is these two scenario will occasionally clash - our server essentially gets the same request twice, which is not ideal and causing issues. I've noticed the majority (if not all) of the problems occur when opening the application when it's no longer in memory (viewDidLoad gets called); the case of bringing the app from the background to foreground while it's still in memory is working as expected, but I have no idea what other implementation I could take to cover all of my bases here. Any insight would be appreciated.
Why not just add a simple boolean flag to your networking logic to make sure only 1 request gets fired. e.g.
class SomeViewController {
private var isFetching = false
...
func pageLoaded() {
guard isFetching == false else {
return
}
isFetching = true
// do some networking
// ....
// inside the callback / error cases
isFetching = false
}
}
depending on how big your app is, if you have many requests and/or the same request being fired on many screens. Move all your networking to another class and have this logic inside the network service rather than the viewController

Timer is not starting after explicitly telling it to [duplicate]

This question already has answers here:
Why would a `scheduledTimer` fire properly when setup outside a block, but not within a block?
(3 answers)
Swift Timer.scheduledTimer() doesn't work
(2 answers)
Closed 3 years ago.
This is slowly beginning to drive me insane because it doesn't make sense...
I'm using Swift and can't seem to get this timer to start no matter what I do.
I have an NSObject that's handling all my time-related things in my app, and in that NSObject I have a function that initializes a timer. It looks like this:
class Time: NSObject {
static let sharedInstance = Time()
private override init(){}
var roundTimer = Timer()
//This is called from my splash screen while the app is loading
func initializeTimers(){
//Initialize the round countdown timer
roundTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(updateRoundTimerCountdown)), userInfo: nil, repeats: true)
print("The round timer should have started")
}
#objc func updateRoundTimerCountdown(){print("Round Timer ran"); Time.roundTimerIsAt -= 1; print("Round Timer: \(Time.roundTimerIsAt)")}
}
From my splash screen where the app loads all the user data, I call Time.sharedInstance.initializeTimers() in order to start it. I get to the point of it printing "The round timer should have started" to the debugger (which happens after the line that should start the timer) but the selector isn't hit at all. The "Round Timer ran" isn't printed to the debugger nor does the timer appear to have started, so what am I doing wrong?
I appreciate any help, even if the answer is glaringly obvious lol :P I've spent too much time on this!
I actually finally got it working! I'm sorry...there would have been too much code if I posted my entire splash screen view controller and Time NSData, but it appears the issue was elsewhere. But maybe someone else may run into this so this may help.
On my splash screen, I perform 15 'app-loading steps' that include several steps that gather information from servers and such... those steps I progress through by using completion handlers to ensure that the data is actually collected.
It's after collecting some of this data that I call the Time.sharedInstance.initializeTimers() function. My call to the servers runs on a different thread other than the main, so apparently by calling this function after the completion handler runs, it's still on the other thread and these timers can't start on anything other than the main thread!?
So all I did was this:
DispatchQueue.main.async {Time.sharedInstance.initializeTimers()}
And it works now... (facepalm). Doesn't make a whole lot of sense to me but... it works :P

Sending app to background and re-launching it from recents in XCTest

I was looking for a solution to my problem where in I need to send my app to background and re-launch it from the recents after a particular time interval.
deactivateAppForDuration() was used to achieve this in Instruments UIAutomation.
Does anybody know how to achieve that in XCTest?
Not positive if this will work, as I haven't tested it yet, but it's worth a shot. If nothing else it should give you a good idea on where to look.
XCUIApplication class provides methods to both terminate and launch your app programmatically: https://developer.apple.com/reference/xctest/xcuiapplication
XCUIDevice class allows you to simulate a button press on the device: https://developer.apple.com/reference/xctest/xcuidevicebutton
You can use these along with UIControl and NSURLSessionTask to suspend your application.
An example of this process using Swift 3 might look something like this (syntax may be slightly different for Swift 2 and below):
func myXCTest() {
UIControl().sendAction(#selector(NSURLSessionTask.suspend), to: UIApplication.shared(), for: nil)
Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(launchApp), userInfo: nil, repeats: false)
}
func launchApp() {
XCUIApplication().launch()
}
Another way may be simply executing a home button press, and then relaunching the app after a timer passes:
func myXCTest {
XCUIDevice().press(XCUIDeviceButton.Home)
Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(launchApp), userInfo: nil, repeats: false)
}
Neither of these ways may do what you're asking, or work perfectly, but hopefully, it will give you a starting point. You can play with it and find a solution that works for you. Good luck!
If you can use Xcode 8.3 and iOS 10.3 with your current tests, then you might want to try this:
XCUIDevice.shared().press(XCUIDeviceButton.home)
sleep(60)
XCUIDevice.shared().siriService.activate(voiceRecognitionText: "Open {appName}")
Be sure to include #available(iOS 10.3, *) at the top of your test suite file.
This will be relatively equivalent to deactivateAppForDuration(). Just change the sleep() to your desired duration.

Timing accuracy with swift using GCD dispatch_after

I'm trying to create a metronome for iOS in Swift. I'm using a GCD dispatch queue to time an AVAudioPlayer. The variable machineDelay is being used to time the player, but its running slower than the time I'm asking of it.
For example, if I ask for a delay of 1sec, it plays at 1.2sec. 0.749sec plays at about 0.92sec, and 0.5sec plays at about 0.652sec. I could try to compensate by adjusting for this discrepancy but I feel like there's something I'm missing here.
If there's a better way to do this altogether, please give suggestions. This is my first personal project so I welcome ideas.
Here are the various functions that should apply to this question:
func milliseconds(beats: Int) -> Double {
let ms = (60 / Double(beats))
return ms
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
if self.playState == false {
return
}
playerPlay(playerTick, delay: NSTimeInterval(milliseconds(bpm)))
}
func playerPlay(player: AVAudioPlayer, delay: NSTimeInterval) {
let machineDelay: Int64 = Int64((delay - player.duration) * Double(NSEC_PER_SEC))
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, machineDelay),dispatch_get_main_queue(), { () -> Void in
player.play()
})
}
I have never really done anything with sound on iOS but I can tell you why you are getting those inconsistent timings.
What happens when you use dispatch_after() is that some timer is set somewhere in the OS and at some point soon after it expires, it puts your block on the queue. "at some point after" is going to be short, but depending on what the OS is doing, it will almost certainly not be close to zero.
The main queue is serviced by the main thread using the run loop. This means your task to play the sound is competing for use of the CPU with all the UI functionality. This means that the chance of it playing the sound immediately is quite low.
Finally, the completion handler will fire at some short time after the sound finishes playing but not necessarily straight away.
All of these little delays add up to the latency you are seeing. Unfortunately, depending on what the device is doing, that latency can vary. This is never going to work for something that needs precise timings.
There are, I think, a couple of ways to achieve what you want. However, audio programming is beyond my area of expertise. You probably want to start by looking at Core Audio. My five minutes of research suggests either Audio Queue Services or OpenAL, but those five minutes are literally everything I know about sound on iOS.
dispatch_after is not intended for sample accurate callbacks.
If you are writing audio applications there is no way to escape, you need to implement some CoreAudio code in one way or another.
It will "pull" specific counts of samples. Do the math (figuratively ;)

Is there a bug with simulator when we trying to make a timer with timeInterval < 0.1 second?

I'm making a new stopwatch Application with Watchkit, first my application is very simple like this:
first of all i tried to make a playButtonPressed to start a timer:
#IBAction func playButtonPressed() {
println("playButton pressed")
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("updateTimer"), userInfo: nil, repeats: true)
startTime = NSDate()
}
with my updateTimer function like this:
func updateTimer() {
duration = NSDate().timeIntervalSinceDate(startTime)
println("updateTimer: \(dateStringFromTimeInterval(duration))")
timeLabel.setText(dateStringFromTimeInterval(duration))
}
the dateStringFromTimeInterval function can help me make a dateString with duration is TimeInterval variable.
every thing is ok with my output on Debug area, i can see the dateString at printOut. But my label is lagging for setting the timeLabel as you can see here:
I don't know why, can any one can help me fix that or may be it is a bug of apple watchkit right now? i don't know it'll be lag on real device or not.
Thanks
You have several good questions in here. Unfortunately I have nothing but bad news for you. I have been working extensively with WKInterfaceTimers over the past couple of weeks and they have severe limitations and bugs associated with them. I have a couple responses broken down here in detail.
Issue 1 - Using a WKInterfaceDate as a Timer
This is going to be really frowned upon by Apple and I wouldn't doubt this would be possible grounds for rejection. As #mattt mentions, you don't want to use an NSTimer to flip the date value. Each time you try to switch the date label, Apple lumps all those changes together and pushes them from the Extension on the iPhone to the Watch over WiFi or Bluetooth. It does this to try to optimize battery life.
Because of this, you will never be able to accurately display the time on the Watch in the way that you are currently doing. The proper way to do this is to use a WKInterfaceTimer.
Issue 2 - Using a WKInterfaceTimer
While WKInterfaceTimers are built to do exactly what you want, they have some limitations. The major one for your use case is that it only does up to second precision, not millisecond. Secondly, the timers are extremely inaccurate. I've seen them anywhere from 50ms to 950ms off. I've followed this radar to bring the issue to Apple's attention.
In summary, your current implementation is not going to be super accurate and will be frowned upon by Apple, and the WKInterfaceTimer is extremely inaccurate and can only perform second precision.
Sorry for the downer answer. :-(

Resources