Page View Controller viewDidLoad() Issues - swift

I'm learning Swift/Xcode and trying to create an app with three pages that you can swipe back and forth using a Page View Controller. The issues I'm having lie in the UIViewController subclass, more specifically the viewDidLoad() function. I'm getting errors like "value of type 'NameOfMyClass' has no member datasource/delegate" and "use of unresolved identifier "setViewControllers". I've followed many tutorials and checked other posts but no one seems to be having these issues. When I attempted to run this I would get a black screen, and now I get a terminated due to signal 15 error.
Here's the relevant code where the errors are popping up:
import UIKit
class RootPageViewController: UIViewController,
UIPageViewControllerDataSource, UIPageViewControllerDelegate {
lazy var viewControllerList:[UIViewController] = {
return [self.VCInstance(name: "MissionOne"),
self.VCInstance(name: "MissionTwo"),
self.VCInstance(name: "MissionThree")]
}()
private func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(withIdentifier: name)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self //...has no member 'dataSource' error
self.delegate = self //...has no member 'delegate' error
if let MissionOne = viewControllerList.first {
setViewControllers([MissionOne], direction: .forward, animated:
true, completion: nil)
//use of unresolved indentifier 'setViewControllers' error
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
The other issue I'm having, and it may be related, is that the Page View Controller is not accepting/recognizing my class file to set as the custom class in the story board.
Thanks in advance for any insight or suggestions.

your RootPageViewController is subclass of UIViewController, not a UIPageViewController

I looks to me that you are mixing two different controllers here.
What you want to do is add 3 RootPageViewController to a UIPageViewController, yet here you are trying to add 3 RootPageViewControllers to a RootPageViewController!
One way to fix it is to have a RootPageViewController and a separate UIPageViewController which will contain all RootPageViewControllers like so:
// This is a UIPageViewController containing all RootPages
class PagesViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
lazy var viewControllerList:[UIViewController] = {
return [RootPageViewController.VCInstance(name: "MissionOne"),
RootPageViewController.VCInstance(name: "MissionTwo"),
RootPageViewController.VCInstance(name: "MissionThree")]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let MissionOne = viewControllerList.first {
setViewControllers([MissionOne], direction: .forward, animated:
true, completion: nil)
}
}
}
And the RootPageViewController which will be added in the UIPageViewController above:
// This is a UIViewController representing a single page
class RootPageViewController: UIViewController {
static func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
// Do what you need a page to do...
}
With such a construction you will have all your errors solved and you will also be able to use PagesViewController as custom class in your Storyboard!

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/

Swift Call Particular function from another class for Present ViewController

My Scenario, I am trying to call a ViewController class file particular function from another one class file. Here, I am getting below warning and ViewController not presenting.
My Code Below ViewControllerA
func previewview(){ // Inside ViewControllerA
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let fileViewController = storyboard.instantiateViewController(withIdentifier: "fileviewcontroller")
let navController = UINavigationController(rootViewController: fileViewController)
self.present(navController, animated: true, completion: nil)
} }
Below code In Another Class File
import UIKit
class FileController {
//MARK:- Call a file preview
ViewControllerA().self.previewview()
}
Warning: Attempt to present on
whose view is not in the window
hierarchy!
Try this
Viewcontroller().FuncName()
Example:
LoginViewController().checkLoginValidation()
Use swift's Delegate Protocol to call a function in a class from different class.
Here is a link for understanding how delegate protocol works.
https://medium.com/#nimjea/delegation-pattern-in-swift-4-2-f6aca61f4bf5
Here is how you can proceed. This is just an example. You can take the idea and implement in you code as per your requirement.
Instead of creating a separate ViewController, create a ViewModel that handles the implementation of saving the file, i.e.
class SaveOptionsViewModel {
func save(file: String, handler: (()->())?) { //add parameters to save a file as per requirement
//save the file here...
handler?()
}
}
Now, in the controller that contains multiple save options, create a property of type SaveOptionsViewModel.
And present PreviewVC from SaveOptionsVC in the handler once the file is saved using the SaveOptionsViewModel after tapping the saveButton.
class SaveOptionsVC: UIViewController {
let viewModel = SaveOptionsViewModel()
#IBAction func onTapSaveButton(_ sender: UIButton) {
self.viewModel.save(file: "") {
if let previewVC = self.storyboard?.instantiateViewController(withIdentifier: "PreviewVC") {
self.present(previewVC, animated: true, completion: nil)
}
}
}
}
Add the custom implementation of PreviewVC as per your requirement.
class PreviewVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
//add the code...
}

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 do I pass data from a View controller into my pop up view controller (swift/ios)

