iOS14开发-网络

819 阅读8分钟

基础知识

App如何通过网络请求数据?

客户服务器模型

  1. App 通过一个 URL 向特定的主机发送一个网络请求加载需要的资源。URL 一般是使用 HTTP(HTTPS)协议,该协议会通过 IP(或域名)定位到资源所在的主机,然后等待主机处理和响应。
  2. 主机通过本次网络请求指定的端口号找到对应的处理软件,然后将网络请求转发给该软件进行处理(处理的软件会运行在特定的端口)。针对 HTTP(HTTPS)请求,处理的软件会随着开发语言的不同而不同,如 Java 的 Tomcat、PHP 的 Apache、.net 的 IIS、Node.js 的 JavaScript 运行时等)
  3. 处理软件针对本次请求进行分析,分析的内容包括请求的方法、路径以及携带的参数等。然后根据这些信息,进行相应的业务逻辑处理,最后通过主机将处理后的数据返回(返回的数据一般为 JSON 字符串)。
  4. App 接收到主机返回的数据,进行解析处理,最后展示到界面上。
  5. 发送请求获取资源的一方称为客户端。接收请求提供服务的一方称为服务端

基本概念

URL

  • Uniform Resource Locator(统一资源定位符),表示网络资源的地址或位置。
  • 互联网上的每个资源都有一个唯一的 URL,通过它能找到该资源。
  • URL 的基本格式协议://主机地址/路径

HTTP/HTTPS

  • HTTP—HyperTextTransferProtocol:超文本传输协议。
  • HTTPS—Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure:超文本传输安全协议。

请求方法

  • 在 HTTP/1.1 协议中,定义了 8 种发送 HTTP 请求的方法,分别是GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
  • 最常用的是 GETPOST

响应状态码

状态码描述含义
200Ok请求成功
400Bad Request客户端请求的语法出现错误,服务端无法解析
404Not Found服务端无法根据客户端的请求找到对应的资源
500Internal Server Error服务端内部出现问题,无法完成响应

请求响应过程

请求响应过程

JSON

  • JavaScript Object Notation。
  • 一种轻量级的数据格式,一般用于数据交互。
  • 服务端返回给 App 客户端的数据,一般都是 JSON 格式。

语法

  • 数据以键值对key : value形式存在。
  • 多个数据由,分隔。
  • 花括号{}保存对象。
  • 方括号[]保存数组。

key与value

  • 标准 JSON 数据的 key 必须用双引号""
  • JSON 数据的 value 类型:
    • 数字(整数或浮点数)
    • 字符串("表示)
    • 布尔值(true 或 false)
    • 数组([]表示)
    • 对象({}表示)
    • null

解析

  • 厘清当前 JSON 数据的层级关系(借助于格式化工具)。
  • 明确每个 key 对应的 value 值的类型。
  • 解析技术
    • Codable 协议(推荐)。
    • JSONSerialization。
    • 第三方框架。

URLSession

使用步骤

  1. 创建请求资源的 URL。
  2. 创建 URLRequest,设置请求参数。
  3. 创建 URLSessionConfiguration 用于设置 URLSession 的工作模式和网络设置。
  4. 创建 URLSession。
  5. 通过 URLSession 构建 URLSessionTask,共有 3 种任务。 (1)URLSessionDataTask:请求数据的 Task。  (2)URLSessionUploadTask:上传数据的 Task。 (3)URLSessionDownloadTask:下载数据的 Task。 
  6. 启动任务。
  7. 处理服务端响应,有 2 种方式。 (1)通过 completionHandler(闭包)处理服务端响应。 (2)通过 URLSessionDataDelegate(代理)处理请求与响应过程的事件和接收服务端返回的数据。

基本使用

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // get()
        // post()
    }

    func get() {
        // 1. 确定URL
        let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申请的key")
        // 2. 创建请求
        let urlRequest = URLRequest(url: url!)
        // cachePolicy: 缓存策略,App最常用的缓存策略是returnCacheDataElseLoad,表示先查看缓存数据,没有缓存再请求
        // timeoutInterval:超时时间
        // let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
        let config = URLSessionConfiguration.default
        // 3. 创建URLSession
        let session = URLSession(configuration: config)
        // 4. 创建任务
        let task = session.dataTask(with: urlRequest) { data, _, error in
            if error != nil {
                print(error!)
            } else {
                if let data = data {
                    print(String(data: data, encoding: .utf8)!)
                }
            }
        }
        // 5. 启动任务
        task.resume()
    }

    func post() {
        let url = URL(string: "http://v.juhe.cn/toutiao/index")
        var urlRequest = URLRequest(url: url!)
        // 指明请求方法
        urlRequest.httpMethod = "POST"
        // 指明参数
        let params = "type=top&key=申请的key"
        // 设置请求体
        urlRequest.httpBody = params.data(using: .utf8)
        let config = URLSessionConfiguration.default
        // delegateQueue决定了代理方法在哪个线程中执行
        let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
        let task = session.dataTask(with: urlRequest)
        task.resume()
    }
}


