Animation segue bug on the navigation bar with large title - swift

My bug:
If navigate from a view controller with large titles enabled to a view controller with large titles disabled i see same bug. Height navigation bar changes not smoothy.
I want animation change height navBar during segue on another viewController like this
Common propertyes for navBar set up in BaseNavigationController
class BaseNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
setNavBarTitlesPropertyes()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
private func setNavBarTitlesPropertyes() {
navigationBar.tintColor = .white
navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.white
]
if #available(iOS 11.0, *) {
navigationBar.prefersLargeTitles = true
navigationBar.largeTitleTextAttributes = [
.foregroundColor: UIColor.white
]
}
}
And my setting navbar in the storyboard:

I found solution for this trouble. UINavigationBar
property translucent should be true, and also bottom and top constraint for tableView in UIViewController should be equal Superview.Top and Superview.Bottom accordingly.

Related

How can I centre a view when using a large nav title?

I have a search view controller, I'd like to show a loading indicator in the centre of the screen, but as I'm using large title navigation, it appears to be offset but the height of the large nav?
How can I offset this so it is in the true centre of the screen?
I am setting it currently using
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor)
])
My view controller is below
final class SearchViewController: UITableViewController {
private var searchLoadingController: SearchLoadingController?
private var searchController: UISearchController?
convenience init(searchLoadingController: SearchLoadingController, searchController: UISearchController) {
self.init(nibName: nil, bundle: nil)
self.searchLoadingController = searchLoadingController
self.searchController = searchController
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text, !text.isEmpty else { return }
searchLoadingController?.search(query: text)
}
}
extension SearchViewController: SearchErrorView {
func display(_ viewModel: SearchErrorViewModel) { }
}
private extension SearchViewController {
func configureUI() {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.searchController = searchController
tableView.tableFooterView = UIView(frame: .zero)
searchController?.searchBar.searchBarStyle = .minimal
searchController?.searchResultsUpdater = self
searchController?.obscuresBackgroundDuringPresentation = false
if let searchingView = searchLoadingController?.view {
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor)
])
}
}
}
If you really want the activity indicator in the center of the screen you need to do some calculations:
func addActivityIndicatorToCenterOfScreen() {
let screenHeight = UIScreen.main.bounds.size.height
let activityIndicatorHeight = searchingView.bounds.size.height
let safeAreaBottomInset = tableView.safeAreaInsets.bottom
let yOffset = (screenHeight - activityIndicatorHeight) / 2 - safeAreaBottomInset
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor, constant: -yOffset),
])
}
Also ensure that you configure the constraints after your views are laid out. viewDidAppear is a good place for that. This is necessary because we need to know the size of the tableView and the safe area.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addActivityIndicatorToCenterOfScreen()
}
Now for some explanation. tableView.safeAreaLayoutGuide includes the portion of the tableView between the navigation bar and the notch at the bottom of the screen. tableView.safeAreaInsets.bottom gives us the height of the notch at the bottom of the screen. The yOffset is the center of the screen minus half the height of the activity indicator minus the height of notch of the bottom of the screen. We will offset the bottom of the activity indicator from the bottom of the safe area. That will place the center of the activity indicator at the center of the screen.
If I were you, I would center the activity indicator between the navigation bar and the top of the notch. It's way cleaner and you can do this in viewDidLoad:
func addActivityIndicatorCenteredBetweenNavBarAndSafeAreaBottom() {
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerYAnchor),
])
}

Unable to hide the navigationBar when embedding SwiftUI in UIKit

