how can the UILabel become a CALayer and cause a crash? - swift

I've got a custom UIView of which several instances are created inside a loop:
let models = CDMyModel.MR_findAllSortedBy("position", ascending: true) as! [CDMyModel]
for view in myViews {
view.removeFromSuperview()
}
self.myViews.removeAll(keepCapacity: true)
for model in models {
let myView = MYFaqView(width: CGRectGetWidth(self.view.frame))
myView.titleLabel.text = model.title
myView.content.text = model.content
myView.titleLabel.sizeToFit()
self.scrollView.addSubview(myView)
self.myViews.append(myView)
}
I do sometimes see crashes in Crashlytics in the line with myView.content.text = model.content:
According to the crash I assume it has something to do with memory, but I really don't know how the myView could have been released at that point.
All this happens in viewWillAppear:. Could the removing before has to do something with this? But I assume everything happens on the main thread, so this shouldn't be a problem as well - I'm really stuck here.
The crash happens on iOS 9.
EDIT
MyFaqView init method:
init(width:CGFloat) {
self.width = width
super.init(frame: CGRectZero)
self.addSubview(self.titleLabel)
self.addSubview(self.toggleImageView)
self.addSubview(self.separatorView)
self.content.clipsToBounds = true
self.addSubview(self.content)
self.translatesAutoresizingMaskIntoConstraints = false
self.clipsToBounds = true
}
EDIT
let content:UILabel = {
let l = UILabel()
l.numberOfLines = 0
if let font = UIFont(name: "OpenSans", size: 14) {
l.font = font
}
return l
}()

These problems are always very tricky to track down.
Basically, what is happening is memory corruption. The address 0x14f822a0 that was previously occupied by your UILabel content has been used by something else, in this case a CALayer. You can verify this if the crash happens locally by entering po 0x14f822a0 in lldb and sure enough it will output that address to be of type CALayer.
With these errors, although the crash line can provide a clue, it is not always the cause of the error. Something has already happened elsewhere.
Although Swift is mostly memory managed, there are still pitfalls for the unwary. Personally I have seen two principal causes of memory corruption. The first is with retain cycles caused by self referencing closures. The second - more pertinent to your problem - is with views related to Storyboards and Xibs.
If we follow this through logically, we can consider that the CALayer now occupies the address space previously taken by your UILabel content. The runtime attempts to send a message to the object is thinks occupies that address, and that is caught by a Swift runtime assert which then triggers a EXC_BAD_INSTRUCTION crash.
Now, for some other object to have taken up residence at that address, the original inhabitant the UILabel content must have been released. So why would the runtime release content? Because it is no longer required, i.e. it is not a subview or property of any view that it still required.
I would bet that if you change content to be a subclass UILabel and add a deinit method that you then breakpoint, you will be surprised to see that it is being unexpectedly deinitialised early on. To test this create a type as follows:
class DebugLabel: UILabel
{
func deinit
{
NSLog("Breakpoint on this line here!")
}
}
Then change content's type to be the DebugLabel above.
So why is all this happening? My money is on you having one of your view properties that has been created programmatically as being either weak or unowned. Perhaps you had these set up previously using an IBOutlet that you then removed but forgot to remove the weak designator?
Check through each and every one carefully and I am sure you will find the cause of the problem above. Nothing that is created programatically either by using an initialiser or UINib should be designated weak or unowned.

Brief viewing shows me two potential problems:
You can break iterator here, that cause undefined behavior -
removeFromSuperview() really remove the view from the hierarchy and release it and reduce the number of elements in myViews.
for view in myViews {
view.removeFromSuperview() }
What do you do here? Seems you repeat the previous step.
self.myViews.removeAll(keepCapacity: true)

Related

Weak view reference won't get deallocated after temporarily added to view hierarchy

I ran into the weirdest thing, maybe someone has an explanation.
Steps:
Create a UIView A
Create a weak reference to A
Add A to the view hierarchy
Remove A from the view hierarchy
Set A to nil
The weak reference still exists.
If you skip step 3 and 4 the weak reference becomes nil as expected.
Code to test:
TestView to check deinit
class TestView: UIView {
deinit {
print("deinit")
}
}
Unit test
class RetainTests: XCTestCase {
func testRetainFails() {
let holderView = UIView()
var element: TestView? = TestView()
holderView.addSubview(element!)
element?.removeFromSuperview()
weak var weakElement = element
XCTAssertNotNil(weakElement)
// after next line `weakElement` should be nil
element = nil
// console will print "deinit"
XCTAssertNil(weakElement) // fails!
}
func testRetainPasses() {
var element: TestView? = TestView()
weak var weakElement = element
XCTAssertNotNil(weakElement)
// after next line `weakElement` should be nil
element = nil
// console will print "deinit"
XCTAssertNil(weakElement)
}
}
Both tests will print out deinit on the console, but in the case of the failing test the weakElement still holds the reference if element was in a view hierarchy for any time, although element got successfully deallocated. How can that be?
(Edit: This is inside an app, NOT a playground)
There's an autorelease attached to TestView when you add and remove it to the superview. If you make sure to drain the autorelease pool, then this test behaves as you expect:
autoreleasepool {
holderView.addSubview(element!)
element?.removeFromSuperview()
}
That said, you cannot rely on this behavior. There is no promise about when an object will be destroyed. You only know it will be after you release your last strong reference to it. There may be additional retains or autoreleases on the object. And there's no promise that deinit will ever be called, so definitely make sure not to put any mandatory logic there. It should only perform memory cleanup. (Both Mac and iOS, for slightly different reasons, never call deinit during program termination.)
In your failing test case,deinit is printed after the assertion fails. You can demonstrate this by placing breakpoints on the failing assertion and the print. This is because the autorelease pool is drained at the end of the test case.
The underlying lesson is that it is very common for balanced retain/autorelease calls to be be placed on an object that is passed to ObjC code (and UIKit is heavily ObjC). You should generally not expect UIKit objects to be destroyed before the end of the current event loop. Don't rely on that (it's not promised), but it's often true.

