Change NavigationBar Size with AdditionalSafeAreaInsets

Jun 21, 2020

Download

If you ever want something like this: Simulator Screen Shot - iPhone 11 Pro Max - 2020-06-22 at 15.04.55 Screen Shot 2563-06-22 at 15.04.04 simply add this line to your viewController

  navigationController?.additionalSafeAreaInsets.top = 100 
  // where 100 is the extra space

To see the code in action, I attached the following function to the BLUE slider

  @IBAction func sliderSlided(_ sender: Any) {
        let top: CGFloat = (CGFloat(200 * ((sender as? UISlider)?.value ?? 1) - 0.5))
        navigationController?.additionalSafeAreaInsets.top = top
  }

ezgif-3-6d10af549716

Also notice that when the NavigationBar changes, the slideIndicator changes too, indicating change in the view's size.

I also add a RED slider with the following function:

    @IBAction func slider2Slided(_ sender: Any) {
        let top: CGFloat = (CGFloat(100 * ((sender as? UISlider)?.value ?? 1)))
        additionalSafeAreaInsets.top = top
    }

ezgif-3-060fd2f92351

Notice how RED slider resizes both the content and the Navigation Bar. Moiving the BLUE slider after the RED to configure the navigationBar.

ezgif-3-6bb323653a37

AdditionalSafeAreaInsets is traditionally use to change the safeAreaInset of the ViewController.

My take on Nested UINavigation

Apr 12, 2020

🎇🎇🎆 One day I just needed to push a UINavigation from a UINavigation, well, easy. Nested Navigation

@IBAction func push_nav(_ sender: Any) {
  let navVC = UINavigationController(rootViewController: ViewController())  
  navigationController?.pushViewController(navVC, animated: true)
}

big red crash reason: ‘Pushing a navigation controller is not supported’

Let’s start over and from the anatomy of how this project will work. (We will not use Storyboard’s Segue, it’s here just to paint the picture) Or download the finish code.

Navigation Blueprint

We still use UINavigationController as the parent but our children are more complex. Let’s focus on the brown ViewController.

  • BrownVC (or NestedViewController) has an embed ViewController with UINavigationController as the embed VC.
  • UINavigationController will present a VC with button “1”
  • Button “1” will create another NestedViewController, blue one, and push BlueVC on to the Root UINavigation.
  • We can repeat button “1” action multiple times.

We start by creating a new UINavigationController. I named it NavViewController

  class NavViewController: UINavigationController {
    override func viewDidLoad() {
    super.viewDidLoad()  
    // we’ll work here in a bit  
  }

Create another subclass of UIViewController.

import UIKit  class NestedViewController: UIViewController {
  var rootVC: UIViewController  
  weak var rootNavigation: UINavigationController?  
  // 1  
  init(rootNavigation: UINavigationController) {  
    guard let vc = rootNavigation.viewControllers.first else { 
      fatalError(“root has not been initialized”)  
    }  
    self.rootVC = vc  
    self.rootNavigation = rootNavigation  
    super.init(nibName: nil, bundle: nil)  
  }  
  required init?(coder: NSCoder) {  
    fatalError(“init(coder:) has not been implemented”)  
  }  
  override func viewDidLoad() {  
    super.viewDidLoad()  
    // 2  
    let childNavigation = rootNavigation ?? UINavigationController(rootViewController: rootVC)  
    childNavigation.willMove(toParent: self)  
    addChild(childNavigation)  
    childNavigation.view.frame = view.frame  
    view.addSubview(childNavigation.view)  
    childNavigation.didMove(toParent: self)  
    childNavigation.navigationBar.isTranslucent = false  
    // 3  rootVC.navigationItem.leftBarButtonItem = 
    UIBarButtonItem(barButtonSystemItem: .close, target: nil, action: #selector(back))  
  }  
  @IBAction func back() {
    navigationController?.popViewController(animated: true)  
  }  
}
  1. We create a new init method, taking in an UINavigationController and assign the UINavigationController to variable.

  2. In viewDidLoad() we insert UINavigationController’s view to NestedViewController.

  3. Add a back button (optional)

open ViewController.swift and insert following method

@IBAction func new_nav(_ sender: Any) {
  let cvc = UIStoryboard(name: “Main”, bundle: nil).instantiateViewController(identifier: “ViewController”)  
  let nestedVC = NestedViewController(rootNavigation: UINavigationController(rootViewController: cvc))  
  var nav = navigationController  
  while ((nav?.navigationController) != nil) {  
    nav = nav?.navigationController  
  }  
  DispatchQueue.main.async {  
    nestedVC.rootVC.navigationController?.navigationBar.barTintColor = UIColor.random()  
  }  
  nav?.pushViewController(nestedVC, animated: true)  
}

Open Main.storyboard and drag in a UINavigationController. Make the UINavigationController the initial ViewController and set the root child controller to ViewController

Storyboard1

Drag in a new button to ViewController and assign touch up inside to new_nav.

Storyboard2

Run and enjoy.

For now, our app will not accept slide back gesture. We will need to configure interactivePopGestureRecognizer.

Open NavViewController.swift and add the follow code to bottom of the file

final class AlwaysPoppableDelegate: NSObject, UIGestureRecognizerDelegate {
  weak var navigationController: UINavigationController?  
  weak var originalDelegate: UIGestureRecognizerDelegate?  
  override func responds(to aSelector: Selector!) -> Bool {  
    if aSelector == #selector(gestureRecognizer(_:shouldReceive:)) { 
      return true 
    } else if let responds = originalDelegate?.responds(to: aSelector) {  
      return responds  
    } else {  
      return false  
    }  
  }  
  override func forwardingTarget(for aSelector: Selector!) -> Any? {  
    return originalDelegate  
  }  
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {  
    return  true  
  }  
}  

protocol Nested {  
  var canNestedSwipeBack: Bool {get set}  
}

Declare a new variable for NavViewController

private let alwaysPoppableDelegate = AlwaysPoppableDelegate()

Inside NavViewController’s viewDidLoad() add these code

setNavigationBarHidden(true, animated: false)  
self.navigationBar.isOpaque = true  
alwaysPoppableDelegate.navigationController = self  
interactivePopGestureRecognizer?.delegate = alwaysPoppableDelegate

Here we assign a new interactivePopGestureRecognizer. Build and run.

The swipe back’s working now but if you’re viewing the completed code you’ll notice another block inside AlwaysPoppableDelegate. Go back to AlwaysPoppableDelegate’s gestureRecognizer and change the inside to.

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {  
  if let nav = navigationController, nav.isNavigationBarHidden, nav.viewControllers.count > 1 {  
    // extra for nested nav viewcontrollers  
    if let nested = (nav.viewControllers.last as? Nested) {  
      return nested.canNestedSwipeBack  
    } else if let nestedVC = (nav.viewControllers.last as? NestedViewController) {  
      return (nestedVC.rootNavigation?.viewControllers.count ?? 0) <= 1  
    }  return true  
  } else if let result = originalDelegate?.gestureRecognizer?(gestureRecognizer, shouldReceive: touch) {  
    return result  
  } else {  
    return false  
  }  
}

gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) is fired when we are performing the swipe back gesture. We override the method to check for nested child array. To see this for yourself, add another button to ViewController and assign it to this method:

