Now I'm practicing build IOS app without using storyboard , but I have a problem want to solve , I created a custom UIView called BannerView and added a background(UIView) and a title(UILabel) , and called this BannerView in the MainVC , but run this app , it crashes at the function setupSubviews() and I don't know why.
import UIKit
import SnapKit
class BannerView: UIView {
var background: UIView!
var title: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
convenience init() {
self.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSubviews()
}
func setupSubviews() {
background.backgroundColor = .gray
title.text = "INHEART"
self.addSubview(background)
self.addSubview(title)
}
override func layoutSubviews() {
background.snp.makeConstraints { make in
make.width.equalTo(ScreenWidth)
make.height.equalTo(BannerHeight)
make.left.top.right.equalTo(0)
}
title.snp.makeConstraints { make in
make.width.equalTo(100)
make.center.equalTo(background.snp.center)
}
}
}
class MainVC: UIViewController {
var bannerView:BannerView!
override func viewDidLoad() {
super.viewDidLoad()
bannerView = BannerView(frame: CGRect.zero)
view.addSubview(bannerView)
}
}
Your properties do not appear to be initialised
var background: UIView!
var title: UILabel!
You should initialize these in your init method
self.background = UIView()
self.title = UILabel()
If you use force unwrapping on a class property you must initialize in the init method. XCode should be complaining to you about this and your error message should show a similar error
You are not initialised the background view please initialised them
self.background = UIView()
self.title = UILabel()
and if you want to create custom view by use of xib the follow them Custum View
You must have to initialised the self.background = UIView() and self.title = UILabel() first.
You can initalised them in setupSubviews() function before the set/assign values to them.
Related
I'm trying to use a xib file to create a reusable component (multiple times in the same View), and everything was fine until i wanted to update some controls from an #IBInspectable property. I found that #IBOutlet's are not set at that moment, so i did a search and found something
http://justabeech.com/2014/07/27/xcode-6-live-rendering-from-nib/
He is saving a the loaded view into proxyView so you could use it in the #IBInspectable. Unfortunately that code is a bit old and doesn't work as is. But my problem is when i try to load the nib as a class it doesn't work. It only works if i load it as UIView.
This line fails as this
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? ValidationTextField
It only works when is like this
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? UIView
I think that the problem is, the xib file's owner is marked as ValidationTextField, but the main view it's UIView. So when you load the nib it brings that UIView, that obviously has no custom properties or outlets.
Many examples about loading xib files say that the class must be in the file owner. So i don't know how to get the custom class using that.
import UIKit
#IBDesignable class ValidationTextField: UIView {
#IBOutlet var lblError: UILabel!
#IBOutlet var txtField: XTextField!
#IBOutlet var imgWarning: UIImageView!
private var proxyView: ValidationTextField?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
override func awakeFromNib() {
super.awakeFromNib()
xibSetup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
self.proxyView?.prepareForInterfaceBuilder()
}
func xibSetup() {
guard let view = loadNib() else { return }
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
// Saving the view in a variable
self.proxyView = view
}
func loadNib() -> ValidationTextField? {
let bundle = Bundle(for: type(of: self))
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? ValidationTextField
}
#IBInspectable var fontSize: CGFloat = 14.0 {
didSet {
let font = UIFont(name: "System", size: self.fontSize)
self.proxyView!.txtField.font = font
self.proxyView!.lblError.font = font
}
}
}
I don't even know if the rest will work. If what the link says it's true, getting the view after loading the nib will let me access to the outlets.
Running that code fails at this line
guard let view = loadNib() else { return }
I guess that it can't convert the UIView to the class so it returns nil, and then exits.
My goal is to have a reusable component that can be placed many times in a single controller. And be able to see its design in the storyboard.
Move xibSetup() to your initializers. awakeFromNib is called too late and it won't be called if the view is created programatically. There is no need to call it in prepareForInterfaceBuilder.
In short, this can be generalized to:
open class NibLoadableView: UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
loadNibContentView()
commonInit()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
loadNibContentView()
commonInit()
}
public func commonInit() {
// to be overriden
}
}
public extension UIView {
// #objc makes it possible to override the property
#objc
var nibBundle: Bundle {
return Bundle(for: type(of: self))
}
// #objc makes it possible to override the property
#objc
var nibName: String {
return String(describing: type(of: self))
}
#discardableResult
func loadNibContentView() -> UIView? {
guard
// note that owner = self !!!
let views = nibBundle.loadNibNamed(nibName, owner: self, options: nil),
let contentView = views.first as? UIView
else {
return nil
}
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = true
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.frame = self.bounds
return contentView
}
}
Note that the view that loads the nib must be the owner of the view.
Then your class will become:
#IBDesignable
class ValidationTextField: NibLoadableView {
#IBOutlet var lblError: UILabel!
#IBOutlet var txtField: XTextField!
#IBOutlet var imgWarning: UIImageView!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
override func commonInit() {
super.commonInit()
updateFont()
}
#IBInspectable var fontSize: CGFloat = 14.0 {
didSet {
updateFont()
}
}
private func updateFont() {
let font = UIFont.systemFont(ofSize: fontSize)
txtField.font = font
lblError.font = font
}
}
I guess that the whole idea about a proxy object comes from misuse of the Nib owner. With a proxy object, the hierarchy would have to be something like this:
ValidationTextField
-> ValidationTextField (root view of the nib)
-> txtField, lblError, imgWarning
which does not make much sense. What we really want is:
ValidationTextField (nib owner)
-> UIView (root view of the nib)
-> txtField, lblError, imgWarning
I want to test the .text of my dashboardLabel, but i don't know how to access it via XCTest.
The DashboardView.swift looks like this:
import UIKit
class DashBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
// MARK: - Create Subviews
func createSubviews() {
backgroundColor = .white
var dashboardLabel : UILabel
dashboardLabel = {
let label = UILabel()
label.text = "Dashboard Label"
label.textColor = .black
label.frame = CGRect(x:60, y:80, width: 200, height: 30)
label.backgroundColor = .green
label.backgroundColor = .lightGray
label.font = UIFont(name: "Avenir-Oblique", size: 13)
label.textAlignment = .center
return label
}()
}
The DashboardViewController.swift looks like this:
import UIKit
class DashBoardViewController: UIViewController {
var dashboardview = DashBoardView()
//MARK: View Cycle
override func loadView() {
view = dashboardview
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
title = "DashBoard"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I know how to test the Title of the DashboardViewController.swift
import XCTest
#testable import DashBoard
class DashBoardTests: XCTestCase {
func test_if_title_is_DashBoard() {
let vc = DashBoardViewController()
let _ = vc.view
XCTAssertEqual(vc.navigationItem.title, "Dashboard")
}
but i have absolutely no clue, how to access the dashboardLabel on the DashBoardView.swift.
I hope this explains my problem and anyone of you can help me, or point me in the right direction!
Thx ✌️
you can do that using accessibilityIdentifier
Refer : iOS XCUITests access element by accesibility
A fellow Software developer told me a solution which is pretty cool! You need to declare the Label as a property as follows
private(set) var dashboardLabel = UILabel()
and now you are able to access the property within the XCTest. Which makes sense, because you can only test things that are accessible from outside
import UIKit
class DashBoardView : UIView {
private(set) var dashboardLabel = UILabel()
}
XCTestFile
import XCTest
#testable import DashBoard
class DashBoardTests: XCTestCase {
func test_if_dashboard_label_has_title_DashBoard() {
let vc = DashBoardView()
XCTAssertEqual(vc.dashboardLabel.text, "DashBoard")
}
}
Hope that helps!
I have created a viewcontroller which consists of two classes. These are displayed below:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var Viewholder: UIImageView!
var txt:String?
override func viewDidLoad() {
super.viewDidLoad()
let vv= TestView(frame: Viewholder.frame)
view.addSubview(vv)
txt= "hello"
let rr = TestView(frame: self.view.frame,textvalue:txt!)
rr.colour = "" // set value
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
class TestView: UIView {
var textvalue:String?
init(frame: CGRect,textvalue) {
self.textvalue= textvalue
super.init(frame: frame)
self.sharedLayout()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
self.setupPaths()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func sharedLayout() {
print(textvalue!) // works here
}
func setupPaths() {
print(textvalue!) // doesnt work here (displays nil)
}
How can I make it work so that the value of textvalue in "setupPaths" gives the correct value so that I can then changee the text of a label. I am having trouble getting the setuppath function to display the passed value as it is returning null. This is stopping me from editting the label with a passed value.
You can create a customView like this
class TestView: UIView {
var colour:String?
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.sharedLayout()
}
init(frame: CGRect,colour:String) {
self.colour = colour
super.init(frame: frame)
self.sharedLayout()
}
func sharedLayout() {
}
}
//
You can create it like this
let rr = TestView(frame: self.view.frame,colour:"test")
// rr.colour = "" // set value
You are badly confused about object-oriented programming. In your code:
let vv= TestView(frame: Viewholder.frame)
view.addSubview(vv)
txt= "hello"
let rr = TestView(frame: self.view.frame,textvalue:txt!)
The let vv line creates a TestView object and installs it as a subview of the view controller's content view.
Then the next 2 lines create a NEW instance of TestView and install text into that 2nd view. You then forget about this second view.
This is like buying a new car, setting the station on the radio of the new car, abandoning the new car, and then going home and wondering why the radio station on your existing car didn't change. The two objects are not the same object and settings in one instance of TestView have no effect on the other instance of TestView.
I work with Nibs. I have two screens that will use the "same" UIView component with the same behavior. It's not the same component because in each screen i placed a UIView and made the same configuration, as show on the image.
To solve this and prevent replicate the same code in other classes i wrote one class, that is a UIView subclass, with all the functions that i need.
After that i made my custom class as superclass of these UIView components to inherit the IBOutlets and all the functions.
My custom class is not defined in a Nib, is only a .swift class.
I made all the necessary connections but at run time the IBOutlets is Nil.
The code of my custom class:
class FeelingGuideView: UIView {
#IBOutlet weak var firstScreen: UILabel!
#IBOutlet weak var secondScreen: UILabel!
#IBOutlet weak var thirdScreen: UILabel!
#IBOutlet weak var fourthScreen: UILabel!
private var labelsToManage: [UILabel] = []
private var willShow: Int!
private var didShow: Int!
override init(frame: CGRect) {
super.init(frame: frame)
self.initLabelManagement()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initLabelManagement()
}
private func initLabelManagement() {
self.initLabelVector()
self.willShow = 0
self.didShow = 0
self.setupLabelToShow(label: labelsToManage[0])
self.setupLabels()
}
private func initLabelVector() {
self.labelsToManage.append(self.firstScreen)
self.labelsToManage.append(self.secondScreen)
self.labelsToManage.append(self.thirdScreen)
self.labelsToManage.append(self.fourthScreen)
}
private func setupLabels() {
for label in labelsToManage {
label.layer.borderWidth = 2.0
label.layer.borderColor = UIColor(hex: "1A8BFB").cgColor
}
}
func willShowFeelCell(at index: Int) {
self.willShow = index
if willShow > didShow {
self.setupLabelToShow(label: labelsToManage[willShow])
}
else if willShow < didShow {
for i in didShow ... willShow + 1 {
let label = labelsToManage[i]
self.setupLabelToHide(label: label)
}
}
}
private func setupLabelToShow(label: UILabel) {
label.textColor = UIColor.white
label.backgroundColor = UIColor(hex: "1A8BFB")
}
private func setupLabelToHide(label: UILabel) {
label.textColor = UIColor(hex: "1A8BFB")
label.backgroundColor = UIColor.white
}
}
I found this question similar to mine: Custom UIView from nib inside another UIViewController's nib - IBOutlets are nil
But my UIView is not in a nib.
EDIT:
I overrided the awakeFromNib but it neither enter the method.
More explanation:
My custom class is only superClass of this component:
Which i replicate on two screens.
One screen is a UITableViewCell and the another a UIViewController.
It's all about to manage the behavior of the labels depending on the screen that is showing at the moment on the UICollectionView
When the initLabelVector() function is called at the required init?(coder aDecoder:) it arrises a unwrap error:
The error when try to open the View:
Cannot show the error with the UITableViewCell because it is called at the beginning of the app and don't appear nothing. To show the error with the screen i needed to remove the call of the UITableViewCell.
The UITableViewCell is registered first with the tableView.register(nib:) and after using the tableView.dequeueReusebleCell.
The UIViewController is called from a menu class that way:
startNavigation = UINavigationController(rootViewController: FellingScreenViewController())
appDelegate.centerContainer?.setCenterView(startNavigation, withCloseAnimation: true, completion: nil)
The problem is this code:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initLabelManagement()
}
The trouble is that init(coder:) is too soon to speak of the view's outlets, which is what initLabelManagement does; the outlets are not hooked up yet. Put this instead:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.initLabelManagement()
}
How I arrived at this answer:
As a test, I tried this:
class MyView : UIView {
#IBOutlet var mySubview : UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
print(#function, self.mySubview)
}
override func awakeFromNib() {
super.awakeFromNib()
print(#function, self.mySubview)
}
}
Here's the output:
init(coder:) nil
awakeFromNib() <UIView: 0x7fc17ef05b70 ... >
What this proves:
init(coder:) is too soon; the outlet is not hooked up yet
awakeFromNib is not too soon; the outlet is hooked up
awakeFromNib is called, despite your claim to the contrary
When the init() from my custom class is called the IBOutlets are not hooked up yet.
So, I created a reference on the parent view and called the iniLabelManagement() from the viewDidLoad() method and everything worked.
Thank you matt, for the help and patience!
This AssetRFPViewController class call a custom view(AssetBasicInfo) in viewDidLoad, if a pass "Hello" its ok but when i pass object to custom view(AssetBasicInfo) its not taken, How can i design a custom view class so its catch object
class AssetRFPViewController: UIViewController
{
var asset:Asset = Asset()
override func viewDidLoad() {
let customView = AssetBasicInfo(frame: self.topHeaderView.bounds)
customView.assetNameValLbl.text = "Hello" // ok
customView.assetObj = self.asset // not ok, passing object
self.topHeaderView.addSubview(customView)
}
}
Now my custom class look like this,
class AssetBasicInfo: UIView {
#IBOutlet var containerView: UIView!
var assetObj:Asset = Asset() // i want value in this object "assetObj"
#IBOutlet var assetNameValLbl: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
print(frame.size.width)
print(frame.size.height)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder : aDecoder)
commonInit()
}
private func commonInit(){
Bundle.main.loadNibNamed("HeaderViewXIB", owner: self, options: nil)
self.addSubview(containerView)
self.containerView.frame = self.frame
containerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.assetNameValLbl.text = assetObj.name // not setting value
print("screenName" , screenName)
}
}