Cocoa Programming for OS X: Web Services

已将书中代码改为 Swift 3 版本。

Web Services APIs

  • URL 描述协议地址
  • URLRquest 把 URL 包装起来,设置 http 方法(GET, PUT, POST 等等)
  • 配置好 URLRequest 之后,用 URLSession 来创建连接。
  • 当你把 URLRequest 交给 URLSession 之后,它会返回一个 URLSessionTask,它包装了相应的请求,你用这个 task 开始和停止连接。
  • 当 URLSessionTask 完成后你得到服务端的相应,你需要把相应的内容解析成结构化的数据。

Project

  1. 创建 RanchForecast 项目。

  2. 打开 MainMenu.xib,删除 Window。

  3. 创建 MainWindowController.swift,修改方法

     override var windowNibName: String?{
         return "MainWindowController"
     }
    
  4. 修改 AppDelegate.swift 实现自己的单窗口应用

         class AppDelegate: NSObject, NSApplicationDelegate {
         var mainWindowController: MainWindowController?
    
         func applicationDidFinishLaunching(_ aNotification: Notification) {
         
         let mainWindowController = MainWindowController()
         mainWindowController.showWindow(self)
         self.mainWindowController = mainWindowController
             }
         }
    

Run,确认窗口显示。

Model

创建数据模型 Course.swift

    import Cocoa
    
    class Course: NSObject {
        let title: String
        let url: URL
        let nextStartDate: Date
        
        init(title: String, url: URL, nextStartDate: Date) {
            self.title = title
            self.url = url
            self.nextStartDate = nextStartDate
            super.init()
        }
    }

创建 ScheduleFetcher.swift 负责请求网络接口,初始化为默认设置

class ScheduleFetcher {

let session: URLSession

init() {
    let config = URLSessionConfiguration.default
    session = URLSession(configuration: config)
}
}

继续之前思考一下:用 ScheduleFetcher 初始化一个请求并取回数据时是异步的,当请求发出之后,得到结果时代码会被呼叫。有几个方式可以实现

  1. 委托模式,可以这样

     protocol ScheduleFetcherDelegate{
         func scheduleFetcher(fetcher: SchduleFetcher, didFetchCourses: [Courses])
         func scheduleFetcher(fetcher: SchduleFetcher, didFailWithError: NSError)
     }
    

这需要加一个 delegate 属性给 ScheduleFetcher,还需要一个方法开始抓取。

  1. NSNotificationCenter 可以但不优雅。通知适合同时发给多个监听者的时候。通知没法通过 userInfo 提供类型安全的对象。

  2. Completion Handler,事件句柄。这是一个非常好的选择。完成句柄适合一次性的回调。我们将用事件句柄的模式,它与它被呼叫的地方非常相像。

enums 提供了一个意义简明的方式来协助值定义,我们可以依赖编译器和它的类型检查来保证清晰。

    class ScheduleFetcher {
     enum FetchCoursesResult {
    case success([Course])
    case failure(NSError)
}

由于 enums 定义在 ScheduleFetcher 中,它作为嵌套类型被引用,在内部可以直接访问 FetchCoursesResult,在外部必须包含类名 ScheduleFetcher.FetchCoursesResult。

func fetchCoursesUsingCompletionHandler(completionHandler: @escaping (FetchCoursesResult) -> (Void)) {
    let url = URL(string: "https://bookapi.bignerdranch.com/courses.json")!
    let request = URLRequest(url: url)
    
    
    
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        var result: FetchCoursesResult
        
        if data == nil {
            result = .failure(error as! NSError)
        }
        else{
            print("Received \(data!.count) bytes", terminator:"\n")
           result = .success([]) //还没实现解析,先传个空值
        }
        
        OperationQueue.main.addOperation({ 
            completionHandler(result)
        })
        
    }
    
    task.resume()
}

在 windowDidLoad() 呼叫它。添加一个 daynamic 数组,稍后将绑定一个 array controller 到这个数组上,dynamic 这个关键词使得这个属性兼容数据绑定。

override func windowDidLoad() {
    super.windowDidLoad()
    
    let fetcher = ScheduleFetcher()
    dynamic var courses: [Course] = []

    fetcher.fetchCoursesUsingCompletionHandler { (result) -> (Void) in
        switch result{
        case .success(let courses):
            print("Got courses: \(courses)", separator: "", terminator: "\n")
            self.courses = courses
        case .failure(let error):
            print("Got error: \(error)", separator: "", terminator: "\n")
            NSAlert(error: error).runModal()
            self.courses = []
        }
    }
}

URLSession, HTTP status codes, error

制造一个 error 做测试

JSON 解析

    func courseFromDictionary(courseDict: NSDictionary) -> Course? {
    let title = courseDict["title"] as! String
    let urlString = courseDict["url"] as! String
    let upcomingArray = courseDict["upcoming"] as! [NSDictionary]
    let nextUpcomingDict = upcomingArray.first!
    let nextStartDateString = nextUpcomingDict["start_date"] as! String
    
    let url = URL(string: urlString)!
    
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let nextStartDate = dateFormatter.date(from: nextStartDateString)!
    
    return Course(title: title, url: url, nextStartDate: nextStartDate)
}

解析数据,返回数组。这里的的 JSONSerialization 与 Swift 2 有很大不同。

    func coursesFromData(data: Data) -> [Course] {
    do {
        let topLevelDict = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
        let courseDicts = topLevelDict["courses"] as! [NSDictionary]
        var courses: [Course] = []
        for courseDict in courseDicts {
            if let course = courseFromDictionary(courseDict: courseDict) {
                courses.append(course)
            }
        }
        return courses
    } catch let error as NSError {
        print(error)
        return []
    }
}

用户界面

  1. 打开 MainWindowController.xib,点 window ,在 inspector 中修改窗口标题。
  2. 添加 table view 到 window 中。Content Mode: View Based. 2 columns。
  3. 拖一个 Date Formatter 到日期栏的 cell 上,设置为 Medium

下面开始做绑定

  1. 拖一个 Array Controller 到画布上(window之外的空白区域)。
  2. binding inspector 面板中:Content Array: File's Owner, Model Key Path: courses
  3. 选择 table view 做绑定:Content: Array Controller;Controller Key: arrangedObjects;Selection Indexes: Array Controller 并且 Controller Key: selectionIndexes;
  4. 最后绑定值。选中 table view cell(像一个缩小的slide的图标),Value:Table Cell View;Model Key Path: objectValue.nextStartDate。title 栏的 cell,Value: bind to Table Cell View,Model Key Path: objectValue.title

Run.

打开网址

双击事件只能用代码设置。拖两个 IBOutlet 到 MainWindowController.swift 中

@IBOutlet weak var tableView: NSTableView!
@IBOutlet var arrayController: NSArrayController!

为 table view 设置双击事件的执行目标

 override func windowDidLoad() {
    super.windowDidLoad()
    
    tableView.target = self
    tableView.doubleAction = #selector(openClass(sender:))

用默认浏览器打开网址

func openClass(sender: AnyObject) {
    if  let course = arrayController.selectedObjects.first as? Course {
        NSWorkspace.shared().open(course.url)
    }
}

你可能感兴趣的:(Cocoa Programming for OS X: Web Services)