Instantiate NSView with custom objects in it using NSNib - swift

I have a subclass of NSView called MyView, and I have a nib file whose File's Owner is MyView. I would like to create copies of a view in my nib file, and so I am using a class function as shown below:
class MyView: NSView {
#IBOutlet var myImageView: NSImageView! // Cocoa class
#IBOutlet var myEditingField: EditingField! // Custom subclass of cocoa object
class func initWithTitle(_ title: String) -> MyView {
let myNib = NSNib(nibNamed: "MyView", bundle: nil)
var myArray = NSArray()
myNib!.instantiate(withOwner: self, topLevelObjects: &myArray) // instantiate view and put in myArray
var myViewInstance = myArray[0] as! MyView
myViewInstance.imageView.image = NSImage(named: title)
myViewInstance.myEditingField.stringValue = title // this line
return myViewInstance
}
}
I have connected an IBOutlet from an NSImageView in the view in my nib file to the property myImageView in the MyView class, and I have connected an IBOutlet from an EditingField, a custom subclass of NSTextField that I wrote, to the myEditingField property. So, to create an instance of MyView simply I do:
let instance = MyView.initWithTitle("foo")
The issue with this method is that when IB creates the view, it is calling the required initializer init(coder:) on the EditingField in the view in the nib file. Originally, I had left my implementation of init(coder:) as simply the default fatalError("init(coder:) is not implemented") because I didn't think IB would call that initializer. I had figured IB would call init(frame:), but in reality it does call init(coder:). So, I tried implementing init(coder:) the following way:
class EditingField: NSTextField {
var id: String
// ... other properties
required init?(coder: NSCoder) {
print("init coder")
self.id = "default"
// ... other properties get default values, just like id
super.init(coder: coder)
}
}
Unfortunately, this did not work either. When I run the project using the above initializer in EditingField, the line myViewInstance.myEditingField.stringValue = title in MyView throws an error. When this happens, the debugger console reveals that the property myEditingField is nil, and, unlike myImageView, hasn't been initialized at all (despite the fact that the print message in init(coder:) still prints!)
So, my questions are (1) how do I initialize/create an NSView from a nib file that has custom objects in it? (2) why does IB call init(coder:) on EditingField? and (3) why is myEditingField nil despite the print message suggesting that the initializer ran?

Related

Unable to instantiate view programmatically

