Why is my array empty when updating the view? (Swift) - swift

Whilst debugging I can see that the fixtureList in the controller returns 6 values. However, when the code jumps into the fixtureView.update(...) within the View, the fixtureList value is 0.
I'm sure it's something to do with creating the same variable in both controller and view, but can't figure it out!
What would be the best way to resolve this issue? Esentially I want the 6 values to be received by the View so that the user interface can be populated.
Controller
var fixtureList = [Fixture]()
func updateScores() {
liveScoreApi.fetchFixtures() { (
isOK, fixture) in
if isOK == true {
for fixture in (fixture?.fixtures)! {
self.fixtureList.append(fixture)
}
self.fixtureView.update(/*dump*/(self.fixtureList))
}
else {
NSLog("error fetching!")
}
}
}
View
class FixtureView: NSView {
#IBOutlet weak var homeNameTextField: NSTextField!
#IBOutlet weak var timeTextField: NSTextField!
#IBOutlet weak var awayNameTextField: NSTextField!
let fixtureList = [Fixture]()
func update(_ fixture: [Fixture]) {
// do UI updates on the main thread
DispatchQueue.main.async {
for fixture in self.fixtureList {
self.homeNameTextField.stringValue = fixture.homeName
self.timeTextField.stringValue = fixture.time
self.awayNameTextField.stringValue = fixture.awayName
print("added \(fixture.homeName)")
}
}
}
}

Your update method should look like this:
//let fixtureList = [Fixture]() // you don't need this
func update(_ fixtureList: [Fixture]) {
// do UI updates on the main thread
DispatchQueue.main.async {
// (though you probably don't want this ultimately)
for fixture in fixtureList {
self.homeNameTextField.stringValue = fixture.homeName
self.timeTextField.stringValue = fixture.time
self.awayNameTextField.stringValue = fixture.awayName
print("added \(fixture.homeName)")
}
}
}
The problem is that you are newing up an array of Fixture objects at the class level. Then, inside your update method, you are attempting to iterate over the new array rather than your passed in array.
Just eliminate the unnecessary array at the class level.
change the name of your parameter to fixtureList
remove self. from fixtureList your for in loop

Related

How can I refer to self during initialization when using IBOutlets for UIViews?

I have this custom UITableViewCell class backing my cells in a table view (I learned this approach from a talk by Andy Matuschak who worked at Apple on the UIKit team).
In the project I'm trying to apply this to now, I'm having a problem initializing the class because of a couple of #IBOutlets that are linked to UIView elements that won't ever have values like the UILabels get upon initialization:
class PublicationTableViewCell: UITableViewCell {
#IBOutlet weak var publicationTitle: UILabel!
#IBOutlet weak var authorName: UILabel!
#IBOutlet weak var pubTypeBadgeView: UIView!
#IBOutlet weak var pubTypeBadge: UILabel!
#IBOutlet weak var topHighlightGradientStackView: UIStackView!
#IBOutlet weak var bottomBorder: UIView!
struct ViewData {
let publicationTitle: UILabel
let authorName: UILabel
let pubTypeBadge: UILabel
}
var viewData: ViewData! {
didSet {
publicationTitle = viewData.publicationTitle
authorName = viewData.authorName
pubTypeBadge = viewData.pubTypeBadge
}
// I have #IBOutlets linked to the views so I can do this:
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
if isSelected || isHighlighted {
topHighlightGradientStackView.isHidden = true
bottomBorder.backgroundColor = UIColor.lightGrey1
} else {
topHighlightGradientStackView.isHidden = false
}
}
}
extension PublicationTableViewCell.ViewData {
init(with publication: PublicationModel) {
// Xcode complains here about referring to the properties
// on the left before all stored properties are initialized
publicationTitle.text = publication.title
authorName.text = publication.formattedAuthor.name
pubTypeBadge.attributedText = publication.pubTypeBadge
}
}
I then initialize it in cellForRow by passing in a publication like this:
cell.viewData = PublicationTableViewCell.ViewData(with: publication)
Xcode is complaining that I'm using self in init(with publication: PublicationModel) before all stored properties are initialized which I understand, but I can't figure out how to fix.
If these weren't UIView properties I might make them optional or computed properties perhaps, but because these are IBOutlets, I think they need to be implicitly unwrapped optionals.
Is there some other way I can get this to work?
First of all:
struct ViewData {
let publicationTitle: String
let authorName: String
let pubTypeBadge: NSAttributedString
}
Then simply:
extension PublicationTableViewCell.ViewData {
init(with publication: PublicationModel)
publicationTitle = publication.title
authorName = publication.formattedAuthor.name
pubTypeBadge = publication.pubTypeBadge
}
}
It's a data model, it shouldn't hold views, it should hold only data.
Then:
var viewData: ViewData! {
didSet {
publicationTitle.text = viewData.publicationTitle
authorName.text = viewData.authorName
pubTypeBadge.attributedString = viewData.pubTypeBadge
}
}
However, I think it would be simpler if you just passed PublicationModel as your cell data. There is no reason to convert it to another struct.
var viewData: PublicationModel! {
didSet {
publicationTitle.text = viewData.publicationTitle
authorName.text = viewData.authorName
pubTypeBadge.attributedString = viewData.pubTypeBadge
}
}

