Why do I need to force the type using this Swift generic function? - swift

I had some repetitive UIViewController boiler-plate scattered around that I wanted to encapsulate, so I defined this generic UIViewController extension method:
extension UIViewController {
func instantiateChildViewController<T: UIViewController>(
storyboardName: String? = nil,
identifier: String? = nil
) -> T {
let storyboard: UIStoryboard!
if let name = storyboardName {
storyboard = UIStoryboard(name: name, bundle: nil)
}
else {
storyboard = UIStoryboard(name: "\(T.self)", bundle: nil)
}
let vc: T!
if let identifier = identifier {
vc = storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
else {
vc = storyboard.instantiateInitialViewController()! as! T
}
self.addChildViewController(vc)
self.view.addSubview(vc.view)
return vc
}
}
However, when I use this extension like so:
class ChildViewController: UIViewController { /*...*/ }
class ParentViewController: UIViewController {
private var childVC: ChildViewController!
//...
func setupSomeStuff() {
self.childVC = self.instantiateChildViewController() //<-- Compiler error
let vc: ChildViewController = self.instantiateChildViewController() //<-- Compiles!
self.childVC = vc
}
}
I get the compiler error Cannot assign value of UIViewController to type ChildViewController! on the line with the comment above. However, if I use an intermediate variable that I explicitly give a type to it works.
Is this a Swift bug? (Xcode 8.1) My interpretation of how generics work is that in this case T should equal the more specific ChildViewController, not the less constrained UIViewController. I get the same issue if I defined childVC as private var childVC: ChildViewController?, the only work-around I've found is the local variable, which obviously makes the extension less compelling, or to do an explicit cast like:
self.childVC = self.instantiateChildViewController() as ChildViewController

I've seen this too. I think there's some weird behavior around Optionals the compiler isn't dealing with as expected.
If you change the return value of the function to an optional value it should work without a problem.
func instantiateChildViewController<T: UIViewController>(//whateverParams) -> T!
or
func instantiateChildViewController<T: UIViewController>(//whateverParams) -> T?
Also, your childVC should be a var rather than a let if you're going to set it anyplace other than an initializer

Related

How to call a function from another controller in swift

I set the Show Charts button on the DetailView Controller which triggers the getChartData function and shows me the values in display view in charts, now I want to call that function in the didselectrow on the main Viewcontroller so that the chart is loaded automatically, but it fails.
When I tried to call that function in didselectrow (DVC.getChartsData) I got the error "Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value"
DVC.getChartsData
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
ViewController:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let Storyboard = UIStoryboard(name: "Main", bundle: nil)
let DVC = Storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
DVC.getDetailName = coin[indexPath.row].name
let formatedRoundingPrice = (coin[indexPath.row].price as NSString).floatValue * currencymodel.indexValue
let formatedPrice = String (format: "%.3f", formatedRoundingPrice)
DVC.getDetailPrice = formatedPrice
self.navigationController?.pushViewController(DVC, animated: true)
let percentage = String ((coin[indexPath.row].percent as NSString).floatValue)
DVC.getDetailPercent = percentage
tableView.deselectRow(at: indexPath, animated: true)
//DVC.getChartData()
}
DetailViewController:
#IBAction func tapLineChart(_ sender: Any) {
getChartData()
}
func getChartData () {
let chart = HITLineChartView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: displayView.bounds.height))
displayView.addSubview(chart)
let max = String((priceResult.max() ?? 0.0).rounded(.up))
let min = String((priceResult.min() ?? 0.0).rounded(.down))
let maxChange = abs((listOfChanges.max()) ?? 0.0).rounded(.up)
let minChange = abs((listOfChanges.min()) ?? 0.0).rounded(.up)
absMaxPercentage = Int(maxChange > minChange ? maxChange : minChange)
titles = ["\(getDetailName) closing price is \(getDetailPrice)"]
print(data)
chart.draw(absMaxPercentage,
values: listOfChanges,
label: (max: max, center: "", min: min),
dates: namesArray,
titles: titles)
addCloseEvent(chart)
finalURL = baseURL + "bitcoin" + "/market_chart?vs_currency=usd&days=5"
print(finalURL)
getBitcoinData(url: finalURL)
}
How to load my charts tap on a specific tableview cell instead of tapping on tapLineChart.
https://imgur.com/fg2502P
https://imgur.com/C4AzaRY
https://imgur.com/jOrwujy
if you want to call a function on viewControllerB that you declare from viewController A.
just create the object of the class file you want to use the function from
var obj mainVC = MainViewController()
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func commonMethod() {
print("From the main class")
}
}
Using that object, call the function in another file where you mean to use it
class OtherViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
mainVC.commonMethod()
// Do any additional setup after loading the view, typically from a nib.
}
}
Additionally, You can also create a new swift file, name it Global.swift, create all your functions that you want to use throughout the application here. They become "global functions"
You will want to use delegates or observers to pass data between view controllers.
I'm new to tutorials, but I wrote a bit about this here: https://www.eankrenzin.com/swift-blog/pass-data-throughout-your-app-with-observers-and-notifications-xcode-11-amp-swift-5
You should use optional binding to unwrap your VC let DVC = Storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
Your code is crashing because of that line. Check your interface builder to make sure the identifier is correct. Edit: this line was not causing a crash, but it is still better to use optional binding.The line is: https://imgur.com/CVP1x6H
NOTE: It is terrible practice to litter your app with instances when delegates and observers could work. Also do NOT have globals. Globals are disastrous for debugging and create tech debt.

