I have a VC that contains of textField and button. I have a simple task to pass textField text data to my model entity. However, while I want to assign textField data to entity data, my data remains nil.
I can't understand what have caused such problem
class InputTextViewController: UIViewController {
#IBOutlet weak var messageTextField: UITextField!
//I receive user from parent VC by segue
var userSendMessageTo: User!
var tableView: UITableView?
var message: Message { return Message(userSendMessageTo) }
// initialising object of my entity class
override func viewDidLoad() {
super.viewDidLoad()
print(userSendMessageTo.name)
//name is right
}
func createMessage() {
message.messageText = messageTextField.text
message.messageTime = Date()
print(message.messageText) //nil
print(message.messageTime) //nil
}
#IBAction func sendMessge(_ sender: Any) {
createMessage()
userSendMessageTo.mesaageHistory.append(message)
print(userSendMessageTo.mesaageHistory[0].messageText) //nil
}
My model
class Message {
var messageText: String?
var messageTime: Date?
var messageInage: UIImage?
var user: User
init(_ user: User) {
self.user = user
}
}
class User {
let name: String
var mesaageHistory = [Message]()
init(name: String) {
self.name = name
}
}
The construct var message: Message { return Message(userSendMessageTo) } returns a new Message object every time it's called.
Therefore you are creating a message object, assigning text to it, then throwing it away. Then you create a second message object, assign a date to it and throw it away. Then you create a third message object and check its text (which is nil) and then you create a fourth message object and check its date, which is also nil.
Try this instead:
class InputTextViewController: UIViewController {
#IBOutlet weak var messageTextField: UITextField!
var userSendMessageTo: User!
var tableView: UITableView?
var message: Message?
func createMessage() {
message = Message()
message!.messageText = messageTextField.text
message!.messageTime = Date()
print(message!.messageText) //won't be nil
print(message!.messageTime) //won't be nil
}
}
You have declared message as a computed variable. This means that each time you reference message you are actually executing the following block of code:
Message(userSendMessageTo)
In other words, each reference to message creates a new instance of Message.
message.messageText = messageTextField.text - Creates a new instance of Message
message.messageTime = Date() - creates a new instance of Message
print(message.messageTime) - create a new instance of Message, and its messageTime property is nil
There is no need to use a property here; your createMessage function should return the new Message:
func createMessage(withText text: text) -> Message {
let message = Message(userSendMessageTo)
message.messageText = text
message.messageTime = Date()
return message
}
#IBAction func sendMessge(_ sender: Any) {
let message = self.createMessage(withText: messageTextField.text)
userSendMessageTo.mesaageHistory.append(message)
print(userSendMessageTo.mesaageHistory[0].messageText)
}
To be honest, if you create a proper initialiser for Message you could get rid of the createMessage function altogether:
class Message {
var messageText: String
var messageTime: Date
var messageInage: UIImage?
var user: User
class Message {
var messageText: String
var messageTime: Date
var messageInage: UIImage?
var user: User
init(_ user: User, text: String = "", date: Date = Date()) {
self.user = user
self.messageTime = date
self.messageText = text
}
}
Then your action method simply becomes:
#IBAction func sendMessge(_ sender: Any) {
let message = Message(userSendMessageTo, text: messageTextField.text, date:Date())
userSendMessageTo.mesaageHistory.append(message)
print(userSendMessageTo.mesaageHistory[0].messageText)
}
Related
I try to perform a segue from a button to a specific userID chatController with this code:
#IBAction func toChatPressed(_ sender: Any) {
let user = jobDetails?.userID
let chatLogController = ChatLogController(collectionViewLayout: UICollectionViewFlowLayout())
chatLogController.user = user
navigationController?.pushViewController(chatLogController, animated: true)
}
But its giving me Cannot assign value of type 'String?' to type 'User?' as an error...
chatController.user:
var user: User? {
didSet {
navigationItem.title = user?.name
observeMessages()
}
}
I get the userID passed from a tableViewCell and is saved in a structure
struct JobDetails {
var jobDetail: String
var userName: String
var userImage: UIImage?
var jobImage: UIImage?
var downloadURL: String?
var userLocation: String
var userID: String?
}
In chatController, the property user is of type User, i.e.
var user: User? {
....
}
Now, in toChatPressed(_:) method, you're setting the value of userID to chatController's user as evident from your code,
let user = jobDetails?.userID
Here userID is a String?. So, user in the above code is of type String?
chatLogController.user = user
In the above code, you are assigning user (of type String?) to chatLogController.user (of type User). This is what your error means.
Solution:
There can be 2 options to resolve the issue.
1. Either create a property userID of type String? in chatLogController, i.e.
var userID: String?
Now, set it from toChatPressed(_:) like,
chatLogController.userID = jobDetails?.userID
2. Or, you can create a User object using jobDetails in toChatPressed(_:) and then set it to chatLogController's user, i.e.
let user = User(jobDetails) //this depends upon the how you've defined User
chatLogController.user = user
In case you still have any confusion, feel free to ask.
You are assigning JobDetails.userID: String? to ChatLogController.user: User?
See your code:
let user = jobDetails?.userID
…
chatLogController.user = user
You have to map the jobDetails?.userID: String to a User?.
Inside of your struct JobDetails, add a new field containing the User object:
struct JobDetails {
var jobDetail: String
var userName: String
var userImage: UIImage?
var jobImage: UIImage?
var downloadURL: String?
var userLocation: String
var userID: String?
var user: User? // <-- New field
}
Whenever you create a new JobDetails, pass the User object to the user field.
Now, you function can use the user field instead of the userID field:
#IBAction func toChatPressed(_ sender: Any) {
let user = jobDetails?.user // <-- user of type User? instead of userID of type String?
let chatLogController = ChatLogController(collectionViewLayout: UICollectionViewFlowLayout())
chatLogController.user = user
navigationController?.pushViewController(chatLogController, animated: true)
}
My app crashes when I click a cell in my tableView of recent posts. The click is supposed to segue me to the MainTextView which has the postReplyButton. The segue worked until I started experimenting with creating comments for the posts.
Here is the MainTextView code:
import Foundation
import UIKit
import Firebase
class MainTextView: UIViewController {
#IBOutlet weak var titleText: UILabel!
#IBOutlet weak var mainText: UILabel!
#IBOutlet weak var commentPlaceHolder: UILabel!
#IBOutlet weak var newCommentLabel: UITextView!
var delegate:NewPostVCDelegate?
#IBAction func postReplyButton() {
// Firebase code here
let postRef = Database.database().reference().child("posts").childByAutoId()
let postObject = [
"comment": newCommentLabel.text,
"timestamp": [".sv": "timestamp"]
] as [String : Any]
postRef.setValue(postObject, withCompletionBlock: { error, ref in
if error == nil {
self.delegate!.didUploadPost(withID: ref.key!)
self.dismiss(animated: true, completion: nil)
} else {
// Handle error
}
})
newCommentLabel.text = String()
commentPlaceHolder.isHidden = false
}
var post: Post?
// MARK: - View Controller LifeCycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setMain()
}
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self as! UITextViewDelegate
}
private func setMain() {
guard let post = self.post else {
return
}
titleText.text = post.text
mainText.text = post.title
}
func textViewDidChange(_commentView: UITextView) {
commentPlaceHolder.isHidden = !newCommentLabel.text.isEmpty
}
}
For reference, here is my Post class code:
import Foundation
class Post {
var id:String
var title: String
var text:String
var createdAt:Date
var comment: [String] = []
init(id: String, title: String,text:String, timestamp:Double, comment: [String] = []) {
self.id = id
self.title = title
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
}
static func parse(_ key:String, data:[String:Any]) -> Post? {
if let title = data["text"] as? String,
let text = data["title"] as? String,
let timestamp = data["timestamp"] as? Double {
return Post(id: key, title: title, text: text, timestamp:timestamp, comment: [])
}
return nil
}
}
I suspect the issue may be with the delegate, which was declared as such in my NewPostViewController:
protocol NewPostVCDelegate {
func didUploadPost(withID id:String)
}
I have tried troubleshooting the storyboard, but everything seems to be in place. Is there an issue of the reuse of the protocol or perhaps the change of adding comments to the Post class itself? Maybe the issue is that I do not in fact want to upload a new post, but really I just want to add a comment to an existing post. If this is the case, how would I change the delegate or create a new one? I can provide more detail if needed. Thank you for your help.
This usually happens if you have an IBOutlet that was created previously with the same postReplyButton name. To check if your app has any other Outlet with the same name go to the Search section in your project and search for postReplyButton and see if you get multiple results for that name. If you do then click on the one which you don't need and delete it from the properties section.
If you have any Outlet which has a bad connection you will see something like this in the properties sections when you click on any one of the search result for postReplyButton
If that does not work then try renaming the Outlet entirely and see if that fixes the problem.
EDITED:
For your issue that you mentioned in the comments try this.
Instead of casting your newCommentLabel as an optional type of UITextViewDelegate just extend your viewController to conform to UITextViewDelegate. This should solve the issue.
class MainTextView: UIViewController, UITextViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self
}
}
Once you add UITextViewDelegate to your viewController you will no longer get the warning in viewDidLoad to cast newCommentLabel as an optional of type UITextViewDelegate.
I have created an app where users can generate posts that are added to a postTableView. Users can then click on any of the cells of postTableView to go to a unique view with the title and text of the post along with a commentTableView filled with user generated comments. Below the commentTableView is a textView that you can write your comment in and a button allowing you to submit your comment. I am trying to code my app so that when you press the button, the text that you wrote in the textView is appended to an array of unique comments for that post. Those comments populate the commentTableView. The following is my current flawed attempt:
Here is the Post Class:
import Foundation
class Post {
var id:String
var title: String
var text:String
var createdAt:Date
var comment: [String] = []
init(id: String, title: String,text:String, timestamp:Double, comment: [String] = []) {
self.id = id
self.title = title
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
}
static func parse(_ key:String, data:[String:Any]) -> Post? {
if let title = data["text"] as? String,
let text = data["title"] as? String,
let timestamp = data["timestamp"] as? Double {
return Post(id: key, title: title, text: text, timestamp:timestamp, comment: [])
}
return nil
}
}
Here is my current view controller that you get when you click on any of the cells from the postTableView:
import Foundation
import UIKit
import Firebase
class MainTextView: UIViewController {
#IBOutlet weak var titleText: UILabel!
#IBOutlet weak var mainText: UILabel!
#IBOutlet weak var commentPlaceHolder: UILabel!
#IBOutlet weak var newCommentLabel: UITextView!
var delegate:NewPostVCDelegate?
#IBAction func postReplyButton() {
// Firebase code here
let postRef = Database.database().reference().child("posts").childByAutoId()
let postObject = [
"comment": newCommentLabel.text,
"timestamp": [".sv": "timestamp"]
] as [String : Any]
postRef.setValue(postObject, withCompletionBlock: { error, ref in
if error == nil {
self.delegate!.didUploadPost(withID: ref.key!)
self.dismiss(animated: true, completion: nil)
} else {
// Handle error
}
})
newCommentLabel.text = String()
commentPlaceHolder.isHidden = false
}
var post: Post?
// MARK: - View Controller LifeCycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setMain()
}
override func viewDidLoad() {
super.viewDidLoad()
newCommentLabel.delegate = self as! UITextViewDelegate
}
private func setMain() {
guard let post = self.post else {
return
}
titleText.text = post.text
mainText.text = post.title
}
func textViewDidChange(_commentView: UITextView) {
commentPlaceHolder.isHidden = !newCommentLabel.text.isEmpty
}
}
How can I fix my errors and programmatically execute my vision of populating my comment section with user for each post?
For
Class 'MainTextView' has no initializers
Replace
var delegate:NewPostVCDelegate
with
var delegate:NewPostVCDelegate?
I am trying to implement MVVM Architecture pattern using Boxing. I have done it simply by Adding the Boxing Class:
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
func bind(listener: Listener?) {
self.listener = listener
}
func bindAndFire(listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init(_ v: T) {
value = v
}}
And then In the ViewController I have referenced a ViewModel, this is my View Controller:
class SignUpViewController: UIViewController {
// UI Outlets
#IBOutlet weak var emailLoginTextField: FloatLabelTextField!
#IBOutlet weak var passwordLoginTextField: FloatLabelTextField!
var viewModel = AuthenticationViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
}}
And This is my View Model:
class AuthenticationViewModel{
let defaults = UserDefaults.standard
let serviceManager = ServiceManager()
var user = User()
func signupUser(email : String?, password: String?){
let parameters : [String:String] = ["email":emailField, "password": password!, "system": "ios"]
serviceManager.initWithPOSTConnection(server: Utitlites.getServerName(), parameters: parameters, methodName: "/api/user/register", completion: { (responseData , errorMessage) -> Void in
let json = (responseData as AnyObject) as! JSON
print(json)
if ErrorHandling.handleErrorMessage(responseData: responseData).0 == true {
self.defaults.set("userId", forKey: json["user"]["id"].stringValue)
//self.userId.value = json["user"]["id"].stringValue
self.user = User(json: json)
}
})
}}
And this is my Model:
class User{
var id = Dynamic("")
var name = Dynamic("")
var email = Dynamic("")
init(){
}
init(json: JSON){
id.value = json["user"]["id"].stringValue
email.value = json["user"]["email"].stringValue
}}
My Question is:
MVVM Architecture wise, is it right to access the model using this line in the ViewController:
viewModel.user.email.bind{
self.emailLoginTextField.text = $0
}
Because I can see now that the View is accessing the Model which I think is not what MVVM Stands for. I need someone to clarify
The best practice to go about this (imo) and according to this raywanderlich video at 31:18 is to actually set the Model to be private, your VC doesn't need to know about it at all, only the ViewModel.
After that, set getters for the Model in the ViewModel like this:
var id: Dynamic<String> = Dynamic("")
var name: Dynamic<String> = Dynamic("")
var email: Dynamic<String> = Dynamic("")
And then, in your ViewModel also, set the User object to have a didSet notifier that will update the ViewModel's data accordingly:
private var user = User() {
didSet {
id = user.id
name = user.name
email = user.email
}
}
Now, you can access these properties only from the ViewModel instead of the Model directly:
viewModel.email.bind{
self.emailLoginTextField.text = $0
}
Oh, and don't forget to set the properties on the Model to be just regular strings ;)
I have a ViewController which saves user inputs to CoreData and after the save is attempted displaying MBProgressHUD to state if the save was successful or not.
I have an AddNewViewController class
class AddNewViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate, UITextFieldDelegate {
#IBOutlet weak var inputErrorMessage: UILabel!
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var amountLabel: UILabel!
#IBOutlet weak var dayPicker: UIPickerView!
#IBOutlet weak var durationPicker: UIPickerView!
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var amountTextField: UITextField!
#IBOutlet weak var notesTextField: UITextField!
//variable to contrain the origin view controller
var originVC: String?
// variables to hold user input
var name: String?
var amount: Double?
var notes: String?
var durationDay: Double?
var durationType: String?
// The days and duration options to display in the pickers
var durationPickerDataSource = ["Day(s)","Week(s)","Month(s)","Year(s)"];
var dayPickerDataSource = ["1","2","3","4","5","6","7","8","9","10","11","12"];
#IBAction func saveButton(sender: AnyObject) {
CoreDataStatic.data.saveIncomeBudgetAndExpenses(originVC!, name: name!, amount: amount, durationDay: durationDay!, durationType: durationType!, notes: notes!)
}
/**
The number of columns in the picker view.
*/
func numberOfComponentsInPickerView(dayPickerView: UIPickerView) -> Int {
return 1
}
/**
The number of items in the picker view. Equal to the number of days(12) and duration options(4) .
*/
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == durationPicker {
return durationPickerDataSource.count;
}
else {
return dayPickerDataSource.count;
}
}
/**
Gets the titles to use for each element of the picker view.
*/
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerView == durationPicker{
durationType = durationPickerDataSource[row]
return durationType
}
else {
durationDay = Double(dayPickerDataSource[row])
return dayPickerDataSource[row]
}
}
/**
Display acknowledgement if the Income, Budget or Fixed Expense saved.
*/
func displayMessage(origin: String) {
var message : String
//Changes the message depending on what the user was trying to save.
if CoreDataStatic.data.saved == true {
message = "\(origin) saved!"
}
else if CoreDataStatic.data.saved == false {
message = "Error: \(origin) failed to save!"
}
else {
message = "Error!"
}
print(message)
//displays acknowledgement for 2 seconds.
/*let acknowledgement = MBProgressHUD.showHUDAddedTo(self.view, animated: true)
acknowledgement.mode = MBProgressHUDMode.Text
acknowledgement.label.text = message
acknowledgement.hideAnimated(true, afterDelay: 2)*/
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.durationPicker.dataSource = self;
self.durationPicker.delegate = self;
self.dayPicker.dataSource = self;
self.dayPicker.delegate = self;
}
A CoreData class:
struct CoreDataStatic {
static let data = CoreData()
}
public class CoreData {
var appDel : AppDelegate
//Manage a collection of managed objects.
let context : NSManagedObjectContext
//Describes an entity in Core Data.
let incomeEntity : NSEntityDescription
let budgetEntity : NSEntityDescription
let fixedExpenseEntity : NSEntityDescription
//Retrieve data from Core Data with the entity 'Scores'.
let income = NSFetchRequest(entityName: "Income")
let budget = NSFetchRequest(entityName: "Budget")
let fixedExpense = NSFetchRequest(entityName: "FixedExpenses")
//Set the key that needs updating which is always 'score'
let nameKeyToUpdate = "name"
let amountDayKeyToUpdate = "amountDay"
let amountWeekKeyToUpdate = "amountWeek"
let amountMonthKeyToUpdate = "amountMonth"
let amountYearKeyToUpdate = "amountYear"
let durationDayKeyToUpdate = "durationDay"
let durationTypeKeyToUpdate = "durationType"
let notesKeyToUpdate = "notes"
var saved : Bool?
func saveIncomeBudgetAndExpenses(origin: String, name: String, amountDay: Double, amountWeek: Double, amountMonth: Double, amountYear: Double, durationDay: Double, durationType: String, notes: String) {
//saving in enity depending on origin view controller
let entity : NSEntityDescription
if origin == "Income" {
entity = NSEntityDescription.entityForName("Income", inManagedObjectContext: context)!
}
else if origin == "Budget" {
entity = NSEntityDescription.entityForName("Budget", inManagedObjectContext: context)!
}
else {
entity = NSEntityDescription.entityForName("FixedExpenses", inManagedObjectContext: context)!
}
let saveNew = NSManagedObject(entity: entity,
insertIntoManagedObjectContext:context)
// add user input to the relevant entity
saveNew.setValue(name, forKey: nameKeyToUpdate)
saveNew.setValue(amountDay, forKey: amountDayKeyToUpdate)
saveNew.setValue(amountWeek, forKey: amountWeekKeyToUpdate)
saveNew.setValue(amountMonth, forKey: amountMonthKeyToUpdate)
saveNew.setValue(amountYear, forKey: amountYearKeyToUpdate)
saveNew.setValue(durationDay, forKey: durationDayKeyToUpdate)
saveNew.setValue(durationType, forKey: durationTypeKeyToUpdate)
saveNew.setValue(notes, forKey: notesKeyToUpdate)
do {
try context.save()
print("saved")
saved = true
}
catch _ {
print("didnt save")
saved = false
}
AddNewViewController().displayMessage(origin)
}
init(){
appDel = (UIApplication.sharedApplication().delegate as! AppDelegate)
context = appDel.managedObjectContext
incomeEntity = NSEntityDescription.entityForName("Income", inManagedObjectContext: context)!
budgetEntity = NSEntityDescription.entityForName("Budget", inManagedObjectContext: context)!
fixedExpenseEntity = NSEntityDescription.entityForName("FixedExpenses", inManagedObjectContext: context)!
}
}
This code runs and as expected however when the commented out section in the displayMessage() function is uncommented I get the following error:
"fatal error: unexpectedly found nil while unwrapping an Optional value"
due to the line self.durationPicker.dataSource = self; in the override viewDidLoad()
Any help would be appreciated.
Note* if i call the displayMessage() within the saveButton function the code works so unsure why it isn't working when calling the message from the CoreData class.
I am unsure if this is the correct way about it but i found a fix.
a variable (bool) was created called attemptSave which is defaulted to false.
within the saveIncomeBudgetAndExpenses try and catch, the attemptSave is changed to true.
The displayMessage() function is now called within both button clicks using an if statement to check if an attemptSave is yes, if so, call function.