@IBAction func same_nav(_ sender: Any) {  
  let cvc = UIStoryboard(name: “Main”, bundle: nil).instantiateViewController(identifier: “ViewController”)  
  navigationController?.pushViewController(cvc, animated: true)  
}

We are presenting another ViewController inside the nested UINavigationController but if we swipe back, there’s a chance that the whole stack will be removed. Our new gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) checks for this and cancel the touch that will remove a whole stack.

Build and run and ask any questions.

Dynamic Type and View Layout

Aug 8, 2019

👓🌳 image 1*hNcTzdrygIGoIV09 RB9qQ Replicating Apple’s Dynamic Type just like in Apple stock apps. Download Project Apple enables user to change the font size of there app and Apple app set a good example in there app such as Music and Contact. You can play around with the setting by going in Setting > Accessibility > Larger Text then flip the switch and drag the bar to maximum. When you go back to Apple Music app, the layout will change. image before and after We begin by creating a new project. In storyboard we put a nested stackView. Outside stackView contains imageView and a stackView with all the lables. Put constraints accordingly. image We also constraint imageView with aspect ratio and width. image Connect outside stackView to its viewController. image Then we add the following code

////  ViewController.swift
//import UIKitclass ViewController: UIViewController {    
    @IBOutlet weak var stackView: UIStackView!    
    override func viewDidLoad() {        
    super.viewDidLoad()        
    // Do any additional setup after loading the view.    
  }    
  //1    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)                //2        
    if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {            
      let contentSize = traitCollection.preferredContentSizeCategory
      if contentSize.isAccessibilityCategory {                
        stackView.axis = .vertical                
        stackView.alignment = .leading                
        stackView.spacing = 2            
      } else {
        stackView.axis = .horizontal                
        stackView.alignment = .center                
        stackView.spacing = 6            
      }      
    }    
  }
}

We add a

override func traitCollectionDidChange(_previousTraitCollection:UITraitCollection?)

To detect when font size changes. We check whether user is using accessibility font scale and change our stackView accordingly. You can use Xcode’s Accessibility Inspector to control Font size on your simulator. image 1*8rBD5SsNKhkdT-Bs u5e7A This post is inspired by UseYourLoaf’s blog

UISearchBar and your UI

Jul 2, 2019

1*xFFtWEQHa276oEAtmjeh6A 🔭🔼📡

Download the finish project.

Let’s say you have a wonderful ViewController and you want to add a search bar with dropdown to it. Apple provides you with a tool: UISearchBar. This tutorial will teach you to put UISearchBar and still have that wonderful layout you had. Today we’ll implement a basic level UISearchBar to get you going. Let’s start by creating a new 1-Page project and tangle a bit with the storyboard. We will add a searchbar later. This is how my looks like: image

nice and clean simple storyboard Add in a new View, this will be the container for our SearchBar. We will use UISearchBar to make our storyboard easier to read. image Connect an outlet of the newly created View to your ViewController. image Create a new swift file, change the class to UITableViewController, then name it anything that make sense to you. This new UITableViewController will be show on the SearchViewController. image