custom Swift function has errors

I have formulated the function below so that I can re-use it in my ios app. However, I am unable to build my app because my function below has errors indicated to me but I cannot see what is wrong with it. The function is intended to move the user to a new page in the ios app. Please can someone advise?
func goToPage(goto storyBoardId: String, ofType typeUIViewController: UIViewController.Type) -> UIViewController {
let storyBoard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let newPage = storyBoard.instantiateViewController(withIdentifier: storyBoardId) as! typeUIViewController // Error: use of undeclared type 'typeUIViewController'
self.present(newPage, animated: true, completion: nil)
return newPage
}
Your function expects a return value of type UIViewController and you return nothing. So either return an instance you create (if you need one). Or remove return value.
There's a slightly modified variation of your function with generics, which does just what you want. The #discardableResult word before the function tells the compiler that the result can be omitted.
#discardableResult
func goToPage<T>(goto storyBoardId: String,
ofType typeUIViewController: T.Type) -> T
where T: UIViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let newPage = storyboard.instantiateViewController(withIdentifier: storyBoardId) as! T
self.present(newPage, animated: true, completion: nil)
return newPage
}
USAGE
// Ignore return value
goToPage(goto: "Page", ofType: ViewController.self)
// Preserve return value:
// Thanks to generics, page and page2 types are inferred by the compiler
// page is CustomController and page2 is LoginController
// and you can access corresponding interface:
var page = goToPage(goto: "Page", ofType: CustomController.self)
var page2 = goToPage(goto: "Page", ofType: LoginController.self)
Update
I see that Honey suggests the right idea, but the problem with typecasting still persists. The reason of that is that the compiler has no idea what kind of type typeUIViewController is. In fact, it is not actually even a type, it's only an inner name of the variable. And there is no way compiler could infer the type of it (to use with as operator). Hence, one of the proper ways to achieve what you are trying is by using generics. Consider generic T as a pattern which meets certain condition.
You need to change UIViewController to UIViewController.Type. For more see here
because a parameter of UIViewController can accept a UIViewController instance e.g. UIViewController(). However you need to get its type information (you don't need an instance), therefore it has to be a parameter of type UIViewController.Type so the value you pass can be something like SomeUIViewControllerSubclass.self which is NOT an instance...
So you have to do this:
func goToPage(goto storyBoardId: String, ofType typeUIViewController: UIViewController.Type) -> UIViewController {
let storyBoard: UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let newPage = storyBoard.instantiateViewController(withIdentifier: storyBoardId) as! typeUIViewController
self.present(newPage, animated: true, completion: nil)
}

Swift: programmatically enumerate outgoing segues from a UIVIewController

I want to list the outgoing segues from a UIViewController, as described in Programmatically enumerate outgoing Segues for a UIViewController, but in Swift. (Swift 2, Xcode 7, iOS8+).
I can do
override func viewDidLoad() {
super.viewDidLoad()
let s = valueForKey("storyboardSegueTemplates")
print("switchingVC: segues: \(s)")
}
and that produces output like
switchingVC: segues: Optional((
"<UIStoryboardPresentationSegueTemplate: 0x1754a130>",
"<UIStoryboardPresentationSegueTemplate: 0x17534f60>",
"<UIStoryboardPresentationSegueTemplate: 0x17534fc0>"
))
but I struggle to produce anything after that. I can't find any definition of the UIStoryboardPresentationSegueTemplate. How can I persuade Swift to tell me what's inside it? How can I find the segue identifier?
Thanks!
this valueForKey("storyboardSegueTemplates") is UNDOCUMENTED property and UIStoryboardPresentationSegueTemplate is UNDOCUMENTED class. Beware of rejection from App Store if you are uploading application to App Store.
If you want to use this in your in-house projects, use as following
for template in (valueForKey("storyboardSegueTemplates") as? [AnyObject])! {
if let identifier = template.valueForKey("identifier") as? String {
print("identifier - " + identifier)
}
else {
print("no identifier for \(template)")
}
}
Found from https://github.com/JaviSoto/iOS9-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UIStoryboardSegueTemplate.h
As per Swift 4.2 and from https://stackoverflow.com/a/35060917/1058199. Thanks /johnykutty.
import UIKit
extension UIViewController {
// Segue aids in Swift
#objc func isValidSegue(_ segueId: String?) -> Bool {
let filteredArray = (value(forKey: "storyboardSegueTemplates") as? NSArray)?.filtered(using: NSPredicate(format: "identifier = %#", segueId ?? ""))
let isValid = (filteredArray?.count ?? 0) > 0
return isValid
}
#objc func segues() -> Array<Any>? {
let segues = self.value(forKey: "storyboardSegueTemplates")
return segues as! Array<Any>?
}
#objc func segueNames() -> Array<AnyHashable> {
var segueNames = Array<Any>()
let filteredArray = (value(forKey: "storyboardSegueTemplates") as? NSArray)?.filtered(using: NSPredicate(format: "identifier != nil" ))
for template in filteredArray! as [AnyObject] {
if let identifier = (template.value(forKey: "identifier") as? String) {
segueNames.append(identifier)
}
else {
segueNames.append("no identifier for \(template)")
}
}
return segueNames as! Array<AnyHashable>
}
}
I know my use of predicates could be better, but Swift is such a PITA when dealing with iterating arrays. Please feel free to improve this.

