I have a modally presented SearchviewController that contains a UISearchController.
When swiping down it gets deallocated, but only if the searchControllers searchBar is not in editing mode. Only if I press its cancel button in advance, it gets deallocated.
How can I make sure it gets deallocated, even when in editing mode? There are definitely no strong self references within any closures...
Presenting ViewController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addButton()
}
func addButton() {
let mediumConfiguration = UIImage.SymbolConfiguration(scale: .large)
var checkButtonImage = UIImage(systemName: "plus", withConfiguration: mediumConfiguration)
checkButtonImage = checkButtonImage?.withTintColor(.label)
let button = UIButton(type: .contactAdd)
button.addTarget(self, action: #selector(onAddViewControllerButtonClicked(sender:)), for: .touchUpInside)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
#objc func onAddViewControllerButtonClicked(sender: UIButton) {
let viewController = SearchViewController()
viewController.view.backgroundColor = .secondarySystemBackground
let navigationController = UINavigationController()
navigationController.viewControllers = [viewController]
self.present(navigationController, animated: true)
}
}
Presented ViewController:
class SearchViewController: UIViewController, UISearchBarDelegate, UISearchResultsUpdating {
override func viewDidLoad() {
super.viewDidLoad()
configureSearchController()
}
var searchController: UISearchController?
func configureSearchController() {
//search
searchController = UISearchController(searchResultsController: nil)
searchController?.searchResultsUpdater = self
searchController?.searchBar.delegate = self
searchController?.hidesNavigationBarDuringPresentation = false
searchController?.searchBar.searchBarStyle = .minimal
searchController?.searchBar.keyboardType = .webSearch
self.navigationItem.searchController?.searchBar.backgroundColor = .clear
self.navigationItem.searchController = searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.definesPresentationContext = true
self.navigationItem.searchController = searchController
}
func updateSearchResults(for searchController: UISearchController) {
return
}
//check deallocation
deinit { print("\(NSStringFromClass(type(of: self))): deallocated") }
}
Can you help with that?
Thank you in advance!
Adding
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.navigationItem.searchController = nil
}
to SearchViewController fixes the problem for me, but admittedly I have no idea as to why this is necessary.
Related
I want to navigate from one View Controller to another.
let vc = SecondViewController()
I have tried until now :
vc.modalPresentationController = .fullScreen
self.present(vc, animated: true) //self refers to the main view controller
Im trying to open a new ViewController when the users manages to register or to log in.I am new to software developing, and I want to ask, is this the best method to navigate from one ViewController to another, im asking because as I can see the mainViewController is not deinit(). I have found other similar questions and tried the answers, the problem is with the:
self.navigationController?.pushViewController
it doesn't work because I don't have any storyboard.
The question is it is right to navigate as explained above?
Thanks,
Typically when you are doing login you would use neither push or present. There are multiple ways of handling this, but the easiest is to embed in some parent (root) VC. Here is an example:
class ViewController: UIViewController {
private var embeddedViewController: UIViewController! {
didSet {
// https://developer.apple.com/documentation/uikit/view_controllers/creating_a_custom_container_view_controller
// Add the view controller to the container.
addChild(embeddedViewController)
view.addSubview(embeddedViewController.view)
// Create and activate the constraints for the child’s view.
embeddedViewController.view.translatesAutoresizingMaskIntoConstraints = false
embeddedViewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
embeddedViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
embeddedViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
embeddedViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// Notify the child view controller that the move is complete.
embeddedViewController.didMove(toParent: self)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let loginVC = LoginViewController()
loginVC.delegate = self
embeddedViewController = loginVC
}
}
extension ViewController: LoginDelegate {
func didLogin() {
embeddedViewController = MainViewController()
}
}
protocol LoginDelegate: AnyObject {
func didLogin()
}
class LoginViewController: UIViewController {
private lazy var loginButton: UIButton = {
let button = UIButton()
button.setTitle("Login", for: .normal)
button.addTarget(self, action: #selector(didTapLoginButton), for: .touchUpInside)
return button
}()
weak var delegate: LoginDelegate?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(loginButton)
view.backgroundColor = .red
loginButton.translatesAutoresizingMaskIntoConstraints = false
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc private func didTapLoginButton() {
delegate?.didLogin()
}
}
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
}
}
Title is not showing in Simulator
import UIKit
class WelcomeSpotifyViewController: UIViewController {
private let signInButton: UIButton = {
let button = UIButton()
button.backgroundColor = .white
button.setTitle("Sign In with Spotify", for: .normal)
button.setTitleColor(.blue, for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Spotify"
view.backgroundColor = .systemGreen
view.addSubview(signInButton)
signInButton.addTarget(self, action: #selector(didTapSignIn), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
signInButton.frame = CGRect(x:20, y: view.height-50-view.safeAreaInsets.bottom, width: view.width-40, height: 50)
}
#objc func didTapSignIn() {
let vc = SpotifyAuthViewController()
vc.navigationItem.largeTitleDisplayMode = .never
navigationController?.pushViewController(vc, animated: false)
}
}
When I launch the simulator, the title does not show. And when I click the button "Sign In with Spotify," I do not transfer to the SpotifyAuthViewController.
Here is the code for the SpotifyAuthViewController
import UIKit
import WebKit
class SpotifyAuthViewController: UIViewController, WKNavigationDelegate {
private let webView: WKWebView = {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences = prefs
let webView = WKWebView(frame: .zero, configuration: config)
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Sign In"
view.backgroundColor = .systemBackground
webView.navigationDelegate = self
view.addSubview(webView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
webView.frame = view.bounds
}
}
After the user is registered, I want them to be directed to a standard login/register page where they create a username, register with email, and create a password.
import UIKit
class LoginViewController: UIViewController {
private let usernameEmailField: UITextField = {
return UITextField()
}()
private let passwordField: UITextField = {
let field = UITextField()
field.isSecureTextEntry = true
return field
}()
private let loginButton: UIButton = {
return UIButton()
}()
private let createAccountButton: UIButton = {
return UIButton()
}()
private let headerView: UIView = {
return UIView()
}()
override func viewDidLoad() {
super.viewDidLoad()
addSubviews()
view.backgroundColor = .systemBackground
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
handleNotAuthenticated()
//Check auth status
}
private func handleNotAuthenticated() {
if SpotifyAuthManager.shared.isSignedIn == false {
let loginVC = WelcomeSpotifyViewController()
loginVC.modalPresentationStyle = .overCurrentContext
present(loginVC, animated: false)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//assign frames
}
private func addSubviews() {
view.addSubview(usernameEmailField)
view.addSubview(createAccountButton)
view.addSubview(passwordField)
view.addSubview(loginButton)
view.addSubview(headerView)
}
#objc private func didTabLoginButton(){}
#objc private func didTapCreateAccountButton(){}
}
If the user is not connected with Spotify, then they are redirected to the WelcomeSpotifyViewController. Once they enter their username and pass, they are taken to the home page.
import UIKit
import FirebaseAuth
class HomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
handleNotAuthenticated()
//Check auth status
}
private func handleNotAuthenticated() {
if Auth.auth().currentUser == nil {
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .fullScreen
present(loginVC, animated: false)
}
}
}
I have been tinkering with this for days now. I can't seem to figure out the issue. Any help would be really appreciated.
I think your problem is that your WelcomeSpotifyViewController don't have navigaiton controller. You should debug and check value of navigation controller
I have two screens. The first one (firstViewController) has a mapView with a UITapGestureRecognizer. When the user taps the screen, an annotations is added to the map and the second screen (secondViewController) is presented.
When the user dismisses the secondViewController and comes back to the first one, the annotation should be removed. I know I have to use delegation, but I just can't make it to work.
This is the code I have now:
class firstViewController: UIViewController, AnnotationDelegate {
let mapView = MKMapView()
var temporaryPinArray = [MKPointAnnotation]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mapView
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
mapView.addGestureRecognizer(gesture)
secondVC.annotationDelegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
mapView.frame = view.bounds
}
#objc func handleTap(_ gestureReconizer: UILongPressGestureRecognizer) {
let location = gestureReconizer.location(in: mapView)
let coordinates = mapView.convert(location, toCoordinateFrom: mapView)
mapView.removeAnnotations(mapView.annotations)
let pin = MKPointAnnotation()
pin.coordinate = coordinates
temporaryPinArray.removeAll()
temporaryPinArray.append(pin)
mapView.addAnnotations(temporaryPinArray)
// Present secondViewController
let secondVC = SecondViewController()
panel.set(contentViewController: secondVC)
panel.addPanel(toParent: self)
}
func didRemoveAnnotation(annotation: MKPointAnnotation) {
mapView.removeAnnotation(annotation)
}
}
Second View Controller
protocol AnnotationDelegate {
func didRemoveAnnotation(annotation: [MKPointAnnotation])
}
class SecondViewController: UIViewController {
var annotationDelegate: AnnotationDelegate!
let mainVC = firstViewController()
let closeButton: UIButton = {
let button = UIButton()
button.backgroundColor = .grey
button.layer.cornerRadius = 15
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(closeButton)
closeButton.addTarget(self, action: #selector(dismissPanel), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
closeButton.frame = CGRect(x: view.frame.width-50, y: 10, width: 30, height: 30)
}
#objc func dismissPanel() {
self.dismiss(animated: true, completion: nil)
annotationDelegate.didRemoveAnnotation(annotation: mainVC.temporaryPinArray)
}
}
Thank you so much for your help!
You created a new instance of firstViewController inside SecondViewController. This instance is unrelated to the actual first one:
let mainVC = firstViewController()
This means that temporaryPinArray is different as well. So instead of passing in this unrelated array...
#objc func dismissPanel() {
self.dismiss(animated: true, completion: nil)
annotationDelegate.didRemoveAnnotation(annotation: mainVC.temporaryPinArray)
}
Just change the function to take no parameters instead:
protocol AnnotationDelegate {
func didRemoveAnnotation() /// no parameters
}
#objc func dismissPanel() {
self.dismiss(animated: true, completion: nil)
annotationDelegate.didRemoveAnnotation() /// no parameters
}
And inside firstViewController's didRemoveAnnotation, reference the actual temporaryPinArray.
func didRemoveAnnotation() {
mapView.removeAnnotations(temporaryPinArray) /// the current array
}
I'm having some difficulty getting the searchbar in my searchcontroller to become firstResponder. I've noticed the delegate methods are not being called, but the searchbar works as intended when I'm typing to filter a list of users.
Definition of searchcontroller:
lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search"
return searchController
}()
Setting it up:
private func setupSearchController() {
self.navigationItem.searchController = searchController
searchController.definesPresentationContext = true
searchController.delegate = self
searchController.isActive = true
searchController.searchBar.delegate = self
searchController.searchBar.becomeFirstResponder()
}
I tried this suggestion from another SO question but the delegate method isn't being called:
func didPresentSearchController(searchController: UISearchController) {
UIView.animate(withDuration: 0.1, animations: { () -> Void in }) { (completed) -> Void in
searchController.searchBar.becomeFirstResponder()
}
}
The problem is you are trying to access UI element(searchbarcontroller) before UI is completely loaded.
This can be done in 2 ways
Use main queue to show keypad
private func setupSearchController() {
self.navigationItem.searchController = searchController
searchController.definesPresentationContext = true
searchController.delegate = self
searchController.isActive = true
searchController.searchBar.delegate = self
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
With this approach keypad will be only show at one time in viewDidLoad
Show keypad in viewDidAppear
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
With this approach keypad will be always shown whenever screen appears.
I'm implementing a simple tableViewController with UISearchcontroller.
The problem is that every time that I press the search field then a black rectangle appear right after the keyboard shows up.
I also tried to use definesPresentationContext and searchBarStyle but it keep showing the rectangle. On the other hand, it looks like doesn't happen in the simulator since there is no keyboard.
Update: Below some photos.
ViewController:
class ListGlobalViewController: UITableViewController, StoryboardSceneBased, ViewModelBased {
static var sceneStoryboard: UIStoryboard = UIStoryboard(name: "DataSelectorViewController", bundle: Bundle.main)
// --------------------
// MARK: - Properties
// --------------------
var viewModel: ListGlobalViewModel!
private let disposeBag = DisposeBag()
private let searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.searchBarStyle = .minimal
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = true
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
configure(with: viewModel)
navigationItem.largeTitleDisplayMode = .automatic
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.searchController.isActive = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
// --------------------
// MARK: - Functions
// --------------------
func configure(with viewModel: ListGlobalViewModel) {
self.tableView.delegate = nil
self.tableView.dataSource = nil
viewModel.outputs.listDataObservable.bind(to: self.tableView.rx.items(cellIdentifier: "listGlobalCell", cellType: ListGlobalCell.self)) { (index, model, cell) in
cell.model = model
}.disposed(by: disposeBag)
searchController.searchBar.rx.text.filterNil().throttle(1, scheduler: MainScheduler.instance).distinctUntilChanged().subscribe(viewModel.inputs.searchBarObservable).disposed(by: disposeBag)
}
}
Navigation Default Values:
let controller = UINavigationController()
controller.navigationBar.isTranslucent = false
controller.navigationBar.prefersLargeTitles = true
controller.definesPresentationContext = true
controller.navigationBar.titleTextAttributes = [ NSAttributedString.Key.font: UIFont.bold(size: 24), NSAttributedString.Key.foregroundColor: UIColor.black ]
if #available(iOS 11, *) {
controller.navigationBar.largeTitleTextAttributes = [ NSAttributedString.Key.font: UIFont.bold(size: 33), NSAttributedString.Key.foregroundColor: UIColor.black ]
}
return controller
Pictures:
Bugged
Use UITextField instead if UISearchBar. You can give any design to uitextfield. UISearch bar has limited designable attributes. Also you can reach any search function with textfield.