Insert UIView at top of other views programmatically - ios

I work on iOS app, and I need a uiview appear in each uiviewcontroller when there is no internet connection available as in facebook messenger, I write the following code to do that:
extension UIViewController: NetworkStatusListener {
public func networkStatusDidChange(status: Reachability.NetworkStatus) {
switch status {
case .notReachable:
debugPrint("ViewController: Network became unreachable")
case .reachableViaWiFi:
debugPrint("ViewController: Network reachable through WiFi")
case .reachableViaWWAN:
debugPrint("ViewController: Network reachable through Cellular Data")
}
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 30))
let headerLbl = UILabel()
headerLbl.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 30)
headerLbl.text = "No Internet Connection!"
headerLbl.backgroundColor = UIColor.red
headerLbl.font = UIFont(name: "Cairo", size: 14)
headerLbl.textAlignment = .center
headerView.addSubview(headerLbl)
//
self.view.insertSubview(headerView, at: 0)
headerView.isHidden = (status == .notReachable)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ReachabilityManager.shared.addListener(listener: self)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
ReachabilityManager.shared.removeListener(listener: self)
}
}
I depend in checking network availability on this
The checking of network is worked correctly but the desired target isn't achieved.
I need no network connection appear in each uiviewcontroller of my project when there is no internet connection and at top of all other views without overlap other or above them, how can I do that?

a couple of things...
First, in your networkStatusDidChange method, you are creating a new headerView each time - regardless of the network availability status. You might want to a) only create the headerView when you actually want to show it; and b) only create it if you haven't already.
Second, I'm pretty sure inserting the view at index 0 puts it at the bottom. Instead, you could just addSubview the first time around, and then call bringSubview(toFront:) on it when you want to show it again.
If your requirement for having each VC be able to display a status message isn't too strict, you may want to consider an alternate, and the reason I bring this up is that I'm not sure how well this approach plays with creating other views / view controllers - i.e. what happens to the status message when a new view is pushed onto a navigation controller, or the user selects another tab in your app (I have no idea about your app, so I'm just providing examples).
If it makes sense for your app, you could also create a dedicated UIViewController subclass for showing the status (instead of an extension that any view controller can access). Then, set up a view controller hierarchy in your app - for example, if your app is a tab bar app:
AppRootViewController
YourTabBarController
YourNetworkStatusViewController
The net status vc can add/remove its view as needed, in response to network availability changes. The advantage is a separation of concerns - only that one VC knows about showing the network status, and all your other view controllers remain independent from it.

I'd prefer a message extension like SwiftMessages. You can easily call the function from any class/function e. g. your network-availability-function.
in your case it'd look like this:
public func networkStatusDidChange(status: Reachability.NetworkStatus) {
switch status {
case .notReachable:
debugPrint("ViewController: Network became unreachable")
let view = MessageView.viewFromNib(layout: .cardView)
view.configureTheme(.warning)
view.configureDropShadow()
view.configureContent(title: "Warning", body: "Network became unreachable.")
SwiftMessages.show(view: view)
case .reachableViaWiFi:
debugPrint("ViewController: Network reachable through WiFi")
case .reachableViaWWAN:
debugPrint("ViewController: Network reachable through Cellular Data")
}
}
For tableviews you can use some Pods like this: https://github.com/HamzaGhazouani/HGPlaceholders or https://github.com/dzenbot/DZNEmptyDataSet

Related

Activity Indicator When Switching Tabs

Using this stackoverflow solution as a guide I have a setup where I have a UITabBarController and two tabs. When changes are made in the first tab (a UIViewController), the second tab (another UIViewController with a UITableView) needs to perform some calculations, which take a while. So I have a UIActivityIndicatorView (bundled with a UILabel) that shows up when the second tab is selected, displayed, and the UITableView data is being calculated and loaded. It all works as desired in the Simulator, but when I switch to my real device (iPhone X), the calculations occur before the second tab view controller is displayed so there's just a large pause on the first tab view controller until the calculations are done.
The scary part for me is when I started debugging this with a breakpoint before the DispatchQueue.main.async call it functioned as desired. So in desperation after hours of research and debugging, I introduced a tenth of a second usleep before the DispatchQueue.main.async call. With the usleep the problem no longer occurred. But I know that a sleep is not the correct solution, so hopefully I can explain everything fully here for some help.
Here's the flow of the logic:
The user is in the first tab controller and makes a change which will force the second tab controller to recalculate (via a "dirty" flag variable held in the tab controller).
The user hits the second tab, which activates this in the UITabController:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let controllerIndex = tabBarController.selectedIndex
if controllerIndex == 1 {
if let controller = tabBarController.viewControllers?[1] as? SecondViewController {
if dirty {
controller.refreshAll()
}
}
}
}
Since dirty is true, refreshAll() is called for the secondController and its implementation is this:
func refreshAll() {
showActivityIndicator()
// WHAT?!?! This usleep call makes the display of the spinner work on real devices (not needed on simulator)
usleep(100000) // One tenth of a second
DispatchQueue.main.async {
// Load new data
self.details = self.calculateDetails()
// Display new data
self.detailTableView.reloadData()
// Clean up the activityView
DispatchQueue.main.async {
self.activityView.removeFromSuperview()
}
}
}
showActivityIndicator() is implemented in the second view controller as such (activityView is a class property):
func showActivityIndicator() {
let avHeight = 50
let avWidth = 160
let activityLabel = UILabel(frame: CGRect(x: avHeight, y: 0, width: avWidth, height: avHeight))
activityLabel.text = "Calculating"
activityLabel.textColor = UIColor.white
let activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.frame = CGRect(x: 0, y: 0, width: avHeight, height: avHeight)
activityIndicator.color = UIColor.white
activityIndicator.startAnimating()
activityView.frame = CGRect(x: view.frame.midX - CGFloat(avWidth/2), y: view.frame.midY - CGFloat(avHeight/2), width: CGFloat(avWidth), height: CGFloat(avHeight))
activityView.layer.cornerRadius = 10
activityView.layer.masksToBounds = true
activityView.backgroundColor = UIColor.systemIndigo
activityView.addSubview(activityIndicator)
activityView.addSubview(activityLabel)
view.addSubview(activityView)
}
So in summary, the above code works as desired with the usleep call. Without the usleep call, calculations are done before the second tab view controller is displayed about 19 times out of 20 (1 in 20 times it does function as desired).
I'm using XCode 12.4, Swift 5, and both the Simulator and my real device are on iOS 14.4.
Your structure is wrong. Time consuming activity must be performed off the main thread. Your calculateDetails must be ready to work on a background thread, and should have a completion handler parameter that it calls when the work is done. For example:
func refreshAll() {
showActivityIndicator()
myBackgroundQueue.async {
self.calculateDetails(completion: {
DispatchQueue.main.async {
self.detailTableView.reloadData()
self.activityView.removeFromSuperview()
}
})
}
}
So the answer is two parts:
Part 1, as guided by matt, is that I was using the wrong thread, which I believe explains the timing issue being fixed by usleep. I have since moved to a background thread with a qos of userInitiated. It seems like the original stackoverflow solution I used as a guide is using the wrong thread as well.
Part 2, as guided by Teju Amirthi, simplified code by moving the refreshAll() call to the second controller's viewDidAppear function. This simplified my code by removing the need for the logic implemented in step 2 above in the UITabController.