I have created a component (swift + xib file)
#IBDesignable
class MainItem: UIView {
let kCONTENT_XIB_NAME = "MainItem";
#IBOutlet weak var newsImage: UIImageView!;
#IBOutlet weak var newsTitle: UILabel!;
#IBInspectable var image:UIImage? {
didSet {
if(image != nil && newsImage != nil) {
newsImage.image = image;
}
}
};
#IBInspectable var title:String = "" {
didSet {
newsTitle.text = title;
}
};
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
// override func viewDidLoad(){
// super.viewDidLoad();
// }
func loadViewFromNib() -> UIView? {
let nib = UINib(nibName: kCONTENT_XIB_NAME, bundle: nil)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func commonInit() {
// standard initialization logic
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
if(newsImage != nil) {
let bounds = CGRect.init(x: newsImage.frame.origin.x,
y: newsImage.frame.origin.y + (newsImage.frame.height / 2),
width: newsImage.frame.width,
height: newsImage.frame.height / 2)
newsImage.addBlackGradientLayerInBackground(frame: bounds, colors:[.clear, .black])
}
}
}
The xib I have done with one tutorial, I have connected the file owner to the class also the IBOutlets are connected. And this worked fine, if I use this component within a storyboard.
Now I am trying to use this in my code
func initSlider() -> [MainItem] {
let slide1 = UINib(nibName: "MainItem", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! MainItem
slide1.image = UIImage(named: "u17119.png")
slide1.title = "bla bla"
return [slide1];
}
I am getting this error on startup
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key newsImage.'
I am not shure why this is comming
Also a side note:
If I uncomment the viewDidLoad section in my component - I can't compile
Method does not override any method from its superclass
Value of type 'UIView' has no member 'viewDidLoad'
It will be useful for you to understand how the code works in your first (working) example. There are two views in the story: the MainItem declared in your code, and the UIView designed in the MainItem.xib file. They are different! The one declared in code is the MainItem. The one in the .xib is just a plain vanilla UIView.
In the .xib file, the File's Owner is declared as being a MainItem. Therefore the File's Owner sprouts newsImage and newsTitle outlets, and these are hooked to subviews of the UIView in the .xib file.
When the MainItem is initialized, it reaches into the .xib file and loads the UIView with itself (the MainItem) as owner. This matches the situation with the outlets, so the outlets are correctly hooked up. And then it plops the UIView into itself as its own subview, with exactly the same size. Thus it acts a host to the UIView.
Let's chart that architecture:
MainItem view --> subview --> xib file UIView
newsImage outlet --------> UIImageView subview
newsTitle outlet --------> UILabel subview
The point is, that is the only architecture under which this xib file is capable of operating correctly.
So in your second example, you attempt to use a completely different architecture. You are no longer in the MainItem; you are in a UIViewController. And you attempt to reach directly into the xib yourself and load the UIView with nil as owner, and with no MainItem to host it. You completely bypass the MainItem and the loading architecture that it establishes! Thus the outlets cannot be hooked up and you crash.
I think that the problem is related to "Bundle" that it must be the "main". So you can try this:
NSBundle.mainBundle().loadNibNamed("MainItem", owner: self, options: nil)
also check that Module is refers to your target (check in Xib View, top right, under the class field).
Furthermore, pay attention to using "!" because force cast can bring a crash, use "?" optional operator.

Setting .attributedText on properties (accessible via outlets inside NIB-loaded UIView) from external reference appears to fail

A nib has been loaded manually via UINib(nibName, bundle).instantiate().
The outlets connecting the nib to a UIView subclass are being successfully initialized and are accessible.
Two of these outlets represent a UILabel and a UITextView - which are being used to present attributed text strings.
Changes to the attributed strings are being performed on mutable copies before replacement via the .attribtedText setter method.
Everything works as expected whenever functions intended to update the attributed text are called either directly in the UIView subclass or the View Controller that loads the nib.
However, when the same function is called via a reference kept inside some other class object elsewhere in the codebase, the updates don't happen.
The Nib's UIView subclass:
class MyView: UIView {
#IBOutlet weak var aLabel: UILabel!
#IBOutlet weak var someText: UITextView!
...
public func applySomeStyle() {
guard
let aLabelMAS = aLabel.attributedText?.mutableCopy() as? NSMutableAttributedString,
var someTextMAS = someText.attributedText.mutableCopy() as? NSMutableAttributedString
else {
return
}
let labelRange = NSRange(location: 0, length: aLabelMAS.length)
aLabelMAS.addAttribute(.backgroundColor, value: UIColor.yellow, range: labelRange)
let someTextRange = NSRange(location: 0, length: someTextMAS.length)
someTextMAS.removeAttribute(.backgroundColor, range: someTextRange)
aLabel.attributedText = aLabelMAS
someText.attributedText = someTextMAS
}
public func doStuff() {
...
applySomeStyle() // No problems -- the attributed strings inside the UILabel and UITextView are updated as intended.
...
}
}
From inside the ViewController that loads the Nib, calls to the applySomeStyle function via the reference to the MyView object work fine.
class MyViewController: UIViewController {
weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
...
myView = UINib(nibName: "MyView", bundle: Bundle.main).instantiate(
withOwner: self, options: nil).first as? MyView
self.view.addSubview(myView)
...
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myView.applySomeStyle() // Again, no problems.
}
}
But if the nib class object is referenced elsewhere -- the changes don't occur.
class SomeOtherViewController: UIViewController
var myVC: MyViewController!
...
func foo() {
myVC.myView.applySomeStyle() // Fails to update the UILabel/UITextView
}
...
}
Executing the attributedText setter, i.e.
someText.attributedText = someNewAttributedString
triggers viewDidLayoutSubviews().
Inside viewDidLayoutSubviews(), I was calling a function (e.g. a 'load content' function) that was also responsible for setting some string attributes. I was using a boolean flag to ensure that subsequent calls to viewDidLayoutSubviews weren't going to trigger that function again.
In this case, that boolean condition was not working as it was intended to, and so other functions that set the attributedText property were leading it to be triggered again, overwriting the changes made by other functions.
I thought I was dealing with an obscure bug.
I thought maybe it was a thread issue.
I thought maybe it was a broken reference of some kind.
I thought wrong.
But I hope that this proves useful to someone out there.

