Skip to content

Instantly share code, notes, and snippets.

@maysamsh
Last active March 14, 2022 01:07
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save maysamsh/fd06dd7bb19281229e89116697ebbcb6 to your computer and use it in GitHub Desktop.
Save maysamsh/fd06dd7bb19281229e89116697ebbcb6 to your computer and use it in GitHub Desktop.
A small extension for UISearchBar which shows an UIActivityIndicator while searching
//
// UISearchBar+Ext.swift
// frazeit
//
// Created by Maysam Shahsavari on 7/30/18.
// Updated on 9/26/19.
// Copyright © 2018 Maysam Shahsavari. All rights reserved.
// Updated: 10/02/2020.
import Foundation
import UIKit
extension UIImage {
func imageWithPixelSize(size: CGSize, filledWithColor color: UIColor = UIColor.clear, opaque: Bool = false) -> UIImage? {
return imageWithSize(size: size, filledWithColor: color, scale: 1.0, opaque: opaque)
}
func imageWithSize(size: CGSize, filledWithColor color: UIColor = UIColor.clear, scale: CGFloat = 0.0, opaque: Bool = false) -> UIImage? {
let rect = CGRect.init(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
color.set()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
extension UISearchBar {
private var textField: UITextField? {
let subViews = self.subviews.flatMap { $0.subviews }
if #available(iOS 13, *) {
if let _subViews = subViews.last?.subviews {
return (_subViews.first { $0 is UITextField }) as? UITextField
} else {
return nil
}
} else {
return (subViews.first { $0 is UITextField }) as? UITextField
}
}
private var searchIcon: UIImage? {
let subViews = subviews.flatMap { $0.subviews }
return ((subViews.first { $0 is UIImageView }) as? UIImageView)?.image
}
private func getViewElement<T>(type: T.Type) -> T? {
let svs = subviews.flatMap { $0.subviews }
guard let element = (svs.first { $0 is T }) as? T else { return nil }
return element
}
func getSearchBarTextField() -> UITextField? {
return getViewElement(type: UITextField.self)
}
func setTextColor(color: UIColor) {
if let textField = getSearchBarTextField() {
textField.textColor = color
}
}
func setTextFieldColor(color: UIColor) {
if let textField = getViewElement(type: UITextField.self) {
switch searchBarStyle {
case .minimal:
textField.layer.backgroundColor = color.cgColor
textField.layer.cornerRadius = 6
case .prominent, .default:
textField.backgroundColor = color
@unknown default:
break
}
}
}
func setPlaceholderTextColor(color: UIColor) {
if let textField = getSearchBarTextField() {
textField.attributedPlaceholder = NSAttributedString(string: self.placeholder != nil ? self.placeholder! : "", attributes: [NSAttributedString.Key.foregroundColor: color])
}
}
///////
private var activityIndicator: UIActivityIndicatorView? {
return textField?.leftView?.subviews.compactMap{ $0 as? UIActivityIndicatorView }.first
}
// Public API
var isLoading: Bool {
get {
return activityIndicator != nil
} set {
let _searchIcon = searchIcon
if newValue {
if activityIndicator == nil {
let _activityIndicator: UIActivityIndicatorView
if #available(iOS 13.0, *) {
_activityIndicator = UIActivityIndicatorView(style: .medium)
} else {
_activityIndicator = UIActivityIndicatorView(style: .gray)
}
_activityIndicator.startAnimating()
_activityIndicator.backgroundColor = UIColor.clear
let clearImage = UIImage().imageWithPixelSize(size: CGSize.init(width: 14, height: 14)) ?? UIImage()
self.setImage(clearImage, for: .search, state: .normal)
textField?.leftViewMode = .always
textField?.leftView?.addSubview(_activityIndicator)
let leftViewSize = CGSize.init(width: 14.0, height: 14.0)
_activityIndicator.center = CGPoint(x: leftViewSize.width/2, y: leftViewSize.height/2)
}
} else {
self.setImage(_searchIcon, for: .search, state: .normal)
activityIndicator?.removeFromSuperview()
}
}
}
}
/*
Usage:
To show the acttivity indicator
searchController.searchBar.isLoading = true
To stop the activity indicator (assuming it will be called when a network or extensive block is finished)
DispatchQueue.main.async {
searchController.searchBar.isLoading = false
}
*/
@antoinepemeja
Copy link

antoinepemeja commented Mar 20, 2019

Hello,

My suggestion to support iOS 10 :

