Swift OSX - Delegate protocol function returns nil, crashes when unwrapping textfield value - swift

I'm working on an OSX app with Swift which makes use of an NSSplitView which holds two view controllers: "TableViewController" and "EntryViewController". I'm using delegates in order to transmit a custom NSObject ("Entry") on click from TableViewController up to the SplitViewController, then back down to the EntryViewController.
My problem is this: When the Entry object is received in the EntryViewController, any attempt to assign its properties to a text field value result in an unexpectedly found nil type error, never mind that the IBOutlets are properly linked, and that it can both print the Entry.property and the textfield string value (provided it is in a different, unrelated function).
I have tried many arrangements to solve this problem, which is why the current configuration might be a bit over-complicated. A delegate relation straight from Table VC to Entry VC caused the same issues.
Is there some way that the IBOutlets are not connecting, even though the view has loaded before the delegate is called? I've read many many articles on delegation—mostly for iOS—and yet can't seem to find the root of my problems. I'll be the first to admit that my grasp of Swift is a little bit piecemeal, so I am open to the possibility that what I am trying to do is simply bad/hacky coding and that I should try something completely different.
Thanks for your help!
TableViewController:
protocol SplitViewSelectionDelegate: class {
func sendSelection(_ entrySelection: NSObject)
}
class TableViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
#IBOutlet var tableArrayController: NSArrayController!
#IBOutlet weak var tableView: NSTableView!
var sendDelegate: SplitViewSelectionDelegate?
dynamic var dataArray = [Entry]()
// load array from .plist array of dictionaries
func getItems(){
let home = FileManager.default.homeDirectoryForCurrentUser
let path = "Documents/resources.plist"
let urlUse = home.appendingPathComponent(path)
let referenceArray = NSArray(contentsOf: urlUse)
dataArray = [Entry]()
for item in referenceArray! {
let headwordValue = (item as AnyObject).value(forKey: "headword") as! String
let defValue = (item as AnyObject).value(forKey: "definition") as! String
let notesValue = (item as AnyObject).value(forKey: "notes") as! String
dataArray.append(Entry(headword: headwordValue, definition: defValue, notes: notesValue))
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.sendDelegate = SplitViewController()
getItems()
print("TVC loaded")
// Do any additional setup after loading the view.
}
// send selection forward to entryviewcontroller
#IBAction func tableViewSelection(_ sender: Any) {
let index = tableArrayController.selectionIndex
let array = tableArrayController.arrangedObjects as! Array<Any>
let obj: Entry
let arraySize = array.count
if index <= arraySize {
obj = array[index] as! Entry
print(index)
print(obj)
sendDelegate?.sendSelection(obj)
}
else {
print("index unassigned")
}
}
}
SplitViewController:
protocol EntryViewSelectionDelegate: class {
func sendSecondSelection(_ entrySelection: NSObject)
}
class SplitViewController: NSSplitViewController, SplitViewSelectionDelegate {
var delegate: EntryViewSelectionDelegate?
#IBOutlet weak var mySplitView: NSSplitView!
var leftPane: NSViewController?
var contentView: NSViewController?
var entrySelectionObject: NSObject!
override func viewDidLoad() {
super.viewDidLoad()
// assign tableview and entryview as child view controllers
let story = self.storyboard
leftPane = story?.instantiateController(withIdentifier: "TableViewController") as! TableViewController?
contentView = story?.instantiateController(withIdentifier: "EntryViewController") as! EntryViewController?
self.addChildViewController(leftPane!)
self.addChildViewController(contentView!)
print("SVC loaded")
}
func sendSelection(_ entrySelection: NSObject) {
self.delegate = EntryViewController() //if this goes in viewDidLoad, then delegate is never called/assigned
entrySelectionObject = entrySelection
print("SVC:", entrySelectionObject!)
let obj = entrySelectionObject!
delegate?.sendSecondSelection(obj)
}
}
And Finally, EntryViewController:
class EntryViewController: NSViewController, EntryViewSelectionDelegate {
#IBOutlet weak var definitionField: NSTextField!
#IBOutlet weak var notesField: NSTextField!
#IBOutlet weak var entryField: NSTextField!
var entryObject: Entry!
override func viewDidLoad() {
super.viewDidLoad()
print("EVC loaded")
}
func sendSecondSelection(_ entrySelection: NSObject) {
self.entryObject = entrySelection as! Entry
print("EVC:", entryObject)
print(entryObject.headword)
// The Error gets thrown here:
entryField.stringValue = entryObject.headword
}
}

You don't need a delegate / protocol since there is a reference to EntryViewController (contentView) – by the way the instance created with EntryViewController() is not the instantiated instance in viewDidLoad.
Just use the contentView reference:
func sendSelection(_ entrySelection: NSObject) {
contentView?.sendSecondSelection(entrySelection)
}

Related

swift Passing data from containverView error

