Skip to content

武汉大学移动编程技术(swift)期末作业,新闻阅读APP

Notifications You must be signed in to change notification settings

weivwang/Read_News

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 

Repository files navigation

*【实验目的】**:*

设计一个新闻阅读APP,实现的主要功能有:

\1. LaunchScreen启动页图片中有学号和姓名

\2. Login登录界面,用户验证无需网络,可在程序代码中写入,用学号作为用户名,密码123可登录,用户登录成功后将用户名写入UserDefaults,下次登录无需再输入用户名。

\3. 主界面有两个tab,一个是用于新闻浏览,一个显示收藏的新闻。

\4. 新闻浏览页功能包括:

(1) 加载时访问提供的新闻API,获取返回的新闻信息,参数page是页码,count是新闻数;

(2) 新闻浏览显示用TableViewController,某条新闻的cell需呈现标题、日期和图片;

(3) 选择某条新闻后,打开有WKWebView控件的页面呈现该url的新闻,并可收藏此新闻,收藏后在收藏tab上显示已收藏的数量;

\5. 收藏页功能包括:

(1) 所有收藏的新闻列表(收藏的新闻存放在本地Sqlite数据库中);

(2) 可以对该列表中的收藏进行删除;

(3) 可通过标题查询收藏的新闻;

(4) 点击收藏的新闻可呈现新闻内容

*【实验环境】(使用的软件)**:*

Xcode Version 12.4

IOS Stimulators - iPhone 11

*【**参考资料**】**:*

《精通IOS开发(第8版)》

高建华老师上课录制视频及课后作业

*【实验方案设计】**:*

\1. 开屏动画(LaunchScreening)

开屏界面使用建项目时自带的LaunchScreen.storyboard文件进行设计,背景图片使用星空山河图,在界面上添加了3个Label分别用来显示:欢迎标语,账号信息和密码信息。设计界面如下:

img

运行界面如下:

img

2,登录界面

登录界面的设计使用系统创建的Main.storyboard进行全部视图的设计,将与ViewController类绑定的界面作为登录界面,勾选is Initial View Controller,表明系统第一次进入显示登录界面。

登录界面如图:

img

Main.storyboard设计方案:

img

登录信息验证模块:

将登录按钮按住control键拖拽到对应的viewcontroller类中,绑定点击事件,代码如下

@IBAction func loginPressed(_ sender: Any) {

​ let usercode = loginTextField.text! ​ let psw = passwordTextField.text! ​ loginTextField.resignFirstResponder() ​ passwordTextField.resignFirstResponder() ​
​ if((usercode == "2019302110426" || loginTextField.text == "2019302110426") && psw == "123"){ ​ let mainBoard:UIStoryboard! = UIStoryboard(name: "Main", bundle: nil) ​ let VCMain = mainBoard!.instantiateViewController(withIdentifier: "vcMain") ​ UIApplication.shared.windows[0].rootViewController = VCMain ​ UserDefaults.standard.set("2019302110426", forKey: "usercode") ​
​ }else{ ​ let p = UIAlertController(title: "登录失败", message: "用户名或密码错误", preferredStyle:.alert) ​ p.addAction(UIAlertAction(title: "确定", style: .default, handler: { ​ (act:UIAlertAction) in self.passwordTextField.text = "" ​ })) ​ present(p, animated: false, completion: nil) ​ } }

登录成功时,执行: let mainBoard:UIStoryboard! = UIStoryboard(name: "Main", bundle: nil) let VCMain = mainBoard!.instantiateViewController(withIdentifier: "vcMain") UIApplication.shared.windows[0].rootViewController = VCMain

来跳转到TableBarViewController

同时使用:UserDefaults.standard.set("2019302110426", forKey: "usercode")

来将学号信息设为默认配置。

若登录失败,则弹出UIAlertController来提示用户用户名或密码错误,效果如下:

img

密码校验成功后,进入主界面。

3 主界面

主界面整体由一个TabBarViewController进行组织,包含3个页面:新闻,收藏和开发者。

分别用来展示新闻列表,收藏新闻列表和开发者信息,整体界面设计图如下:

img

3.1 新闻界面

img

该界面实现细节如下:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // debug print(NewsManager.shared.news.count) return NewsManager.shared.news.count }

返回行数为NewsManager的实例通过API获取到的news的数量。

News类定义如下:

class News:NSObject, Codable { var title:String = "" var path:String = "" var passtime:String = "" var image:String = ""

private enum CodingKeys: String, CodingKey{ case title case path case passtime case image }

init(title:String) { self.title = title }

override var description: String { return "title:(title)" }

Cell的显示内容如下:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell2", for: indexPath)

