iOS - How to initialize custom UIView with specific Frame from NIB - ios

I am wondering what is the cleanest way to initialize a custom UIView with a specific frame.
The UIView is designed from a XIB file.
Here is my implementation :
class CustomView : UIView {
#IBOutlet var outletLabel: UILabel!
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
private func setupView() {
// Set text for labels
}
}
Here is how I want to initialize it in my ViewController :
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
let frame = CGRect(x: 0, y: 0, width: screenWidth - 50, height: 70)
let customView = CustomView.init(frame: frame)
But it is not working, I have a white UIView without any outlets.
And if I do this instead :
// Extension to UIView to load Nib
let customView : CustomView = UIView.fromNib()
I can see my view from XIB file, with its width/height used in the Interface Builder.
What is I want to load the view from XIB file BUT with specific frame ?
Am I missing something about initialization ?

You can have a NibLoading class like:
// NibLoadingView.swift
//source:https://gist.github.com/winkelsdorf/16c481f274134718946328b6e2c9a4d8
import UIKit
// Usage: Subclass your UIView from NibLoadView to automatically load a xib with the same name as your class
#IBDesignable
class NibLoadingView: UIView {
#IBOutlet weak var view: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
nibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
nibSetup()
}
private func nibSetup() {
backgroundColor = .clear
view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.translatesAutoresizingMaskIntoConstraints = true
addSubview(view)
}
private func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of:self))
let nib = UINib(nibName: String(describing: type(of:self)), bundle: bundle)
let nibView = nib.instantiate(withOwner: self, options: nil).first as! UIView
nibView.anchorAllEdgesToSuperview()
return nibView
}
}
extension UIView {
func anchorAllEdgesToSuperview() {
self.translatesAutoresizingMaskIntoConstraints = false
if #available(iOS 9.0, *) {
addSuperviewConstraint(constraint: topAnchor.constraint(equalTo: (superview?.topAnchor)!))
addSuperviewConstraint(constraint: leftAnchor.constraint(equalTo: (superview?.leftAnchor)!))
addSuperviewConstraint(constraint: bottomAnchor.constraint(equalTo: (superview?.bottomAnchor)!))
addSuperviewConstraint(constraint: rightAnchor.constraint(equalTo: (superview?.rightAnchor)!))
}
else {
for attribute : NSLayoutAttribute in [.left, .top, .right, .bottom] {
anchorToSuperview(attribute: attribute)
}
}
}
func anchorToSuperview(attribute: NSLayoutAttribute) {
addSuperviewConstraint(constraint: NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: superview, attribute: attribute, multiplier: 1.0, constant: 0.0))
}
func addSuperviewConstraint(constraint: NSLayoutConstraint) {
superview?.addConstraint(constraint)
}
}
Then your view will subclass the NibLoadingClass like:
class YourUIView: NibLoadingView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Set your XIB class in File's Owner like:
In this case it will be YourUIView
Then instantiate it:
let myView = YourUIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width-60, height: 170))

