Delegate nil after setting it - ios

I am using the delegate method but for some odd reason my delegate variable seems to be nil when I want to call the delegate method. I can't for the life of me figure out what I'm doing wrong
protocol ProfileProtocol {
func buttonTapped()
}
class ProfileView: UIView {
var delegate: ProfileProtocol?
#IBOutlet weak var button: UIButton!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func awakeFromNib() {
configure()
}
func setup() {
...
}
#IBAction func buttonTapped(_ sender: UIButton) {
// delegate nil
delegate?.buttonTapped()
}
}
ProfileViewController (yes it conforms to ProfileProtocol):
override func viewDidLoad() {
swipeableView.nextView = {
createCardView()
}
}
func createCardView() -> UIView {
let cardView = ProfileView(frame: swipeableView.bounds)
cardView.delegate = self
let contentView = Bundle.main.loadNibNamed("ProfileCardView", owner: self, options: nil)?.first! as! UIView
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = cardView.backgroundColor
cardView.addSubview(contentView)
activeCardView = cardView
return cardView
}
func buttonTapped() {
self.performSegue(withIdentifier: "profileToEmojiCollection", sender: self)
}
Whenever I tap the button in my ProfileView, my ProfileViewController should perform a segue, however the delegate method isn't even being called because delegate is nil when I tap the button

