How to reference the default UIView to animate in Swift? - ios

I have 2 view controllers: "MapScreenVC" and "PullUpMenuVC"; When you tap a button on the "MapScreenVC", the "PullUpMenuVC" should appear, pulling up from the bottom. Them problem I am having is that in regards to the default UIView for the "PullUpMenuVC", any visual changes I make in storyboards do not show up.
That said, when I call my prepareBackgroundView() function, the view changes accordingly upon build.
How can I edit the view in storyboards but still animate the view in code? I feel like I have a misunderstanding of how views are referenced from the storyboard. Is the view that I am animating not the default view of the view controller, which is the same view that should be visually edited in the storyboard?
My "PullUpMenuVC" class:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
prepareBackgroundView()
}
func prepareBackgroundView()
{
view.backgroundColor = .white
view.tintColor = defaultTint
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.35) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.main.bounds.height * 1
self?.view.frame = CGRect(0, yComponent, frame!.width, frame!.height)
}
This is a screenshot of my storyboard:
I should also point out that I have connected the "PullUpMenuVC" class to the view controller in my storyboard.

I think you have misunderstood how this is usually done in iOS and Xcode. From your description above I understand you want to do something that looks like a Modal View Presentation.
If you use the default modal view presentation iOS offers for doing that transition between ViewController, you will not have to implement the animations yourself. In fact you can do the whole think in the Storyboard by just hooking up the segues.
I am attaching some reading material that might help you.
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PresentingaViewController.html
https://www.raywenderlich.com/462-storyboards-tutorial-for-ios-part-2
Custom transition if you need it.
https://www.raywenderlich.com/359-ios-animation-tutorial-custom-view-controller-presentation-transitions
Hope the above help.

Related

Share Extension Modal Presentation Style iOS 13 is not working

I implemented the share extension and I want animate my View Controller with a crossDissolve, so i set the modalPresentationStyle = .overFullScreen and modalTransitionStyle = crossDissolve but it seems not working. The VC still appear from the bottom to the top and with the new iOS 13 modal style (not completly full screen).
Anyone know how to solve it? It tried both with and without storyboard.
NB: I'm not talking about a normal VC presentation, but the presentation of the share extension, it means that it's another app that present my VC.
One way to do it would be to have the system presented viewcontroller as a container.
And then present your content viewcontroller inside modally.
// this is the entry point
// either the initial viewcontroller inside the extensions storyboard
// or
// the one you specify in the .plist file
class ContainerVC: UIViewController {
// afaik presenting in viewDidLoad/viewWillAppear is not a good idea, but this produces the exact result you are looking for.
// meaning the content slides up when extension is triggered.
override func viewWillAppear() {
super.viewWillAppear()
view.backgroundColor = .clear
let vc = YourRootVC()
vc.view.backgroundColor = .clear
vc.modalPresentationStyle = .overFullScreen
vc.loadViewIfNeeded()
present(vc, animated: false, completion: nil)
}
}
and then use the content viewcontroller to show your root viewcontroller and its view hierarchy.
class YourRootVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let vc = UIViewController() // your actual content
vc.view.backgroundColor = .blue
vc.view.frame = CGRect(origin: vc.view.center, size: CGSize(width: 200, height: 200))
view.addSubview(vc.view)
addChild(vc)
}
}
Basically a container and a wrapper in order to get the control over the views being displayed.
Source: I had the same problem. This solution works for me.

Swift Presenting Controller Dismiss Bar Indicator