I am trying to hide the navigationBar when putting some SwiftUI inside of a UIKit UIViewController:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
But it does not go away. When I take away the SwiftUI however, it works. Does anyone know how to solve this?
Edit:
I am instantiating a view like this:
let controller = UIHostingController(rootView: view())
where view is the SwiftUI and then adding this to the UIView() as you would any UIKit element.
UIHostingViewController respects the navigationBarHidden value of your SwiftUI view. You can either call .navigationBarHidden(true) at the end of your SwiftUI view, or you can use the custom UIHostingController subclass shown in the example below.
Solution:
import SwiftUI
import UIKit
class YourHostingController <Content>: UIHostingController<AnyView> where Content : View {
public init(shouldShowNavigationBar: Bool, rootView: Content) {
super.init(rootView: AnyView(rootView.navigationBarHidden(!shouldShowNavigationBar)))
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Example of usage:
let hostVc = YourHostingController(shouldShowNavigationBar: false, rootView: YourSwiftUIView())
Using the modifier .navigationBarHidden(true) did not work in our case. It had no effect.
Our solution is to subclass UIHostingController and don't let it access the UINavigationController at all. For example:
import UIKit
import SwiftUI
final public class RestrictedUIHostingController<Content>: UIHostingController<Content> where Content: View {
/// The hosting controller may in some cases want to make the navigation bar be not hidden.
/// Restrict the access to the outside world, by setting the navigation controller to nil when internally accessed.
public override var navigationController: UINavigationController? {
nil
}
}
Note that this solution relies on underlying code in UIKit and SwiftUI accessing the UINavigationController and setting the navigation bar hidden state based on the UIViewController.navigationController-property. This may break in the future if Apple decides to change on this assumption.
Ran into this problem yesterday, too.
I am presenting a modal UINavigationController with a UIViewController as rootViewController, which embeds a SwiftUI View via UIHostingController.
Setting the usual setNavigationBarHidden in viewDidAppear of the UIViewController stops working as soon as the SwiftUI View is embedded.
Overview:
Root ViewController: setNavigationBarHidden in viewWillAppear
Navigation Bar Visible:
UINavigationController > root UIViewController > embedded UIHostingController
Navigation Bar Invisible:
UINavigationController > root UIViewController > no UIHostingController
After some debugging I realized that the UIHostingController itself calls setNavigationBarHidden again.
So the reason for this problem is, that the UIHostingControllers alters the surrounding UINavigationController's UINavigationBar.
One easy fix:
Set the Navigation Bar property in the first presented SwiftUI View that is embedded by your UIHostingController.
var body: some View {
MyOtherView(viewModel: self.viewModel)
.navigationBarHidden(true)
}
This will revert the adjustment SwiftUI and the UIHostingController are trying to apply to your surrounding UINavigationController.
As there is no guarantee about the interaction between SwiftUI and UIKit (that it uses underlying UIKit), I would suggest keeping the setNavigationBarHidden in the surrounding viewDidAppear together with this modifier, too.
Hiding navigation bar from a class that is extending UIHostingController seems to work when setNavigationBarHidden is called in viewDidAppear instead of viewWillAppear.
override func viewDidAppear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(true, animated: false)
super.viewDidAppear(animated)
}
In my case, I had to use this UIHostingController subclass.
class NavigationBarHiddenUIHostingController<Content: View>: UIHostingController<Content> {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if navigationController?.isNavigationBarHidden == false {
navigationController?.isNavigationBarHidden = true
}
}
}
I want to include my approach here just in case someone find it useful when working with SwiftUI.
I found out that the problem was that UIHostingController was overriding something on my declare of
navigationController?.setNavigationBarHidden(true, animated: false)
So i just created a custom UIHostingController and used viewWillAppear(_ animated:Bool):
class UIHostingViewControllerCustom:UIHostingController<YourView>{
override func viewWillAppear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(true, animated: false)
}
}
Then when you are adding that UIHostingController into your ViewController:
let hostingController = UIHostingViewControllerCustom(rootView: YourView())
hostingController.view.backgroundColor = .clear
addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)
hostingMapView.didMove(toParent: self)
//Constraints
hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: -view.safeAreaInsets.top).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -view.safeAreaInsets.bottom).isActive = true
Nothing worked for me so I added an observer to hide navigationBar in the parent view:
private var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
observer = navigationController?.observe(
\.navigationBar.isHidden,
options: [.new]
) { [weak self] _, change in
guard change.newValue == false else { return }
self?.navigationController?.navigationBar.isHidden = true
}
}
This seems fixed on iOS 16: if you add a symbolic breakpoint for -[UINavigationController setNavigationBarHidden:animated:] and you p $arg3 you'll find that it was nil (false) on iOS 14/15 and it's now 1 (true) on iOS 16, in case you did call setNavigationBarHidden(true somewhere before this internal call happens, i.e. the internal call doesn't overwrite your code anymore.
On iOS 14, #TParizek's solution works (modifier .navigationBarHidden(true)), but on iOS 15 I had to call setNavigationBarHidden(true on the first viewDidLayoutSubviews call.
Unfortunately, if you are making UIHostingViewController without UINavigationController, you would need to make some adaptions to the frame itself(actually to reduce its topAnchor to 48).
It appears that navigationBar spacing shows up only on next viewWillAppear and layout of subviews.
Here is the solution that I have used for my UIHostingViewController.
Firstly, I have made function(inside of my UIHostingViewController) that would set origin(x,y) of my inner subview and set the constraints to self.view.
It has condition(to not do that every time, only when navigation bar spacing shows up):
private var savedView: UIView?
private func removeAdditionalTopSpacing() {
if view.subviews.count == 0 {
return
}
var widgetFrame = view.subviews[0].frame
let widgetStartingPoint = widgetFrame.origin.y
widgetFrame.origin.y = 0
widgetFrame.origin.x = 0
self.view.subviews[0].frame = widgetFrame
self.view.subviews[1].frame = widgetFrame
if widgetStartingPoint > 0 {
self.savedView = self.view
self.savedView?.translatesAutoresizingMaskIntoConstraints = false
self.savedView?.widthAnchor.constraint(equalTo: self.savedView!.subviews[0].widthAnchor).isActive = true
self.savedView?.heightAnchor.constraint(equalTo: self.savedView!.subviews[0].heightAnchor).isActive = true
self.savedView?.centerXAnchor.constraint(equalTo: self.savedView!.subviews[0].centerXAnchor).isActive = true
self.savedView?.centerYAnchor.constraint(equalTo: self.savedView!.subviews[0].centerYAnchor).isActive = true
self.view = self.savedView
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
Important note:
Reason why I have saved current view inside of private variable savedView is because of his existence and memory release. In this way it won't be lost when removeFromSuperView got called.
There are always 2 subviews of UIHostingViewController.view. One for content and another one for hit range. Both are moved for 48 points down when navigation bar spacing shows up.
There are two places where I have called it: viewDidAppear() and viewDidLayoutSubviews():
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
removeAdditionalTopSpacing()
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
removeAdditionalTopSpacing()
}
Hi to all here is my solution how to hide AND BACK navigation bar
import Foundation
import SwiftUI
import UIKit
class HostingController <Content>: UIHostingController<AnyView> where Content : View {
private weak var previousViewController: UIViewController?
private var shouldShowNavigationBar: Bool
private let shouldShowNavigationBarAfterBack: Bool
public init( rootView: Content, previousViewController: UIViewController?,
shouldShowNavigationBar: Bool = false, shouldShowNavigationBarAfterBack: Bool = true) {
self.previousViewController = previousViewController
self.shouldShowNavigationBar = shouldShowNavigationBar
self.shouldShowNavigationBarAfterBack = shouldShowNavigationBarAfterBack
super.init(rootView: AnyView(rootView))
}
override func viewDidLayoutSubviews() {
navigationController?.setNavigationBarHidden(!shouldShowNavigationBar, animated: false)
super.viewDidLayoutSubviews()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf
.previousViewController?
.navigationController?
.setNavigationBarHidden(!strongSelf.shouldShowNavigationBarAfterBack, animated: false)
}
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
How to use it:
let viewController = HostingController(rootView: view, previousViewController: previousViewController)
In case if you need all parameters make not default you can call:
let viewController = HostingController(rootView: view, previousViewController: previousViewController, shouldShowNavigationBar: false, shouldShowNavigationBarAfterBack: false)
previousViewController - it's a controller that make push of this new one controller.
I used SwiftUI introspect library to hide the extra navigation bar that was only showing for OS version lower than 16.
.introspectNavigationController(customize: { navigationController in
navigationController.navigationBar.isHidden = true
})
Do u know where you put in UIKit function inside of swiftUI ?
inside of
var body: some View {
}
you need to call your ViewControllerWrapper class that class need to include some methods in order to use your UIKit class. UIViewControllerRepresentable implementation its also need.

How to change Navigation Bar back to translucent after making it transparent

I have a view controller in my navigation stack that needs to have a transparent navigation bar, while still showing the back button.
I'm able to achieve that with one line of code inside viewWillAppear:
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
However, when I try to go back to the previous view, I'm setting the background image back to nil or .none but I'm losing the translucent effect that was previously on there when I do that.
I've tried setting all the following options in viewWillDisappear and none seem to bring the translucency back. It just appears white no matter what I do. The shadow on the bottom is also gone too:
self.navigationController?.navigationBar.isTranslucent = true
self.navigationController?.navigationBar.barStyle = .default
self.navigationController?.navigationBar.backgroundColor = .none
self.navigationController?.navigationBar.setBackgroundImage(.none, for: .default)
Initial Navigation Bar:
Transparent Navigation Bar:
After Transitioning Back:
In viewWillAppear make the navigation bar transparent
override func viewWillAppear(_ animated: Bool) { self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = true
}
And backg to translucent in viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
self.navigationController?.navigationBar.shadowImage = nil
self.navigationController?.navigationBar.isTranslucent = false
}
After spending time poking around in the UINavigationBar internals, I did discover a simple method that seems to work, and does not require any configuration of the standard UINavigationBar attributes we've previously fiddled with to achieve transparency. The following is tested working on iOS 12.2.x:
class TallNavigationBar: UINavigationBar {
private lazy var maskingView: UIView = {
let view = UIView(frame: bounds)
view.backgroundColor = .clear
return view
}()
var isTransparent = false {
didSet {
guard isTransparent != oldValue, let bkgView = subviews.first else { return }
bkgView.mask = isTransparent ? maskingView : nil
}
}
}
Obviously, whenever fiddling (even slightly) with undocumented internals: use at your own risk!
This worked for my app which needs to revert to an opaque navigation bar after popping from a transparent navigation bar.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
navigationController?.navigationBar.shadowImage = nil
navigationController?.navigationBar.isTranslucent = true
navigationController?.navigationBar.backgroundColor = nil
}

UITabbar jumps when segueing to a controller without a tabbar after modal presentation on iPhone X

I have a Master-Detail application with a tab bar on the Master Controller screen and I have a problem with iPhone X. When I segue from the Master to the Detail, everything works fine. If I show a modal window from the Master first and then go to the Detail, the tab bar freezes with the wrong size for a while. If I fix the tab bar size, everything animates fine. With this, for example:
class MyTabBar: UITabBar {
var tabBarHeight: CGFloat = 83
override func sizeThatFits(_ size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
return CGSize(width: superSize.width, height: self.tabBarHeight)
}
}
However, I don't want to fix the size, as it's too complicated to manage all possible sizes and orientations.
I've found a solution:
class MyTabBarController: UITabBarController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.setNeedsLayout()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.animate(withDuration: 0.4) { [weak self] in
self?.view.layoutIfNeeded()
}
}
}

