EXC_BAD_ACCESS when access != nil static var - ios

Context: The app needs to be connected to a bluetooth device 100% of the time. While using the app, the CBPeripheral object is stored in a static variable for use throughout the user's session in app.
//This is in AppDelegate class.
static var connectedDevice: CBPeripheral? = nil
.........
class BluetoothScanner : NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
//(...)
func connectToDevice(peripheral: CBPeripheral)
{
AppDelegate.connectedDevice = peripheral
//(...)
}
}
At the start of the app, access operations to that variable occur in a loop. The first accesses to the variable are correct. But always, before ending the loop (at about 40 access to var), it happens EXC_BAD_ACCESS
if(AppDelegate.connectedDevice != nil)
{
//Thread 3: EXC_BAD_ACCESS (code=2, address=0x16ec8ff28)
print("Device debug: " + AppDelegate.connectedDevice!.debugDescription)
}
The problem occurs when calling 'debugDescription' but the same problem occurs when calling any other function of 'connectedDevice'
AppDelegate.connectedDevice is only accessed (in that period) it is never modified, nor released.
I have tested the app for two weeks very thoroughly and without problems. But when uploading it to the AppStore it indicated that I needed to update the XCode to the latest version in order to upload compilation to the AppStore. This problem has appeared without touching the code, only updating the XCode. I have even tested (before updating XCode) that this object is not released during hours of use of the app.

Related

SwiftUI #AppStorage not always persisting value

I'm using #AppStorage with a String property. When changing the value of the property, the view automatically updates to reflect the change as expected. However, the majority of the time it hasn't persisted in UserDefaults. I'm feeling daft with the idea i've missed something here. Is anyone else seeing this?
Steps:
Launch app
Change value
Kill and re-launch app
Change value to something else
Kill and re-launch app
Environment:
Xcode 13.4 (13F17a)
iOS 15.5 - iPhone 13 Pro Simulator
Very simple example:
struct ContentView: View {
#AppStorage("token") var token: String = ""
var body: some View {
Form {
Section("Token") {
Text(token)
}
Section("Debug") {
Button("Update 01") {
token = "token-01"
}
Button("Update 02") {
token = "token-02"
}
}
}
}
}
This typical scenario can happen in development phase but not in production phase because there user doesn't have stop button like Xcode and UserDefault class write changes to disk before the app terminated by system.
So we know that user's default database is written to disk asynchronously, so may be the changes we made to default doesn't written to database when we stop the app from stop button on Xcode, you can try it own on your own by stopping app by swiping, your data will be stored when launch next time but when you stop from Xcode your data will not be saved
The answers above are correct, but I don't see the solution anywhere so I'll add it here:
Just always background your app before you terminate it from Xcode
PS: the answer is not, nor has never been, to call .synchronize on user defaults

Swift 4 KVO block crash: observed object deallocated while observer was still registered

I started developing my app recently with iOS 11 as target version, because that was the default value. I have now lowered the version to 9.3, because of reasons.
The app is pure swift 4, using the new KVO block thing.
I fixed the few compile-time errors I had with safeAreaInsets and whatnot, and the app built successfully. A quick job. Nice.
I tried running it on an iPhone 7 iOS 10.3.1 Simulator, and lord - it was a train wreck. I guess UITableViewAutomaticDimension wasn't really a thing back in the days.
Anyway, I have fixed most of the layout-issues, but now I'm stuck with a few hard crashes. Everywhere I've used this new KVO it crashes when I navigate back.
My navigation-pushed ViewController is KVO-listening to a field inside an object it holds. When I pop the navigation, the viewController and the object is deallocated, in that order, and the app crashes, giving me this error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fdf2e724250 of class MyProject.MyObject was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x60800003fd80> (
<NSKeyValueObservance 0x610000050020: Observer: 0x61000006f140, Key path: isSelected, Options: <New: NO, Old: NO, Prior: NO> Context: 0x0, Property: 0x6180000595f0>
)'
As far as I can tell, it says that the observed object MyObject was deallocated while there was someone observing the variable isSelected.
This is the oberservation-code in MyViewController that observes MyObject.
var observations:[NSKeyValueObservation] = []
func someFunction() {
observations.removeAll()
let myObject = MyObject(/*init*/)
self.myObject = myObject
observations.append(myObject.observe(\.isSelected, changeHandler: { [weak self] (object, value) in
//Do stuff
}))
}
I was under the impression that this new magic KVO-block-style would solve world peace, but apparently that only applies to iOS 11.
Now, I have tried a few things, but I can't get it to not crash. It happens every single time, and I don't understand why.
Since the crash logs tells me that the observed object is being deallocated while an object is observing it, but I also know that the observing object is deallocated before the observed object, I have tried doing this in the observer:
//In MyViewController
deinit {
observations.forEach({$0.invalidate()})
observations.removeAll()
print("Observers removed")
}
But this doesn't help. I have also done this:
//In MyObject
deinit{
print("MyObject deinit")
}
And when I do the thing - I get the following output:
>Observers removed
>MyObject deinit
>WORLD WAR 5 STACK TRACE
I have also tried
//In MyViewController
deinit{
self.myObject.removeObserver(self, forKeyPath: "isSelected")
}
But I get the output saying Can't remove because it is not registered as an observer. So I guess MyViewController isn't actually the observer that gets attached when using this new KVO.
Why didn't any of this work? Where and when do I have to remove the observers in < iOS11?
It looks like you're running into a bug that I reported about a year ago, but which unfortunately has received very little attention:
https://bugs.swift.org/browse/SR-5752
Since this bug hasn't bitten me in a while, I had hoped that it'd been fixed in the Swift overlay, but I just tried copying my code from the bug report into an iOS project and running it in the 10.3.1 simulator, and sure enough, the crash came back.
You can work around it like this:
deinit {
for eachObservation in observations {
if #available(/*whichever version of iOS fixes this*/) { /* do nothing */ } else {
self.removeObserver(eachObservation, forKeyPath: #keyPath(/*the key path*/))
}
eachObservation.invalidate()
}
observations.removeAll()
}
Make sure you only do this on iOS versions affected by the bug, because otherwise you'll remove an observation that's already been removed, and then that will probably crash. Isn't this fun?

