介绍
UIView 会占用屏幕上一个矩形的空间。
主要处理两件事:画出矩形控件,并处理其中的事件。
UIView 是层级结构,UIView 只有一个父 View,但可以有多个子 View。子 View 的顺序和子 View 返回的数组中的位置有关(storyboard 中左侧的树形结构图中的先后顺序)。
UIView 可以直接在 storyboard 里面拖拽使用,也可以使用纯代码方式使用。
UILabel、UITextField、UIButton
UILabel
显示静态文本。
文字换行
使用 storyboard:设置Lines为 0,然后在Text中用option+回车换行。
使用代码:label.numberOfLines = 0,设置文字的时候用\n换行。
UITextField
输入框。
框内左边视图
textField.leftView =UIImageView(image:UIImage(systemName:"phone"))textField.leftViewMode = .always复制代码
横线式输入框
classViewController:UIViewController{@IBOutletvartextfield:UITextField!overridefuncviewDidLoad(){super.viewDidLoad() }// 自动布局时放这里overridefuncviewDidLayoutSubviews(){super.viewDidLayoutSubviews()// 设置无边框textfield.borderStyle = .none// 调用textfield.setBottomBorder(UIColor.red,1) }}extensionUITextField{funcsetBottomBorder(_color: UIColor,_lineWidth: Int){// 创建一个层letbottomBorder =CALayer()letlineWidth =CGFloat(lineWidth) bottomBorder.borderColor = color.cgColor// 设置层的framebottomBorder.frame =CGRect(x:0, y: frame.size.height - lineWidth, width: frame.size.width, height: frame.size.height)// 设置宽度bottomBorder.borderWidth = lineWidth// 插入layer.addSublayer(bottomBorder)// 裁剪layer.masksToBounds =true}}复制代码
设置提示文字颜色
// 使用NSAttributedStringtextField.attributedPlaceholder =NSAttributedString(string:"请输入信息", attributes: [.foregroundColor :UIColor.red])复制代码
UIButton
按钮,最重要的是点击事件。
文字换行
使用 storyboard:设置 Lines Break 为Word Wrap,然后在 title 中用option+回车换行。
使用代码:titleLabel.lineBreakMode = NSLineBreakByWordWrapping;,设置文字的时候用\n换行。
登录案例
classViewController:UIViewController{@IBOutletvarusername:UITextField!@IBOutletvarpassword:UITextField!@IBActionfuncloginBtnClicked(_sender:Any){letuname = username.textletupwd = password.text// 可以在这里对输入的信息进行判断print("用户名为:\(uname!), 密码为:\(upwd!)") }overridefuncviewDidLoad(){super.viewDidLoad() }// 触摸屏幕方法overridefunctouchesBegan(_touches: Set, with event: UIEvent?){// 退键盘的方式之一view.endEditing(true) }}复制代码
UITextView
多行文本输入框。
使用类似 UITextField。
内容可滚动。
UIImageView
图片控件
Tom猫案例
classViewController:UIViewController{varimageArray: [UIImage] = [UIImage]()@IBOutletvartomcat:UIImageView!@IBActionfuncdrink(_sender:Any){ imageArray.removeAll()varimgName =""// 1.加载drink的动画图片forindexin0...80{// drink_XX.jpgimgName ="drink_\(index).jpg"// 通过名字构造一张图片letimage =UIImage(named: imgName) imageArray.append(image!) }// 2.让图片进行动画的播放// 图片数组tomcat.animationImages = imageArray// 动画时间tomcat.animationDuration =3.0// 动画次数tomcat.animationRepeatCount =1// 开始动画tomcat.startAnimating() }overridefuncviewDidLoad(){super.viewDidLoad() }}复制代码
UISwitch、UISlider、UIStepper 、UISegmentControl
classViewController:UIViewController{@IBOutletvarlight:UIImageView!@IBOutletvarvoice:UIImageView!@IBOutletvarproduct:UILabel!@IBOutletvarflower:UIImageView!overridefuncviewDidLoad(){super.viewDidLoad() }// sender 谁触发这个事件 就将谁传进来@IBActionfuncvalueChanged(_sender:Any){// UISwitchletswitchUI = senderas?UISwitchifletswitchUI = switchUI {ifswitchUI.isOn { light.image =UIImage(named:"light.png") }else{ light.image =UIImage(named:"nomal.png") } }// UISliderletslider = senderas?UISliderifletslider = slider {ifslider.value <0.3{ voice.image =UIImage(named:"low.png") }elseifslider.value <0.7{ voice.image =UIImage(named:"middle.png") }else{ voice.image =UIImage(named:"high.png") } }// UIStepperletstepper = senderas?UIStepperifletstepper = stepper {letvalue = stepper.valueifvalue < stepper.maximumValue { product.text ="您购买了\(Int(value))件商品"}ifvalue == stepper.minimumValue { product.text ="您未购买任何商品"} }// UISegmentedControlletsegment = senderas?UISegmentedControlifletsegment = segment {ifsegment.selectedSegmentIndex ==0{ flower.image =UIImage(named:"red.png") }elseifsegment.selectedSegmentIndex ==1{ flower.image =UIImage(named:"purple.png") } } }}复制代码
思考:汤姆猫和本案例,事件都是相同的,那么能否用一个 IBAction 完成?
UIActivityIndicatorView、UIProgressView
UIActivityIndicatorView:无进度的进度条。
UIProgressView:有进度的进度条。
classViewController:UIViewController{@IBOutletvarindicator:UIActivityIndicatorView!@IBOutletvarprogressView:UIProgressView!overridefuncviewDidLoad(){super.viewDidLoad() }overridefunctouchesBegan(_touches: Set, with event: UIEvent?){ indicator.stopAnimating()// UIView动画// 动画执行的时间// 动画执行的操作UIView.animate(withDuration:5.0) {// 千万不要直接设置progress,因为这样是不会有动画效果的// self.progressView.progress = 1.0// 必须要用带animated参数的方法来进行设置 才会有动画self.progressView.setProgress(1.0, animated:true) } }}复制代码
UIDatePicker
日期选择器
classViewController:UIViewController{@IBOutletvarbirthday:UITextField!overridefuncviewDidLoad(){super.viewDidLoad()letdatePicker =UIDatePicker(frame:CGRect(x:0, y:0, width: view.bounds.size.width, height:300)) datePicker.datePickerMode = .dateAndTime// 当控件datePicker发生valueChanged事件时 会调用target的action方法datePicker.addTarget(self, action:#selector(getBirthday),for: .valueChanged) birthday.inputView = datePicker }@objcfuncgetBirthday(datePicker: UIDatePicker){// 获取日期letdate = datePicker.date// 日期格式化// 2018.10.17 2018/10/17 2018-10-17 2018年10月17日letdateFormatter =DateFormatter()// 24小时制,hh小写12小时制dateFormatter.dateFormat ="yyyy年MM月dd日 HH:mm:ss"// 赋值给birthdaybirthday.text = dateFormatter.string(from: date) }overridefunctouchesBegan(_touches: Set, with event: UIEvent?){// 退键盘的另外一种方式birthday.resignFirstResponder() }}复制代码
iOS 14 新增了卡片式日期选择器,且成为默认样式。如果需要显示成滚轮模式,需要手动设置:
datePicker.preferredDatePickerStyle = .wheels复制代码
注意:需要在 frame 之前设置。
给输入框的 inputView 设置 UIDatePicker。
UIPickerView
选择器控件
数据源(DataSource)
代理(Delegate)
可以通过代码和拽线的方式设置数据源和代理。
classViewController:UIViewController{letprovince: [String] = ["安徽","浙江","江苏","山东","河南","湖北"]letcity: [String] = ["合肥","杭州","南京","济南","郑州","武汉","厦门","长沙"]overridefuncviewDidLoad(){super.viewDidLoad() }}extensionViewController:UIPickerViewDataSource{// UIPickerViewDataSource// 返回选择器的列数funcnumberOfComponents(inpickerView: UIPickerView)->Int{return2}funcpickerView(_pickerView: UIPickerView, numberOfRowsInComponent component: Int)->Int{ifcomponent ==0{returnprovince.count}else{returncity.count} }}extensionViewController:UIPickerViewDelegate{// UIPickerViewDelegate// 该方法会调用多次 根据numberOfRowsInComponent的返回值决定// 每一次调用就应该返回一个数据 它会自动从第0行开始设置title// 6行 0 1 2 3 4 5funcpickerView(_pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int)->String?{ifcomponent ==0{returnprovince[row] }else{returncity[row] } }// 设置UIViewfuncpickerView(_pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?)->UIView{returnUIImageView(image:UIImage(systemName:"person")) }// 设置高度funcpickerView(_pickerView: UIPickerView, rowHeightForComponent component: Int)->CGFloat{return44}// 选择的数据列(滚动的时候调用)funcpickerView(_pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int){ifcomponent ==0{print(province[row]) }else{print(city[row]) } }}复制代码
说明:
titleForRow方法在代理方法里而不是在数据源方法里。
内容除了设置 String 类型,还可以设置 UIView 类型,且一旦设置了 UIView,设置 String 的失效。
代理方法可以设置内容的高度。
数据联动
在某一列滚动的时候,重新设置联动列的显示数据,然后进行刷新操作。
UIScrollView、UIPageControl
UIScrollView
滚动控件
三个重要属性
contentSize:UIScrollView 滚动的范围。
contentOffset:UIScrollView 当前显示区域的顶点相对于内容左上角的偏移量(滚动到了什么位置)。
contentInset:ScrollView的内容相对于 UIScrollView 的上下左右的留白。
UIPageControl
页面指示器
一般配合 UIScrollView 分页使用。
图片轮播
classViewController:UIViewController{// 屏幕宽度letbannerW =UIScreen.main.bounds.size.width// 高度letbannerH =260varbanner:UIScrollView!varpageControl:UIPageControl!overridefuncviewDidLoad(){super.viewDidLoad()// 设置UIScrollViewsetupBanner()// 设置UIPageControlsetupPageControl() }funcsetupBanner(){ banner =UIScrollView(frame:CGRect(x:0, y:0, width:Int(bannerW), height: bannerH))// 是否显示滚动条banner.showsHorizontalScrollIndicator =false// 是否需要弹簧效果banner.bounces =false// 是否分页banner.isPagingEnabled =true// 代理banner.delegate =self// 添加图片foriin0...4{// x坐标letw =Int(bannerW) * i// 图片控件letimg =UIImageView(frame:CGRect(x: w, y:0, width:Int(bannerW), height: bannerH)) img.image =UIImage(named:"img_\(i)") banner.addSubview(img) }// 设置contentSizebanner.contentSize =CGSize(width: bannerW *5.0, height:0) view.addSubview(banner) }funcsetupPageControl(){ pageControl =UIPageControl(frame:CGRect(x:0, y:0, width:200, height:20))// 指示器的颜色pageControl.pageIndicatorTintColor =UIColor.red// 当前页的颜色pageControl.currentPageIndicatorTintColor =UIColor.cyan// 总页数pageControl.numberOfPages =5pageControl.center =CGPoint(x: bannerW *0.5, y:240.0)// 监听事件pageControl.addTarget(self, action:#selector(pageIndicate),for: .valueChanged) view.addSubview(pageControl) }@objcfuncpageIndicate(pageControl: UIPageControl){letcurrentIndex = pageControl.currentPage// 设置偏移banner.setContentOffset(CGPoint(x:Int(bannerW) * currentIndex, y:0), animated:true) }}extensionViewController:UIScrollViewDelegate{funcscrollViewDidEndDecelerating(_scrollView: UIScrollView){// 获取contentOffsetletcontentOffset = scrollView.contentOffset// 获取索引letindex = contentOffset.x / bannerW// 设置当前页pageControl.currentPage =Int(index) }}复制代码
UITableView
表视图,是 iOS 开发中最重要的 UI 控件之一。
整体结构
一个 UITableView 由 Header + 多个 Section + Footer 组成。
一个 Section 由 Header + 多个 Row + Footer 组成。
一个 Row 就是 UITableViewCell。
UITableViewCell结构
里面有一个contentView,显示的内容放在上面。
contentView里默认有 3 个控件:2 个UILabel、1一个UIImageView,并由此产生了四种不同的 UITableViewCell 的显示样式。
类似 PickerView,需要提供数据源以显示数据。
基本使用
classViewController:UIViewController{overridefuncviewDidLoad(){super.viewDidLoad() }}extensionViewController:UITableViewDataSource{// 有多少分组funcnumberOfSections(intableView: UITableView)->Int{return1}// 每个分组中有多少行functableView(_tableView: UITableView, numberOfRowsInSection section : Int)->Int{return20}// 每一行的内容functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{letcell =UITableViewCell(style: .subtitle, reuseIdentifier:"abc")// default 只显示textLabel和imageView// subtitle value1 三个都显示// value2 只显示textLabel和detailTextLabelcell.textLabel?.text ="AAA"cell.detailTextLabel?.text ="BBB"cell.imageView?.image =UIImage(named:"setting_about_pic")returncell }}复制代码
UITableViewCell重用
重用原理
重用好处
重用标识符
classViewController:UIViewController{overridefuncviewDidLoad(){super.viewDidLoad() }}extensionViewController:UITableViewDataSource{// 有多少分组funcnumberOfSections(intableView: UITableView)->Int{return1}// 一个分组中有多少行functableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{return20}// 每一行长什么样functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{/**
// 纯代码实现复用
// 去重用池子中找cell
var cell = tableView.dequeueReusableCell(withIdentifier: "abc")
// 池子中没有就创建一个新的
if cell == nil {
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "abc")
}
cell?.textLabel?.text = "AAA"
cell?.detailTextLabel?.text = "BBB"
cell?.imageView?.image = UIImage(named: "setting_about_pic")
return cell!
*/// SB方式实现复用letcell = tableView.dequeueReusableCell(withIdentifier:"abc") cell?.textLabel?.text ="AAA"cell?.detailTextLabel?.text ="BBB"cell?.imageView?.image =UIImage(named:"setting_about_pic")returncell! }}复制代码
数据源
数据不再固定,而是由外界提供,多使用数组。
classViewController:UIViewController{varcontent:Array?vardetailContent:Array?overridefuncviewDidLoad(){super.viewDidLoad() content = ["iPhone 4","iPhone 4s","iPhone 5","iPhone 5s","iPhone 6","iPhone 6 Plus","iPhone 6s","iPhone 6s Plus","iPhone 7","iPhone 7 Plus","iPhone 8","iPhone 8 Plus","iPhone X","iPhone Xs","iPhone XR","iPhone Xs Max","iPhone 11","iPhone 11 Pro","iPhone 11 Pro Max","iPhone 12 mini","iPhone 12","iPhone 12 Pro","iPhone 12 Pro Max"] detailContent = ["iPhone 4 - iOS 4","iPhone 4s - iOS 5","iPhone 5 - iOS 6","iPhone 5s - iOS 7","iPhone 6 - iOS 8","iPhone 6 Plus - iOS 8","iPhone 6s - iOS 9","iPhone 6s Plus - iOS 9","iPhone 7 - iOS 10","iPhone 7 Plus - iOS 10","iPhone 8 - iOS 11","iPhone 8 Plus - iOS 11","iPhone X - iOS 11","iPhone Xs - iOS 12","iPhone XR - iOS 12","iPhone Xs Max - iOS 12","iPhone 11 - iOS 13","iPhone 11 Pro - iOS 13","iPhone 11 Pro Max - iOS 13","iPhone 12 mini - iOS 14","iPhone 12 - iOS 14","iPhone 12 Pro - iOS 14","iPhone 12 Pro Max - iOS 14"] }}extensionViewController:UITableViewDataSource{// 有多少分组funcnumberOfSections(intableView: UITableView)->Int{return1}// 一个分组中有多少行functableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{returncontent!.count}// 每一行长什么样functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{// SB方式实现复用letcell = tableView.dequeueReusableCell(withIdentifier:"ABC") cell?.textLabel?.text = content?[indexPath.row] cell?.detailTextLabel?.text = detailContent?[indexPath.row] cell?.imageView?.image =UIImage(named:"iPhone")returncell! }}复制代码
代理
classViewController:UIViewController{varcontent:Array?vardetailContent:Array?overridefuncviewDidLoad(){super.viewDidLoad() content = ["iPhone 4","iPhone 4s","iPhone 5","iPhone 5s","iPhone 6","iPhone 6 Plus","iPhone 6s","iPhone 6s Plus","iPhone 7","iPhone 7 Plus","iPhone 8","iPhone 8 Plus","iPhone X","iPhone Xs","iPhone XR","iPhone Xs Max","iPhone 11","iPhone 11 Pro","iPhone 11 Pro Max","iPhone 12 mini","iPhone 12","iPhone 12 Pro","iPhone 12 Pro Max"] detailContent = ["iPhone 4 - iOS 4","iPhone 4s - iOS 5","iPhone 5 - iOS 6","iPhone 5s - iOS 7","iPhone 6 - iOS 8","iPhone 6 Plus - iOS 8","iPhone 6s - iOS 9","iPhone 6s Plus - iOS 9","iPhone 7 - iOS 10","iPhone 7 Plus - iOS 10","iPhone 8 - iOS 11","iPhone 8 Plus - iOS 11","iPhone X - iOS 11","iPhone Xs - iOS 12","iPhone XR - iOS 12","iPhone Xs Max - iOS 12","iPhone 11 - iOS 13","iPhone 11 Pro - iOS 13","iPhone 11 Pro Max - iOS 13","iPhone 12 mini - iOS 14","iPhone 12 - iOS 14","iPhone 12 Pro - iOS 14","iPhone 12 Pro Max - iOS 14"] }}extensionViewController:UITableViewDataSource{// 有多少分组funcnumberOfSections(intableView: UITableView)->Int{return1}// 一个分组中有多少行functableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{returncontent!.count}// 每一行长什么样functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{// SB方式实现复用letcell = tableView.dequeueReusableCell(withIdentifier:"ABC") cell?.textLabel?.text = content?[indexPath.row] cell?.detailTextLabel?.text = detailContent?[indexPath.row] cell?.imageView?.image =UIImage(named:"iPhone")returncell! }}extensionViewController:UITableViewDelegate{// Section头部functableView(_tableView: UITableView, titleForHeaderInSection section: Int)->String?{return"iPhone 大全"}// Section尾部functableView(_tableView: UITableView, titleForFooterInSection section: Int)->String?{return"iOS大全"}// 选中(点击行)functableView(_tableView: UITableView, didSelectRowAt indexPath: IndexPath){ tableView.deselectRow(at: indexPath, animated:true)letcontentText = content?[indexPath.row]letdetailText = detailContent?[indexPath.row]print("\(contentText!)--\(detailText!)") }// 行高functableView(_tableView: UITableView, heightForRowAt indexPath: IndexPath)->CGFloat{return80.0}// Section头部高functableView(_tableView: UITableView, heightForHeaderInSection section: Int)->CGFloat{return100.0}// Section尾部高functableView(_tableView: UITableView, heightForFooterInSection section: Int)->CGFloat{return60.0}}复制代码
编辑
classViewController:UIViewController{@IBOutletvartableView:UITableView!varcontent:Array?vardetailContent:Array?overridefuncviewDidLoad(){super.viewDidLoad() content = ["iPhone 4","iPhone 4s","iPhone 5","iPhone 5s","iPhone 6","iPhone 6 Plus","iPhone 6s","iPhone 6s Plus","iPhone 7","iPhone 7 Plus","iPhone 8","iPhone 8 Plus","iPhone X","iPhone Xs","iPhone XR","iPhone Xs Max","iPhone 11","iPhone 11 Pro","iPhone 11 Pro Max","iPhone 12 mini","iPhone 12","iPhone 12 Pro","iPhone 12 Pro Max"] detailContent = ["iPhone 4 - iOS 4","iPhone 4s - iOS 5","iPhone 5 - iOS 6","iPhone 5s - iOS 7","iPhone 6 - iOS 8","iPhone 6 Plus - iOS 8","iPhone 6s - iOS 9","iPhone 6s Plus - iOS 9","iPhone 7 - iOS 10","iPhone 7 Plus - iOS 10","iPhone 8 - iOS 11","iPhone 8 Plus - iOS 11","iPhone X - iOS 11","iPhone Xs - iOS 12","iPhone XR - iOS 12","iPhone Xs Max - iOS 12","iPhone 11 - iOS 13","iPhone 11 Pro - iOS 13","iPhone 11 Pro Max - iOS 13","iPhone 12 mini - iOS 14","iPhone 12 - iOS 14","iPhone 12 Pro - iOS 14","iPhone 12 Pro Max - iOS 14"] }@IBActionfuncedit(_sender:Any){ tableView.setEditing(true, animated:true) }@IBActionfuncdone(_sender:Any){ tableView.setEditing(false, animated:true) }}extensionViewController:UITableViewDataSource{// 有多少分组funcnumberOfSections(intableView: UITableView)->Int{return1}// 一个分组中有多少行functableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{returncontent!.count}// 每一行长什么样functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{// SB方式实现复用letcell = tableView.dequeueReusableCell(withIdentifier:"ABC") cell?.textLabel?.text = content?[indexPath.row] cell?.detailTextLabel?.text = detailContent?[indexPath.row] cell?.imageView?.image =UIImage(named:"iPhone")returncell! }}extensionViewController:UITableViewDelegate{// 允许编辑functableView(_tableView: UITableView, canEditRowAt indexPath: IndexPath)->Bool{returntrue}// 提交编辑functableView(_tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath){ifeditingStyle == .delete {// 1.删除数组中对应的数据content?.remove(at: indexPath.row) detailContent?.remove(at: indexPath.row)// 2.TableView显示的那一样删除tableView.deleteRows(at: [indexPath], with: .automatic) }elseifeditingStyle == .insert {// 1.增加一条数据content?.insert("iPhone 1", at: indexPath.row) detailContent?.insert("iPhone 1 - iPhone OS", at: indexPath.row)// 2.增加一行tableView.insertRows(at: [indexPath], with: .automatic) } }// 删除按钮的文字functableView(_tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath)->String?{return"删除"}// 编辑的风格(默认是删除)functableView(_tableView: UITableView, editingStyleForRowAt indexPath: IndexPath)->UITableViewCell.EditingStyle{return.insert }// 能否移动functableView(_tableView: UITableView, canMoveRowAt indexPath: IndexPath)->Bool{returntrue}// 移动表格functableView(_tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath){// 内容letcontentText = content?[sourceIndexPath.row]// 先删除content?.remove(at: sourceIndexPath.row)// 再增加content?.insert(contentText!, at: destinationIndexPath.row)// 详情操作和内容一样letdetailContentText = detailContent?[sourceIndexPath.row] detailContent?.remove(at: sourceIndexPath.row) detailContent?.insert(detailContentText!, at: destinationIndexPath.row) }}复制代码
索引
classViewController:UIViewController{@IBOutletvartableView:UITableView!varsectionTitles: [String] = ["A","C","F","G","H","M","S","T","X","Z"]varcontentsArray: [[String]] = [ ["阿伟","阿姨","阿三"], ["陈晨","成龙","陈鑫","陈丹"], ["芳仔","房祖名","方大同","范伟"], ["郭靖","郭美美","过儿","郭襄"], ["何仙姑","和珅","郝歌","何仙姑"], ["马云","毛不易"], ["孙周","沈冰","史磊"], ["陶也","淘宝","图腾"], ["项羽","夏紫薇","许巍","许晴"], ["祝枝山","周杰伦","张柏芝"], ]overridefuncviewDidLoad(){super.viewDidLoad() tableView.sectionIndexBackgroundColor =UIColor.red }}extensionViewController:UITableViewDataSource{funcnumberOfSections(intableView: UITableView)->Int{returnsectionTitles.count}functableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{returncontentsArray[section].count}functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{letcell = tableView.dequeueReusableCell(withIdentifier:"abc") cell?.textLabel?.text = contentsArray[indexPath.section][indexPath.row]returncell! }functableView(_tableView: UITableView, titleForHeaderInSection section: Int)->String?{returnsectionTitles[section] }}extensionViewController:UITableViewDelegate{// 索引的标题funcsectionIndexTitles(fortableView: UITableView)-> [String]? {returnsectionTitles }// 点击索引functableView(_tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int)->Int{// 点击的索引标题print(title)// 一定要返回index 否则 点击索引不会自动滚动到指定位置returnindex }}复制代码
自定义UITableViewCell
用 3 种自定义 Cell 的方式分别实现下面的案例:
iPhone 信息展示
新闻列表
下拉刷新
classViewController:UIViewController{@IBOutletvartableView:UITableView!varcontent:Array = ["iPhone 4","iPhone 4s","iPhone 5","iPhone 5s","iPhone 6","iPhone 6 Plus","iPhone 6s","iPhone 6s Plus","iPhone 7","iPhone 7 Plus","iPhone 8","iPhone 8 Plus","iPhone X","iPhone Xs","iPhone XR","iPhone Xs Max","iPhone 11","iPhone 11 Pro","iPhone 11 Pro Max","iPhone 12 mini","iPhone 12","iPhone 12 Pro","iPhone 12 Pro Max"]vardetailContent:Array = ["iPhone 4 - iOS 4","iPhone 4s - iOS 5","iPhone 5 - iOS 6","iPhone 5s - iOS 7","iPhone 6 - iOS 8","iPhone 6 Plus - iOS 8","iPhone 6s - iOS 9","iPhone 6s Plus - iOS 9","iPhone 7 - iOS 10","iPhone 7 Plus - iOS 10","iPhone 8 - iOS 11","iPhone 8 Plus - iOS 11","iPhone X - iOS 11","iPhone Xs - iOS 12","iPhone XR - iOS 12","iPhone Xs Max - iOS 12","iPhone 11 - iOS 13","iPhone 11 Pro - iOS 13","iPhone 11 Pro Max - iOS 13","iPhone 12 mini - iOS 14","iPhone 12 - iOS 14","iPhone 12 Pro - iOS 14","iPhone 12 Pro Max - iOS 14"]overridefuncviewDidLoad(){super.viewDidLoad()// 创建UIRefreshControlletrefresh =UIRefreshControl()// 设置显示的标题refresh.attributedTitle =NSAttributedString(string:"下拉刷新")// 设置下拉事件refresh.addTarget(self, action:#selector(loadData),for: .valueChanged)// 放到tableView的头部tableView.refreshControl = refresh }@objcfuncloadData(){// 延迟执行DispatchQueue.main.asyncAfter(deadline: .now() +3) {// 增加一条数据self.content.insert("iPhone 3GS", at:0)self.detailContent.insert("iPhone 3GS - iOS 3", at:0)// 刷新表格 结束刷新的状态self.tableView.reloadData()// 停止刷新if(self.tableView.refreshControl?.isRefreshing)! {self.tableView.refreshControl?.endRefreshing() } } }}extensionViewController:UITableViewDataSource{// 一个分组中有多少行publicfunctableView(_tableView: UITableView, numberOfRowsInSection section: Int)->Int{returncontent.count}// 每一行长什么样publicfunctableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath)->UITableViewCell{// SB方式实现复用letcell = tableView.dequeueReusableCell(withIdentifier:"ABC") cell?.textLabel?.text = content[indexPath.row] cell?.detailTextLabel?.text = detailContent[indexPath.row] cell?.imageView?.image =UIImage(named:"iPhone")returncell! }}复制代码
静态单元格
需要使用 UITableViewController。
直接在 storyboard 中布局,不需要使用数据源方法,但如果需要使用到代理方法,仍然需要在控制器中实现相应的方法。
适用于基本不需要动态修改、布局固定的页面,如个人中心、设置等。
微信“发现”界面案例。
UITableViewDiffableDataSource
在 iOS 13 中引入了新的 API — Diffable Data Source,它不仅能够驱动 UITableView 和 UICollectionView,而且可以更简单高效的实现数据的刷新。
核心知识
UITableViewDiffableDataSource:创建 UITableView 数据源。
NSDiffableDataSourceSnapshot:UITableView 的状态。
apply(_:animatingDifferences:):当要显示或更新数据时,通过调用 NSDiffableDataSourceSnapshot 对象的 apply 方法将其提供给数据源,该方法将比较当前显示的快照(渲染模型)和新快照以获得差异,最后以设定的动画方式应用这些变化从而刷新界面。
案例
创建数据源。
vardataSource:UITableViewDiffableDataSource!overridefuncviewDidLoad(){super.viewDidLoad() dataSource =UITableViewDiffableDataSource(tableView: tableView) {// 该闭包是tableView(_:cellForRowAtIndexPath:)方法的替代品(tableView:UITableView, indexPath:IndexPath, city:City) ->UITableViewCell?inletcell = tableView.dequeueReusableCell(withIdentifier:"cell",for: indexPath) cell.textLabel?.text = city.namereturncell }// 刷新时的动画dataSource.defaultRowAnimation = .fade}复制代码
DataSourceSnapshot 负责变更后的数据源处理,其有 append、delete、move、insert 等方法。
enumSection:CaseIterable{casemain}// 获取NSDiffableDataSourceSnapshotvarsnapshot =NSDiffableDataSourceSnapshot()// 更改数据snapshot.appendSections([.main])snapshot.appendItems(filteredCities, toSection: .main)// 更新dataSource.apply(snapshot, animatingDifferences:true)复制代码
为了确保 Diff 生效,数据源的 Model 必须具有唯一 Identifier,且遵循 Hashable 协议。
structCity:Hashable{letidentifier =UUID()letname:Stringfunchash(into hasher:inoutHasher){ hasher.combine(identifier) }staticfunc==(lhs: City, rhs: City)->Bool{returnlhs.identifier == rhs.identifier }funccontains(query: String?)->Bool{guardletquery = queryelse{returntrue}guard!query.isEmptyelse{returntrue}returnname.contains(query) }}复制代码
UICollectionView
集合视图,是 iOS 开发中最重要的 UI 控件之一。
整体结构
一个 UICollectionView 由 Header + 多个 Section + Footer 组成。
一个 Section 由 Header + 多个 Item + Footer 组成。
一个 Item 就是 UICollectionViewCell。
类似 UITableView,需要提供数据源以显示数据。
支持 Diffable Data Source,类为 UICollectionViewDiffableDataSource,使用方式类似 UITableViewDiffableDataSource。
UICollectionViewFlowLayout
与 UITableView 不同,UICollectionView 需要提供布局参数,常用的有UICollectionViewFlowLayout,通过它可以设置内容的大小、间距和方向等信息。
classViewController:UIViewController{@IBOutletvarcollectionView:UICollectionView!letscreenW =UIScreen.main.bounds.size.widthoverridefuncviewDidLoad(){super.viewDidLoad()// 布局letlayout =UICollectionViewFlowLayout()// 设置item大小layout.itemSize =CGSize(width: (screenW -15.0) *0.5, height:212)// item之间的间距layout.minimumInteritemSpacing =5.0// 行间距layout.minimumLineSpacing =5.0// 组边距layout.sectionInset =UIEdgeInsets(top:0,left:5, bottom:0,right:5)// 滚动方向layout.scrollDirection = .vertical collectionView.collectionViewLayout = layout }}extensionViewController:UICollectionViewDataSource{funcnumberOfSections(incollectionView: UICollectionView)->Int{return1}funccollectionView(_collectionView: UICollectionView, numberOfItemsInSection section: Int)->Int{return10}funccollectionView(_collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)->UICollectionViewCell{letcell = collectionView.dequeueReusableCell(withReuseIdentifier:"abc",for: indexPath)returncell }}extensionViewController:UICollectionViewDelegate{funccollectionView(_collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath){print("\(indexPath.row)") }}复制代码
UICollectionViewCompositionalLayout
在 iOS 13 中 UICollectionView 推出了一种新的组合布局 UICollectionViewCompositionalLayout,这是一次全新的升级。
介绍
UICollectionViewCompositionalLayout 是在已有的 Item 和 Section 的基础上,增加了一个 Group 的概念。多个 Item 组成一个 Group ,多个 Group 组成一个 Section,因此层级关系从里到外变为:Item -> Group -> Section -> Layout。
核心知识
NSCollectionLayoutSize
决定了一个元素的大小。表达一个元素的 Size 有三种方法:
fractional:表示一个元素相对于他的父视图的比例。(Item 的父视图是 Group,Group 的父视图是 Section) 。
// 占据Group宽和高各25%letitemSize =NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(0.25))复制代码
absolute:表示将元素的宽或者高写成固定一个值。
letwidthDimension =NSCollectionLayoutDimension.absolute(200)letheightDimension =NSCollectionLayoutDimension.absolute(200)复制代码
estimated:表示预估高度。一般用于自适应大小,会根据自动布局决定元素的大小。
letwidthDimension =NSCollectionLayoutDimension.estimated(200)letheightDimension =NSCollectionLayoutDimension.estimated(200)复制代码
NSCollectionLayoutItem
描述一个 Item 的布局,定义如下:
classNSCollectionLayoutItem{convenienceinit(layoutSize:NSCollectionLayoutSize)varcontentInsets:NSDirectionalEdgeInsets}复制代码
NSCollectionLayoutGroup
Group 是新引入的组成布局的基本单元,它有三种形式:
水平(horizontal)
垂直(vertical)
自定义(custom)
Group 的大小页需要通过 NSCollectionLayoutSize 决定。如果是自定义布局,需要传入一个 NSCollectionLayoutGroupCustomItemProvider 来决定这个 Group 中 Item 的布局方式。
定义如下:
classNSCollectionLayoutGroup:NSCollectionLayoutItem{classfunchorizontal(layoutSize:NSCollectionLayoutSize,subitems: [NSCollectionLayoutItem]) ->Selfclassfuncvertical(layoutSize:NSCollectionLayoutSize,subitems: [NSCollectionLayoutItem]) ->Selfclassfunccustom(layoutSize:NSCollectionLayoutSize,itemProvider:NSCollectionLayoutGroupCustomItemProvider) ->Self}复制代码
NSCollectionLayoutSection
描述一个 Section 的布局,定义如下:
classNSCollectionLayoutSection{convenienceinit(layoutGroup:NSCollectionLayoutGroup)varcontentInsets:NSDirectionalEdgeInsets}复制代码
使用步骤
创建 Item 的 NSCollectionLayoutSize,然后创建 NSCollectionLayoutItem。
创建 Group 的 NSCollectionLayoutSize,然后根据 Item 创建 NSCollectionLayoutGroup。
根据 Group 创建 NSCollectionLayoutSection。
根据 Section 创建 UICollectionViewCompositionalLayout。
由里而外,由小到大地创建布局,然后组合。
案例
funcgenerateLayout()->UICollectionViewCompositionalLayout{// 1. 构造Item的NSCollectionLayoutSizeletitemSize =NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(1.0))// 2. 构造NSCollectionLayoutItemletitem =NSCollectionLayoutItem(layoutSize: itemSize)// 3. 构造Group的NSCollectionLayoutSizeletgroupSize =NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.1))// 4. 构造NSCollectionLayoutGroupletgroup =NSCollectionLayoutGroup.horizontal( layoutSize: groupSize, subitems: [item])// 5. 构造NSCollectionLayoutSectionletsection =NSCollectionLayoutSection(group: group)// 6. 构造UICollectionViewCompositionalLayoutletlayout =UICollectionViewCompositionalLayout(section: section)returnlayout}复制代码
NSCollectionLayoutBoundarySupplementaryItem
附加视图,一般用于设置头部和尾部 View。
// 头部大小letheaderSize =NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))// 头部letheader =NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind:"header", alignment: .top)// 尾部大小letfooterSize =NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))// 尾部letfooter =NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind:"footer", alignment: .bottom)// pinToVisibleBounds决定是否悬停header.pinToVisibleBounds =true// 设置Section的头尾section.boundarySupplementaryItems = [header, footer]复制代码
附加视图使用之前需要注册SupplementaryView,后面会进行讲解。
NSCollectionLayoutAnchor
在 Item 中,可能需要给其加上小红点或者未读消息数等附加视图,在 UICollectionViewCompositionalLayout 中,可以通过 NSCollectionLayoutSupplementaryItem 和 NSCollectionLayoutAnchor 这两个类来实现这样的需求。
实现一个UICollectionReusableView。
classBadgeView:UICollectionReusableView{staticletreuseIdentifier ="badge"letimageView =UIImageView(image:UIImage(systemName:"heart.fill"))overrideinit(frame:CGRect) {super.init(frame: frame) configure() }}复制代码
注册SupplementaryView。
collectionView.register(BadgeView.self, forSupplementaryViewOfKind:"badge", withReuseIdentifier:BadgeView.reuseIdentifier)复制代码
设置SupplementaryView。
dataSource.supplementaryViewProvider = { (collectionView:UICollectionView, kind:String, indexPath:IndexPath) ->UICollectionReusableView?inifletbadgeView = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier:BadgeView.reuseIdentifier,for: indexPath)as?BadgeView{returnbadgeView }else{fatalError("Cannot create Supplementary") }}复制代码
设置Badge。
// Badge位置letbadgeAnchor =NSCollectionLayoutAnchor(edges: [.top, .trailing],fractionalOffset:CGPoint(x:0.5, y: -0.5))// Badge大小letbadgeSize =NSCollectionLayoutSize(widthDimension: .absolute(16),heightDimension: .absolute(16))// Badgeletbadge =NSCollectionLayoutSupplementaryItem(layoutSize: badgeSize, elementKind:"badge", containerAnchor: badgeAnchor)// 附加Badgeletitem =NSCollectionLayoutItem(layoutSize: itemSize, supplementaryItems: [badge])复制代码
Lists in UICollectionView
iOS 14 中 UICollectionView 的功能得以继续增强,可以在一定程度上替换 UITableView。
创建UICollectionView
为 UICollectionView 配置 List 式的布局,还可以配置滑动菜单。
extensionViewController{// 创建列表式UICollectionViewfuncmakeCollectionView()->UICollectionView{varconfig =UICollectionLayoutListConfiguration(appearance: .insetGrouped)// 右侧滑动删除config.trailingSwipeActionsConfigurationProvider = { indexPathin// 找到删除的内容guardletcity =self.dataSource.itemIdentifier(for: indexPath)else{returnnil}returnUISwipeActionsConfiguration( actions: [UIContextualAction( style: .destructive, title:"Delete", handler: { [weakself]_,_, completionin// 调用删除数据self?.deleteCity(city: city, indexPath: indexPath)self?.updateList()// 最后一定要调用completioncompletion(true) } )] ) }// 左侧滑动添加config.leadingSwipeActionsConfigurationProvider = { indexPathinreturnUISwipeActionsConfiguration( actions: [UIContextualAction( style: .normal, title:"Add", handler: { [weakself]_,_, completionin// 调用增加数据self?.addCity(city:City(name:"芜湖"), indexPath: indexPath)self?.updateList() completion(true) } )] ) }// 列表布局letlayout =UICollectionViewCompositionalLayout.list(using: config)returnUICollectionView(frame: view.frame, collectionViewLayout: layout) }}复制代码
注册Cell
可以像 UITableView 一样,填充 Cell 的内容。
extensionViewController{// 注册CellfuncmakeCellRegistration()->UICollectionView.CellRegistration {UICollectionView.CellRegistration{ cell,_, cityin// 自定义Cell显示的内容cell.cityLabel.text = city.name// AccessoryViewcell.accessories = [.disclosureIndicator()] } }}复制代码
配置数据源
extensionViewController{// 配置数据源funcmakeDataSource()->UICollectionViewDiffableDataSource {UICollectionViewDiffableDataSource( collectionView: collectionView, cellProvider: { view, indexPath, iteminview.dequeueConfiguredReusableCell( using:self.makeCellRegistration(),for: indexPath, item: item ) } ) }}复制代码
更新数据
enumSection:CaseIterable{casefirstcasesecond}extensionViewController{funcupdateList(){varsnapshot =NSDiffableDataSourceSnapshot()// 添加两个分组snapshot.appendSections(Section.allCases)// 分别往两个分组添加数据snapshot.appendItems(firstCities, toSection: .first) snapshot.appendItems(secondCities, toSection: .second) dataSource.apply(snapshot) }}复制代码
使用
classViewController:UIViewController{// 创建UICollectionViewprivatelazyvarcollectionView = makeCollectionView()// 创建DataSourceprivatelazyvardataSource = makeDataSource()letcityNames = ["北京","南京","西安","杭州","芜湖"]// 第一组varfirstCities: [City] = []// 第二组varsecondCities: [City] = []overridefuncviewDidLoad(){super.viewDidLoad()fornameincityNames { firstCities.append(City(name: name)) secondCities.append(City(name: name)) }// CollectionView关联DataSource collectionView.dataSource = dataSource view.addSubview(collectionView)// 第一次进来刷新updateList() }}// 增加与删除数据extensionViewController{// 删除数据funcdeleteCity(city: City, indexPath: IndexPath){ifindexPath.section ==0{ firstCities.remove(at: firstCities.firstIndex(of: city)!) }else{ secondCities.remove(at: secondCities.firstIndex(of: city)!) } }// 增加数据funcaddCity(city: City, indexPath: IndexPath){ifindexPath.section ==0{ firstCities.append(city) }else{ secondCities.append(city) } }}复制代码
纯代码
使用步骤
// 1.创建UIViewletsubView =UIView()// 2.设置framesubView.frame =CGRect(x:0, y:0, width:200, height:200)// 1和2可以合并// let subView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))// 3.设置其他属性subView.backgroundColor = .red// 4.UIControl可以添加事件...// 5.添加到父Viewview.addSubview(subView)复制代码
添加事件
iOS 14 之前使用 Target-Action 方式添加事件。
// UITextFieldlettextField =UITextField()textField.addTarget(self, action:#selector(handlerEvent),for: .editingChanged)@objcfunchandlerEvent(_sender: UITextField){print(sender.text)}// UIButtonletbutton =UIButton()button.addTarget(self, action:#selector(handlerEvent),for: .touchUpInside)@objcfunchandlerEvent(_sender: UIButton){print("按钮点击")}// UISwitchletswi =UISwitch()swi.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UISwitch){print(sender.isOn)}// UISliderletslider =UISlider()slider.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UISlider){print(sender.value)}// UIStepperletstepper =UIStepper()stepper.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UISlider){print(sender.value)}// UISegmentedControlletsegmentedControl =UISegmentedControl()segmentedControl.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UISegmentedControl){print(sender.selectedSegmentIndex)}// UIPageControlletpageControl =UIPageControl()pageControl.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UIPageControl){print(sender.currentPage)}// UIDatePickerletdatepicker =UIDatePicker()datepicker.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)@objcfunchandlerEvent(_sender: UIDatePicker){print(sender.date)}// UIRefreshControllettableView =UITableView(frame:UIScreen.main.bounds)letrefreshControl =UIRefreshControl()refreshControl.addTarget(self, action:#selector(handlerEvent),for: .valueChanged)tableView.refreshControl = refreshControl@objcfunchandlerEvent(_sender: UIRefreshControl){print(sender.isRefreshing)}复制代码
iOS 14 支持 Action 回调的方式添加事件。
// UITextFieldlettextField =UITextField()textField.addAction(UIAction{ actioninlettextField = action.senderas!UITextFieldprint(textField.text) },for: .editingChanged)// UIButton// 方式一letbutton =UIButton(primaryAction:UIAction{_inprint("按钮点击")})// 方式二letbtn =UIButton()btn.addAction(UIAction{_inprint("按钮点击") },for: .touchUpInside)// UISwitchletswi =UISwitch()swi.addAction(UIAction{ actioninletswi = action.senderas!UISwitchprint(swi.isOn) },for: .valueChanged)// UISliderletslider =UISlider()slider.addAction(UIAction{ actioninletslider = action.senderas!UISliderprint(slider.value) },for: .valueChanged)// UIStepperletstepper =UIStepper()stepper.addAction(UIAction{ actioninletstepper = action.senderas!UIStepperprint(stepper.value) },for: .valueChanged)// UISegmentedControlletsegmentedControl =UISegmentedControl()segmentedControl.addAction(UIAction{ actioninletsegmentedControl = action.senderas!UISegmentedControlprint(segmentedControl.selectedSegmentIndex) },for: .valueChanged)// UIPageControlletpageControl =UIPageControl()pageControl.addAction(UIAction{ actioninletpageControl = action.senderas!UIPageControlprint(pageControl.currentPage) },for: .valueChanged)// UIDatePickerletdatepicker =UIDatePicker()datepicker.addAction(UIAction{ actioninletdatepicker = action.senderas!UIDatePickerprint(datepicker.date) },for: .valueChanged)// UIRefreshControllettableView =UITableView(frame:UIScreen.main.bounds)letrefreshControl =UIRefreshControl()refreshControl.addAction(UIAction{ actioninletrefreshControl = action.senderas!UIRefreshControlprint(refreshControl.isRefreshing) },for: .valueChanged)tableView.refreshControl = refreshControl