Modify IBOutlet property from outside of viewDidLoad - Xcode Storyboards

I have a separate class which when called upon updates the ToolTip (a text property) for an NSButton in a pistonViewController via its IBOutlet.
However, whenever I try to perform the action, I get the error
"Unexpectedly found nil while implicitly unwrapping an Optional value"
since pistonViewController.piston.tooltip didn't work, I created an instance above the class:
let pistonView = pistonViewController();
and then from within the separate class called pistonView.set_piston();
func set_piston(index: Int) {
piston1.toolTip = "yay it worked!";
}
I get the same error: found nil.
How to get the correct instance of the pistonViewController (the one that appears on viewDidLoad) so that piston1 will not be nil?
There is this solution, but it looks needlessly complex. This one appears to only work on iOS, using a storyboard.InstantiateViewController command that does not work on MacOS. This MacOS solution is poorly explained and does not appear to work.
"[How do I] Modify IBOutlet property from outside of viewDidLoad"
(But what you're really asking is how you modify a view controller's views from outside of the view controller.)
The short answer is "Don't do that." It violates the principle of encapsulation. You should treat a view controller's view properties as private, and only modify them inside the view controller's code.
(To misquote Groucho Marx: "Doc, it crashes when I do this". "Then don't do that!")
Instead, add a public property (pistonToolTip) in your PistonViewController (Class names should begin with upper-case letters).
class PistonViewController: UIViewController {
var pistonToolTip: String {
didSet {
piston?.tooltip = pistonToolTip
}
}
}
And in case you set pistonToolTip before your PistonViewController has loaded its views, add this line to viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
piston?.tooltip = pistonToolTip
// The rest of your viewDidLoad code
}
Ultimately I just set it up in viewDidLoad, with a timer waiting for the other program to get the variables that will then be assigned to the pistons.
The lack of effective pointers to instances of View Controllers makes anything else not possible or perhaps just arcane and difficult.

Update to Xcode 11.3.1 - navigationBar and half of the Views disappear after storyboard refactoring

