A Feathers API is really easy to talk to. A native iOS client would be awesome and @corymsmith has done some prototyping here. It should behave similar to the Feathers client and feel familiar to people coming from the JS client. The things it should support:
authenticate method that authenticates against the server and stores the JWT somewherelogout method that destroys the stored JWTbefore and after hook chains to mutate data or query paramsfeathers-client is a consolidation module for all the modules above. This is so that we can create a standalone bundle for people without JS module loaders. I don't think we need to do this for iOS. These client modules should all be bundled inside the one iOS client.
We have a repository with some initial code set up for this if anyone wants to tackle it.
:+1:
:100:% would want this/be down to help out in some way
Maybe @corymsmith can post up what he's got and then we can decide where to go from there @startupthekid. Would love some help!
I just added an initial implementation with REST support for 'find' and 'get', will be easy to add 'update' 'remove', etc. I rewrote this to use NSURLSession vs using external dependencies like AlamoFire, etc https://github.com/feathersjs/feathers-ios
I have some code for Primus / Socket.io already but needs to be migrated into this codebase
This is sweet! Pretty stoked.
Yeah this does look pretty awesome. I've been building something analogous in an app of mine and I have a few things that would be good to consider when building this client out.
1) Swift vs Objective-C.
Objective-C is definitely more dynamic and there's a lot of things you can do with it in terms of dynamic dispatch, method swizzling, etc that you can't in Swift. However what you do gain in Swift is type safety.
Consider the following feathers protocol method:
- (void)find:(NSDictionary *)params
completion:(FeathersFindCompletionBlock)completionHandler;
As a primarily iOS developer, what I would be most concerned with is what type of model this returns. The completion block returns an array but without type specification, I don't know whether it returns JSON or an array of models or both even.
Consider the same method but in Swift
func find<T: FeathersModel>(parameters: [String: AnyObject], completionHandler handler: ([T], NSError?) -> Void)
Now as the consumer of the function I can directly specify what model should be returned in the completion handler without encoding that knowledge into the service.
The FeathersModel protocol could look something like:
protocol FeathersModel {
init?(dictionary: [String: AnyObject])
static func JSONKeysByPropertyKey() -> [String: String]
}
^This is pretty similar to a Swift implementation of Mantle which is surprisingly difficult to emulate (which I discovered while making Blueprint). Instead of building an object mapping framework yourselves, something like ObjectMapper, which has Alamofire extensions, would be a great choice.
Then the service protocol methods can look something like:
func find<T: Mappable>(parameters: [String, AnyObject], completionHandler handler: ([T], NSError?) -> Void
where the implementation would be done using Alamofire.
Alamorfire.Manager.request(.GET, "\(self.baseURL)\(self.queryString(parameters))").responseArray { (response: Response<[T], NSError>) in
if case let .Failure(error) = response.result {
handler([], error)
} else if case let .Success(value) = response.result {
handler(value, nil)
}
}
Alamofire.Manager is based on NSURLSession so it wouldn't be that different than using NSURLSession manually.
2) Functional vs callbacks
I'm a big fan of ReactiveCocoa and modeling events (especially network events) as Signals and SignalProducers. I don't know if reactive versions of the methods protocol should be the defacto way of doing it or a submodule (pod Feathers/ReactiveCocoa) that the end user can use if they so choose.
I've written a few reactive extensions for ObjectMapper's extensions on Alamofire that look like this:
func rac_Object<T: Mappable>(queue: dispatch_queue_t? = nil, keyPath: String? = nil) -> SignalProducer<T, NSError> {
return SignalProducer { observer, disposable in
self.responseObject(queue, keyPath: keyPath) { (response: Response<T, NSError>) in
if case let .Failure(error) = response.result {
observer.sendFailed(error)
} else if case let .Success(value) = response.result {
observer.sendNext(value)
observer.sendCompleted()
}
}
}
}
func rac_ObjectArray<T: Mappable>(queue: dispatch_queue_t? = nil, keyPath: String? = nil) -> SignalProducer<[T], NSError> {
return SignalProducer { observer, disposable in
self.responseArray(queue, keyPath: keyPath) { (response: Response<[T], NSError>) in
if case let .Failure(error) = response.result {
observer.sendFailed(error)
} else if case let .Success(value) = response.result {
observer.sendNext(value)
observer.sendCompleted()
}
}
}
}
Using this pattern makes it really easy to something like this:
if let service = FeathersService.services["users"] {
let user: User = service.get("someID").then(self.validateSession())
}
instead of
service.get("someID") { result, error in
if error != nil {
print("ERROR ABANDON SHIP")
} else {
self.verifySession()
}
}
3) Network fetching vs other sources
One of the things I like most about feathers on my backends, particularly feathers-mongoose, is that I do a find on the users service and the correct models are automatically fetched, same with create, etc.
To that end, what I believe would make the ios client super powerful and ultimately lead to people using it is the ability to not just fetch from a Node.js service but from a local persistence framework, whether that's CoreData or Realm or some 3rd option.
Again this is where Swift can really help out since protocol oriented programming is incredibly powerful. The goal I would think is to keep the design as @corymsmith made it, with one Feathers object where the services are registered and one FeathersService class that implements the service protocol.
enum ServiceType<T> {
case Remote(NSURL, String)
case Realm(T.Type)
case CoreData(entityName: String)
}
protocol ServiceProtocol {
typealias Model
var type: ServiceType<Model> { get set }
init(type: ServiceType<Model>)
}
And then for each type of support (Realm, CoreData), just add an extension:
extension ServiceProtocol where T: Object { } //Realm
I haven't quite figured out a protocol design for specifying what model class is associated with the service and if that should be done on a function by function basis with generics or if ServiceProtocol should be a generic type constraint.
Thinking about it some more, the associated enum would look something more like:
enum ServiceType<Model> {
case Realm(modelClass: Model.Type)
case Remote(modelClass: Model.Type, baseURL: String, servicePath: String)
case CoreData(modelClass: Model.Type, entityName: String)
}
@startupthekid Completely agree, the Android client that I've started uses Generics and Google's GSON library to handle the mapping to and from your models as well. I like the thought of different sources as I often have to write all of the custom local persistence code once I've fetched the data from an API anyways, would be nice to abstract that into a feathers service as you've suggested.
@corymsmith there's definitely a bunch of different ways to go about this. When I was building it out for one of my apps, I did run into some problems with Swift generics and storing services.
For example I have my service defined as:
enum Service<Model> {
case Realm(Model.Type)
case Network(Model.Type, NSURL, String)
}
but in my service manager (same as Feathers.h), the dictionary that stores the services would have to be of type [String: Service<T>] where all the Ts would have to be same (can't store both Service<User> and Service<Post> for example.
My workaround was just to define a BaseModel and have the storage type be [String: Service<BaseModel>] which isn't great but _shrug_.
Another issue I found was how exactly to go about implementing the different protocol methods for the different enum cases. My original thought was to use extensions:
extension Service where Self == .Realm {
}
but I quickly found out that you can only constrain to classes and protocols (and there's also no variadic generics in Swift like in C++ :-1:). Worse case it could just be a switch:
extension Service {
func find(parameters: [String: AnyObject]? = nil) -> SignalProducer<Model, NSError> {
switch self {
case let .Realm(model): //do something
case let .Network(model, URI, servicePath): //do something else
}
}
}
@corymsmith @daffl just for reference, what I ended up doing in my app is making a protocol:
protocol ServiceType {
typealias Model
func get(id: String) -> SignalProducer<Model, NSError>
}
and then for each the type of resource fetching (Networking, Realm, etc), I created a one concrete struct implementation so I ended up overall with two structs, RealmService, and NetworkService, both which implement ServiceProtocol.
struct RealmService<T: Object>: ServiceType {
func get(id: String) -> SignalProducer<T, NSError> {
return SignalProducer { observer, disposable in
Persistence.sharedStack.fetch(T).filter(NSPredicate {
if let object = $0.0 as? BaseModel where object.id == id {
return true
}
return false
})
}
}
}
I did it this way because each separate method of getting data may need to put constraints on the Model associated type.
Then I can just do:
let userRealmService = RealmService<User.self>()
userRealmService.get("someID")
@startupthekid Sorry we kind of dropped the ball on this. @corymsmith and I have been really busy and have been more focused on React Native as of late, so I finally had some time to adequately review all your comments. I'm not an iOS dev so I had to do a bunch of reading in order to grok what you were talking about. I've updated the description with high level requirements for what we are probably looking to implement.
@corymsmith and I chatted a bit about this yesterday and it might make sense to write this in Swift and then have some bridging headers where required, however because Swift doesn't support variadic generics, it might make it a bit tricky. So now that I said that, maybe Obj-C with bridging headers to Swift. Definitely some work to be done here you two would be best to make that call.
I think you are bang on in wanting to do something similar to ReactiveCocoa. We are planning on making the JS client optionally reactive as well. It provides a really clean interface.
As far as Realm support goes, ideally it should be like a plugin. I'm inclined to make the default storage of stuff like, say an auth token or your model data, in memory. I think the developer should be able to do something like feathers.use() for CoreData, SQLite, or Realm (as long as you have linked the Library, we don't bundle them obviously). My thought process here is that it wouldn't force anyone into a specific storage engine, and it being in-memory by default would allow people to prototype stuff really quickly without having to do any linking or anything.
Allowing people to just store things in memory might be a dumb idea in iOS land but keeping things flexible and optional is key to developer happiness, even if it is a bit more work for us. So even if we need to use a datastore by default you should be able to interchange, CoreData, Realm, SQLite with just a couple lines.
Since @corymsmith and I are so busy with RN, @startupthekid if you have some time and motivation to work on this, you can totally take the lead and we'll gladly support you with PR reviews, testing and architecture discussions. Let me know and we'll give you read/write access to the repo. :smile:
In the absence of this, I've been using Moya, which is fantastic but there are some things I don't like about it, the primary thing that I think feathers could vastly improve on: endpoint targets.
Basically to sum it up, Moya is an abstraction layer on Alamofire and for each kind of request you want, you create an associated enum that looks like:
enum API {
case CreateUser(User)
case UpdateUser(User)
case CreatePost(postName: String, postImage: UIImage)
}
Which means if you want to support find, get, put, etc, that's a separate target for each and it gets cumbersome fast. Even for the small app I'm working on now I have 18 separate cases. I actually ended up making a protocol and mimicks feathers behavior:
protocol CRUD {
typealias Model
func create() -> SignalProducer<Model, Types.Error>
static func find(parameters: [NSObject: AnyObject]?) -> SignalProducer<[Model], Types.Error>
static func get(id: String) -> SignalProducer<Model, Types.Error>
func update() -> SignalProducer<Model, Types.Error>
remove() -> SignalProducer<Model, Types.Error>
}
I use this protocol on models and model classes but the same sort of thing would work on service objects.
There's also some good things Moya does, which would be more long term for this project, like stubbing requests support.
The things it should support:
- Service communication over REST similar to feathers-rest/client
- Service communication over Websockets similar to feathers-socket-commons/client
- Authentication similar to feathers-authentication/client
- An authenticate method that authenticates against the server and stores the JWT somewhere
- A logout method that destroys the stored JWT
- See docs for more info on the auth flow.
- Hooks similar to feathers-hooks
- Ability to register before and after hook chains to mutate data or query params
Ok so these are just some preliminary thoughts I've having (I haven't looked at this repo in a while either):
1) Services should (mostly) be auto-generated.
One of the things I love about feathers is that depending on my DB service, I can wrap my models in a call to feathers-mongoose or what have you. That would mean creating separate packages like feathers-realm or feathers-core-data that a model class would be passed to and return a service.
If you want to create custom services, you can just inherit from a class we provide: Service. Or maybe not a class but a protocol unless we need the storage properties of a class.
2) DBs as plugins
I like this idea and I think it's somewhat necessary because iOS is a different paradigm, sort of a combination of feathers-client and the feathers npm client. A service has to be able to a) make a network request and b) if there's a db plugin configured, persist/fetch/delete from that service.
Consider if a user is using Realm for example, when you call create it should first create on the network and then persist the model to Realm AFTER the network call has successfully complete. Other calls like find can run in parallel because the service should return the local model asap so the user has a copy to work with.
3) Terminology should be changed around
Again since iOS is a different beast than node, some of the terms should be changed to more accurately represent whats going on. The app should be called something like provider because it's not used in the same way as it is in express.
That's pretty much all I have for now, I'll have to put some more thought into it. And we should have an architecture discussion before we start writing any code. Personally I think it should model the existing feathers model as closely as possible.
With that in mind, you'd have something like:
feathers())provider.service("serviceName") and created with provider.use)feathers-rest or feathers-socketio) that you pass into a providerAnd the cool thing with if this is distributed via Cocoapods, all the db wrappers, configurations, and plugins can be subspecs:
pod 'Feathers'
pod 'Feathers/Realm'
pod 'Feathers/Rest'
pod 'Feathers/RX'
pod 'Feathers/ReactiveCocoa'
@startupthekid :+1: to all of that from me. What you think @corymsmith?
I was thinking the feathers aka provider object would look like:
class Feathers() {
static let singleton = Feathers()
func service(serviceName: String) -> Service {}
}
Then as the end user you'd do: Feathers.singleton.service('users').get(id, params)
Singleton seems the way to go so that the main provider is always in memory. I don't know if users will use (or want?) the ability to create sub-apps like they can in node. Seems like a feature that's not really needed?
@startupthekid that's pretty much exactly what @corymsmith and I were thinking.
He's working on a more complete Android implementation right now that will probably serve as a decent guide for what we should do on iOS when we're not dealing with a dynamic language.
@ekryski sounds great. I'd be glad to help out whenever I can. Pretty busy this month but come next month I'll have a lot of free time to hack away.
@ekryski I can definitely start on it tonight though, get the Pod all set up and whatnot so if you want to add me as a collaborator, that'd be awesome. I can kick it off in glorious feathers style.
@startupthekid done! Looking forward to seeing what you cook up :smile: If you can still remember to do PRs that would be sweet. Mostly for visibility at this point.
This may be a dumb question but I've never been part of a repo that either wasn't my own or one where I was the only dev on it. How do you guys manage PRs when you have write access to a repo? Make a branch and pull request to master?
@startupthekid you got it! Ideally we've been having at least one other person review each other's PRs. It's a good sanity check and makes it so that at least one other person knows what is going on. @corymsmith will be the best guy to review but I will do my best to help out as well.
Soon we'll be formalizing how we want to do releases and what the branching strategies should be but for now it's pretty open season.
Just try and keep commits and PRs smaller so they are easy to review. Your commits will show up in the #activity channel in Slack. Stoked!
@ekryski Excellent! Last quick thing: normally I build a lot of my stuff Reactively with ReactiveCocoa from the ground up but it seems like the better option is to build it with callbacks and then have reactive extensions packaged as a subspec. Maybe a promises subspec to using PromiseKit. Do ya'll have any preference as to how you want it?
Really what we're talking about doing here isn't just building the iOS equivalent of Feathers but also of hooks, rest, socketio, etc. And I was curious because hooks in JS can return promises but also use callbacks, largely in part because JS is 100x more dynamic than Swift.
Plus like I said before, feathers-ios is an interesting beast in that it's a combo of functionality from both client and server packages.
I'm leaning towards promises as the default and a Reactive extension/plugin later on. The callback stuff in JS is more to support Feathers 1.x versions.
In reality, because this is a different platform I would go with whatever is going to be the most commonplace/familiar for people on iOS. If promises are common enough then let's go with that. If people would find promises weird then maybe callbacks are better. I'll let you and @corymsmith decide.
@corymsmith I would say callbacks are definitely the most common; I don't know a whole ton of people that use PromiseKit. But playing the devil's advocate for a second, if someone looking at this library is already familiar with feathers, they'll already understand promises and how feathers uses them.
@ekryski just issued my first PR. It was a little wonky because I had to blow master away, run a new pod install to get the correct setup, then force update master so they'd have common history, then issue the pull request. Should definitely not have to do a force push again, it's just with Cocoapods initial setup and already existing repos.
What do you want me to do with branches I'm not using anymore like pods-integration? Leave them or give them the axe?
@startupthekid I've been building out the Android version so far with callbacks but was considering switching to Promises or potentially supporting Observables using RXJava.
I think it would be fine to start with promises and if we need to add callback support or extensions then we can do that.
As far as the old branch you can just give it the axe if you've already merged it in or you're not using it anymore.
Awesome sauce, thanks dudes for the quick responses. I'll have some prelim stuff for ya'll to peek at in a bit.
@startupthekid No worries and thanks alot for helping out!
I'll use PromiseKit since it's the most widely used. I'm all for not doing anything with callbacks, omitting them completely just makes everyone's life easier.
@startupthekid Here's how I was registering services and setting up Socket.IO in Android:
Feathers.getInstance()
.setBaseUrl("http://localhost:3030/")
.configure(new FeathersSocketIO())
.use("posts", Article.class);
FeathersService<Article> articleService = Feathers.service("posts");
So I was thinking that if it's more familiar and @corymsmith is going with callbacks then why don't we just do that for now and see how it's shaping up. We can always migrate to promises or add a promise extension later. I'm not too worried about people coming from JS to iOS or Android and being exactly the same. Just that all the client libs should be in the same spirit.
Yeah I agree. I flip-flopped once I realized it was much easier to build extensions off callbacks than off promises.
Ok. Let's close out this issue and then any following issues/discussions can happen either on the feathers-ios repo and slack. I might start setting up separate channels, at least for the different clients.
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue with a link to this issue for related bugs.
Most helpful comment
Yeah I agree. I flip-flopped once I realized it was much easier to build extensions off callbacks than off promises.