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.
Related
I have a scrollView that contains a dynamic amount of WeatherViewControllers each displaying the weather data of a different city the user has saved. The user can segue from the WeatherViewControllers to a CityListViewController. Where they can add and remove cities from their list which in turn should add and remove WeatherViewControllers from the scrollView upon dismissing the CityListViewController, this is where I am running into a problem.
Currently I am trying to use a protocol in to call the func reloadScrollView which calls viewDidLoad in the scrollViewController upon dismissing(viewDidDisappear) the CityListViewController but am getting an error:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
when it gets to:
totalScrollView.addSubview(weatherScreen.view)
Using debugger I have found that totalScrollView is nil and that is causing the problem. Is there a way to make the scrollView load so it is not nil when dismissing the other viewController
OR
is the a better time to call use this protocol to call this function?
Side Note: Upon initially opening the app the scrollView loads properly with all the correct WeatherViewControllers in the UIScrollView and the correct cities in the list.
class ScrollViewController: UIViewController, ScrollReloadProtocol {
func reloadScrollView() {
print("SCROLL RELOADED!!!!!*******")
self.viewDidLoad()
}
#IBOutlet var totalScrollView: UIScrollView!
var pages = [ViewController]()
var x = 0
var weatherScreensArray = [SavedCityEntity]()
var weatherScreenStringArray = [String]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var horizString = "H:|[page1(==view)]"
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
//userDefaults used to keep track of which screen is which to put different cities on different viewControllers
defaults.set(0, forKey: "screenNumber")
//load cities to get number of cities saved
loadCities()
var views : [String: UIView] = ["view": view]
//create all weatherWeatherControllers
while x <= weatherScreensArray.count {
pages.append(createAndAddWeatherScreen(number: x))
weatherScreenStringArray.append("page\(x+1)")
views["\(weatherScreenStringArray[x])"] = pages[x].view
let addToHoriz = "[\(weatherScreenStringArray[x])(==view)]"
horizString.append(addToHoriz)
x+=1
}
horizString.append("|")
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[page1(==view)]|", options: [], metrics: nil, views: views)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horizString, options: [.alignAllTop, .alignAllBottom], metrics: nil, views: views)
NSLayoutConstraint.activate(verticalConstraints + horizontalConstraints)
}
//Function to create and add weatherViewController
func createAndAddWeatherScreen(number: Int) -> ViewController {
defaults.set(number, forKey: "screenNumber")
let story = UIStoryboard(name: "Main", bundle: nil)
let weatherScreen = story.instantiateViewController(identifier: "View Controller") as! ViewController
weatherScreen.view.translatesAutoresizingMaskIntoConstraints = false
totalScrollView.addSubview(weatherScreen.view)
addChild(weatherScreen)
weatherScreen.didMove(toParent: self)
return weatherScreen
}
}
Skipping that fact that your are not doing it right, let's forcus on the one issue at a time. You are trying to access the totalScrollView implicitly in the viewDidLoad where if the outlet is linked it should be loaded at that point. If it is nil you should:
Make sure that you have the .storyboard or .xib file defining the ScrollViewController layout.
Make sure you are loading this controller from that storyboard/xib.
Make sure that the view controller in the storyboard/xib file has set its class to ScrollViewController, similar to the following print screen:
Make sure that the outlet is linked in the storyboard/xib to this property in your code file (probably ScrollViewController.swift). If not:
open storyboard and sorucecode file in separate editors
drag and drop from the dot on the left of the property declaration to the UIScrollView in the storyboard
make sure that there is added a link to Referencing Outlets
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.
I'm having the hardest time finding an answer for this.
I have a xib view that is within a scrollview that is within a view controller. In the xib I have a button with an action and I need to segue to a view controller I have in my storyboard. I also would like to be able to use a custom segue.
So far, I have read that I can instantiate the viewcontroller from the storyboard to segue to it. But then I don't know how to present that controller.
thanks for any help...
UPDATE:
this is the code I'm using to perform the segue.
In parent ViewController:
static var referenceVC: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
print("viewdidload")
LevelSelectViewController.referenceVC = self
setupScrollView()
}
code in xib view file
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sightWordController")
let parent = LevelSelectViewController.referenceVC!
let segue = InFromRightCustomSegue(identifier: "test", source: parent, destination: vc)
segue.perform()
As noted in the comments, Segues are typically confined to storyboard usage as noted in the documentation. You can implement a custom xib view in a storyboard via #IBDesignable like approaches and have you're view load from the xib into the storyboard file/class. This way, you gain the benefits of both worlds. Otherwise, you may want to approach this in another fashion (such as delegates/target-action events, etc).
You may also climb the responder chain and call a segue related to the VC loaded from the storyboard (the segue doesn't necessarily have to be attached to any particular action) via getting a reference to the VC and calling the segue. You can climb the responder chain in a manner such as the example code below:
protocol ChildViewControllerContainer {
var parentViewController: UIViewController? { get }
}
protocol ViewControllerTraversable {
func viewController<T: UIViewController>() -> T?
}
extension UIView: ViewControllerTraversable {
func viewController<T: UIViewController>() -> T? {
var responder = next
while let currentResponder = responder {
guard responder is T else {
responder = currentResponder.next
continue
}
break
}
return responder as? T
}
}
extension UITableViewCell: ChildViewControllerContainer {
weak var parentViewController: UIViewController? {
return viewController() as UIViewController?
}
}
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?
I have tabbarController where i put parent viewController with container view inside.
public override func viewDidLoad() {
viewControllers = [
ParentViewController()
]
}
On init i'm initializing 2 child view controllers and adding 1st controller (that does't contain MapView) as child viewController.
At some point of time i need to switch between child controllers, and in that point app crashes
public class ParentViewController: UIViewController {
#IBOutlet weak var containerView: UIView!
let firstChildController: ViewControllerWithoutMapView
let secondChildController: ViewControllerWithMapView
init() {
firstChildController = ViewControllerWithoutMapView()
secondChildController = ViewControllerWithMapView()
super.init(nibName: "ParentViewController", bundle: nil)
}
public override func viewDidLoad() {
firstChildController.view.frame = containerView.bounds
addChildViewController(firstChildController)
firstChildController.willMoveToParentViewController(nil)
containerView.addSubview(firstChildController.view)
firstChildController.didMoveToParentViewController(self)
}
func switchChildControllers() {
secondChildController.view.frame = containerView.bounds <<<<< crash here
.....
}
}
I know about crashes that appears if you're not importing MapKit, i tried to import it everywhere - no luck.
What is the correct way to switch child viewControllers with MapView inside one of it?