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()
})
}
}
Related
Looking to add a tap gesture to an array of UIViews - without success. Tap seems not to be recognised at this stage.
In the code (extract) below:
Have a series of PlayingCardViews (each a UIView) showing on the main view.
Brought together as an array: cardView.
Need to be able to tap each PlayingCardView independently (and then to be able to identify which one was tapped).
#IBOutlet private var cardView: [PlayingCardView]!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(tapCard(sender: )))
for index in cardView.indices {
cardView[index].isUserInteractionEnabled = true
cardView[index].addGestureRecognizer(tap)
cardView[index].tag = index
}
}
#objc func tapCard (sender: UITapGestureRecognizer) {
if sender.state == .ended {
let cardNumber = sender.view.tag
print("View tapped !")
}
}
You need
#objc func tapCard (sender: UITapGestureRecognizer) {
let clickedView = cardView[sender.view!.tag]
print("View tapped !" , clickedView )
}
No need to check state here as the method with this gesture type is called only once , also every view should have a separate tap so create it inside the for - loop
for index in cardView.indices {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapCard(sender: )))
I will not recommend the selected answer. Because creating an array of tapGesture doesn't make sense to me in the loop. Better to add gesture within PlaycardView.
Instead, such layout should be designed using UICollectionView. If in case you need to custom layout and you wanted to use scrollView or even UIView, then the better approach is to create single Gesture Recognizer and add to the superview.
Using tap gesture, you can get the location of tap and then you can get the selectedView using that location.
Please refer to below example:
import UIKit
class PlayCardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.red
}
}
class SingleTapGestureForMultiView: UIViewController {
var viewArray: [UIView]!
var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: UIScreen.main.bounds)
view.addSubview(scrollView)
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(tapGetsure(_:)))
scrollView.addGestureRecognizer(tapGesture)
addSubviews()
}
func addSubviews() {
var subView: PlayCardView
let width = UIScreen.main.bounds.width;
let height = UIScreen.main.bounds.height;
let spacing: CGFloat = 8.0
let noOfViewsInARow = 3
let viewWidth = (width - (CGFloat(noOfViewsInARow+1) * spacing))/CGFloat(noOfViewsInARow)
let viewHeight = (height - (CGFloat(noOfViewsInARow+1) * spacing))/CGFloat(noOfViewsInARow)
var yCordinate = spacing
var xCordinate = spacing
for index in 0..<20 {
subView = PlayCardView(frame: CGRect(x: xCordinate, y: yCordinate, width: viewWidth, height: viewHeight))
subView.tag = index
xCordinate += viewWidth + spacing
if xCordinate > width {
xCordinate = spacing
yCordinate += viewHeight + spacing
}
scrollView.addSubview(subView)
}
scrollView.contentSize = CGSize(width: width, height: yCordinate)
}
#objc
func tapGetsure(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: scrollView)
print("location = \(location)")
var locationInView = CGPoint.zero
let subViews = scrollView.subviews
for subView in subViews {
//check if it subclass of PlayCardView
locationInView = subView.convert(location, from: scrollView)
if subView.isKind(of: PlayCardView.self) {
if subView.point(inside: locationInView, with: nil) {
// this view contains that point
print("Subview at \(subView.tag) tapped");
break;
}
}
}
}
}
You can try to pass the view controller as parameter to the views so they can call a function on parent view controller from the view. To reduce memory you can use protocols. e.x
protocol testViewControllerDelegate: class {
func viewTapped(view: UIView)
}
class testClass: testViewControllerDelegate {
#IBOutlet private var cardView: [PlayingCardView]!
override func viewDidLoad() {
super.viewDidLoad()
for cardView in self.cardView {
cardView.fatherVC = self
}
}
func viewTapped(view: UIView) {
// the view that tapped is passed ass parameter
}
}
class PlayingCardView: UIView {
weak var fatherVC: testViewControllerDelegate?
override func awakeFromNib() {
super.awakeFromNib()
let gr = UITapGestureRecognizer(target: self, action: #selector(self.viewDidTap))
self.addGestureRecognizer(gr)
}
#objc func viewDidTap() {
fatherVC?.viewTapped(view: self)
}
}
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:
[
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 collectionView cell that should either display an image or an icon that is generated as a custom UIView (lets say IconView).
Currently, I implemented this by adding an UIImageView and an IconView as subviews to a container view.
When an image is provided, the image property of UIImageView is simply updated. When a new IconView is provided it is currently always added as a subview to the container view. Therefore, before adding, it is first checked whether an IconView has already been added, and if so it is removed.
Although this implementation works, it is not very elegant and seems not efficient since it results in scrolling issues when the number of rows increase.
Would there be a better (more efficient) way to implement this for a single CollectionViewCell?
class CustomCell: UICollectionViewCell {
internal var image: UIImage? {
didSet {
self.imageView.image = image!
}
}
internal var iconView: IconView? {
didSet {
if !(self.iconContainerView.subviews.flatMap{ $0 as? IconView}.isEmpty) {
self.iconView!.removeFromSuperview()
}
self.iconView!.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.iconView!)
self.image = nil
}
}
fileprivate var imageView: UIImageView!
fileprivate var iconContainerView: UIView!
fileprivate var layoutConstraints = [NSLayoutConstraint]()
override init(frame: CGRect) {
super.init(frame: frame)
// ContainerView
self.iconContainerView = UIView()
self.iconContainerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.iconContainerView)
// ImageView
self.imageView = UIImageView()
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.iconContainerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
self.iconContainerView.widthAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
// Deactivate non-reusable constraints
_ = self.layoutConstraints.map { $0.isActive = false }
self.layoutConstraints = [NSLayoutConstraint]()
if let iconView = self.iconView {
self.imageView.isHidden = true
self.layoutConstraints.append(iconView.centerYAnchor.constraint(equalTo: self.iconContainerView.centerYAnchor))
self.layoutConstraints.append(iconView.centerXAnchor.constraint(equalTo: self.iconContainerView.centerXAnchor))
self.layoutConstraints.append(iconView.heightAnchor.constraint(equalToConstant: 40))
self.layoutConstraints.append(iconView.widthAnchor.constraint(equalToConstant: 40))
} else {
self.imageView.isHidden = false
self.iconView?.isHidden = true
self.layoutConstraints.append(self.imageView.leadingAnchor.constraint(equalTo: self.iconContainerView.leadingAnchor))
self.layoutConstraints.append(self.imageView.trailingAnchor.constraint(equalTo: self.iconContainerView.trailingAnchor))
self.layoutConstraints.append(self.imageView.topAnchor.constraint(equalTo: self.iconContainerView.topAnchor))
self.layoutConstraints.append(self.imageView.bottomAnchor.constraint(equalTo: self.iconContainerView.bottomAnchor))
}
_ = self.layoutConstraints.map {$0.isActive = true}
}
}
Don't ad and remove the IconView when setting. Add both in the same spot and change the isHidden, alpha, or opacity or bringSubviewToFront. This is much less main thread intensive.