已将书中代码改为 Swift 3 版本。
Web Services APIs
- URL 描述协议地址
- URLRquest 把 URL 包装起来,设置 http 方法(GET, PUT, POST 等等)
- 配置好 URLRequest 之后,用 URLSession 来创建连接。
- 当你把 URLRequest 交给 URLSession 之后,它会返回一个 URLSessionTask,它包装了相应的请求,你用这个 task 开始和停止连接。
- 当 URLSessionTask 完成后你得到服务端的相应,你需要把相应的内容解析成结构化的数据。
Project
创建 RanchForecast 项目。
打开 MainMenu.xib,删除 Window。
-
创建 MainWindowController.swift,修改方法
override var windowNibName: String?{ return "MainWindowController" }
-
修改 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 初始化一个请求并取回数据时是异步的,当请求发出之后,得到结果时代码会被呼叫。有几个方式可以实现
-
委托模式,可以这样
protocol ScheduleFetcherDelegate{ func scheduleFetcher(fetcher: SchduleFetcher, didFetchCourses: [Courses]) func scheduleFetcher(fetcher: SchduleFetcher, didFailWithError: NSError) }
这需要加一个 delegate 属性给 ScheduleFetcher,还需要一个方法开始抓取。
NSNotificationCenter 可以但不优雅。通知适合同时发给多个监听者的时候。通知没法通过 userInfo 提供类型安全的对象。
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 []
}
}
用户界面
- 打开 MainWindowController.xib,点 window ,在 inspector 中修改窗口标题。
- 添加 table view 到 window 中。Content Mode: View Based. 2 columns。
- 拖一个 Date Formatter 到日期栏的 cell 上,设置为 Medium
下面开始做绑定
- 拖一个 Array Controller 到画布上(window之外的空白区域)。
- binding inspector 面板中:Content Array: File's Owner, Model Key Path: courses
- 选择 table view 做绑定:Content: Array Controller;Controller Key: arrangedObjects;Selection Indexes: Array Controller 并且 Controller Key: selectionIndexes;
- 最后绑定值。选中 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)
}
}