Insert just enough code to present a tableViewController.

import UIKitclass SearchTableViewController: UITableViewController {    
  var filterData: [String]!    
  override func viewDidLoad() {        
    super.viewDidLoad()    
  }    
// MARK: - Table view data source    
  override func numberOfSections(in tableView: UITableView) -> Int {    
    return 1   
  }    
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {        
    return filterData.count    
  }        
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {        
      let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
      cell.textLabel?.text = filterData[indexPath.row]        
      return cell    
    }    
  }

Add the following code to ViewController.

import UIKitclass ViewController: UIViewController {    
  @IBOutlet weak var searchBar: UISearchBar!        
  /// Search controller to help us with filtering.    
  private var searchController: UISearchController!        
  /// Secondary search results table view.    
  private var resultsTableController: SearchTableViewController!
  override func viewDidLoad() {        
    super.viewDidLoad()                
    resultsTableController = SearchTableViewController()
    resultsTableController.tableView.delegate = self                
    // 1        
    searchController = UISearchController(searchResultsController: resultsTableController)        
    searchController.searchResultsUpdater = self        
    searchController.searchBar.autocapitalizationType = .none
    // replace searchBar with searchController's searchbar
    searchBar.addSubview(searchController.searchBar)
    searchController.delegate = self
    searchController.dimsBackgroundDuringPresentation = false 
    // The default is true.        
    searchController.searchBar.delegate = self 
    // Monitor when the search button is tapped.                
    // set true to make searchbar moves to navigatoion        
    searchController.hidesNavigationBarDuringPresentation = false
    /** Search presents a view controller by applying normal view controller presentation semantics.         This means that the presentation moves up the view controller hierarchy until it finds the root         view controller or one that defines a presentation context.         */
    /** Specify that this view controller determines how the search controller is presented.         The search controller should be presented modally and match the physical size of this view controller.         */
    definesPresentationContext = true            
    }
  }
  // MARK: - UITableViewDelegateextension 
  ViewController: UITableViewDelegate {        
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {        
      let selectedString: String        
      // 2         
      // Check to see which table view cell was selected.        
      if tableView === resultsTableController.tableView {
      selectedString = resultsTableController.filterData[indexPath.row]
    } else {            
      selectedString = ""        
    }                
    print(selectedString)                
    tableView.deselectRow(at: indexPath, animated: false)    
  }    
}
// MARK: - UISearchBarDelegate
extension ViewController: UISearchBarDelegate {
  func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    searchBar.resignFirstResponder()
  }
}
// MARK: - UISearchControllerDelegate
// Use these delegate functions for additional control over the search controller.
extension ViewController: UISearchControllerDelegate {
   func presentSearchController(_ searchController: UISearchController) {
    // 3        
    // to move table under searchbar
    resultsTableController.view.frame = CGRect(x: 0, y: searchBar.frame.maxY, width: self.view.frame.width, height: self.view.frame.height - searchBar.frame.maxY)
    debugPrint("UISearchControllerDelegate invoked method: \(#function).")
  }
  func willPresentSearchController(_ searchController: UISearchController) {
    debugPrint("UISearchControllerDelegate invoked method: \(#function).")
  }
  func didPresentSearchController(_ searchController: UISearchController) {
    // to move content to correct location
    resultsTableController.tableView.contentInset = UIEdgeInsets(top: searchBar.frame.size.height, left: 0, bottom: 0, right: 0)
    debugPrint("UISearchControllerDelegate invoked method: \(#function).")
  }
  func willDismissSearchController(_ searchController: UISearchController) {
    debugPrint("UISearchControllerDelegate invoked method: \(#function).")
  }
  func didDismissSearchController(_ searchController: UISearchController) {
    debugPrint("UISearchControllerDelegate invoked method: \(#function).")
  }    
}
// MARK: - UISearchResultsUpdating
extension ViewController: UISearchResultsUpdating {    
  func updateSearchResults(for searchController: UISearchController) {        // Update the filtered array based on the search text.        
  let searchResults = ["abcde","cdefg","efghi","ghijk"]                
  // Strip out all the leading and trailing spaces.        
  let whitespaceCharacterSet = CharacterSet.whitespaces        
  let strippedString =            searchController.searchBar.text!.trimmingCharacters(in: whitespaceCharacterSet)                
  let filteredResults = searchResults.filter { $0.contains(strippedString) }
  // Apply the filtered results to the search results table.        
  if let resultsController = searchController.searchResultsController as? SearchTableViewController {            
  resultsController.filterData = filteredResults            
  resultsController.tableView.reloadData()        
  }    
 }    
}

Create a new SearchBarViewController. Add a searchbar to our view. Connect SearchBarViewController to our SearchTableViewController. SearchTableViewController datasource. Move SearchBarViewController tableView right under the searchBar.

image

Build and run. — — — — — — — — — — — — — — — — — — — — — — — — — To improve: Make SearchTableViewController a ViewController. Animation.

Confusians

© 2015 - 2021