技术控

    今日:58| 主题:49211
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] Modern Swift Networking with Swish

[复制链接]
纷纷扰扰 发表于 2016-10-5 20:34:54
346 14

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
Video & transcription below provided by Realm. Realm Swift is a replacement for SQLite & Core Data written for Swift!
   Learn more about Realm
      Swish is a framework that is a wrapper around  NSURLRequest  s. It provides some structure to building requests and executing them in your app. Generally, I’ve seen a lot of huge web service classes with really broad interfaces. It’s similar to the massive view controller problem, where you end up not really knowing what it’s doing; it doesn’t have any single responsibility, and it’s therefore harder to test.
   We’ll discuss how Swish helps you solve that problem by building requests and executing them. We’ll also discuss the protocols, how they work, and how to write tests for them.
   Argo, the Out of Box Parser

    Argo is the JSON parser that is supported by Swish. With APIs, we’re working with JSON a lot of the time. Argo helps parse that into our model objects. It’s not a requirement for Swish, but it’s recommended and supported out of the box.
    To begin, we’ll set up a model called a Comment which has an id , a text and a user :
   [code]struct Comment {
    let id: Int
    let text: String
    let user: String
}

extension Comment: Decodable {
    // Assume: { "id": , "commentText": "Hello world", "user": "ralph" }
    static func decode(json:JSON) -> Decoded {
        return curry(Comment.init)
            <^> json <| "id"
            <*> json <| "commentText"
            <*> json <| "user"
    }
}[/code]    We have a simple struct definition, and at the bottom we implement this protocol called Decodable which comes from Argo. It’s the JSON parser. We can see it implements the decode function and it returns this Decoded type of a comment.
    Decoded is a lot like a Result type, in that it can have a success or a failure case as an enum. With Decoded , it can have a comment or not, and if it doesn’t, it’s going to have more information about why it doesn’t. (i.e. “This JSON was missing a key,” or “It had a string here when it was expecting an integer.”)
    You can see the structure here, that we’re initializing a Comment with this JSON and we’re pulling out these keys to do that.
   Building a GET Request

   Here’s our first request, where we see some actual Swish and common patterns. We also define structs and implement individual objects for each request we want to implement:
   [code]struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(
            string: "https://www.example.com/comments/\(id)"
        )!
        return NSURLRequest(URL:url)
    }
}[/code]   (Note: you can use a class instead of a struct here, if you want.)
    We define the CommentRequest which will get a comment from the server. The first thing here is the typealias. What we’re saying here is that towards the end of the execution of the request, this method should be called so that the result is a Comment . This is what we’re expecting, and it can use that information to do things like parse it for us.
    Next up we have the id which is just properties on the struct that tells us what data we need to build the requests. There’s nothing about the protocol here.
    Lastly, we have the build method which returns an NSURLRequest . This builds up using an NSURL API, the request we interpolate in the id and then we return that request.
   Execute a GET Request

   Executing it looks like this:
   [code]let request = CommentRequest(id: 1)

