WatchKit InterfaceController vs ViewController - ios

Are awakeWithContext, willActivate, didDeactivate the same as viewDidLoad, viewWillAppear, viewDidAppear in terms of functionality?
I am porting code from a Swift Apple Watch tutorial that was created back when people had to add their own watch AppViewController file to test their watch apps.
The included files and things have changed with the official watch release of Xcode obviously so I’m wondering where to put where.
For example there is some code in the older AppViewController file and so I just copy/pasted it into the new InterfaceController. I put code that was in viewDidLoad, viewWillAppear, viewDidAppear into awakeWithContext, willActivate, didDeactivate respectively.
It seems the methods are different. I got 1 error saying that setText doesn’t exist:
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
…and 2 errors saying view doesn’t exist:
iconLabel.frame = self.view.bounds
self.view.insertSubview(iconLabel, atIndex: 1)
It’s like WatchKit doesn’t use some of the normal property methods or something.
Error Messages:
http://i.imgur.com/wXMdt3c.png
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
self.view.insertSubview(iconLabel, atIndex: 1) // Xcode error
}
override func willActivate() {
super.willActivate()
iconLabel.frame = self.view.bounds // Xcode error
iconLabel.textAlignment = .Center
iconLabel.font = UIFont.boldSystemFontOfSize(132)
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
newBeat()
NSTimer.scheduledTimerWithTimeInterval(8,
target: self,
selector: Selector("newBeat"),
userInfo: nil,
repeats: true)
beat()
}
func newBeat() {
// 1
if ++currentBeatPatternIndex == beatPatterns.count {
currentBeatPatternIndex = 0
}
// 2
currentBeatPattern = beatPatterns[currentBeatPatternIndex]
// 3
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)" // Xcode error
iconLabel.text = currentBeatPattern.icon
}
func beat() {
// 1
UIView.animateWithDuration(currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 2
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.shrinkFactor, self.shrinkFactor)
},
completion: { _ in
// 3
UIView.animateWithDuration(self.currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 4
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.expandFactor, self.expandFactor)
},
completion: { _ in
// 5
self.beat()
}
)
}
)
}
}

You are right that awakeWithContext, willActivate, and didDeactivate are very similar to the existing UIViewController methods like viewDidLoad, viewWillAppear, and viewDidUnload. The errors you're seeing however are related to the way WatchKit currently works. In order for a watch app to run, all the code is executed on an iPhone but the Apple Watch itself assumes responsibility for the UI elements. What that means is that any views that constitute your watch app MUST be included on your watch app's storyboard. Also, as a direct result, views cannot be instantiated and added to a parent view. All UI elements must be included in your watch app's storyboard and the watch will lay them out based on their arrangement in interface builder. This means you cannot call addSubview and you cannot set an element's frame. You can only adjust its size and set its hidden property to hide or show it on the watch. As far as this method goes –
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
You are calling the method wrong. In swift the parameters are included in the parentheses. You can call it this way if it's what you mean
bpmLabel.setText("\(currentBeatPattern.bpm)")
but setText is a method that takes a string parameter and cannot be assigned with =
As far as the animations go, I think you're out of luck. Watch apps currently are more like widgets than iOS apps and things like UIView animations and frame math are not available you. You should definitely read
up on WatchKit because there's no way you'll be able to port an iOS app directly to the watch like this.

Related

How to Prevent variables to be initialized twice?