This should be an easy question for most of you. Presenting view controllers like in the attached photo now have a bar at the top of them (see red arrow) to indicate that the user can swipe down to dismiss the controller. Please help with any of the following questions:
What is the proper term for this icon?
Is it part of swift's ui tools / library or is it just a UIImage?
Can someone provide a simple snippet on how to implement - perhaps it is something similar to the code below
let sampleController = SampleController()
sampleController.POSSIBLE_OPTION_TO_SHOW_BAR_ICON = true
present(sampleController, animated: true, completion: nil)
Please see the red arrow for the icon that I am referring to
grabber
grabber is a small horizontal indicator that can appear at the top edge of a
sheet.
In general, include a grabber in a resizable sheet. A grabber shows people that they can drag the sheet to resize it; they can also
tap it to cycle through the detents. In addition to providing a visual
indicator of resizability, a grabber also works with VoiceOver so
people can resize the sheet without seeing the screen. For developer
guidance, see prefersGrabberVisible.
https://developer.apple.com/design/human-interface-guidelines/ios/views/sheets/
From iOS 15+ UISheetPresentationController has property prefersGrabberVisible
https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801906-prefersgrabbervisible
A grabber is a visual affordance that indicates that a sheet is
resizable. Showing a grabber may be useful when it isn't apparent that
a sheet can resize or when the sheet can't dismiss interactively.
Set this value to true for the system to draw a grabber in the
standard system-defined location. The system automatically hides the
grabber at appropriate times, like when the sheet is full screen in a
compact-height size class or when another sheet presents on top of it.
Playground snippet for iOS 15:
import UIKit
import PlaygroundSupport
let viewController = UIViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 380, height: 800)
viewController.view.backgroundColor = .white
PlaygroundPage.current.liveView = viewController.view
PlaygroundPage.current.needsIndefiniteExecution = true
let button = UIButton(primaryAction: UIAction { _ in showModal() })
button.setTitle("Show page sheet", for: .normal)
viewController.view.addSubview(button)
button.frame = CGRect(x: 90, y: 100, width: 200, height: 44)
func showModal {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.view.backgroundColor = .blue.withAlphaComponent(0.5)
viewControllerToPresent.modalPresentationStyle = .pageSheet // or .formSheet
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
viewController.present(viewControllerToPresent, animated: true, completion: nil)
}
The feature you are asking is not available in UIKit.
You have to implement custom view-controller transition animation with subclassing UIPresentationController for rendering that pull up/down handle.
UIPresentationController (developer.apple.com)
For custom presentations, you can provide your own presentation controller to give the presented view controller a custom appearance. Presentation controllers manage any custom chrome that is separate from the view controller and its contents. For example, a dimming view placed behind the view controller’s view would be managed by a presentation controller. Apple Documentation
This can be achieved by any UIView or you can use any image if you want by adding subview to UIPresentationController's contentView above the presentedView.
To provide the swipe gesture to dismiss/present, you must implement UIPercentDrivenInteractionController.
You can refer to this tutorial below for detailed understanding.
UIPresentationController Tutorial By raywenderlich.com
You should look for presentationDirection = .bottom in your case.
For gesture driven dismissal, you should check below tutorial
Custom-UIViewcontroller-Transitions-getting-started
I hope this might help you.
If you need to add this indicator within the view controller that is being presented if you do not want to do any custom presentations and just work with the default transitions.
The first thing to think about is your view hierarchy, is the indicator going to be part of your navigation bar or perhaps your view does not have navigation bar - so accordingly you probably need some code to find the correct view to add this indicator to.
In my scenario, I needed a navigation bar so my view controllers were within a navigation controller but you could do the same inside your view controllers directly:
1: Subclass a Navigation Controller
This is optional but it would be nice to abstract away all of this customization into the navigation controller.
I do a check to see if the NavigationController is being presented. This might not be the best way to check but since this is not part of the question, refer to these answers to check if a view controller was presented modally or not
class CustomNavigationController: UINavigationController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this checks if the ViewController is being presented
if presentingViewController != nil {
addModalIndicator()
}
}
private func addModalIndicator() {
let indicator = UIView()
indicator.backgroundColor = .tertiaryLabel
let indicatorSize = CGSize(width: 30, height: 5)
let indicatorX = (navigationBar.frame.width - indicatorSize.width) / CGFloat(2)
indicator.frame = CGRect(origin: CGPoint(x: indicatorX, y: 8), size: indicatorSize)
indicator.layer.cornerRadius = indicatorSize.height / CGFloat(2.0)
navigationBar.addSubview(indicator)
}
}
2: Present the Custom Navigation Controller
let someVC = UIViewController()
let customNavigationController = CustomNavigationController()
customNavigationController.setViewControllers([stationsVC], animated: false)
present(playerNavigationController, animated: true) { }
3: This will produce the following results
You might need to alter some logic here based on your scenario / view controller hierarchy but hopefully this gives you some ideas.

