MultipeerConnectivity MCPeerID does not conform to Codable - ios

I'm writing a simple P2P chat program and thus far I've been storing MCPeerID's displayName property as a string in my model to determine who to send a particular message to. On each send operation, I search through the connectedPeers array and copy the MCPeerID into a receiver list whenever the displayName matches the string I have in my model.
This can be problematic in the case where two peers have the same name. And I'm also not satisfied with having perform a search for each send. So I'm trying to use MCPeerIDs directly in my model. However, Xcode complains that MCPeerID does not conform to Encodable nor Decodable, and I'm not sure how to fix this.
My model represents a topic that maintains a list of participants as well as a log of who said what. Thus, I can sync a new participant when they join and update existing participants when new messages are added. My model looks something like the following:
import Foundation
import MultipeerConnectivity
class Task : Codable {
var uuidStr: String = UUID().uuidString
var topic : String
var history : [String] = []
var users : [MCPeerID] = []
...
private enum CodingKeys: String, CodingKey {
case uuidStr
case topic
case history
case users
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(uuidStr, forKey: .uuidStr)
try container.encode(topic, forKey: .topic)
try container.encode(history, forKey: .history)
try container.encode(users, forKey: .users)
}
...
}
(I haven't shown the standard init() as this is not where the problem lies.)
After looking at the documentation, I see a method with the signature MCPeerID.init?(coder: NSCoder) and func encode(with: NSCoder) but I'm not sure how to specify the NSCoder parameter. Any help on how to serialise the users property would be much appreciated.

There seem to be a few issues here (p2p is a complex subject):
1. identifying users (business level),
The peer name MCPeerID(displayName: ) is not the peer identifier, it's just aimed at the human viewer (e.g. chatroom users). Point 3) below shows a way how a peer can include other user info when advertising their presence.
2. identifying peers (network level),
The framework (MC) creates a unique peer id for you and recommends the app re-use (ie. persist & reload) across app launches (MCPeerID - official docs - example objetive-c using NSUserDefaults storage api).
3. sharing peer session info (session level)
In your model, you're serialising the Task, fine (though no mention of what format, .xml, .plist, .json etc), but I don't see why you're wanting to serialise these MCPeerIDs?
Each peer on connecting is supposed to re-use their own peerID, and, optionally, if they wish to advertise additional info to potential peers they could provide a dictionary with info about the business-user behind the peer object e.g:
myAdvertiser = MCNearbyServiceAdvertiser(peer: reusableLocalPeerID, discoveryInfo: ["uuidStr" : "my-user-id-here"], serviceType: "my-tasks-app")
(MCNearbyServiceAdvertiser docs).
4. separating concerns (data level)
I would separate the framework (MC) from business model (Task, User ...) , to something like this:
class Task: Codable { var users: [User] ... }
class User: Codable { uid:String ...}
struct PeerRecord: { var peer:MCPeerID, var user_id:String, var name:String ... }
var foundPeers:[MCPeerID]
var connectedPeers:[String:PeerRecord] // e.g. ["a-user-id": PeerRecord(peer: ...)]
5. serialising
Apple example at point 2) above shows how to serialise MCPeerID using UserDefaults api.
Generally, when one extends Codable, one also expects to use an encoder (JSON, XML etc) somewhere in the app,
passing to this encoder's .encode(...) method the object in question e.g:
func storeTasks(tasksCodable:[Task]) -> Bool {
if let encoded_in_data_format = try? JSONEncoder().encode(tasksCodable) {
UserDefaults.standard.set(encoded_in_data_format, forKey: "tasks")
return true
}
return false
}

Related

Access modification Apple Combine

