Place activity indicator over uitable view - swift

I've placed an Activity indicator in a UITableView within the storyboard.
How can I center it in the middle of the screen and in front of the table view?
I've tried:
self.activityIndicator.center = self.tableView.center
But now, the activity indicator is in the top-middle of the view!

To display an activity indicator while data is loading in a UITableView, you can create a UIView and add it to the main view (in the center of the UITableView). If you want to display it as shown bellow:
You can do that with the fnuctions setLoadingScreen and removeLoadingScreen (if you want to see a sample project you can go here):
class TableViewController: UITableViewController {
// MARK: Properties
/// Data source of the tableView
var rows: [String] = []
/// View which contains the loading text and the spinner
let loadingView = UIView()
/// Spinner shown during load the TableView
let spinner = UIActivityIndicatorView()
/// Text shown during load the TableView
let loadingLabel = UILabel()
// MARK: Methods
override func viewDidLoad() {
super.viewDidLoad()
setLoadingScreen()
loadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rows.count
}
override func tableView(tableView: UITableView, cellForRowAt indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath)!
cell.textLabel!.text = rows[indexPath.row]
return cell
}
// MARK: Private methods
// Load data in the tableView
private func loadData() {
// Simulate a delay of some operations as a HTTP Request
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(20)) {
self.rows = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]
self.tableView.reloadData()
self.tableView.separatorStyle = .singleLine
self.removeLoadingScreen()
}
}
// Set the activity indicator into the main view
private func setLoadingScreen() {
// Sets the view which contains the loading text and the spinner
let width: CGFloat = 120
let height: CGFloat = 30
let x = (tableView.frame.width / 2) - (width / 2)
let y = (tableView.frame.height / 2) - (height / 2) - (navigationController?.navigationBar.frame.height)!
loadingView.frame = CGRect(x: x, y: y, width: width, height: height)
// Sets loading text
loadingLabel.textColor = .gray
loadingLabel.textAlignment = .center
loadingLabel.text = "Loading..."
loadingLabel.frame = CGRect(x: 0, y: 0, width: 140, height: 30)
// Sets spinner
spinner.activityIndicatorViewStyle = .gray
spinner.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
spinner.startAnimating()
// Adds text and spinner to the view
loadingView.addSubview(spinner)
loadingView.addSubview(loadingLabel)
tableView.addSubview(loadingView)
}
// Remove the activity indicator from the main view
private func removeLoadingScreen() {
// Hides and stops the text and the spinner
spinner.stopAnimating()
spinner.isHidden = true
loadingLabel.isHidden = true
}
}

Here's the steps: Followed Chrissukhram instructions.
drag a view on top of table view Step
drag activity
indicator on to the view
Align the activity indicator to
horizontal and vertical centre in container
make IBOutlet
connection for both view and activity indicator
start the
activity indicator
stop and hide : activityIndicatorLarge.hidesWhenStopped = true
activityView.hidden = true

The easiest way is to add a UIActivityIndicatorView in your tableView backgroundView
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.startAnimating()
tableView.backgroundView = spinner
it will be centered by default.

If you use complex view:
func setupSpinner(){
spinner = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 40, height:40))
spinner.color = UIColor(Colors.Accent)
self.spinner.center = CGPoint(x:UIScreen.main.bounds.size.width / 2, y:UIScreen.main.bounds.size.height / 2)
self.view.addSubview(spinner)
spinner.hidesWhenStopped = true
}

If this activity indicator is for loading information into the UITableView you should create a separate loading UIView that is the same size as the UITableView and place the Activity Indicator in the center of that view, not the UITableView itself.
Then add the loading UIView to your main view (above the table view) and set it to hidden or visible depending on the status of the loading.

I think you used directly UITableView in your storyboard. This is why you can't drag uiview top of the main view(tableview). Delete UItableview controller and drag a UIViewController into the storyboard, instead. Then drag a tableview into it. then drag another view over tableview for activity indicator. don't forget to handle UItableviewdelegate and datasourse methods in your UIViewController.

