I've been learning iOS development for the past three weeks, I'm currently following a course on Udemy so far so good.
However I'm following one of the lectures whereby we build an Instagram Clone.
The instructor is using three arrays which are as follows:
var usernames = [""] // Stores all usernames
var userIds = [""] // Stores all Id's of the given usernames
var isFollowing = [false] // Stores where or not you're following that user
To me trying to keep track of what userId goes with what username using two arrays is basically an accident waiting to happen so I decided to set off and find a more feasible approach. I reverted back to my .Net days and decided to create a list so I went and created a class as follows:
class Users{
var Username : NSString = ""
var UserId : NSString = ""
var Following : Bool = false
}
Now inside my ViewController I make a call to Parse which returns me a list of users and I'm basically trying to loop through the response, and add them to the list class as shown here:
var t = [Users]() // After googling the web, this seems to be the syntax for a list declaration ?
let u = Users()
for object in users{
if let o = object as? PFUser {
u.Username = o.username!
u.UserId = o.objectId!
u.Following = o.IsFollowing!
self.t.append(u)
}
}
print(self.t)
Now when I print this to the console I see the following:
ParseStarterProject_Swift.Users
As I have one user at present, however when I try to loop through T and display the username in the console it doesn't display anything.
for x in t {
print(x.Username)
}
Your basic intuition is correct, it's better to have an array of custom objects, not multiple arrays.
Regarding making it more Swifty, consider your Users type. You might want something like:
struct User {
let username: String
let userId: String
let following: Bool
}
Note,
property names should start with lowercase letter;
Users should probably be called User, as it represents a single user;
we don't generally initialize values to default values like that, but rather specify them in the initializer;
we probably use String not NSString;
if a property cannot change, you'd use let, not var;
properties begin with lower case letters;
Then you can do something like:
var t = [User]()
for object in users {
if let o = object as? PFUser {
t.append(User(username: o.username!, userId: o.objectId!, following: o.IsFollowing!)
}
}
print(t)
Clearly, with all of those ! forced unwrapping operators, you'd want to be confident that those fields were populated for all of those properties.
Using struct is nice because (a) it's a value type; (b) you get the initializer for free; and (c) you can just print them. If you really wanted User to be a reference type (a class), you'd do something like:
class User {
let username: String
let userId: String
let following: Bool
init(username: String, userId: String, following: Bool) {
self.username = username
self.userId = userId
self.following = following
}
}
And if you wanted to be able to just print them, you'd define it to conform to CustomStringConvertible:
extension User: CustomStringConvertible {
var description: String { return "<User; username = \(username); userId = \(userId); following = \(following)>" }
}
With the class, you can feel free to change that description computed property to show it in whatever format you want, but it illustrates the idea.
You are correct in considering that keeping track of what userId goes with what username using two arrays is dangerous, you in the correct direction with your approach.
First, I would just like to suggest that you use correct naming convention:
Classes should be singular (except in very specific cases).
Variable/property names should begin with lowercase.
This would mean that your user class should look like this:
class User {
var username : NSString = ""
var userId : NSString = ""
var following : Bool = false
}
I will keep your existing naming use for the next part. The main problem with your code is that the variable "u" is a object which you create only once and then modify it. You should be creating a new "Users" object for each user instead of modifying the original. If you don't do this you will just have an array with the same user multiple times. This is how your code would look now:
var t = [Users]()
for object in users {
if let o = object as? PFUser {
let u = Users()
u.Username = o.username!
u.UserId = o.objectId!
u.Following = o.IsFollowing!
self.t.append(u)
}
}
print(self.t)
Next you mention that when you print to console you see the text: ParseStarterProject_Swift.Users, that is because Swift does not automatically print a pretty text with the content of your object. In order for it to print something more detailed, your "Users" object would need to implement the CustomStringConvertible. You can see a more detailed answer about that here: how-can-i-change-the-textual-representation-displayed-for-a-type-in-swif.
Lastly, you mention that when you loop trough "t" and display the username in the console it does not display anything. This is caused by one of two things:
Because there are no users being returned from parse, so the "t" array is actually empty. Try print(t.count) to see how many objects are in the array.
Because your "Users" object declares an empty string "" as the default username and the username is not being set correctly when getting the data from the parse. Which means that it IS actually printing something, just that it is an empty string. Try defining a different default value like var username : NSString = "Undefined" to see if it prints something.
Good luck learning swift!
Related
I try to create a button which should bring the normal user to the chatController with the creator of a post
There are a number of issues here, and really too many to address in a single answer but let me try to point you in the right direction.
Lets start with this
let user = job?.addedByUser
self.messagesController?.showChatControllerForUser(user)
so the user var is assigned from the Job Class
class Job {
var addedByUser: String!
Notice that addedByUser property is a string, therefor the userVar is a string.
However, you're passing it to a function which requires a user class, not a string.
func showChatControllerForUserCreator(_ user: User) {
Which then throws an error
Cannot convert value of type 'String?' to expected argument type
'User'
The way to fix it is to decide if a user is a String or something else. It would most likely be a UserClass that you create
class UserClass {
var uid = ""
var user_name = ""
}
at the same time you need to have your users stored in a users node
users
uid_0
user_name = "Henry"
uid_1
user_name = "Bud"
and in your Jobs class, store a reference to the user with it's uid.
This could get rather lengthy but the next step is, after you loaded your job, to then load the user from it's uid. The JobClass would be
class Job {
var addedByUid: String! //this will be the users Firebase uid
etc etc
class User {
var uid : String
var profileImageURL : String
init(uid : String, profileImageURL : String) {
self.uid = uid
self.profileImageURL = profileImageURL
}
}
If my project were to start out with a User model such as the one above, and I have a huge-scaled application where this User is being initialized in over 20 files, if I were to go in and add a new required property such as age, I would have to fix my initializers for every single file. Worse off, I would have to go in after each initializer and set the new property on its own line.
If I were required to add in 25 new properties over the course of production, this would be a nightmare.
What is the best way to handle large models like this that might change in the future?
I would not initialise an object in 20+ location. That makes the code fragile. Put an extra layer like user management where you can ask the current user / ask for any user. And make the init there, in one single place.
You do something like this
class User {
var uid : String
var profileImageURL : String
init(all : [String:Any]) {
self.uid = all["uid"] as? String ?? ""
self.profileImageURL = all["profileImageURL"] as? String ?? ""
}
}
or write a Codable class to decode the dictionary directly
I'm experiencing trouble understanding how updating objects works in Realm. I'd appreciate help in helping me to understand how updating nested objects work and why it doesn't work the way I expect it.
I started using Realm just recently, and here's what I want to use it for: I have a set of key value pairs stored on my server, that serve as localized values for strings used in my iOS app. On app launch every now and then I want to update my strings, so I pull them from the server and store them locally in realm on my iOS device. I want to have only ONE instance of those strings on my device.
Here are the classes:
import RealmSwift
public class LocalizedStrings: Object {
dynamic var id = 1
dynamic var version: String = ""
let assets = List<LocalizedString>()
override public static func primaryKey() -> String? {
return "id"
}
}
public class LocalizedString: Object {
dynamic var key: String = ""
dynamic var value: String = ""
}
Here's how I update the LocalizedStrings object:
realm.add(localizedStrings, update: true)
Here's how I access my strings:
func getLocalizedString(forKey key: String) -> String {
var result = key
try! realm.write {
let queryResult = realm.objects(LocalizedString.self).filter("key == %#", key)
// print(queryResult)
if queryResult.count == 1 {
result = queryResult[0].value(forKey: "value") as! String
}
}
return result
}
Now, I would expect, that whenever I update my LocalizedStrings, that the localizedStrings.assets list would get updated with new values. But instead, the assets are not updated, the list reference gets updated and I end up having multiple instances of the same string, which is not what I would expect from an update function. When I try to access a particular LocalizedString, it turns out there's multiple instances:
(...)
[19] LocalizedString {
key = update;
value = Update;
},
[20] LocalizedString {
key = update;
value = Update;
}
Perhaps I'm missing something obvious and I would really appreciate if someone could point me in the right direction, so I'd be able to achieve the behavior I'm looking for (which would be having the nested object actually updated, rather than having unnecessary duplicates of my objects).
Thanks!
Ok, so this answer helped me figure out, what was wrong with my setup. I was missing primaryKey in LocalizedString class.
From the answer above on how realm.add(object, update: true) works:
Documentation :
parameter object: The object to be added to this Realm.
parameter update: If true, the Realm will try to find an existing copy of the object (with the same primary
key), and update it. Otherwise, the object will be added.
So the same thing happens with nested objects. They can not be updated unless they have primaryKey.
I'm having problems with an object that returns me in Realm, the strange thing is that if I printo console the object if I start it well but however if I try to access its value it tells me that it is empty.
The structure of the object is as follows:
class Favourite : Object {
var character : Character!
}
I create an object and add it to the DB
let fav = Favourite()
fav.character = character
FavouriteDao.sharedInstance.addFavourite(characterFavourite: fav)
Get all objects of favorite type
func getAllFavourites() -> Results {
return realm.objects(Favourite.self)
}
When I get the item and do a print
Favourite {
character = Character {
name = Spider-Man;
descriptionC = Bitten by a radioactive spider, high school student Peter Parker gained the speed, strength and powers of a spider. Adopting the name Spider-Man, Peter hoped to start a career using his new abilities. Taught that with great power comes great responsibility, Spidey has vowed to use his powers to help people.;
thumbnail = Thumbnail {
id = 815D93D0-C116-4267-978C-9E47C0074D0D;
path = http://i.annihil.us/u/prod/marvel/i/mg/3/50/526548a343e4b;
extensionImage = jpg;
};
};
If I try to access the character element it tells me that it is nil
Somebody manages to understand because if I make a print of the favorite object it shows me that there is inside a character object but nevertheless if I try to accede to it it says that it does not exist?
What you do is totally wrong from the very beginning. You should read the realm docs first. https://realm.io/docs/swift/latest/#getting-started
For example.
class Favourite : Object {
var character : Character!
}
is not something you should do in Realm.
Assuming your Character is well-defined, the code should be dynamic var character : Character? = nil at least.
I've got a number of user properties in a user viewcontroller class ie
//user vars
var email: String?
var givenName: String?
var familyName:String?
var phone: String?
var dob: NSDate?
In a method within that class i retrieve user data from coredata and set the user text fields with that data in a loop
for i in 0 ..< userTextFields.count {
let field = userTextFields[i]
let fieldName = userTextFieldKeyNames[i]
let fieldText = currentUser.valueForKey(fieldName) as? String
field.text = fieldText
}
the fieldName variable in the loop matches the class's ivars above. Is there a way i can reference the ivars within the loop by matching it with the fieldName string so I can set the values of the ivars with the fetched coredata values ie in a literal sense saying something like the following ...
if self.property.name.text == fieldName {
self.property.value == fieldText
}
ie somehow resolving the various property names withing the class ... or is this bad design? .... if so any suggestions on achieving the same result in a better way
Not Swift-ish, as it's bypassing compile time type checking. Best option is to keep them all in a dictionary, and use a protocol to define allowed data types in the dictionary, but even that is rather poor