Test for UITableViewCell existence failing from inside a split view controller

I am testing the existence of a table view cell and the following code works perfectly fine on an iPhone 7:
let complaintCell = self.app.tables.cells.element(boundBy: 0)
XCTAssert(complaintCell.exists)
complaintCell.tap()
Now the problem is that if I run the same test on an iPad where the view controller is embedded inside a split view controller, the test fails:
The table view is still visible on the master view controller:
So I can't find why the test fails, even if the table view is the only visible one. Any hint?
Full code:
func testNavigation() {
let complaintCell = self.app.tables.cells.element(boundBy: 0)
XCTAssert(complaintCell.exists)
complaintCell.tap()
XCTAssert(self.app.navigationBars["Complaint #100"].exists)
XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)
let editButton = self.app.buttons["editComplaint"]
XCTAssert(editButton.exists)
editButton.tap()
XCTAssert(self.app.navigationBars["Complaint #100"].exists)
XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)
let saveButton = self.app.buttons["Save"]
XCTAssert(saveButton.exists)
saveButton.tap()
let okButton = self.app.buttons["Ok"]
XCTAssert(okButton.exists)
okButton.tap()
}
Update
I was able to isolate the problem: if I just create a new project, a master detail application and I set the accessibility identifier of the main table view, and then I test for its existence, the test fails:
let table = self.app.tables["TableView"]
XCTAssert(table.waitForExistence(timeout: 5.0))
The steps necessary in order to reproduce this problem are very simple, you just need to create a master detail application, set the accessibility identifier of the table view and then run the above code. But if you want you can also clone this repository, which I used as a demo to isolate the problem: https://github.com/ralzuhouri/tableViewTestDemo
I encountered the same problem when trying to test for tableview cell existence on the simulator. On a simulated iPhone device the test would succeed, whereas on an iPad device it would fail.
I found that the problem lay in the fact that a UITest that references a table will fail if the app's current view does not have the tableview whose data you would like to test. On an iPhone the view by default had a back button that would transition the app from its detail view back to the master viewcontroller which contained the tableview. On the iPad simulator this back button was not there, and so it could not transition correctly to the tableview's view, making the entire test fail.
func testTableCellsExist() {
let app = XCUIApplication()
app.launch()
app.navigationBars["AppName.DetailView"].buttons["Root View Controller"].tap()
let tablesQuery = app.tables["MasterTable"]
let testCell = tablesQuery.cells.element(boundBy: 49)
XCTAssert(tablesQuery.cells.count == 50)
XCTAssert(testCell.exists)
}
What I did to make the test succeed for both iPad and iPhone device simulations was to make it so that the app would launch and display the tableview's viewcontroller at the outset. This was done by adding this code to the UISplitViewController swift file:
class SplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
}
extension SplitViewController: UISplitViewControllerDelegate {
func splitViewController(
_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController) -> Bool {
// Return true to prevent UIKit from applying its default behavior
return true
}
}
The explanation for the above code can be found here:
Open UISplitViewController to Master View rather than Detail
Regardless, after the above modification, the assertions for tableview cell existence should now succeed even on an iPad because the view is now correctly set to the view which has the tableview being queried.
If you don't want your app to start with this view by default, then you'll have to make sure that your test code transitions to the tableview's view before the assertions are made.
Also, if you follow my example through, be sure to remove this line in the test case, because it no longer becomes necessary to navigate to the tableview's VC after the modifications to the UISplitViewController code:
app.navigationBars["AppName.DetailView"].buttons["Root View Controller"].tap()
UPDATE (October 25): Master Detail App Project - Basic TableView Existence Test
I attempted to create a basic Master Detail app as you suggested, and tried the test as well. A basic test for the tableview failed again with me when I selected an iPad device to simulate, because it shows only the detail view (no table). I modified my test so that if the device is an iPad, it will check its orientation, and set the landscape orientation as required, before checking for the table's existence. I only modified the test and nothing else, and what was previously a failure became a success. I also set the accesibility identifier for the tableview in the MasterVC's viewDidLoad, but I believe the results would be the same whether we set the identifier or not. Here's the test code:
func testExample() {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
if UIDevice.current.userInterfaceIdiom == .pad {
print("TESTING AN IPAD\n")
print(XCUIDevice.shared.orientation.rawValue)
// For some reason isPortrait returns false
// If rawValue == 0 it also seems to indicate iPad in portrait mode
if (XCUIDevice.shared.orientation.isPortrait || XCUIDevice.shared.orientation.rawValue == 0){
XCUIDevice.shared.orientation = .landscapeRight
}
XCTAssert(app.tables["MyTable"].exists)
//XCTAssert(app.tables["MyTable"].waitForExistence(timeout: 5.0))
} else if UIDevice.current.userInterfaceIdiom == .phone {
print("TESTING AN IPHONE\n")
XCTAssert(app.tables["MyTable"].exists)
}
// XCTAssert(app.tables["MyTable"].exists)
}
I added an else for the iPhone case, and print logs to say which type of device is being tested. Whether .exists or .waitForExistence is used, the tests should always succeed. But as was pointed out, .waitForExistence is better in the case where the table view takes time to load up.
Hope this helps. Cheers!
UPDATE (OCTOBER 27): Test Fails on a Real iPad
OP has discerned that the test -- which succeeds for simulated iPhone and iPad devices -- fails on a real device (for more information, please check his informative comment for this answer below). As the test failed on a real iPad which was already in landscape mode (see screenshot), it may be assumed that XC UITest functionality is broken in this regard.
In any case I hope these tests and results will prove helpful (they certainly taught me a thing or two) to others as well.
Cheers :D
Your problem seems to be with the orientation of the device as I noticed when trying it on your test project. I have managed to come up with two solutions, both work perfectly choose between 1 or 1a as per comments in code. Option 1 will work on both iPhones and iPads option 1a is iPad specific or the larger iPhones which display the master in landscape as well.
First set the tableViews accessibility identifier in your code to easily reference the correct tableView:
complaintTableView.accessibilityIdentifier = "ComplaintsTableView"
Then in your tests just reference the identifier - here is the full test according to your question above:
func testNavigation() {
// 1 - Hit the back button on the collapset master view
let masterButton = app.navigationBars.buttons.element(boundBy: 0)
if masterButton.exists {
masterButton.tap()
print("BACK TAPPED")
}
// 1a - Or just use the line below to rotate the view to landscape which will automatically show the master view
XCUIDevice.shared.orientation = UIDeviceOrientation.landscapeRight
let complaintsTable = app.tables["ComplaintsTableView"]
XCTAssertTrue(complaintsTable.exists, "Table does not exist")
let complaintCell = complaintsTable.cells.firstMatch
XCTAssert(complaintCell.exists)
complaintCell.tap()
XCTAssert(self.app.navigationBars["Complaint #100"].exists)
XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)
let editButton = self.app.buttons["editComplaint"]
XCTAssert(editButton.exists)
editButton.tap()
XCTAssert(self.app.navigationBars["Complaint #100"].exists)
XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)
let saveButton = self.app.buttons["Save"]
XCTAssert(saveButton.exists)
saveButton.tap()
let okButton = self.app.buttons["Ok"]
XCTAssert(okButton.exists)
okButton.tap()
}
You shall probably use waitForExistence() instead of .exists
.exists executes as soon as it is called. You TableView may not be prepared to be tested at this moment.
I would try to replace .exists with waitForExistence()
let complaintCell = self.app.tables.cells.element(boundBy: 0)
XCTAssert(complaintCell.waitForExistence(timeout: 10))
complaintCell.tap()
Also you can ditch this XCTAssert.
tap() will wait for existence for 3 seconds and then produce an error message, if the cell does not exist.
The shortened version will be:
app.tables.cells.firstMatch.tap()
I've downloaded your demo.
While iPhone app start with Master View, the iPad app goes with SplitView or Detail View (in portrait orientation).
Portrait View should start with Master + Detail View. Other option to change UI tests like this:
func testExample() {
let table = self.app.tables["TableView"]
if !table.waitForExistence(timeout: 1) {
app.navigationBars.element.swipeRight()
XCTAssert(table.waitForExistence(timeout: 2))
}
...
}

