iOS 10 - NSKeyValueObservation crash on deinit - swift

I'm using NSKeyValueObservation to observe properties in a subclass ofWKWebView.
It works well on iOS 11, but crashes on deinit on iOS 10.
Printed Error Log on Console
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x15209e600 of class Rakuemon.WebView was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x170232da0> (
<NSKeyValueObservance 0x170259bf0: Observer: 0x17027d500, Key path: loading, Options: <New: NO, Old: NO, Prior: NO> Context: 0x0, Property: 0x170643ba0>
<NSKeyValueObservance 0x170643480: Observer: 0x170c72f80, Key path: estimatedProgress, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x170643330>
<NSKeyValueObservance 0x170642c70: Observer: 0x17086c0c0, Key path: title, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x1706437b0>
)'
Codes
class WebView: WKWebView {
// MARK: - Properties
weak var delegate: WebViewDelegate?
// MARK: - Private properties
private var contentSizeObserver: NSKeyValueObservation?
private var loadingObserver: NSKeyValueObservation?
private var estimatedProgressObserver: NSKeyValueObservation?
private var titleObserver: NSKeyValueObservation?
// MARK: - Life cycle
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
setupObserver()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Private functions
private extension WebView {
func setupObserver() {
contentSizeObserver = scrollView.observe(\.contentSize, options: [.old, .new], changeHandler: { [unowned self] _, change in
guard let oldSize = change.oldValue, let newSize = change.newValue, oldSize != newSize else { return }
self.delegate?.webView?(self, didChangeSizeFrom: oldSize, to: newSize)
})
loadingObserver = observe(\.isLoading, changeHandler: { [unowned self] _, _ in
self.delegate?.webViewIsLoading?(self)
})
estimatedProgressObserver = observe(\.estimatedProgress, options: [.new], changeHandler: { [unowned self] _, change in
guard let newValue = change.newValue else { return }
self.delegate?.webView?(self, didChangeEstimatedProgress: newValue)
})
titleObserver = observe(\.title, options: [.new], changeHandler: { [unowned self] _, change in
guard let title = change.newValue else { return }
self.delegate?.webView?(self, didChangeTitle: title ?? "")
})
}
}
Question
I also found the contentSizeObserver, which observed scrollView.contentSize, not properties of self, didn't caused crash.
So, what is the proper way to observe self's properties on iOS 10 through NSKeyValueObservation? or unregister it?

Updated on 2019/10/16
This seems to still happen on iOS 10.3 with Xcode 11 and Swift 5.1, I created a sample project to test it by using code from SR-5752.
The easiest way I figure out so far is like so:
// Environment: Xcode 11.1, Swift 5.1, iOS 10.3
deinit {
if #available(iOS 11.0, *) {} else if let observer = observer {
removeObserver(observer, forKeyPath: "foo")
}
}
Can notice that I only call removeObserver(_:forKeyPath) on iOS 10 and lower because as Stacy Smith mentioned, this crash on iOS 13 (can be reproduced easily in the sample project).
I also tried Bryan Rodríguez's suggestion, but without luck. Maybe I missed something? 🤔
// Environment: Xcode 11.1, Swift 5.1, iOS 10.3
deinit {
// Also tried only either one, no luck
self.observer?.invalidate()
self.observer = nil
}

Looks like apple bug – https://bugs.swift.org/browse/SR-5816
This is happening because NSKeyValueObservation holds a weak reference to an object. That weak reference turns to nil too soon.
In some cases workaround can be found by using lifecycle methods (viewDidDissapear and such).
In other cases you should use old obj-c api (addObserver / removeObserver / observeValue) to support iOS 10.

This worked for me:
deinit {
titleObserver = nil
}

Related

Swift "Reference to property ... closure requires explicit use of 'self' to make capture semantics explicit"