I am new to iOs app development and I wonder if my app architecture is correct. A bug in my app make me believe that some of my variables are initialized several times.
To summarize my app : 2 screens and 2 views but only one controller. Depending the current view, the viewDidLoad have different results.
I don't believe this is the right way to do. I guess the idea would be to create a controller for each view ?
But my main concern here : in my viewDidLoad, when the main screen is loaded, I set up a notification observer. I believe( because of bugs ) that this observer is setup each time the screen load and then is called multiple times.
My questions here : Where to put this listener , is there a place that will run the code only once this view is loaded ? It should be fixed by putting this listener into a variable ?
Is the AppDelegate application function a right place for that kind of things ?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Remove iphone sleep mode
UIApplication.shared.isIdleTimerDisabled = true
//Setup the external scanner object
self.scanner.delegate = self
self.scanner.connect()
// Init the saved values
let defaults = UserDefaults.standard
// --------------- MAIN VIEW ---------------
if(mainView != nil){
// Add a notification receiver
// Will receive results ### request
NotificationCenter.default.addObserver(self, selector: #selector(mainTextNewLineNotification), name: Notification.Name(rawValue: "sendingToView"), object: nil)
// Layout setup
mainTextView.layer.cornerRadius = 6.0
[...]
}
// --------------- SETTINGS VIEW ---------------
if(settingsView != nil){
//Fill the field with saved values
inputHost.text = defaults.string(forKey: "hostname")
inputPort.text = String(defaults.integer(forKey: "port"))
if(defaults.string(forKey: "timeout") != nil){
inputTimeout.text = defaults.string(forKey: "timeout")
}
if(UserDefaults().string(forKey: "confirmSwitch") == "On"){
confirmSwitch.isOn = true
} else {
confirmSwitch.isOn = false
}
}
}
You don't need to care about removeObserver logic in your case (since you use simple subscription using -selector, not -block). From Apple doc:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc(deinit) method.
Each time when you initialize instance of UIViewController and if system loads its view viewDidLoad will be called. viewDidLoad is called once during UIViewController lifecycle. So your logic for subscription is correct.
I don't know your application entire logic so it is hard what is the reason of your bug.

How to turn on iPhone's flash for 1 second?

In the settings for my app, there is a switch allowing the user to turn iPhone's flash on or off (flash is used to indicate certain points in app logic while it's running). What I want to implement is this: when the user toggles this switch on, I want it to, well, flash for a split second to indicate its 'ON' state.
Now, I know how to set torchMode on or off - this is implemented in the app itself, but I'm not sure how to correctly make it 'blink' for settings purpose. One of the ways I thought of is to use following code (toggleFlash() is a static method for toggling torchMode implemented in main code):
UIView.animate(withDuration: 1.0, animations: {
ViewController.toggleFlash(on: true)
}, completion: { (_) in
ViewController.toggleFlash(on: false)
})
This does make it 'blink', but only for a moment - not 1 second. Besides, I'm not so sure if it's at all correct to use animate for this purpose. Another idea is to use Thread.sleep, but this looks like an even worse practice.
Can someone recommend better solutions?
You could use a timer.
func flashForOneSecond() {
ViewController.toggleFlash(on: true)
flashOffTimer = Timer.scheduledTimer(timeInterval:1, target:self, selector:#selector(self.switchFlashOff), userInfo:nil, repeats:false)
}
#objc func switchFlashOff() {
ViewController.toggleFlash(on: false)
}
Probably something like this:
func flash() {
ViewController.toggleFlash(on: true)
let time = DispatchWallTime.now() + DispatchTimeInterval.seconds(1)
DispatchQueue.main.asyncAfter(wallDeadline: time) {
ViewController.toggleFlash(on: false)
}
}
wallDeadline is reliable and the solution is packed in one function.

iOS Timer function heap corruption