Why is my button displayed but not clickable?

I've created a ViewController containing a user button, which is going to be present in several View Controllers in my application.
I'm adding this ViewController dynamically to the needed ViewControllers. The user button is shown, but it's not clickable. What am I doing wrong?
I've tried setting constraints to the view containing the button, setting the container view's frame, disabling user interaction in the container view (not in the button) and nothing seems to work
import UIKit
class ModulePageViewController: UIPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.addSharedButtonsSubView()
}
func addSharedButtonsSubView() {
let sharedButtons = storyboard?.instantiateViewController(withIdentifier: sharedButtonsViewControllerName)
view.addSubview((sharedButtons?.view)!)
sharedButtons?.view.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)
addChild(sharedButtons!)
sharedButtons?.didMove(toParent: self)
}
}
You can create a custom view (not ViewController) containing the button and just use it where you need in you app.
#LeCalore ...
I would recommend if you want to use a button or any more stuff on multiple View Controllers then you should just make a new ViewController with that button and whatever else you want on it then use it where ever you want.
ViewController -> Present As Pop Over (Presentation : Over Current Context)
I think that's a better approach atleast for starters.
Else, as user said ... you can make a custom view programatically and call it wherever you need that's another approach but it might give you a bit of trouble.
Open to others view if there's one better.
Gluck