I'm quite new with Swift and I'm making this mini game type app that counts the score and updates the label in the view controller. I want to pass that score from a view controller into another external pop up view controller I created.
#IBAction func Button7Tapped(_ sender: AnyObject)
{
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
Above is my code for the external pop up view controller I created, which also has a separated .swift file. How would I go about taking my game.score and passing that into my Popup view controller?
In your finalScoreViewController swift file add a new property.
final class FinalScoreViewController: UIViewController {
var score: Int?
}
And then just assign it when you're instantiating it.
#IBAction func Button7Tapped(_ sender: AnyObject) {
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
scorepopVC.score = game.score //THIS LINE
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
It is better to use storyboard to open the ViewController. In storyboard, right click and drag from you button to the second view controller (the one that you wish to open).
Choose the segue type that you wish to use. In your case, I think Present Modally will work fine.
You will see a line between the two UIViewControllers in storyboard. That is the segue. Tap on it. In the Attributes inspector give the segue an identifier. For instance "myFirstSegue".
Then in the code of the UIViewController that contains your button override prepare(for:sender:). This method is called when preparing for the segue to happen. I.o.w when you tap on the button. You have access to the destination UIViewController and can therefor access and set the properties on it.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "myFirstSegue" {
if let vc = segue.destination as? MyViewController {
//here you set your data on the destination view controller
vc.myString = "Hello World"
}
}
}
Note that we check the identifier, because all segues that go from this ViewController to other ViewControllers will call prepare(for:sender:)
It's quite simple, Just add a property in your finalScoreViewController (if you are not already done this) and -for example- call it score:
class finalScoreViewController: UIViewController {
var score: String?
// ...
Add this line to the Button7Tapped action (where you set a value for finalScoreViewController's score):
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
// add this line:
scorepopVC.score = "My score"
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
Finally, in finalScoreViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let scr = score {
print(scr)
}
}
Hope that helped.
You do not actually have to pass the variable to the next view controller. All you have to do is create a variable outside of the View Controller class, and voila, you can access your variable from anywhere, in any swift file. For example:
var score = 0
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
}
#IBAction func Button7Tapped(_ sender: AnyObject){
score += 1
}
}
And then in the other View Controller, you would have something like this:
#IBOutlet weak var scoreLabel: UILabel!
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
var timer1 = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateScore), userInfo: nil, repeats: true)
}
#objc func updateScore() {
scoreLabel.text = "You have \(score) points!"
}

Having issues setting delegate with Observer Pattern

I'm trying to realize the Observer Pattern and I'm experiencing some difficulty as my delegate doesn't seem to be setting properly.
In my Main.storyboard I have a ViewController with a container view. I also have an input box where I'm capturing numbers from a number keypad.
Here's my storyboard:
I'm trying to implement my own Observer Pattern using a protocol that looks like this:
protocol PropertyObserverDelegate {
func willChangePropertyValue(newPropertyValue:Int)
func didChangePropertyValue(oldPropertyValue:Int)
}
My main ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var numberField: UITextField!
// observer placeholder to be initialized in implementing controller
var observer : PropertyObserverDelegate?
var enteredNumber: Int = 0 {
willSet(newValue) {
print("//Two: willSet \(observer)") // nil !
observer?.willChangePropertyValue(5) // hard coded value for testing
}
didSet {
print("//Three: didSet")
observer?.didChangePropertyValue(5) // hard coded value for testing
}
}
#IBAction func numbersEntered(sender: UITextField) {
guard let inputString = numberField.text else {
return
}
guard let number : Int = Int(inputString) else {
return
}
print("//One: \(number)")
self.enteredNumber = number // fires my property observer
}
}
My ObservingViewController:
class ObservingViewController: UIViewController, PropertyObserverDelegate {
// never fires!
func willChangePropertyValue(newPropertyValue: Int) {
print("//four")
print(newPropertyValue)
}
// never fires!
func didChangePropertyValue(oldPropertyValue: Int) {
print("//five")
print(oldPropertyValue)
}
override func viewDidLoad() {
super.viewDidLoad()
print("view loads")
// attempting to set my delegate
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
}
}
Here's what my console prints:
What's happening
As you can see when my willSet fires, my observer is nil which indicates that I have failed to set my delegate in my ObservingViewController. I thought I set my delegate using these lines:
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
However, I must be setting my delegate incorrectly if it's coming back nil.
Question
How do I properly set my delegate?
You are calling into the storyboard to instantiate a view controller and setting it as the observer, however that instantiates a new instance of that view controller, it doesn't mean that it is referencing the one single "view controller" that is in the storyboard. ObservingViewController needs another way to reference the ViewController that has already been created.
So #Chris did reenforce my suspicions which helped me to figure out a solution for assigning my delegate to my view controller properly.
In my ObservingViewController I just need to replace the code in my viewDidLoad with the following:
override func viewDidLoad() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let vc = app.window?.rootViewController as! ViewController
vc.observer = self
}
Rather than creating a new instance of my view controller, I'm now getting my actual view controller.