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)
}
Related
I'm writing some Unit Tests and I need to create a mock instance of GIDGoogleUser to make sure my API returns a correct instance of my model User class which is subset of the fields in GIDGoogleUser.
Since GIDGoogleUser does not expose an initializer, and all it's properties are read only, I can't create a mock instance and inject it in to my converter. Is there any way I can do this?
For simplicity, this is what I'm doing:
struct User {
let name: String
init(googleUser: GIDGoogleUser) {
name = googleUser.profile.name
}
}
I'm not sure what you mean when you say you can't mock GIDGoogleUser. Here is a mock of GIDGoogleUser I made just now:
First, declare the protocols that GIDGoogleUser and GIDProfileData will conform to, as well as our mocks we'll make in a bit:
protocol GoogleUserProtocol {
associatedtype Profile: ProfileDataProtocol
var profile: Profile! { get }
}
protocol ProfileDataProtocol {
var name: String! { get }
}
Then, have GIDGoogleUser and GIDProfileData conform to these protocols:
extension GIDGoogleUser: GoogleUserProtocol {}
extension GIDProfileData: ProfileDataProtocol {}
Then, create our mock classes (or structs as I opted for in this case), and have them conform to the above protocols:
struct MockGoogleUser: GoogleUserProtocol {
let profile: MockProfileData!
}
struct MockProfileData: ProfileDataProtocol {
let name: String!
}
Finally, adjust User's initializer to take not a GIDGoogleUser, but instead anything that conforms to GoogleUserProtocol:
struct User {
let name: String
init<G>(googleUser: G) where G: GoogleUserProtocol {
name = googleUser.profile.name
}
}
This will let you create mock Google User instances and inject them into your User, like so:
let mockProfileData = MockProfileData(name: "Mock User Name")
let mockGoogleUser = MockGoogleUser(profile: mockProfileData)
let mockUser = User(googleUser: mockGoogleUser)
print(mockUser.name) // prints "Mock User Name"
And you can of course still init your User with "real" Google User objects too:
let realGoogleUser: GIDGoogleUser = ... // get a GIDGoogleUser after signing in
let realUser = User(googleUser: realGoogleUser)
print(realUser.name) // prints whatever the real GIDGoogleUser's name is
I'm wondering if it's a good idea to include CRUD methods inside custom Swift classes, or are they better off in a separate class?
For example I have a class called User.swift:
class User {
var firstName: String
var lastName: String
var id: int
}
Now, would it be okay to include the get and create methods here? These methods will make API calls via Alamofire:
class User {
var firstName: String
var lastName: String
var id: int
static func add(user: User) -> User {
let parameters = ["firstName": user.FirstName , "lastName": user.LastName]
return sendRequest(.POST, url: "example.com/users", parameters: parameters)
}
static func getById(userId: Int) -> User {
return sendRequest(.GET, url: "example.com/users/\(userId)")
}
}
Should these methods be in a separate class, like in an ApiHelper class?
My application passes around the User object in arrays and dictionaries in several places, so wondering if it's good to keep it clean with just the properties.
I think better declare such methods in some ApiHelper/Router singletone class, as well as they must work async, work with some parse system (RestKit probably) and return fetched objects via closures with some delay
The different opinions about the right approach often causes a heated discussion. And this also extends to questions whether we should perform validation in the model class (User) or in the controller, how we should handle versions, serialisation, errors, undo/redo, locking, asynchronicity, etc., etc.
And the resulting code should still be clean, comprehensible, extensible, testable and put into a library so that we can reuse it in other projects!
IMHO, there's no right approach. IMHO, I would start with the following principles:
Your User class is seen in your solution as an Entity. An entity has a property ID and possibly other principal methods, for example, it exposes an init which takes a dictionary of attributes where the class/struct instance can be initialised.
That entity also knows about a "Persistent Store", so your User class may also conform to a protocol "Storable", which exposes class and instance methods like save, create, update, delete, query, etc.
This is only the tip of an iceberg what comprises a complete solution. You might look how others have solved this problem. For ideas, see
Use Core Data objects which you populate from the JSON
Implement the Active Record Pattern
Look at some Object Relational Mappers (there are a lot of implementations and libraries). Even though, you need to map from JSON to Objects, these gives some hints.
If you are still not satisfied, take a look at ASP.NET
Leveraging the "Active Record" approach you may start with something like this:
public final class User {
public init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
public var firstName: String
public var lastName: String
public internal(set) var id: Int
}
protocol ActiveRecord {
static func create(object: Self) throws -> Self
static func fetch(id: Int) throws -> Self
static func update(object: Self) throws -> Self
static func delete(id: Int) throws
}
extension User: ActiveRecord {
static func create(object: User) throws -> User {
...
}
static func fetch(id: Int) throws -> User {
...
}
static func update(object: User) throws -> User {
...
}
static func delete(id: Int) throws {
...
}
}
I am making an app for iOS in Xcode.
My question is:
How do I make the app show the signup/login view when it is needed, but not show it when the user is already logged in?
Is it communicating with the database every time the app launches?
I am planning on using MySQL for creating a database with simple users (username, score, friends).
Is there a tutorial that will show me the steps for doing this?
I have no experience with databases.
Help is appreciated.
I'm gonna give you a comprehensive answer.
Don't use NSUserDefaults and don't store password it's a bad solution
NSUserDefaults data is not encrypted, it may cause security issue.
Let's create a structured user class instead
When the user logged in, you will need to make sure you have access to user data throughout the app so you can get the data on any screen when you need it.
To achieve this, we need to make a great structure to organize this properly. Remember that current user and another users are both "user" so we will use the same class.
Create a class and name it "EDUser" (you can choose other name if you want).
This class will contain a user information (either current user or other user).
More than that, this class will have capability to log the user in.
Here's a picture of what the class might look like:
class EDUser {
var firstName: String
var lastName: String?
var birthDate: NSDate?
init(firstName: String, lastName: String?, birthDate: NSDate?) {
self.firstName = firstName
self.lastName = lastName
self.birthDate = birthDate
}
}
// MARK: - Accessor
extension EDUser {
class var currentUser: EDUser? {
get {
return loadCurrentUserFromDisk()
}
set {
saveCurrentUserToDiskWithUser(newValue)
}
}
}
// MARK: - Log in and out
extension EDUser {
class func loginWithUsername(username: String,
andPassword password: String,
callback: (EDUser?, NSError) -> Void) {
// Access the web API
var parameters = [
"username": username,
"password": password
]
YourNetworkingLibrary.request(.POST,
"https://api.yourwebsite.com/login",
parameters: parameters).responseJSON {
response in
if response.statusCode == .Success {
let user = EDUser(firstName: response["firstName"],
lastName: response["lastName"],
birthDate: NSDate.dateFromString(response["birthDate"]))
currentUser = user
callback(currentUser, nil)
} else {
callback(nil, yourError)
}
}
}
class func logout() {
deleteCurrentUserFromDisk()
}
}
// MARK: - Data
extension EDUser {
class private func saveCurrentUserToDiskWithUser(user: EDUser) {
// In this process, you encode the user to file and store it
}
class private func loadCurrentUserFromDisk() -> EDUser? {
// In this process, you get the file and decode that to EDUser object
// This function will return nil if the file is not exist
}
class private func deleteCurrentUserFromDisk() {
// This will delete the current user file from disk
}
}
// MARK: - Helper
extension NSDate {
class func dateFromString(string: String) -> NSDate {
// convert string into NSDate
}
}
Use Case
Now with everything in place, we can use it like this
Non-blocking logging in process
EDUser.loginWithUsername(username: "edward#domain.com",
password: "1234") {
user, error in
if error == nil {
// Login succeeded
} else {
// Login failed
}
}
Logging out
EDUser.logout()
Check whether the user is logged in
if EDUser.currentUser != nil {
// The user is logged in
} else {
// No user logged in
// Show the login screen here
}
Get current user data on any screen
if let currentUser = EDUser.currentUser {
// do something with current user data
}
Store other user as object
let user = EDUser(firstName: "Edward",
lastName: "Anthony",
birthDate: NSDate())
You need to look at a bunch of tutorials, I recommend these guys:
https://www.raywenderlich.com
They have free tutorials in ObjC and Swift as well as video courses!
A REST API normally respond with a 401 (Unauthorized code) if there isn't a valid/logged user, so every time I get a 401 I show a login within a modal view.
So when you load the app, call a GET currentUser endpoint, if it returns a 401 the app will show the login, if not, the app will show the default rootViewController.
I like this way because if for any reason, your session is no longer valid, the app will show the login view.
I was wondering how to accomplish an active record like (not with core data, but with rest, but this part I don't think will be an issue).
The issue its that don't understand well the reflection in swift, and my goal its to have a base class called Collection like (just prototype not really working code here):
public class Collection
{
public var Id: String
public static func Name () -> String
{
// accomplished but not with static method
}
public static func Find () -> [ChildClass] // the child class how can I obtain dynamically?
{
// restful things
return [ChildClass, ChildClass, ...] // rest result maped from json
}
public static func FindById (id : String) {}
// also Save && Delete methods
}
This kind of things I accomplish in c# with WSD-Data exactly in this class
So the usage maybe will:
public class User : Collection
{
public var FirstName: String
public var LastName: String
}
or c# like (dunno if this can be done in swift)
public class User : Collection<User> // we pass user as reference class c# like
{
public var FirstName: String
public var LastName: String
}
I hear also other alternatives to get this done, because don't know so well swift and the way that things are done with it.
I'm coding an app where a logged in user has got a couple of extra functionalities than a non logged in user. Basically, I have more or less 5 tabs. When I launch the app, the user immediately gets the login page. He can decide to skip it. If he skips it, there'll only be 3 tabs for him. If he logs in successfully, there'll be 5.
I already have my login page made. I just don't know how I can store a session if the user is logged in correctly, and only display a certain number of tabs if the user isn't. I come from PHP, I've just started learning Objective-C, so I'm looking for the same thing as $_SESSION in PHP, more or less.
So: if user logs in, store session, and show all the tabs. If he doesn't, only show a limited number of tabs.
How should I approach this?
In terms of storing the session, I assume username and password is enough.
You could store the username as you wish in NSUserDefaults or CoreData if you are using it. Storing a password is best using the keychain. SSKeychain makes it easy to do this.
[SSKeychain setPassword:password forService:myAppName account:userName]
You could store the fact they are logged in in-memory, but on app relaunch check by:
NSString *password = [SSKeychain passwordForService:myAppName account:userName];
if (password != nil)
{
// Logged in
}
If the user logs out, easy as deleting the password from the keychain by
[SSKeychain deletePasswordForService:myAppName account:userName]
Session handling is done automatically when you use NSURLConnection, so you can store the users data in a Sesssion on the server.
What you might be looking for is called a Singleton design pattern (some people reject it, but it can be very handy). What you do is create one object that is available everywhere in your code. In this object you for example store a BOOL that indicates whether the user has logged in or not. For example:
(I didn't run this, just to get the idea)
Mananger myManager* = [Manager sharedManager];
if(myManager.loggedIn){
//Show 5 tabs
}else{
//Show 3 Tabs
}
This code can be used in every class so you can always access your user's data. Manager would be a seperate class in this case that provides singleton functionality. Check out how to make one here: http://www.johnwordsworth.com/2010/04/iphone-code-snippet-the-singleton-pattern/
I'm gonna give you a comprehensive answer.
Don't use NSUserDefaults as session it's a bad solution
NSUserDefaults data is not encrypted, it may cause security issue.
Let's create a structured user class instead
When the user logged in, you will need to make sure you have access to user data throughout the app so you can get the data on any screen when you need it.
To achieve this, we need to make a great structure to organize this properly. Remember that current user and another users are both "user" so we will use the same class.
Create a class and name it "EDUser" (you can choose other name if you want).
This class will contain a user information (either current user or other user).
More than that, this class will have capability to log the user in.
Here's a picture of what the class might look like:
class EDUser {
var firstName: String
var lastName: String?
var birthDate: NSDate?
init(firstName: String, lastName: String?, birthDate: NSDate?) {
self.firstName = firstName
self.lastName = lastName
self.birthDate = birthDate
}
}
// MARK: - Accessor
extension EDUser {
class var currentUser: EDUser? {
get {
return loadCurrentUserFromDisk()
}
set {
saveCurrentUserToDiskWithUser(newValue)
}
}
}
// MARK: - Log in and out
extension EDUser {
class func loginWithUsername(username: String,
andPassword password: String,
callback: (EDUser?, NSError) -> Void) {
// Access the web API
var parameters = [
"username": username,
"password": password
]
YourNetworkingLibrary.request(.POST,
"https://api.yourwebsite.com/login",
parameters: parameters).responseJSON {
response in
if response.statusCode == .Success {
let user = EDUser(firstName: response["firstName"],
lastName: response["lastName"],
birthDate: NSDate.dateFromString(response["birthDate"]))
currentUser = user
callback(currentUser, nil)
} else {
callback(nil, yourError)
}
}
}
class func logout() {
deleteCurrentUserFromDisk()
}
}
// MARK: - Data
extension EDUser {
class private func saveCurrentUserToDiskWithUser(user: EDUser) {
// In this process, you encode the user to file and store it
}
class private func loadCurrentUserFromDisk() -> EDUser? {
// In this process, you get the file and decode that to EDUser object
// This function will return nil if the file is not exist
}
class private func deleteCurrentUserFromDisk() {
// This will delete the current user file from disk
}
}
// MARK: - Helper
extension NSDate {
class func dateFromString(string: String) -> NSDate {
// convert string into NSDate
}
}
Use Case
Now with everything in place, we can use it like this
Non-blocking logging in process
EDUser.loginWithUsername(username: "edward#domain.com",
password: "1234") {
user, error in
if error == nil {
// Login succeeded
} else {
// Login failed
}
}
Logging out
EDUser.logout()
Check whether the user is logged in
if EDUser.currentUser != nil {
// The user is logged in
} else {
// No user logged in
// Show the login screen here
}
Get current user data on any screen
if let currentUser = EDUser.currentUser {
// do something with current user data
}
Store other user as object
let user = EDUser(firstName: "Edward",
lastName: "Anthony",
birthDate: NSDate())