I have a custom UIView class called MyView and a View Controller.
When the user taps a button on the UIView, I want to call a function on the view controller. I'm trying to achieve this through delegation
custom UIClass
#objc protocol MyViewDelegate{
optional func expandCollapse()
}
class MyView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate{
weak var delegate:MyViewDelegate?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if self.subviews.count == 0 {
loadNib()
}
}
override init(frame:CGRect){
super.init(frame: frame)
loadNib()
}
func loadNib(){
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "MyView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! MyView
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(view);
}
#IBAction func expandit(sender: AnyObject) {
//this is where it fails. delegate is nil
delegate!.expandCollapse!()
}
}
My View Controller
class ViewController2: UIViewController, MyViewDelegate {
#IBOutlet weak var theview: UIView!
var myview : MyView?
override func viewDidAppear(animated: Bool) {
super.viewWillAppear(animated)
myview = MyView(frame: CGRectMake(0,0,theview.frame.size.width,theview.frame.size.height))
self.theview.addSubview(myview!)
myview!.delegate = self
}
func expandCollapse() {
viewheight.constant = 172
UIView.animateWithDuration(0.5) {
self.view.layoutIfNeeded()
}
}
In the UIView, the delegate is always nil. What am I missing?
Using delegation for this is simply unsuitable. You are fighting UIKit design patterns.
The whole situation is very simple.
You have your ViewController.
Then you have your totally independent custom view.
Essentially, you want somehow to route the TouchUpInside event from the button to get to viewController.
If your Custom view contains a button, then the accessibility level of this button is internal by default. Looking at the code, I assume you created the button in Interface builder. Make an outlet from the custom view class to the button, so that there is a programatically accessible reference to it.
Your view controller declares an instance of this custom view. Then, in viewDidLoad you have to use the target-action pattern.
self.customView.button.addTarget(target: self, action: "expandCollapse", forControlEvents: .TouchUpInside)
That's basically all there is to it.
I'm not entirely confident of my ARC understanding, but I believe the issue is that your delegate is a weak reference and there's nothing keeping a reference to the delegate after it's set, so it' deallocated.
Replace it with this and I believe it will work:
var delegate:MyViewDelegate?
Try assigning the delegate to "myview" before adding it to "theview"
The problem is in the loadNib() member function.
You're creating two instances of "MyView". The second instance being added as a subview.
You're setting the delegate in one instance and referring to a nil delegate in the other instance.
Try using a static class method like below to create one instance of "MyView"
class func loadFromNib() -> MyView? {
guard let myView = Bundle.main.loadNibNamed("MyView", owner: nil, options: nil)?.first as? MyView else {
assertionFailure("Failed to load nib for 'MyView'!")
return nil
}
return myView
}
Doing it this way, you won't need the custom init()s either.
Hope that helps!
Related
I'm trying to use custom views from xib files, but I don't know how to set the placeholder view in the storyboard equal to one of the xibs. Also, I can't just set the placeholder uiview's property to one of the xibs because I need to be able to switch between different xibs. This is what I have so far.
import UIKit
class Viewcontroller: UIViewController {
#IBOutlet weak var placeholderView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
placeholderView = view
}
class func instanceFromNib() -> CustomView {
let view = UINib(nibName: "CustomView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! CustomView
return view
}
Create a custom class for your Xib view where you load the nib as follows:
class MyCustomView: UIView{
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)
}
}
You can add all of the outlets and stuff you need there.
The view in your storyboard must be of the type of your Xib view. Change it both in your code and in your storyboard:
#IBOutlet weak var placeholderView: MyCustomView!
From storyboard:
Click the view
Go to the identity inspector
In the class thing write MyCustomView
Instead of thinking in terms of a "placeholder" view that will be replaced by some other view, think about it as a "container" view... you would add the xib's view as a subview, just as you would add any other subview.
So, when you want to show the view from a xib, you would:
remove the current subview from the container (if one is already there)
load the view from the desired xib
add it as a subview to the container
set constraints relative to the container
I am trying to delegate a function with which I will call the new ViewController. In run, it gets this error:
setValue: forUndefinedKey:]: this class is not key value coding-compliant for the key mapView. '
What am I doing wrong or how can I fix it? I will be grateful for all the tips.
import UIKit
import MapKit
ViewController.swift:
class ViewController: UIViewController, CustomViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName:"Maps", bundle:nil)
let MapsView = nib.instantiate(withOwner: self, options: nil).first as! Maps
MapsView.delegate = self
}
func showWaypointDetails() {
print("show")
}
Maps.swift:
protocol CustomViewDelegate: class {
func showWaypointDetails()
}
class GpsMaps: UIView, MKMapViewDelegate {
#IBOutlet var view: UIView!
#IBOutlet weak var mapView: MKMapView!
weak var delegate: CustomViewDelegate?
}
It looks like you are either not instantiating the right view or the view is not set up correctly.
In your Maps.xib file:
Check if the "File's Owner" points to your Maps.swift class
Check if you have a MKMapView outlet named "mapView"
You are casting the nib (which I assume contains a MKMapView) as Maps, which is just a simple protocol. In doing this you will lose all mapView functionality as the app won't know it's a MapView.
You need to load it and cast it as the MapView Class you are using, and then adopt the protocol in your MapView class...
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName:"Maps", bundle:nil)
let mapsView = nib.instantiate(withOwner: self, options: nil).first as! MyMapViewClass
mapsView.delegate = self
}
extension MyMapViewClass: Maps {
// implement showWaypointDetails() or implement it as a default in a protocol extenstion
}
This question deals with the same code with my previous question.
(The previous question is Why is this View nil in awakeFromNib()?)
But totally different approach.
It's not the duplicated question.
So let me restructure my question.
First, let me summarize my project.
Thanks to previous answer, I can reuse xib now.
This is CardContentView.xib
Codes are below
#IBDesignable class CardContentView: UIView {
#IBOutlet weak var backgroundView: UIView!
#IBInspectable var nibName:String?
var contentView: UIView?
override func awakeFromNib() {
super.awakeFromNib()
xibSetup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
contentView?.prepareForInterfaceBuilder()
}
func xibSetup(){
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
guard let nibName = nibName else { return nil }
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
And this is CardView.xib, the property follows 'class CardContentView'
Codes are below
class CardView: UIView {
#IBOutlet weak var cardContentView: CardContentView!
override func awakeFromNib() {
cardContentView.layer.cornerRadius = 16
}
}
This is Main.storyboard. with a single UIView.
Codes are below
class ViewController: UIViewController {
#IBOutlet weak var cardView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
But, I want to reuse class 'CardView' to 'cardView' property in ViewController.
I want to get 'background' property to 'cardView' property (In simple words, I want to change the yellow box to the blue box in CardContentView!).
And I want to make 'cardView' property with a transition effect.
But, I cannot make extension because it's not in a UIView class, but in a ViewController class.
So I need to make another UIView class.
That's why I made a CardView class separately.
(Of course, I want to organize codes neatly by making another file.)
So, I tried 2 different ways.
Set the property 'cardView' class as CardView, and connect it to ViewController.
class ViewController: UIViewController {
#IBOutlet weak var cardView: CardView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
Cast 'CardView' class to other property in ViewController
override func viewDidLoad() {
super.viewDidLoad()
let tappedView = self.cardView as! CardView
self.view.addSubview(tappedView)
}
But both of them make errors.
1. When I made '#IBOutlet weak var cardView: CardView!', it throws error message, 'Unexpectedly found nil while unwrapping an Optional value, cardContentView = nil'.
2. When I made 'let tappedView = self.cardView as! CardView', it throws error message, 'Could not cast value of type 'UIView' (0x10a229ff8) to 'AwakeFromNibTutorial.CardView' (0x1060aa8d0).'
How can I solve this problem?
Please help me!
In the CardView class's awakeFromNib(), you are not calling super.awakeFromNib().
You would have to use the same logic (xibSetup()) that you used to load the CardContentView from it's corresponding XIB, to load the CardView also from its XIB.
In the ViewController in your storyboard, have you assigned the class of the UIView to CardView?
I'm working on a Swift app and running into a crazy odd problem. I have a simple storyboard view controller setup, it has 3 UIButtons a UIImageView and a UIView (as a subview of the main view).
I want to programmatically add a WKWebView to the UIView.
Easy enough right? All of the above are in the Storyboard View, declared in the custom UIViewController class and connected in IB. However at run time, everything is nil. This is a snippet of my code:
#IBOutlet var button1 : UIButton!;
#IBOutlet var button2 : UIButton!;
#IBOutlet var button3 : UIButton!;
#IBOutlet weak var containerForWebView: UIView!
var webView: WKWebView!
override func loadView()
{
}
override func viewDidLoad()
{
super.viewDidLoad()
displayWebPage()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
}
private func displayWebPage()
{
webView = WKWebView()
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.frame = CGRect(origin: CGPoint.zero, size: containerForWebView.frame.size)
webView.navigationDelegate = self
containerForWebView.addSubview(webView)
let url = URL(string: "https://www.google.com")!
webView.load(URLRequest(url: url))
}
When the code calls the displayWebPage() method I break on the first line. In the debugger you can see all of the UIViewController properties are nil. The IBOutlets are Nil and the _view variable of the UIViewController itself is nil.
I don't understand why this is happening. Everything is pretty simple and easily connected. I never ran into this sort of issue in Objective-C. Any advice is appreciated!
Remove loadView implementation. Do not override that method, always use viewDidLoad instead.
If you override loadView then you are responsible for creating controller's view and assigning it to self.view.
I'm rendering a Xib file which is a subview in a view controller, the view controller is called by setViewController in a pageviewcontroller . It works great. I then placed the same Xib sub view in a view controller called by my tab view controller and it doesn't render.
To try and get to the bottom of this weird occurrence I have removed all code such that there is no class attached to the UIviewcontroller parent of the subview Xib in both cases. It is now a pure IB implementation of the Xib and the Xib classes are identical.
So in my comparison between the two implementations the only difference is one is opened from a page view controller and the other from a tab. I even copied the working view controller just so I knew they were identical.
Has anyone had this experience (where the same Xib doesn't render in one workflow compared to another) or have an idea as to why the two implementations cause different results??
Update: I took it one step further. Now there is absolutely no code. The only thing that is in the Xib is an unattached label. It only renders when the parent view is called from a page view controller! Wtf
Signin View Xib. (Works when instantiated from UIPageViewController:
SigninView.swift
class SigninView: UIView, GIDSignInUIDelegate, GIDSignInDelegate {
var view: UIView!
var nibName: String = "SigninView"
var delegate: SigninViewDelegate?
#IBOutlet weak var googleButton: GIDSignInButton!
#IBOutlet weak var signoutButton: UIButton!
override init(frame: CGRect) {
// properties
super.init(frame: frame)
// Set anything that uses the view or visible bounds
setup()
}
override func awakeFromNib() {
print("AWAKING!")
}
#IBAction func tapSignOut(sender: AnyObject) {
GIDSignIn.sharedInstance().signOut()
UserManager().logout()
self.updateDisplay()
}
required init?(coder aDecoder: NSCoder) {
// properties
super.init(coder: aDecoder)
// Setup
setup()
}
func setup() {
view = loadViewFromNib()
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().delegate = self
view.frame = bounds
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
addSubview(view)
self.updateDisplay()
}
func updateDisplay() {
self.googleButton.layer.hidden = false
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
func signIn(signIn: GIDSignIn!, presentViewController viewController: UIViewController!) {
//present vc on delegate
let delegateVC = self.delegate as! UIViewController
delegateVC.presentViewController(viewController, animated: true, completion: nil)
}
func signIn(signIn: GIDSignIn!, dismissViewController viewController: UIViewController!) {
print("Dismiss view controller")
let delegateVC = self.delegate as! UIViewController
delegateVC.dismissViewControllerAnimated(true, completion: nil)
self.updateDisplay()
}
func signIn(signIn: GIDSignIn!, didSignInForUser user: GIDGoogleUser!, withError error: NSError!) {
var success = false
if (user != nil) {
print("signed in? UID: \(user.userID), email: \(user.profile.email), name: \(user.profile.name), pic: \(user.profile.imageURLWithDimension(150))")
UserManager().createUser(user)
LocalDataStorage().saveContext()
success = true
} else {
print("User not signed in.")
}
self.updateDisplay()
if self.delegate != nil && self.delegate!.signInComplete != nil {
self.delegate!.signInComplete!(success)
}
}
func signIn(signIn: GIDSignIn!, didDisconnectWithUser user: GIDGoogleUser!, withError error: NSError!) {
print("disconnected")
}
}
Working UIViewController that contains working Signin Subview
Working call from UIPageViewController
let vc = UIStoryboard(name: "Intro", bundle: nil).instantiateViewControllerWithIdentifier("IntroPage2")
setViewControllers([getSlide(index)],direction: .Forward,animated: true,completion: nil)
UIViewController called from tab view that does not render signin subview
I am posting AN answer but really it's more of a work around. After hours of trying to finagle the views I get the code working consistently. To get it working created a new Xib and new VC file for the Signin module. I copied all the code from my original Signin class to the new one and it worked. No code was altered.
So if anyone comes across the gnarly painful situation give that a shot! This could very well be an XCode bug.