You can create custom class like this...
import UIKit
class CustomView: UIView {
class func mainView() -> CustomView {
let nib = UINib(nibName: "nib", bundle: nil)
let view = nib.instantiate(withOwner: self, options: nil).first as! CustomView
return view
}
override init(frame: CGRect) {
super.init(frame: frame)
let view = CustomView.mainView()
view.frame = frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Initialize view where you want...
let view = CustomView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = UIColor.blue
self.view.addSubview(view)

Related

How to make a button work on a custom view from .xib instantiated programmatically

The button works if I add the view via the Interface Builder but it doesn't work when I add the view programmatically.
.xib design:
My custom view class:
class customView: UIView {
static let singleton1 = customView()
#IBOutlet weak var newView: UIView!
#IBAction func changeColor(_ sender: Any) {
newView.backgroundColor = UIColor.systemBlue // this is what the button should do
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
window?.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
window?.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
}
func commonInit() {
let viewFromXib = Bundle.main.loadNibNamed("customView", owner: self, options: nil)![0] as! UIView
viewFromXib.bounds = self.bounds
addSubview(viewFromXib)
}
This is the way I instantiate it:
#IBAction func showView(_ sender: Any) {
let playerview = customView.singleton1
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(playerview)
playerview.tag = 100
UIApplication.shared.keyWindow?.addSubview(playerview)// yes it needs to be in the window
Not sure if my initializer is wrong or something else.
You are trying to load "customView" xib into its own init method. Try below.
Replace commonInit() method with getView(frame:CGRect)->UIView
class customView: UIView {
static let singleton1 = customView()
#IBOutlet weak var newView: UIView!
#IBAction func changeColor(_ sender: Any) {
newView.backgroundColor = UIColor.systemBlue // this is what the button should do
}
override init(frame: CGRect) {
super.init(frame: frame)
window?.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
window?.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude)
}
func getView(frame:CGRect)->UIView{
let viewFromXib = Bundle.main.loadNibNamed("customView", owner: nil, options: nil)![0] as! UIView
viewFromXib.frame = frame
return viewFromXib
}
}
Replace showView() method
#IBAction func showView(_ sender: Any) {
let playerview = customView.singleton1.getView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 200))
self.view.addSubview(playerview)
playerview.tag = 100
UIApplication.shared.keyWindow?.addSubview(playerview)
}

How to change navigationBar height & use custom view