APIClient().performRequest(request) { result in
    switch result{ // Result
    case let .Success(comment):
        print("Here's the comment: \(comment)")
    case let .Failure(error):
        print("Oh no, an error: \(error)")
    }
}[/code]    First, we build the request object. With this object, we can pass it around, compose it with other things, and do whatever we want with it because it’s not executed yet; it’s just a representation of what that request is. We then initialize this API client which comes from Swish and we say, “perform the request” and then we pass in a callback which then gets a result type of Comment or a Swish error.
    We can print it out if it’s a success case, otherwise if it’s a failure, we’ll get some information about that error contained within the SwishError type. The SwishError can be that it failed to parse the JSON, or you got a 500 status code from the server, a couple different things, but it’s just going to have more context as to why it fails, then you can do things accordingly.
   Build a POST request

    A POST request isn’t much different. We define this struct, implement the request protocol, and do the same thing about having a ResponseObject of a Comment :
   [code]struct CreateCommentRequest: Request {
    typealias ResponseObject = Comment
   
    let text: String
    let user: String
   
    var jsonPayload: [String: AnyObject] {
        return ["text": text, "user": user]
    }
   
    func build() -> NSURLRequest {
        // ...
    }
}[/code]    Some create requests might not have responses, so we place void into the ResponseObject , or have a typealias for an empty response that says it’s not going to return anything.
    Next, we have these properties again, where it defines what we need to create a Comment : we have the id , the text and a user . The server will define our id for us, so to actually create one, we just need the text and the user .
    As for jsonPayload , is just a computed property, not a requirement. It clarifies what our JSON is and this is what will be sent to the server. It’s just a dictionary where we pass in those values from the object itself.
    Finally, we have the build method, which is not super complicated:
   [code]struct Create CommentRequest: Request {
    // ...
    func build() -> NSURLRequest {
        let url = NSURL(string: "https://ww.example.com/comments")!
        let request =  NSMutableURLRequest(URL: url)
        request.HTTPMethod = "POST"
        request.setValue("appication/json", forHTTPHeaderField: "Accept")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.jsonPayload = jsonPayload
        return request
    }
}[/code]    It returns the NSURLRequest , and in this case we create a URL, then we create an NSMutableURLRequest because we need to change some things, and we set some headers. So far, all of this has been the API provided by NSURLRequest , it’s not anything custom.
    However, request.jsonPayload = jsonPayload is custom; that was the dictionary that was our property, and we just set that. The Swish extension runs that through NSJSONSerialization . We don’t want to type that all over our app, so you do that for us.
   Execute POST request

   [code]let request = CreateCommentRequest(text: "Hola", user: "ralph")

APIClient().performRequest(request) { result in
    switch result{ // Result
    case let .Success(comment):
        print("Here's the comment: \(comment)")
    case let .Failure(error):
        print("Oh no, an error: \(error)")
    }
}

// => Comment(id: 2, text: "Hola", user: "ralph")[/code]    In this case, we pass it into this performRequest method on an APIClient . We get the result back of the comment or the error. APIClient is a struct that Swish provides that implements the client protocol. In a lot of cases, we’d use something like dependency injection to pass this in so that under a test, we can pass in a fake one instead. It does support a couple different options like running it on a different queue or scheduling it, but it’s a really lightweight thing. It basically takes your requests and then passes them and runs them through the default NSURLSession .
    Argo’s Decodable

    Argo’s Decodable has one static function that it exposes, plus this associated type of DecodedType :
   [code]protocol Decodable {
    associatedtype DecodedType = Self
    static func decode(_ json: JSON) -> Decoded
}[/code]    We implement the decode method, it takes some JSON and it returns a Decoded of, in this case, Self which is the comment and then we do the same implementation we did earlier:
   [code]protocol Decodable {
    associatedtype DecodedType = Self
    static func decode(_ json: JSON) -> Decoded
}

extension Comment: Decodable {
    staticc func decode(json: JSON) -> Decoded {
        return curry(Comment.init)
            <^> json <| "id"
            <*> json <| "commentText"
            <*> json <| "user"
    }
}[/code]   Swish has two different protols here:
   [code]protocol Parser {
    associatedtype Representation
    static func parse(j: AnyObject) -> Result
}

protocol Request {
    associatedtype ResponseObject
    associatedtype ResponseParser: Parser = JSON
   
