专题1.将十六进制颜色转换成UIColor,并扩展到UIColor里
本质上是十六进制转十进制的移位运算(不用太理解)
extension UIColor { convenience init?(hex: String) { var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") var rgb: UInt64 = 0 Scanner(string: hexSanitized).scanHexInt64(&rgb) let red, green, blue, alpha: CGFloat switch hexSanitized.count { case 3: // RGB red = CGFloat((rgb >> 16) & 0xFF) / 255.0 green = CGFloat((rgb >> 8) & 0xFF) / 255.0 blue = CGFloat(rgb & 0xFF) / 255.0 alpha = 1.0 case 6: // RRGGBB red = CGFloat((rgb >> 16) & 0xFF) / 255.0 green = CGFloat((rgb >> 8) & 0xFF) / 255.0 blue = CGFloat(rgb & 0xFF) / 255.0 alpha = 1.0 case 8: // AARRGGBB red = CGFloat((rgb >> 16) & 0xFF) / 255.0 green = CGFloat((rgb >> 8) & 0xFF) / 255.0 blue = CGFloat(rgb & 0xFF) / 255.0 alpha = CGFloat((rgb >> 24) & 0xFF) / 255.0 default: return nil } self.init(red: red, green: green, blue: blue, alpha: alpha) } }然后就可以在其他页面使用了,注意十六进制的表示方式是#,如#0CB6D6
专题2.组件嵌套功能实现,利用For循环重复使用组件
拆解:
先拆解需要实现的页面,看看是否有可以重复利用的地方。
是否需要点击(决定了使用何种视图)?
有哪些是确定,不会变的,有哪些是随着元素改变会改变的?
拆解:
背景不可点击,而小的元素可以点击。
小的组件之间共享一套内部的排列方式,有着相同的结构(头像与描边,字体位置,背景大小),但文本显示与图片显示都不同。
思路:
1.先把背景和固定的图片用UIImage和UIView写出来
2.在另一页创建一个新的类,它继承自UIControl,用于存放每个组件中的固定元素。
注意命名方式,变量和函数都用小写字母开头+驼峰式命名。
3.创建一个Model(是一个结构体),内容为需要修改的元素属性(如颜色,文本,图片等),注意,能存String、UIColor,就不要传图片,因为图片很耗费内存,将图片导入Assets之后就可以用字符串的方式传图片了。
4.在Model内以函数的方式将个性化数据以数组的形式返回,便于调用(函数调用时,利用索引)。
5.确定控件位置:在FirstViewControl(也就是写了其他东西的页面)创建一个函数。创建一些变量,根据行与列之间的关系,利用数组的索引来控制每个组件的位置(x值与y值)
6.通过for来遍历结构体中的元素,确保每一个元素被返回。
7.配置控件属性:通过调用Model中的函数来配置变动的控件属性。
具体操作:
1.先把背景和固定的图片用UIImage和UIView写出来
上一部分的内容,不多赘述。
固定的图片部分
lazy var quick: UIImageView = { let quick = UIImageView(frame: .init(origin: CGPoint(x:16, y: 15),size: CGSize(width: 82, height: 26))) quick.image = UIImage(named: "home_img_personality") return quick }()固定的背景部分:因为不需要有任何功能,所以使用UIView。
设置背景的属性
lazy var mbtiwiki: UIView = {//不需要交互 let mbtiwiki: UIView = UIView(frame: .init(x: 16, y: 510, width: self.view.frame.width - 32, height: 500)) mbtiwiki.backgroundColor = .white mbtiwiki.layer.cornerRadius = 24 mbtiwiki.layer.masksToBounds = true mbtiwiki.layer.shadowColor = UIColor.black.cgColor // 阴影颜色 mbtiwiki.layer.shadowOpacity = 0.5 // 阴影不透明度 mbtiwiki.layer.shadowOffset = CGSize(width: 0, height: 2) // 阴影偏移 mbtiwiki.layer.shadowRadius = 4 // 阴影模糊半径 mbtiwiki.addSubview(quick) return mbtiwiki }()2.在另一页创建一个新的类,它继承自UIControl,用于存放每个组件中的固定元素。
创建一个继承自UIControl的类,使用UIControl,是因为这个组件要当做按钮使用。
继承自UIControl的类结构,要如何重写初始化器?
class QuickknowBtn: UIControl { override init(frame: CGRect) {//初始化,类都要初始化 super.init(frame: frame)//super 关键字来调用UIControl 的初始化方法,确保所有继承自父类的初始化逻辑都被执行 setupUI()//调用自定义的控件(这是一个函数) } ... required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } //记得最后要加上这一段为什么要重写父类的初始化器?
当创建一个自定义控件(比如按钮或文本框)时,它通常是从现有的控件(父类)继承来的(这里就是UIControl)。父类有自己的初始化代码来设置控件的基础功能。
为什么要重写初始化器?
- 确保基础功能正常:通过
super.init()调用父类的初始化器,你确保它的基本功能都被初始化,比如按钮的外观和行为。 - 传递必要的参数:将自定义控件所需的参数(比如大小和位置)传递给父类,让它来处理。
- 后续配置:在调用
super.init()之后,继续添加自己的设置,比如调整颜色、添加子视图等(这里就是setupUI())
确保父类的初始化逻辑被执行,这里是写死的(所有重写父类控件的都要这样写)。
super.init(frame: frame)调用确保父类UIControl的初始化逻辑被执行。如果不调用这个方法,父类内部的状态可能不会正确设置,这可能导致运行时错误或未定义行为。所以:在重写父类初始化器的时候必须使用super
使用函数自定义控件
func setupUI() {//调用一个设置用户界面的方法(其实就是那一小块) }在类中自定义需要的控件
(这个过程和系统的FirstViewController过程是一样的)
注意把需要变动的部分留出来不写,就比如背景颜色,文本等等。
//背景矩形 lazy var bgrect: UIView = { let bgrect: UIView = UIView(frame: .init(x: 10, y: 20, width: 68, height: 52)) bgrect.layer.cornerRadius = 8 bgrect.layer.masksToBounds = true return bgrect }() //名称 lazy var mbtinames: UILabel = { let mbtinames: UILabel = UILabel(frame: .init(x: 25, y: 50, width: 42, height: 16)) mbtinames.font = .systemFont(ofSize: 16) mbtinames.textAlignment = .center return mbtinames }() //头像 lazy var headimg: UIImageView = { let headimg = UIImageView(frame: .init(origin: CGPoint(x: 25, y: 0), size: CGSize(width: 40, height: 40))) headimg.layer.cornerRadius = 20 headimg.layer.masksToBounds = true headimg.layer.borderWidth = 2//?设置边框 return headimg }()在控件函数里添加视图
(这是固定的,能适用于所有组件的部分)这样就可以把很多个小部分组合到一起了。
如果要在其他View里调用它,就可以直接使用QuickknowBtn
func setupUI() {//调用一个设置用户界面的方法(其实就是那一小块) // self.backgroundColor = .brown self.addSubview(bgrect) self.addSubview(mbtinames) self.addSubview(headimg) }3.创建一个Model(是一个结构体)。
内容为需要修改的元素属性(如颜色,文本,图片等),注意,能存成String、UIColor,就不要传图片,因为图片很耗费内存,将图片导入Assets之后就可以用字符串的方式传图片了。
这个其实就是MVC里的M,用写好的Model去渲染页面。
烧烤一下,有16组数据,他们的文本和颜色,图片都不一样,我们需要一个地方去存储这些数据,而且要方便随时调用。
又烧烤,存储几组数据,且可以随时调用,用数组的索引(index)可以做到,所以决定用数组来存储这些数据。
那么具体有哪几组数据需要存储呢?分别有背景颜色(bgColor),文本内容(text), 图片名(imgPath), 文本颜色(textColor)。
于是创建一个结构体,定义这些需要储存的数据作为变量。
struct MBTIbaikeModel { var bgColor: UIColor var text: String var imgPath: String var textColor: UIColor }4.在Model内以函数的方式将个性化数据以数组的形式返回,便于调用(函数调用时,利用索引)。
又烧烤,应该怎么使用数组存这些数据呢?
我们的目的是,根据需求,返回对应的一些我们需要的值。
所以最终决定定义一个函数mbtiNames,需要返回的值定义为names,在函数的返回值中存储这个数组。
static func mbtiNames() -> [MBTIbaikeModel] { let names = [MBTIbaikeModel(bgColor: UIColor(hex: 0xEEFAF0), text: "INFJ", imgPath: "INFJ_head", textColor: UIColor(hex: 0x339F7B)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEEFAF0), text: "INFP", imgPath: "INFP_head", textColor: UIColor(hex: 0x339F7B)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEEFAF0), text: "ENFJ", imgPath: "ENFJ_head", textColor: UIColor(hex: 0x339F7B)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEEFAF0), text: "ENFP", imgPath: "ENFP_head", textColor: UIColor(hex: 0x339F7B)), MBTIbaikeModel(bgColor: UIColor(hex: 0xF7F0FF), text: "INTJ", imgPath: "INTJ_head", textColor: UIColor(hex: 0x9B52D0)), MBTIbaikeModel(bgColor: UIColor(hex: 0xF7F0FF), text: "INTP", imgPath: "INTP_head", textColor: UIColor(hex: 0x9B52D0)), MBTIbaikeModel(bgColor: UIColor(hex: 0xF7F0FF), text: "ENTJ", imgPath: "ENTJ_head", textColor: UIColor(hex: 0x9B52D0)), MBTIbaikeModel(bgColor: UIColor(hex: 0xF7F0FF), text: "ENTP", imgPath: "ENTP_head", textColor: UIColor(hex: 0x9B52D0)), MBTIbaikeModel(bgColor: UIColor(hex: 0xFFF8E7), text: "ISTP", imgPath: "ISTP_head", textColor: UIColor(hex: 0xE99623)), MBTIbaikeModel(bgColor: UIColor(hex: 0xFFF8E7), text: "ISFP", imgPath: "ISFP_head", textColor: UIColor(hex: 0xE99623)), MBTIbaikeModel(bgColor: UIColor(hex: 0xFFF8E7), text: "ESTP", imgPath: "ESTP_head", textColor: UIColor(hex: 0xE99623)), MBTIbaikeModel(bgColor: UIColor(hex: 0xFFF8E7), text: "ESFP", imgPath: "ESFP_head", textColor: UIColor(hex: 0xE99623)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEBF8FF), text: "ISTJ", imgPath: "ISTJ_head", textColor: UIColor(hex: 0x4386D2)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEBF8FF), text: "ISFJ", imgPath: "ISFJ_head", textColor: UIColor(hex: 0x4386D2)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEBF8FF), text: "ESTJ", imgPath: "ESTJ_head", textColor: UIColor(hex: 0x4386D2)), MBTIbaikeModel(bgColor: UIColor(hex: 0xEBF8FF), text: "ESFJ", imgPath: "ESFJ_head", textColor: UIColor(hex: 0x4386D2)), ] return names因为结构体是值类型,调用的时候需要实例。要想直接调用它,需要使用static,这部分被包含在结构体内。
5.确定控件位置:在FirstViewControl中创建一个函数
现在我们有两部分存储控件属性的模块MBTIbaikeModel与QuickknowBtn,需要考虑的就是:
1.如何确定每个控件显示的位置?
2.如何加载这些储存控件属性的模块?
3.如何调用这些控件把所有组件打印排列出来?
确定了要解决的问题之后,考虑在函数里完成这些复杂的操作,所以先定义一个函数loadQuiknow。
func loadQuiknow() {}控件的位置x和y并不是固定的,它会随着每个控件所属的行与列的不同,而有不同的值。既然会变化,那必须要用到计算属性。先定义两个x和y的初始值
func loadQuiknow() { var startx = 16//这个是x初始值 var starty = 41 + 16 //这个是y初始值 } //参数在这里并不重要,重要的是函数运行的返回结果,返回的就是我们配置好的控件。横向每个组件的距离是它本身的宽度加他们之间的间隔。
纵向每个组件的距离是它本身的长度加他们之间的间隔。
我们计算每个组件的x值和y值的时候需要用到这些数据,分别定义他们,以便使用。
func loadQuiknow() { var startx = 16//这个是x初始值 var starty = 41 + 16 //这个是y初始值 let xgap = 17 let ygap = 34 let itemSize = 68//这个组件的长宽一致,都是68,所以只定义了一个 }如果要这部分在页面中显示,必须通过addSubView的方式才能把视图添加到页面上。
确定每个控件的位置:这里是一个4×4的结构,同列的x值相同,同行的y值相同。
根据行与列之间的关系,那么我们可以利用数组的索引来控制每个组件的位置。
既然是要把全部控件都打显示出来,可以使用遍历for…in去控制。那么for的是什么呢?遍历的就是数组的索引,这样可以确保数组里的每个内容都被经历。
既然我们要使用数组,在函数里必须用一个常量先加载它(就是结构体中的函数返回的数组)。
let quicklyModels = MBTIbaikeModel.mbtiNames() for idx in 0..<quicklyModels.count {} //idx即为数组的索引6.通过for来遍历结构体中数组的元素,确保每一个元素被返回。
因为要调用数组中的某个元素来进行渲染,而且是通过索引来找到它的,用一个常量来定义它。
for idx in 0..<quicklyModels.count { let model = quicklyModels[idx]//根据数组的索引来决定要调用的元素 }因为有4行和4列,同一行x值递增,y值不变。同一列y值递增,x值不变。
也就是说索引为0-3时,x值递增,y值不变。
考虑用商和余数来控制递增或不变的x值和y值。注意要保持是整数,不能是小数。
简单验证下:
假设idx为3,3 / 4取整数商就是0,余数为1,所以y值不需要递增,x值需要。
取一个第二行的索引,假设idx为5,取整数,商为1,余数为2,x值和y值都需要递增。
for idx in 0..<quicklyModels.count { let model = quicklyModels[idx]//根据数组的索引来决定要调用的元素 let col = CGFloat(idx % 4 )//计算x值需要的参数 let row = CGFloat(idx / 4 )//计算y值需要的参数 let x = startx + Int(col) * (itemSize + xgap) let y = starty + Int(row) * (itemSize + ygap) }因为QuickknowBtn类是继承自父类UIControl的,所以可以直接调用UIControl的控件,创建一个实例btn,使用(frame: .init)来表示组件的位置。
for idx in 0..<quicklyModels.count { let model = quicklyModels[idx]//根据数组的索引来决定要调用的元素 let col = CGFloat(idx % 4 )//计算x值需要的参数 let row = CGFloat(idx / 4 )//计算y值需要的参数 let x = startx + Int(col) * (itemSize + xgap) let y = starty + Int(row) * (itemSize + ygap) let btn = QuickknowBtn(frame: .init(x: x, y: y, width: 68, height: 68)) }7.配置控件属性:通过调用Model中的函数来配置变动的控件属性。
现在已经解决前两个问题,需要通过Model来渲染这些组件,为它们添加不同的属性。因为要渲染的对应索引中的元素,仍然使用idx来控制需要渲染的元素,所以这里直接使用model来快速调用
for idx in 0..<quicklyModels.count { let model = quicklyModels[idx]//根据数组的索引来决定要调用的元素 let col = CGFloat(idx % 4 )//计算x值需要的参数 let row = CGFloat(idx / 4 )//计算y值需要的参数 let x = startx + Int(col) * (itemSize + xgap) let y = starty + Int(row) * (itemSize + ygap) let btn = QuickknowBtn(frame: .init(x: x, y: y, width: 68, height: 68)) btn.bgrect.backgroundColor = model.bgColor//设置背景色 btn.mbtinames.text = model.text // 设置文本 btn.mbtinames.textColor = model.textColor//设置文本颜色 btn.headimg.layer.borderColor = model.textColor.cgColor//设置描边色 btn.headimg.image = UIImage(named: model.imgPath) //设置显示的图片 }设置好之后把创建的QuickknowBtn的实例btn添加到背景视图mbtiwiki里, 让它可以正常显示。
for idx in 0..<quicklyModels.count { let model = quicklyModels[idx]//根据数组的索引来决定要调用的元素 let col = CGFloat(idx % 4 )//计算x值需要的参数 let row = CGFloat(idx / 4 )//计算y值需要的参数 let x = startx + Int(col) * (itemSize + xgap) let y = starty + Int(row) * (itemSize + ygap) let btn = QuickknowBtn(frame: .init(x: x, y: y, width: 68, height: 68)) btn.bgrect.backgroundColor = model.bgColor//设置背景色 btn.mbtinames.text = model.text // 设置文本 btn.mbtinames.textColor = model.textColor//设置文本颜色 btn.headimg.layer.borderColor = model.textColor.cgColor//设置描边色 btn.headimg.image = UIImage(named: model.imgPath) //设置显示的图片 mbtiwiki.addSubview(btn) }因为这本身是是一个函数里的内容,要使用一个函数必须调用它。
注意:必须在添加页面之后再调用这个函数。
self.view.addSubview(titleImg) ... //添加的其他页面 loadQuiknow()最终效果图:
专题3:添加滚动视图(使用UIScrollView)
现在我们写好了好几个模块,但是屏幕太短了,要看到下面的内容,必须要让模块滑动起来。
UIScrollView视图
用于滚动操作,现在在需要滚动的页面创建一个UIScrollView。
lazy var scrollView: UIScrollView = { let scrollView = UIScrollView(frame: self.view.bounds) scrollView.contentSize = CGSize(width: self.view.frame.width, height: 1000) scrollView.alwaysBounceVertical = true // 开启垂直回弹 return scrollView }() }1.通过.contentSize来控制需要滚动的范围。CGSize是一个用于表示宽度和高度的结构体。这里用来创建一个表示内容大小的实例。
self.view.frame.width获取当前视图(通常是视图控制器的主视图)的宽度。
2.通过.alwaysBounceVertical来开启垂直回弹
将需要滚动的视图添加进UIScrollView视图中,并将UIScrollView添加进当前页面的视图中
self.view.addSubview(scrollView) scrollView.addSubview(test_32q) scrollView.addSubview(test_72q) scrollView.addSubview(mbtiwiki) scrollView.addSubview(careerbtn) scrollView.addSubview(widgetbtn)最终可以实现滚动效果