Using Xcode 11.3.1, Simulator11.3.1, iPhoneX, Swift5.1.3, iOS13.3,
I am wondering why half of my app suddenly disappears !!
Could it be the update to Xcode 11.3.1 ???
The following shows a screenshot of the Xcode Debug View Hierarchy.
The left side is what the iPhone 11 Pro Simulator shows and the right side is the Debug View Hierarchy:
Clearly there are many more objects in the view hierarchy (such as the round buttons at the bottom) that are not shown on the Simulator (and also not on a physical iPhoneX). Also the NavigationBar is missing completely !!!!
The blue highlighted object is a custom navigationBar (consisting of a stackView). This worked before but not since the Xcode update. I am really not believing this. What could go wrong here ??
If it is not the Xcode-update, then my refactoring of the storyboard could also be a cause of this view-losses.
Before my refactoring, the VC at question was a ChildViewController of another ViewController. Now, it is the entry point of the App. Could this change bring the view-losses ? I want to see a NavigationController with largeTitle. But there is no NavigationController whatsoever now!
Here is the code that sets up the navigationBar:
override func viewDidLoad() {
// set up navigationItem and navigationController look and feeel
navigationItem.largeTitleDisplayMode = .always
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationController?.set_iOS12_lookAndFeel()
navigationItem.title = "bluub"
}
And the needed NavigationController extension:
import UIKit
extension UINavigationController {
func set_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
self.keep_iOS12_lookAndFeel()
} else {
let attrLargeTitle = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
self.navigationBar.largeTitleTextAttributes = attrLargeTitle
let attrTitle = AppConstants.FontAttributes.NavBar_TitleTextAttributes
self.navigationBar.titleTextAttributes = attrTitle
}
}
private func keep_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithDefaultBackground()
navBarAppearance.backgroundEffect = .init(style: .systemThickMaterialDark)
navBarAppearance.titleTextAttributes = AppConstants.FontAttributes.NavBar_TitleTextAttributes
navBarAppearance.largeTitleTextAttributes = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
navBarAppearance.buttonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_ButtonAppearance_Normal
navBarAppearance.doneButtonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_Done_ButtonAppearance_Normal
self.navigationBar.standardAppearance = navBarAppearance
self.navigationBar.scrollEdgeAppearance = navBarAppearance
}
}
}
.
---------------- more findings -----------------------------
After another storyboard refactoring, I could bring back the round menu buttons. However, the largeTitle-NavigationBar is still completely missing.
Frankly, the latest refactoring did not introduce any new constraints or other storyboard settings as before. The fact that I kicked out the NavigationController and replaced it by an identical new one, plus, re-assigned one or the other constraint of the menu-button-View, did bring the bottom menu back alive. As far as I can tell, no difference to the previous storyboard was introduced.
It is very annoying why a storyboard needs to be redrawn basically to render correctly. Something seems corrupt here as for the Xcode functionality with storyboard !
But lets leave this talk.
My remaining question:
How can I bring back a missing NavigationBar ?????????
.
---------------- another finding -----------------------------
If I reassign the "first-entry-ViewController" to the old ViewController that eventually adds the Menu-button-ViewController as a ChildViewController --> then everything works!
If I assign the "first-entry-ViewController" to be the Menu-button-ViewController directly, then the NavigationBar disappears !
Here is the overview:
I finally found a solution.
It indeed had to do with my login-architecture of this app.
The fact that only by setting the "first-entry-ViewController" as the old-Main-ViewController made a difference:
This old-Main-ViewController (that eventually adds the Menu-button-ViewController as its Child) did have the following line in its viewWillAppear method:
navigationController?.setNavigationBarHidden(true, animated: animated)
Its intention was actually to never show the navigationBar of its own. But instead load a ChildViewController that itself shows a navigationBar of its own.
The strange thing with storyboard: Even tough setting the Menu-button-ViewController as first-entry does somehow still consider the navigationController-hiding mechanism of the previous first-entry setting. This seems a bug to me inside storyboard. I would assume that visible navigationBar is the default behaviour. But having set it once to be hidden keeps it hidden, even tough the hiding-command is no longer executed. Anyway, very strange behaviour.
By eliminiting that line - or better - by adding it "with hidden = false" inside the Menu-Button-ViewController, makes the NavigationBar being shown again !!!
My learning is to keep an eye on all navigationController actions or mutations throughout the entire App hierarchy. The fact that a single ViewController might mutate something on its navigationController might not be enough. You have to check event parent-ViewControllers or segue-parents as well. And most annoying, applying a different first-entry to a VC does require you to overwrite default behaviours of your views to make sure your views are shown !

IBOutlet is nil

I have created a standard outlet for a view that will hold different information based on the button selected on the previous screen.
#IBOutlet weak var labelView: UIView!
It shows it is connected in both the story board view and on the code itself, however, every time I get to any reference to the labelView such as:
if detail.description == "About"
{
labelView.backgroundColor = UIColor.red
}
Then the app crashes out with:
fatal error: unexpectedly found nil while unwrapping an Optional value
I have tried everything I can think of or read on the internet:
Removed and replaced the connection
Deleted the derived data folder like one post suggested
Created a reference to self.view to force it to load
Moved it to viewDidAppear
Moved it to viewWillAppear
Moved it to viewDidLoad (which is where it is currently being
called)
I am sure at this point that the answer is rather simple and I am just completely missing it.
To see where the outlet is being set to nil, try this:
#IBOutlet weak var labelView: UIView? {
didSet {
print("labelView: \(labelView)")
}
}
You should see it set to an initial value when the view is loaded. If it then gets set to nil, put a breakpoint on the print and your should be able to see from the backtrace where it's happening.
Views are lazy initialized. In case you are calling the affected line of code before viewDidLoad() in the views life cycle, try to access viewin advance:
if detail.description == "About" {
_ = self.view
labelView.backgroundColor = UIColor.red
}

How can I use didSet to change the text of a UITextField (IBOutlet)?

I'd like to set text value of UITextField (IBOutlet) in the DidSet of my model object that I pass.
Here the code:
let manageSettingViewController: ManageSettingViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ManageSettingViewController") as ManageSettingViewController
self.navigationController?.pushViewControllerCustom(manageSettingViewController)
manageSettingViewController.setting = setting
And in the didSet of manageSettingViewController:
var setting: Setting? {
didSet
{
keyTextField.text = setting?.label
valueTextField.text = setting?.value
}
How can I set the text? Because in this case Xcode crash because "keyTextField is nil" :(
You're setting manageSettingViewController.setting right after instantiating manageSettingViewController -- at this point, it hasn't loaded its view from the nib/storyboard yet, so all of its IBOutlet variables (which presumably keyTextField and valueTextField are) are still nil. Those text fields are hooked up as of when ManageSettingViewController's viewDidLoad method is called.
You could change your didSet to check the optional outlets before setting them, or assign through optional chaining:
didSet {
keyTextField?.text = setting?.label
valueTextField?.text = setting?.value
}
This would avoid the crash, but it would also fail to change your text field's content. You'd have to also implement viewDidLoad for ManageSettingViewController to check its setting property and set its text fields accordingly.
Of course, that would duplicate the code from your didSet. That code might still be useful if you want to set setting from elsewhere and have the UI update automatically, but didSet won't help you for updating UI before the UI loads.