Swift - changing app when banner loads. - xcode 6 GM

I have been working on a mainly text-based application with an Iadbanner in the bottom.
However as we all know, Iads aren't always there. So I would like to be able to dynamically update the height of my textView, so when the banner is hidden, the textView takes up the wasted space. And resize it when a banner is loaded.
Here's what I currently have for the Viewcontroller in question
import UIKit
import iAd
class DetailVC: UIViewController, UITextViewDelegate, ADBannerViewDelegate {
//Our label for displaying var "Items/cellName"
#IBOutlet var imageViewOutlet: UIImageView!
//connect in IB connection inspector with your ADBannerView
#IBOutlet var adBannerView: ADBannerView!
//Receiving variable assigned to our mainVC var "items"
var cellName: String = ""
var imageView: UIImageView = UIImageView()
var image = UIImage(named: "handcuffs.png")
var textViewText: String = ""
var textView: UITextView = UITextView(frame: CGRect(x: 5.0, y: 238.0, width: 315.00, height: 283.00))
//height = 332 for full screen 283 for small
override func viewDidLoad() {
super.viewDidLoad()
println("+--------------------+")
println("| Detail view loaded |")
println("+--------------------+")
// Iad stuff
self.adBannerView.delegate = self
self.canDisplayBannerAds = true
self.adBannerView.hidden = true //hide until ad loaded
//Setting up the textView
textView.text = textViewText
textView.editable = false
textView.backgroundColor = UIColor.clearColor()
textView.font = UIFont(name: "Helvetica", size: 15)
//adding textview as subview
self.view.addSubview(textView)
//ImageViewOutlets
imageViewOutlet.image = image
//Assign your string var to your navbar title
self.title = cellName
func bannerViewWillLoadAd(banner: ADBannerView!) {
NSLog("bannerViewWillLoadAd")
}
func bannerViewDidLoadAd(banner: ADBannerView!) {
NSLog("bannerViewDidLoadAd")
//self.textView.removeFromSuperview()
//textView = UITextView(frame: CGRect(x: 0.0, y: 238.0, width: 320.00, height: 283.00))
//self.view.addSubview(textView)
self.adBannerView.hidden = false //now show banner as ad is loaded
}
func bannerViewActionDidFinish(banner: ADBannerView!) {
NSLog("bannerViewActionDidFinish")
//optional resume paused app code
}
func bannerViewActionShouldBegin(banner: ADBannerView!, willLeaveApplication willLeave: Bool) -> Bool {
NSLog("bannerViewActionShouldBegin")
//optional pause app code
return true
}
func bannerView(banner: ADBannerView!, didFailToReceiveAdWithError error: NSError!) {
NSLog("didFailToReceiveAdWithError")
}
//... your class implementation code
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The commented out code in the function "bannerViewDidLoadAd" is what I thought would have fixed my issue. Sadly that function seems to never run? I'm not very familiar with Iads so hopefully someone out there can give me a hint as to how to change the height of a textView when an ad loads.
CanDisplayBannerAds :
Set this to enable automatic management of banner ad display with the view controller. It's important to note that this will modify the view hierarchy of the view controller by inserting a new container view above the view controller's view. The impact is that the view controller's view property will no longer return the originally provided view, it will return the new container. To access the original view, use the originalContentView property.
So, remove this line :
self.canDisplayBannerAds = true
Everything should work.