来来来,继续作业系列的博客,老规矩了,直接上要求
这次作业涉及到iOS开发中最常用的一个UI组件UITableView,App Store中的几乎所有工具型和应用型App都使用了UITableView,而初学UITableView又不是那么容易,因为它涉及到很多代理和数据源的问题,但用熟练了,会觉得它非常地方便。
废话不多说了,先来完成要求吧,在这个过程中,博主会讲解一些细节方面的东西。
首先是需要用到作业3中的类,点击这里,看看细节。我这里就不多说了,直接把全部代码贴出来吧。各位最好新建一个文件来放这个类,不然会使ViewController显得非常臃肿。
Person.swift
import Foundation
//性别的枚举
enum Gender: Int {
case male //男性
case female //女性
case unknow //未知
//重载>操作符,方便后面排序使用
static func >(lhs: Gender, rhs: Gender) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
//公寓的枚举
enum Department {
case one, two, three
}
//学校协议
protocol SchoolProtocol {
var department: Department { get set }
func lendBook()
}
//人类
class Person: CustomStringConvertible {
var firstName: String //姓
var lastName: String //名
var age: Int //年龄
var gender: Gender //性别
var fullName: String { //全名
get {
return firstName + lastName
}
}
//构造方法
init(firstName: String, lastName: String, age: Int, gender: Gender) {
self.firstName = firstName
self.lastName = lastName
self.age = age
self.gender = gender
}
convenience init(firstName: String, age: Int, gender: Gender) {
self.init(firstName: firstName, lastName: "", age: age, gender: gender)
}
convenience init(firstName: String) {
self.init(firstName: firstName, age: 0, gender: Gender.unknow)
}
required convenience init() {
self.init(firstName: "")
}
//重载==
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.fullName == rhs.fullName && lhs.age == rhs.age && lhs.gender == rhs.gender
}
//重载!=
static func !=(lhs: Person, rhs: Person) -> Bool {
return !(lhs == rhs)
}
//实现CustomStringConvertible协议中的计算属性,可以使用print直接输出对象内容
var description: String {
return "fullName: \(self.fullName), age: \(self.age), gender: \(self.gender)"
}
//输出Person XXX is running
func run() {
print("Person \(self.fullName) is running")
}
}
//教师类
class Teacher: Person, SchoolProtocol {
var title: String //标题
var department: Department //公寓
//构造方法
init(title: String, firstName: String, lastName: String, age: Int, gender: Gender, department: Department) {
self.title = title
self.department = department
super.init(firstName: firstName, lastName: lastName, age: age, gender: gender)
}
init(title: String, department: Department) {
self.title = title
self.department = department
super.init(firstName: "", lastName: "", age: 0, gender: .unknow)
}
convenience required init() {
self.init(title: "", department: Department.one)
}
//重写父类的计算属性
override var description: String {
return "title: \(self.title), fullName: \(self.fullName), age: \(self.age), gender: \(self.gender), department: \(self.department)"
}
//重载父类run方法
override func run() {
print("Teacher \(self.fullName) is running")
}
//遵循协议的方法
func lendBook() {
print("Teacher \(self.fullName) lend a book")
}
}
//学生类
class Student: Person, SchoolProtocol {
var stuNo: Int //学号
var department: Department //公寓
//构造方法
init(stuNo: Int, firstName: String, lastName: String, age: Int, gender: Gender, department: Department) {
self.stuNo = stuNo
self.department = department
super.init(firstName: firstName, lastName: lastName, age: age, gender: gender)
}
convenience init(stuNo: Int, firstName: String, lastName: String, age: Int, gender: Gender) {
self.init(stuNo: stuNo, firstName: firstName, lastName: lastName, age: age, gender: gender, department: .one)
}
init(stuNo: Int, department: Department) {
self.stuNo = stuNo
self.department = department
super.init(firstName: "", lastName: "", age: 0, gender: Gender.unknow)
}
required convenience init() {
self.init(stuNo: 0, department: .one)
}
//重写父类的计算属性
override var description: String {
return "stuNo: \(self.stuNo), fullName: \(self.fullName), age: \(self.age), gender: \(self.gender), department: \(self.department)"
}
//重载父类run方法
override func run() {
print("Student \(self.fullName) is running")
}
//遵循协议的方法
func lendBook() {
print("Teacher \(self.fullName) lend a book")
}
}
接下来就开始进入主题
为了方便我们对界面的布局,我们使用UINavigationController来放置按钮,所以需要在AppDelegate.swift的application(_:didFinishLaunchingWithOptions:)方法中添加下面这句代码
self.window?.rootViewController = UINavigationController(rootViewController: ViewController())
这句代码的作用就是将UIWindow的根视图控制器设置为UINavigationController并将该导航栏控制器的根视图控制器设置为ViewController。这样我们的ViewController就放置在导航栏控制器中了。
然后我们需要在ViewController中做如下几个属性的声明
//学生数组
var students = [Student]()
//教师数组
var teachers = [Teacher]()
//定义表头数组
var tableTitle = ["Teacher", "Student"]
//定义一个表视图
var table: UITableView!
//右边按钮
var rightItem: UIBarButtonItem!
//弹出框
var alert: UIAlertController!
这些在下面的代码中会使用到,大致的作用看注释应该差不多能理解了。
然后我们需要创建数据,并按要求排序
//生成3个Teacher对象
for i in 1...3 {
let temp = Teacher(title: "教授", firstName: "张", lastName: "\(i)", age: 21, gender: .female, department: .one)
teachers.append(temp)
}
//生成4个Student对象
for i in 1..<5 {
let temp = Student(stuNo: 2015110100 + i, firstName: "李", lastName: "\(i)", age: 19, gender: .male, department: .two)
students.append(temp)
}
//按全名排序
teachers.sort { return $0.fullName < $1.fullName }
students.sort { return $0.fullName < $1.fullName }
在准备好数据之后,我们就需要创建表视图了
//创建表视图,并设置代理和数据源
table = UITableView(frame: self.view.bounds)
table.delegate = self
table.dataSource = self
self.view.addSubview(table)
然后,我们在导航栏控制器上添加两个按钮,左边添加一个“添加”按钮,用于添加学生;右边添加“编辑”按钮,用于对表视图进行编辑操作。
//导航栏控制器右边的按钮
rightItem = UIBarButtonItem(title: "编辑", style: .plain, target: self, action: #selector(edit))
self.navigationItem.rightBarButtonItem = rightItem
//导航栏控制器左边的按钮
let leftItem = UIBarButtonItem(title: "添加", style: .plain, target: self, action: #selector(addStudent))
self.navigationItem.leftBarButtonItem = leftItem
我们添加的“编辑”按钮在开始时声明,是因为我们需要更改它的title属性,当我们进入编辑状态时,需要将它改为“完成”;当它退出编辑状态时,需要将其改为“编辑”。
/// 编辑表视图
@objc func edit() {
if table.isEditing {
rightItem.title = "编辑"
table.isEditing = false
} else {
rightItem.title = "完成"
table.isEditing = true
}
}
“编辑”按钮的功能做完之后,就需要设置“添加”按钮的功能了。按要求需要输入相应的学生信息,我们这里就使用UIAlertController来弹出提示框,让用户输入相应的信息。
/// 添加学生提示框
@objc func addStudent() {
alert = UIAlertController(title: "hh", message: "ss", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.placeholder = "学生学号"
}
alert.addTextField { (textField) in
textField.placeholder = "学生姓"
}
alert.addTextField { (textField) in
textField.placeholder = "学生名"
}
alert.addTextField { (textField) in
textField.placeholder = "学生性别"
}
alert.addTextField { (textField) in
textField.placeholder = "学生年龄"
}
let OKBtn = UIAlertAction(title: "确定", style: .default) { (alert) in
self.add()
}
let cancelBtn = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alert.addAction(OKBtn)
alert.addAction(cancelBtn)
self.present(alert, animated: true, completion: nil)
}
/// 添加学生
func add() {
let no = Int(alert.textFields![0].text!)
let firstName = alert.textFields![1].text!
let lastName = alert.textFields![2].text!
let gender: Gender
switch alert.textFields![3].text! {
case "男":
gender = .male
case "女":
gender = .female
default:
gender = .unknow
}
let age = Int(alert.textFields![4].text!)
let student = Student(stuNo: no!, firstName: firstName, lastName: lastName, age: age!, gender: gender)
students.append(student)
table.reloadData()
}
我们先初始化一个alert型的UIAlertController,除了alert,常用的还有actionSheet,读者可以自行百度区别。然后向alert里面添加了5个UITextField用于用户输入学生信息,然后添加两个按钮,一个“确定”,一个“取消”,功能就不说了。注意UIAlertController上的按钮是UIAlertAction,而不是UIButton。在初始化UIAlertController时可以添加事件在按钮上,我们在“确定”按钮上添加了一个函数,用于将用户填的信息加入到表视图中。最后调用UIViewController的present方法将提示框弹出即可。
在add()方法中,我们将UITextField中的内容一一取出并做相关的处理,即可创建一个学生的实例,然后添加到学生数组中。这时我们只更新的数据,而界面(也就是表视图)并没有更新,所以我们需要让表视图重新加载一次数据。(博主并没有做输入的验证,各位读者可以自行扩展)。
在做完这些基础的操作之后,我们的数据和功能就已经准备完成了,接下来就要进行表视图的操作了。在最开始初始化表视图时,我们指定了其代理和数据源,这是系统定义个两个协议,通过这两个协议,我们可以很方便地对表视图进行操作和调整。所以我们需要在类的声明部分遵循UITableViewDelegate协议和UITableViewDataSource协议。截图如下:
UITableViewDelegate协议没有必须要实现的方法,而UITableViewDataSource中有两个方法必须要实现。
第一个是指定表视图中单元格行数的方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return teachers.count
} else {
return students.count
}
}
因为我们这里有两种不同的数据信息需要展示,所以博主做了两个section(部分),分别返回两个section的行数即可。
第二个是指定表视图中的单元格的方法
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = tableTitle[indexPath.section]
var cell = tableView.dequeueReusableCell(withIdentifier: identifier)
if cell == nil {
let style: UITableViewCellStyle = (identifier == "Teacher") ? .subtitle : .default
cell = UITableViewCell(style: style, reuseIdentifier: identifier)
cell?.accessoryType = .disclosureIndicator
}
switch identifier {
case "Teacher":
cell?.textLabel?.text = teachers[indexPath.row].fullName
cell?.detailTextLabel?.text = teachers[indexPath.row].title
case "Student":
cell?.textLabel?.text = students[indexPath.row].fullName
default:
break
}
return cell!
}
在这个方法中,我们先初始化了一个identifier常量,这个常量用于单元格的重用。在表视图中,往往有很多的单元格,但如果我们每滚动到一个单元格的位置时都要将其初始化的话,这样就会是界面卡顿,并且浪费很多内存,所以我们需要优化一下表视图,而系统刚好就提供了这种优化的方法,那就是注册单元格的重用。根据不同的identifier可以重用不同的单元格,在表视图滚动时,只需要更改单元格展示的数据即可,这样就可以大大的提高效率。
重用的机制是将已经初始化的单元格放入队列中,待需要时将其取出,更新数据。所以我们要使用dequeueReusableCell(withIdentifier:)方法从队列中取出单元格。但我们第一次进入界面时,队列中并没有单元格,所以我们取出的cell会是空的,我们就可以在这里初始化cell,并设置其样式。当cell取出后,就可以将数据展示在上面了,最后返回cell即可。
因为我们这里有两个部分的数据,所以我们需要指定section数
func numberOfSections(in tableView: UITableView) -> Int {
return tableTitle.count
}
然后为了用户方便,我们可以指定每一个section的头,来提示一些信息
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return tableTitle[section]
}
做完这一些操作之后,我们就可以看到界面上展示的效果了。运行程序看看
初始界面
点击“添加”按钮
点击“编辑”按钮
到这里我们发现,我们点击“添加”按钮后输入信息,并点击“确定”,新添加的学生信息可以添加到表视图中,但我们点击编辑按钮后,却删除不了单元格,那是因为我们还缺少一些必要的设置。
首先我们需要使用代理设置每一个单于格的编辑类型,因为我们已经做了添加按钮,所以我们直接指定每一个单元格都是删除
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .delete
}
我们可以更改一下滑动出来的删除按钮显示的文字
func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
return "删除"
}
在做完这些之后,我们还是不能删除单元格,那是因为还需要一个方法来真正的删除数据
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
if indexPath.section == 0 {
teachers.remove(at: indexPath.row)
} else {
students.remove(at: indexPath.row)
}
tableView.deleteRows(at: [indexPath], with: .left)
}
}
在这个方法中,我们需要先判断编辑的类型,当为delete时,先删除section对应的数组中的数据,然后再删除表视图中的cell。删除cell使用deleteRows(at:with:)方法可以指定删除时的动画效果。
到这里,我们就可以删除单元格了。这时我们有了添加和删除的功能,但我们需要实现点击单元格给出选择反馈和移动cell。
首先实现点击单于给出反馈,我们做成点击某个cell后,弹出一个提示框,提示框中显示选中的是学生还是教师,并显示其名字。
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let category = tableTitle[indexPath.section]
let name: String
if indexPath.section == 0 {
name = teachers[indexPath.row].fullName
} else {
name = students[indexPath.row].fullName
}
let message = "you selected \(category), name: \(name)"
let alert = UIAlertController(title: "系统提示", message: message, preferredStyle: .alert)
let OKBtn = UIAlertAction(title: "确定", style: .default, handler: nil)
alert.addAction(OKBtn)
self.present(alert, animated: true, completion: nil)
}
做完这个之后,我们最后就需要实现单元格的移动了
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
if sourceIndexPath.section != destinationIndexPath.section {
tableView.reloadData()
} else {
if sourceIndexPath.section == 0 {
teachers.insert(teachers.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
} else {
students.insert(students.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
}
}
在这个方法中,我们限制了单元格只能在自己的section里面移动,一旦跨section移动,那就会回到原来的位置。我们只要实现了这个方法,就可以将单元格移动了,但相应的,我们需要更改数据数组中相应元素的位置,这样才不会引起错误。
到这里,我们就基本完成了作业的要求,运行看一下效果。
添加学生信息
删除单元格
在Swift4.0之后,我们实现了删除功能之后,可以不需要点击“编辑”按钮进入编辑状态再删除单元格了,我们可以在cell上向左滑动,就会有删除按钮出现
最后是移动单于格了。
我们先进入编辑状态,然后点击右边的三根横线的按钮,就可以拖动单元格了。
到这里,本次作业完成,各位读者如果需要完善功能,可以在代码中进行改进,下面附上这次作业的ViewController的所有代码。
ViewConroller.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//学生数组
var students = [Student]()
//教师数组
var teachers = [Teacher]()
//定义表头数组
var tableTitle = ["Teacher", "Student"]
//定义一个表视图
var table: UITableView!
//右边按钮
var rightItem: UIBarButtonItem!
//弹出框
var alert: UIAlertController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.title = "table"
self.view.backgroundColor = UIColor.white
//生成3个Teacher对象
for i in 1...3 {
let temp = Teacher(title: "教授", firstName: "张", lastName: "\(i)", age: 21, gender: .female, department: .one)
teachers.append(temp)
}
//生成4个Student对象
for i in 1..<5 {
let temp = Student(stuNo: 2015110100 + i, firstName: "李", lastName: "\(i)", age: 19, gender: .male, department: .two)
students.append(temp)
}
//按全名排序
teachers.sort { return $0.fullName < $1.fullName }
students.sort { return $0.fullName < $1.fullName }
//创建表视图,并设置代理和数据源
table = UITableView(frame: self.view.bounds)
table.delegate = self
table.dataSource = self
self.view.addSubview(table)
//导航栏控制器右边的按钮
rightItem = UIBarButtonItem(title: "编辑", style: .plain, target: self, action: #selector(edit))
self.navigationItem.rightBarButtonItem = rightItem
//导航栏控制器左边的按钮
let leftItem = UIBarButtonItem(title: "添加", style: .plain, target: self, action: #selector(addStudent))
self.navigationItem.leftBarButtonItem = leftItem
}
/// 添加学生提示框
@objc func addStudent() {
alert = UIAlertController(title: "hh", message: "ss", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.placeholder = "学生学号"
}
alert.addTextField { (textField) in
textField.placeholder = "学生姓"
}
alert.addTextField { (textField) in
textField.placeholder = "学生名"
}
alert.addTextField { (textField) in
textField.placeholder = "学生性别"
}
alert.addTextField { (textField) in
textField.placeholder = "学生年龄"
}
let OKBtn = UIAlertAction(title: "确定", style: .default) { (alert) in
self.add()
}
let cancelBtn = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alert.addAction(OKBtn)
alert.addAction(cancelBtn)
self.present(alert, animated: true, completion: nil)
}
/// 添加学生
func add() {
let no = Int(alert.textFields![0].text!)
let firstName = alert.textFields![1].text!
let lastName = alert.textFields![2].text!
let gender: Gender
switch alert.textFields![3].text! {
case "男":
gender = .male
case "女":
gender = .female
default:
gender = .unknow
}
let age = Int(alert.textFields![4].text!)
let student = Student(stuNo: no!, firstName: firstName, lastName: lastName, age: age!, gender: gender)
students.append(student)
table.reloadData()
}
/// 编辑表视图
@objc func edit() {
if table.isEditing {
rightItem.title = "编辑"
table.isEditing = false
} else {
rightItem.title = "完成"
table.isEditing = true
}
}
// MARK: delegate
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
return "删除"
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let category = tableTitle[indexPath.section]
let name: String
if indexPath.section == 0 {
name = teachers[indexPath.row].fullName
} else {
name = students[indexPath.row].fullName
}
let message = "you selected \(category), name: \(name)"
let alert = UIAlertController(title: "系统提示", message: message, preferredStyle: .alert)
let OKBtn = UIAlertAction(title: "确定", style: .default, handler: nil)
alert.addAction(OKBtn)
self.present(alert, animated: true, completion: nil)
}
// MARK: data source
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
if indexPath.section == 0 {
teachers.remove(at: indexPath.row)
} else {
students.remove(at: indexPath.row)
}
tableView.deleteRows(at: [indexPath], with: .left)
}
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
if sourceIndexPath.section != destinationIndexPath.section {
tableView.reloadData()
} else {
if sourceIndexPath.section == 0 {
teachers.insert(teachers.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
} else {
students.insert(students.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
}
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return tableTitle.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return tableTitle[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return teachers.count
} else {
return students.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = tableTitle[indexPath.section]
var cell = tableView.dequeueReusableCell(withIdentifier: identifier)
if cell == nil {
let style: UITableViewCellStyle = (identifier == "Teacher") ? .subtitle : .default
cell = UITableViewCell(style: style, reuseIdentifier: identifier)
cell?.accessoryType = .disclosureIndicator
}
switch identifier {
case "Teacher":
cell?.textLabel?.text = teachers[indexPath.row].fullName
cell?.detailTextLabel?.text = teachers[indexPath.row].title
case "Student":
cell?.textLabel?.text = students[indexPath.row].fullName
default:
break
}
return cell!
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}