// MARK:- URLSessionDataDelegate
extension ViewController: URLSessionDataDelegate {
    // 开始接收数据
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        // 允许接收服务器的数据,默认情况下请求之后不接收服务器的数据即不会调用后面获取数据的代理方法
        completionHandler(URLSession.ResponseDisposition.allow)
    }

    // 获取数据
    // 根据请求的数据量该方法可能会调用多次,这样data返回的就是总数据的一段,此时需要用一个全局的Data进行追加存储
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        let result = String(data: data, encoding: .utf8)
        if let result = result {
            print(result)
        }
    }

    // 获取结束
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print(error)
        } else {
            print("=======成功=======")
        }
    }
}

注意:如果网络请求是 HTTP 而非 HTTPS,默认情况下,iOS 会阻断该请求,此时需要在 Info.plist 中进行如下配置。

<key>NSAppTransportSecurity</key>
<dict>
	<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

下载数据

class ViewController: UIViewController {
    // 下载进度
    @IBOutlet var downloadProgress: UIProgressView!
    // 下载图片
    @IBOutlet var downloadImageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        download()
    }

    func download() {
        let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/wall.png")!
        let request = URLRequest(url: url)
        let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
        let task = session.downloadTask(with: request)
        task.resume()
    }
}

extension ViewController: URLSessionDownloadDelegate {
    // 下载完成
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 存入沙盒
        let savePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        // 文件类型根据下载的内容决定
        let fileName = "\(Int(Date().timeIntervalSince1970)).png"
        let filePath = savePath + "/" + fileName
        print(filePath)
        do {
            try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: filePath))
            // 显示到界面
            DispatchQueue.main.async {
                self.downloadImageView.image = UIImage(contentsOfFile: filePath)
            }
        } catch {
            print(error)
        }
    }

    // 计算进度
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        DispatchQueue.main.async {
            self.downloadProgress.setProgress(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite), animated: true)
        }
    }
}

上传数据

上传数据需要服务端配合,不同的服务端代码可能会不一样,下面的上传代码适用于本人所写的服务端代码

  • 数据格式。

上传数据格式

  • 实现。
class ViewController: UIViewController {
    let YFBoundary = "AnHuiWuHuYungFan"
    @IBOutlet var uploadInfo: UILabel!
    @IBOutlet var uploadProgress: UIProgressView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        upload()
    }

    func upload() {
        // 1. 确定URL
        let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/UploadServlet")!
        // 2. 确定请求
        var request = URLRequest(url: url)
        // 3. 设置请求头
        let head = "multipart/form-data;boundary=\(YFBoundary)"
        request.setValue(head, forHTTPHeaderField: "Content-Type")
        // 4. 设置请求方式
        request.httpMethod = "POST"
        // 5. 创建NSURLSession
        let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
        // 6. 获取上传的数据(按照固定格式拼接)
        var data = Data()
        let header = headerString(mimeType: "image/png", uploadFile: "wall.png")
        data.append(header.data(using: .utf8)!)
        data.append(uploadData())
        let tailer = tailerString()
        data.append(tailer.data(using: .utf8)!)
        // 7. 创建上传任务 上传的数据来自getData方法
        let task = session.uploadTask(with: request, from: data) { _, _, error in
            // 上传完毕后
            if error != nil {
                print(error!)
            } else {
                DispatchQueue.main.async {
                    self.uploadInfo.text = "上传成功"
                }
            }
        }
        // 8. 执行上传任务
        task.resume()
    }

    // 开始标记
    func headerString(mimeType: String, uploadFile: String) -> String {
        var data = String()
        // --Boundary\r\n
        data.append("--" + YFBoundary + "\r\n")
        // 文件参数名 Content-Disposition: form-data; name="myfile"; filename="wall.jpg"\r\n
        data.append("Content-Disposition:form-data; name=\"myfile\";filename=\"\(uploadFile)\"\r\n")
        // Content-Type 上传文件的类型 MIME\r\n\r\n
        data.append("Content-Type:\(mimeType)\r\n\r\n")

        return data
    }

    // 结束标记
    func tailerString() -> String {
        // \r\n--Boundary--\r\n
        return "\r\n--" + YFBoundary + "--\r\n"
    }

    func uploadData() -> Data {
        let image = UIImage(named: "wall.png")
        let imageData = image!.pngData()
        return imageData!
    }
}

extension ViewController: URLSessionTaskDelegate {
    // 上传进去
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        DispatchQueue.main.async {
            self.uploadProgress.setProgress(Float(totalBytesSent) / Float(totalBytesExpectedToSend), animated: true)
        }
    }
    
    // 上传出错
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            print(error)
        }
    }
}