I like to keep my custom views modular, and do things programmatically, it avoids the use of a Xib.
You should keep your view's responsibilities and subviews to the view itself. Ultimately the View receiving the the action(s) should be responsible for calling the delegate's methods. Also nextView is a closure that returns a UIView: (() -> UIView?)? not a UIView, a call to a function in a closure is not an explicit return you should return the view: let view = createCardView() return view.
ProfileView.swift
import UIKit
protocol ProfileProtocol {
func buttonTapped()
}
class ProfileView: UIView {
var delegate: ProfileProtocol?
lazy var button: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
button.setTitle("Profile Button", for: .normal)
button.backgroundColor = UIColor.black
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
}
#objc func buttonTapped(_ sender: UIButton) {
// Check for a nil delegate, we dont want to crash if one is not set
if delegate != nil {
delegate!.buttonTapped()
} else {
print("Please set ProfileView's Delegate")
}
}
func setup() {
//setup subviews
self.addSubview(button)
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
button.heightAnchor.constraint(equalToConstant: 50).isActive = true
button.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
}
You can create ProfileView's like any other UIView, but remember to set the Delegate of each of them after creation:
swipeableView.nextView = {
let view = createProfileView() //set properties during creation?
view.delegate = self
//set properties after creation?
//view.backgroundColor = UIColor.red
return view
}
ViewController.swift
import UIKit
class ViewController: UIViewController, ProfileProtocol {
lazy var profileView: ProfileView = {
let view = ProfileView()
view.backgroundColor = UIColor.lightGray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
profileView.delegate = self
setup()
}
func buttonTapped() {
print("Do Something")
}
func setup() {
self.view.addSubview(profileView)
profileView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
profileView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.7).isActive = true
profileView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
profileView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Related

UIVIewController Not Getting Deinitialized When Popping

I am building a settings screen in an app where I have a list of cells. If a user taps on a cell it pushes another controller onto the stack. However, I have this flow in several places in my app.
Therefore, I decided to reuse a generic controller and initialize it with sections (depending on which cell was tapped)
However, when popping a UIViewController it isn't getting deinitialized
VIEW CONTROLLER CODE
// Class
class ProfileController: UIViewController {
private let authService: AuthSerivce
private let sections: [FormSectionComponent]
init(authService: AuthSerivce,
sections: [FormSectionComponent]) {
self.authService = authService
self.sections = sections
super.init(nibName: nil, bundle: nil)
}
}
// Cell Delegate
extension ProfileController: NavigateCellDelegate {
func navigate(cell: NavigateCell) {
guard let sections = cell.item?.components else { return }
let controller = ProfileController(authService: authService, sections: sections)
self.navigationController?.pushViewController(controller, animated: true)
}
}
CELL CODE
protocol NavigateCellDelegate {
func navigate(cell: NavigateCell)
}
class NavigateCell: UICollectionViewCell {
var item: NavigateComponent?
var delegate: NavigateCellDelegate?
lazy var titleLabel: UILabel = {
let view = UILabel()
view.numberOfLines = 0
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func bind(_ item: FormItemComponent) {
guard let item = item as? NavigateComponent else { return }
self.item = item
setUpView(item: item)
addTapGestureRecogniser()
}
func addTapGestureRecogniser() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapGesture))
self.addGestureRecognizer(tapGesture)
self.isUserInteractionEnabled = true
}
#objc func tapGesture() {
delegate?.navigate(cell: self)
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = ""
}
}
extension NavigateCell {
func setUpView(item: NavigateComponent) {
titleLabel.text = item.title
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
} // END
UPDATED WEAK DELEGATE IN CELL
protocol NavigateCellDelegate: AnyObject {
func navigate(cell: NavigateCell)
}
class NavigateCell: UICollectionViewCell {
weak var item: NavigateComponent?
weak var delegate: NavigateCellDelegate?
Figured it out - The problem was with my DiffableDataSource and not declaring [weak self]
return UICollectionViewDiffableDataSource(collectionView: profileView.collectionView) { [weak self] collectionView, indexPath, item in

How can I detect a tap event in a child class of UIView?

I'm creating an app with swift.
I've made child classes from UIView. After making them and writing some processes there, I feel that I want them to detect touch events.
But they aren't children of UIButton.
I'd not like to force them to detect touch events using UIGestureRecognizer. Because UIGestureRecognizer needs to be used in UIViewController. I'd like to write codes of detecting touch just in the view.
Are there any ways to detect touch events just in UIView?
You can simply add the gesture to the subclass of UIView as other said, but if you want to include the gesture within the definition of the subclass and make it more modular, you can use the notification dispatch mechanism to broadcast the gesture to the registered view controller.
First, you create a name for the notification:
extension Notification.Name {
static let CustomViewTapped = Notification.Name("CustomViewTapped")
}
Then, you add the gesture to your custom view:
class CustomView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let tap = UIGestureRecognizer(target: self, action: #selector(tapped))
self.addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func tapped(_ sender: UIGestureRecognizer) {
NotificationCenter.default.post(name: .CustomViewTapped, object: self)
}
}
And, finally, observe the broadcast from your view controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(customViewTapped), name: .CustomViewTapped, object: nil)
let customView = CustomView()
self.view.addSubview(customView)
}
#objc func customViewTapped(_ sender: UIGestureRecognizer) {
}
}
You can add UIGestureRecognizer to UIView. Another way you can add invisible UIButton on top of your UIView.
If you use UIView subclass, you can use something following and handle tap in action closure
class TappableView: UIView {
var action: (()->())? = nil
init(frame: CGRect) {
super.init(frame: frame)
initialization()
}
func initialization() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGesture)
}
#objc private func tapGesture() {
action?()
}
}
class childView: UIView {
var action: (()->())? = nil
init(frame: CGRect) {
super.init(frame: frame)
initialization()
}
func initialization() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGesture)
}
#objc private func tapGesture() {
//Action called here
}
}
//MARK:- View Tap Handler
extension UIView {
private struct OnClickHolder {
static var _closure:()->() = {}
}
private var onClickClosure: () -> () {
get { return OnClickHolder._closure }
set { OnClickHolder._closure = newValue }
}
func onTap(closure: #escaping ()->()) {
self.onClickClosure = closure
isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(onClickAction))
addGestureRecognizer(tap)
}
#objc private func onClickAction() {
onClickClosure()
}
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: .init(x: 0, y: 0, width: 80, height: 50))
view.backgroundColor = .red
view.onTap {
print("View Tapped")
}
}

UIBarButtonItem not Showing

