Swift URLSession应用:网络新闻案例
一、 前言
纯小白新手,总结记录一下学习swift的过程,希望有所帮助
这个新闻页面是我依据B站YanFung大佬完成的,并根据我出现的问题进行了一些个人的添加修改,总结了一下我作为初学者学习开发页面的具体步骤
YanFung大佬: 26 网络新闻案例-界面布局_哔哩哔哩_bilibili
效果图:


二、 API获取
我在聚合数据申请了免费的新闻API来模拟新闻页面,具体申请步骤如下:
2.1 登录并申请API
网址:天聚地合 - 聚合数据_API接口开放平台_API接口大全_免费API数据接口服务
成功登录后,搜索我们需要的API,我使用的是新闻头条这个API:

【选择免费的50次】->【申请】
由于我已经申请过了,所以显示的选项不太一下,没有申请过的页面长这样

2.2 使用我们的API
申请成功后,在【个人中心】--【数据中心】--【我的API】找到刚刚申请的API,以及我们专属的【APPKey】

点击【测试】查看详细信息

点击【发送请求】可以查看请求结果

点击【接口文档】,查看开发时需要用到的【接口说明】、【请求参数】、【返回参数】
注意:编写Model页面时,Model内的参数名称、参数数量、参数类型必须与下图返回结果中的参数完全一致,否则无法成功获取数据



成功获取了我们开发时需要用到的API,正式开始页面开发吧
三、技术简介
3.1 组件
涉及到的一些知识有:UITableView、URLSession网络请求、WKWebView、Codable等等
涉及到的第三方组件是Kingfisher,用来传输新闻中的图片
3.2 架构模式
使用最经典的MVC架构模式,也是UIKit 框架的默认设计模式 。
MVC模式的组成:
- Model :数据模型(NewsModel.swift),负责存储数据和业务逻辑(如校验、计算)。
- View :视图(NewsTableViewCell.swift),负责界面展示,不处理业务逻辑。
- Controller :控制器(NewsViewController),协调 Model 与 View,接收用户输入(View 事件),调用 Model 处理数据,更新 View 展示。
开始具体实现
四、Model --- 【NewsModel】页面
根据API文档中的内容,构造结构体,并分别初始化
4.1 最外层(NewsModel)
使用Apifox工具查看的更清楚一点:

代码实现,分别对应上述最外层的参数:
struct NewsModel: Codable {
var reason:String?
var result:Results?
var error_code:Int?
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.reason = try container.decodeIfPresent(String.self, forKey: .reason)
self.result = try container.decodeIfPresent(Results.self, forKey: .result)
self.error_code = try container.decodeIfPresent(Int.self, forKey: .error_code)
}
}
4.2 Result

代码实现,对应NewsModel里的result参数:
struct Results:Codable{
var stat:String?
var data:[DataItem]?
//初始化
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.stat = try container.decodeIfPresent(String.self, forKey: .stat)
self.data = try container.decodeIfPresent([DataItem].self, forKey: .data)
}
}
4.3 DataItem