In the viewDidLayoutSubviews method of your tableViewController try this:
override func viewDidLayoutSubviews() {
let screenSize = UIScreen.main.bounds
activityIndicator.frame = CGRect(x: screenSize.width / 2, y: screenSize.height / 2, width: 5, height: 5)
activityIndicator.color = .white
activityIndicator.hidesWhenStopped = true
if let baseView = view.superview {
baseView.addSubview(activityIndicator)
}
}
activityIndicator is a stored property of my TableViewController class.

Related

UICollectionView: Resizing cells with animations and custom layouts

I have a collection view with a custom layout and a diffable datasource. I would like to resize the cells with an animation.
With a custom layout: it looks like the content of the cells scaled to the new size during the animation. Only when the cells reaches its final size, it is finally redrawn (see animation below)
With a stock flow layout the subviews are laid out correctly during the animation: the label remains at the top, no resizing and same for the switch.
So I would assume that the issue is in the layout implementation, what could be added to the layout allow a correct layout during the animation?
Description
The custom layout used in this example is FSQCollectionViewAlignedLayout, I also got it with the custom I used in my code. I noticed that for some cases, it is important to always return the same layout attributes objects, which FSQCollectionViewAlignedLayout doesn't do. I modified it to not return copies, and the result is the same.
For the cells (defined in the xib), the contentMode of the cell and its contentView are set to .top. The cell's contentView contains 2 subviews: a label (laid out in layoutSubviews) and a switch (laid out with constraints, but its presence has no effect on the animation).
The code of the controller is:
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout {
enum Section {
case main
}
#IBOutlet weak var collectionView : UICollectionView!
var layout = FSQCollectionViewAlignedLayout()
var dataSource : UICollectionViewDiffableDataSource<Section, String>!
var cellHeight : CGFloat = 100
override func loadView() {
super.loadView()
layout.defaultCellSize = CGSize(width: 150, height: 100)
collectionView.setCollectionViewLayout(layout, animated: false) // uncommented to use the stock flow layout
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { cv, indexPath, item in
guard let cell = cv.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { return nil }
cell.label.text = item
return cell
})
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])
snapshot.appendItems(["1", "2"], toSection: .main)
dataSource.apply(snapshot)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: cellHeight)
}
#IBAction func resizeAction(_ sender: Any) {
if let layout = collectionView.collectionViewLayout as? FSQCollectionViewAlignedLayout {
UIView.animate(withDuration: 0.25) {
layout.defaultCellSize.height += 50
let invalidation = FSQCollectionViewAlignedLayoutInvalidationContext()
invalidation.invalidateItems(at: [[0, 0], [0, 1]])
layout.invalidateLayout(with: invalidation)
self.view.layoutIfNeeded()
}
}
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
UIView.animate(withDuration: 0.25) {
self.cellHeight += 50
let invalidation = UICollectionViewFlowLayoutInvalidationContext()
invalidation.invalidateItems(at: [[0, 0], [0, 1]])
layout.invalidateLayout(with: invalidation)
self.view.layoutIfNeeded()
}
}
}
}
class Cell : UICollectionViewCell {
#IBOutlet weak var label: UILabel!
override func layoutSubviews() {
super.layoutSubviews()
label.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 30)
}
}
I spent a developer support ticket on this question. The answer was that custom animations when reloading cells are not supported.
So as a workaround, I ended up creating new cells instances that I add to the view hierarchy, then add autolayout constraints so that they overlap exactly the cells being resized. The animation is then run on those newly added cells. At the end of the animation, these cells are removed.

Adding images to table view cell problem using stack views

