I have a problem with accessing an instance var in an event handler.
When I'm trying to access an instance var in an event handler, it seems to always have the initial value, even though the value has changed since.
In my simple demo app, I have a horizontal slider and a UIImageView:
ViewController.swift:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var slider: UISlider!
var canvas:MyCanvas?
override func viewDidLoad() {
super.viewDidLoad()
self.canvas = MyCanvas()
}
#IBAction func sliderValueChanged(sender: UISlider) {
self.canvas!.sliderValueUpdated(sender.value);
}
}
MyCanvas.swift:
import Foundation
import UIKit
class MyCanvas : UIImageView {
// the var I care about
var sliderValue:Float = 0.5;
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override init() {
super.init()
}
func sliderValueUpdated (newValue:Float) {
self.sliderValue = newValue;
// prints the correct value
println(self.sliderValue)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
// always prints initial value, 0.5
println(self.sliderValue)
}
}
I set MyCanvas.swift as custom class for the UIImageView.
So when I adjust the slider, it prints out the correct value for self.sliderValue. However, when I'm firing a touch event after that, it always prints out the initial value, 0.5 instead of the slider value.
Why is that?
How do I fix this?
I think your problem is that you don't have an IBOutlet to your canvas. By initializing the canvas in viewDidLoad, the initialized canvas is not actually on the screen (you would have to add it to the subviews). Thus, your sliderValueChanged IBAction is not actually updating the sliderValue property of the MyCanvas on the screen. I recommend setting your UIImageView in the storyboard as a MyCanvas class and making an IBOutlet to that MyCanvas object in your Storyboard. Then remove the canvas = MyCanvas() from your viewDidLoad function.
Related
newbie swift programmer here that needs help with something that probably is trivial...
This is in different File, different View:
class CanvasContainerView: UIView {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// here are all of the calculations that lead to scoreString being calculated..
var scoreString = round(1000 * accuracyTotal / angleTotal)
}
//lots of other code
}
I need the value of my calculated "scoreString" to be accessible in a different file, different controller. How can i pass this value through if i'm using TouchesMoved function ? Below is how i tried to implement it in different file but failed miserably as there is no "scoreString" in scope:
import UIKit
class CanvasMainViewController: UIViewController {
#IBOutlet weak var ScoreText: UITextField!
/// Prepare the drawing canvas.
/// - Tag: CanvasMainViewController-viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
ScoreText.text = "\(CanvasContainerView.touchesMoved(scoreString))"
}
//lots of code
}
In the end i want my score to be displayed on top of the screen:
So i am aware that there are questions like this already on Stack but whats specific about my question is that im using this "touchesMoved" overriden function that doesn't let me return any value. It shouts it needs void return type.
I will be very thankful for any help.
Using a delegate is the good method :
// the protocol (ie the method called by container view in the controller)
protocol CanvasContainerViewDelegate {
func scoreUpdate(fromView view:CanvasContainerView, newScore: Double)
}
class CanvasContainerView: UIView {
// the delegate that will be called
var delegate: CanvasContainerViewDelegate?
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// here are all of the calculations that lead to scoreString being calculated..
var scoreString = round(1000 * accuracyTotal / angleTotal)
// Telling the delegate the new score value
delegate?.scoreUpdate(fromView: self, newScore: scoreString)
}
//lots of other code
}
class CanvasMainViewController: UIViewController, CanvasContainerViewDelegate {
#IBOutlet weak var ScoreText: UITextField!
#IBOutlet weak var containerView: CanvasContainerView!
/// Prepare the drawing canvas.
/// - Tag: CanvasMainViewController-viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// set the controller as a delegate of the view
containerView.delegate = self
}
// Update the score (called by the view)
func scoreUpdate(fromView view: CanvasContainerView, newScore: Double) {
ScoreText.text = "\(newScore)"
}
//lots of code
}
Apparently adding public in front of the var declared on top of the file solved the problem. Thank you for participation in trying to help me.
I have created a viewcontroller which consists of two classes. These are displayed below:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var Viewholder: UIImageView!
var txt:String?
override func viewDidLoad() {
super.viewDidLoad()
let vv= TestView(frame: Viewholder.frame)
view.addSubview(vv)
txt= "hello"
let rr = TestView(frame: self.view.frame,textvalue:txt!)
rr.colour = "" // set value
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
class TestView: UIView {
var textvalue:String?
init(frame: CGRect,textvalue) {
self.textvalue= textvalue
super.init(frame: frame)
self.sharedLayout()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
self.setupPaths()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func sharedLayout() {
print(textvalue!) // works here
}
func setupPaths() {
print(textvalue!) // doesnt work here (displays nil)
}
How can I make it work so that the value of textvalue in "setupPaths" gives the correct value so that I can then changee the text of a label. I am having trouble getting the setuppath function to display the passed value as it is returning null. This is stopping me from editting the label with a passed value.
You can create a customView like this
class TestView: UIView {
var colour:String?
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.sharedLayout()
}
init(frame: CGRect,colour:String) {
self.colour = colour
super.init(frame: frame)
self.sharedLayout()
}
func sharedLayout() {
}
}
//
You can create it like this
let rr = TestView(frame: self.view.frame,colour:"test")
// rr.colour = "" // set value
You are badly confused about object-oriented programming. In your code:
let vv= TestView(frame: Viewholder.frame)
view.addSubview(vv)
txt= "hello"
let rr = TestView(frame: self.view.frame,textvalue:txt!)
The let vv line creates a TestView object and installs it as a subview of the view controller's content view.
Then the next 2 lines create a NEW instance of TestView and install text into that 2nd view. You then forget about this second view.
This is like buying a new car, setting the station on the radio of the new car, abandoning the new car, and then going home and wondering why the radio station on your existing car didn't change. The two objects are not the same object and settings in one instance of TestView have no effect on the other instance of TestView.
I work with Nibs. I have two screens that will use the "same" UIView component with the same behavior. It's not the same component because in each screen i placed a UIView and made the same configuration, as show on the image.
To solve this and prevent replicate the same code in other classes i wrote one class, that is a UIView subclass, with all the functions that i need.
After that i made my custom class as superclass of these UIView components to inherit the IBOutlets and all the functions.
My custom class is not defined in a Nib, is only a .swift class.
I made all the necessary connections but at run time the IBOutlets is Nil.
The code of my custom class:
class FeelingGuideView: UIView {
#IBOutlet weak var firstScreen: UILabel!
#IBOutlet weak var secondScreen: UILabel!
#IBOutlet weak var thirdScreen: UILabel!
#IBOutlet weak var fourthScreen: UILabel!
private var labelsToManage: [UILabel] = []
private var willShow: Int!
private var didShow: Int!
override init(frame: CGRect) {
super.init(frame: frame)
self.initLabelManagement()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initLabelManagement()
}
private func initLabelManagement() {
self.initLabelVector()
self.willShow = 0
self.didShow = 0
self.setupLabelToShow(label: labelsToManage[0])
self.setupLabels()
}
private func initLabelVector() {
self.labelsToManage.append(self.firstScreen)
self.labelsToManage.append(self.secondScreen)
self.labelsToManage.append(self.thirdScreen)
self.labelsToManage.append(self.fourthScreen)
}
private func setupLabels() {
for label in labelsToManage {
label.layer.borderWidth = 2.0
label.layer.borderColor = UIColor(hex: "1A8BFB").cgColor
}
}
func willShowFeelCell(at index: Int) {
self.willShow = index
if willShow > didShow {
self.setupLabelToShow(label: labelsToManage[willShow])
}
else if willShow < didShow {
for i in didShow ... willShow + 1 {
let label = labelsToManage[i]
self.setupLabelToHide(label: label)
}
}
}
private func setupLabelToShow(label: UILabel) {
label.textColor = UIColor.white
label.backgroundColor = UIColor(hex: "1A8BFB")
}
private func setupLabelToHide(label: UILabel) {
label.textColor = UIColor(hex: "1A8BFB")
label.backgroundColor = UIColor.white
}
}
I found this question similar to mine: Custom UIView from nib inside another UIViewController's nib - IBOutlets are nil
But my UIView is not in a nib.
EDIT:
I overrided the awakeFromNib but it neither enter the method.
More explanation:
My custom class is only superClass of this component:
Which i replicate on two screens.
One screen is a UITableViewCell and the another a UIViewController.
It's all about to manage the behavior of the labels depending on the screen that is showing at the moment on the UICollectionView
When the initLabelVector() function is called at the required init?(coder aDecoder:) it arrises a unwrap error:
The error when try to open the View:
Cannot show the error with the UITableViewCell because it is called at the beginning of the app and don't appear nothing. To show the error with the screen i needed to remove the call of the UITableViewCell.
The UITableViewCell is registered first with the tableView.register(nib:) and after using the tableView.dequeueReusebleCell.
The UIViewController is called from a menu class that way:
startNavigation = UINavigationController(rootViewController: FellingScreenViewController())
appDelegate.centerContainer?.setCenterView(startNavigation, withCloseAnimation: true, completion: nil)
The problem is this code:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initLabelManagement()
}
The trouble is that init(coder:) is too soon to speak of the view's outlets, which is what initLabelManagement does; the outlets are not hooked up yet. Put this instead:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.initLabelManagement()
}
How I arrived at this answer:
As a test, I tried this:
class MyView : UIView {
#IBOutlet var mySubview : UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
print(#function, self.mySubview)
}
override func awakeFromNib() {
super.awakeFromNib()
print(#function, self.mySubview)
}
}
Here's the output:
init(coder:) nil
awakeFromNib() <UIView: 0x7fc17ef05b70 ... >
What this proves:
init(coder:) is too soon; the outlet is not hooked up yet
awakeFromNib is not too soon; the outlet is hooked up
awakeFromNib is called, despite your claim to the contrary
When the init() from my custom class is called the IBOutlets are not hooked up yet.
So, I created a reference on the parent view and called the iniLabelManagement() from the viewDidLoad() method and everything worked.
Thank you matt, for the help and patience!
I've recently dived into the world of Swift 3 and Xcode 8 and have been working on a simple game to learn as much as possible but I have run into an issue where my custom class is crashing when I segue into it because the IBOutlets which I dragged into it are nil.
I am using StoryBoard and AutoLayout and have dragged several fields into my class statistics:UIScrollView from the storyboard.
class StatisticsView: UIScrollView, sendLabelDelegate {
#IBOutlet weak var GamesWonNum: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.GamesWonNum = aDecoder.decodeObject(forKey: "GamesWonNum") as! UILabel!
}
override func encode(with aCoder: NSCoder) {
aCoder.encode(self.GamesWonNum, forKey: "GamesWonNum")
}
func setLabel(data: String) {
let temp = Int(data)
setNumber(num: temp!)
}
override func awakeFromNib() { }
override init(frame: CGRect) {
super.init(frame: frame)
}
func setNumber(num: Int){
GamesWonNum.text = String(num)
}
}
I have made sure that the statistics class is set as the identifier for the storyboard scrollview. I've done a lot of research into the topic, and I couldn't find a concrete answer on exactly how this works. I found in another post that calling the function awakefromNib() will fix the issue and it has, but I want to understand what awakefromNib() does and why I cannot simply init() the view.
My view Controller hierarchy is as follows:
MainMenuVC | --Segue--> SettingsVC
MainMenyVC | --Segue--> StatisticsVC
class StatisticsVC: UIViewController, sendLabelDelegate{
#IBOutlet weak var StatsScrollView: UIScrollView!
var settingsLabel:String = ""
override func viewDidLoad() {
super.viewDidLoad()
if UIScreen.main.bounds.height < 600 {
StatsScrollView.contentSize.height = 700
print("screensizeif: \(UIScreen.main.bounds.height), \(StatsScrollView.contentSize.height)")
}else{
StatsScrollView.contentSize.height = UIScreen.main.bounds.height
print("screensizeelse: \(UIScreen.main.bounds.height), \(StatsScrollView.contentSize.height)")
}
StatsScrollView.contentSize.width = UIScreen.main.bounds.width
StatsScrollView.isUserInteractionEnabled = true
}
func setLabel(data: String) {
settingsLabel = data
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
}
My understanding is that the viewController will init all of its views before going into the viewdidLoad() function, so why is awakefromNib() necessary and what exactly should I define inside of it. Further, why can't I initialize the class inside init()? If I try to set the GamesWon Outlet inside init(), the outlet is nil.
Its a bit difficult for me to wrap my head around the relationship that exists here between a view controller, the custom scrollview I have, and the outlets being nil. If anyone could clarify I would very much appreciate it.
Thank you!
I've got view controller (using Storyboards if matters). Controller got custom view inside it let's call it AView. The view is laid out on storyboard as UIView object with custom class set. AView's contents are on separate XIB because I need this highly reusable. Here's how code looks like:
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //doesn't work
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
aView.setup(false) //doesn't work
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
aView.setup(false) //do work but glitches
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
leftConstraint.active = true
rightConstraint.active = false
self.layoutIfNeeded()
}
}
I need to setup this view before it appears, based on some parameters. I'm modifying only its internal content from inside. If I call aView.setup(shouldBeOnLeft:) in viewDidLoad or viewWillAppear constraints don't update or maybe do but I don't see changes. If I move it to viewDidAppear it works but obviously I see misplaced views for a while (state before setup).
The question is: how to get it work as intended and without view's manipulation form view controller and independent on how and where setup method is called unless it's inside or right after VC's viewDidLoad? Only thing that VC needs to know is to call setup with parameter.
You should call it inside viewDidLayoutSubviews(), at this point it have set the view parameters, and you can manipulate the view before is presented to the user (so, no glitches)
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/viewDidLayoutSubviews
Discussion
Called to notify the view controller that its view has just laid out its subviews.
OK I figured that out - but I don't actually like it in 100%
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //now it does work
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
self.yesIfLeft = shouldBeOnLeft
}
private var yesIfLeft = false {
didSet{
self.updateLayout()
}
}
private func updateLayout() {
leftConstraint.active = self.yesIfLeft
rightConstraint.active = !self.yesIfLeft
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateLayout()
}
}
Whole point is to keep reference to settings that cause layout change and update them also in layoutSubviews(). Doing this way VC doesn't have to know about internals of AView (that's a big plus for me) but requires that AView holds it's configuration which is some extra work to do. I'm not marking this answer as accepted for now because maybe someone will have some better idea.