Lifecycle of singleton instance after iOS tuned from background

I have EventManager and Reg class singleton (Obj-C):
EventManager
class EventManager : NSObject {
override init() {
super.init()
Reg.shared().id = myId
Reg.shared().delegate = self
}
}
Reg (singleton)
#implementation Reg
//...
+(Reg*) shared{
static dispatch_once_t once;
static Reg *manager;
dispatch_once(&once, ^ { manager = [[Reg alloc] init]; });
return manager;
}
//...
#end
So here is my call in Controller:
class ViewController: UIViewController {
let manager = EventManager()
override func viewDidLoad() {
super.viewDidLoad()
let a = SomeHandler.instance
}
DispatchQueue.global(qos: .default).async {
SomeHandler.instance.registerBlocks({ obj in
let m = EventManager()
}, failureBlock: { (a, b, e) in
let m = EventManager()
}, status: { (a, b, c) in
}) { value in
let m = EventManager()
}
}
I get callback from SomeHandler.instance.registerBlocks sometimes after 10-15 sec when device entered to background and turned back to foreground
My question is: what happens with Reg instance?
If application is always active, each time when I call EventManager() I should get same instance of Reg because its singleton.
But when device enters to background OS deallocs all instances so when user opens app again Reg.shared() should return different instance, is it true?
What happens if old instance of Reg.shared() did some long job like sending HTTP requests?
To further elaborate on Paulw11's comment, please refer to the documentation about an app's life cycle.
A lot of people don't really specify what they mean with "background":
They just pressed the home button so the home screen or some other app is shown on the screen
The just put the device to sleep
They did either of this some time ago
They terminated the app with Xcode or restarted the device, seeing the app in the task manager (double pressing home button), so they assume it is in a background mode
Some other common unclear usages of "background" might also exist, but I think you'll get the picture.
As Paulw11 correctly said the only time your singleton will be deallocated is when the app enters the "not running" state, i.e. it is terminated. Note that this happens in case 4 I listed, but whether the app is listed in the task manager or not is not indicative of it running or not! I'm saying this since I have met people saying "my app just went to background, but when I put it to foreground again it appears all my on-start code is executed again!"
Case 3 can also ultimately result in your app being terminated (i.e. it goes from "suspended" to "not running"), but it needn't be so (depends on device usage and so forth).
The first two cases will result in the app at first entering background mode (your singleton is still there) and then suspended mode (the app is not doing anything anymore, but the memory is still intact, so your singleton won't be re-inited later).
In the end, a (true) singleton will only ever be deallocated when the app terminates, everything else would be grossly mis-using the term (also note that here lies the danger of singletons). The OS doesn't randomly go into your app's memory allocation and takes away its stuff. The only thing it does do is sending the memory warnings to let the app decide how to save memory on its own (if it's not suspended already). Only if even that does not "reign in" the app's resource usage memory is "freed": By killing it entirely.
#Paulw11: I didn't want to steal your answer, so why don't you make one from your comment. snaggs can then accept that. :)

Call ExtensionDelegate to create/refresh data for Complication

All my data creation is done in the ExtensionDelegate.swift.
The problem is ExtensionDelegate.swift doesn't get called before the function getCurrentTimelineEntryForComplication in my ComplicationController.swift.
Any ideas? Here is my code and details:
So my array extEvnts is empty in my ComplicationController.swift:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
let extEvnts = ExtensionDelegate.evnts
}
Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array:
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
private let session = WCSession.defaultSession()
var receivedData = Array<Dictionary<String, String>>()
static var evnts = [Evnt]()
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TeamColor"] as? String, let matchValue = userInfo["Matchup"] as? String {
receivedData.append(["TeamColor" : tColorValue , "Matchup" : matchValue])
ExtensionDelegate.evnts.append(Evnt(dataDictionary: ["TeamColor" : tColorValue , "Matchup" : matchValue]))
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if WCSession.isSupported() {
session.delegate = self
session.activateSession()
}
}
}
EDIT:
Per Apple, it looks like this has something to do with it, but for some reason I have no idea how to actually implement it because I'm not able to call mydelegate.evnts:
// Get the complication data from the extension delegate.
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
var data : Dictionary = myDelegate.myComplicationData[ComplicationCurrentEntry]!
So I've tried something like this, and still can't get it working because I'm still getting no data:
func someMethod() {
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
let dict = ExtensionDelegate.evnts
print("ExtensionDel.evnts: \(dict.count)")
}
Useful question that helped me here
In the function requestedUpdateDidBegin() you can update the information that you will display in your complication. So in this method you may make a call to your parent app using a WatchConnectivity method like sendMessage:replyHandler:errorHandler: to receive new information.
You can use NSUserDefaults to store your imperative data that will be used in your ComplicationController, then load this information from NSUserDefaults for your complication. I store this data in user defaults so that I always have old data to display in case the new data fails to load.
TL/DR: Have the extension tell ClockKit to update the complication after the data is received.
First issue:
So my array extEvnts is empty in my ComplicationController.swift ... Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array
Your array is empty because the data hasn't been received at that point.
You can't (get the complication controller to) force the watch (extension) to receive data which may not have even been transmitted yet.
If you look at the WCSession Class Reference, transferUserInfo queues data to be transferred in the background, when the system decides it's a good time to send the info.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
Second issue:
You're trying to combine updating your app and your complication based on data sent from your phone. But your app and your complication don't necessarily run together. It's not surprising or unexpected that the watch updates the complication before any data has even been sent/received. The App Programming Guide for watchOS mentions that
Complications exist entirely in the WatchKit extension. Their user interface is not defined in the Watch app. Instead, it is defined by an object implementing the CLKComplicationDataSource protocol. When watchOS needs to update your complication, it launches your WatchKit extension. However, the Watch app’s executable is not launched.
There's no mechanism for the complication controller to say, "Wait, I'm not ready to provide an update. The complication controller can't wait on (or as mentioned, force) the watch extension to receive data.
It's only responsibility is to immediately return data based on what's currently available to it. If there's no data, it must return an empty timeline.
Approaching this problem:
You shouldn't necessarily think of app updates and complication updates as the same thing. The first is not budgeted, but the second is budgeted. If you update your complication too often, you may exceed your daily budget, and no further updates will occur for the remainder of the day.
Complications aren't meant to be frequently updated. Complications should provide as much data as possible during each update cycle. You shouldn't ask the system to update your complication within minutes. You should provide data to last for many hours or for an entire day.
Having covered that, you could wait until your extension has received data, then can ask ClockKit to extend your timeline, so new entries can be added to it. extendTimelineForComplication: is documented in the CLKComplicationServer Class reference.
As an aside, if your data is urgent, you should use transferCurrentComplicationUserInfo. It's a high-priority message, which is placed at the head of the queue, and the extension is woken up to receive it. See this answer for a comparison between it and transferUserInfo.
You also could setup a singleton to hold your data which the watch app and complication controller both use. This was mentioned in an answer to an old question of yours, and also recommended by an Apple employee on the developer forums.

