IOS开发笔记
IOS ICON 制作网站
生成整套图标 https://appicon.co
swift驼峰法命名
- 变量:小驼峰法f命名
例:diceImageView
btnRoll
-
文件名:大驼峰法命名
全大写
变量申明
-
申明一个变量就是制造一个容器,将数据放进去
- 容器有两种形式:var 和 let
- var是变量,可变的
- let是常量,是不可变的
let耗内存小,var可变。
首先考虑用let,如果提示不能给常量重新赋值,改为var
- 容器有两种形式:var 和 let
-
不同类型的数据不能直接进行运算(Float和Double不可运算)
字符串差值
let fullName = "\(name) liu"
布尔类型
进行逻辑判断时用
let aa = true
let bb: Bool = true
// 默认是Double类型,小数优先使用Double类型
let bb = 3.14
// swift类型推断:根据等号右侧的值推断是字符串
var str = "Hello,playground"
// 在变量中三指轻点提示推断类型
//申明index1的类型是Int,默认是0,=前后有空格,空格不对称会报错
var index1: Int = 0
Int 3
Float 3.14 // 精度不同,
Double 3.1415926 // 用的较多
Bool true,false
String "Angela","Philipp" // 一定要用双引号
Int(取随机数)
index1 = Int.random(in:1...6 ) // 闭区间...会生成1到6之间的六个数字
摇骰子示例
- motionEnded 手机摇晃结束后,执行{}内的代码
//
// ViewController.swift
// Dice
//
// Created by 徐斌 on 2020/11/10.
//
import UIKit
class ViewController: UIViewController {
let diceImages = ["dice1","dice2","dice3","dice4","dice5","dice6"]
var index = 0
@IBOutlet weak var diceImageFirst: UIImageView!
@IBOutlet weak var sourceLabel: UILabel!
@IBOutlet weak var diceImageSecond: UIImageView!
@IBAction func btnRoll(_ sender: Any) {
rollBegin()
}
func rollBegin() {
let firstIndex = indexRandom()
let secondIndex = indexRandom()
let sourceNum = firstIndex + secondIndex + 2
diceImageFirst.image = UIImage(named: diceImages[firstIndex])
diceImageSecond.image = UIImage(named: diceImages[secondIndex])
sourceLabel.text = String(sourceNum)
}
// 摇手机手势结束之后执行函数
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
print("摇晃了手机")
rollBegin()
}
func indexRandom() -> Int {
return Int.random(in: 0...(diceImages.count-1))
}
override func viewDidLoad() {
super.viewDidLoad()
rollBegin()
// Do any additional setup after loading the view.
}
}
// round 是小数, pow()是平方
let bmi = round(weight / (pow(height,2)))
var sum = 0
// 对1到10进行取模运算,输出满足条件的数
for i in 1...10 where i % 2 == 0{
print(i)
}
//外部参数,内部参数 howMany是外部参数,total是内部参数
//一般我们不用外部参数
//也可以写下划线 _ ,也不常用
//func song( _ total:Int){
func song(howMany total:Int){
for i in (1...total).reversed(){
print("现在有\(i)"部iphone,卖出了一部,还剩\(i-1)部)
}
print("全部卖光啦")
}
song(howMany: 20)
关于tag的使用
-
使用场景:
设置button的tag值,该值为Int类型,区分哪一个button
-
例子:木琴app
// // ViewController.swift // 木琴 // // Created by 徐斌 on 2020/11/10. // import UIKit //A 是音频 V 是视频 Foundation 基础功能 import AVFoundation class ViewController: UIViewController { var player:AVAudioPlayer! // 文件需要拖拽到xcode根目录中,不能拖到访达,会找不到文件 let sounds = ["note1","note2","note3","note4","note5","note6","note7"] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } // 拖拽UIButton的时候要选择Action ,type选择UIButton // button的tag属性在view那一栏 @IBAction func btnMusic(_ sender: UIButton) { play(tag: sender.tag) } func play(tag:Int) { // Bundle是app的根目录,main是主目录,forResource 是资源名字 withExtension是扩展名 let url = Bundle.main.url(forResource: sounds[tag], withExtension: "wav") //打印的是主目录的绝对路径 print(Bundle.main.bundlePath) // 在执行播放的时候会提示可能抛错,需要做异常处理 //错误提示:Errors thrown from here are not handled //进行docatch //错误提示:Value of optional type 'URL?' must be unwrapped to a value of type 'URL' // 翻译:可选类型“URL”的值?‘ 必须解包到“URL”类型的值‘ // 提示需要解包,用感叹号对 url 进行解包 do { // 错误提示:Result of 'AVAudioPlayer' initializer is unused // 提示'AVAudioPlayer未被使用,赋值给一个变量 player = try AVAudioPlayer(contentsOf: url!) player.play() }catch{ print(error) } } }
作用域(scope范围)
- 局部变量:在{}内定义的变量或常量,在它处是不能用的
- 全局变量:在{}外定义的变量或常量,在公共区域,都可以调用
通俗理解:在{}内定义的变量,其它地方是用不了的
MVC模式
- MVC是一个约定俗成的套路
- Model 数据管理
- View 展示
- Controller 处理数据,传递
面向对象编程
Object-oriented programming (OOP)
class Question{
let answer:Bool
let questionText:String
init(correctQuestion:String,text:Bool) {
questionText = correctQuestion
answer = text
}
}
// 初始化:从类变成对象的一个过程
let question1 = Question( correctQuestion:"马云是中国首富" ,text: true)
table View 使用
连接数据源和代理
- 将数据展示到tableView中
- 将Table view拖动到main.storyboard中,给cell设定identifier(唯一标识符)
- 将Table view和view controller进行连接,按住control点击tableview拖动到view controller上,和dataSource、Delegate关联
- 到view controller中,实现UITableViewDataSource和UITableViewDelegate协议
会有提示Type 'ViewController' does not conform to protocol - 'UITableViewDataSource',UITableViewDataSource需实现 numberOfRowsInSection 和cellForRowAt indexPath两个功能,代码如下:
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
var areas = ["闵行区辛庄镇","兰州七里河区","三明市尤Navicat溪县","西宁城西区","广州白云区","闽侯县上街镇","哈尔滨市南岗区","临沂市","成都武侯区","汕头市金平区","长沙市芙蓉区",]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return areas.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//dequeueReusableCell 可重复使用单元
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath)
cell.textLabel!.text = areas[indexPath.row]
//传递图片
cell.imageView?.image = UIImage(named: "xing")
return cell
}
// 隐藏状态栏
override var prefersStatusBarHidden: Bool{
return true
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
至此在tableView中实现数据传递
UITableViewDataSource实现数据传递
UITableViewDelegate实现样式
FansArea Project
删除原来自带的Table View Controller Scene,拖一个全新的进来会自动添加Table View Cell
new file 选择 Cocoa Tounh Class 文件( Cocoa Tounh 是IOS的触摸库)
Class:AreaTableViewController 自定义的
Subclass of UITableViewController 继承自
子类与父类概念(面向对象)
新建完进main.storyboard 的 Custom Class位置进行关联
修改控制器默认方法
override关键字:Swift中子类用于覆盖父类的方法
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var provinceLabel: UILabel!
@IBOutlet weak var partLabel: UILabel!
@IBOutlet weak var thumbImageView: UIImageView!
属性,用来存储用于显示到UI上的值
@IBOutlet 用于引用storyboard上的一个UI组件
@IBAction 用于响应一个UI组件的交互事件
as类型转换
从一个类型转换到另一个类型
生活中
人 > 黄种人(衍生)
Swift > 编程语言(溯源)
美元 > 人名币(格式)
as!强制转换(失败app会崩溃)
as?安全转换(失败不会崩溃)
单元格交互(没有加载代码,问题未解决)
理解UITableViewDelegate :delegate是一种“模式(pattern)”,每一个代理负责一个具体的角色或任务,类似“分包”,以保持系统整体上的整洁。
UITableView Delegate:页眉页脚、单元格交互,排序等等
隐藏状态栏
// MARK: - 隐藏状态栏
override var prefersStatusBarHidden: Bool{
return true
}
UIAlertController 通知框
-
中部提示框 (1-2个选项)
-
地步弹出菜单(3个选项以上)
UIAlertAction菜单项
参数:title:标题 取消
style:样式 .cancel
handle:响应 nil(无操作)把单项添加到UIAlertController:addAction方法
-
显示:self.present
示例代码:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.section,indexPath.row)
let memu = UIAlertController(title: "交互菜单", message: "您点击了第\(indexPath.row)行", preferredStyle: .alert)
let option1 = UIAlertAction(title: "确定", style: .default, handler: nil)
memu.addAction(option1)
let option2 = UIAlertAction(title: "取消", style: .cancel, handler: nil)
memu.addAction(option2)
let option3 = UIAlertAction(title: "删除", style: .destructive, handler: nil)
memu.addAction(option3)
self.present(memu, animated: true, completion: nil)
}
UIAlertController弹出菜单
//UIAlertController弹出菜单
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.section,indexPath.row)
let memu = UIAlertController(title: "交互菜单", message: "您点击了第\(indexPath.row)行", preferredStyle: .actionSheet)
// handler 交互
let option1 = UIAlertAction(title: "取消", style: .default, handler: nil)
let option2 = UIAlertAction(title: "我去过", style: .default){_ in
let cell = tableView.cellForRow(at: indexPath)
// 打勾
cell?.accessoryType = .checkmark
// 详情按钮
// cell?.accessoryType = .detailButton
// 细节披露按钮
// cell?.accessoryType = .detailDisclosureButton
}
memu.addAction(option1)
memu.addAction(option2)
self.present(memu, animated: true, completion: nil)
// 取消选中,默认动画开启(那个点击之后灰色的选中状态)
tableView.deselectRow(at: indexPath, animated: true)
}
可选链
cell 跟一个“?”是什么意思
tableView.cellForRow(at:indexPath) ? .accessoryType = .checkmark
// 如果没有cell,accessoryType 就默认checkmark
// 问号会自动加上,有可能是不成功的
一炮双响的BUG
BUG原因
如果UITableView有N个cell要显示,默认可能只会创建10个,以便重复利用
当重用cell的时候,只更新了图像和文字,所以需要同时更新选中状态
三步解决BUG
跟踪所有cell的选中状态
//使用状态数组
//第一步创建了11个false
var visited = [Bool](repeatElement(false,count: 11))
修正BUG
//第二步“我去过”行为中保存状态
let option2 = UIAlertAction(title: "我去过", style: .default){_ in
let cell = tableView.cellForRow(at: indexPath)
// 打勾
cell?.accessoryType = .checkmark
// 保存状态
self.visited[indexPath.row] = true
//-------------------------------------------------------------------------------------
//第三步重用cell时读取状态
//override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
if visited[indexPath.row]{
//true去过为选中
cell.accessoryType = .checkmark
}else{
//false 样式为 none
cell.accessoryType = .none
}
//ifelse等同于以下代码 !!!!!!!!问号前面必须有空格,否则报错
//可选链的应用
cell.accessoryType = visited[indexPath.row] ? .checkmark : .none
将默认的√替换成图片,根据源码的提示
open var accessoryView: UIView? // if set, use custom view. ignore accessoryType. tracks if enabled can calls accessory action
//翻译:如果设置,请使用自定义视图。 忽略编辑附件类型。 轨道如果启用,可以调用附件操作
呃。。。不知其原理,误打误撞
把默认的 √ 改为自定义的爱心图片,上代码
//将cell.accessoryType = visited[indexPath.row] ? .checkmark : .none 替换下面一行
cell.accessoryView = visited[indexPath.row] ? UIImageView(image: UIImage(named: "fav")) : .none
//将cell.accessoryType = .checkmark替换下面一行
cell?.accessoryView = UIImageView(image: UIImage(named: "fav"))
//前提导入fav图片
单元格删除以及右滑菜单
删除行
-
删除动作的用户交互
-
删除行对应的数据
-
相应从列表上界面上消失
启动表格的华东删除功能
IOS中通常用户在一行上往右第一步:在自动创建的代码片段中可以找到以下这些代码
// Override to support editing the table view. override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { // Delete the row from the data source tableView.deleteRows(at: [indexPath], with: .fade) } else if editingStyle == .insert { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } }
第二步:从模型中删除相应的数据
indexPath已经提供,从各数组中删除相应项即可(界面和数据要同步,否则出错,行号无法对应// MARK: - 左滑删除 // Override to support editing the table view. override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { // Delete the row from the data source areas.remove(at: indexPath.row) areaImages.remove(at: indexPath.row) parts.remove(at: indexPath.row) provinces.remove(at: indexPath.row) //只反应变化的部分-删除 tableView.deleteRows(at: [indexPath], with: .fade) //整体刷新,画面比较生硬,没有动画效果 //tableView.reloadData() } else if editingStyle == .insert { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } }
定制单元格右滑菜单
更多滑动菜单项
- 一旦实现,系统便不提供删除按钮了
增加分享子菜单
//MARK: - 自定义右滑
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delAction = UIContextualAction(style: .destructive, title: "删除") { (_, _, completion) in
self.areas.remove(at: indexPath.row)
self.areaImages.remove(at: indexPath.row)
self.parts.remove(at: indexPath.row)
self.provinces.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
let shareAcition = UIContextualAction(style: .normal, title: "分享"){
(_,_,completion)in
let text = "这是城市景点\(self.areas[indexPath.row])"
let image = UIImage(named: self.areaImages[indexPath.row])!
let ac = UIActivityViewController(activityItems: [text,image], applicationActivities: nil)
// ipad分享弹窗
if let pc = ac.popoverPresentationController{
if let cell = tableView.cellForRow(at: indexPath){
pc.sourceView = cell
pc.sourceRect = cell.bounds
}
}
self.present(ac, animated: true)
completion(true)
}
let config = UISwipeActionsConfiguration(actions: [delAction,shareAcition])
return config
}
导航相关术语
导航方式:把一个视图叠在另一个之上,加上返回按钮,这种层次结构叫:导航栈(Stack)
场景和转场:在Storyboard中添加多个控制器并且相连,规定相互如何过渡,无需写人物代码
场景:通常指屏幕上的内容,比如一个视图控制器
转场:描述在2个场景之间的过渡
Push(压入)和Modal(模态)是2个常见的过渡类型
创建导航控制器
目标:控制器潜入一个导航控制器,点击浏览下一页显示区域详情
添加一个场景
从组件库中拖一个视图控制器,简单起见,只放一个UIImageView,添加充满视图的约束,填充模式为Aspect Fill
连接视图
通过转场(Segue)把视图连在一起,在电影中,转场的过渡需要无缝 顺滑 ,方显自然
点击列表控制器的单元格会过渡到详情场景
在视图大纲中选中Cell,按住Control拖到详情视图
转场类型
Show:选择Show,目的地视图会被压入导航栈顶不,导航条会根据一个后退按钮用以返回原视图,这是最常见的方式
Show detail:与Show相似,但会替换原视图,将没有导航条和后退按钮
Present Modally:模态显示内容,目的地视图会从底部向上弹出,通常用于显示跟页面连贯性不强的视图,比如:用户注册(无论在哪个页面,都可能会调用此功能)
Present as popover:iPad中常用,模态显示一个带箭头指向圆角矩形弹窗,类似一个弹出菜单
定制详情页
创建新类
详情页默认是系统的UIViewController
只提供基本功能,要显示图片很显然不够用
New File - Cocoa Touch Class
命名AreaDetailViewController,继承自UIViewController
标识分栏中进行关联
创建一个areaImage变量
用户点击后,需要有一种方法传递图像名,这个变量用作数据传递
var areaImageName = ""
从Storey Board中按住control拖到代码中待用
使用转场来传值
转场在Model层的作用
转场不仅实现可视化的过渡效果,还可以控制其中的信息传递
在过渡之前,原控制器会得到通知,触发prepare(for segue,...)方法
标识转场:一个转场可能会被多个源出发,,为了方便识别,最好给转场加上一个标识符(identifier)
发法:选中转场(左侧点击:show segue "showAreaDetail" to "Area Detail View Controller"),再到右侧属性栏identifier:showAreaDetail
定制prepare(for segue)方法
- 确认是由置顶的segue触发
- 获取转场的 目标控制器,并转成具体的类
- 设置 目标控制器 的属性
- 详情页视图全部载入完毕时,设置图片
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
// 确认是由置顶的segue触发
if segue.identifier == "showAreaDetail"{
// 获取转场的 目标控制器,并转成具体的类
let dest = segue.destination as! AreaDetailViewController
// 设置 目标控制器 的属性
dest.areaImageName = areaImages[tableView.indexPathForSelectedRow!.row]
}
}
//———————————————————————————————————————————————————————————————————————————————————
// areaImageName 用于接收转场过来的值
var areaImageName = ""
@IBOutlet weak var areaImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// 显示图片
areaImageView.image = UIImage(named: areaImageName)
// Do any additional setup after loading the view.
}
重新设计详情页
- 从组件库拖一个tableview controller(同时删除原有的Detail View Controller)
- 点击(上一个场景的原型单元格)拖动到新的tableview controller上进行,并设置新的tableview controller的identifier为 DetailCell,行高为36
- 拖动一个ImageView到原型单元格之上,设定高度为300
- 新建一个控制器DetailTableViewController,和Main.storyboard中新的tableview controller进行绑定
- 拖一个image view 的 outlet
图像填充模式
- UIImageView及成语UIView,它是一个图片容器
- Model指图片填充方式,常用3种:
1.Scale to Fill(拉伸 - 默认)
图片适应容器的尺寸,图片可以被完全展示,但可能会被拉伸而改变比例
2.Aspect Fit(居中)
图片保持原比例不变,可以完整显示并居于容器中间
3.Aspect Fill(平铺)
图片原始尺寸不变,如果超出容器尺寸,会被截断一部分
选择平铺模式,启用属性栏中Drawing的“Clip To Bounds”进行裁剪(否则会延伸出去覆盖其它视图)
定制原型单元格和控制器
拖两个Label到Cell中
文字堆叠相对于父视图的约束
-
垂直居中于单元格
-
左右边距为0
-
字段值的一半宽
对一个view设置多个约束的快捷方法:在大纲视图中按Control拖动到目标视图直至出现选项菜单,这时按住Shift键,就可以对各项进行勾选,选项菜单不会消失。
自动布局中的一个字属性
Content Hugging Priority(简称CHP)
目前Stack View管理2个Label,但为满足约束,整体需延展。其内部组件是如何分配的呢?
“字段”为什么比“值”要更长?
一个视图的布局CHP值越靠后,越接近固有尺寸,越不会被延展(Horizontal的值)
设定两个Label的宽度比例
对字段设置到值的等宽约束,而后再调整为0.5
按住Control点击其中一个Label拖动到另一个Label上,设置等宽比例为0.5
新建单元格控制器
继承自UITableViewCell,命名为DetailTableViewCell
把原型单元格置顶为此类
更新详情页
//转场代码修改如下,将行数传过去
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
// 确认是由置顶的segue触发
if segue.identifier == "showAreaDetail"{
// 获取转场的 目标控制器,并转成具体的类
let dest = segue.destination as! DetailTableViewController
// 设置 目标控制器 的属性
dest.area = areas[tableView.indexPathForSelectedRow!.row]
}
}
//--------------------------------------------------------------------------------------------
// DetailTableViewCell.swift
// testUITableViewDelegate
//
// Created by 徐斌 on 2020/11/23.
//
import UIKit
class DetailTableViewCell: UITableViewCell {
@IBOutlet weak var fieldLabel: UILabel!
@IBOutlet weak var valueLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
//—————————————————————————————————————————————————————————————————————————————————————————————
//
// DetailTableViewController.swift
// testUITableViewDelegate
//
// Created by 徐斌 on 2020/11/20.
//
import UIKit
class DetailTableViewController: UITableViewController {
//定义数组
var area: Area!
@IBOutlet weak var LargeImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// 显示图片
LargeImageView.image = UIImage(named: area.image)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 对应的 Identifier设置,withIdentifier: "DetailCell"
let cell = tableView.dequeueReusableCell(withIdentifier: "DetailCell", for: indexPath) as! DetailTableViewCell
switch indexPath.row {
case 0:
cell.fieldLabel.text = "地名"
cell.valueLabel.text = area.name
case 1:
cell.fieldLabel.text = "省"
cell.valueLabel.text = area.province
case 2:
cell.fieldLabel.text = "地区"
cell.valueLabel.text = area.part
case 3:
cell.fieldLabel.text = "去过与否"
// 问号前要加空格,否则会报错
cell.valueLabel.text = area.isVisited ?"去过":"没去过"
default:
break
}
return cell
}
}
美化列表外观和导航条
对详情页列表做改进
更改列表背景
在viewDidLoad方法加入(稍微偏灰)
tableView.backgroundColor = UIColor(white: 0.98, alpha: 1)
单元格透明
在tableView的cellForRow方法里加入:
cell.backgroundColor = UIColor.clear
可以让单元格透明
可以让单元格透明
移除空行的分割线以及分割线颜色
在viewDidLoad方法加入
tableView.tableFooterViewUIView(fram:CGRect.zero)
更改分割线颜色(比背景稍深,但不明显)
tableView.separatorColor = UIColor(white:0.9, alpha:1)
对导航条做改进
1.代码:
IOS的UI组件外观批量设置
可以使用APpearance API来定制大多数UI控件的外观,通过appearance代理机制来实现
UINavigationBar.appearance()
更改导航条背景及字体(字体名查询:http://iosfonts.com)
在整个app的入口AppDelegate的application(_:didFinishLaunchingWithOptions:)方法里加入如下代码:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UINavigationBar.appearance().barTintColor = UIColor(red: 242/255, green: 116/255, blue: 119/255, alpha: 1)
UINavigationBar.appearance().tintColor = UIColor.white
if let barFont = UIFont(name: "Avenir-Light", size: 24){
UINavigationBar.appearance().titleTextAttributes = [
NSAttributedString.Key.font: barFont,
NSAttributedString.Key.foregroundColor: UIColor.white
]
}
return true
}
更改返回按钮标题
在列表控制器
viewDidLoad方法里加入(仅留箭头)在AreaTableViewController中添加:
navigationItem.backBarButtonItem = UIBarButtonItem(title:"",style: .plain, target: nail, actionL nil)
在DetailTableViewController中,设置地区
self.title = area.name
隐藏导航条河更改状态条样式
滑动时隐藏导航条
选中左侧大纲中的Navigation Controller,右侧 Hide Bars 对On Swipe 打勾
局部控制(单个视图中加入一下方法,有导航的情况下)
override func viewDidAppear(_ animated: Bool) {
self.navigationController?.navigationBar.barStyle = .black
}
//或Storyboard上直接设置style为black
//无导航情况下
override var preferredStatusBarStyle: UIStatusBarStyle{
return .lightContent
}
无导航条的全局方法
在AppDelegate的didFinishLaunchingWithOption方法中加入:
UIApplication.shared.statusBarStyle = .lightContent
UItableView的新特性“自适应单元格”,
可根据内容动态调整单元格感度,非常实用
只要做好布局约束和几行代码
“自适应单元格”支持“动态字体”
自适应单元格
使用概要:
-
对单元格(动态宽度变化的部分)添加自动布局约束
-
指定列表的一个预计行高(不必准确)
-
设置列表的实际行为自动
//实例代码: tableView.estimatedRowHeight = 50 // 预计行高 tableView.rowHeight = UITableViewAutomaticDimension // 自动尺寸
貌似现在不需要添加以上两行代码也可实现自适应单元格
需要设置标签的顶边距和底边距为0
IOS动画
ios中创建精致的动画效果不需要写复杂的代码。位于UIView类中:
// TimeInterval:动画时常
// animations:需要实现的动画
UIView.animate(withDuration: TimeInterval, animations:() -> Void>)
// 通常写成闭包
// UIView.animate(withDuration: TimeInterval) {
// code
// }
这个方法有若干变量,可提供额外的配置和特性。是每一个视图动画的基础
动画是用快速显示一系列静态图片(帧)来模拟动画和图形转换的过程。
Animation 源于animal(名词:动物),animate(动词:使生命化)
动画是物体移动或尺寸变化的“幻影”
动画的挑战
如何生成首帧与尾帧
我们只需提供首帧和尾帧,中间的ios会帮我们生成
添加评价视图
-
在详情页的图片右上方添加一个评价按钮(图片)
无法直接拖动按钮到ImageView之上?
因UITableViewController有规定的3部分:
页眉视图、原型单元格、页脚视图
由于我们已经添加了一个ImageView作为页眉,所以必须将整个页眉视图放入一个视图容器。
1、拖一个空view作为容器,把ImageView加入其中,再拖入按钮。
2、设定容器的宽、高与图像的大小一致 3、约束:高度和宽度40,顶距10,右边距10
属性:Type:System,图片:rating,(view部分)背景:红,前景(tint):白
添加运行时属性:圆角半径为宽度的一半:20(可以在Custom Class中找到User Defined Runtime Attributes中,添加Key Path为layer.cornerRadius,Type为Number,Value为20)
这样就不需要添加裁剪 -
点评价后弹出一个新页面,显示好、中、差3个按钮(动画出现),以便评价
- 点击评价按钮时,谈一个模态视图让用户评价。
拖一个新view controller,加一个image view,任意选一张大图片,设置等宽、等高、水平和垂直居中,以便充满容器(image view满屏)
添加大标题Label:标题“我来过,评价一下吧”,白色
Title 1 字体样式:水平居中,顶距:50 - 1个按钮(差评)
属性:type:system(view部分):背景红,前景白,图片:dislike
运行时属性(Custom Class中找到User Defined Runtime Attributes):layer.cornerRadius 35,单个尺寸约束:70x70
然后复制2个按钮,快捷键:选中组件,Shift+Option拖动
中评图片:good 好评图片:great
组合成StackView约束:水平居中,顶距100
属性:间距10 - 复制一个按钮,图片为close,背景色为默认尺寸和约束改为30,顶边距和右边距为10
- 点击评价按钮时,谈一个模态视图让用户评价。
添加转场以及返回
-
从评价按钮到评价场景
到实现模态过渡,要在两者之间建立一个转场关联
Ctrl+拖动评价按钮到评价场景,菜单中选择Present modally的转场类型
建立之后,把这个转场的identifier命名为showReview -
为评价视图定义出口
模态展现的呃视图系统不提供返回按钮
这时我们需要手动定义一个“反向转场”(unwind segue)
在这里可以用于让模态视图退场(dismiss)
反向转场步骤
1、在目标控制器定义一个方法:此方法只有一个参数,类型必须是UIStoreboardSegue,且有修饰符@IBAction
这里的目标控制器就是详情控制器,也就是DetailTableViewController.swift中添加:@IBAction func close(segue:UIStoryboardSegue){ //仅返回即可,无需任何操作 }
2、在Storyboard上指定反向转场关联
Ctrl拖动 关闭按钮 至视图的Exit(出口处),选择close
可视化特效
-
New File > Cocoa Touch Class
-
命名为:ReviewViewController继承自UIViewController
-
与评价视图关联,Ctrl拖动ImageView到代码中作为背景图片使用:
@IBOulet weak var bgImageView: UIImageView!
背景虚化特效
可以使用UIViewIEffectView对一个视图应用可视化特效
配合UIBlurEffect类,可轻松添加一个背景虚化特效
步骤:
1、创建一个特效视图,指定特效类型(有暗、亮、超亮)
2、设定特效视图的大小
3、把特效视图叠加在原视图之上(原视图的子视图)
在viewDidLoad方法,加以下代码:
let blurEffectView = UIVisualEffectView()
让视图动起来
使用UIView.animate
给3个评价按钮的堆叠视图添加一个从无到有的“放大”动画
动画步骤
-
首帧:0(不可见)
-
尾帧:正常大小
如何改变视图尺寸呢?
transform函数集 - 缩放、旋转和移动视图
CGAffineTransform(scaleX:0,y:0) (CG计算机动画)
仿射变换之-范围缩放设置一个视图对象的transform属性设置为变换后的值
首帧:0(不可见)
- 将Rating Stack View与控制器关联:
@IBOutlet weak var ratingStackView:UIStackView!
- 设置变形属性,在viewDidLoad里添加:
ratingStackView.transform = CGAffineTransform(scaleX:0,y:0)
设置动画启动时机及时长
选择在当前图每一次显示完毕的时候,就在viewDidAppear加入:
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 0.3) {
self.ratingStackView.transform = CGAffineTransform.identity
}
}
动画延迟0.3秒
内容是从0尺寸到原始大小(CGAffineTransform.identity)
IOS7开始有的,更酷的一种动画效果
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3,initialSpringVelocity: 0.5,options: [],animations: {
self.ratingStackView.transform = CGAffineTransform.identity
}, completion: nil)
}
Damping:达到尾帧之前的震荡摆动效果时间
initialSpringVelocity:初始速度
冉冉升起动画
使用CGAffineTransform
//在viewDidload()中
//MARK: - 动画组和上(冉冉升起)
let startScalex = CGAffineTransform(scaleX: 0, y: 0)
let startPos = CGAffineTransform(translationX: 0, y: 500)
ratingStackView.transform = startScalex.concatenating(startPos)
//在viewDidAppear中
//MARK: - 动画组和下(冉冉升起)
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
let endScale = CGAffineTransform.identity
let endPos = CGAffineTransform(translationX: 0, y: 0)
self.ratingStackView.transform = endPos.concatenating(endScale)
}
}
反向转场和传值
如何把评价回传,让详情页更新评价视图?
基础步骤:
- 在评论视图中添加一个Action方法,任一个评价(好中差)按钮点击时会被触发
- 在这个方法中,检测是哪一个按钮的点击,保存相应的评价到属性中
- close关闭按钮action方法会被触发,我们通过转场参数UIStoryboardSegue方法获取源控制器,返回评价
- 接着更新详情页的评价按钮图片
模型更新
import Foundation
struct Area {
var name : String
var province : String
var part : String
var image : String
var isVisited : Bool
var rating = ""
//初始化
init(name: String,province:String,part:String,image:String,isVisited:Bool) {
self.name = name
self.province = province
self.part = part
self.image = image
self.isVisited = isVisited
}
}
评价按钮IBAction
把任一个评价按钮与控制器关联,命名为ratingBtnTap
拖动类型选择Button,选择Action,并设置tag的值,再次检查是否三个按钮都关联,否则点击不触发
tag用来区分评价按钮(0:差,1:中,2:好)
定义反向转场id
将反向转场的identifier命名为unwindToDetailView,以便在代码中使用(点击Exit下面一行的Unwin segue to “closeWithSegue...”),设置identifier为unwindToDetailView
保存评价
在评价控制器ReviewViewController.swift中添加一个评价变量:
var rating: String?
在ratingBtnTap这个Action中判断按钮,保存不同的评价值。
用perfotmSegue方法进行反向转场。
@IBAction func ratingBtnTap(_ sender: UIButton) {
switch sender.tag {
case 0:rating = "dislike"
case 1:rating = "good"
case 2:rating = "great"
default:
break
}
// 代码级的转场(点击评价图片触发)
performSegue(withIdentifier: "unwindToDetailView", sender: self)
// 要去完善close方法,取回评价
}
close方法取回评价
// MARK: Close方法取回评价
//从segue参数重获得来源控制器,如果有评价,则保存到当前area实例中,更新评价按钮图片
@IBAction func close(segue: UIStoryboardSegue) {
//segue的来源是ReviewViewController
let reviewVC = segue.source as! ReviewViewController
//判断是否有值
if let rating = reviewVC.rating{
// 有值的话赋值给area.rating
self.area.rating = rating
// 更新ratingBtn的图片
self.ratingBtn.setImage(UIImage(named: rating), for: .normal)
}
}
启用MapKit并添加互动
IOS中的MapKit框架提供地图的显示、导航、地图标注,增加涂层等
利用MapKit可以继承一个全功能地图到app中,无需任何编码
启用
- 选择工程的TARGETS,然后在Signing&Capabilities中打开Maps即可
- 在详情页评价按钮下面加一个“地图”按钮,可以打开地图场景来显示地点
- 添加地图视图
拖一个空视图控制器到storyboard上,再加一个MapView再其上
约束:四周边距都为0,水平垂直居中 - 建立转场,设置转场ID(showmap)
转场需设置id - 运行
MapView提供各式选项,可以实现缩放、华东定制化
可将地图从标准显示模式,切换到卫星或者混合显示
还可启用“用户位置”功能
如何定位地点
MapKit中的Geocoder类用于这种转换,实现地址坐标间的互转
数据保存在“地标(Placemark)”对象中
实例化一个CLGeocoder,调用geocodeAddressString方法即可
实际地址没有固定的格式,此方法会联网查询后返回一个地标对象数组。
地址越准确,返回的地标越接近。如果地址不太准确,可能返回多个地标
通过解析完毕的地标对象(CLPlacemark类),就可以获得地址的坐标
import MapKit
import PlaygroundSupport
// 实例化CLGeocoder类
let coder = CLGeocoder()
var placemark : CLPlacemark?
coder.geocodeAddressString("上海市普陀区石泉路300弄5号楼102室") { (placemarks, error) in
if error != nil{
print(error!)
}
if let ps = placemarks{
placemark = ps.first
}
}
// 转换2D
placemark?.location?.coordinate
地图标注
常见的标注是一个探矿,左侧可附加一张图片
开发角度,标注包含2个不同对象:
数据对象(一个object):保存有地标的数据,遵从MKAnnotation协议
视图对象(一个view):用于展现地标,默认是图钉📌。如果你想用铅笔代替图钉,需要自定义次view
MapKit默认以上2个对象,无需自己创建
添加地图控制器
New File> Cocoa Touch Class
弗雷UIViewControler,命名为MapViewController
然后指定为地图视图的控制器
在源码的开头,加上import MapKit
Ctrl拖动视图上的Map View到远嘛,建立关联:
再添加一个区域变量:
@IBOutlet weak var mapView:MKMapView!
再添加一个区域变量:
var area:Area!
添加标注代码
import UIKit
import MapKit
class MapViewController: UIViewController {
var area: Area!
@IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
let coder = CLGeocoder()
coder.geocodeAddressString(area.name) { (ps,error) in
guard let ps = ps else{
print(error ?? "位置错误")
return
}
let place = ps.first
let annotation = MKPointAnnotation()
annotation.title = self.area.name
annotation.subtitle = self.area.province
if let loc = place?.location{
annotation.coordinate = loc.coordinate
self.mapView.showAnnotations([annotation], animated: true)
self.mapView.selectAnnotation(annotation, animated: true)
}
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
至此运行会出错
Unexpectedly found nil while implicitly unwrapping an Optional value
是因为没有传递数据
在详情控制器,转场前prepareForSegue加入以下代码(打开注释的代码即可)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showMap" {
let dest = segue.destination as! MapViewController
dest.area = self.area
print(self.area!)
}
}
添加标注图片
需要遵从MKMapViewDelegate协议,这个协议可以地图更想相关信息。
为了遵从协议,需要在地图控制器源码的类声明处加上:
class MapViewController: UIViewController ,MKMapViewDelegate{
在视图大纲中,Ctrl拖动MapView到地图控制器,选中delegate。这样地图控制器就成为MapView的代理,负责实现指定的方法。
标注视图的方法实现
//打viewfor就会出来
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// 如果是用户自己的位置,进行排除
if annotation is MKUserLocation{
return nil
}
let id = "myid"
var av = mapView.dequeueReusableAnnotationView(withIdentifier: id) as? MKPinAnnotationView
if av == nil{
av = MKPinAnnotationView(annotation: annotation, reuseIdentifier: id)
av?.canShowCallout = true
}
let leftIconView = UIImageView(frame: CGRect(x: 0, y: 0, width: 53, height: 53))
leftIconView.image = UIImage(named: area.image)
av?.leftCalloutAccessoryView = leftIconView
return av
}
定制图钉颜色
IOS9支持更改图钉颜色,pinTintColor属性
av?.pinTintColor = UIColor.green
地图其他功能
MKMapView 类有很多功能可以控制,比如:
showTraffice - 显示交通信息
showScale - 显示比例尺(左上角)
showCompass - 显示指南针按钮(右上角)
在viewDidload中加入
mapView.showsCompass = true
mapView.showsScale = true
mapView.showsTraffic = true
gard 可以用于前置判断
静态Table View
静态单元格
从组件库拖一个Table View Controller ,把Content属性改为Static Cells(默认3个空单元格)
在左侧大纲视图中选中Table View,把Content属性改为Static Cells
在左侧大纲视图中选中Table View Section,把Rows设置为5
第一行:照片;第二行:名称Label和文本框;第三:省Label和文本框;第四:地区Label和文本框;第五:我来过Label和是/否 按钮
Cell1
添加一个图片素材(photoalbum)到Assets
Cell:行高为250,背景色:亮灰(Light Grey)
然后添加一个imageView到其上,image为photoalbum,Mode:Aspect Fit
约束:水平&垂直居中
Cell2
Cell:行高72
拖一个Label,title:地名
拖一个Text Field到其下,与推介的蓝色虚线对其
placeholder:输入地名
选中这2个组件,使用布局条菜单的推介布局:“Reset To Suggested Constraints”
Cell3、3
参照Cell2
Cell4
拖一个Label,title:“我来过”
在其下添加1个按钮:title:是
background:红:tint:白
再复制一个“否”按钮
选中这3个组件,使用布局条菜单的推介布局:“Reset To Suggested Constraints”
嵌入一个导航控制器
选中表格控制器 Editor > Embed in > Navigation
Controller,标题为新增
添加新场景的入口
要进入添加页面,在地名列表视图的导航条添加一个+ 按钮
拖一个bar button item到导航条右侧(此按钮专用于导航栏和工具栏)
System Item 选:Add
建立转场
按住Ctrl,拖动+按钮到新控制器的导航控制器,创建一个Present Modally转场,identifier设为addArea
提供新的出口
在新的导航条上增加一个按钮,System Item:Cancel,Tint:白
当用户点击取消 按钮会退场(滑落效果)
反向转场:
1.需在退回的控制器中加入参数类型为UIStoryboard,修饰符为IBaction的方法:
//MARK: - addArea反向转场
@IBAction func close(segue: UIStoryboard) {
}
2.Storyboard识别这个出口方法之后,就可以拖动 取消 按钮到出口
取消按钮与Exit进行关联,选择closeWithSegue
调用系统相册
当点第一个单元格(图片位置),我们想让用户从系统相册选择一张图
UIImagePickerController
首先,添加新增控制器,继承自UITableViewController
命名为AddAreaController,并与View绑定
静态Table View无需数据源相关代码,删除控制器中默认实现的方法:
nu mberOfSections
numberOfRowslnSection
单元格选择事件
// MARK: - 单元格选择事件
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 如果选择的是第一行
if indexPath.row == 0{
// 访问相册的权限检测。此类前提条件的判断,适用于guard语句
// photoLibrary相册库
// .camera 可以调用摄像头拍照
guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else {
// 无权限则不执行
print("相册不可用!")
return
}
let picker = UIImagePickerController()
// 不允许编辑相册
picker.allowsEditing = false
picker.sourceType = .photoLibrary//.camera 可以调用摄像头拍照
self.present(picker, animated: true, completion: nil)
}
// 单元格取消选择
tableView.deselectRow(at: indexPath, animated: true)
}
运行出错
info.plist中加入2项隐私权限提示设置,摄像头和相册使用描述
获取相册中的图片
与相册互动,需要遵守2个协议
AddAreaController.swift添加协议:
class AddAreaControllerTableViewController: UITableViewController,UIImagePickerControllerDelegate,UINavigationControllerDelegate {
当用户从相册中选中了一张图片,以下方法被触发:
// MARK: - 取回图片
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
<#code#>
}
实现这个方法,可以从方法的参数中取回图片
视图image view与控制器关联
Ctrl拖动Image View 到控制器,outlet为coverImageView
选择照片后的处理
// MARK: - 取回图片
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// 返回的info有好几种key,我们选择OriginalImage(原始图片),返回的是UIImage的实例
coverImageView.image = (info[UIImagePickerController.InfoKey.originalImage] as! UIImage)
// 充满屏幕
coverImageView.contentMode = .scaleAspectFit
//裁边
coverImageView.clipsToBounds = true
// 视图退场
dismiss(animated: true, completion: nil)
}
还需要实现代理
在单元格选择事件代码块中,添加 picker.delegate = self
// MARK: - 单元格选择事件
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 如果选择的是第一行
if indexPath.row == 0{
// 访问相册的权限检测。此类前提条件的判断,适用于guard语句
// photoLibrary相册库
guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else {
// 无权限则不执行
print("相册不可用!")
return
}
let picker = UIImagePickerController()
// 不允许编辑相册
picker.allowsEditing = false
picker.sourceType = .photoLibrary
//实现代理
picker.delegate = self
self.present(picker, animated: true, completion: nil)
}
// 单元格取消选择
tableView.deselectRow(at: indexPath, animated: true)
}
代码实现约束(NSLayoutConstraints)
使用代码进行布局
图片显示比例不正常,因为有些布局约束缺失了
例:
在选中图片后,加入以下代码约束:
封面图,相对于父容器等宽、等高的约束
// MARK: - 取回图片
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
// 返回的info有好几种key,我们选择OriginalImage(原始图片),返回的是UIImage的实例
coverImageView.image = (info[UIImagePickerController.InfoKey.originalImage] as! UIImage)
// 充满屏幕
coverImageView.contentMode = .scaleAspectFill
//裁边
coverImageView.clipsToBounds = true
// _ = NSLayoutConstraint(item: coverImageView as Any, attribute: .width, relatedBy: .equal, toItem: coverImageView.superview, attribute: .width, multiplier: 1, constant: 0)
// let coverHeightConstraint = NSLayoutConstraint(item: coverImageView as Any, attribute: .height, relatedBy: .equal, toItem: coverImageView.superview, attribute: .height, multiplier: 1, constant: 0)
// coverHeightConstraint.isActive = true
// coverHeightConstraint.isActive = true
let coverWidthConstraint = NSLayoutConstraint(item: coverImageView, attribute: .width, relatedBy: .equal, toItem: coverImageView.superview, attribute: .width, multiplier: 1, constant: 0)
let coverHeightConstraint = NSLayoutConstraint(item: coverImageView, attribute: .height, relatedBy: .equal, toItem: coverImageView.superview, attribute: .height, multiplier: 1, constant: 0)
coverHeightConstraint.isActive = true
coverHeightConstraint.isActive = true
// 视图退场
dismiss(animated: true, completion: nil)
}