ViewController is blank after navigating away and returning

I'm building an app where two view controllers share a UIView subclass as the main source of UI. It works perfectly when the app is starting, but if I navigate away from the initial view, and return to it, all of the UI is lost. What do I need to do to preserve the views UI post-navigation?
My app flow is: MainView -> TableView -> DetailView
Just going from Main to Table to Main itself makes the UI vanish.
(rank isn't 10 yet, so here's a link to view: https://gfycat.com/enormousanchoredindochinesetiger)
What I do is load the UI in the UIView class through layoutSubviews, and in the UIViewControllers I set the instantiate the class, UI in the loadViews method by saying view = viewClass. I've tried adding this (view = viewClass) to viewWillAppear() as well, but it does nothing.
I've also tried creating two unique view classes in case instantiating was a problem. It didn't change anything.
ViewController:
override func loadView() {
super.loadView()
view = baseView
view.backgroundColor = .white
self.navigationController?.isNavigationBarHidden = true
requestLaunchData()
setButtonTargets()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
view = baseView
}
//How I push to the next view
#objc func upcomingButtonTapped() {
let vc = TableViewController()
navigationController?.pushViewController(vc, animated: true)
vc.upcomingLaunches = upcomingLaunches
}
UIView:
class BaseView: UIView {
//Lots of labels and buttons instantiated
override func layoutSubviews() {
super.layoutSubviews()
setUI() //adding subviews
}
//Layout configurations
}
Before it was this structure, I had all the UI (labels, buttons, a map) directly created and configured in each ViewController, which made both massive. But, it also worked.
I solved it after a night's rest.
So here's how you need to use a custom UIView class as your ViewController's view:
class YourView: UIView {
//Create your properties, views, etc.
let exampleView = UIView()
override layoutSubviews(){
super.layoutSubviews()
addSubview(exampleView)
//Add layouts, etc.
}
And then in your ViewController, in either viewDidLoad, or loadViews (like me here):
let customView = YourView()
override func loadView() {
super.loadView()
view = customView //Sets the entire view to all the UI you created in the custom class
}
The FATAL mistake I made was this:
override layoutSubviews(){
super.layoutSubviews()
if let sView = superview { //This gives you frame size and such
sView.addSubview(exampleView)
}
}
This sets the UI's memory to the SuperView, which gets lost the moment you leave that view, because of view = customView. So my controller was rendering view = customView, which was empty, because all the UI was set to the superView which was superseded by customView.
I hope that helps anyone trying to use this same architecture in the future.

iOS 11 prefersLargeTitles not updating until scroll

I implemented a basic UIViewController with a UITableView that's wrapped in a UINavigationController. I set prefersLargeTitles to true:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Coffees"
}
However, the title stays small until I scroll the view, at which point it enlarges. I tried moving that call to where I create the UINavigationController, but there was no effect. I am sure the navigationController is not nil when I set prefersLargeTitles.
Should I be updating that property elsewhere? Or should I file a Radar?
Update:
This only seems to happen if my view contains a UITableView or is itself a UITableViewController
I recently hit the same issue and none of the suggestions worked for me. Instead, all I needed to do was to invoke sizeToFit(). Sample code:
private func configureNavigator() {
guard let navigationController = navigationController else { return }
navigationController.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
navigationController.navigationBar.sizeToFit()
}
I hope this helps!
For me the only working solution is:
DispatchQueue.main.async { [weak self] in
self?.navigationController?.navigationBar.sizeToFit()
}
in
viewWillAppear()
I had the same issue only on one tableview ...
I had to set :
self.tableView.contentInsetAdjustmentBehavior = .never
so that my tableview stop scrolling when uiviewcontroller was loaded.
It's the tableview automatic scrolling that makes the large title being hidden
Hope this helps
I had the same problem. Although you are not using Storyboards but I hope this could help someone. I checked "Prefer Large Titles" for the Navigation Controller (not the View Controller) I embedded my TableViewController in. All the View Controllers after the Navigation Controller turned and had large titles, and it should work.
Same issue with Swift 5.2
my view contains tableView and prefersLargeTitles is not updating until scroll, I fixed it by setting
self.tableView.contentInsetAdjustmentBehavior = .never
Modifying the contentInset of the tableView with top:1 will force the NavigationBar to expand and display the large titles.
Obj-C
-(void) viewWillAppear:(BOOL)animated {
if (#available(iOS 11.0, *)) {
tableView.contentInset = UIEdgeInsetsMake(1, 0, 0, 0);
}
}
Swift
override func viewWillAppear(_ animated: Bool) {
if #available(iOS 11.0, *) {
tableView.contentInset = UIEdgeInsetsMake(1, 0, 0, 0)
}
}
Note: If you have a tableView.reloadData() in your viewWillAppear make sure to call it after editing the contentInset
In my case the solution was to set tableView's top align to Safe Area and not Superview
I ran into the same issue and found that it’s usually best to set the prefersLargeTitles property from the view controller or object that sets it up, and to do so before it is presented.
For instance, if the view controller in question is shown upon app launch:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
let someViewController: UIViewController = CustomViewController()
let theNavController = UINavigationController(rootViewController: someViewController)
theNavController.navigationBar.prefersLargeTitles = true
window.rootViewController = theNavController
window.makeKeyAndVisible()
return true
}
or if presenting a particular view controller:
let someViewController: UIViewController = CustomViewController()
let theNavController = UINavigationController(rootViewController: someViewController)
theNavController.navigationBar.prefersLargeTitles = true
present(theNavController, animated: true, completion: nil)
I found this method to be a more sure-fire way to ensure that the navigation title is displayed accordingly. Hope this helps! :)
I have wasted some considerable amount of time on this as prefersLargeTitle saga works on some view controllers as expected and with some it produces the same issue above.
Solution for me was to uncheck Extended Edges Under Top Bars in IB - for those view controllers who show large title momentarily until the contents of the table view are loaded then navigation bar jumps back up to regular size. It only shows the large title when scrolling the table view down.
This is backward compatible with iOS 10 and does not leave any empty space above the first row in the table view.
I had checked prefersLargeTitle on the navigation controllers attributes inspector only in IB - nothing in code. Same for largeTitleDisplayMode = .always
As for why this happens with some view controllers and not others, I have absolutely no idea!
In the storyboard I set the Navigation Item's Large Title to Never.
In my ViewController's viewDidLoad method I set the following:
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
Programmatically:
In AppDelegate.swift:
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let navigationController = UINavigationController.init(rootViewController: ViewController())
window?.rootViewController = navigationController
In ViewController:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .automatic
}
override func loadView() {
super.loadView()
view.addSubview(tableView)
view.addSubview(loadingView)
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
tableView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor)
])
}
Make sure your tableView has beed previously added to your view.
I tried all of the other answers but what worked for me was to set the content offset of my UITableView to CGPoint(x: 0, y: -1) in viewDidLoad:
tableView.setContentOffset(CGPoint(x: 0, y: -1), animated: false)
I just had this same issue and, in my case, it turns out that the Storyboard structure that was working in iOS 10 with Swift 3 (and also works with iOS 11 with Swift 3) was causing the issue on iOS 11 with Swift 4.
To elaborate:
I had a regular UIViewController in my storyboard that I had set to a UINavigationController subclass (my hierarchy is similar to yours, with UITabBarController subclass → UINavigationController subclass → UITableViewController subclass).
In iOS 10, this worked fine.
In iOS 11, this also works fine when you run the existing Swift 3 app.
However, with the Swift 4 app, running on iOS 11, I was seeing the same symptoms you described (large titles only appear when you pull/scroll the view down).
To fix, I replaced the UIViewController-based elements in the Storyboard with actual UINavigationController instances (which contain a UINavigationBar explicitly in the Storyboard – I have a hunch this is where the crux of the issue stems from, as the UIViewController instances didn’t have that element explicitly declared within the Storyboard).
Anyway, that fixed the issue for me.
I’ll file radar as this looks like a Swift 4-based regression as, for me, it works both in iOS 10 with Swift 3 and in iOS 11 with Swift 3.
General changing the behaviour of the navigationBar should be done in viewWillAppear(_:)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.prefersLargeTitles = true
}
After doing that it worked fine for me.
One more possible solution is to end refresh in your refreshHandler(). like this-
#objc func refreshPage() {
self.refreshControl?.endRefreshing() //End here
self.loadTableData() //Get fresh data and reload table
}
I solved this issue via storyboard
Navigation Controller -> Navigation Bar -> Attributes inspector -> Prefers Large Titles(Checked)
View Controller -> Navigation Item -> Attributes inspector -> Large Title (Automatic or Always checked)
I think It does seem a little bit dummy but I effectively solved the problem with this:
self.navigationItem.prompt = ""
self.navigationItem.prompt = nil
It's like navigationBar needs a sort of update in one of its elements to update the layout.
Sometimes to update something in navigationBar I need to hide and unhide it.. That's why I think there is a best way to do it.. For the moment that's my workaround.
I had a similar issue with navigation bar, but in my case it had a custom title view, and navigation bar remained empty until table view is scrolled down, which triggered UILayoutContainerView to layout its subviews, one of which are navigation controller's view and navigation bar. I assume the root of it is the same as the large title navigation bar issue.
Anchoring tableView to the safeAreaLayoutGuide didn't work out for me, largeTitleDisplayMode couldn't be other then .never
So I managed to fix it by calling self.navigationController?.view.setNeedsUpdateConstraints in the top presented controller's viewDidAppear(animated:) function, or scheduling this call for the next run loop in viewWillAppear(animated:), like:
DispatchQueue.main.async {
self.navigationController?.view.setNeedsUpdateConstraints()
}
In this case, navigation bar appeared with the correct content and size along with presenting transition, instead of popping in after transition was completed
What worked for me is setting the self.navigationController?.navigationBar.prefersLargeTitles = true before calling tableviews reload method.
The answers above for tableview also works for collection view if parent view not scrolled before navigation:
self.collectionView.contentInsetAdjustmentBehavior = .never
If parent view scrolled before navigation the solution above did not work in my case. I had to add the answer above as:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.sizeToFit()
}
That does seem like a weird behavior at first, but try setting the navigationItem.largeTitleDisplayMode to always. The default is automatic - and it's not defined how that works in the docs.
Also wrote / will update an answer about large titles here.
I had the similar issue. The view is a table view. The property of prefersLargeTitles is set at viewDidLoad event. Then I set view's title in viewWillAppear event.
override open func viewDidLoad() {
if #available(iOS 11.0, *) {
self.navigationController?.navigationBar.prefersLargeTitles = true
} else {
// Fallback on earlier versions
}
...
}
override open func viewWillAppear(_ animated: Bool) {
self.navigationItem.title = "something"
...
}
In my prepare segue event, I set navigation item's tile to nil so that the next view left navigation var item displays "Back" automatically.
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
self.navigationItem.title = nil
...
}
The first time the table view displays large title correctly. However, if I select a row to next view and back to the table view, the navigation item's title becomes empty.
After several hours' struggling, I finally find out that the view's title should be set in viewDidAppear event! It seems that whatever set to view's title in Will event would be reset by UIKit internally back to nil. So it has to be set in a different event.
override func viewDidAppear(_ animated: Bool) {
self.navigationItem.title = "something"
...
}
override open func viewWillAppear(_ animated: Bool) {
// self.navigationItem.title = "something" // Remove it and set title in Did event!
...
}
Before I introduced this iOS 11 new feature, my app runs OK. It seems that the new feature has some changes in UIKit so that the previous version app may need some updates/changes to make it work.
I had the same issue and fixed it by changing order of views in my ViewController in InterfaceBuilder.
It seems like if the first view in Hierarchy is NOT a ScrollView then NavigationBar appears in LargeTitle mode and does not animates together with scroll view. If you need to have Navigation Bar title to reflect your scroll then you need to put your scroll view as the first in view hierarchy.
Also, I am not completely sure in this but looks like Navigation Bar appearance in standard or Large Title mode depends on views hierarch of previous Controller.
Similar issue for me with a UITableViewController added to a UIViewController. In my instance, these view controllers are themselves embedded in a UITabBarController and only the first tab displayed correctly used a large title. Other tabs required a manual scroll before the large title was displayed.
The only thing I could get to work was adjusting the contentInset as per #pau-senabre's answer, except I the top inset wasn't helpful for me. Instead, I set the left inset and then reset it on the next runloop.
private var isFirstAppearance = true
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isFirstAppearance {
applyLargeTitlesFix()
}
}
private func applyLargeTitlesFix() {
let originalInset = tableViewController.tableView.contentInset
tableViewController.tableView.contentInset = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 0)
DispatchQueue.main.async { [weak self] in
self?.tableViewController.tableView.contentInset = originalInset
}
isFirstAppearance = false
}
Same issue here with Swift 4.2, iOS 12, and refactored Storyboards.
Tried adding prefersLargeTitles = true to viewWillAppear and viewDidLoad, but neither fixed my issue.
Instead, I copied the refactored storyboards back into main.storyboard and found the option to enable large titles in IB. Set that option, then refactored the storyboards back out and everything is working now. For some reason, the initial refactoring stripped out the option and I couldn't enable it programmatically.
I had the same issue (iOS 14, Xcode 12.2).
It only affected navigation controllers displaying table views.
I had originally set tableView.tableFooterView = UIView() to get rid of extra separators after the last cell. Setting the footer view to nil fixed the scrolled-up navigation title.
I had same issue.
I have set below code in viewdidload method. and it get fixed.
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationItem.largeTitleDisplayMode = .never