Cannot load view using NSNib

Anyone knows the correct way to instantiate a view using NSNib in Swift ?
The code below crashes.
import Cocoa
class ViewController: NSViewController {
var myview:NSView!
override func viewDidLoad() {
super.viewDidLoad()
let obj = AutoreleasingUnsafeMutablePointer<NSArray?>()
if let success = NSNib(nibNamed: "View", bundle: NSBundle.mainBundle())?.instantiateWithOwner(self, topLevelObjects: obj) where success {
if let m = obj.memory { // fatal error:
myview = m[0] as! NSView
}
}
}
}
You are seeing the fatal error there because the value you are trying to access is nil and you are forcefully trying to unwrap the optional. You should safely unwrap optionals this is what they are meant for. So, always use if statement when you try to access optionals, then you won't get this error.

Sending data to another view: can't unwrap option

I know that this has to be a simple fix, but can't seem to understand why my code is not working. Basically I am trying to send a value from a text field in 1 view to a 2nd view's label.
ViewController.swift
#IBOutlet var Text1st: UITextField
#IBAction func Goto2ndView(sender: AnyObject) {
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
//view2.Label2nd.text=text;
self.navigationController.pushViewController(view2, animated: true)
}
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = (VC.Text1st.text) //it doesn't like this, I get a 'Can't unwrap Option.. error'
println(string)
}
-------EDITED UPDATED CODE FROM (drewag)-------
ViewController.swift
let text = "text"
var sendString = Text1st.text
println(sendString) //successfully print it out.
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
view2.Label2nd.text=sendString;
self.navigationController.pushViewController(view2, animated: true)
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = self.Label2nd.text
println(string) //still getting the error of an unwrap optional.none
}
var VC = ViewController() creates a new instance of ViewController. Unless there is a default value, you are not going to get any value out of VC.Text1st.text. You really should use a string variable on your second view controller to pass the data to it.
Also, a note on common formatting:
Class names should start with a capital letter (as you have)
Method / function names should start with a lower case letter
UIViewController subclasses should have "Controller" included in their name, otherwise, it looks like it is a subclass of UIView which is an entirely different level of Model View Controller (the architecture of all UIKit and Cocoa frameworks)
Edit:
Here is some example code:
class ViewController1 : UIViewController {
...
func goToSecondView() {
var viewController = ViewController2()
viewController.myString = "Some String"
self.navigationController.pushViewController(viewController, animated: true)
}
}
class ViewController2 : UIViewController {
var myString : String?
func methodToUseMyString() {
if let string = self.myString {
println(string)
}
}
...
}
Note, I am not creating ViewController2 using a storyboard. I personally prefer avoiding storyboards because they don't scale well and I find editing them to be very cumbersome. You can of course change it to create the view controller out of the storyboard if you prefer.
jatoben is correct that you want to use optional binding. IBOutlets are automatically optionals so you should check the textfield to see if it is nil.
if let textField = VC.Text1st {
println(textField.text)
}
This should prevent your app from crashing, but it will not print out anything because your text field has not yet been initialized.
Edit:
If you want to have a reference to your initial ViewController inside your second you're going to have to change a few things. First add a property on your second viewcontroller that will be for the first view controller:
#IBOutlet var Label2nd: UILabel //existing code
var firstVC: ViewController? //new
Then after you create view2, set it's firstVC as the ViewController you are currently in:
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2 //already in your code
view2.firstVC = self //new
Finally in your viewDidLoad in your second view controller, use firstVC instead of the ViewController you recreated. It will look something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let textField = firstVC?.Text2nd {
println(textField.text)
}
}
Use optional binding to unwrap the property:
if let string = VC.Text1st.text {
println(string)
}