I'm trying to think in terms of API Design because ultimately I want to ship code to myself or others.
Let's make some assumptions in order to get a succinct scenario. We will assume I have some Code that authenticates with my server and returns a user object. Defined simply like this:
public struct User: Codable {
public let id: UUID
public let email: String
public let name: String
}
I'm writing this code as an SDK I would ship to myself or a third party where all the guts of AUTH are handled. Using Apples new Combine framework I might expose a publisher for the consumer of my SDK like this.
public enum CurrentUserError: Error {
case loggedOut
case sessionExpired
}
public struct MyAuthFrameworkPublishers {
static let currentUser: CurrentValueSubject<User?, CurrentUserError> = CurrentValueSubject<User?, CurrentUserError>(nil)
}
Now, my private auth could accomplish it's task and retrieve a user then publish that to anything outside the SDK that it listening like so:
class AuthController {
func authenticate() {
///Get authenticated user.
let user = User.init(id: UUID(), email: "some#some.com", name: "some")
MyAuthFrameworkPublishers.currentUser.send(user)
}
}
let authController = AuthController.init()
authController.authenticate()
Is there a way to keep or stop the user of this SDK from sending it's own User object to the publisher? Like a private or access controller .send() function in combine?
Do you really want to use CurrentUserError as your Failure type? Sending a failure ends any subscriptions to the subject and fails new ones immediately. If the session expires, don't you want to let the user log in again? That would require publishing another User? output, which you cannot do if you have published a failure.
Instead, use Result<User?, CurrentUserError> as your Output type, and Never as your Failure type.
Now, on to your question. In general, the way to prevent the user from calling send is to vend an AnyPublisher instead of a Subject:
public class AuthController {
public let currentUser: AnyPublisher<Result<User?, AuthenticationError>, Never>
public func authenticate() {
let user: User = ...
subject.send(user)
}
public init() {
currentUser = subject.eraseToAnyPublisher()
}
private let subject = CurrentValueSubject<Result<User?, AuthenticationError>, Never>(nil)
}

Issue in making a struct Codable

I have Application protocol with 2 variables. And I have component struct that has a variable, which confirms to Application protocol. I need to save this struct in disk . So I'm confirming it to Codable protocol. While doing so I'm getting an error like this ,
"Protocol type 'Application' cannot conform to 'Decodable' because only concrete types can conform to protocols"
Here is my code,
public protocol Application {
var name : String {get}
var ownerName : String {get}
}
public struct component : Codable {
let application : Application
private enum CodingKeys: String, CodingKey {
case application
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
application = try values.decode(Application.self, forKey: .application)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(application, forKey: .application)
}
}
I'm new to swift so sorry if I'm missing something very obvious. Im not able to fix this and I need some help in right direction. Thank you in advance.
How you address this strongly depends on the problem you're solving.
If you want to store and load exactly these two keys in JSON, then Application should be a struct (as jawadAli notes).
If you mean to store more information, but a given Component is tied to exactly one type of Application, then you want Component to be generic:
public struct Component<App: Application & Codable> : Codable {
let application : App
...
}
Note the addition of & Codable. If all things that conform to Application should be Codable, then you can make that a requirement of Application:
public protocol Application: Codable {
var name : String {get}
var ownerName : String {get}
}
It is important to understand that this does not make Application conform to Codable. It means that in order to conform to Application, a type must also conform to Codable. It is never possible to decode an abstract type (i.e. a protocol).
If you mean to store more information, but a given Component doesn't actually know what kind of Application it holds, then this is a more complicated problem (and often over-complicated and should be rethought; if you find you're using a lot of as? tests, then you should almost certainly redesign). If that's your problem, you should explain a bit more what problem you're solving, and we can discuss how to solve it. (It generally requires some kind of dynamic type registration system, and a JSON format that supports metadata about types. Or you can switch to NSCoder and not use JSON.)
Use Struct that is confirming to codable instead of protocol will solve the issue
public struct Application : Codable {
var name : String
var ownerName : String
}

Storing collection of custom objects where each I stored in keychain