Swift: how to change a class into a struct

I have written a very simple MVC program with a label and a button. The label displays the value of a counter which is incremented when the button is pressed.
The code that follows gives the model file and the view controller file. The model file is a class.
That code works.
Here is the model file:
import Foundation
protocol MVCModelDelegate {
var message: String {get set}
}
// when I change class to struct
// delegate is set to nil
// causing the program not to update the label
class Model {
var delegate: MVCModelDelegate?
var counter: Int
var message: String {
didSet {
delegate?.message = model.message
}
}
init(counter: Int, message: String) {
self.counter = counter
self.message = message
}
}
// create instance
var model = Model(counter: 0, message: "")
// counting
func incrementCounter() {
model.counter += 1
model.message = "Counter Value: \(model.counter)"
}
Here is the view controller file
import Cocoa
class ViewController: NSViewController, MVCModelDelegate {
// communication link to the model
var model1 = model
var message = model.message {
didSet {
didUpdateModel(message: message)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.model1.delegate = self
}
// update display
func didUpdateModel(message: String) {
Label1.stringValue = model1.message
}
// Label
#IBOutlet weak var Label1: NSTextField! {
didSet {
Label1.stringValue = " counter not started"
}
}
// Button
#IBAction func testButton(_ sender: NSButton) {
incrementCounter()
}
}
Problem: I now want to change the code of the Model file to use a struct in place of a class as this very simple program does not need all the functionalities of a class. But as soon as I change class for struct. The program still runs, but the label is not updated as the delegate variable is set to nil and never gets another value.
Question: What am I missing? The swift documentation encourages to use struct when possible. And I do not see in that code what could be a problem of transforming the class into a struct.
A struct is immutable and it is not passed by reference like a class, but by value. This means that when you do
var model1 = model
You are creating a copy of model.
modifying model1 will not propagate the changes to model, therefore all you do in incrementCounter is not reflected in your view controller.
If you want your incrementCounter to affect the viewController it must use the model in your viewController instance.
If your intention is to share the model properties and change the label value when this model is updated, struct is not the best choice.

Initializing class object swift 3

