Storyboard dependency injection for scene with custom objects - ios

I have a custom init? method on destination scene called ListCountriesViewController which is called inside a method "createListCountriesViewController" located in ViewController and set on a storyboard segue instantiation property at "Connection Inspector" which connects ViewController and ListCountriesViewController scenes. ListCountriesViewController contains a custom object which is created via storyboard "Object". When segue executes the method "createListCountriesViewController" is executed twice, and the second execution terminates with NSExeption:
Thread 1: "Custom instantiated
<pocStoryboardDependencyInjection.ListCountriesViewController:
0x148005e40> must be kind of class
pocStoryboardDependencyInjection.CustomObject"
after deleting the custom object from storyboard scene the problem is solved.
Is Storyboard Dependency Injection, which was included in UIKit/iOS 13, compatible with objects which are set on scene like: Objects (custom class), Additional views (custom class), Gestures (custom class) If yes, how can I solve my problem?
class ViewController: UIViewController {
#IBSegueAction
func createListCountriesViewController(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ListCountriesViewController? {
let controller = ListCountriesViewController(coder, userForm: ["field1"])
return controller
}
}
class ListCountriesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var customObject: CustomObject!
init?(_ coder: NSCoder, userForm: UserForm) {
self.userForm = userForm
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError()
}
}
class CustomObject: NSObject {
}

Is Storyboard Dependency Injection, which was included in UIKit/iOS 13, compatible with objects which are set on scene like: Objects, Additional views, Gestures ?
Evidently not. As your example demonstrates (perfectly), the mere presence of the additional top-level object in the scene causes the segue action to misbehave. No reference / outlet to the storyboard CustomObject is needed to elicit the crash.
I regard this as a bug; you should file a report with Apple.

Related

Swift: 'super.init' isn't called on all paths before returning from initializer?

I am getting this error on the last brace of a init in a class of mine. The class looks something like the following (I market the spot where error happens):
class RecordingViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
let cameraButton:UIButton?
let camPreview:UIView?
init (cameraButton: UIButton!, camPreview: UIView!) {
self.cameraButton = cameraButton
self.camPreview = camPreview
} //get error here
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//do a bunch of other stuff
}
I have looked here and here for a solution but both seem like solutions that are either really bad or that are too specific to that question, thus they have not work for me.
I was hoping for a solution to my problem done in such a way that it can help me understand why this error is happening.
Since you inherit from UIViewController, you should call super.init right after you set the variables in your init function
When you inherit a class and implement a new init function or override its own init function you should (almost) always call super.init. Let's take your example, you inherited from UIViewController. UIViewController has a few init functions that you can use to initialize a view controller. if you don't call super.init, all the code inside those functions will not get called and possibly the view controller won't get initialized.
Anyway, this piece of code should work for you:
class ViewController: UIViewController {
var button: UIButton?
init(button: UIButton) {
self.button = button
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is what I found on Swift Programming Language:
In the first phase, each stored property is assigned an initial value by the class that introduced it. Once the initial state for every stored property has been determined, the second phase begins, and each class is given the opportunity to customize its stored properties further before the new instance is considered ready for use.
A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer.
Hope this can explain that question.

Using segues in storyboard make a view controller that has non optional properties?

I have a view controller which has a property.
class GameVC: UIViewController {
var game:Game?
override func viewDidLoad() {
super.viewDidLoad()
}
}
For the view controller to make any sense it has to have a game property. For this reason I want the property not to be optional (I'd also prefer not to have to unwrap it when I use it the whole way through).
I'm currently overriding prepare for segue in the VC before this one and setting the game property. This is because I want to definitely be using segues and storyboards. Is there anyway I can make this view controller have a custom init and still use segues and storyboards?
Yes. You have two choices:
Initialize in the declaration:
class GameVC: UIViewController {
let game = Game()
Implement init(coder:) or awakeFromNib:
class GameVC: UIViewController {
let game : Game
required init?(coder aDecoder: NSCoder) {
self.game = Game()
super.init(coder:aDecoder)
}
In my opinion there is no good way for dependency injection when using storyboards. I think that storyboards are against object oriented design principles. For software development in a team I wouldn't recommend to use them, except for prototyping.
Instead, I use simple xib files and try to make the screens (aka UIViewControllers) as independent as possible. I also implement navigation between screens in own wireframe classes to separate the animated navigation from the main purpose of the UIViewController (constructing/managing the view tree). Then it's possible to inject the necessary objects without completely loosing the benefits of using interface builder.
Example:
final class MyViewController: UIViewController {
convenience override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
self.init()
}
convenience required init(coder aDecoder: NSCoder) {
self.init()
}
convenience init() {
fatalError("Not supported!")
}
init(viewModel: ViewModel, wireframe: Wireframe) {
self.viewModel = viewModel
self.wireframe = wireframe
super.init(nibName: "MyViewController", bundle: Bundle(for: type(of: self)))
}
private let viewModel: ViewModel
private let wireframe: Wireframe
override func viewDidLoad() {
super.viewDidLoad()
// do something with viewModel
}
#IBAction func close() {
wireframe.close()
}
}
let wireframe: Wireframe = ConcreteWireframe()
let viewModel: ViewModel = ConcreteViewModel()
let screen: UIViewController = MyViewController(viewModel: viewModel, wireframe: wireframe)
No, Storyboard (and XIB files) use init?(coder:) to instantiate elements, you can't pass additional parameters to this init.
Common practice is using implicit unwrapped optionals:
var game:Game!
This way, you can use it like a non-optional property, and it will crash on runtime if not set before used.
If you set this property in prepare(for:sender:) it will be ready in viewDidLoad, so it can be used safely.
In my apps, I just use something like
var game = Game()
That way it will never be nil and I can set the value before I segue.

How to handle Keyboard Events from Subview in Parent View

I'm fairly new to iOS development and I ran into a problem which seems simple yet I cannot solve it whichever way I try.
I have a custom view class #IBDesignable class ValidatedInputFieldView: UIView that hold 2 UI elements.
One of those is UITextField.
ValidatedInputFieldView is added to its parent view class ViewController: UIViewController, UITextFieldDelegate.
I want ViewController to respond to textFieldShouldReturn event from UITextField, and not the ValidatedInputFieldView where the UITextField is in.
I've tried exposing the delegate field of the UITextView:
#IBOutlet var textFieldDelegate:UITextFieldDelegate?
And setting it in the ValidatedInputFieldView:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
contentTextField.delegate = textFieldDelegate
}
And then linking it in the IB using the workaround:
Declare the outlet's type as AnyObject or NSObject, connect objects to the outlet using Interface Builder, then change the outlet's type back to the protocol.
But it simply does not work.
Debug says the object is nil.
I'm having trouble understanding how are those events that happen in Subview passed to the Parent view and what should I use to expose delegates.
A typical style I like to follow is to keep the items contained in their Views. What I would do is put the textFieldShouldReturn function in the ValidateInputView and set the delegate of the UITextField to the ValidateInputView. Then, in the textFieldShouldReturn function, post a notification using
NSNotificationCenter.defaultCenter().postNotificationName("textFieldFunction", object: self)
and make sure you are listening in the ViewController for this with:
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: Selector("textFieldFunction:"),
name: "textFieldFunction",
object: nil)
and create the function textFieldFunction in the ViewController to handle it like this:
func textFieldFunction(notification: NSNotification) {
//Put code to handle return press here
}
Make sure you put this in the ViewController as well:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
OR for no notification
Use the solution in this sample project:
https://mega.nz/#!koYDTZpa!uGZ6oUbKxRaWuGSM9FbCuV0t8oz5mtu35rZlLEL7Ehs
Sorry, I would post code, but there isn't much since it is mostly all through the IB.

How To Init A UIView That Exists In Interface Builder But Also Requires Init In Code

Here is my view class
class V_TakePhoto:UIView{
var _takePhotoCallback:(iImage)->Void?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
_takePhotoCallback = nil
}
#IBAction func takePhoto(sender: AnyObject) {
println("Here we go!")
}
func initWithCameraCallback((iImage)->Void)
{
}
}
This class is a UIView subclass. In the interface builder I selected the ViewController class, and then selected its View object. I assigned V_TakePhoto to this view object.
In the ViewController class, which I assigned to C_TakePhoto class, I want to init the V_TakePhoto class.
As you can see, I want it to have a callback variable that it gets passed at run time. However, because the view is already getting initialized from the interface builder, init(coder) is getting called first.
As it stands right now it seems hacky that I need to have 2 init functions. One where interface builder calls it, then again when my ViewController inits the view with its callback. Also I will have a number of variables, and I need to pre-init them in the init(coder) call then RE-init them again when the ViewController calls the 'true' init on the V_PhotoClass. Seems very hacky to me, there must be a clean 'correct' way to do this.
Can you suggest a cleaner way to handle a situation where you have variables and need to init a view despite there being an init(coder) call from the interface builder?
I would suggest creating a function in V_TakePhoto and call it in both V_TakePhoto's init(coder) and ViewController's viewDidLoad(), something like :
In V_TakePhoto :
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
specialInit()
}
func specialInit() {
// some of your view initialization
}
In your View Controller :
#IBOutlet weak var takePhotoView: V_TakePhoto!
override func viewDidLoad() {
super.viewDidLoad()
// this method is called after the view controller has loaded its view hierarchy into memory.
takePhotoView.specialInit() // RE-init
}

How to subclass UITableViewController in Swift

I want to subclass UITableViewController and be able to instantiate it by calling a default initializer with no arguments.
class TestViewController: UITableViewController {
convenience init() {
self.init(style: UITableViewStyle.Plain)
}
}
As of the Xcode 6 Beta 5, the example above no longer works.
Overriding declaration requires an 'override' keyword
Invalid redeclaration of 'init()'
NOTE This bug is fixed in iOS 9, so the entire matter will be moot at that point. The discussion below applies only to the particular system and version of Swift to which it is explicitly geared.
This is clearly a bug, but there's also a very easy solution. I'll explain the problem and then give the solution. Please note that I'm writing this for Xcode 6.3.2 and Swift 1.2; Apple has been all over the map on this since the day Swift first came out, so other versions will behave differently.
The Ground of Being
You are going to instantiate UITableViewController by hand (that is, by calling its initializer in code). And you want to subclass UITableViewController because you have instance properties you want to give it.
The Problem
So, you start out with an instance property:
class MyTableViewController: UITableViewController {
let greeting : String
}
This has no default value, so you have to write an initializer:
class MyTableViewController: UITableViewController {
let greeting : String
init(greeting:String) {
self.greeting = greeting
}
}
But that's not a legal initializer - you have to call super. Let's say your call to super is to call init(style:).
class MyTableViewController: UITableViewController {
let greeting : String
init(greeting:String) {
self.greeting = greeting
super.init(style: .Plain)
}
}
But you still can't compile, because you have a requirement to implement init(coder:). So you do:
class MyTableViewController: UITableViewController {
let greeting : String
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(greeting:String) {
self.greeting = greeting
super.init(style: .Plain)
}
}
Your code now compiles! You now happily (you think) instantiate this table view controller subclass by calling the initializer you wrote:
let tvc = MyTableViewController(greeting:"Hello there")
Everything looks merry and rosy until you run the app, at which point you crash with this message:
fatal error: use of unimplemented initializer init(nibName:bundle:)
What Causes the Crash and Why You Can't Solve It
The crash is caused by a bug in Cocoa. Unknown to you, init(style:) itself calls init(nibName:bundle:). And it calls it on self. That's you - MyTableViewController. But MyTableViewController has no implementation of init(nibName:bundle:). And does not inherit init(nibName:bundle:), either, because you already provided a designated initializer, thus cutting off inheritance.
Your only solution would be to implement init(nibName:bundle:). But you can't, because that implementation would require you to set the instance property greeting - and you don't know what to set it to.
The Simple Solution
The simple solution - almost too simple, which is why it is so difficult to think of - is: don't subclass UITableViewController. Why is this a reasonable solution? Because you never actually needed to subclass it in the first place. UITableViewController is a largely pointless class; it doesn't do anything for you that you can't do for yourself.
So, now we're going to rewrite our class as a UIViewController subclass instead. We still need a table view as our view, so we'll create it in loadView, and we'll hook it up there as well. Changes are marked as starred comments:
class MyViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { // *
let greeting : String
weak var tableView : UITableView! // *
init(greeting:String) {
self.greeting = greeting
super.init(nibName:nil, bundle:nil) // *
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() { // *
self.view = UITableView(frame: CGRectZero, style: .Plain)
self.tableView = self.view as! UITableView
self.tableView.delegate = self
self.tableView.dataSource = self
}
}
Also you'll want, of course, to add the minimal required data source methods. We now instantiate our class like this:
let tvc = MyViewController(greeting:"Hello there")
Our project compiles and runs without a hitch. Problem solved!
An Objection - Not
You might object that by not using UITableViewController we have lost the ability to get a prototype cell from the storyboard. But that is no objection, because we never had that ability in the first place. Remember, our hypothesis is that we are subclassing and calling our own subclass's initializer. If we were getting the prototype cell from the storyboard, the storyboard would be instantiating us by calling init(coder:) and the problem would never have arisen in the first place.
Xcode 6 Beta 5
It appears that you can no longer declare a no-argument convenience initializer for a UITableViewController subclass. Instead, you need to override the default initializer.
class TestViewController: UITableViewController {
override init() {
// Overriding this method prevents other initializers from being inherited.
// The super implementation calls init:nibName:bundle:
// so we need to redeclare that initializer to prevent a runtime crash.
super.init(style: UITableViewStyle.Plain)
}
// This needs to be implemented (enforced by compiler).
required init(coder aDecoder: NSCoder!) {
// Or call super implementation
fatalError("NSCoding not supported")
}
// Need this to prevent runtime error:
// fatal error: use of unimplemented initializer 'init(nibName:bundle:)'
// for class 'TestViewController'
// I made this private since users should use the no-argument constructor.
private override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
}
Props to matt for a great explanation. I've made use of both matt's and #Nick Snyder's solutions, however I ran into a case in which neither would quite work, because I needed to (1) initialize let fields, (2) use init(style: .Grouped) (without getting a runtime error), and (3) use the built-in refreshControl (from UITableViewController). My workaround was to introduce an intermediate class MyTableViewController in ObjC, then use that class as the base of my table view controllers.
MyTableViewController.h
#import <UIKit/UIKit.h>
// extend but only override 1 designated initializer
#interface MyTableViewController : UITableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;
#end
MyTableViewController.m:
#import "MyTableViewController.h"
// clang will warn about missing designated initializers from
// UITableViewController without the next line. In this case
// we are intentionally doing this so we disregard the warning.
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
#implementation MyTableViewController
- (instancetype)initWithStyle:(UITableViewStyle)style {
return [super initWithStyle:style];
}
#end
Add the following to Project's Bridging-Header.h
#import "MyTableViewController.h"
Then use in swift. Example: "PuppyViewController.swift":
class PuppyViewController : MyTableViewController {
let _puppyTypes : [String]
init(puppyTypes : [String]) {
_puppyTypes = puppyTypes // (1) init let field (once!)
super.init(style: .Grouped) // (2) call super with style and w/o error
self.refreshControl = MyRefreshControl() // (3) setup refresh control
}
// ... rest of implementation ...
}
I did it like this
class TestViewController: UITableViewController {
var dsc_var: UITableViewController?
override convenience init() {
self.init(style: .Plain)
self.title = "Test"
self.clearsSelectionOnViewWillAppear = true
}
}
Creating and displaying a instance of TestViewController in a UISplitViewController did work for me with this code.
Maybe this is bad practice, please tell me if it is (just started with swift).
For me there's still a problem when there are non optional variables and the solution of Nick Snyder is the only one working in this situation
There's just 1 problem:
The variables are initialized 2 times.
Example:
var dsc_statistcs_ctl: StatisticsController?
var dsrc_champions: NSMutableArray
let dsc_search_controller: UISearchController
let dsrc_search_results: NSMutableArray
override init() {
dsrc_champions = dsrg_champions!
dsc_search_controller = UISearchController(searchResultsController: nil)
dsrc_search_results = NSMutableArray.array()
super.init(style: .Plain) // -> calls init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) of this class
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
// following variables were already initialized when init() was called and now initialized again
dsrc_champions = dsrg_champions!
dsc_search_controller = UISearchController(searchResultsController: nil)
dsrc_search_results = NSMutableArray.array()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
matt's answer is the most complete, but if you do want to use a tableViewController in the .plain style (say for legacy reasons). Then all you need to do is call
super.init(nibName: nil, bundle: nil)
instead of
super.init(style: UITableViewStyle.Plain) or self.init(style: UITableViewStyle.Plain)
I wanted to subclass UITableViewController and add a non-optional property which requires overriding the initializer and dealing with all the problems described above.
Using a Storyboard and a segue gives you more options if you can work with an optional var rather than a non-optional let in your subclass of UITableViewController
By calling performSegueWithIdentifier and overriding prepareForSegue in your presenting view controller, you can get the instance of the UITableViewController subclass and set the optional variables before initialization is completed:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "segueA"{
var viewController : ATableViewController = segue.destinationViewController as ATableViewController
viewController.someVariable = SomeInitializer()
}
if segue.identifier == "segueB"{
var viewController : BTableViewController = segue.destinationViewController as BTableViewController
viewController.someVariable = SomeInitializer()
}
}
I've noticed a similar error when using static tableview cells and you gotta implement this:
init(coder decoder: NSCoder) {
super.init(coder: decoder)
}
if you implement:
required init(coder aDecoder: NSCoder!) {
// Or call super implementation
fatalError("NSCoding not supported")
}
I was just getting a crash there... Kind of as expected. Hope this helps.
Not sure it is related to your question, but in case you want to init UITableView controller with xib, Xcode 6.3 beta 4 Release Notes provide a workaround:
In your Swift project, create a new empty iOS Objective-C file. This will trigger a sheet asking you “Would you like to configure an Objective-C bridging header.”
Tap “Yes” to create a bridging header
Inside [YOURPROJECTNAME]-Bridging-Header.h add the following code:
#import UIKit;
#interface UITableViewController() // Extend UITableViewController to work around 19775924
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER ;
#end
class ExampleViewController: UITableViewController {
private var userName: String = ""
static func create(userName: String) -> ExampleViewController {
let instance = ExampleViewController(style: UITableViewStyle.Grouped)
instance.userName = userName
return instance
}
}
let vc = ExampleViewController.create("John Doe")

Resources