I have a custom navigationBar view like this.
And I use sizeThatFits seems not work.
How to achieve this?
class ListVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let height: CGFloat = 140
let bounds = self.navigationController?.navigationBar.bounds
self.navigationController?.navigationBar.frame = CGRect(x: 0, y: 0, width: bounds?.width ?? UIScreen.main.bounds.width, height: height)
}
}
class CustomNavi: UINavigationBar {
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInit()
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: 140)
}
private func commonInit() {
let bundle = Bundle.init(for: CustomNavi.self)
if let viewToAdd = bundle.loadNibNamed("CustomNavi", owner: self, options: nil), let contentView = viewToAdd.first as? UIView {
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
}

How to set inputViewStyle on UIInputView (initialized in XIB)?

I'm implementing a view above the keyboard with some additional buttons.
I saw that you can use UIInputView like this:
let keyboardAccessoryView = UIInputView(frame: .init(x: 0, y: 0, width: 0, height: 40), inputViewStyle: .keyboard)
textView.inputAccessoryView = keyboardAccessoryView
The result looks good (matches the background in both light and dark mode).
I'd prefer if I could define the interface in a XIB though, I find that easier. But the property inputViewStyle is read-only, so it seems you can only set the inputView style during initialization.
I tried subclassing UIInputView and overriding inputViewStyle but that doesn't change the style for some reason.
Is there a way to init this with the .keyboard style and still use a xib?
You can load your XIB view and add it as a subview to your UIInputView:
class InputTestViewController: UIViewController {
#IBOutlet var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let keyboardAccessoryView = UIInputView(frame: .init(x: 0, y: 0, width: 0, height: 40), inputViewStyle: .keyboard)
// I have MyInputView configured to load itself from a XIB
let v = MyInputView()
// add it to the UIInputView
keyboardAccessoryView.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: keyboardAccessoryView.topAnchor),
v.leadingAnchor.constraint(equalTo: keyboardAccessoryView.leadingAnchor),
v.trailingAnchor.constraint(equalTo: keyboardAccessoryView.trailingAnchor),
v.bottomAnchor.constraint(equalTo: keyboardAccessoryView.bottomAnchor),
])
textView.inputAccessoryView = keyboardAccessoryView
}
}
class MyInputView: UIView, NibLoadable {
override init(frame: CGRect) {
super.init(frame: frame)
setupFromNib()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFromNib()
}
}
public protocol NibLoadable {
static var nibName: String { get }
}
public extension NibLoadable where Self: UIView {
static var nibName: String {
return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
}
static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
func setupFromNib() {
guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
addSubview(view)
view.backgroundColor = .clear
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
Here's how my custom view looks in IB (four buttons in a horizontal stack view):
and here's how it looks at run-time:

Hiding/Showing Input Accessory View

I have a custom inputAccessoryView and am trying to toggle hiding/showing it. I don't want to utilize .isHidden or .removeFromSuperView(), rather, use the bottom slide in/out, which seems to be native as I present other viewControllers onto the hierarchy and this animation executes.
I've tried to resignFirstResponder with no luck and there doesn't seem to be any existing commentary around this. Any thoughts would be appreciated as I am admittedly stumped.
class CustomInputAccessoryView: UIView {
let customTextView: CustomInputTextView = {
let tv = CustomInputTextView()
tv.isScrollEnabled = false
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
autoresizingMask = .flexibleHeight
addSubview(customTextView)
customTextView.topAnchor.constraint(equalTo: topAnchor, constant: 12).isActive = true
customTextView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: 8).isActive = true
customTextView.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
customTextView.rightAnchor.constraint(equalTo: rightAnchor, constant: 10).isActive = true
customTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
class CustomInputTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
override var canResignFirstResponder: Bool {
return true
}
override var canBecomeFirstResponder: Bool {
return true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init coder has not been implemented")
}
}
//in viewcontroller
lazy var inputContainerView: CustomInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 60)
let customInputAccessoryView = CustomInputAccessoryView(frame: frame)
return customInputAccessoryView
}()
//onLoad
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
I don't know if this is really what you're going for, but give it a try.
Tapping anywhere in the view will show/hide the input accessory view:
class TestInputViewController: UIViewController {
//in viewcontroller
lazy var inputContainerView: CustomInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 60)
let customInputAccessoryView = CustomInputAccessoryView(frame: frame)
return customInputAccessoryView
}()
override var canBecomeFirstResponder: Bool {
return true
}
//onLoad
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
override func viewDidLoad() {
super.viewDidLoad()
// so we can see the frame
inputContainerView.backgroundColor = .blue
// tap anywhere in view to show / hide input accessory view
let g = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
view.addGestureRecognizer(g)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
#objc func didTap(sender: UITapGestureRecognizer) {
if self.isFirstResponder {
resignFirstResponder()
} else {
becomeFirstResponder()
}
}
}
class CustomInputAccessoryView: UIView {
let customTextView: CustomInputTextView = {
let tv = CustomInputTextView()
tv.isScrollEnabled = false
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
autoresizingMask = .flexibleHeight
addSubview(customTextView)
customTextView.translatesAutoresizingMaskIntoConstraints = false
customTextView.topAnchor.constraint(equalTo: topAnchor, constant: 12).isActive = true
customTextView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true
customTextView.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
customTextView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
customTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
class CustomInputTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
override var canResignFirstResponder: Bool {
return true
}
override var canBecomeFirstResponder: Bool {
return true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init coder has not been implemented")
}
}
Not related to showing / hiding, but a few of your constraints were wrong, causing the text view to be misplaced.

XIB view not showing with correct layout [iOS Swift]

I'm trying to make a custom alert view and facing some issues with the fact that the displayed view is cutting the bottom half of the view (Images below)
How it's being displayed:
Desired Output:
So basically, I have a XIB called CustomAlertView supported by a class of same name with init as follows:
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("CustomAlertView", owner: self, options: nil)
contentView.frame = self.bounds
contentView.translatesAutoresizingMaskIntoConstraints = true
addSubview(contentView)
//contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
I have another class that is responsible for creating an alert, CustomAlert, using the customAlertView. This CustomAlert class is creating the backgroundView and dialogView( which I'm trying to add my customAlertView to it) with the following code:
func initialize(title:String, description:String){
dialogView.clipsToBounds = true
backgroundView.frame = frame
backgroundView.backgroundColor = UIColor.black
backgroundView.alpha = 0.6
backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTappedOnBackgroundView)))
addSubview(backgroundView)
dialogView.frame.origin = CGPoint(x: 0, y: 0)
dialogView.frame.size = CGSize(width: frame.width-32, height: frame.height/3)
dialogView.backgroundColor = UIColor.white
dialogView.layer.cornerRadius = 6
let alertView = CustomAlertView.init(frame: self.bounds)
alertView.titleLabel.text = title
alertView.descriptionLabel.text = description
alertView.cancelButton.backgroundColor = UIColor.brown
dialogView.addSubview(alertView)
addSubview(dialogView)
}
I believe that I'm making a confusion with the frames and bounds but couldn't find a solution.
I'd like the desired output to be placed perfectly inside the dialogView.
EDIT
Code for my .show function in CustomAlert
func show(animated:Bool){
self.backgroundView.alpha = 0
self.dialogView.center = CGPoint(x: self.center.x, y: self.frame.height + self.dialogView.frame.height/2)
UIApplication.shared.delegate?.window??.rootViewController?.view.addSubview(self)
if animated {
UIView.animate(withDuration: 0.33, animations: {
self.backgroundView.alpha = 0.66
})
UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 10, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.dialogView.center = self.center
}, completion: { (completed) in
})
}else{
self.backgroundView.alpha = 0.66
self.dialogView.center = self.center
}
}
Github link git-alert-view
For anyone facing the same difficulties as me, I was able to accomplish the wanted result.
I used AutoLayouts as suggested by #HAK. But instead of writing my own NSLayoutConstraint I used roberthein library called TinyConstraints.
Basically, I used as follow:
Instead of
NSLayoutConstraint.activate([
alertView.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0),
alertView.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0),
alertView.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0),
alertView.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant:
0)])
with TinyConstraints:
alertView.edges(to: superview)
That´s it
Change your CustomAlertView like this:
class CustomAlertView: UIView {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var descriptionLabel: UILabel!
#IBOutlet weak var confirmButton: UIButton!
#IBOutlet weak var cancelButton: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
static func customAlert() -> CustomAlertView {
return Bundle.main.loadNibNamed("CustomAlertView", owner: self, options: nil)!.first as! CustomAlertView
}
}
Your CustomAlert's initialize method like this:
func initialize(title:String, description:String){
dialogView.clipsToBounds = true
backgroundView.frame = frame
backgroundView.backgroundColor = UIColor.black
backgroundView.alpha = 0.6
backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTappedOnBackgroundView)))
addSubview(backgroundView)
dialogView.frame.origin = CGPoint(x: 0, y: 0)
dialogView.frame.size = CGSize(width: frame.width-32, height: frame.height/3)
dialogView.backgroundColor = UIColor.white
dialogView.layer.cornerRadius = 6
let alertView = CustomAlertView.customAlert()
alertView.titleLabel.text = title
alertView.descriptionLabel.text = description
alertView.cancelButton.backgroundColor = UIColor.brown
dialogView.addSubview(alertView)
addSubview(dialogView)
}
In the CustomAlertView xib:
1. Select fileowner and remove the class (default to NSObject).
2. Select fileowner and then remove all the outlets.
3. Select your content view and give it class = CustomAlertView.
4. Select your CustomAlertView and make all outlet connections.
Final Xib:
And you have a working alert:
PS: Adjust the UI accordingly.
In your xib class add this :
override func awakeFromNib() {
super.awakeFromNib()
xibSetup()}
func xibSetup() {
guard let view = loadViewFromNib() else { return }
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
Reference from this : https://medium.com/zenchef-tech-and-product/how-to-visualize-reusable-xibs-in-storyboards-using-ibdesignable-c0488c7f525d#.3c0javomy**

Resources