Swift view appears to delay being loaded into window hierarchy - swift

A very strange thing is happening, it appears my initial view controller loads up, performs its logic and all but the actual view itself isn't added to the hierarchy until everything else happens.
protocol OnboardDisplayLogic: class
{
func handleUserConfirmation(viewModel: OnboardModels.CheckForUser.ViewModel)
}
class OnboardViewController: UIViewController, OnboardDisplayLogic {
var interactor: OnboardInteractor?
var router: (NSObjectProtocol & OnboardRoutingLogic & OnboardDataPassing)?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
//MARK: Setup
private func setup() {
let viewController = self
let interactor = OnboardInteractor()
let presenter = OnboardPresenter()
let router = OnboardRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
private func setupView() {
view.backgroundColor = UIColor.brown
}
//MARK: Routing
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let scene = segue.identifier {
let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
if let router = router, router.responds(to: selector) {
router.perform(selector, with: segue)
}
}
}
//MARK: View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
interactor?.checkForUser()
}
func handleUserConfirmation(viewModel: OnboardModels.CheckForUser.ViewModel) {
if let username = viewModel.username {
print("Logged in user: \(username)")
router?.routeToUserFeed(segue: nil)
} else {
print("No user logged in")
router?.routeToLoginRegister(segue: nil)
}
}
The reason I think it's delayed is that although I've set the view background colour to brown, using breakpoints at the print("user not logged in") code I get that printed to console, because there is no user logged in as determined the Interactor, but view remains white. It's not until I step over and complete running all the code does the screen then become brown.
Therefore I'm getting the error
Warning: Attempt to present ... whose view is not in the window
hierarchy!
as something is preventing the view from being added until the very end.

Actual views aren't loaded until they enter the view controller lifecycle.
If you need to load the view to setup before it's added to a superview's hierarchy, just use the getter of the view (let _ = viewController.view).
UIViewControllers also have the loadView() method, check it out: Apple documentation.
We've also had these kind of problems with Clean Swift. The correct way of working would be to instantiate from the storyboard and just use normal segues/lifecycle, just as MVC does for Apple examples.
Instead of doing let viewControllerToPush = MyVCSublass(...), you should instantiate from storyboard let viewControllerToPush = storyboard?.instantiate... and use that view controller.

Related

Testing tableview.reloadData()

while using a MockTableView this code still not calling reloadData() from the mock,
please i wanna know what is wrong here.
following this book: Test-Driven IOS Development with Swift 4 - Third Edition
page 164, i was as an exercise
full code repo - on github
ItemListViewController.swift
import UIKit
class ItemListViewController: UIViewController, ItemManagerSettable {
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate &
ItemManagerSettable)!
var itemManager: ItemManager?
override func viewDidLoad() {
super.viewDidLoad()
itemManager = ItemManager()
dataProvider.itemManager = itemManager
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController =
storyboard?.instantiateViewController(
withIdentifier: "InputViewController")
as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
}
ItemListViewControllerTest.swift
import XCTest
#testable import ToDo
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
var addButton: UIBarButtonItem!
var action: Selector!
override func setUpWithError() throws {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier:
"ItemListViewController")
sut = vc as? ItemListViewController
addButton = sut.navigationItem.rightBarButtonItem
action = addButton.action
UIApplication.shared.keyWindow?.rootViewController = sut
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {}
func testItemListVC_ReloadTableViewWhenAddNewTodoItem() {
let mockTableView = MocktableView()
sut.tableView = mockTableView
guard let addButton = sut.navigationItem.rightBarButtonItem else{
XCTFail()
return
}
guard let action = addButton.action else{
XCTFail()
return
}
sut.performSelector(onMainThread: action, with: addButton, waitUntilDone: true)
guard let inputViewController = sut.presentedViewController as?
InputViewController else{
XCTFail()
return
}
inputViewController.titleTextField.text = "Test Title"
inputViewController.save()
XCTAssertTrue(mockTableView.calledReloadData)
}
}
extension ItemListViewControllerTest{
class MocktableView: UITableView{
var calledReloadData: Bool = false
override func reloadData() {
calledReloadData = true
super.reloadData()
}
}
}
You inject a MockTableview Then you call loadViewIfNeeded(). But because this view controller is storyboard-based and the table view is an outlet, the actual table view is loaded at this time. This replaces your MockTableview.
One solution is:
Call loadViewIfNeeded() first
Inject the MockTableview to replace the actual table view
Call viewDidLoad() directly. Even though loadViewIfNeeded() already called it, we need to repeat it now that we have a different tableview in place.
Another possible solution:
Avoid MockTableview completely. Continue to use a real table view. You can test whether it reloads data by checking whether the number of rows matches the changed data.
Yet another solution:
Avoid storyboards. You can do this with plain XIBs (but these lack table view prototype cells) or programmatically.
By the way, I see all your tearDownWithError() implementations are empty. Be sure to tear down everything you set up. Otherwise you will end up with multiple instances of your system under test alive at the same time. I explain there here: https://qualitycoding.org/xctestcase-teardown/