i have a problem while I'm initializing object of some class. What is wrong with that? (I can upload all my code but it is large if needed)
Edit:
My view controller code:
import UIKit
class ViewController: UIViewController{
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var answerStackView: UIStackView!
// Feedback screen
#IBOutlet weak var resultView: UIView!
#IBOutlet weak var dimView: UIView!
#IBOutlet weak var resultLabel: UILabel!
#IBOutlet weak var feedbackLabel: UILabel!
#IBOutlet weak var resultButton: UIButton!
#IBOutlet weak var resultViewBottomConstraint: NSLayoutConstraint!
#IBOutlet weak var resultViewTopConstraint: NSLayoutConstraint!
var currentQuestion:Question?
let model = QuizModel()
var questions = [Question]()
var numberCorrect = 0
override func viewDidLoad() {
super.viewDidLoad()
model.getQuestions()
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setAll(questionsReturned:[Question]) {
/*
// Do any additional setup after loading the view, typically from a nib.
// Hide feedback screen
dimView.alpha = 0
// Call get questions
questions = questionsReturned
// Check if there are questions
if questions.count > 0 {
currentQuestion = questions[0]
// Load state
loadState()
// Display the current question
displayCurrentQuestion()
}
*/
print("Called!")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
My QuizModel code:
import UIKit
import FirebaseDatabase
class QuizModel: NSObject {
override init() {
super.init()
}
var ref:FIRDatabaseReference?
var test = [[String:Any]]()
var questions = [Question]()
weak var prot:UIPageViewControllerDelegate?
var first = ViewController()
func getQuestions(){
getRemoteJsonFile()
}
func pars(){
/*let array = test
var questions = [Question]()
// Parse dictionaries into Question objects
for dict in array {
// Create question object
let q = Question()
// Assign question properties
q.questionText = dict["question"] as! String
q.answers = dict["answers"] as! [String]
q.correctAnswerIndex = dict["correctIndex"] as! Int
q.module = dict["module"] as! Int
q.lesson = dict["lesson"] as! Int
q.feedback = dict["feedback"] as! String
// Add the question object into the array
questions += [q]
}
*/
//Protocol setAll function
first.setAll(questionsReturned: questions)
}
func getRemoteJsonFile(){
ref = FIRDatabase.database().reference()
ref?.child("Quiz").observeSingleEvent(of: .value, with: { (snapchot) in
print("hey")
let value = snapchot.value as? [[String:Any]]
if let dict = value {
self.test = dict
self.pars()
}
})
}
This isn't my all code but I think that is the most important part. In QuizModel code I'm reskining my code from getting json file to get data from firebase so you can see names of functions like 'getRemoteJSONFile' and in parse function parsing json but it isn't an issue of my problem I think
It looks like you're trying to initialize a constant before you've initialized the viewController it lives in. Your have to make it a var and initialize it in viewDidLoad (or another lifecycle method), or make it a lazy var and initialize it at the time that it's first accessed.
The problem boils down to the following:
class ViewController ... {
let model = QuizModel()
}
class QuizModel ... {
var first = ViewController()
}
The variable initializers are called during object initialization. When you create an instance of ViewController, an instance of QuizModel is created but its initialization creates a ViewController which again creates a new QuizModel and so on.
This infinite cycle will sooner or later crash your application (a so called "stack overflow").
There is probably some mistake on your side. Maybe you want to pass the initial controller to QuizModel, e.g.?
class ViewController ... {
lazy var model: QuizModel = {
return QuizModel(viewController: self)
}
}
class QuizModel ... {
weak var viewController: ViewController?
override init(viewController: ViewController) {
self.viewController = viewController
}
}
Also make sure you are not creating ownership cycles (that's why I have used weak).
In general it's a good idea to strictly separate your model classes and your UI classes. You shouldn't pass view controllers (or page view controller delegate) to your model class. If you need to communicate with your controller, create a dedicated delegate for that.

Using NSTreeController with NSOutlineView

I'm trying (unsuccessfully) to build a TreeController-controlled NSOutlineView. I've gone through a bunch of tutorials, but they all pre-load the data before starting anything, and this won't work for me.
I have a simple class for a device:
import Cocoa
class Device: NSObject {
let name : String
var children = [Service]()
var serviceNo = 1
var count = 0
init(name: String){
self.name = name
}
func addService(serviceName: String){
let serv = "\(serviceName) # \(serviceNo)"
children.append(Service(name: serv))
serviceNo += 1
count = children.count
}
func isLeaf() -> Bool {
return children.count < 1
}
}
I also have an even more simple class for the 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
Finally, I have the ViewController:
class ViewController: NSViewController {
var stepper = 0
dynamic var devices = [Device]()
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
}
#IBAction func addService(_ sender: Any) {
for i in 0..<devices.count {
devices[i].addService(serviceName: "New Service")
}
}
}
Obviously I have 2 buttons, one that adds a 'device' and one that adds a 'service' to each device.
What I can't make happen is any of this data show up in the NSOutlineView. I've set the TreeController's Object Controller Property to Mode: Class and Class: Device, and without setting the Children, Count, or Leaf properties I get (predictably):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
If I then set the Children property to 'children' things go very bad:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
All I'm trying to do is set up the NSOutlineView to take input from the NSTreeController so that when a new 'Device' is added to the devices[] array, it shows up in the Outline View.
If anyone could point me in the right direction here I'd be most grateful.
Much gratitude to Warren for the hugely helpful work. I've got it (mostly) working. A couple of things that I also needed to do, in addition to Warren's suggestions:
Set the datastore for the Tree Controller
Bind the OutlineView to the TreeController
Bind the Column to the TreeController
Bind the TableView Cell to the Table Cell View (yes, really)
Once all that was done, I had to play around with the actual datastore a bit:
var name = "Bluetooth Devices Root"
var deviceStore = [Device]()
#IBOutlet var treeController: NSTreeController!
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
deviceStore.append(Device(name: "Bluetooth Devices"))
self.treeController.content = self
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
if(deviceStore[0].name == "Bluetooth Devices"){
deviceStore.remove(at: 0)
}
Turns out the Root cannot be child-less at the beginning, at least as far as I can tell. Once I add a child, I can delete the place-holder value and the tree seems to work (mostly) as I want. One other thing is that I have to reload the data and redisplay the outline whenever the data changes:
outlineView.reloadData()
outlineView.setNeedsDisplay()
Without that, nothing. I still don't have the data updating correctly (see comments below Warren's answer) but I'm almost there.
To state the obvious, a NSTreeController manages a tree of objects all of which need to answer the following three questions/requests.
Are you a leaf i.e do you have no children? = leafKeyPath
If you are not a leaf, how many children do you have ? = countKeyPath
Give me your children! = childrenKeyPath
Its simple to set these up in IB or programatically. A fairly standard set of properties is respectively.
isLeaf
childCount
children
But its totally arbitrary and can be any set of properties that answer those questions.
I normally set up a protocol named something like TreeNode and make all my objects conform to it.
#objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
For your Device object you answer 2 out 3 question with isLeaf and children but don't answer the childCount question.
Your Device's children are Service objects and they answer none of that which is some of the reason why you are getting the exceptions.
So to fix up your code a possible solution is ...
The Service object
class Service: NSObject, TreeNode {
let name: String
init(name: String){
self.name = name
}
var isLeaf:Bool {
return true
}
var childCount:Int {
return 0
}
var children:[TreeNode] {
return []
}
}
The Device object
class Device: NSObject, TreeNode {
let name : String
var serviceStore = [Service]()
init(name: String){
self.name = name
}
var isLeaf:Bool {
return serviceStore.isEmpty
}
var childCount:Int {
return serviceStore.count
}
var children:[TreeNode] {
return serviceStore
}
}
And a horrible thing to do from a MVC perspective but convenient for this answer. The root object.
class ViewController: NSViewController, TreeNode {
var deviceStore = [Device]()
var name = "Henry" //whatever you want to name your root
var isLeaf:Bool {
return deviceStore.isEmpty
}
var childCount:Int {
return deviceStore.count
}
var children:[TreeNode] {
return deviceStore
}
}
So all you need to do is set the content of your treeController. Lets assume you have an IBOutlet to it in your ViewController.
class ViewController: NSViewController, TreeNode {
#IBOutlet var treeController:NSTreeController!
#IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
Now each time you append a Device or add a Service just call reloadItem on the outlineView (that you also need an outlet to)
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
outlineView.reloadItem(self, reloadChildren: true)
}
Thats the basics and should get you started but the docs for NSOutlineView & NSTreeController have a lot more info.
EDIT
In addition to the stuff above you need to bind your outline view to your tree controller.
First ensure your Outline View is in view mode.
Next bind the table column to arrangedObjects on the tree controller.
Last bind the text cell to the relevant key path. In your case it's name. objectValue is the reference to your object in the cell.

App crash when successfully passing data

I have a simple app. A table view passing user information from 1 view controller to another. It successfully prints out my user object and I can see the data in the console, however when I go to extract the data from the object into my view it does not display
#IBOutlet weak var chatPartnerLabel: UILabel!
var user: User? {
didSet {
print(user) //This prints out all information fine
chatPartnerLabel.text = user?.firstName //This crashes the app because chatPartnerLabel.text is nil
}
}
override func viewDidLoad() {
super.viewDidLoad()
chatPartnerLabel.text = user?.firstName //This comes up as nil
}
I have also tried hardcoding the label to see if it works properly
chatPartnerLabel.text = "My text" //Changes the label to My Text
I have also tried putting user?.firstName in viewDidAppear and viewWillAppear both display an empty label.
As you can see in the console my user is there with data. The crash I get when attempting to set is coming up as nil
What am I doing wrong to successfully change my chatPartnerLabel to either the name or firstname?
class User {
var firstName = "fn"
}
///uiviewcontroller:
#IBOutlet weak var chatPartnerLabel: UILabel!
var user: User? {
didSet {
print(user) //This prints out all information fine
chatPartnerLabel.text = user?.firstName //This crashes the app
}
}
both:
override func viewDidLoad() {
super.viewDidLoad()
user = User()
and:
override func viewDidLoad() {
super.viewDidLoad()
chatPartnerLabel.text = user?.firstName
work! I've tested!
We need more information and correct callstack. May be chatPartnerLabelis nil while unwrapping.
if you are setting user in segue or before pushing/presenting controller, it will crash, because label is not instantiated yet. That's why you have to rewrite your code to:
#IBOutlet weak var chatPartnerLabel: UILabel! {
didSet {
print(user?.firstName) // just for check
chatPartnerLabel.text = user?.firstName
}
}
var user: User? {
didSet {
print(user) //This prints out all information fine
}
}
This works Swift 3. I added a Boolean control to know when viewDidLoad is called.
var user: (firstName : String, lastName : String, controlView : Bool)? {
didSet {
if (self.user?.controlView)! {
print(user!)
chatPartnerLabel.text = user?.firstName
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
user?.controlView = true
}