    func build() -> NSURLRequest
    func parse(j: ResponseParser.Representation)
                ->  Result
}[/code]    I included Parser here because it’s referenced in the Request protocol, but we won’t go too much into it. It just takes some AnyObject and turns it into some arbitrary representation.
    In the JSON case, we can assume this gets AnyObject and will turn it into JSON. You could do other things like take AnyObject and do XML, or into whatever you want.
    The ResponseParser has to be this Parser protocol, and it defaults to JSON. The JSON is where Argo comes in so that Swish will implement the parser protocol on the JSON type from Argo, and then it sets it as the default one. This means that in a standard case, we’re using Argo with a JSON request and everything works. You don’t have to do anything custom to do that.
    If you wanted to swap this out, the ResponseParser is one of the ways you could do that. On each request, you could change the parse to something else.
    Below, we see that in the parse mehthod, which takes a parameter that is the type of Representation of the ResponseParser . In JSON’s case, that’s also JSON. We see in the implementation that it takes JSON and returns ResponseObject , which is usually self. If we put that here side-by-side with the request, we see that implemented:
   [code]protocol Request {
    associatedtype ResponseObject
    associatedtype ResponseParser: Parser = JSON
   
    func build() -> NSURLRequest
    func parse(j: ResponseParser.Representation) ->  Result
}

struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(string: "https://www.example.com/comments/\(id)")!
        return NSURLRequest(URL: url)
    }
}[/code]    Notice how we’ve never seen the parse method, since it is often implemented for you by protocol extensions. We know it’s an Argo thing, and we know it’s Decodable and so we can implement that for you.
   To show you what that would look like in some of the cases where you do need to implement it:
   [code]struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(
            string: "https://www.example.com/comments/\(id)"
        )!
        return NSURLRequest(URL:url)
    }
}0[/code]    We’ve implemented this parse method with two different return s. It takes JSON, as we expected, and it returns this result of a Comment and SwishError . The default behavior takes .fromDecoded , which is a method on the result type, and turns the Decoded into this result. We just pass in the JSON to this decode method on Comment .
    That’s the default one that we can do for you in most cases. Where that wouldn’t happen is when you have a JSON response that has a root key in it. Instead of getting a Comment back at the root of the JSON request, you might get an object that has a Comment key and then inside of there is the Comment . Argo doesn’t know how to parse that by default, so you would do the second return line: return .fromDecoded(j <| "comment") that says root key on it, where you say, “pull out this comment value and then parse that”, and using Argo’s helper to do that for you.
   Stepping back from the details of parsing, let’s talk about testing. Using Swish testing becomes a lot easier and more straightforward. We have some nice helpers that make it really simple to write tests to protect you from your future self.
    We like to use Quick and Nimble because of the expressiveness they give us. We like the asynchronous helpers that Nimble has. We also use Nocilla , another framework which will stub out the entire network stack so we that when you hit a certain URL, you can return a certain JSON instead.
   Testing with Swish is nice because we build up these request objects and we can then make assertions on them. We can call a build method get the URL request back, and then do whatever we want with it and it’s not executed yet so we don’t need to stub the network in most cases.
   [code]struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(
            string: "https://www.example.com/comments/\(id)"
        )!
        return NSURLRequest(URL:url)
    }
}1[/code]   To retrieve a comment, we use helpers. There’s one that says “it behaves like a GET request, and then we pass in a Comment”. This makes an assertion that the HTTP request is using the GET method, but there are other things it can do as well.
    Let’s say your API has specific requirements. One API I worked with recently had a really interesting situation where if I set the ContentType header along with the GET request, it would blow up. By default were doing that, so we decided we needed to make some changes to get the API to work. We extended the itBehavesLike helper to say, “if it’s a GET request, make sure we’re not sending that header”. This way, we protected ourselves from our future selves if we were to forget that happens, but we also say to the future developers who come in, “this was intentional.” Somebody’s gonna see the lack of a header when GET requests are sent and know that there’s a test for this. This is an intentional behavior that they expected, so if I want to change this, why should I, and is there something I’m missing here that I might need to know?
    it("points to /comments/:id") is a really simple test that says it should hit this endpoint, and we should take the id off the CommentRequest and pass it in. It’s just one of those things that protects you. If you change the URLs, it’s nice to have a test to make sure this is what the URL is supposed to be so somebody doesn’t accidentally have a typo and blows everything up.
   We create a comment:
   [code]struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(
            string: "https://www.example.com/comments/\(id)"
        )!
        return NSURLRequest(URL:url)
    }
}2[/code]   This has a really similar API, as you can see. The main difference between those two and the previous one is that we don’t pass in any values for the values, because it doesn’t need them. It’s not using these for anything, so we’re just being lazy. What we normally do in real apps is extend the requests and have factory methods that might fill in default values for things, so that if we do care about the value we can just pass in that one and not have to worry about the other ones.
   Another use of this is that it behaves like a POST request in that it defines another one that behaves like an authenticated request. Some requests need authorization headers and need some sort of user validation, while others don’t. We’re able to do that same kind of thing with a really simple one-liner that says, “it behaves like an authenticator request”. Then we can check the header and we can say, “it has the header that we expected, it has the same thing that’s in its user defaults”.
   In the last test, we test the payload. This time we actually pass in values. We say, “hi, from Ralph”. And Ralph says, “hi”, and then we make tests and we say that it has a payload of this. It’s basically the same thing we passed in.
   These are all really simple cases, but there are definitely cases where these become much more valuable, like if you need to serialize it in a different way.
    A more interesting example is another API I’ve worked with recently, where when you send in your first name and last name, you need to put zeros around it. As Jake Craige, I have to send my name to the API as 0Jake0 and 0Craige0 . I don’t know what they’re doing to support that or what kind of parsing they’re trying to do, but that’s a requirement. I also don’t want to store my name like that in my app. On top of that, I have to parse it to remove the zeros real quick.
   If you’re a good citizen of git, you might leave a really nice commit message that explains why you did this kind of thing. Then they can do a blame and figure out why it happened.
   [code]struct CommentRequest: Request {
    typealias ResponseObject = Comment
   
    let id: Int
   
    func build() -> NSURLRequest {
        let url = NSURL(
            string: "https://www.example.com/comments/\(id)"
        )!
        return NSURLRequest(URL:url)
    }
}3[/code]    In the second line, we stub out the entire network stack where we’re going through the full request response cycle. We’re using some type inference here from Swift, .comment is from an enum called JSON Fixture, and it’s just a really nice expressive way to stub this request. It’s using Nocilla to stub out the network. It says, “when this URL gets hit, return this response”.
   This response is actually a JSON file on disk that has what we’re expecting for the response. This test gives you the actual parsing of the JSON, and really that full from-start-to-finish kind of request. You’re not actually hitting the network, but you’re as close as you’re going to get without doing that.
   This is useful if you’re doing a UI test and you can’t really reach into all the places you need to put in a fake API client or something.
    In the implementation, we have a response that’s supposed to be a comment and we say it’s an optional, and then when you perform the request at the end in this callback we say the response = $0.value so that $0 is a result type. A result type has a value method that’s an optional of if there’s a value or not. So it’s getting you back from that wrapped result type to just an optional case. Our test will fail anyway if it doesn’t have a value, so it’s okay to do it this way.
   Lastly, we see a Nimble helper that’s really useful in a lot more cases than just this network stuff. We say “we expect the response to eventually equal this value”. It just does some looping and just checks it until some timeout and says, this should be there or not.
   What else can you do with Swish?

    You can definitely do PUT requests, DELETE requests, and whatever other ones, they’re the same idea. You can use dependency injection in a lot of places to stub things out where you might otherwise not be able to because everything you use in Swish is backed by some protocol. You’re able to use dependency injection to put in fake clients and not hit the network, or make some validations. You can cancel in-flight requests, you can resume them, you can do whatever you can do with, NSURLSessionDataTask . When you call performRequest on the API client that’s what it returns, and so at that point you can do whatever you want with it. You can cancel it if you’re going to another screen. You can stop it, start it, whatever you want and that’s where we see that it’s just a light wraparound.
    The API client does support an option if you wanna run a request on a background thread or on the main thread. You can also support arbitrary JSON parsers. There’s another protocol called a Deserializer which works together with Parser to turn the response from the server which is given to us as NSData into some type down the line. With that combination, e you can support any type you want.
    You don’t have to use Argo, you could use Mapper , Object Mapper , SwiftyJSON or many of the other ones out there.
    But don’t stop there! If you wanted to turn a response into a UIImage or something else, you could. If you needed XML for some reason, XML into some XML, anything you want. It’s all pluggable.
   If you have any problems with it open an issue or talk to me!
   Q: What about Swift 3.0 and the grand renaming? You guys seem to be a little bit behind. Maybe it’s just getting tired of changing everything every week.
   JC: Yeah, I don’t do a lot of work around that stuff. It’s typically other people who are doing a lot of the maintenance on these, which is great. Other than that, it’s definitely something we will support because we’re using almost all of our open source iOS libraries on a day-to-day basis. We’re a consulting company, so we have multiple teams doing different things. We like to be on the most modern stuff possible, so it’s definitely going to be supported. I don’t know a timeline, but once it’s out we’re gonna use it. Until then, as always, PRs are welcome if you wanna help out.
    Q: You said the API client works on different threads and has some other options. Are there any options for the NSURLSession , like background downloads and things like that?
    JC: No, there’s not. That’s one of those places where we just expect you to implement a client with the protocols backing it. What I’ve done in a case for that kind of stuff was just ended up calling through to it when I needed to. But yeah, you implement a client which tells you to implement this performRequest method and you can do whatever you want to do with that. It’s all a very lightweight, there’s not much code in this. It doesn’t actually do any networking, it’s just relying on iOS’s APIs to do all that.
    See the discussion on Hacker News .
   See full transcription         