  //todo init cell
  cell.textLabel?.text = NewsManager.shared.news[indexPath.row].title
  cell.detailTextLabel?.text = NewsManager.shared.news[indexPath.row].passtime

// cell.imageView?.image = UIImage(named: NewsManager.shared.news[indexPath.row].image)

  let iurl = URL(string: NewsManager.shared.news[indexPath.row].image)
  let data = try! Data(contentsOf: iurl!)
  cell.imageView?.image = UIImage(data: data)
  
  //当下拉到底部,执行loadMore()
  if (indexPath.row == NewsManager.shared.news.count-1) {
    loadMore()
  }
  return cell
 }

在storyboard中将cell的style改为Subtitle,设置标题为news数组对应行数的标题

设置下面的详细内容为news的passtime

图片加载中,news[i].image实际是一个String类型的值,本身是一个url,所以使用:let iurl = URL(string: NewsManager.shared.news[indexPath.row].image) let data = try! Data(contentsOf: iurl!) cell.imageView?.image = UIImage(data: data)

来进行加载。

在storyboard界面将cell与下一个界面相连,实现点击cell跳转的功能。

重写prepare方法实现数据的传递:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

  if let dest = segue.destination as? DetailViewController
  {
    dest.news = NewsManager.shared.news[tableView.indexPathForSelectedRow!.row]
  }
}

当点击新闻cell后,会将所选中的行数对应的news[]数组里面的News对象传递给DetailViewController类

在DetailViewController中,使用WKWebView控件来加载网页新闻

DetailViewController.swift代码如下:

import UIKit import WebKit

class DetailViewController: UIViewController { var news:News?

override func viewDidLoad() { super.viewDidLoad() guard let path = news?.path else {return}

guard let url = URL(string: path) else { return }
let urlRequest = URLRequest(url: url) 
self.DetailNews.load(urlRequest)

​ // Do any additional setup after loading the view. }

@IBOutlet weak var DetailNews: WKWebView!

@IBAction func saveButtonTapped(_ sender: Any) { ​
​ //?? "" 代表默认值为空,是跟着提示走的 ​ NewsDAL.saveNews(title: news!.title, url: news?.path ?? "",passtime:news?.passtime ?? "", image:news?.image ?? "") ​ //debug ​ let sqlite = SQLiteManager.sharedInstance ​ if !sqlite.openDB() {return} ​ let sql = "SELECT * FROM newstable;" ​ let queryresult = sqlite.execQuerySQL(sql: sql) ​ print(queryresult ?? "") ​ let p = UIAlertController(title: "成功", message: "收藏成功", preferredStyle:.alert) ​ p.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) ​ present(p, animated: false, completion: nil) }

各部分功能如下:

@IBOutlet weak var DetailNews: WKWebView! 用来注册控件

guard let path = news?.path else {return} guard let url = URL(string: path) else { return } let urlRequest = URLRequest(url: url) self.DetailNews.load(urlRequest)

使用上个界面传递过来的News实例的path进行新闻加载。

在界面中,我设置了一个收藏按钮,用于进行新闻收藏,当点击按钮,会调用NewsDAL的saveNews方法,将该新闻的title,passtime,path,和image插入数据库。

并会弹出提示框用于显示是否收藏成功。显示该界面显示效果如下:

img

点击收藏新闻后,收藏成功提示:

img

3.2 收藏界面:

在新闻浏览界面收藏的新闻会显示到收藏界面,因为收藏界面与数据库息息相关,所以完成收藏界面之前,需要先完成数据库部分。

数据库设计

数据库部分主要包含两个文件:SQLiteManager.swift和NewsDAL.swift

SQLiteManager.swift内容如下:

基本都是参考老师上课代码实现,用于执行sql语句。

import Foundation

class SQLiteManager:NSObject { private var a:Int = 0 private var dbPath:String! private var database:OpaquePointer? = nil

static var sharedInstance:SQLiteManager { return SQLiteManager() }

override init() { super.init() let dirPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! dbPath = dirPath.appendingPathComponent("app.sqlite").path

}

//open database

func openDB() -> Bool { let result = sqlite3_open(dbPath, &database) if result != SQLITE_OK { print("fail to open database") return false } return true }

//close database func closeDB(){ sqlite3_close(database) }

//execute the statement:select func execQuerySQL(sql:String)->[[String:AnyObject]]?{ let cSql = sql.cString(using: String.Encoding.utf8)! var statement:OpaquePointer? = nil if sqlite3_prepare_v2(database, cSql, -1, &statement, nil) != SQLITE_OK{ sqlite3_finalize(statement) print("执行(sql)错误\n") let errmsg = sqlite3_errmsg(database) if errmsg != nil{ print(errmsg!) }

  return nil
}

var rows = [[String:AnyObject]]()

while sqlite3_step(statement) == SQLITE_ROW{
  rows.append(record(stmt:statement!))
}

sqlite3_finalize(statement)

return rows

}

private func record(stmt:OpaquePointer)->[String:AnyObject]{ var row = String:AnyObject

for col in 0 ..< sqlite3_column_count(stmt){
  let cName = sqlite3_column_name(stmt, col)
  let name = String(cString:cName!,encoding: String.Encoding.utf8)
  
  var value:AnyObject?
  
  switch (sqlite3_column_type(stmt, col)) {
  case SQLITE_FLOAT:
    value = sqlite3_column_double(stmt, col) as AnyObject
  case SQLITE_INTEGER:
    value = Int(sqlite3_column_int(stmt, col)) as AnyObject
  case SQLITE_TEXT:
    let cText = sqlite3_column_text(stmt, col)
    value = String.init(cString: cText!) as AnyObject
  case SQLITE_NULL:
    value = NSNull()
  default:
    a+=1
  }
  row[name!] = value ?? NSNull()
}
return row

}

//execute the statement:create,insert,update,delete func exeNoneQuery(sql:String) -> Bool{

var errMsg:UnsafeMutablePointer<Int8>? = nil
let cSql = sql.cString(using: String.Encoding.utf8)

if sqlite3_exec(database, cSql, nil, nil, &errMsg) == SQLITE_OK{
  return true
}
let msg = String.init(cString: errMsg!)
print(msg)
return false

} }