Presented TabBarController disappearing after attempted segue

Short synopsis (XCode 7.2, Swift2, iOS 9.2 as target):
1) In a first.storyboard, I have a single viewController.
2) In a second.storyboard, I have a tabbarController, with multiple navigationControllers with tableviewControllers (see attached image). Also of note is when second.storyboard is the one used on launch, everything works correctly.
3) the main UI for the app is in the first.storyboard, and I want to present the tabbarcontroller in the second.storyboard
4) No matter which way I present it (storyboard reference/segue, presentViewController, showViewController), the tabbarcontroller and all the initial views work, but if I tap a tableviewcell to segue to another view, the whole tabbarcontroller and contents disappear, leaving me back at the viewcontroller in first.storyboard.
I can cheat, and set the rootViewController manually and things seem to work
let sb = UIStoryboard(name: "second", bundle: nil)
let navController = sb.instantiateViewControllerWithIdentifier("secondIdentifier") as! UITabBarController
UIApplication.sharedApplication().keyWindow?.rootViewController = navController
And I suspect I can add an animation to this to not have the transition not be so stark. But this seems like something I shouldn't have to do, and kind of a pain to troubleshoot in the future. Am I missing something fundamental in making this work?
EDIT: Video of it not working https://youtu.be/MIhR4TVd7CY
NOTE: The last app I made originally targeted iOS4, and I did all the views programatically. It seemed like all the updates to IB and segues etc would make life more manageable (and for the most part that has been true), but this is still my first foray in to it, so I may be missing some important points of information to describe the issue.
I have found a superior way to deal with this: UIViewControllerTransitioningDelegate
It's a bit of extra work to implement, but it produces a "more correct" result.
My solution was to make a custom UIStoryboardSegue that will do the animation as well as set the rootViewController.
import UIKit
class changeRootVCSeguePushUp: UIStoryboardSegue {
override func perform() {
let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let sourceView = self.sourceViewController.view
let destinationView = self.destinationViewController.view
let sourceFrame = sourceView.frame
let destinationStartFrame = CGRect(x: 0, y: sourceFrame.height, width: sourceFrame.width, height: sourceFrame.height)
let destinationEndFrame = CGRect(x: 0, y: 0, width: sourceFrame.width, height: sourceFrame.height)
destinationView.frame = destinationStartFrame
applicationDelegate.window?.insertSubview(self.destinationViewController.view, aboveSubview: self.sourceViewController.view )
UIView.animateWithDuration(0.25, animations: {
destinationView.frame = destinationEndFrame
}, completion: {(finished: Bool) -> Void in
self.sourceViewController.view.removeFromSuperview()
applicationDelegate.window?.rootViewController = self.destinationViewController
})
}
}
I could not find a way in interface builder, or in code other than changing the rootViewController to get this working. I would end up with various random navigation issue like overlapping navigation bars, segue animations not working correctly until I changed tabs, full on lockups with no information in the console, etc.
I have previously presented a tabBarcontroller modally (without changing rootviewController), but everything was done in code (working as of ios7 and objective-c). No clue what is going on under the covers when the view hierarchies are made in a storyboard, but wondering if this is perhaps a bug.
Thanks to multiple other answers here on stackoverflow to get to mine!