NSWindowController designated initializer puzzle

I'm trying to make this code work:
class MyWindowController: NSWindowController
{
let thing: Thing
convenience init(thing: Thing)
{
self.thing = thing
super.init(windowNibName: NSNib.Name(rawValue: "MyNib"))
}
}
The problem, of course, is that a convenience initializer can't call init from a superclass. So how do I initialize my thing and still be able to call init(windowNibName:), which is itself a convenience initializer? I'd rather not have to re-implement the nib loading myself, but how do I avoid it if I can only use designated initializers?
According to the NSWindowController documentation:
You can also implement an NSWindowController subclass to avoid requiring client code to get the corresponding nib's filename and pass it to init(windowNibName:) or init(windowNibName:owner:) when instantiating the window controller. The best way to do this is to override windowNibName to return the nib's filename and instantiate the window controller by passing nil to init(window:). Using the init(window:) designated initializer simplifies compliance with Swift initializer requirements.
You can implement your class as:
class MyWindowController: NSWindowController
{
let thing: Thing
override var windowNibName: NSNib.Name? {
return NSNib.Name(rawValue: "MyNib")
}
init(thing: Thing) {
self.thing = thing
super.init(window: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to subclass custom UIViewController in Swift?

I'd like to create a reusable view controller UsersViewControllerBase.
UsersViewControllerBase extends UIViewController, and implements two delegates (UITableViewDelegate, UITableViewDataSource), and has two views (UITableView, UISegmentedControl)
The goal is to inherit the implementation of the UsersViewControllerBase and customise the segmented items of segmented control in UsersViewController class.
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
class UsersViewController: UsersViewControllerBase {
}
The UsersViewControllerBase is present in the storyboard and all outlets are connected, the identifier is specified.
The question is how can I init the UsersViewController to inherit all the views and functionality of UsersViewControllerBase
When I create the instance of UsersViewControllerBase everything works
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
But when I create the instance of UsersViewController I get nil outlets
(I created a simple UIViewController and assigned the UsersViewController class to it in the storyboard )
let usersViewController = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewController") as? UsersViewController
It looks like views are not inherited.
I would expect init method in UsersViewControllerBase that gets controller with views and outlets from storyboard:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
init(){
let usersViewControllerBase = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("UsersViewControllerBase") as? UsersViewControllerBase
self = usersViewControllerBase //but that doesn't compile
}
}
And I would init UsersViewController:
let usersViewController = UsersViewController()
But unfortunately that doesn't work
When you instantiate a view controller via instantiateViewControllerWithIdentifier, the process is essentially as follows:
it finds a scene with that identifier;
it determines the base class for that scene; and
it returns an instance of that class.
And then, when you first access the view, it will:
create the view hierarchy as outlined in that storyboard scene; and
hook up the outlets.
(The process is actually more complicated than that, but I'm trying to reduce it to the key elements in this workflow.)
The implication of this workflow is that the outlets and the base class are determined by the unique storyboard identifier you pass to instantiateViewControllerWithIdentifier. So for every subclass of your base class, you need a separate storyboard scene and have hooked up the outlets to that particular subclass.
There is an approach that will accomplish what you've requested, though. Rather than using storyboard scene for the view controller, you can instead have the view controller implement loadView (not to be confused with viewDidLoad) and have it programmatically create the view hierarchy needed by the view controller class. Apple used to have a nice introduction to this process in their View Controller Programming Guide for iOS, but have since retired that discussion, but it can still be found in their legacy documentation.
Having said that, I personally would not be compelled to go back to the old world of programmatically created views unless there was a very compelling case for that. I might be more inclined to abandon the view controller subclass approach, and adopt something like a single class (which means I'm back in the world of storyboards) and then pass it some identifier that dictates the behavior I want from that particular instance of that scene. If you want to keep some OO elegance about this, you might instantiate custom classes for the data source and delegate based upon some property that you set in this view controller class.
I'd be more inclined to go down this road if you needed truly dynamic view controller behavior, rather than programmatically created view hierarchies. Or, even simpler, go ahead and adopt your original view controller subclassing approach and just accept that you'll need separate scenes in the storyboard for each subclass.
So, you have your base class:
class UsersViewControllerBase: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var segmentedControl: UISegmentedControl!
#IBOutlet weak var tableView: UITableView!
//implementation of delegates
}
[A] And your subclass:
class UsersViewController: UsersViewControllerBase { var text = "Hello!" }
[B] A protocol that your subclass will be extending:
protocol SomeProtocol {
var text: String? { get set }
}
[C] And some class to handle your data. For example, a singleton class:
class MyDataManager {
static let shared = MyDataManager()
var text: String?
private init() {}
func cleanup() {
text = nil
}
}
[D] And your subclass:
class UsersViewController: UsersViewControllerBase {
deinit {
// Revert
object_setClass(self, UsersViewControllerBase.self)
MyDataManager.shared.cleanup()
}
}
extension UsersViewController: SomeProtocol {
var text: String? {
get {
return MyDataManager.shared.text
}
set {
MyDataManager.shared.text = newValue
}
}
}
To properly use the subclass, you need to do (something like) this:
class TestViewController: UIViewController {
...
func doSomething() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
//Instantiate as base
let usersViewController = storyboard.instantiateViewControllerWithIdentifier("UsersViewControllerBase") as! UsersViewControllerBase
//Replace the class with the desired subclass
object_setClass(usersViewController, UsersViewController.self)
//But you also need to access the property 'text', so:
let subclassObject = usersViewController as! UsersViewController
subclassObject.text = "Hello! World."
//Use UsersViewController object as desired. For example:
navigationController?.pushViewController(subclassObject, animated: true)
}
}
EDIT:
As pointed out by #VyachaslavGerchicov, the original answer doesn't work all the time so the section marked as [A] was crossed out. As explained by an answer here:
object_setClass in Swift
... setClass cannot add instance variables to an object that has already been created.
[B], [C], and [D] were added as a work around. Another option to [C] is to make it a private inner class of UsersViewController so that only it has access to that singleton.
The problem is that you created a scene in the storyboard, but you didn't give the view controller's view any subviews or connect any outlets, so the interface is blank.
If your goal is to reuse a collection of views and subviews in connection with instances of several different view controller classes, the simplest way, if you don't want to create them in code, is to put them in a .xib file and load it in code after the view controller's own view-loading process (e.g. in viewDidLoad).
But if the goal is merely to "customise the segmented items of segmented control" in different instances of this view controller, the simplest approach is to have one view controller class and one corresponding interface design, and perform the customization in code. However, you could load just that segmented control from its own .xib in each case, if it's important to you design it visually.

Accessing function in MasterViewController from DetailViewController (swift)

I have a function in MasterViewController
func removeLocation(city: String){
objects.removeObject(city)
self.tableView.reloadData()
}
In my DetailViewController I check whether the city is valid and if its not, I want to remove it from the table in MasterViewController. I pass self in prepareForSegue() from MasterView to DetailView and I assign it to
var masterViewController: MasterViewController
But then I get an error saying that it is not initialized and it want me to have this initializer
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Which breaks my whole program when I run it and gives me an expected fatal error.
Ho I can create an instance of MasterView in DetailView without an initializer, or access a function in MasterView from DetailView?
This is Swift btw, I found some advices on how to solve it in C but I couldn't implement them.
Use an optional.
var masterViewController: MasterViewController?
Optionals do not have to be initialized when the class is created.
In prepareForSegue() assign the pointer as usual:
destinationViewController.masterViewController = self
Then when you need to call removeLocation:
masterViewController?.removeLocation("London")