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
}
}
Related
I have a long register form that consists of 4 steps (the content is not relevant), here are the mockups:
My problem is that I need to share a progress view between multiple views. This view should have an animation of growth. What would be the right and clean way to do this with UIKit? Should I create a custom Navigation Bar with that progress? Or use child controllers in some way?
I've been searching over here but the other questions I found are very old (like 7 years ago) and I don't know if there could be better solutions.
Thanks a lot!
There are various ways to do this...
One common approach is to set the "progress view" as the navigation bar's Title View -- but that won't show it below the navigation bar.
So, another approach is to subclass UINavigationController and add a "progress view" as a subview. Then, implement willShow viewController and/or didShow viewController to update the progress.
As a quick example, assuming we have 4 "steps" to navigate to...
We'll start with defining a "base" view controller, with two properties that our custom nav controller class will use:
class MyBaseVC: UIViewController {
// this will be read by ProgressNavController
// to calculate the "progress percentage"
public let numSteps: Int = 4
// this will be set by each MyBaseVC subclass,
// and will be read by ProgressNavController
public var myStepNumber: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// maybe some stuff common to the "step" controllers
}
}
Then, each "step" controller will be a subclass of MyBaseVC, and will set its "step number" (along with anything else specific to that controller):
class Step1VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 1
// maybe some other stuff specific to this "step"
}
}
class Step2VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 2
// maybe some other stuff specific to this "step"
}
}
class Step3VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 3
// maybe some other stuff specific to this "step"
}
}
class Step4VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 4
// maybe some other stuff specific to this "step"
}
}
Then we can setup our custom nav controller class like this (it's not really as complicated as it may look):
class ProgressNavController: UINavigationController, UINavigationControllerDelegate {
private let outerView = UIView()
private let innerView = UIView()
private var pctConstraint: NSLayoutConstraint!
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// for this example, we're using a simple
// green view inside a red view
// as our "progress view"
// we set it up here, but we don't add it as a subview
// until we navigate to a MyBaseVC
// we know we're setting
// outerView height to 20
// innerView height to 12 (4-points top/bottom "padding")
// so let's round the ends of the innerView
innerView.layer.cornerRadius = 8.0
outerView.backgroundColor = .systemRed
innerView.backgroundColor = .systemGreen
outerView.translatesAutoresizingMaskIntoConstraints = false
innerView.translatesAutoresizingMaskIntoConstraints = false
outerView.addSubview(innerView)
// initialize pctConstraint
pctConstraint = innerView.widthAnchor.constraint(equalTo: outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
NSLayoutConstraint.activate([
innerView.topAnchor.constraint(equalTo: outerView.topAnchor, constant: 4.0),
innerView.leadingAnchor.constraint(equalTo: outerView.leadingAnchor, constant: 4.0),
innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor, constant: -4.0),
pctConstraint,
])
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// if the next VC to show
// is a MyBaseVC subclass
if let _ = viewController as? MyBaseVC {
// add the "progess view" if we're coming from a non-MyBaseVC controller
if outerView.superview == nil {
view.addSubview(outerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 4.0),
outerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
outerView.heightAnchor.constraint(equalToConstant: 20.0),
])
// .alpha to Zero so we can "fade it in"
outerView.alpha = 0.0
// we just added the progress view,
// so we'll let didShow "fade it in"
// and update the progress width
} else {
self.updateProgress(viewController)
}
} else {
if outerView.superview != nil {
// we want to quickly "fade-out" and remove the "progress view"
// if the next VC to show
// is NOT a MyBaseVC subclass
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 0.0
}, completion: { _ in
self.outerView.removeFromSuperview()
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
self.pctConstraint.isActive = true
})
}
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// if the VC just shown
// is a MyBaseVC subclass
// AND
// outerView.alpha < 1.0 (meaning it was just added)
if let _ = viewController as? MyBaseVC, outerView.alpha < 1.0 {
self.updateProgress(viewController)
}
// otherwise, updateProgress() is called from willShow
}
private func updateProgress(_ viewController: UIViewController) {
if let vc = viewController as? MyBaseVC {
// update the innerView width -- the "progress"
let nSteps: CGFloat = CGFloat(vc.numSteps)
let thisStep: CGFloat = CGFloat(vc.myStepNumber)
var pct: CGFloat = .leastNonzeroMagnitude
// sanity check
// avoid error/crash if either values are Zero
if nSteps > 0.0, thisStep > 0.0 {
pct = thisStep / nSteps
}
// don't exceed 100%
pct = min(pct, 1.0)
// we can't update the multiplier directly, so
// deactivate / update / activate
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: pct, constant: -8.0)
self.pctConstraint.isActive = true
// if .alpha is already 1.0, this is effectively ignored
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 1.0
})
// animate the "bar width"
UIView.animate(withDuration: 0.3, animations: {
self.outerView.layoutIfNeeded()
})
}
}
}
So, when we navigate to a new controller:
we check to see if it is an instance of MyBaseVC
if Yes
add the progress view (if it's not already there)
get the step number from the new controller
update the progress
if Not
remove the progress view
I put up a complete example you can check out and inspect here: https://github.com/DonMag/ProgressNavController
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
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 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()
})
}
}