我又新建了NewsDAL类,用于建表,和封装收藏和取消收藏的功能

代码如下:

import Foundation class NewsDAL{ static func initDB(){ let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB() {return}

//这里存image用的是TEXT类型,因为News类中的image是string类型,实际上是一个url
//在tableview加载图片时,使用的是如下方法:
//let iurl = URL(string: NewsManager.shared.news[indexPath.row].image)
//let data = try! Data(contentsOf: iurl!)
//cell.imageView?.image = UIImage(data: data)

let createNews = "CREATE TABLE IF NOT EXISTS newstable('title' TEXT NOT NULL PRIMARY KEY,'url' TEXT,'passtime' TEXT,'image' TEXT);"
let result = sqlite.exeNoneQuery(sql: createNews)
print("初始化结果:\(result)")
return

} static func saveNews(title:String,url:String,passtime:String,image:String){ let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB(){return} let sql = "INSERT OR REPLACE INTO newstable(title,url,passtime,image) VALUES('(title)','(url)','(passtime)','(image)'); " let result = sqlite.execQuerySQL(sql: sql) print("添加结果:(String(describing: result))") sqlite.closeDB() return } static func deleteNews(title:String){ let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB() {return} let sql = "DELETE FROM newstable WHERE title = '(title)';" let result = sqlite.exeNoneQuery(sql: sql) print("删除结果:(result)") sqlite.closeDB() return } }

主要包含3个静态方法:initDB():用于初始化数据库,建表。其中,我设置了4个表项:分别是title,url,passtime,image。

saveNews(title:String,url:String,passtime:String,image:String):用于将收藏新闻插入数据库

点击收藏按钮后调用该方法。

deleteNews(title:String):用于取消收藏时,将新闻从数据库中删除。

收藏界面如图:

img

这里收藏界面存在bug,是图片无法显示。Debug发现是查询数据库的image结果为空,但将queryresult输出发现image字段不为空,可能是在解包或赋值image变为空了,但碍于时间原因,且该效果不影响实际使用,所以没有进一步解决。

该界面实现细节如下:

override func viewDidLoad() { super.viewDidLoad() let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB() {return} queryResult = sqlite.execQuerySQL(sql: "SELECT * FROM newstable;") sqlite.closeDB() initSearch()

setRefreshView()
loadMore()

}

初始化界面时,将queryresult赋值为查询结果。

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB() {return 0} queryResult = sqlite.execQuerySQL(sql: "SELECT * FROM newstable;") sqlite.closeDB() return queryResult!.count }

返回cell的数量为查询结果的数量。

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows let sqlite = SQLiteManager.sharedInstance if !sqlite.openDB() {return 0} queryResult = sqlite.execQuerySQL(sql: "SELECT * FROM newstable;") sqlite.closeDB() return queryResult!.count }

cell的内容加载如下:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "news", for: indexPath)

cell.textLabel?.text = queryResult?[indexPath.row]["title"]! as? String
resultViewController.allNewsTitle.append(cell.textLabel?.text ?? " ")
cell.detailTextLabel?.text = queryResult?[indexPath.row]["passtime"]! as? String
let iurl = URL(string: (queryResult?[indexPath.row]["url"]! as? String)!)
let data = try! Data(contentsOf: iurl!)
cell.imageView?.image = UIImage(data: data)
return cell

}

