I want to initialize xib-files programmatically in Swift, so I created the class MyView.
The initialization of the xib is declared in the setup() method, where loadNibNamed() is called. This returns an additional view, which I have to add as a subview to my current/initial view.
I saw in User Interface Inspector that behind MyView is the initial view, which has of course also own properties. I do not like this behaviour and do not want to modify properties twice. In the end I want to achieve that the instance from the initializer would be replaceable with the instance that has been created by the call of loadNibNamed(); figurative something like self = view.
I added the code of the initializers and the setup() method.
required init?(coder aDecoder: NSCoder) {
NSLog("init with NSCoder")
super.init(coder: aDecoder)
}
init(in frame: CGRect) {
NSLog("init with CGRect")
super.init(frame: frame)
setup()
}
private func setup() {
NSLog("setting up")
let view = Bundle.main.loadNibNamed("MyView", owner: self, options: nil)!.first as! MyView
addSubview(view)
}
You cannot substitute one self for another in an initializer. init and nib-loading are related, but the relationship runs the opposite way from your proposal: loading the view from the nib will call your init(coder:).
What you need is not an initializer but a factory.
Give MyView a class method (class func) that a client can call to load the nib and return the instance.
class func new() -> MyView {
let view = Bundle.main.loadNibNamed("MyView", owner: nil, options: nil)!.first as! MyView
return view
}
Usage:
let v = MyView.new()
Related
I have been debugging for a day and decided I have no idea what is causing the error in my app. It would be awesome if anyone could help me out figure it out.
So I created a custom UIView from a Nib File with class name ManualScreen. xibsetup() basically is in UIView extension which just loads from the Nib file. I want to send the button tap from my view to ViewController. I directly did not add this view to the ViewController because I need to remove this ManualScreen view and add another view in its place when Segment Control is moved to another option.
class ManualScreen: UIView {
var mManualViewListener:ManualViewListener!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: CGRect.zero)
xibSetup()
}
#IBOutlet weak var counterLabel: UILabel!{
didSet {
print("labelView: \(String(describing: counterLabel))")
}
}
#IBAction func addButton(_ sender: UIButton) {
if(mManualViewListener != nil){ ->>>this is always nil for some reason
print("insdie the listener counting")
mManualViewListener.addCount()
}else{
print("listener is nil")
}
}
func addListener(manualViewListener:ManualViewListener){
print("adding listener")
mManualViewListener = manualViewListener
}
}
This UIView is initilized in the Viewcontroller and this Viewcontroller also implements my delegate protocol. When I initalized my customView in the Viewcontroller, I add this Viewcontroller as the delegate by doing
var manualScreen = ManualScreen()
manualScreen.addListener(manualViewListener: self)
My delegate protocol is
protocol ManualViewListener {
func addCount()
}
Once listener is set, I should be able to send some event (here button touch) from my view to the ViewController using manualViewListener.addcount(). But it says my manualViewListener is nil always.
I have just written a small portion of my code here as writing everything will be not feasible. If anyone wants to see the whole app, here is the GitHub link to the thing I am working. https://github.com/Rikenm/Auto-Counter-iOS
It doesn't look pretty for now. I am just working on the functionality right now.
And finally thank you for the help.
Your problem is here
override init(frame: CGRect) {
super.init(frame: CGRect.zero)
xibSetup() // this is the problem maker
}
you add a new view of the same class above it and for sure it's listener object is nil with the the screen view that you instantiate here
mManualScreen = ManualScreen()
mManualScreen.addListener(manualViewListener: self)
//
extension UIView{
func xibSetup() {
let view = loadFromNib()
addSubview(view)
stretch(view: view)
}
// 2. Loads the view from the nib in the bundle
/// Method to init the view from a Nib.
///
/// - Returns: Optional UIView initialized from the Nib of the same class name.
func loadFromNib<T: UIView>() -> T {
let selfType = type(of: self)
let bundle = Bundle(for: selfType)
let nibName = String(describing: selfType)
let nib = UINib(nibName: nibName, bundle: bundle)
guard let view = nib.instantiate(withOwner: self, options: nil).first as? T else {
fatalError("Error loading nib with name \(nibName) ")
}
return view
}
}
Instead you need
var mManualViewListener:ManualViewListener!
static func loadFromNib() -> ManualScreen {
let view = Bundle.main.loadNibNamed("ManualScreen", owner: self, options: nil)?.first as! ManualScreen
return view
}
with
mManualScreen = ManualScreen.loadFromNib()
mManualScreen.addListener(manualViewListener: self)
The problem is that you're creating 2 separate ManualScreen instances. Your method xibSetup creates and returns another ManualScreen instance and adds it as a subview of your first ManualScreen, which is attached to your detail view controller. If you set a breakpoint within addManualScreen() in your DetailViewController and inspect mManualScreen's subviews, you'll see another one.
Hence, you're setting the mManualViewListener delegate property to a ManualScreen, but the extra ManualScreen (which you shouldn't be creating) added as a subview from xibSetup() is intercepting the action, and that view doesn't have an mManualViewListener attached to it.
You should fix your view instantiation to only create one instance of ManualScreen and you will fix the problem.
I tried adding a couple of breakpoints to your code. It seems the way you're adding the view is a little (a lot?) off.
Settings Breakpoints
First off, I added a breakpoint to your addManualScreen method in line 89:
containerView.addSubview(mManualScreen)
And another breakpoint in your ManualScreen itself, the function addButton, line 51:
if(mManualViewListener != nil){
First Breakpoint Hit
OK, breakpoint one hit. What is mManualScreen at this point?
po mManualScreen
gives us amongst other things the object ID Auto_Counter.ManualScreen: 0x7fcfebe018d0
is the delegate set?
po mManualScreen.mManualViewListener
indeed it is: some : <Auto_Counter.DetailViewController: 0x7fcfeb837fb0>
Second Breakpoint Hit
OK, second breakpoint hit when I tap the + button. Is the mManualListener still set?
po mManualViewListener
Nope, we get nil
Lets take a look at the object itself then
po self
gives us
Auto_Counter.ManualScreen: 0x7fcfe8d4b300
Hang on, that's not the same object ID!
The Problem
Now take a look at xibSetup
func xibSetup() {
let view = loadFromNib()
addSubview(view)
stretch(view: view)
}
Here is where your second/inner view is created! And this is the view that reacts to your #IBAction.
Solution
You should rethink how you create your manual view, I can't really come up with the correct solution as it seems a bit convoluted at the moment, but you need to use either the nib creation method...or create it manually.
Update It seems others has found the correct solution. I hope my answer helps you in how to diagnose these kinds of problems another time at least then so you can reduce the frustration period from a day to maybe just half a day :)
Hope that helps.
I'm trying to get to grip with Swift and wanted some advice...
I have a UIView that exists on a number of screens; specifically, it's a logo that uses a number of elements/parameters to style it correctly i.e. shadow, shape, image etc.
For my first viewcontroller I set this up as a function that is called from the viewDidLoad function. Then in my second viewcontroller I have the same logo... here is my question,
Should I load the first view controller from the story board and then reference the function in the second viewcontroller OR should I have just made the logo a class that either viewcontroller can reference? My gut says it should be a class...
Thanks in advance
For a reusable view you’d create a XIB in which you design the view plus a view controller class in which you instantiate the xib, like this:
class ReusableView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "ReusableView", bundle: bundle)
if let view = nib.instantiate(withOwner: self, options: nil).first as? UIView {
view.frame = bounds
addSubview(view)
}
}
}
In your view controller(s), you’d then simply place a placeholder UIView at the desired location and set its custom type to ReusableView. By connecting an outlet from this view into your view controller you’d have access to the view’s properties.
Please note that you will have to leave the Custom View property in the XIB set as UIView and set File’s Owner to ReusableView instead. Otherwise you’ll create an infinite loop.
WARNING! As I pointed out on another answer, DO NOT load the same nib from within awakeFromNib, or else you'll create an infinite loop of loading nibs.
Your gut instinct is correct, I would say. Create a custom class for your reusable view to go in. If you decide to create a nib for your class, I recommend instantiating it from a static function.
class ReusableView: UIView {
static func newFromNib() -> ReusableView {
let bundle = Bundle(for: ReusableView.self)
let nib = UINib(nibName: "ReusableView", bundle: bundle)
guard let view = nib.instantiate(withOwner: self, options: nil).first as? ReusableView else {
preconditionFailure("Could not instantiate ReusableView")
}
return view
}
override func awakeFromNib() {
super.awakeFromNib()
// Configuration ONLY if you use a Nib.
}
override init(frame: CGRect) {
super.init(frame: frame)
// Configuration if you DO NOT use a nib
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Then, you can use it in a view controller:
class ViewController: UIViewController {
#IBOutlet weak var reusableViewContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Create a new instance of your reusable view
let reusableView = ReusableView.newFromNib()
// If not using a nib, just use the 'init(frame:)' method
// let reusableView = ReusableView(frame: .zero)
// Add your reusable view to the view hierarchy
self.reusableViewContainer.addSubview(reusableView)
// Layout your view as necessary:
// For example, if using AutoLayout:
self.reusableViewContainer.topAnchor.constraint(equalTo: reusableView.topAnchor).isActive = true
self.reusableViewContainer.bottomAnchor.constraint(equalTo: reusableView.bottomAnchor).isActive = true
self.reusableViewContainer.leadingAnchor.constraint(equalTo: reusableView.leadingAnchor).isActive = true
self.reusableViewContainer.trailingAnchor.constraint(equalTo: reusableView.trailingAnchor).isActive = true
}
}
Of course, you don't have to use a container view. Place it wherever you need it in your view hierarchy, and lay it out as you do for other views.
I use the following code to force a subclass of UIView to load from a XIB file whose name is the actual class name:
class NibView : UIView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
guard isRawView() else { return self }
for view in self.subviews {
view.removeFromSuperview()
}
let view = instanceFromNib()
return view
}
func isRawView() -> Bool {
// What here?
}
}
The purpose of the isRawView() method is to determine whether this view has been created from code, or it's been loaded from the corresponding XIB file.
The implementation I've used so far is:
func isRawView() -> Bool {
// A subview created by code (as opposed to being deserialized from a nib)
// has 2 subviews, both implementing the `UILayerSupport` protocol
return
self.subviews.count == 2 &&
self.subviews.flatMap(
{ $0.conforms(to: UILayoutSupport.self) ? $0 : nil }).count == 2
}
which uses a trick to determine if the view is created from code, because in such cases it contains exactly 2 subviews, both implementing the UILayoutSupport protocol.
This works nicely when a NibView subclass is instantiated from code. However it doesn't work if the view is created as part of a view controller in a storyboard (and presumably the same happens for view controllers and views loaded from XIB files).
Long story to explain the reason of my question: is there a way for a UIView to know whether it's been loaded from a XIB file, and possibly the name of that file? Or, otherwise, an alternative way of implementing the isRawView() method, which should:
return false if the view has been deserialized from an associated XIB file (whose name is the class name)
return true otherwise
Make use of the provided init functions.
init(frame:) -> From code
init(coder:) -> From nib
Example code:
override init(frame: CGRect) {
super.init(frame: frame)
print("From code")
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("From nib")
}
I can print out the class name like this:
print(NSStringFromClass(type(of: self)).components(separatedBy: ".").last ?? "Couldn't get it")
You should be able to use that, maybe with some slight adjustments, to get what you need.
Looking at the a lecture slide in the Stanford iOS 9 course here, he is creating a new UIView with two initializers (one if the UIView was created from storyboard, and one if it was created in code). The following code is written at the bottom of that particular slide:
func setup() {....} //This contains the initialization code for the newly created UIView
override init(frame: CGRect) { //Initializer if the UIView was created using code.
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) { //Initializer if UIView was created in storyboard
super.init(coder:aDecoder)
setup()
}
The rule is that you must initialize ALL of your own properties FIRST before you can grab an init from a superclass. So why is it that in this case he calls his superclass init super.init BEFORE he initializes himself setup()? Doesn't that contradict the following rule:
Safety check 1 A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer.
As mentioned above, the memory for an object is only considered fully initialized once the initial state of all of its stored properties is known. In order for this rule to be satisfied, a designated initializer must make sure that all its own properties are initialized before it hands off up the chain.
I haven't seen all the rest of the code in this example, but the rule is only that your properties have to be initialized (i.e. the memory they occupy has to be set to some initial value) before calling super.init(), not that you can't run extra setup code.
You can even get away with sort of not-really-initializing your properties by either declaring your properties lazy var, or using var optionals which automatically initialize to nil. You can then set them after your call to super.init().
For example:
class Foo: UIView {
var someSubview: UIView! // initializes automatically to nil
lazy var initialBackgroundColor: UIColor? = {
return self.someSubview.backgroundColor
}()
init() {
super.init(frame: .zero)
setup() // do some other stuff
}
func setup() {
someSubview = UIView()
someSubview.backgroundColor = UIColor.whiteColor()
addSubview(someSubview)
}
}
What is best strategy to load custom UIViews with XIB and Outlets? At this moment I have code listed below. I think this code is bad because I have 2 UIViews as container and in future probably problem with constraints.
UIViewController ( I don't want all outlets and actions in one big ViewController )
func showCategories() {
if(self.categoriesView == nil) {
self.categoriesView = CategoriesView()
}
self.view.addSubview(self.categoriesView!)
}
Custom UIView - CategoriesView
class CategoriesView, ...protocols... {
#IBOutlet var table:UITableView!
override init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame:CGRect) {
super.init(frame: frame)
}
override init() {
super.init()
let views = NSBundle.mainBundle().loadNibNamed("CategoriesView", owner: self, options: nil)
let view = views![0] as CategoriesView
self.frame = view.frame
self.addSubview(view)
}
....
}
In Apple's MVC, it's best to avoid views with too much logic in them. If you want to compose a complex view using component subviews, then look at Creating Custom Container View Controllers.
If you are already using storyboards, a container view will take care of most of the complexity for your.