……

IOS开发笔记

IOS ICON 制作网站

图标生成 https://www.canva.com/

生成整套图标 https://appicon.co


swift驼峰法命名

  • 变量:小驼峰法f命名

​ 例:diceImageView

​ btnRoll

  • 文件名:大驼峰法命名

    ​ 全大写


变量申明

  • 申明一个变量就是制造一个容器,将数据放进去

    • 容器有两种形式:var 和 let
      • var是变量,可变的
      • let是常量,是不可变的

    let耗内存小,var可变。

    首先考虑用let,如果提示不能给常量重新赋值,改为var

  • 不同类型的数据不能直接进行运算(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是一个约定俗成的套路
    1. Model 数据管理
    2. View 展示
    3. 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 使用

连接数据源和代理

  1. 将数据展示到tableView中
  2. 将Table view拖动到main.storyboard中,给cell设定identifier(唯一标识符)
  3. 将Table view和view controller进行连接,按住control点击tableview拖动到view controller上,和dataSource、Delegate关联
  4. 到view controller中,实现UITableViewDataSource和UITableViewDelegate协议
    会有提示Type 'ViewController' does not conform to protocol
  5. '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. 中部提示框 (1-2个选项)

  2. 地步弹出菜单(3个选项以上)

    UIAlertAction菜单项
    参数:title:标题 取消
    style:样式 .cancel
    handle:响应 nil(无操作)

    把单项添加到UIAlertController:addAction方法

  3. 显示: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图片

单元格删除以及右滑菜单

删除行

  1. 删除动作的用户交互

  2. 删除行对应的数据

  3. 相应从列表上界面上消失

    ​ 启动表格的华东删除功能
    ​ 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)方法

  1. 确认是由置顶的segue触发
  2. 获取转场的 目标控制器,并转成具体的类
  3. 设置 目标控制器 的属性
  4. 详情页视图全部载入完毕时,设置图片
		//	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的新特性“自适应单元格”,
可根据内容动态调整单元格感度,非常实用
只要做好布局约束和几行代码
“自适应单元格”支持“动态字体”

自适应单元格
使用概要:

  1. 对单元格(动态宽度变化的部分)添加自动布局约束

  2. 指定列表的一个预计行高(不必准确)

  3. 设置列表的实际行为自动

    //实例代码:
    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会帮我们生成


添加评价视图

  1. 在详情页的图片右上方添加一个评价按钮(图片)

    无法直接拖动按钮到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)
    ​ 这样就不需要添加裁剪

  2. 点评价后弹出一个新页面,显示好、中、差3个按钮(动画出现),以便评价

    1. 点击评价按钮时,谈一个模态视图让用户评价。
      拖一个新view controller,加一个image view,任意选一张大图片,设置等宽、等高、水平和垂直居中,以便充满容器(image view满屏)
      添加大标题Label:标题“我来过,评价一下吧”,白色
      Title 1 字体样式:水平居中,顶距:50
    2. 1个按钮(差评)
      属性:type:system(view部分):背景红,前景白,图片:dislike
      运行时属性(Custom Class中找到User Defined Runtime Attributes):layer.cornerRadius 35,单个尺寸约束:70x70
      然后复制2个按钮,快捷键:选中组件,Shift+Option拖动
      中评图片:good 好评图片:great
      组合成StackView约束:水平居中,顶距100
      属性:间距10
    3. 复制一个按钮,图片为close,背景色为默认尺寸和约束改为30,顶边距和右边距为10

添加转场以及返回

  1. 从评价按钮到评价场景
    到实现模态过渡,要在两者之间建立一个转场关联
    Ctrl+拖动评价按钮到评价场景,菜单中选择Present modally的转场类型
    建立之后,把这个转场的identifier命名为showReview

  2. 为评价视图定义出口
    模态展现的呃视图系统不提供返回按钮
    这时我们需要手动定义一个“反向转场”(unwind segue)
    在这里可以用于让模态视图退场(dismiss)
    反向转场步骤
    1、在目标控制器定义一个方法:此方法只有一个参数,类型必须是UIStoreboardSegue,且有修饰符@IBAction
    这里的目标控制器就是详情控制器,也就是DetailTableViewController.swift中添加:

    @IBAction func close(segue:UIStoryboardSegue){
    	//仅返回即可,无需任何操作
    }
    

    2、在Storyboard上指定反向转场关联
    Ctrl拖动 关闭按钮 至视图的Exit(出口处),选择close


可视化特效

  1. New File > Cocoa Touch Class

  2. 命名为:ReviewViewController继承自UIViewController

  3. 与评价视图关联,Ctrl拖动ImageView到代码中作为背景图片使用:

    @IBOulet weak var bgImageView: UIImageView!
    

背景虚化特效
可以使用UIViewIEffectView对一个视图应用可视化特效
配合UIBlurEffect类,可轻松添加一个背景虚化特效
步骤:
1、创建一个特效视图,指定特效类型(有暗、亮、超亮)
2、设定特效视图的大小
3、把特效视图叠加在原视图之上(原视图的子视图)
在viewDidLoad方法,加以下代码:

let blurEffectView = UIVisualEffectView()

让视图动起来

使用UIView.animate
给3个评价按钮的堆叠视图添加一个从无到有的“放大”动画

动画步骤

  1. 首帧:0(不可见)

  2. 尾帧:正常大小

    如何改变视图尺寸呢?
    transform函数集 - 缩放、旋转和移动视图
    CGAffineTransform(scaleX:0,y:0) (CG计算机动画
    仿射变换之-范围缩放

    设置一个视图对象的transform属性设置为变换后的值

    首帧:0(不可见)

    1. 将Rating Stack View与控制器关联:
    @IBOutlet weak var ratingStackView:UIStackView!
    
    1. 设置变形属性,在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)
        }
        }

反向转场和传值

如何把评价回传,让详情页更新评价视图?
基础步骤:

  1. 在评论视图中添加一个Action方法,任一个评价(好中差)按钮点击时会被触发
  2. 在这个方法中,检测是哪一个按钮的点击,保存相应的评价到属性中
  3. close关闭按钮action方法会被触发,我们通过转场参数UIStoryboardSegue方法获取源控制器,返回评价
  4. 接着更新详情页的评价按钮图片

模型更新

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中,无需任何编码

启用

  1. 选择工程的TARGETS,然后在Signing&Capabilities中打开Maps即可
  2. 在详情页评价按钮下面加一个“地图”按钮,可以打开地图场景来显示地点
  3. 添加地图视图
    拖一个空视图控制器到storyboard上,再加一个MapView再其上
    约束:四周边距都为0,水平垂直居中
  4. 建立转场,设置转场ID(showmap)
    转场需设置id
  5. 运行
    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!

添加标注代码

截屏2020-11-30 上午12.47.29

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的代理,负责实现指定的方法。

截屏2020-11-30 上午1.10.55

标注视图的方法实现

截屏2020-11-30 上午1.12.51

//打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
        
    }

截屏2020-11-30 上午1.23.04

定制图钉颜色
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项隐私权限提示设置,摄像头和相册使用描述截屏2020-12-01 上午12.01.33

获取相册中的图片
与相册互动,需要遵守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)
使用代码进行布局
图片显示比例不正常,因为有些布局约束缺失了
例:截屏2020-12-01 上午12.41.28

在选中图片后,加入以下代码约束:
封面图,相对于父容器等宽、等高的约束

   // 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)
    }

截屏2020-12-01 上午12.52.53


posted on 2020-11-18 16:54  Exlo  阅读(429)  评论(0编辑  收藏  举报

导航