    var isLoading: Bool {
        get {
            
            return activityIndicator != nil
        } set {
            if newValue {
                if activityIndicator == nil {
                    
                    var style: UIActivityIndicatorView.Style = UIActivityIndicatorView.Style.gray
                    var backgroundColor: UIColor = UIColor.white
                    
                    if #available(iOS 11.0, *) {
                        style = UIActivityIndicatorView.Style.white
                        backgroundColor = UIColor.clear
                    }
                    
                    let _activityIndicator = UIActivityIndicatorView(style: style)
                    _activityIndicator.startAnimating()
                    _activityIndicator.backgroundColor = backgroundColor

                    if #available(iOS 11.0, *) {
                        self.setImage(UIImage(), for: .search, state: .normal)
                    }

                    textField?.leftView?.addSubview(_activityIndicator)
                    let leftViewSize = textField?.leftView?.frame.size ?? CGSize.zero
                    _activityIndicator.center = CGPoint(x: leftViewSize.width/2, y: leftViewSize.height/2)
                }
            } else {
                if #available(iOS 11.0, *) {
                    let _searchIcon = searchIcon
                    self.setImage(_searchIcon, for: .search, state: .normal)
                }
                activityIndicator?.removeFromSuperview()
            }
        }
    }

@tushar40
Copy link

tushar40 commented Feb 9, 2020

Yes great extension, thank you.
Unfortunately it doesn't work on iOS 10, it shows a square. Do you have an idea to fix it ?

There is a problem in the line self.setImage(_searchIcon, for: .search, state: .normal)
the image coming from it has accessibilityFrame = (0,0,0,0)
if i use a custom image from _searchIcon, for e.g = _searchIcon = UIImage(systemName: "search")
It is working.

@maysamsh
Copy link
Author

Try the new code, It works fine on iOS11 too.

@onursahindur
Copy link

Yes great extension, thank you.
Unfortunately it doesn't work on iOS 10, it shows a square. Do you have an idea to fix it ?

There is a problem in the line self.setImage(_searchIcon, for: .search, state: .normal)
the image coming from it has accessibilityFrame = (0,0,0,0)
if i use a custom image from _searchIcon, for e.g = _searchIcon = UIImage(systemName: "search")
It is working.

tushar40 is right. @maysamsh great effort! However there is a bug after setting isLoading to false on main queue. The search bar icon is not showing again and disappears after the indicator removed from the screen.

@maysamsh
Copy link
Author

@onursahindur Have you tried the new code?

@onursahindur
Copy link

@maysamsh yes, I tried on Swift 5 with Xcode 11.2.1, on Simulator running iOS 13.2. However after the search icon replaced with the indicator, the search icon disappears from the left side of search field's text field.

@bubudrc
Copy link

bubudrc commented Mar 5, 2022

First all, thanks for the gist and the great solution. I will like to suggest some improvements:

To remove the warning on iOS 13 and use the correct style:

if activityIndicator == nil {
                    let _activityIndicator: UIActivityIndicatorView
                    if #available(iOS 13.0, *) {
                        _activityIndicator = UIActivityIndicatorView(style: .medium)
                    } else {
                        _activityIndicator = UIActivityIndicatorView(style: .gray)
                    }

use first instead of filter

private var textField: UITextField? {
        let subViews = self.subviews.flatMap { $0.subviews }
        if #available(iOS 13, *) {
            if let _subViews = subViews.last?.subviews {
                return (_subViews.first { $0 is UITextField }) as? UITextField
            } else {
                return nil
            }
        } else {
            return (subViews.first { $0 is UITextField }) as? UITextField
        }
    }

    private var searchIcon: UIImage? {
        let subViews = subviews.flatMap { $0.subviews }
        return  ((subViews.first { $0 is UIImageView }) as? UIImageView)?.image
    }

    private func getViewElement<T>(type: T.Type) -> T? {
        let svs = subviews.flatMap { $0.subviews }
        guard let element = (svs.first { $0 is T }) as? T else { return nil }
        return element
    }

remove force

private func setPlaceholderTextColor(color: UIColor) {
        if let textField = getSearchBarTextField() {
            textField.attributedPlaceholder = NSAttributedString(string: self.placeholder ?? "", attributes: [NSAttributedString.Key.foregroundColor: color])
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment