PushRow and UISplitViewController navigation - ios

I am working on a UISplitViewController subclass that has a UINavigationController as master viewcontroller (the reason for this is that I need the navigation bar). The UINavigationController has a FormViewController subclass as rootViewController. In the form I am using a PushRow to show a list of selectable options. The problem is that when I tap the PushRow in a regular-width environment the SelectorViewController is pushed in the master navigation stack. I would like to see the SelectorViewController showed as a UISplitViewController detail instead.
I created a DummyNavigationController that looks like this:
class DummyNavigationController: UINavigationController {
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
if let parent = parent as? UISplitViewController {
return parent.isCollapsed ? self : parent
}
return self
}
}
Using this as navigation controller for the FormViewController I am able to see the SelectorViewController on the detail but I don't like this approach.
Is there anything else that I can do?

I know this is not a very specific answer but I lack the reputation to comment.
You can embed only one side of the SplitViewController in a UINavigationController instead of the whole SplitViewController, this way when you segue to the SelectorViewController it will only show it on one side of the SplitViewController.

Related

Sending information between ViewControllers in swift

I have two classes:
class ExplorerViewController: UIViewController {
lazy var studyButton: ExploreButton = {
let button = ExploreButton()
button.setTitle("Study", forState: .Normal)
return button
}()
}
and
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
}
I'm trying to make it so that when I click the studyButton, it sends the button title to ViewController and goes to that view.
I'm not using storyboards and am having trouble with segues since every tutorial seems to give different examples that are specific to the things they've been working with and 95% of them seem to be operating with storyboard. Can someone give me a general way of how to do this?
How do I give the starting view controller an identifier because it isn't instantiated like the other controllers that I 'move' to after. How can I move from ViewController to ExplorerViewController and then move back to that same ViewController (with all changes intact).
Create an initializer for your ViewController that receives the "title" variable:
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
var btnTitle: String?
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?, btnTitle:String?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.btnTitle = btnTitle
}
}
When creating the ViewController object use this initializer.
var viewController = ViewController(nibName: "ViewController", bundle: nil, btnTitle: title
You can initialize UIViewController that you want navigate to, assign data to properties in that controller and call this method:
presentViewController(viewController, animated: true, completion: nil)
For example:
let destinationViewController = ViewController()
destinationViewController.frame = self.view.frame
destinationViewController.buttonTitle = "title"
self.presentViewController(destinationViewController, animated: true, completion: nil)
Although I would suggest you to get familiar with Storyboards and perform navigation with Segues.
Make sure of two things:-
1.) You have given your viewController an StoryBoard ID lets say "viewControllerVC_ID" in it's Identity inspector
2.) You have NavigationController Embed in to your Initial entry point View Controller
In ViewController declare a variable
var btnLabelTxt : String!
Create an #IBAction of that button in ExplorerViewController :-
#IBAction func exploreBtnAction(sender : UIButton!){
let vcScene = self.navigationController?.storyboard?.instantiateViewControllerWithIdentifier("viewControllerVC_ID") as! ViewController
vcScene.btnLabelTxt = "Study"
//or you can just access the button itself in the viewController and set the title
//By vcScene.yourBtn.setTitle("Study", forState: .Normal)
self.navigationController?.pushViewController(vcScene, animated: true)
}
please see this question How to push viewcontroller ( view controller )? for how to switch between views.
to pass data once you have reference to the new view, you can assign the data to a property of that view.

Loading UIViewController from storyboard through its custom init method

I have custom init method for my UIViewController, I want to load it from storyBoard as only XYZController() gives blank View.
convenience init() {
self.init(imageURL: nil)
}
init(imageURL: NSURL?){
//code to load the ViewController from StoryBoard
super.init(nibName: nil, bundle: NSBundle.mainBundle())
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
If you want to load the view controller from the storyboard, then you're going to have to follow a slightly different pattern.
First, name the segue to your view controller in the storyboard (click on it, then enter a name for 'Identifier' in the properties tab.
When the segue is triggered, prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) will be called. You can make sure you're moving to the appropriate view controller by checking the segue's identifier property, then cast the segue's destinationViewController to you XYZViewController.
After that, you should be able to call a setup function or however you want to supply the image url to the controller.

Custom init for UIViewController in Swift with interface setup in storyboard

I'm having issue for writing custom init for subclass of UIViewController, basically I want to pass the dependency through the init method for viewController rather than setting property directly like viewControllerB.property = value
So I made a custom init for my viewController and call super designated init
init(meme: Meme?) {
self.meme = meme
super.init(nibName: nil, bundle: nil)
}
The view controller interface resides in storyboard, I've also make the interface for custom class to be my view controller. And Swift requires to call this init method even if you are not doing anything within this method. Otherwise the compiler will complain...
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
The problem is when I try to call my custom init with MyViewController(meme: meme) it doesn't init properties in my viewController at all...
I was trying to debug, I found in my viewController, init(coder aDecoder: NSCoder) get called first, then my custom init get called later. However these two init method return different self memory addresses.
I'm suspecting something wrong with the init for my viewController, and it will always return self with the init?(coder aDecoder: NSCoder), which, has no implementation.
Does anyone know how to make custom init for your viewController correctly ?
Note: my viewController's interface is set up in storyboard
here is my viewController code:
class MemeDetailVC : UIViewController {
var meme : Meme!
#IBOutlet weak var editedImage: UIImageView!
// TODO: incorrect init
init(meme: Meme?) {
self.meme = meme
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
/// setup nav title
title = "Detail Meme"
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
editedImage = UIImageView(image: meme.editedImage)
}
}
As it was specified in one of the answers above you can not use both and custom init method and storyboard.
But you still can use a static method to instantiate ViewController from a storyboard and perform additional setup on it.
It will look like this:
class MemeDetailVC : UIViewController {
var meme : Meme!
static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "IdentifierOfYouViewController") as! MemeDetailVC
newViewController.meme = meme
return newViewController
}
}
Don't forget to specify IdentifierOfYouViewController as view controller identifier in your storyboard. You may also need to change the name of the storyboard in the code above.
You can't use a custom initializer when you initialize from a Storyboard, using init?(coder aDecoder: NSCoder) is how Apple designed the storyboard to initialize a controller. However, there are ways to send data to a UIViewController.
Your view controller's name has detail in it, so I suppose that you get there from a different controller. In this case you can use the prepareForSegue method to send data to the detail (This is Swift 3):
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "identifier" {
if let controller = segue.destinationViewController as? MemeDetailVC {
controller.meme = "Meme"
}
}
}
I just used a property of type String instead of Meme for testing purposes. Also, make sure that you pass in the correct segue identifier ("identifier" was just a placeholder).
As #Caleb Kleveter has pointed out, we can't use a custom initializer while initialising from a Storyboard.
But, we can solve the problem by using factory/class method which instantiate view controller object from Storyboard and return view controller object.
I think this is a pretty cool way.
Note: This is not an exact answer to question rather a workaround to solve the problem.
Make class method, in MemeDetailVC class, as follows:
// Considering your view controller resides in Main.storyboard and it's identifier is set to "MemeDetailVC"
class func `init`(meme: Meme) -> MemeDetailVC? {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MemeDetailVC") as? MemeDetailVC
vc?.meme = meme
return vc
}
Usage:
let memeDetailVC = MemeDetailVC.init(meme: Meme())
One way that I've done this is with a convenience initializer.
class MemeDetailVC : UIViewController {
convenience init(meme: Meme) {
self.init()
self.meme = meme
}
}
Then you initialize your MemeDetailVC with let memeDetailVC = MemeDetailVC(theMeme)
Apple's documentation on initializers is pretty good, but my personal favorite is the Ray Wenderlich: Initialization in Depth tutorial series which should give you plenty of explanation/examples on your various init options and the "proper" way to do things.
EDIT: While you can use a convenience initializer on custom view controllers, everyone is correct in stating that you cannot use custom initializers when initializing from the storyboard or through a storyboard segue.
If your interface is set up in the storyboard and you're creating the controller completely programmatically, then a convenience initializer is probably the easiest way to do what you're trying to do since you don't have to deal with the required init with the NSCoder (which I still don't really understand).
If you're getting your view controller via the storyboard though, then you will need to follow #Caleb Kleveter's answer and cast the view controller into your desired subclass then set the property manually.
There were originally a couple of answers, which were cow voted and deleted even though they were basically correct. The answer is, you can't.
When working from a storyboard definition your view controller instances are all archived. So, to init them it's required that init?(coder... be used. The coder is where all the settings / view information comes from.
So, in this case, it's not possible to also call some other init function with a custom parameter. It should either be set as a property when preparing the segue, or you could ditch segues and load the instances directly from the storyboard and configure them (basically a factory pattern using a storyboard).
In all cases you use the SDK required init function and pass additional parameters afterwards.
Swift 5
You can write custom initializer like this ->
class MyFooClass: UIViewController {
var foo: Foo?
init(with foo: Foo) {
self.foo = foo
super.init(nibName: nil, bundle: nil)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.foo = nil
}
}
UIViewController class conform to NSCoding protocol which is defined as:
public protocol NSCoding {
public func encode(with aCoder: NSCoder)
public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}
So UIViewController has two designated initializer init?(coder aDecoder: NSCoder) and init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).
Storyborad calls init?(coder aDecoder: NSCoder) directly to init UIViewController and UIView,There is no room for you to pass parameters.
One cumbersome workaround is to use an temporary cache:
class TempCache{
static let sharedInstance = TempCache()
var meme: Meme?
}
TempCache.sharedInstance.meme = meme // call this before init your ViewController
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
self.meme = TempCache.sharedInstance.meme
}
As of iOS 13 you can initialize the view controller that resides in a storyboard using:
instantiateViewController(identifier:creator:) method on the UIStoryboard instance.
tutorial:
https://sarunw.com/posts/better-dependency-injection-for-storyboards-in-ios13/
Although we can now do custom init for the default controllers in the storyboard using instantiateInitialViewController(creator:) and for segues including relationship and show.
This capability was added in Xcode 11 and the following is an excerpt from the Xcode 11 Release Notes:
A view controller method annotated with the new #IBSegueAction attribute can be used to create a segue’s destination view controller in code, using a custom initializer with any required values. This makes it possible to use view controllers with non-optional initialization requirements in storyboards. Create a connection from a segue to an #IBSegueAction method on its source view controller. On new OS versions that support Segue Actions, that method will be called and the value it returns will be the destinationViewController of the segue object passed to prepareForSegue:sender:. Multiple #IBSegueAction methods may be defined on a single source view controller, which can alleviate the need to check segue identifier strings in prepareForSegue:sender:. (47091566)
An IBSegueAction method takes up to three parameters: a coder, the sender, and the segue’s identifier. The first parameter is required, and the other parameters can be omitted from your method’s signature if desired. The NSCoder must be passed through to the destination view controller’s initializer, to ensure it’s customized with values configured in storyboard. The method returns a view controller that matches the destination controller type defined in the storyboard, or nil to cause a destination controller to be initialized with the standard init(coder:) method. If you know you don’t need to return nil, the return type can be non-optional.
In Swift, add the #IBSegueAction attribute:
#IBSegueAction
func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
PetController(
coder: coder,
petName: self.selectedPetName, type: .dog
)
}
In Objective-C, add IBSegueAction in front of the return type:
- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
sender:(id)sender
segueIdentifier:(NSString *)segueIdentifier
{
return [PetController initWithCoder:coder
petName:self.selectedPetName
type:#"dog"];
}
In XCode 11/iOS13, you can use
instantiateViewController(identifier:creator:)
also without segues:
let vc = UIStoryboard(name: "StoryBoardName", bundle: nil).instantiateViewController(identifier: "YourViewControllerIdentifier", creator: {
(coder) -> YourViewController? in
return YourViewController(coder: coder, customParameter: "whatever")
})
present(vc, animated: true, completion: nil)
Disclaimer: I do not advocate for this and have not thoroughly tested its resilience, but it is a potential solution I discovered while playing around.
Technically, custom initialization can be achieved while preserving the storyboard-configured interface by initializing the view controller twice: the first time via your custom init, and the second time inside loadView() where you take the view from storyboard.
final class CustomViewController: UIViewController {
#IBOutlet private weak var label: UILabel!
#IBOutlet private weak var textField: UITextField!
private let foo: Foo!
init(someParameter: Foo) {
self.foo = someParameter
super.init(nibName: nil, bundle: nil)
}
override func loadView() {
//Only proceed if we are not the storyboard instance
guard self.nibName == nil else { return super.loadView() }
//Initialize from storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let storyboardInstance = storyboard.instantiateViewController(withIdentifier: "CustomVC") as! CustomViewController
//Remove view from storyboard instance before assigning to us
let storyboardView = storyboardInstance.view
storyboardInstance.view.removeFromSuperview()
storyboardInstance.view = nil
self.view = storyboardView
//Receive outlet references from storyboard instance
self.label = storyboardInstance.label
self.textField = storyboardInstance.textField
}
required init?(coder: NSCoder) {
//Must set all properties intended for custom init to nil here (or make them `var`s)
self.foo = nil
//Storyboard initialization requires the super implementation
super.init(coder: coder)
}
}
Now elsewhere in your app you can call your custom initializer like CustomViewController(someParameter: foo) and still receive the view configuration from storyboard.
I don't consider this a great solution for several reasons:
Object initialization is duplicated, including any pre-init properties
Parameters passed to the custom init must be stored as optional properties
Adds boilerplate which must be maintained as outlets/properties are changed
Perhaps you can accept these tradeoffs, but use at your own risk.
Correct flow is, call the designated initializer which in this case is the init with nibName,
init(tap: UITapGestureRecognizer)
{
// Initialise the variables here
// Call the designated init of ViewController
super.init(nibName: nil, bundle: nil)
// Call your Viewcontroller custom methods here
}
This solution shows a way to have custom initializers but still be able to use Storyboard WITHOUT using the self.init(nib: nil, bundle: nil) function.
To make it possible to use that, let’s first tweak our MemeDetailsVC to also accept an NSCoder instance as part of its custom initializer, and to then delegate that initializer to super.init(coder:), rather than its nibName equivalent:
class MemeDetailVC : UIViewController {
var meme : Meme!
#IBOutlet weak var editedImage: UIImageView!
init?(meme: Meme, coder: NSCoder) {
self.meme = meme
super.init(coder: aDecoder)
}
#available(*, unavailable, renamed: "init(product:coder:)")
required init?(coder: NSCoder) {
fatalError("Invalid way of decoding this class")
}
override func viewDidLoad() {
title = "Detail Meme"
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
editedImage = UIImageView(image: meme.editedImage)
}
}
And then, you instantiate & show the View Controller this way:
guard let viewController = storyboard?.instantiateViewController(
identifier: "MemeDetailVC",
creator: { coder in
MemeDetailVC(meme: meme, coder: coder)
}
) else {
fatalError("Failed to create Product Details VC")
}
//Then you do what you want with the view controller.
present(viewController, sender: self)
// View controller is in Main.storyboard and it has identifier set
Class B
class func customInit(carType:String) -> BViewController
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let objClassB = storyboard.instantiateViewController(withIdentifier: "BViewController") as? BViewController
print(carType)
return objClassB!
}
Class A
let objB = customInit(carType:"Any String")
navigationController?.pushViewController(objB,animated: true)

UINavigationController in UIViewController programmatically within class

I'm trying to add a navigation controller to my UIViewController subclass programmatically (I'm not using storyboards) and I wanted to find the best place to init it and configure such.
I have tried viewDidLoad (the views weren't initialized by init) and a convenience init (just to make sure) method but no luck.
Here's how I'm creating it:
override init!(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let nav = UINavigationController(rootViewController: self)
}
What would be the most appropriate method to place this logic in? I would like to keep this logic contained within this VC.
I am able to accomplish this by creating the nav. with a root vc from the presenting VC, but this leaks this logic and I'd rather not do that.
I'm not sure this is what you're looking for, but you could put some logic within a method in the controller you're presenting to determine whether it should be presented with or without a navigation controller; doing it this way means the presenting view controller doesn't need to know whether the controller it's presenting is embedded in a navigation controller or not. The presenting controller, would call this method after instantiating your controller. The presented controller could look something like this,
class NextViewController: UIViewController {
var wantsNavigationcontroller = true
func viewControllerWithOrWithoutNavigationController() -> UIViewController {
if wantsNavigationcontroller {
let nav = UINavigationController(rootViewController: self)
return nav
}else{
return self
}
}
}
The presenting controller would do this,
#IBAction func PresentNextcontroller(sender: UIButton) {
var nextVC = NextViewController()
self.presentViewController(nextVC.viewControllerWithOrWithoutNavigationController(), animated: true, completion: nil)
}

InAppSettingsKit Inside Container - Navigation Issues

I have implemented InAppSettingsKit without issue as a modal controller. All is well there. Then I wanted to get fancy!
I added a Container to one of my custom view controllers and made the embedded segue controller target a TableViewController, everything displays just fine. Even child panes show without issue. But then my issue starts, when I go to a child pane I cannot get back. That is to say that there is no navigation controls!!
I've read all of the other posts where most people have issues showing child panes, this isn't an issue for me, I just can't get back from them as there are no navigation controls.
I'm not sure if it's important but my custom view controller with the Container is part of a tab view controller. Perhaps this is why there is no navigation controls? Anyone else experiencing this or have a quick fix? Seems like I'm missing something simple, a setting someplace.
I'll keep working on this and update if I figure something out!
UPDATE
Our application hides the navigation bar right off the bat. I found that by unhiding this I'm able to now navigate, which makes sense. It would appear that the only way around this would be to override viewWillAppear/viewDidDisappear in the child panes to enable/disable the navigation panes...which is now looking like a real pain (no pun intended) because I'll have to create custom subviews. Trying to figure out if there is a class I can extend from InAppSettingsKit to just add these overrides.
You can find out what I had to do here: https://github.com/futuretap/InAppSettingsKit/issues/277
Also, please note that my use case was to have InAppSettingKit work in a container inside of an existing view controller and I wanted it to have a navigation bar that was previously hidden.
If this is your use case, read on...
I really didn't want to have to reinvent the wheel, so I created a subclass of the already existing IASKAppSettingsViewController class and then added in the appropriate appear/disappear functionality.
I guess this might be the way to extend the base class anywhere you want, you just need the basics set up and then have at it!
Hope this helps someone else!!
class CustomSubviewController: IASKAppSettingsViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(style: UITableViewStyle) {
super.init(style: style)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
func myinit (file: NSString, specifier: IASKSpecifier) -> CustomSubviewController {
var vc = CustomSubviewController()
vc.showDoneButton = false;
vc.showCreditsFooter = false; // Does not reload the tableview (but next setters do it)
vc.delegate = self.delegate;
vc.settingsStore = self.settingsStore;
vc.file = specifier.file();
vc.hiddenKeys = self.hiddenKeys;
vc.title = specifier.title();
return vc
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
}

Resources