I'm coming from Python and JavaScript background, and I'm struggling to understand why I'm getting this error and how to fix it. "Reference to property 'eggMessage' in closure requires explicit use of 'self' to make capture semantics explicit". I'm just trying to update the text of the eggMessage label with the current countdown. (Swift5, xcode 13)
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var eggMessage: UILabel!
var softTime = 5
var mediumTime = 7
var hardTime = 12
func updateMessage(text: String) {
eggMessage.text = text
}
func createTimer(timeLeft: Int) {
var countdown = timeLeft
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {
[weak self] timer in
DispatchQueue.main.async { [weak self] in
if countdown > 0 {
print(countdown, "<--- TIME LEFT")
eggMessage.text = String(countdown) // <---- ERROR!!
updateMessage(text: String(countdown)) // <---- ERROR!!
countdown -= 1
} else {
timer.invalidate()
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
timer.fire()
})
}
#IBAction func eggSelection(_ sender: UIButton) {
let hardness: String = sender.currentTitle!
switch hardness {
case "Soft":
print(softTime)
createTimer(timeLeft: softTime)
case "Medium":
print(mediumTime)
case "Hard":
print(hardTime)
default:
print("pass")
}
}
}
The error is due to the fact that your closure's lifetime is not tied to the lifetime of your ViewController. You need to tell the compiler the relationship you desire for the two lifetimes. In general, use [weak self] and self?.foo; this tells the compiler that you want the ViewController to be able to destruct at will (like when the app terminates), and you want the closure not to crash your app if it runs after the ViewController has destructed.
To get a solid grasp of the issue, read about retain cycles in the Swift Book.

Observe a NSManagedObject in Swift 4 can cause crashes when modified in another thread?

What I have:
a NSManagedObject that sets a dynamic property to true when it's deleted from CoreData
override func prepareForDeletion() {
super.prepareForDeletion()
hasBeenDeleted = true
}
And within a view, I observe this NSManagedObject with the new Observe pattern of Swift 4
// I added this to observe the OBSERVED deletion to avoid a crash similar to:
// "User was deallocated while key value observers were still registered with it."
private var userDeletionObserver: NSKeyValueObservation?
private func observeUserDeletion() {
userDeletionObserver = user?.observe(\.hasBeenDeleted, changeHandler: { [weak self] (currentUser, _) in
if currentUser.hasBeenDeleted {
self?.removeUserObservers()
}
})
}
private func removeUserObservers() {
userDeletionObserver = nil
userObserver = nil
}
private var userObserver: NSKeyValueObservation?
private var user: CurrentUser? {
willSet {
// I remove all observers in willSet to also cover the case where we try to set user=nil, I think it's safer this way.
removeUserObservers()
}
didSet {
guard let user = user else { return }
// I start observing the NSManagedObject for Deletion
observeUserDeletion()
// I finally start observing the object property
userObserver = user.observe(\.settings, changeHandler: { [weak self] (currentUser, _) in
guard !currentUser.hasBeenDeleted else { return }
self?.updateUI()
})
}
}
So now, here come one observation and the question:
Observation: Even if I don't do the observeUserDeletion thing, the app seems to work and seems to be stable so maybe it's not necessary but as I had another crash related to the observe() pattern I try to be over careful.
Question details: Do I really need to care about the OBSERVED object becoming nil at any time while being observed or is the new Swift 4 observe pattern automatically removes the observers when the OBSERVED object is 'nilled'?

Xcode "po" command retains value

I've been debugging my code and found that my manager was deinitialised (that was cause of my bug - not calling delegate methods).
What's strange, that during debugging process I've used "po" command after setting the manager's delegate (weak) and it prevented it from being deinitialised (delegate methods were called).
Why is that? Is it proper behaviour?
Xcode 8.3, swift 3.1
EDIT:
//a tap starts everything :)
#IBAction func shareButtonPressed(_ sender: Any) {
let requestManager = FacebookPostRouteRequest() //bug fixed by changing to instance variable
requestManager.delegate = self
requestManager.showShareBadgeDialog(self.badge!, onViewController: self)
}
//in FacebookPostRouteRequest
final weak var delegate: FacebookPostRouteRequestDelegate?
func showShareBadgeDialog(_ badge: Badge, onViewController viewController: UIViewController) {
let dialog = self.initDialog(onViewController: viewController)
guard let imageURL = badge.imageURL else {
self.delegate?.facebookPostRouteRequest(self, didCompleteWithResult: false)
return
}
dialog.shareContent = self.generateImageShareContent(imageURL)
self.show(dialog)
}
private func show(_ dialog: FBSDKShareDialog) {
OperationQueue.main.addOperation {
dialog.delegate = self //when printed out dialog.delegate delegate methods were called! Deinit of FacebookPostRouteRequest is not called.
let showResult = dialog.show()
...
}
}
extension FacebookPostRouteRequest: FBSDKSharingDelegate {
func sharer(_ sharer: FBSDKSharing!, didCompleteWithResults results: [AnyHashable : Any]!) {
...
}
//other delegate methods implemented as well
}
Your problem is here:
#IBAction func shareButtonPressed(_ sender: Any) {
let requestManager = FacebookPostRouteRequest()
requestManager.delegate = self
requestManager.showShareBadgeDialog(self.badge!, onViewController: self)
}
After the last line, the requestManager object will be disposed because it's no longer referenced and will not call any of the delegate methods.
Make requestManager an instance variable:
let requestManager = FacebookPostRouteRequest()
#IBAction func shareButtonPressed(_ sender: Any) {
requestManager.delegate = self
requestManager.showShareBadgeDialog(self.badge!, onViewController: self)
}
Your issues with the debugger are probably race conditions for stopping the main thread.