Xcode Wkwebview not Loading

I have a Xcode Project with a Webview and a TabBar and with the TabBar I can switch between WebViews.
My Problem is that when I put something in my ShoppingCard under lieferworld.de and switch with the TabBar to my Shopping Card url the Items in there are not Visible. How can I solve this? the ShoppingCard URL ends with .php. Below is the code which is implemented
Here is also a Video were you can see the error:
https://youtu.be/qU3Mu1G7MY0
Viewhome:
import UIKit
import WebKit
class viewHome: UIViewController, WKUIDelegate {
#IBOutlet var webViewHome: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webViewHome = WKWebView(frame: .zero, configuration: webConfiguration)
webViewHome.uiDelegate = self
webViewHome.configuration.preferences.javaScriptEnabled = true
//webViewHome.configuration.preferences.javaEnabled = true
view = webViewHome
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://lieferworld.de")
let request = URLRequest(url: url!)
webViewHome.configuration.preferences.javaScriptEnabled = true
//webViewHome.configuration.preferences.javaEnabled = true
webViewHome.load(request)
}
#IBAction func GoBackHome(_ sender: Any) {
if webViewHome.canGoBack {
webViewHome.goBack()
}
}
#IBAction func GoForwardHome(_ sender: Any) {
if webViewHome.canGoForward {
webViewHome.goForward()
}
}
}
ViewShopping | Shopping Cart Class:
import UIKit
import WebKit
class viewShopping: UIViewController, WKUIDelegate {
#IBOutlet var webViewShopping: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webViewShopping = WKWebView(frame: .zero, configuration: webConfiguration)
webViewShopping.uiDelegate = self
//webViewShopping.configuration.preferences.javaEnabled = true
webViewShopping.configuration.preferences.javaScriptEnabled = true
view = webViewShopping
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://lieferworld.de/warenkorb.php")
let request = URLRequest(url: url!)
webViewShopping.configuration.preferences.javaScriptEnabled = true
//webViewShopping.configuration.preferences.javaEnabled = true
webViewShopping.load(request)
}
#IBAction func goBackShoppingCart(_ sender: Any) {
if webViewShopping.canGoBack {
webViewShopping.goBack()
}
}
#IBAction func goForwardShoppingCart(_ sender: Any) {
if webViewShopping.canGoForward {
webViewShopping.goForward()
}
}
#IBAction func webViewRefresh(_ sender: Any) {
webViewShopping.reload()
}
}
Your data is not being shared between the two UIViewControllers because your WKWebViews are different instances of each other and don't share the same data. It's as if you opened two different tabs in your web browser. If you're logged in, and the cart data is stored on the server, it should work, if you refresh the cart's page. If it's cookie based, you'll need to ensure those cookies are common among the two web views. Instead, use the tab to redirect to the cart's page instead of creating a new View Controller instance, or put the instance of the WKWebView into a static variable that can be shared between the two tabs. You'll have to do a bit of hiding/showing of that view so that you don't see it loading, but that can be handled via the delegate.
If the data is stored on the server, you can simple just reload the webpage within the viewDidAppear() overridden function in your VC. It really depends on what you're going for, but those are a few suggestions that'll work.

AVPlayerViewController pops entire stack of view controllers when closed

I am using an AVPlayerViewController to display video content within my application. This player can be reached via a sequence of views. The sequence is embedded in a navigation view controller. The problem I have is that whenever I close the player the entire stack of view controllers is popped from the list of view controllers in the navigation controller which means that I am sent back to my home screen (however I only want to pop the AVPlayerViewController from the list and return to the screen before). I tried to find a way to override the close Button but did not find a way. Moreover I tried to push a notification and handle it in the home screen by reinitializing the entire stack of view controllers - this solution works but does not seem like the appropriate solution. I attached the class that inherits from AVPlayerViewController and the code that implements the viewController. Thankful for any hint.
import UIKit
import AVKit
import AVFoundation
// MARK: - EduMediaVideoViewController
class EduMediaVideoViewController: AVPlayerViewController, EduMediaViewController {
// MARK: Stored Type Properties
weak var eduMediaDelegate: EduMediaElementDelegate?
var videoMedia: VideoMediaElement?
// MARK: Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setUpVideo()
}
// MARK: Instance Methods
func setContent(content: MediaElement) {
guard let videoMedia = content as? VideoMediaElement else {
return
}
self.videoMedia = videoMedia
}
// MARK: Private Instance Methods
private func setUpVideo() {
let playerItem = videoMedia?.video
let player = AVPlayer(playerItem: playerItem)
self.player = player
player.play()
}
private func updateVideoProgress() {
self.eduMediaDelegate?.updateProgress(id: 1, progress: 1)
}
}
// create an extension of AVPlayerViewController
extension EduMediaVideoViewController {
// override 'viewWillDisappear'
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let category = eduMediaDelegate?.getCategory() else {
return
}
let dict = ["category": category]
NotificationCenter.default.post(name: .kAVDismissNotification, object: nil, userInfo: dict)
}
}
extension Notification.Name {
static let kAVDismissNotification = Notification.Name.init("dismissing")
}
The code that initializes the video controller:
let eduVideoViewController = EduMediaVideoViewController()
eduVideoViewController.setContent(content: mediaElement)
eduVideoViewController.eduMediaDelegate = self
navigationController?.pushViewController(eduVideoViewController, animated: false)

