UI要求实现一个性别选择,很明显就是UISegementedControl,只是外观需要自己修改实现一下,第一反应就想着去使用UISegementedControl了(当然你也可以自定义View去实现)
本文很浅薄地接涉及到了KVO ,KVC 等Runtime机制
UI如上图所示,任务清单如下
Segment
文字
边框
背景
(选中和未选中两个状态)
Segment之间的间距
消除原生效果 把蓝色前景和白色背景都改掉
genderSegmentedControl.tintColor = .clear genderSegmentedControl.backgroundColor = .black
Segment title title自定义比较简单,查一下UISegementedControl的文档,调用接口就完事了
open func setTitleTextAttributes (_ attributes: [NSAttributedString.Key : Any ]?, for state: UIControl.State)
边框&背景 既然我们用的是UISegementedControl
,盲猜它的subviews
也是UISegement
,可惜查UIKit
里面并没有这个类,看来这是个私有类,不能用.(现在只是猜测subview是什么类,后文去验证)
但是不管它是什么类,它都是UIView
的子类就对了,UIView
能设置边框border
和背景background
,那就把它的子类当成UIView
用就完事了
for segment in self .subviews { segment.layer.borderWidth = 1 segment.layer.borderColor = UIColor .white.cgColor segment.setDiagonalBackground(show: !segmentIsSelected) }
现在边框和背景都设置了,但是不能随着状态改变,后续需要绑定状态
间距 查文档,UISegementedControl还是不能直接设置间距,只有一个设置分割图片的方法
func setDividerImage (_ dividerImage: UIImage?, forLeftSegmentState leftState: UIControl.State, rightSegmentState rightState: UIControl.State, barMetrics: UIBarMetrics)
那就自己生成图片,设置好尺寸,实现间距自定义
fileprivate extension UIImage { class func render (size : CGSize , _ draw : () -> Void ) -> UIImage ? { UIGraphicsBeginImageContext (size) defer { UIGraphicsEndImageContext () } draw() return UIGraphicsGetImageFromCurrentImageContext ()? .withRenderingMode(.alwaysTemplate) } }
依照api的功能,根据两边的不同状态设置不同的分割图片,这里全部设置为透明的图片
for leftState in [UIControl .State .normal,.selected] { for rightState in [UIControl .State .normal,.selected] { self .setDividerImage(UIImage .render(size: CGSize (width: 16 , height: 24 ), { UIColor .clear.setFill() }), forLeftSegmentState: leftState, rightSegmentState: rightState, barMetrics: .default ) } }
添加观察 做到这里,只是一个静态的选择控件,还需要根据选中的Segment去改变UI
初步设想 观察整体: UISegmentedControl.selectedSementIndex 这个属性是可以直接访问到的,只要这个有变化,咱们就去重新设置subviews的外观
既然有索引index
,根据index去找到subviews中选中的Segment,重新设置外观就完成了
(但是索引对应的UISegment和你直观的思想是冲突的,处理起来逻辑会很混乱)
后续的想法: 观察局部: Segment.isSelected 如果说每一个Segment
本身都有一个属性isSelected
,那直接监听此属性,让Segment
自己改变外观就好了. 接下来就需要去验证我们的想法
UISegementedControl的subview是什么 新建一个playground,进行验证
import UIKitlet a = UISegmentedControl (items: ["a" ,"b" ])a.selectedSegmentIndex = 1 let b = a.subviews.first!print (b)
确实是一个UISegment,也确实是一个私有类,文档没有给出此类的任何接口,无法直接调用
Segment 是否有 isSelected 属性 UISegment 是私有类,没有文档可看,只能借助runtime机制去看一下它有什么属性成员变量与属性的区别
for x in a.subviews { print ("------ivar-----" ) var pcount:UInt32 = 0 var prs = class_copyPropertyList(x.classForCoder, &pcount) for i in 0 ..<Int (pcount) { let ivar = String (cString: ivar_getName(prs![i])!, encoding: .utf8) let property = String (cString: property_getName(prs![i]), encoding: .utf8) print (ivar! + " " + property!) } }
selected
就是我们需要的属性,打印出来看看是不是布尔型(其实是Int)
for x in a.subviews { let n = x.value(forKey: "isSelected" )! print (n) print (n is Int ) }
由此可知,选中为1,未选中为0
添加监听 再新建一个playground,做一个小demo,测试一下
import UIKitimport PlaygroundSupportclass MyViewController : UIViewController { override func loadView () { let view = UIView () self .view = view } let sc = UISegmentedControl (items: ["first" ,"second" ,"third" ]) override func viewDidLoad () { super .viewDidLoad() view.backgroundColor = .black view.addSubview(sc) sc.frame.origin = CGPoint (x: 100 , y: 100 ) sc.addObserver(self , forKeyPath: "selectedSegmentIndex" , options: .new, context: nil ) for x in sc.subviews { x.addObserver(self , forKeyPath: "selected" , options: .new, context: nil ) } } override func observeValue (forKeyPath keyPath: String?, of object: Any ?, change: [NSKeyValueChangeKey : Any ]?, context: UnsafeMutableRawPointer?) { let value = change![.newKey]! as ! Int if keyPath == "selectedSegmentIndex" { print ("current selected:\(value)" ) }else if keyPath == "selected" { if value == 1 { (object as ! UIView ).backgroundColor = .white }else { (object as ! UIView ).backgroundColor = .yellow } } } } PlaygroundPage .current.liveView = MyViewController ()
封装控件 既然功能上可行,那就封装好一个自定义UISegementedControl,方便使用,其实仅仅是对外观的修改,功能没有任何变化
import UIKitclass TaoSegmentedControl : UISegmentedControl { private init () { super .init (frame: .zero) config() } private override init (frame: CGRect ) { super .init (frame: frame) } override init (items:[Any ]?) { super .init (items: items) config() for segment in subviews { segment.addObserver(self , forKeyPath: "selected" , options: [.new,.initial], context: nil ) } } required init ?(coder aDecoder: NSCoder ) { fatalError ("init(coder:) has not been implemented" ) } private func config () { self .tintColor = .clear self .backgroundColor = .black self .setTitleTextAttributes([.font:UIFont .systemFont(ofSize: 12 , weight: .semibold)], for : .normal) self .setTitleTextAttributes([.font:UIFont .systemFont(ofSize: 12 , weight: .semibold)], for : .selected) self .setTitleTextAttributes([.foregroundColor:UIColor .gray], for : .normal) self .setTitleTextAttributes([.foregroundColor:UIColor .white], for : .selected) for leftState in [UIControl .State .normal,.selected] { for rightState in [UIControl .State .normal,.selected] { self .setDividerImage(UIImage .render(size: CGSize (width: 16 , height: 24 ), { UIColor .clear.setFill() }), forLeftSegmentState: leftState, rightSegmentState: rightState, barMetrics: .default ) } } } override func observeValue (forKeyPath keyPath: String?, of object: Any ?, change: [NSKeyValueChangeKey : Any ]?, context: UnsafeMutableRawPointer?) { let segment = object as ! UIView let isSelected = (segment.value(forKey: "isSelected" ) as ! Int ) == 1 segment.layer.borderWidth = 1 segment.layer.borderColor = isSelected ? UIColor .white.cgColor : UIColor .gray.cgColor segment.backgroundColor = isSelected ? UIColor .black: UIColor .darkGray } deinit { for segment in subviews { segment.removeObserver(self , forKeyPath: "isSelected" ) } } } fileprivate extension UIImage { class func render (size : CGSize , _ draw : () -> Void ) -> UIImage ? { UIGraphicsBeginImageContext (size) defer { UIGraphicsEndImageContext () } draw() return UIGraphicsGetImageFromCurrentImageContext ()? .withRenderingMode(.alwaysTemplate) } }
和UISegmentedControl的调用别无二致,最后的效果如下
仅仅是一个简单的控件,没有考虑到后续insertSegment
的情况,本文中也没有用条纹背景,用灰色背景替代,有兴趣的小伙伴可以自己去实现完善.