UISegmented Control Disappears After Dismissing View

In working on this app with a TabBar at the bottom, NavBar at the top with a Segmented Control:
I have an issue where the View A (Segment One) with a UITableView, upon selecting a cell and displaying a new view with more details, when I click back, the Segmented control at the top will disappear and the TableView from View A will be pushed up.
This doesn't always happen - sometimes after many tries or sometimes just one. I haven't found any correlation to what's causing it.
I have found that if I select View B from the segmented Control, then back to View A, then click on one of the table cells to get to the details screen and then click back, 100% of the time the Top Nav Bar disappears with the segmented control.
TabBarItemOneViewController
let segmentOneVC: SegmentOneViewController
let segmentTwoVC: SegmentTwoViewController
var currentViewController: UIViewController
let viewControllerYLoc = 60 // statusBarHeight + topBarHeight
let viewWidth = Helper.getViewWidth()
let tabBarHeight = 40
func pressedSegItem(segControl: UISegmentedControl){
let viewControllerHeight = Int(self.view.frame.height)
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
let selectedIndex = segControl.selectedSegmentIndex
previouslySelectedMyLoadsIndex = selectedIndex
self.currentViewController.removeFromParentViewController()
if(selectedIndex == 0){
currentViewController = segmentOneVC
}
else if(selectedIndex == 1){
currentViewController = segmentTwoVC
}
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
public init() {
segmentOneVC = SegmentOneViewController(nibName: nil, bundle: nil)
segmentTwoVC = SegmentTwoViewController(nibName: nil, bundle: nil)
if(previouslySelectedIndex == 0){
currentViewController = segmentOneVC
}
else{
currentViewController = segmentTwoVC
}
super.init(nibName: nil, bundle: nil)
self.calculateItems()
self.addSegmentedControl()
let viewControllerHeight = (Int(self.view.frame.height) - viewControllerYLoc) - tabBarHeight
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
self.currentViewController.view.frame = viewFrame
self.addChildViewController(segmentOneVC)
self.addChildViewController(segmentTwoVC)
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
SegmentOneViewController (note: SegmentTwoViewController is identical)
let cellReuseIdentifier = "ItemDetailTableViewCell"
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = indexPath.row
let dataItem = self.dataArray[row]
let itemDetailVC = ItemDetailViewController()
itemDetailVC.dataItem = dataItem
self.present(itemDetailVC, animated: true, completion: nil)
}
func addTableView(){
self.tableView = UITableView()
tableView.register(ItemDetailTableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
tableView.frame = CGRect(x: 0, y: 0, width: Int(viewWidth), height: (Int(self.view.frame.height) - bottomOfTopNavBar) - heightOfTabBar)
self.view.addSubview(tableView)
}
override func viewDidAppear(_ animated: Bool){
super.viewDidAppear(animated)
loadData()
tableView.dataSource = self
tableView.delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
addTableView()
}
ItemDetailViewController
// Connected to a back button in a top Navigation Bar
func goBack(){
self.dismiss(animated: false, completion: nil)
}
Nice graphics BTW... that illustration make it much easier to understand your problem.
Also BTW, I'm an Obj-C person, still learning the nuances of Swift, so please let me know if my syntax or otherwise is incorrect. I'm also relatively inexperienced in using container VC's.
​
I've written my response in two parts.
The First Part is my attempt to solve your problem.
The Second Part is my suggestion for an alternative for you to consider.
First Part
This is my understanding of the order/sequence of execution in your code...
Parent View with Segmented Control
public init () : on instantiation of the parent view controller, two child VCs (segmentOneVC and segmentTwoVC) are instantiated and depending on previous selection are assigned as currentViewController. Then you add a segmented control to the TabBarItemOneViewController.
User taps a segmented control.
Depending on user input, either the SegmentOneViewController or SegmentTwoViewController view is added as a subview to the TabBarItemOneViewController.view. (Note that this is also done when the VC is initialised.)
Child View
override func viewDidLoad() : once the view did load, you call the function addTableView.
func addTableView() : in this custom function you instantiate your table view and place it within the SegmentOneViewController, which is itself I assume a UIViewController.
override func viewDidAppear(_ animated: Bool) : you call the custom function loadData and set your table view data source and delegate.
Back Button
User taps the back button.
Child VC is dismissed and the TabBarItemOneViewController becomes the active view on screen.
Let's look at what does not happen in the view controller lifecycle when the back button is pressed... Item 1 in the list.
This may explain the inconsistency.
Try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Don't tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is still there.
Now try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is no longer there.
Why? Because the custom function pressedSegItem that I assume has a target action assigned to the segmented control, will overwrite your init, which is where you add the segmented control into the tab bar view controller.
So by way of example, try placing the code to instantiate your segmented control instead in an override function of viewWillAppear of the TabBarItemOneViewController VC.
So a couple of concepts to think about...
lazy loading to save memory allocation - only instantiate the objects you need when the user specifically requests that function in the app;
order of execution of each function in the UIViewController lifecycle; and
which functions are executed once and which are executed each time your view becomes first responder / the active view.
Some reading recomendations:
This SO question titled :Looking to understand the iOS UIViewController lifecycle" presents a lot of good information, but understand that some of the information is incorrect due to deprecation of viewDidUnload from iOS 6.
Which is why you should always go to the Apple documentation for UIViewController to refer to the latest API reference.
Second Part
By providing this alternative, I'm not suggesting that your approach is incorrect, however I am suggesting an alternative for you to consider.
I've always used tab bar controllers to change views and segmented controls to filter data sets or change the appearance of the current view.
Think about using a UISegmentedControl to manage or adjust the data set within only one table view. This will alleviate the need for multiple view controllers and the juggling act of managing these.
For example, when writing your data source and delegate methods / functions for your tabel view, you can include the following code to ensure the table view loads and responds accordinagly:
let selectedIndex = segControl.selectedSegmentIndex
if(selectedIndex == 0) {
rowHeight = 20 'for example
} else {
rowHeight = 30 'for example
}
Then you'd need to relaod your table view to effect the changes.

Resources