in my prototype cell I have a horizontal stack view and I connected that to my UITableViewCell and I defined an update function In my UITableViewCell that adds multiple images to the stack view and I called the function In TableViewController cellForRowAt but nothing nothing happens.
//inside UITableViewCell
#IBOutlet weak var lineStack: UIStackView!
func update(_ imageName: String){
let numberOfCopies = Int(deviceWidth/50)
let startX = (Int(deviceWidth) - (numberOfCopies*50))/2
xValue = xValue + startX
let image = UIImage(named: imageName)
for _ in 1...Int(numberOfCopies) {
xValue = xValue + heightValue
let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: xValue , y: 0, width: widthValue, height: heightValue)
lineStack.addSubview(imageView)
}
}
//inside TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Line", for: indexPath) as! LineTableViewCell
let imageName = imagesName[indexPath.row]
cell.update(imageName)
return cell
}
Your post indicates you want your images to be 50 x 50 points... use auto-layout constraints with .addArrangedSubview():
class TestCell: UITableViewCell {
#IBOutlet var lineStack: UIStackView!
func update(_ imageName: String) {
let numberOfCopies = Int(deviceWidth/50)
let image = UIImage(named: imageName)
for _ in 1...Int(numberOfCopies) {
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
lineStack.addArrangedSubview(imageView)
}
}
}
EDIT Sample result, with stack view centered horizontally:
Stack view is set to:
Axis: Horizontal
Alignment: Fill
Distribution: Fill
Spacing: 0
and this is the layout with constraints:
Note that the stack view has a Width constraint of 10, but its Priority is 250 ... that allows the stack view to stretch horizontally, while keeping IB satisfied with the constraints.
For the line:
lineStack.addSubview(imageView)
Instead of addSubview() you need to do it with addArrangedSubview(). The former is the regular way of adding a subview to a view, whereas the latter is specifically for UIStackView and tells the view to insert it properly.

Making a collection view scroll out side menu swift

I've looked all over the place for an answer and can't find one. I'm creating an app that has a menu that pops out from the left side using a button and animations. The menu itself is a collection view. Currently when the menu button is pressed, the entire collection view pops out from the left side. I want to do this same thing, but I want the collection view to respond to a swipe from left to right. Please keep in mind I don't want the individual things INSIDE the collection view to move, but the collection view itself as a whole menu. How would I go about doing this?
Good evening!
If i understand your question correctly. You simply have a button which you want to use to animate a slide in of a UICollectionView. Here is what I would do:
Create a new collection view class: (if you want a different frame size of the collectionView remember to change it in both animations as well as the initial setup in showSlideCollectionView
import UIKit
class CollectionViewLauncher : NSObject {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.white
return cv
}()
// reuseidentifier for the cells in the collectionview
let cellId = "cellId"
var viewController: UIViewController?
func showSlideCollectionView() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(collectionView)
let width: CGFloat = window.frame.width * 0.55
//this is the original frame for the collectionview (so it is off screen by default)
collectionView.frame = CGRect(x: -window.frame.width, y: 0, width: width, height: window.frame.height)
//this animates the collectionview on screen
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
self.collectionView.frame = CGRect(x: 0, y: 0, width: width, height: window.frame.height)
}, completion: nil)
}
}
func hideSlideCollectionView() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: {
//sliding the collectionview off screen
if let window = UIApplication.shared.keyWindow {
self.collectionView.frame = CGRect(x: window.frame.width, y: 0, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}, completion: nil)
}
override init() {
super.init()
collectionView.dataSource = self
collectionView.delegate = self
//register your own cell class type here. The code wont work unless you create a cell class of type UICollectionViewCell and customise it there and then register the cell here. (you can also create a nib file and register that.
collectionView.register(YourCell.self, forCellWithReuseIdentifier: cellId)
}
}
extension CollectionViewLauncher: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! YourCell //your cell class
//setup what you need in the cells
return cell
}
//add additional UICollectionView Data source methods if you need further customisation of the collectionview.
}
Register your cell class in the overrode init() method inside the CollectionViewLauncher.
In the your ViewController do the following.
lazy var collectionViewLauncher : CollectionViewLauncher = {
let launcher = CollectionViewLauncher()
launcher.viewController = self
return launcher
}()
//in your button action you call the following to animate the collection view to appear.
collectionViewLauncher.showSlideCollectionView()
//when you want it to dissapear call the following to animate it away (perhaps in a UITapGestureRecognizer or something).
collectionViewLauncher.hideSlideCollectionView()
//example button action
#objc func yourButtonsAction(sender: UIButton) {
collectionViewLauncher.showSlideCollectionView()
}
Edit:
If you have data that you need to pass to this collection view from your view controller i would recommend implementing it directly in the same class as your view controller instead. (Just do exactly the same but put the stuff in the init() method in viewDidLoad instead and the rest within the ViewController class).
If you want it to be slidable using a drag gesture instead. You should use the approach in the edit here and then use a pan gesture to move it instead of the show/hideSlideCollectionView methods.
I hope this helps.