KVO not working (swift 2.2)?

In my app, OneVC is one of child ViewControllers of PageViewController, TwoVC is the embed view controller of OneVC's Container View.
When the user drag the scroll view in OneVC, I want the drag action can be not just update the content in OneVC from web API, but notify TwoVC to update too.
Both OneVC and TwoVC will appear at interface at the same time when launch.
I'm following Apple's "Using Swift with Cocoa and Objective-C" "Key-Value Observing" instruction to imply KVO, but no notification is sent when the observed property changes. Please see below my code:
OneVC is the object to be observed.
class OneVC: UIViewController, UIScrollViewDelegate {
dynamic var isDragToUpdate = false
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y < -150 {
if isDragToUpdate {
isDragToUpdate = false
} else {
isDragToUpdate = true
}
print(isDragToUpdate)
}
}
}
TwoVC is the observer
class TwoVC: UIViewController {
let oneVC = OneVC()
override viewDidLoad() {
oneVC.addObserver(self, forKeyPath: "isDragToUpdate", options: [], context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("hoh")
guard keyPath == "isDragToUpdate" else {return}
print("hah")
}
deinit {
oneVC.removeObserver(self, forKeyPath: "isDragToUpdate")
}
}
I checked row by row, and find many other stackoverflow answers, but still no idea what's going wrong on my code, when drag and release the scrollview, none of "hoh" and "hah" are print in console, except print(isDragToUpdate) is printed properly.
Thank you in advance!

WKWebView Help Support

I am able to implement the new WebKit in 7.1 Deployment. I can use it without error on the devices running in iOS8 up. However, when the device falls below iOS8, my WKWebView becomes nil even after the initialization, my suspect was even if you silence webkit and successfully add it on your project and the deployment was 7.1, if the OS actually fall below iOS8 this WebKit becomes unvalable.
I want to confirm this error so I can proceed. Since this webkit was introduced as of the release of swift and iOS8. Thanks
Here is a simple example, where I create a new protocol and extend both UIWebView and WKWebView from the same protocol. With this, it makes a easy to keep track of both these views inside my view controller and both of these use common method to load from url, it makes easy for abstraction.
protocol MyWebView{
func loadRequestFromUrl(url: NSURL!)
}
extension UIWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
extension WKWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
// This is a simple closure, which takes the compared system version, the comparison test success block and failure block
let SYSTEM_VERSION_GREATER_THAN_OR_EQUAL: (String, () -> (), () -> ()) -> Void = {
(var passedVersion: String, onTestPass: () -> (), onTestFail: () -> ()) in
let device = UIDevice.currentDevice()
let version = device.systemVersion
let comparisonOptions = version.compare(passedVersion, options: NSStringCompareOptions.NumericSearch, range: Range(start: version.startIndex, end: version.endIndex), locale: nil)
if comparisonOptions == NSComparisonResult.OrderedAscending || comparisonOptions == NSComparisonResult.OrderedSame{
onTestPass()
}else{
onTestFail()
}
}
class ViewController: UIViewController{
var webView: MyWebView!
override func viewDidLoad() {
super.viewDidLoad()
SYSTEM_VERSION_GREATER_THAN_OR_EQUAL("8.0",
{
let theWebView = WKWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
},
{
let theWebView = UIWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
})
webView.loadRequestFromUrl(NSURL(string: "http://google.com"))
}
}