So I am new to iOS and am using trying to have a view controller with a timer that periodically updates the UI. The issue that I am seeing is that I am getting heap corruption, more specifically EXC_BAD_ACCESS KERN_INVALID_ADDRESS error that is caused by objc_retain call.
This error is happening in several places but all within my Timer function and higher on the call stack __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
is being called in each case.
I must be missing a reference or not releasing something properly, here is the code
func scheduleTimer() {
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerFunc), userInfo: nil, repeats: true)
}
func timerFunc() {
if let gps = sdlService?.getLatestLocation() {
let clCoor = CLLocationCoordinate2D(locStruct: gps)
self.updateLatestDriverIcon(gps: gps, coor: clCoor)
if isRecording {
self.addNextPathPoint(coor: clCoor)
}
latestCoor = clCoor
}
}
func updateLatestDriverIcon(gps: LocationStruct, coor: CLLocationCoordinate2D) {
if latestCoor == nil {
car = MarkerAnnotation(coordinate: coor, title: carMarker)
mapView.addAnnotation(car!)
latestCoor = coor
mapView.centerOnLatestGPS(animated: false)
markerView.rotation = MathUtils.wrap(gps.bearing, min: 0, max: 360)
} else if coor.isDifferent(to: latestCoor!) {
if isMapFollowingCar {
mapView.centerOnLatestGPS(animated: false)
}
car!.coordinate = coor
markerView.rotation = MathUtils.wrap(gps.bearing, min: 0, max: 360)
}
}
Now this timer function is referencing properties of my view controller, as well as a nested function (updateLatestDriverIcon). I have seen crashes on the mapView.centerOnLatestGPS() func, and multiple places within the markerView.rotation call stack all with the same error codes listed above.
What am I missing here?
EDIT:
Here is a stack trace from crashlytics. I am using events triggered over an external accessory so I can be attached to the debugger:
Stack Trace
So after several weeks of tracking this thing down, we found it was due to an animation on a UIView. Not exactly sure why it was throwing errors where it did, if anyone knows why that would be very helpful! Here is some more info on the architecture:
We had a screen updating a UI at about 10HZ and was driven by a timer using the above code. The animation was done on a UIView subclass that was done off of the main thread which was being rendered into a bitmap context. This was being done at ~30Hz.
The animation code:
UIView.animate(
withDuration: self.animationDuration,
animations: { self.currentGearValue = actualGearValue },
completion: { (isComplete) in /* not sure we need this yet */ })
I haven't tested it but it might be because the animation is overlapped if the previous one isn't finished by the time the next animation gets started.

ReplayKit RPBroadcastActivityViewController iPad

How do I present the RPBroadcastActivityViewController on iPads.
I am using the standard code to start a recording
RPBroadcastActivityViewController.load { [unowned self] (broadcastActivityViewController, error) in
// If an error has occurred, display an alert to the user.
if let error = error {
self.showAlert(message: error.localizedDescription)
return
}
// Present vc
if let broadcastActivityViewController = broadcastActivityViewController {
broadcastActivityViewController.delegate = self
// present
self.present(...
}
}
Works on iPhones but on iPads nothing is presented and the app kind of freezes. I have been checking out games on the app store that use this feature and I noticed the same problem.
E.g on the game Tower Dash nothing is presented when pressing the live stream button on iPads, it only works on iPhones.
I have been trying to play around with popover presentations but nothing seems to work.
Am I missing something?
UPDATE: This seems to be a bug. Even in apples own Swift Playground app this happens.
UPDATE2: Apple has actually responded to my bug report and told me that I need to present the View Controller on iPads as a popover like so
UIDevice.current.userInterfaceIdiom == .pad {
broadcastAVC.popoverPresentationController?.sourceView = view
broadcastAVC.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
broadcastAVC.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0) // no arrow
}
However it still doesnt work for me. As I mentioned this happens on apples own Swift Playground app so it must be a bug.
Fixed:
I forgot to add this line in the code mentioned above
broadcastAVC.modalPresentationStyle = .popover
You are correct that Apple's demo app does not include this little detail, but it isn't a bug. This is what I use to get it to work on an iPad. iPads require a popover to present the view and a popover needs an anchor. I chose to anchor it to the leftBarButtonItem.
if let unwrappedPreview = preview {
unwrappedPreview.previewControllerDelegate = self
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.phone {
self.present(unwrappedPreview, animated: true, completion: nil)
}
else {
unwrappedPreview.popoverPresentationController?.barButtonItem = self.navigationItem.leftBarButtonItem!
unwrappedPreview.modalPresentationStyle = UIModalPresentationStyle.popover
unwrappedPreview.preferredContentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height)
self.present(unwrappedPreview, animated: true, completion: nil)
}
}
iOS 10.1 beta 2 still have the same problem.
For now, I found the only way to present a RPBroadcastActivityViewController on iPad is to present it on a trait collection's horizontal compact environment.
So you may need to tell your user switch to Split View mode before select a broadcast-supported app, then switch back to full screen. After back to full screen, you can use RPBroadcastController.startBroadcast(handler:) to start broadcast.

Why does Example app using Photos framework use stopCachingImagesForAllAssets after each change?

So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.

Resources