Modern Swift Networking with Swish-1 (framework,therefore,structure,building,provides)
         Jake Craige

     Jake Craige is a Developer at thoughtbot, a consulting company of designers and developers that partner with you to build great web and mobile products. He's really excited about Swift and how its static typing and functional capabilities lead to safe and creative solutions to common problems.
      Twitter
友荐云推荐




上一篇:Staged vs Stageless Handlers
下一篇:Dividing React Work Between Designers and Developers
酷辣虫提示酷辣虫禁止发表任何与中华人民共和国法律有抵触的内容!所有内容由用户发布,并不代表酷辣虫的观点,酷辣虫无法对用户发布内容真实性提供任何的保证,请自行验证并承担风险与后果。如您有版权、违规等问题,请通过"联系我们"或"违规举报"告知我们处理。

小依娜 发表于 2016-10-5 22:14:24
沙发抢不到,板凳也行啊!
回复 支持 反对

使用道具 举报

naoju 发表于 2016-10-6 14:13:52
咨询纷纷扰扰一个非技术性问题
回复 支持 反对

使用道具 举报

zywiq 发表于 2016-10-8 11:49:02
有谁会在时过境迁之后还在那里等你。
回复 支持 反对

使用道具 举报

傅盛 发表于 2016-10-20 00:52:19
阅!谢谢分享
回复 支持 反对

使用道具 举报

黄清涛 发表于 2016-10-20 17:47:27
小白一个 顶一下
回复 支持 反对

使用道具 举报

个iut 发表于 2016-10-23 13:27:30
LZ敢整点更有创意的不?兄弟们等着围观捏~
回复 支持 反对

使用道具 举报

瞳哖旳糖果箼 发表于 2016-10-26 01:51:06
兰州烧饼,鉴定完毕!
回复 支持 反对

使用道具 举报

云凝 发表于 2016-10-26 10:10:11
垃圾内容,路过为证。
回复 支持 反对

使用道具 举报

AsmInheli 发表于 2016-10-26 21:08:04
高手云集 果断围观
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读

扫码访问 @iTTTTT瑞翔 的微博
回页顶回复上一篇下一篇回列表手机版
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )|网站地图 酷辣虫

© 2001-2016 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表