I have a password managing app. Currently, I store all of the user's accounts (i.e. Netflix, Spotify, etc) in an array that is then stored in User Defaults. Now, I want to make things more secure by storing each account in iOS keychain. I realized that even if I store each account in keychain, I will still need to keep track of them all in a data structure (for populating tableviews, etc) and that data structure will need to be stored somewhere. I am struggling to understand the best way to go about implementing this.
My current design approach is to have:
• An array of custom Account objects stored in User Defaults
• each Account in the array is stored in keychain
I am using Locksmith for working with keychain (Note: I'm not married to this framework).
Locksmith requires me to implement several protocols in my Account class. This is then confusing me when it comes to trying to encode Account objects into the master array being stored in User Defaults. I am trying to make Account conform to Codable and am royally confused about what is going/needs to go on on this point.
class Account: ReadableSecureStorable,
CreateableSecureStorable,
DeleteableSecureStorable,
GenericPasswordSecureStorable,
Codable {
var username: String // can i make these let's
var password: String
// Required by GenericPasswordSecureStorable
let service: String
var account: String { return username }
// Required by CreateableSecureStorable
var data: [String: Any] {
return ["password": password]
}
init(service: String, username: String, password: String) {
self.username = username
self.password = password
self.service = service
}
private enum CodingKeys: String, CodingKey {
case username
case password = "secret"
case service
}
}
struct AccountDefaults {
static private let accountsKey = "accountsKey"
static var accounts: [Account] = {
guard let data = UserDefaults.standard.data(forKey: accountsKey) else { return [] }
return try! JSONDecoder().decode([Account].self, from: data)
}() {
didSet {
guard let data = try? JSONEncoder().encode(accounts) else { return }
UserDefaults.standard.set(data, forKey: accountsKey)
}
}
}
I was getting errors stating that Account does not conform to decodable before adding the codingKeys enum.
From here on, I can't figure out where to go.
When I add an account, I can store it with Locksmith. Will the default codingKeys password override the real password I'm trying to save into keychain?
I know I'm not supposed to include multiple questions here, but the thoughts going through my head are:
What do I need to encode/decode?
How does using Locksmith affect what I need to encode/decode?
Will the password need to be encoded?
Codable is just an alias for Decodable & Encodable
where Decodable and Encodable are protocols.
So,
What do I need to encode/decode?
If you mean JSON,
For converting into JSON use default implementations. For simple structs just allow this protocol conformance.
How does using Locksmith affect what I need to encode/decode? Regarding this – no effect.
Will the password need to be encoded?
your password is a string. Obviously, we be easily encoded into JSON.
If you mean the password security, the Keychain with 'only unlocked' level is good.

Save User model in UserDefaults and append stored Users to array

I have a User model declared like this:
struct User: Codable {
let nickname : String
let fullName: String
let profilePicUrl: URL
}
Then I save the followed user like this
let followedUser = User(nickname: username, fullName: fullName, profilePicUrl: url)
UserDefaults.standard.set(try! PropertyListEncoder().encode(followedUser), forKey: "user")
Next, in the ViewController which should display the followed users count I retrieve the UserDefaults data like this
var followedUsers = [User]()
if let storedObject: Data = UserDefaults.standard.object(forKey: "user") as? Data {
do {
let storedUser: User = try PropertyListDecoder().decode(User.self, from: storedObject)
followedUsers.append(storedUser)
} catch {
print(error)
}
}
Now, my followedUsers array is being updated with the last stored user each time. How can I append my followedUsers array in a proper way?
If you use Codable , then it make sense to use JSONEncoder & JSONDecoder and store the users as an array
do {
// How to save multiple records
var followers = [User]() // Your array of users
let data = try JSONEncoder().encode(followers) //Convert to Data
UserDefaults.standard.set(data, forKey: "data") // Save to User Default
// How to fetch multiple records
// Check If data available in user default
if let da = UserDefaults.standard.data(forKey: "data") {
// Convert Data back to your cod-able struct
let stored = try JSONDecoder().decode([User].self, from: da)
}
}
catch {
print(error)
}
struct User: Codable {
let nickname , fullName: String
let profilePicUrl: URL
}
Don't encourage using userDefault , you can go with CoreData / Realm
As #Sateesh and #Sanjukta said, storing the users in an array and saving that to UserDefaults is one solution.
The reason why your followedUsers array is updated with only one user, is that when you create a new user, you update the same key in UserDefaults each time.
It should be noted that UserDefaults is mainly used to store small amounts of data. Depending on how many User objects you plan to store, UserDefaults might not be the best solution.
The two solutions for data storage that come to mind are Core Data and Realm. Core Data is an Apple framework with which you can storage data on device. You can read more about it here: Core Data
The second option is to use Realm. Realm is is an open source database management system and can be used for on-device data storage too.
If you are interested in the differences between Realm and Core Data, for starters I can recommend this article.
Personally I use Realm in my own projects because I find it easier to implement and faster than Core Data. Please note that this is just my personal preference and others might disagree. Make sure you listen to other opinions and experiment with both before picking sides.
So let's suppose you want to store the User in Realm, you would need to do the following:
Install Realm for your project.
In your podfile add the following: pod 'RealmSwift'.
Run pod install in terminal and use the newly created .xcworkspace from now on.
In Xcode, in AppDelegate import RealmSwift and insert the following code to the didFinishLaunchingWithOptions method:
do {
_ = try Realm()
} catch {
print("Error initialising new Realm \(error)")
}
This initializes a new default Realm, where your User data will be saved.
Create a new User.swift file. In that, import RealmSwift and insert the following:
class User: Object {
#objc dynamic var nickName: String = ""
#objc dynamic var fullName: String = ""
#objc dynamic var profilePicURL: URL?
}
This creates a Realm object, which will contain the data for your users.
Save the followed user like this:
In the view controller where you want to save the user data import RealmSwift, and under the class declaration create a new instance of realm by let realm = try! Realm()
When you have the user data, save to Realm with the following code:
:
let followedUser = User()
do{
try realm.write {
followedUser.nickName = username
followedUser.fullName = fullName
followedUser.profilePicURL = url
realm.add(followedUser)
}
} catch {
print("Error saving to persistent container! \(error)")
}
In the view controller where you need the user data, create an instance of Realm just like before with let realm = try! Realm()
, retrieve users from Realm with the following code: let followedUsers = realm.objects(User.self)
This retrieves all Users from the default realm.
If you need to count the followedUsers you can do so by: followedUsers.count
Hopes this approach helps you to achieve what you wanted in the first place.
Please make the array of object then store the array in userDefaults.
After that you can easily retrive it.
It may help you. Thank you.

Swift: CoreData error and how to set up entities and DataModels?

So far I can create my own Data Models in a swift file. Something like:
User.swift:
class User {
var name: String
var age: Int
init?(name: String, age: Int) {
self.name = name
self.age = age
}
}
When I create a Core Data model, ie. a UserData entity, (1) do I have to add the same number of attributes as in my own data model, so in this case two - the name and age?
Or (2) can it has just one attribute eg. name (and not age)?
My core data model:
UserData
name
age
The second problem I have is that when I start the fetch request I get a strange error in Xcode. This is how I start the fetchRequest (AppDelegate is set up like it is suggested in the documentation):
var users = [User]()
var managedObjectContext: NSManagedObjectContext!
...
func loadUserData() {
let dataRequest: NSFetchRequest<UserData> = UserData.fetchRequest()
do {
users = try managedObjectContext.fetch(dataRequest)
....
} catch {
// do something here
}
}
The error I get is "Cannot assign values of type '[UserData] to type [User].
What does this error mean? In the official documentation are some of the errors described, but not this particularly one.
If you are designing a user model in core data you don't have to write the class yourself. In fact by default Xcode will generate subclasses of NSManagedObject automatically once you create them in your project's, but you can also manually generate them if you would like to add additional functionality:
Then you can go to Editor and manually generate the classes
Doing this will give you User+CoreDataClass.swift and User+CoreDataProperties.swift. I see in your question you are asking about how the core data model compares to your 'own' model, but if you're using core data then that IS the model. The generated User class, which subclasses NSManagedObject, is all you need.
Then you might fetch the users like this:
let userFetch = NSFetchRequest(entityName: "User")
do {
users = try managedObjectContext.executeFetchRequest(userFetch) as! [User]
} catch {
fatalError("Failed to fetch users: \(error)")
}
You cannot use custom (simple) classes as Core Data model.
Classes used as Core Data model must be a subclass of NSManagedObject.
If your entity is named UserData you have to declare the array
var users = [UserData]()
The User class is useless.

Resources