重写prepare方法,实现数据传递:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

if let dest = segue.destination as? CollectDetailViewController
{
  let sqlite = SQLiteManager.sharedInstance
  if !sqlite.openDB() {return}
  queryResult = sqlite.execQuerySQL(sql: "SELECT * FROM newstable;")
  sqlite.closeDB()
  print(queryResult ?? " ")
  
  if let indexPath = tableView.indexPathForSelectedRow{
    dest.newstitle = (queryResult?[indexPath.row]["title"]! as? String)!
    //print(queryResult?[indexPath.row]["title"]! as? String)
    print((queryResult?[indexPath.row]["title"]! as? String)!)
    //print(dest.title)
    dest.newspath = (queryResult?[indexPath.row]["url"]! as? String)!
  dest.newspasstime = (queryResult?[indexPath.row]["passtime"]! as? String)!
  dest.newsimage = (queryResult?[indexPath.row]["image"]! as? String)!
  }
}

}

根据标题搜索功能:

img

实现代码如下:

import UIKit

class SearchResultTableViewController: UITableViewController,UISearchResultsUpdating {

var allNewsTitle:[String] = [] var filterNewsTitle:[String] = []

override func viewDidLoad() { super.viewDidLoad()

tableView.register(UITableViewCell.self, forCellReuseIdentifier: "news")

let sqlite = SQLiteManager.sharedInstance
if !sqlite.openDB() {return}
let sql = "SELECT * FROM newstable;"
let queryresult = sqlite.execQuerySQL(sql: sql)
print(queryresult ?? "")

​ }

// MARK: - Table view data source

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return filterNewsTitle.count }

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "news", for: indexPath)

​ cell.textLabel?.text = filterNewsTitle[indexPath.row] ​ // Configure the cell...

​ return cell }

在CollectViewController中写了如下方法:

func initSearch() {

searchcontroller = UISearchController(searchResultsController: resultViewController)
let searchBar = searchcontroller.searchBar
searchBar.placeholder = "输入新闻标题"
searchBar.sizeToFit()
searchBar.scopeButtonTitles = ["标题"]
tableView.tableHeaderView = searchBar
searchcontroller.searchResultsUpdater = resultViewController
self.definesPresentationContext = true

}

在初始化界面调用,用于将搜索框显示在页面上。

点击收藏界面的新闻cell,同样可以进入对应的新闻界面:

img

该界面,我设置了取消收藏按钮,当点击取消按钮,会调用NewsDAL.deleteNews(title:String)方法,将该新闻从数据库中删除。并会弹出提示框,提示用户删除成功。显示效果如下:

img

返回收藏界面刷新后,该条新闻被删除:

img

3.3 开发者界面

开发者界面主要是自己diy出来的,完整软件做下来感觉非常有成就感,所以设计了一个开发者界面,感觉很cool。

img

总结下来,还有几点问题没有解决:

1, 收藏界面的图片加载不出来,从数据库中读取到的image为String类型,是一个url,从数据库中读取出来后,使用如下方法进行加载:let iurl = URL(string: (queryResult?[indexPath.row]["url"]! as? String)!) let data = try! Data(contentsOf: iurl!) cell.imageView?.image = UIImage(data: data)

采用是和新闻浏览界面相同的加载方法,debug发现查询结果image字段不为空,但最后加载不出来。

2, 在设计数据库表时,我最初想的是设计四个字段title,url,passtime,image用来存储信息,写到搜索功能时,我的Allnews[]只能存放titile字段,无法组成完整的News对象,导致无法实现搜索结果点击跳转功能。后面考虑应该将整个News对象存入数据库,既简单又能实现上述功能。但碍于时间原因以及其他科目考试压力,未能完成修改。

3, 刷新功能,下滑刷新功能只要刷一次就一直不会消失,这个控件是老师提供的,我也不太了解。

4, 在自己手机上测试时存在闪退情况,且不稳定发生,时有时无。在模拟器上运行没有闪退情况。

5, 第一次进入加载新闻速度较慢。

总体来讲,写的过程中遇到了bug,有时网上也查不出来,关于swift的资料还是比较少,stackoverflow上能提供的资料相对较多,但也有一些是对不上自己的问题,大多数都是靠分析输出报错进行解决,也被迫提高了自己的debug水平。

遇到问题时参考老师录制的视频也很好的给与了灵感和帮助。

而且整个过程下来,能够明显感觉到将课上所学习的知识运用了起来,自己做完的软件,能运行起来是一件非常又成就感的事情。

About

武汉大学移动编程技术(swift)期末作业,新闻阅读APP

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published