WKWebView

  • 用于加载 Web 内容的控件。
  • 使用时必须导入WebKit模块。

基本使用

  • 加载网页。
// 创建URL
let url = URL(string: "https://www.abc.edu.cn")
// 创建URLRequest
let request = URLRequest(url: url!)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载网页
webView.load(request)
  • 加载本地资源。
// 文件夹路径
let basePath = Bundle.main.path(forResource: "localWeb", ofType: nil)!
// 文件夹URL
let baseUrl = URL(fileURLWithPath: basePath, isDirectory: true)
// html路径
let filePath = basePath + "/index.html"
// 转成文件
let fileContent = try? NSString(contentsOfFile: filePath, encoding: String.Encoding.utf8.rawValue)
// 创建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加载html
webView.loadHTMLString(fileContent! as String, baseURL: baseUrl)

注意:如果是本地资源是文件夹,拖进项目时,需要勾选Create folder references,然后用Bundle.main.path(forResource: "文件夹名", ofType: nil)获取资源路径。

与JavaScript交互

创建WKWebView

lazy var webView: WKWebView = {
    // 创建WKPreferences
    let preferences = WKPreferences()
    // 开启JavaScript
    preferences.javaScriptEnabled = true
    // 创建WKWebViewConfiguration
    let configuration = WKWebViewConfiguration()
    // 设置WKWebViewConfiguration的WKPreferences
    configuration.preferences = preferences
    // 创建WKUserContentController
    let userContentController = WKUserContentController()
    // 配置WKWebViewConfiguration的WKUserContentController
    configuration.userContentController = userContentController
    // 给WKWebView与Swift交互起一个名字:callbackHandler,WKWebView给Swift发消息的时候会用到
    // 此句要求实现WKScriptMessageHandler
    configuration.userContentController.add(self, name: "callbackHandler")    
    // 创建WKWebView
    var webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
    // 让WKWebView翻动有回弹效果
    webView.scrollView.bounces = true
    // 只允许WKWebView上下滚动
    webView.scrollView.alwaysBounceVertical = true
    // 设置代理WKNavigationDelegate
    webView.navigationDelegate = self
    // 返回
    return webView
}()

创建HTML

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no"/>
    </head>
    <body>
        iOS传过来的值:<span id="name"></span>
        <button onclick="responseSwift()">响应iOS</button>
        <script type="text/javascript">
            // 给Swift调用
            function sayHello(name) {
                document.getElementById("name").innerHTML = name
                return "Swift你也好!"
            }
            // 调用Swift方法
            function responseSwift() {
                // 这里的callbackHandler是创建WKWebViewConfiguration是定义的
                window.webkit.messageHandlers.callbackHandler.postMessage("JavaScript发送消息给Swift")
            }
        </script>
    </body>
</html>

两个协议

  • WKNavigationDelegate:判断页面加载完成,只有在页面加载完成后才能在实现 Swift 调用 JavaScript。WKWebView 调用 JavaScript:
// 加载完毕以后执行
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    // 调用JavaScript方法
    webView.evaluateJavaScript("sayHello('WebView你好!')") { (result, err) in
        // result是JavaScript返回的值
        print(result, err)
    }
}
  • WKScriptMessageHandler:JavaScript 调用 Swift 时需要用到协议中的一个方法来。JavaScript 调用 WKWebView:
// Swift方法,可以在JavaScript中调用
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print(message.body)
}

ViewController

class ViewController: UIViewController {
    // 懒加载WKWebView
    ...

    // 加载本地html
    let html = try! String(contentsOfFile: Bundle.main.path(forResource: "index", ofType: "html")!, encoding: String.Encoding.utf8)

    override func viewDidLoad() {
        super.viewDidLoad()
        // 标题
        title = "WebView与JavaScript交互"
        // 加载html
        webView.loadHTMLString(html, baseURL: nil)
        view.addSubview(webView)
    }
}

// 遵守两个协议
extension ViewController: WKNavigationDelegate, WKScriptMessageHandler {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        ...
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        ...
    }
}

SFSafariViewController

  • iOS 9 推出的一种 UIViewController,用于加载与显示 Web 内容,打开效果类似 Safari 浏览器的效果。
  • 使用时必须导入SafariServices模块。
import SafariServices

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        showSafariViewController()
    }

    func showSafariViewController() {
        // URL
        let url = URL(string: "https://www.baidu.com")
        // 创建SFSafariViewController
        let sf = SFSafariViewController(url: url!)
        // 设置代理
        sf.delegate = self
        // 显示
        present(sf, animated: true, completion: nil)
    }
}

extension ViewController: SFSafariViewControllerDelegate {
    // 点击左上角的完成(done)
    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        print(#function)
    }

    // 加载完成
    func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
        print(#function)
    }
}