Two UIViews that share ALMOST the same UIViewController

I have a "Buy" page and a "Sell" page for products. They share the SAME UIViewController except for the fact that in one UIViewController it shows items for sale, and the other one for purchase. (Another difference is one button - it's has string A displayed in one and B in the other.
Can I make them share the same UIViewController with those changes, tho ? Can I define some arguments for each view ?
You could use a base UIViewController that had two trivially different subclasses. Put all of the logic in the base and just the differences in the subclasses...
class BaseVC: UIViewController {
#IBOutlet weak var button: UIButton!
var items: [String] = []
#IBAction func buttonPressed(_ sender: Any) {
// do stuff
}
// add all of your tableView or similar common stuff here...
}
class AVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.button.titleLabel?.text = "A"
self.items = ["Sell1", "Sell2"]
}
}
class BVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.button.titleLabel?.text = "B"
self.items = ["Buy1", "Buy2"]
}
}
Create a xib file,
Add a view in it,
Use that view in view controller with different functions on different buttons
Watch this video for help: https://www.youtube.com/watch?v=tvQxXoV527w&t=754s
Use a BaseViewController and make it the parent class of your new view controllers
Something like below:
BaseController:
import UIKit
class BaseViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
//Common initiation codes
}
// Write all the codes that can be shared between the controllers
}
Make separate classes for each of your view controllers and modify them as you require:
class SettingsController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()//This calls everything you wrote as init code for BaseController
//Write any view controller specific init code here
}
//If you have any other settings controller specific code, write here
}
Since I haven't seen your code, I can't say for sure if this is the right way. This method would still mean that you don't have to rewrite your code for each view controller classes and still keep it clean.
The above method is preferred when you are doing your layout and views by code.
If you are using a story board and depending on your specific need, I would suggest you do something like this instead:
//Initiate your controller like this from the previous controller
#IBAction func goToSettingsController(_ sender: Any) {
let baseController = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ControllerID") as! BaseController
baseController.id = "Settings"
self.present(baseController, animated: true, completion: nil)
}
#IBAction func goToAboutController(_ sender: Any) {
let baseController = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ControllerID") as! BaseController
baseController.id = "About"
self.present(baseController, animated: true, completion: nil)
}
And in your BaseController:
class BaseController: UIViewController{
var id: String!
#IBOutlet weak var backGround: UIView!
override func viewDidLoad() {
super.viewDidLoad()
customInit()
}
func customInit(){
switch id{
case "Settings":
self.backGround.backgroundColor = .purple //Any controller specific code
break
case "About":
self.backGround.backgroundColor = .green //Any controller specific code
break
default:
break
}
}
}
You won't need three separate classes as mentioned before, but you can use the BaseController class for both your ViewControllers.

How to add a tap gesture to multiple UIViewControllers

I'd like to print a message when an user taps twice on the remote of the Apple TV. I got this to work inside a single UIViewController, but I would like to reuse my code so that this can work in multiple views.
The code 'works' because the app runs without any problems. But the message is never displayed in the console. I'm using Swift 3 with the latest Xcode 8.3.3. What could be the problem?
The code of a UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
_ = TapHandler(controller: self)
}
The code of the TapHandler class
class TapHandler {
private var view : UIView?
required init(controller : UIViewController) {
self.view = controller.view
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.message))
tapGesture.numberOfTapsRequired = 2
self.view!.addGestureRecognizer(tapGesture)
self.view!.isUserInteractionEnabled = true
}
#objc func message() {
print("Hey there!")
}
}
Your TapHandler just getting released. Try This:
var tapHandler:TapHandler? = nil
override func viewDidLoad() {
super.viewDidLoad()
tapHandler = TapHandler(controller: self)
}
I have tested the code and is working.