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:
[
Related
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
Good Evening,
I am trying to dismiss a keyboard on a modally presented UITableView.
The UITextView is Created as a UINib and registered in the UITableView.
I tried to set the keyboard as "Dismiss On Drag" in storyboard, and nothing worked. I also wrote the following code and connected the UITextFieldDelegate on the UINib.
Here is the code in the UITextView Nib.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(text == "\n") {
textView.resignFirstResponder()
return false
}
return true
}
View Did Load:
override func awakeFromNib() {
super.awakeFromNib()
textView.delegate = self
}
Use scroll view, this is an example because I don't understand well what do you to do in your project... First set the controller that include your textView to UIScrollViewDelegate, after that declare your objects and their attributes:
class YourController: UIViewController, UIScrollViewDelegate {
let scrollView2: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let textV: UITextView = {
let v = UITextView()
v.backgroundColor = .yourColor
v.isUserInteractionEnabled = true
v.textColor = .white
v.font = .systemFont(ofSize: 20, weight: .regular)
v.translatesAutoresizingMaskIntoConstraints = false
v.heightAnchor.constraint(equalToConstant: 1000).isActive = true // assign here the text view height to allow your scrollview to scroll
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
scrollView2.delegate = self
setupConstraints() // call the function for constraints
}
below viewDidLoad write constraint function:
fileprivate func setupConstraints() {
view.addSubview(scrollView2)
scrollView2.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView2.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView2.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView2.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollView2.addSubview(textV)
textV.leadingAnchor.constraint(equalTo: scrollView2.leadingAnchor).isActive = true
textV.trailingAnchor.constraint(equalTo: scrollView2.trailingAnchor).isActive = true
textV.topAnchor.constraint(equalTo: scrollView2.topAnchor).isActive = true
textV.bottomAnchor.constraint(equalTo: scrollView2.bottomAnchor).isActive = true
// this is important for scrolling
textV.widthAnchor.constraint(equalTo: scrollView2.widthAnchor).isActive = true
}
now call scrollViewWillBeginDragging and assign interactive mode to your scrollview keyboardDismissMode:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollView2.keyboardDismissMode = .interactive
}
This is the result
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
}
}
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.
}
}
I have a table view that I would like to be able to search through. I understand how to do that, but is there a way that I can have a bar button item that expands to a search bar (of a certain width) right inside my table view header? For example, can I make something like below in swift?
Here's a working example:
class ExpandableView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override var intrinsicContentSize: CGSize {
return UILayoutFittingExpandedSize
}
}
class ViewController: UIViewController {
var leftConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
assert(navigationController != nil, "This view controller MUST be embedded in a navigation controller.")
// Expandable area.
let expandableView = ExpandableView()
navigationItem.titleView = expandableView
// Search button.
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(toggle))
// Search bar.
let searchBar = UISearchBar()
searchBar.translatesAutoresizingMaskIntoConstraints = false
expandableView.addSubview(searchBar)
leftConstraint = searchBar.leftAnchor.constraint(equalTo: expandableView.leftAnchor)
leftConstraint.isActive = false
searchBar.rightAnchor.constraint(equalTo: expandableView.rightAnchor).isActive = true
searchBar.topAnchor.constraint(equalTo: expandableView.topAnchor).isActive = true
searchBar.bottomAnchor.constraint(equalTo: expandableView.bottomAnchor).isActive = true
}
#objc func toggle() {
let isOpen = leftConstraint.isActive == true
// Inactivating the left constraint closes the expandable header.
leftConstraint.isActive = isOpen ? false : true
// Animate change to visible.
UIView.animate(withDuration: 1, animations: {
self.navigationItem.titleView?.alpha = isOpen ? 0 : 1
self.navigationItem.titleView?.layoutIfNeeded()
})
}
}