avoiding retain cycles for UITableView done programmatically from a ViewController that is a child of another ViewController

I have a ViewController named MainViewController that acts as the central page for the app. This MainViewController has 6 properties in it that are instances of other viewControllers
lazy var shelvesView: ShelvesViewController = {
return ShelvesViewController()
}()
lazy var goalsView: GoalsViewController = {
return GoalsViewController()
}()
lazy var shoppingView: ShoppingListViewController = {
return ShoppingListViewController()
}()
lazy var tipsView: TipsViewController = {
return TipsViewController()
}()
lazy var myDenView: MyDenViewController = {
return MyDenViewController()
}()
lazy var settingsview: SettingsViewController = {
return SettingsViewController()
}()
when the mainViewController loads it starts out with the shelvesViewController laid over it just underneath the mainViewControllers custom NavBar like so
func setupShelvesView() {
shelvesView.willMove(toParentViewController: self)
addChildViewController(shelvesView)
self.view.addSubview(shelvesView.view)
shelvesView.view.frame = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
shelvesView.didMove(toParentViewController: self)
globalCurrentView = 1
}
I also have a menu that slides over this mainViewController with a list of different pages the user can navigate to. when the user switches to a new page, say the goalsViewController, the shelvesViewController will be animated off screen, removed from the parentViewController(MainViewController) and the goalsViewController will be initialized, moved to the MainViewController and animated onscreen in the same frame as the shelvesView. Anytime i switch a VC from the menu i use this method.
func changeVCfrom(OldVC oldVC: UIViewController, newVC: UIViewController) {
let newStartFrame = CGRect(x: 0 + self.view.frame.width, y: 0, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let newEndframe = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let oldfinishFrame = CGRect(x: 0 - self.view.frame.width, y: 0, width: self.view.frame.width, height: self.view.frame.height)
oldVC.willMove(toParentViewController: nil)
self.addChildViewController(newVC)
newVC.view.frame = newStartFrame
transition(from: oldVC, to: newVC, duration: 0.2, options: [.curveEaseOut], animations: {
oldVC.view.frame = oldfinishFrame
newVC.view.frame = newEndframe
}, completion: { (success) in
oldVC.willMove(toParentViewController: nil)
oldVC.view.removeFromSuperview()
oldVC.removeFromParentViewController()
newVC.willMove(toParentViewController: self)
self.view.addSubview(newVC.view)
})
}
the problem here is that i have done all my views programmatically and any of the above viewControllers(lazy properties) that have a tableView propery on them have a memory leak. any time the VC goes off the MainViewController the memory used for the tableView is not being dealocated and anytime it comes back on the MainViewController is is being allocated again for more memory.
as of right now this is how ive been setting up my tableViews
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupObjects()
tableView.register(ShelfTableViewCell.self, forCellReuseIdentifier: "shelfCell")
}
func setupTableView() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
setupTableViewConstraints()
}
func setupTableViewConstraints() {
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
I understand that I need to have my tableviews either weak, unowned or somehow get the tableview to completely deallocate when off screen. but whatever i try I get errors all over the place that i cant resolve or my ViewController is not able to load the tableview. I apologize for the lengthy question, but any help on this would be very much appreciated.
The table views are not getting deallocated when your child view controllers go off screen.
Your MainViewController owns those other view controllers. Yes, they're lazy, but once they're called for the first time they are initialized along with the tableviews they own.
The child view controllers are not deallocated by simply moving them offscreen. All you're doing is changing the frame and removing them as a child view controller of MainViewController. Since the they're still in memory, the table view is also still in memory.
If you really want to deallocate the table view every time the controller moves off screen, you can just set
self.tableView = nil
There shouldn't be a retain cycle here because the tableView's datasource and delegate properties are weak, meaning that they don't increase the reference count of the view controller.
while scrolling up and down on the tableView my memory would go up sharply. Turns out i had set up the dequeReusableCell wrong.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}
essentially what was happening was every time the viewWillAppear was hit i reloaded the tableViewData which ran this code
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
which was causing the memory leak
replacing the above code with the following resolved the issue
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath) as! ShelfTableViewCell
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}