So Im trying to create a UIBarButtonItem with a custom UIView by subclassing it like so.
import UIKit
import SnapKit
class LocationManager: UIBarButtonItem {
let createdView = UIView()
lazy var currentCityLabel: UILabel = {
let currentCityLabel = UILabel()
currentCityLabel.text = "Philadelphia, PA"
guard let customFont = UIFont(name: "NoirPro-SemiBold", size: 20) else {
fatalError("""
Failed to load the "CustomFont-Light" font.
Make sure the font file is included in the project and the font name is spelled correctly.
"""
)
}
currentCityLabel.adjustsFontForContentSizeCategory = true
return currentCityLabel
}()
lazy var downArrow: UIImageView = {
let downArrow = UIImageView()
downArrow.contentMode = .scaleAspectFit
downArrow.image = UIImage(named: "downArrow")
return downArrow
}()
override init() {
super.init()
setupViews()
}
#objc func setupViews(){
customView = createdView
createdView.addSubview(currentCityLabel)
currentCityLabel.snp.makeConstraints { (make) in
make.left.equalTo(createdView.snp.left)
make.top.bottom.equalTo(createdView)
}
createdView.addSubview(downArrow)
downArrow.snp.makeConstraints { (make) in
make.left.equalTo(currentCityLabel.snp.right).offset(5)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
However, when I create it and assign it in my viewController I see nothing
import UIKit
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
#objc func setupViews(){
guard let collection = collectionView else {
return
}
collection.backgroundColor = .white
let customLeftBar = LocationManager()
self.navigationController?.navigationItem.leftBarButtonItem = customLeftBar
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I've looked at other post and none seem to quite match my situation. I'm beginning to think it is because I didn't give the UIView a frame but I am not exactly sure how I would do that in this instance if that is the case. Anyone see anything I don't that could potentially help me solve this problem. Also setting a target doesn't work I tried two different ways and none of them triggers a thing
#objc func setupBarButtonItems(){
let customLeftBar = LocationManager()
customLeftBar.action = #selector(self.leftBarPressed)
customLeftBar.target = self
customLeftBar.customView?.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.leftBarPressed))
customLeftBar.customView?.addGestureRecognizer(tapGesture)
navigationItem.leftBarButtonItem = customLeftBar
}
#objc func leftBarPressed(){
print("left bar tapped")
}
Change your adding line from
self.navigationController?.navigationItem.leftBarButtonItem = customLeftBar
to
self.navigationItem.leftBarButtonItem = customLeftBar
When add the barItem, you need to add it via navigationItem of the ViewController, not NavigationController
EDITED for add the action
Your custom UIBarButtonItem is a Custom View's BarButtonItem, so the target and selector will not working.
You can add your custom action by adding a button into your customView, and send the action via closure
Init your closure
var didSelectItem: (() -> Void)?
Add the create button code in your #objc func setupViews()
let button = UIButton(type: .custom)
createdView.addSubview(button)
button.snp.makeConstraints { (maker) in
maker.top.bottom.leading.trailing.equalTo(createdView)
}
// button.backgroundColor = UIColor.cyan // uncomment this line for understand about the barbuttonitem's frame
button.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
and add the function
#objc func didTap(_ button: UIButton) {
print("Did tap button")
}
In your viewController, you can get the tap action by
customLeftBar.didSelectItem = { [weak self] in
self?.leftBarPressed()
}
Unfortunately, your barbuttonitem's default frame is 30x30, so you must be set the frame for your barbuttonitem. If not, you can only catch the tap action in 30x30 area (uncomment the code for see it)

how canBecomeFirstResponder bring custom inputAccessoryView