Heloo, i have this problem when im using contaienerView with static tableview. i want to pass my data from my main view controller to my tableview, and im having this break where it said my data is null, but it wasnt null because i already fill that data that i want to pass in. im using firebase and dictionary. heres my code :
my main controller :
class MainController: UITableViewController, AddPatientController {
private var patientLists = [PatientList]() // empty array buat isi list yg isinya nama pasien
var Segue : String = "PatientName"
var Segue2 : String = "PatientNotes"
let user : User = Auth.auth().currentUser!
private var rootRef : DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
self.rootRef = Database.database().reference()
populateList()
}
private func populateList() {// 5. func buat fetch data dari db ke hp
self.rootRef.child(self.user.emailWithoutSpecialChar).observe(.value) { (snapshot) in
self.patientLists.removeAll()
let pasienListDict = snapshot.value as? [String:Any] ?? [:] //7. ini berarti return buat kl dict nya kosong, ini buat akses ke valuenya yg isinya itu dict[String:Any]
for (key,_) in pasienListDict {
if let pasienlistdict = pasienListDict[key] as? [String:Any]{
if let pasienlist = PatientList(pasienlistdict) {
self.patientLists.append(pasienlist)
// ini buat ngemasukin ke dalem dictionarynya, ini buat store datanya dan ngambil datanya dari firebase db
}
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
} override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == Segue {
let nc = segue.destination as! UINavigationController
let addPatientName = nc.viewControllers.first as! AddListController
addPatientName.delegate = self
}
else if segue.identifier == Segue2 {
guard let indexPath = self.tableView.indexPathForSelectedRow else {return}
let nc = segue.destination as! PasienProfileController
nc.pasien = self.patientLists[indexPath.row]
}
}
and this is my controller that should recieve the data
class NotesController: UITableViewController, AddNotesDelegate {
var pasien : PatientList!
private var rootRef : DatabaseReference!
var Segue1 : String = "AddNotes"
var Segue2 : String = "PasienNotes"
override func viewDidLoad() {
super.viewDidLoad()
self.title = pasien.name // this is the line where my code break cause it says the data is null
self.rootRef = Database.database().reference()
}
my pasienprofilecontroller and my pasienProfileTableController( the containverView one) :
class PasienTableController: UITableViewController {
#IBOutlet weak var dataKunjunganLbl: UILabel!
#IBOutlet weak var diagnosaPasienLbl: UILabel!
#IBOutlet weak var alergiPasienLbl: UILabel!
var delegete : PasienTableControllerDelegate?
var patientList = [PatientList]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
class PasienProfileController: UIViewController {
#IBOutlet weak var TinggiLbl: UILabel!
#IBOutlet weak var beratLbl: UILabel!
#IBOutlet weak var GolDarahLbl: UILabel!
#IBOutlet weak var NamaLbl: UILabel!
#IBOutlet weak var ImagePic: UIImageView!
var pasien : PatientList!
#IBOutlet weak var ContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.ImagePic.makeRounded()
self.NamaLbl.text = pasien.name
}
this is the image of my storyboard to get a clear picture of what im trying to do
so my "Patient" view controller is my main controller, which after that it will show the profile pasien controller, which is uiview with container view that contain static tableview. and when i try to hit that " Data Kunjungan Pasien" Cell, it gets the error
so is theres something wrong with my logic? why it keep saying null while it can successfully show the data from Patient view to patientProfileView?
*this is my git if you guys wanna clone and check my error https://gitlab.com/afipermanaa/skripsi.git
Thanks for the help
I dont get it
if you want to pass the data from prepareForSegue with this code
else if segue.identifier == Segue2 {
guard let indexPath = self.tableView.indexPathForSelectedRow else {return}
let nc = segue.destination as! PasienProfileController
nc.pasien = self.patientLists[indexPath.row]
}
to NotesController why do you cast "nc" as PasienProfileController ?
its not the same class
i don't see where you set pasien value.
I hope it helps you if not please explain
cannot comment due to low rep, so I tried to explain it as clear as possible.
you may try to define that variable
private var patientLists = [PatientList]()
as static like below.
private static var patientLists = [PatientList]()
when you try to pass it through segue with this code
let nc = segue.destination as! PasienProfileController
nc.pasien = self.patientLists[indexPath.row]
your class creates a new instance of patientList array when there is 'self'. so, when you define as static, there won't be any new instance of it.
private static var patientLists = [PatientList]()

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
}
}

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.

How to pass value from NSViewController to custom NSView of NSPopover?

By using the delegation protocol I have tried to pass a string (inputFromUser.string) from NSViewController - mainController to custom subclass of NSView of NSPopover - PlasmidMapView, to drawRect function, see code below. But, it didn’t work. I don’t know where a mistake is. Maybe there is another way to pass this string.
Update
File 1.
protocol PlasmidMapDelegate {
func giveDataForPLasmidMap(dna: String)
}
class MainController: NSViewController {
#IBOutlet var inputFromUser: NSTextView!
var delegate: plasmidMapDelegate?
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
delegate?.giveDataForPLasmidMap(dna!)
}
}
File 2
class PlasmidMapView: NSView, PlasmidMapDelegate {
var dnaForMap = String()
func giveDataForPLasmidMap(dna: String) {
dnaForMap = dna
}
override func drawRect(dirtyRect: NSRect) {
let objectOfMainController = MainController()
objectOfMainController.delegate = self
//here I have checked if the string dnaForMap is passed
let lengthOfString = CGFloat(dnaForMap.characters.count / 10)
let pathRect = NSInsetRect(self.bounds, 10, 45)
let path = NSBezierPath(roundedRect: pathRect,
xRadius: 5, yRadius: 5)
path.lineWidth = lengthOfString //the thickness of the line should vary in dependence on the number of typed letter in the NSTextView window - inputDnaFromUser
NSColor.lightGrayColor().setStroke()
path.stroke()
}
}
Ok, there's some architecture mistakes. You don't need delegate method and protocol at all. All you just need is well defined setter method:
I. Place your PlasmidMapView into NSViewController-subclass. This view controller must be set as contentViewController-property of your NSPopover-control. Don't forget to set it the way you need in viewDidLoad-method or another.
class PlasmidMapController : NSViewController {
weak var mapView: PlacmidMapView!
}
II. In your PlacmidMapView don't forget to call needsDisplay-method on dna did set:
class PlasmidMapView: NSView {
//...
var dnaForMap = String() {
didSet {
needsDisplay()
}
//...
}
III. Set dna-string whenever you need from your MainController-class.
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
if let controller = popoverPlasmidMap.contentViewController as? PlasmidMapController {
controller.mapView.dna = dna
} else {
fatalError("Invalid popover content view controller")
}
}
In order to use delegation your class PlasmidMapView needs to have an instance of the MainController (btw name convention is Class, not class) and conform to the PlasmidMapDelegate (once again name convention dictates that it should be PlasmidMapDelegate). With that instance you then can:
mainController.delegate = self
So, after several days I have found a solution without any protocols and delegation as Astoria has mentioned. All what I needed to do was to make #IBOutlet var plasmidMapIBOutlet: PlasmidMapView!for my custom NSView in MainController class and then to use it to set the value for the dnaForMap in #IBAction func actionPopoverPlasmidMap(sender: AnyObject).
class PlasmidMapView: NSView
{
var dnaForMap = String()
}
class MainController: NSViewController
{
#IBOutlet var inputFromUser: NSTextView!
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPopoverPlasmidMap(sender: AnyObject)
{
plasmidMapIBOutlet.dnaForMap = inputDnaFromUser.string!
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
}
}

swift save multiple manage objects

Having issues saving my manage objects within my code. For some reason when i place data in the first view controller everything works well. For instance
I place new categories such as "Fruits", "Dairy", "Meats". The first view controller takes the data. When I click on the specific item such as "Dairy", and put in "Milk" for items within that section. If I go back to the previous view controller and click on "Meats", I see the same data i put in under "Dairy". How do i properly manage my NSManage objects.
Here is my code below.
import UIKit
import CoreData
class HomeSpecificItemViewController: UIViewController {
var selectedItem : [Items] = []
#IBOutlet weak var itemNameTextField: UITextField!
#IBOutlet weak var brandNameTextField: UITextField!
#IBOutlet weak var caloriesTextField: UILabel!
#IBOutlet weak var priceTextField: UILabel!
#IBOutlet weak var amountTextField: UITextField!
#IBOutlet weak var threshHoldNumberField: UITextField!
#IBOutlet weak var stepper: UIStepper!
override func viewDidLoad() {
super.viewDidLoad()
stepper.wraps = true
stepper.autorepeat = true
stepper.maximumValue = 10
// Do any additional setup after loading the view.
}
#IBAction func saveButton(sender: AnyObject) {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let itemDescription = NSEntityDescription.insertNewObjectForEntityForName("Items", inManagedObjectContext: context) as! Items
itemDescription.setValue(itemNameTextField.text, forKey: "Items")
itemDescription.setValue(brandNameTextField.text, forKey: "Items")
do {
try context.save()
}catch _ {
}
/*
let request = NSFetchRequest(entityName: "Items")
let results : [AnyObject]?
do {
results = try context.executeFetchRequest(request)
}catch _ {
results = nil
}
if results != nil {
self.itemDescription = results as! [Items]
}
*/
}
#IBAction func cancelPressed(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func increaseNumberStepper(sender: UIStepper) {
threshHoldNumberField.text = Int(sender.value).description
}
}
Do you have a specific view controller for each category? If so, what you have to do is add predicates to your more specific view controllers.
Something like:
var request = NSFetchRequest(entityName: "Food")
request.predicate = NSPredicate(format: "category == %#", "Meat")
meats = try! context.executeFetchRequest(request)
This would return an array of all Food objects whose category atribute holds the string "Meat".
I was saving my data to core data without properly declaring the manage context and without assigning the text labels to the core data object.
issue resolved!