代码实现,对应Result里的data参数:
struct DataItem:Codable{
var uniquekey:String?
var title:String?
var date:String?
var category:String?
var author_name:String?
var url:String?
var thumbnail_pic_s:String?
var thumbnail_pic_s02:String?
var thumbnail_pic_s03:String?
var is_content:Int?
//初始化
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uniquekey = try? container.decodeIfPresent(String.self, forKey: .uniquekey)
self.title = try? container.decodeIfPresent(String.self, forKey: .title)
self.date = try? container.decodeIfPresent(String.self, forKey: .date)
self.category = try? container.decodeIfPresent(String.self, forKey: .category)
self.author_name = try? container.decodeIfPresent(String.self, forKey: .author_name)
self.url = try? container.decodeIfPresent(String.self, forKey: .url)
self.thumbnail_pic_s = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s)
self.thumbnail_pic_s02 = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s02)
self.thumbnail_pic_s03 = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s03)
self.is_content = try? container.decodeIfPresent(Int.self, forKey: .is_content)
}
}
4.4 Model模块完整实现
import UIKit
struct NewsModel: Codable {
var reason:String?
var result:Results?
var error_code:Int?
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.reason = try container.decodeIfPresent(String.self, forKey: .reason)
self.result = try container.decodeIfPresent(Results.self, forKey: .result)
self.error_code = try container.decodeIfPresent(Int.self, forKey: .error_code)
}
}
struct Results:Codable{
var stat:String?
var data:[DataItem]?
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.stat = try container.decodeIfPresent(String.self, forKey: .stat)
self.data = try container.decodeIfPresent([DataItem].self, forKey: .data)
}
}
struct DataItem:Codable{
var uniquekey:String?
var title:String?
var date:String?
var category:String?
var author_name:String?
var url:String?
var thumbnail_pic_s:String?
var thumbnail_pic_s02:String?
var thumbnail_pic_s03:String?
var is_content:Int?
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uniquekey = try? container.decodeIfPresent(String.self, forKey: .uniquekey)
self.title = try? container.decodeIfPresent(String.self, forKey: .title)
self.date = try? container.decodeIfPresent(String.self, forKey: .date)
self.category = try? container.decodeIfPresent(String.self, forKey: .category)
self.author_name = try? container.decodeIfPresent(String.self, forKey: .author_name)
self.url = try? container.decodeIfPresent(String.self, forKey: .url)
self.thumbnail_pic_s = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s)
self.thumbnail_pic_s02 = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s02)
self.thumbnail_pic_s03 = try? container.decodeIfPresent(String.self, forKey: .thumbnail_pic_s03)
self.is_content = try? container.decodeIfPresent(Int.self, forKey: .is_content)
}
}
五、View --- 【NewsTableViewCell】类
然后我们来构造View视图,设置NewsTableView的单元格类,在这里要提前部署好第三方库Kingfisher
5.1 初始化相关变量参数
每个cell中涉及到四个参数,分别是
- thumbnail_pic_s:新闻的第一张图片,用于在TableView页面展示
- title:新闻的标题
- author_name:作者的姓名
- date:新闻发表时间
注意:在设置变量名时尽量与API接口文档中的参数名一致
代码实现,参数的一些UI设置可自行设置
//图片ImageView
lazy var thumbnail_pic_s: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 5
return imageView
}()
//标题Label
lazy var title: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = .black
return label
}()
//作者名字Label
lazy var author_name: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .black
return label
}()
//发表日期Label
lazy var date: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .black
return label
}()
5.2 UI布局
使用自动布局对各组件进行约束布局
func setupViews(){
contentView.addSubview(thumbnail_pic_s)
contentView.addSubview(title)
contentView.addSubview(date)
contentView.addSubview(author_name)
thumbnail_pic_s.translatesAutoresizingMaskIntoConstraints = false
title.translatesAutoresizingMaskIntoConstraints = false
date.translatesAutoresizingMaskIntoConstraints = false
author_name.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
thumbnail_pic_s.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
thumbnail_pic_s.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
thumbnail_pic_s.heightAnchor.constraint(equalToConstant: 70),
thumbnail_pic_s.widthAnchor.constraint(equalToConstant: 70),
title.topAnchor.constraint(equalTo: thumbnail_pic_s.topAnchor),
title.leadingAnchor.constraint(equalTo: thumbnail_pic_s.trailingAnchor, constant: 15),
title.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
author_name.leadingAnchor.constraint(equalTo: title.leadingAnchor),
author_name.bottomAnchor.constraint(equalTo: thumbnail_pic_s.bottomAnchor),
date.trailingAnchor.constraint(equalTo: title.trailingAnchor),
date.bottomAnchor.constraint(equalTo: author_name.bottomAnchor)
])
}
5.3 重写相关方法
- 单元格被选中时有高亮效果,确保父类(NewsTableViewCell)的默认选中行为(如高亮效果)先执行
override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } - 初始化cell,调用UI布局方法setupViews()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupViews() } - 必需初始化方法, 我们的Cell 完全通过代码创建(不使用 Storyboard/XIB),直接抛出错误(如示例代码),因为这种情况不会被调用。
-
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
5.4 填充单元格
使用Kingfisher请求网页中的第一张图片,作为 TableView 界面的展示图片,再分别将title、author_name、date参数赋值给相对应的变量
func configure(item:DataItem){
if let url = URL(string: item.thumbnail_pic_s ?? " "){
thumbnail_pic_s.kf.setImage(with: url)
}
title.text = item.title
author_name.text = item.author_name
date.text = item.date
}
5.5 完整代码
import UIKit
import Kingfisher
class NewsTableViewCell: UITableViewCell {
lazy var thumbnail_pic_s: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 5
return imageView
}()
lazy var title: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = .black
return label
}()
lazy var author_name: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .black
return label
}()
lazy var date: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .black
return label
}()
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews(){
contentView.addSubview(thumbnail_pic_s)
contentView.addSubview(title)
contentView.addSubview(date)
contentView.addSubview(author_name)
thumbnail_pic_s.translatesAutoresizingMaskIntoConstraints = false
title.translatesAutoresizingMaskIntoConstraints = false
date.translatesAutoresizingMaskIntoConstraints = false
author_name.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
thumbnail_pic_s.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
thumbnail_pic_s.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
thumbnail_pic_s.heightAnchor.constraint(equalToConstant: 70),
thumbnail_pic_s.widthAnchor.constraint(equalToConstant: 70),
title.topAnchor.constraint(equalTo: thumbnail_pic_s.topAnchor),
title.leadingAnchor.constraint(equalTo: thumbnail_pic_s.trailingAnchor, constant: 15),
title.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
author_name.leadingAnchor.constraint(equalTo: title.leadingAnchor),
author_name.bottomAnchor.constraint(equalTo: thumbnail_pic_s.bottomAnchor),
date.trailingAnchor.constraint(equalTo: title.trailingAnchor),
date.bottomAnchor.constraint(equalTo: author_name.bottomAnchor),
])
}
func configure(item:DataItem){
if let url = URL(string: item.thumbnail_pic_s ?? " "){
thumbnail_pic_s.kf.setImage(with: url)
}
title.text = item.title
author_name.text = item.author_name
date.text = item.date
}
}
六、Controller --- 【NewsViewController】类
最后我们构造NewsViewController类,协调Model与View
6.1 UI设置
- 初始化类型为UITableView的变量newsTableView,并显示在视图
- 定义一个类型为[DataItem]的data数组,用于在网络请求中接受数据
lazy var newsTableView: UITableView = { let tableView = UITableView(frame: view.bounds, style: .plain) tableView.dataSource = self //遵循代理 tableView.delegate = self //注册单元格 tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: "NewsTableViewCell") tableView.backgroundColor = .systemGroupedBackground return tableView }() private func setupViews(){ view.addSubview(newsTableView) } var data:[DataItem]?
6.2 网络请求
创建一个getNewsData方法进行网络请求
注意:URL中的key就是聚合中获取的AppKey

代码实现:
func getNewsData(){
//这里替换为自己的key
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=1dfa14f092*******358072acf15af2")
let urlRequest = URLRequest(url:url!)
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: urlRequest){(data,response,error) in
let decoder = JSONDecoder()
if let data = data{
do{
print(data)
let newsModel = try decoder.decode(NewsModel.self, from: data)
//在主线程刷新UI
DispatchQueue.main.async {
self.data = newsModel.result?.data
self.newsTableView.reloadData() //刷新tableview
}
}
catch{
print(error)
}
}
}
task.resume() //启动任务
}
6.3 添加代理
NewsViewController遵循两个代理:UITableviewDelegate,UITableViewDataSource。添加下面的几个代理方法
- numberOfRowsInSection:设置单元格数量,获取data数据后,将data.count设置为单元格的数量
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let data = self.data{ return data.count } return 0 } - cellForRowAt:设置单元格,cell为NewsTableViewCell单元格类,定义一个dataItem变量,获取每个cell的具体数据,将dataItem获取的数据填充到每个cell中
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell:NewsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as! NewsTableViewCell let dataItem = self.data![indexPath.row] cell.configure(item: dataItem) return cell } - heightForRowAt:自定义单元格高度,这里设置每个单元格高度为110
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 110 } - didSelectRowAt:单元格点击事件,点击单元格后跳转到新闻详情页面,并将详情页面(NewsDetailsViewController)的标题设置为新闻标题;将dataItem的数据传给nDsVC.selectedItem
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let dataItem = self.data![indexPath.row ] let nDsVC = NewsDetailsViewController() nDsVC.selectedItem = dataItem self.navigationController?.pushViewController(nDsVC, animated: true) }
6.4 完整代码
import UIKit
import Foundation
import Kingfisher
class NewsViewController: UIViewController {
lazy var newsTableView: UITableView = {
let tableView = UITableView(frame: view.bounds, style: .plain)
tableView.dataSource = self
tableView.delegate = self
tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: "NewsTableViewCell")
tableView.backgroundColor = .systemGroupedBackground
return tableView
}()
private func setupViews(){
view.addSubview(newsTableView)
}
var data:[DataItem]?
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
getNewsData()
}
func getNewsData(){
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=1dfa14f*********58072acf15af2")
let urlRequest = URLRequest(url:url!)
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: urlRequest){(data,response,error) in
let decoder = JSONDecoder()
if let data = data{
do{
print(data)
let newsModel = try decoder.decode(NewsModel.self, from: data)
DispatchQueue.main.async {
self.data = newsModel.result?.data
self.newsTableView.reloadData() //刷新tableview
}
}
catch{
print(error)
}
}
}
task.resume()
}
}
extension NewsViewController:UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let data = self.data{
return data.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:NewsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "NewsTableViewCell", for: indexPath) as! NewsTableViewCell
let dataItem = self.data![indexPath.row]
cell.configure(item: dataItem)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 110
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let dataItem = self.data![indexPath.row ]
let nDsVC = NewsDetailsViewController()
nDsVC.selectedItem = dataItem
self.navigationController?.pushViewController(nDsVC, animated: true)
}
}
七、新闻详情页面
点击TableView中的单元格后,跳转到新闻详情页面,这是一个网页,我们用WKWebView实现。
7.1 UI设置
- 定义一个类型为DataItem的变量selectedItem,用于从NewsViewController页面接收数据
- 初始化一个WKWebView页面,布局范围为真个页面视图
- 将页面标题设置为新闻标题
var selectedItem:DataItem? lazy var wkWebView: WKWebView = { let web = WKWebView(frame: view.frame) return web }() override func viewDidLoad() { super.viewDidLoad() self.title = selectedItem?.title getsNewsData() }
7.2 网络请求
- 创建getsNewsData方法进行网络请求
- url就是我们获取的数据中的url
- 加载页面
func getsNewsData(){ let url = URL(string: (selectedItem?.url)!) let request = URLRequest(url: url!) wkWebView.load(request) view.addSubview(wkWebView) }
7.3 完整实现
import UIKit
import WebKit
class NewsDetailsViewController: UIViewController {
var selectedItem:DataItem?
lazy var wkWebView: WKWebView = {
let web = WKWebView(frame: view.frame)
return web
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = selectedItem?.title
getsNewsData()
}
func getsNewsData(){
let url = URL(string: (selectedItem?.url)!)
let request = URLRequest(url: url!)
wkWebView.load(request)
view.addSubview(wkWebView)
}
}
八、总结
作为新手菜鸟,我尽量细致的记录了这个简单网络新闻界面的实现方法流程,但仍存在许多不足,希望各位大佬热心建议
更多推荐
所有评论(0)