How to detect memory warnings in iOS App Extension

I'm writing an iOS extension that extends NEPacketTunnelProvider in the NetworkExtension framework released in iOS 9. I'm running into a situation where iOS is killing the extension once hits 6MB of memory used.
In a regular iOS app, there are two ways to detect memory warnings and do something about it. Either via [UIApplicationDelegate applicationDidReceiveMemoryWarning:(UIApplication*)app] or [UIViewController didReceiveMemoryWarning]
Is there a similar way to detect memory warnings within an extension? I've searched up and down the iOS extension documentation but have come up empty thus far.
ozgur's answer does not work. UIApplicationDidReceiveMemeoryWarningNotification is a UIKit event and I haven't found a way to get access to that from an extension. The way to go is the last of these options: DISPATCH_SOURCE_TYPE_MEMORYPRESSURE.
I've used the following code (Swift) in a Broadcast Upload Extension and have confirmed with breakpoints that it is called during a memory event right before the extension conks out.
let source = DispatchSource.makeMemoryPressureSource(eventMask: .all, queue: nil)
let q = DispatchQueue.init(label: "test")
q.async {
source.setEventHandler {
let event:DispatchSource.MemoryPressureEvent = source.mask
print(event)
switch event {
case DispatchSource.MemoryPressureEvent.normal:
print("normal")
case DispatchSource.MemoryPressureEvent.warning:
print("warning")
case DispatchSource.MemoryPressureEvent.critical:
print("critical")
default:
break
}
}
source.resume()
}
I am not very familiar with the extensions API, however my basic hunch says that you can register any of your object as observers of UIApplicationDidReceiveMemoryWarningNotification from within that class:
NSNotificationCenter.defaultCenter().addObserverForName(UIApplicationDidReceiveMemoryWarningNotification,
object: nil, queue: .mainQueue()) { notification in
print("Memory warning received")
}

Resources