I'm trying to make custom inputAccessoryView contain UITextField and docking it bottom like chat application
but if i didn't use canBecomeFirstResponder = true inputAccessoryView are hidden(?)
this is my code
class MainVC: UITableViewController {
lazy var inputTextFieldContainer: UIView = {
// custom inputAccessoryView
}()
override func viewDidLoad(){
super.viewDidLoad()
}
override var inputAccessoryView: UIView? {
get{
return containerView
}
}
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
}
how canBecomeFirstResponder bring custom inputAccessoryView
i read about UIResponder and responder chain on Apple docs
but i couldn't match that concept to this issue.
They said UIResponder will handle the events and i make my MainVC become first responder by canBecomeFirstResponder = true
and inputAccessoryView are shown
but what is the exact event in this case and situation
Since your code inherits from UITableViewController here a complete example with it:
Start with defining a accessory view. Since you mentioned as an example a chat app it could be a textfield and a send button:
class SendMessageView: UIView {
private let textField = UITextField()
private let sendButton = UIButton()
init() {
super.init(frame: CGRect.zero)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) not implemented")
}
private func setup() {
sendButton.setTitle("Send", for: UIControlState.normal)
sendButton.setTitleColor(UIColor.blue, for: UIControlState.normal)
self.addSubview(textField)
self.addSubview(sendButton)
self.backgroundColor = UIColor.groupTableViewBackground
textField.backgroundColor = UIColor.white
self.autoresizingMask = .flexibleHeight
textField.translatesAutoresizingMaskIntoConstraints = false
sendButton.translatesAutoresizingMaskIntoConstraints = false
textField.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
textField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16).isActive = true
sendButton.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: 8).isActive = true
sendButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16).isActive = true
sendButton.centerYAnchor.constraint(equalTo: textField.centerYAnchor).isActive = true
}
override var intrinsicContentSize: CGSize {
let contentHeight = self.textField.intrinsicContentSize.height + 16
return CGSize(width: UIScreen.main.bounds.width, height: contentHeight)
}
}
Next you need a custom UITableView which uses our accessory view:
class CustomTableView: UITableView {
private let sendMessageView = SendMessageView()
override var canBecomeFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return self.sendMessageView
}
}
Finally one could define a TableViewController using this custom table view:
class TableViewController: UITableViewController {
override func loadView() {
self.tableView = CustomTableView()
self.view = self.tableView
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.becomeFirstResponder()
}
...
The result would look like this:
[

Constraints not working when device is rotated

I have this UIViewController:
import UIKit
class ViewController: UIViewController {
var object: DraggableView?
override func viewDidLoad() {
super.viewDidLoad()
// Create the object
object = DraggableView(parent: self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Add subview
object?.setup()
}
}
And I have this class to add the view in this VC:
import UIKit
class DraggableView {
var parent: UIViewController!
let pieceOfViewToShow: CGFloat = 30.0
init(parent: UIViewController) {
self.parent = parent
}
func setup() {
let view = UIView(frame: parent.view.frame)
view.backgroundColor = UIColor.red
parent.view.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
view.heightAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.heightAnchor).isActive = true
// I need to show only a piece of the view at bottom, so:
view.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow).isActive = true
}
}
Problem
Everything is correct but when the device rotates it loses the constraint and the added view is lost.
I think the problem is in the next line that is not able to update the correct height [parent.view.frame.height] when the device is rotated.
view.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow).isActive = true
How could I make to update this constant when rotating?
I'm using Swift 3.
You can try using traitCollectionDidChange callback on the UIView to update the constraint when a rotation changes, for that to work you'll need to make DraggableView a subclass of the UIView:
import UIKit
class DraggableView: UIView {
var parent: UIViewController!
let pieceOfViewToShow: CGFloat = 30.0
// keep the constraint around to have access to it
var topConstraint: NSLayoutConstraint?
init(parent: UIViewController) {
super.init(frame: parent.view.frame)
self.parent = parent
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.backgroundColor = UIColor.red
parent.view.addSubview(self)
self.translatesAutoresizingMaskIntoConstraints = false
self.leadingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
self.heightAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.heightAnchor).isActive = true
// keep a reference to the constraint
topConstraint = self.topAnchor.constraint(equalTo: parent.view.safeAreaLayoutGuide.topAnchor, constant: parent.view.frame.height - pieceOfViewToShow)
topConstraint?.isActive = true
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// update the constraints constant
topConstraint?.constant = parent.view.frame.height - pieceOfViewToShow
}
}

Resources