UITesting, XCTest current ViewController Class

Simple problem. I got button which perform segue to next view controller.
I want to write UI XCTest to tell me did it open view controller i wanted.
The UI Testing framework doesn't have access to your applications code which makes class assertions on instances impossible. You are not able to directly tell the class of the controller which is on screen.
However, if you think about your test a little differently you can make a very similar assertion. Write your tests as if you are the user. Your user doesn't care if he/she is looking at a ItemDetailViewController or a ItemListTableViewController so neither should your tests.
The user cares what's on the screen. What's the title? Or, what are the names of these buttons? Following that logic you are rewrite your test to assert based on those items, not the name of the coded class.
For example, if you are presenting your controller in a navigation stack you can assert the title.
let app = XCUIApplication()
app.buttons["View Item"].tap()
XCTAssert(app.navigationBars["Some Item"].exists)
Or, if the screen is presented modally but you know some static text or buttons, use those.
let app = XCUIApplication()
app.buttons["View Item"].tap()
XCTAssert(app.staticTexts["Item Detail"].exists)
XCTAssert(app.buttons["Remove Item"].exists)
Comment of Matt Green gave me a good idea. We can define an unused label/button, ideally inside a base view controller and assign it an accessibility label to perform a query to find out which view controller is presented.
public class BaseViewController: UIViewController {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
public override func viewDidLoad() {
super.viewDidLoad()
if let identifier = self.theClassName.split(separator: ".").last {
button.accessibilityIdentifier = String(identifier)
view.addSubview(button)
}
}
}
public class DatePickerViewController: BaseViewController {
...
}
func testExample() {
let app = XCUIApplication()
app.launch()
app.navigationBars.buttons["DateSelector"].tap()
XCTAssertTrue(app.buttons["DatePickerViewController"].exists)
}
Note that inorder to make this approach work you have to add the view you use to identify view controller, in this case a button, should be added as a sub view and has to have a non zero frame.

Resources