iOS - add image and text in title of Navigation bar

I would like to create a nav bar similar to what's in the image that's attached.
The title of the nav bar will be a combination of an image and text.
Should this be done per any best practice?
How can it be done?
As this answer shows, the easiest solution is to add the text to your image and add that image to the navigation bar like so:
var image = UIImage(named: "logo.png")
self.navigationItem.titleView = UIImageView(image: image)
But if you have to add text and an image separately (for example, in the case of localization), you can set your navigation bar's title view to contain both image and text by adding them to a UIView and setting the navigationItem's title view to that UIView, for example (assuming the navigation bar is part of a navigation controller):
// Only execute the code if there's a navigation controller
if self.navigationController == nil {
return
}
// Create a navView to add to the navigation bar
let navView = UIView()
// Create the label
let label = UILabel()
label.text = "Text"
label.sizeToFit()
label.center = navView.center
label.textAlignment = NSTextAlignment.Center
// Create the image view
let image = UIImageView()
image.image = UIImage(named: "Image.png")
// To maintain the image's aspect ratio:
let imageAspect = image.image!.size.width/image.image!.size.height
// Setting the image frame so that it's immediately before the text:
image.frame = CGRect(x: label.frame.origin.x-label.frame.size.height*imageAspect, y: label.frame.origin.y, width: label.frame.size.height*imageAspect, height: label.frame.size.height)
image.contentMode = UIViewContentMode.ScaleAspectFit
// Add both the label and image view to the navView
navView.addSubview(label)
navView.addSubview(image)
// Set the navigation bar's navigation item's titleView to the navView
self.navigationItem.titleView = navView
// Set the navView's frame to fit within the titleView
navView.sizeToFit()
Use horizontal UIStackView should be much cleaner and easier
Please add the next extension to UIViewController
extension UIViewController {
func setTitle(_ title: String, andImage image: UIImage) {
let titleLbl = UILabel()
titleLbl.text = title
titleLbl.textColor = UIColor.white
titleLbl.font = UIFont.systemFont(ofSize: 20.0, weight: .bold)
let imageView = UIImageView(image: image)
let titleView = UIStackView(arrangedSubviews: [imageView, titleLbl])
titleView.axis = .horizontal
titleView.spacing = 10.0
navigationItem.titleView = titleView
}
}
then use it inside your viewController:
setTitle("yourTitle", andImage: UIImage(named: "yourImage"))
(this will align the text and the icon together to the center, if you want the text to be centered and the icon in the left, just add an empty UIView with width constraint equal to the icon width)
here is my 2 cents for Swift 4, since accepted answer didn't work for me (was mostly off the screen):
// .. in ViewController
var navBar = CustomTitleView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// =================== navBar =====================
navBar.loadWith(title: "Budget Overview", leftImage: Images.pie_chart)
self.navigationItem.titleView = navBar
}
class CustomTitleView: UIView
{
var title_label = CustomLabel()
var left_imageView = UIImageView()
override init(frame: CGRect){
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
setup()
}
func setup(){
self.addSubview(title_label)
self.addSubview(left_imageView)
}
func loadWith(title: String, leftImage: UIImage?)
{
//self.backgroundColor = .yellow
// =================== title_label ==================
//title_label.backgroundColor = .blue
title_label.text = title
title_label.font = UIFont.systemFont(ofSize: FontManager.fontSize + 5)
// =================== imageView ===================
left_imageView.image = leftImage
setupFrames()
}
func setupFrames()
{
let height: CGFloat = Navigation.topViewController()?.navigationController?.navigationBar.frame.height ?? 44
let image_size: CGFloat = height * 0.8
left_imageView.frame = CGRect(x: 0,
y: (height - image_size) / 2,
width: (left_imageView.image == nil) ? 0 : image_size,
height: image_size)
let titleWidth: CGFloat = title_label.intrinsicContentSize.width + 10
title_label.frame = CGRect(x: left_imageView.frame.maxX + 5,
y: 0,
width: titleWidth,
height: height)
contentWidth = Int(left_imageView.frame.width)
self.frame = CGRect(x: 0, y: 0, width: CGFloat(contentWidth), height: height)
}
var contentWidth: Int = 0 //if its CGFloat, it infinitely calls layoutSubviews(), changing franction of a width
override func layoutSubviews() {
super.layoutSubviews()
self.frame.size.width = CGFloat(contentWidth)
}
}
Swift 4.2 + Interface Builder Solution
As a follow-on to Lyndsey Scott's answer, you can also create a UIView .xib in Interface Builder, use that to lay out your title and image, and then update it on-the-fly via an #IBOutlet. This is useful for dynamic content, internationalization, maintainability etc.
Create a UIView subclass with a UILabel outlet and assign your new .xib to this class:
import UIKit
class FolderTitleView: UIView {
#IBOutlet weak var title : UILabel!
/// Create an instance of the class from its .xib
class func instanceFromNib() -> FolderTitleView {
return UINib(nibName: "FolderTitleView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! FolderTitleView
}
}
Connect the label to your outlet (title in my example) in your .xib, then in your UIViewController:
/// Reference to the title view
var folderTitleView : FolderTitleView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Set the screen title to match the active folder
updateTitle()
}
/// Updates the title of the navigation controller.
func updateTitle() {
self.title = ""
if folderTitleView == nil {
folderTitleView = FolderTitleView.instanceFromNib()
self.navigationItem.titleView = folderTitleView
}
folderTitleView!.title.text = "Listening"
folderTitleView!.layoutIfNeeded()
}
This results in a nice self-centering title bar with an embedded image that you can easily update from code.
// worked for me
create a view and set the frame
now add the image in the view and set the frame
after adding the image, add the label in same view and set the frame
after adding the image and label to view, add same view to navigationItem
let navigationView = UIView(frame: CGRect(x: 0, y: 0, width: 50 , height: 55))
let labell : UILabel = UILabel(frame: CGRect(x: -38, y: 25, width: 150, height: 25))
labell.text = "Your text"
labell.textColor = UIColor.black
labell.font = UIFont.boldSystemFont(ofSize: 10)
navigationView.addSubview(labell)
let image : UIImage = UIImage(named: ValidationMessage.headerLogoName)!
let imageView = UIImageView(frame: CGRect(x: -20, y: 0, width: 100, height: 30))
imageView.contentMode = .scaleAspectFit
imageView.image = image
//navigationItem.titleView = imageView
navigationView.addSubview(imageView)
navigationItem.titleView = navigationView