一、 前言

纯小白新手,总结记录一下学习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模式的组成:

  1. Model :数据模型(NewsModel.swift),负责存储数据和业务逻辑(如校验、计算)。
  2. View :视图(NewsTableViewCell.swift),负责界面展示,不处理业务逻辑。
  3. 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中涉及到四个参数,分别是

  1. thumbnail_pic_s:新闻的第一张图片,用于在TableView页面展示
  2. title:新闻的标题
  3. author_name:作者的姓名
  4. 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 重写相关方法
  1. 单元格被选中时有高亮效果,确保父类(NewsTableViewCell)的默认选中行为(如高亮效果)先执行
        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)
        }
  2. 初始化cell,调用UI布局方法setupViews()
      override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setupViews()
        }
  3. 必需初始化方法, 我们的Cell 完全通过代码创建​(不使用 Storyboard/XIB),​直接抛出错误​(如示例代码),因为这种情况不会被调用。
  4. 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设置
  1. 初始化类型为UITableView的变量newsTableView,并显示在视图
  2. 定义一个类型为[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。添加下面的几个代理方法

  1. numberOfRowsInSection:设置单元格数量,获取data数据后,将data.count设置为单元格的数量
       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            if let data = self.data{
                return data.count
            }        
            return 0
        }
  2. 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
        }
  3. heightForRowAt:自定义单元格高度,这里设置每个单元格高度为110
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 110
        }
  4. 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设置
  1. 定义一个类型为DataItem的变量selectedItem,用于从NewsViewController页面接收数据
  2. 初始化一个WKWebView页面,布局范围为真个页面视图
  3. 将页面标题设置为新闻标题
        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  网络请求
  1. 创建getsNewsData方法进行网络请求
  2. url就是我们获取的数据中的url
  3. 加载页面
        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)
    }
}

八、总结

作为新手菜鸟,我尽量细致的记录了这个简单网络新闻界面的实现方法流程,但仍存在许多不足,希望各位大佬热心建议

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