ios UIAppearance 协议

一、前言

iOS 上提供了一个比较强大的工具UIAppearance,我们通过UIAppearance设置一些UI的全局效果,这样就可以很方便的实现UI的自定义效果又能最简单的实现统一界面风格。

+ (id)appearance ; 这个是这个协议里最重要的方法了 .
这个方法是统一全部改,比如你可以设置UIView.appearance().backgroundColor = UIColor.orange, 这样所有View默认颜色就都是橘色。

二、主题设置的前提

为什么我们可以给它们设置主题属性呢?哪些对象 哪些属性 可以设置主题属性呢?

1. 那些控件和类,可以设置主题呢?

回答:只要遵守了UIAppearance协议的类,都可以设置主题

查看UIView的头文件,可得,UIView可以设置主题,那么不是所有继承UIView的控件就都可以设置主题了吗?是的

观看可得,不仅,只是控件可以设置主题,UIBarItem等只要遵守了UIAppearance协议的类,都可以设置主题

这里列举了一部分:

  • UIView
  • UIActivitiIndicatorView
  • UIBarButtonItem
  • UIBarItem
  • UINavgationBar
  • UIPopoverControll
  • UIProgressView
  • UISearchBar
  • UISegmentControll
  • UISlider
  • UISwitch
  • UITabBar
  • UITabBarItem
  • UIToolBar
  • UIViewController

2.遵守UIAppearance协议的类的,那些属性可以设置主题呢?

通过主题对象设置属性的前提: 属性后面是否带有UI_APPEARANCE_SELECTOR的方法

- (void)setTitleTextAttributes:(NSDictionary *)attributes forState:(UIControlState)state
NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;

因为并不是所以属性可以设置主题,设置主题属性是有前提的。

查看头文件可得,可以设置UITabBarItem类,发现setTitleTextAttributes:可以设置UITabBarItem文字主题

  • 案例:设置所有的UITabBarItem,普通与选中状态下的文字颜色

三、运用主题appearance,是否会生效,何时会生效

1、主题会生效的场景:先设置控件主题,后添加控件到视图上

  • 添加控件时,添加的那一刻会检查主题,会根据主题设置控件 =》主题会生效

2、主题不会生效的场景:先添加控件,后设置主题

  • 控件已经添加,后设置主题,对以前的添加的控件不起作用了

如果先添加控件,后设置主题,主题失效,我们该如何解决呢?

最优方案:当然是改下调用顺序,先设置appearance,在添加控件。

当然作为脑洞或者探究原理,也可以重新把View移除,然后在添加一次,触发一次渲染,实测是生效的。(但是这样的代码,谁看谁挠头,还是不要在上线版本中写。)

总结一下:
1、控件遵守了UIAppearance协议,才能对控件进行appearance设置
2、被UI_APPEARANCE_SELECTOR这个宏修饰的属性一定能使用appearance进行设置,其他属性则不保证具备该功能
3、appearance设置需要在该控制显示之前设置完成,否则会无效
 

三、实现原理

UIApearance 实际上是一个协议(Protocol),我们可以用它来获取一个类的外观代理(Appearance Proxy)。该协议需实现这几个方法:

+ (instancetype)appearance;
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 详细方法见 UIKit/UIAppearance.h

另外一个与之对应的协议是 UIAppearanceContainer,该协议并没有任何约定方法。因为它只是作为一个容器。

常见的,如 UIView 实现了 UIAppearance 这两种协议,既可以获取外观代理,也可以作为外观容器。 而 UIViewController 则是仅实现了 UIAppearanceContainer 协议,很简单,它本身是控制器而不是 view,作为容器,为 UIView 等服务。

事实上,在使用中,我们所有的视图类都继承自 UIView,UIView 的容器也基本上是 UIView 或 UIController,基本不需要自己去实现这两个协议。对于需要支持使用 appearance 来设置的属性,在属性后增加 UI_APPEARANCE_SELECTOR 宏声明即可。 文档中也有解释 UI_APPEARANCE_SELECTOR 用来标记属性用于外观代理,支持哪些类型等等。

To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR.Appearance property selectors must be of the form:- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;You may have no axes or as many as you like for any property. PropertyType may be any standard iOS type: id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets or UIOffset. IntegerType must be either NSInteger or NSUInteger; we will throw an exception if other types are used in the axes.

翻译一下:

要参与外观代理 API,请在头文件中用UI_APPEARANCE_SELECTOR标记您的外观属性选择器。

外观属性选择器必须采用以下形式:

- (void) setProperty:(属性类型) property forAxis1:(整数类型) axis1 axis2:(整数类型) axis2 axisN:(整数类型) axisN;
- (属性类型) propertyForAxis1:(整数类型) axis1 axis2:(整数类型) axis2 axisN:(整数类型) axisN;

对于任何属性,您可以没有参数,也可以有任意多个参数。属性类型可以是任何标准的 iOS 类型:id、NSInteger、NSUInteger、CGFloat、CGPoint、CGSize、CGRect、UIEdgeInsets 或 UIOffset。整数类型必须是 NSInteger 或 NSUInteger;如果在参数中使用其他类型,我们将抛出异常。

demo验证原理

写一个简单的小 Demo,自定义 CardView,有两个 subview: headerView 和 footerView,声明 2 个属性:

@property (nonatomic, strong) UIColor *headerColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *bodyColor UI_APPEARANCE_SELECTOR;

Setter 方法都加断点调试:

- (void)setHeaderColor:(UIColor *)headerColor
{_headerColor = headerColor;self.headerView.backgroundColor = _headerColor;
}- (void)setBodyColor:(UIColor *)bodyColor
{_bodyColor = bodyColor;self.bodyView.backgroundColor = _bodyColor;
}

在 ViewController 的 view 中加一个按钮,点击则创建并添加 CardView,每行代码均加断点:

- (IBAction)createButtonTouched:(id)senderCardView *cardView = [[CardView alloc] initWithFrame:CGRectMake(20, 100, 80, 120)];[self.view addSubview:cardView];cardView.headerColor = [UIColor greenColor];
}

另外,在较早的时候,添加 appearance 设置:

[CardView appearance].headerColor = [UIColor redColor];
[CardView appearance].bodyColor = [UIColor orangeColor];

运行发现,在通过 appearance 设置属性的时候,并没有调用 setter 方法,由此可知 appearance 并不会生成实例,立即赋值。当 cardView 被添加到主视图(即视图树)中去的时候,才依次调用两个 setter 方法,调用栈如下

ios_uiappearance_image_1

从 15 至 11 可以看出确实是加入到视图树中才触发的,从 7 至 2 可以基本猜测出,appearance 设置的属性,都以 Invocation 的形式存储到 _UIApperance 类中(事实上 _UIApperance 类中就有一个 _appearanceInvocations 数组),等到视图树 performUpdates 的时候,会去检查有没有相关的属性设置,有则 invoke。(这里可以看看 NSInvocation)

紧接着,它进入了 bodyColor 的 setter

ios_uiappearance_image_2

然后,当手动设置属性的时候,它是直接进入 setter 的。

ios_uiappearance_image_3

到这里,基本清晰了。

每一个实现 UIAppearance 协议的类,都会有一个 _UIApperance 实例,保存着这个类通过 appearance 设置属性的 invocations,在该类被添加或应用到视图树上的时候,它会检查并调用这些属性设置。这样就实现了让所有该类的实例都自动统一属性。

当然,如果后面又手动设置了属性,肯定会覆盖了。从上面可以知道,appearance 生效是在被添加到视图树时,所以,在此之后设置 appearance,则不会起作用,而在手动设置属性之后被添加到视图树上,手动设置的会被覆盖。appearance 只是起到一个代理作用,在特定的时机,让代理替所有实例做同样的事。

尝试一下,去掉 UI_APPEARANCE_SELECTOR 宏声明,然后通过 appearance 设置属性,会怎么样呢? 测试后发现,结果是一样的。也就是说 UI_APPEARANCE_SELECTOR 并没有干什么事,正如文档所说,只是 tag 一下。看 UI_APPEARANCE_SELECTOR 宏定义如下

	#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))

由此可见,UI_APPEARANCE_SELECTOR 真的啥都没干。。只是为了代码可读性,方便开发者使用,还是在需要的地方加上它。

原理小结:

1. 调用 +appearance 获得 proxy 对象,这个 appearance 对象并不是真正的 UIView 的实例,而是 UIKit 给你的一个 proxy,它不会马上设置属性,而是记录这个调用。

2. UIKit 使用 NSInvocation 保存调用信息,包括方法名和参数值。UIAppearance 会创建一个 NSInvocation,记录你设置了 selector: 和 value, 在合适的时机重新调用设置。

3.在控件显示时统一“回放”设置,当后续创建某个 UIView 实例时,UIKit 会:

  • 检查当前 UIView 类型是否有 appearance 设置;

  • 遍历已记录的 selector;

  • 创建 NSInvocation,将目标值更新为这个实例的属性

使用方式: iOS笔记之Appearance方法 - 代码先锋网
实现原理: iOS UIAppearance 探秘 | 流光不加少

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91382.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91382.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

进阶数据结构:用红黑树实现封装map和set

​ 嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的 passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let’s go! 我的博客:yuanManGa…

【数据结构初阶】--二叉树(五)

&#x1f525;个人主页&#xff1a;草莓熊Lotso &#x1f3ac;作者简介&#xff1a;C研发方向学习者 &#x1f4d6;个人专栏&#xff1a; 《C语言》 《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》 ⭐️人生格言&#xff1a;生活是默默的坚持&#xff0c;毅力是永久的…

redis布隆过滤器解决缓存击穿问题

在电商系统中&#xff0c;商品详情页是一个典型的高频访问场景。当用户请求某个商品的详情时&#xff0c;系统会优先从缓存中获取数据。如果缓存中没有该商品的详情&#xff0c;系统会去数据库查询并更新缓存。然而&#xff0c;如果某个热门商品的缓存失效&#xff0c;大量请求…

1+1>2!特征融合如何让目标检测更懂 “场景”?

来gongzhonghao【图灵学术计算机论文辅导】&#xff0c;快速拿捏更多计算机SCI/CCF发文资讯&#xff5e;在多模态大模型&#xff08;MLLM&#xff09;时代&#xff0c;特征融合与目标检测的研究方向正变得愈发关键。从红外与可见光图像的融合&#xff0c;到语音活动检测中的特征…

详解赛灵思SRIO IP并提供一种FIFO封装SRIO的收发控制器仿真验证

概述RapidIO标准定义为三层&#xff1a;逻辑层、传输层、物理层。逻辑层&#xff1a;定义总体协议和包格式&#xff0c;包含设备发起/完成事务的必要信息。传输层&#xff1a;提供包传输的路由信息&#xff08;对顶层不可见&#xff09;。物理层&#xff1a;描述设备级接口细节…

深度学习:简介与任务分类总览

一、什么是深度学习&#xff1f;1.1 深度学习的定义深度学习&#xff08;Deep Learning&#xff09;是机器学习的一种特殊形式&#xff0c;它依赖于具有多层结构的神经网络自动从数据中学习特征并完成任务&#xff0c;如图像识别&#xff0c;语音识别&#xff0c;自然语言处理等…

MSPM0开发学习笔记:二维云台画图(2025电赛 附源代码及引脚配置)

前言 今年的电赛&#xff08;2025&#xff09;&#xff0c;很多题都与云台相关&#xff0c;因此为备战电赛&#xff0c;博主这边也是准备了一个由两个42步进电机驱动的云台并提前进行调试&#xff0c;避免赛题出来之后手忙脚乱的&#xff0c;这边的两个42步进电机采用同一个驱…

借助 Wisdom SSH 的 AI 助手构建 Linux 开发环境

借助Wisdom SSH的AI助手构建Linux开发环境 在Linux系统的开发场景中&#xff0c;快速、准确地搭建开发环境至关重要。Wisdom SSH凭借其强大的AI助手&#xff0c;能极大简化这一过程&#xff0c;其官网为ssh.wisdomheart.cn。以下以在Ubuntu 22.04服务器上构建Python开发环境&am…

Python 程序设计讲义(44):组合数据类型——集合类型:创建集合

Python 程序设计讲义&#xff08;44&#xff09;&#xff1a;组合数据类型——集合类型&#xff1a;创建集合 目录Python 程序设计讲义&#xff08;44&#xff09;&#xff1a;组合数据类型——集合类型&#xff1a;创建集合一、集合的特征二、创建集合&#xff1a;使用set()函…

10 - 大语言模型 —Transformer 搭骨架,BERT 装 “双筒镜”|解密双向理解的核心

目录 1、为什么 BERT 能 “懂” 语言&#xff1f;先看它的 “出身” 2、核心逻辑 2.1、“自学阶段”—— 预训练&#xff0c;像婴儿学说话一样积累语感 2.1.1、简述 2.1.2、核心本事&#xff1a;“双向注意力”&#xff0c;像人一样 “聚焦重点” 2.2、“专项复习”—— …

【Spring Boot 快速入门】四、MyBatis

目录MyBatis&#xff08;一&#xff09;入门简介MyBatis 入门LombokMyBatis 基础操作数据准备删除预编译新增更新查询XML 映射文件MyBatis&#xff08;一&#xff09;入门 简介 MyBatis 是一款 优秀的持久层框架&#xff0c;它支持 自定义 SQL、存储过程以及高级映射&#xf…

Spring IOC 基于Cglib实现含构造函数的类实例化策略

作者&#xff1a;小凯 分享、让自己和他人都能有所收获&#xff01; 一、前言 技术成长&#xff0c;是对场景设计细节不断的雕刻&#xff01; 你觉得自己的技术什么时候得到了快速的提高&#xff0c;是CRUD写的多了以后吗&#xff1f;想都不要想&#xff0c;绝对不可能&#xf…

composer 常用命令

### 设置镜像源全局设置composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/当个项目设置composer config repo.packagist composer https://mirrors.aliyun.com/composer/恢复官方源composer config -g --unset repos.packagist### 常用源阿里云…

【python】Python爬虫入门教程:使用requests库

Python爬虫入门教程&#xff1a;使用requests库 爬虫是数据获取的重要手段&#xff0c;下面我将通过一个完整的示例&#xff0c;教你如何使用Python的requests库编写一个简单的爬虫。我们将以爬取豆瓣电影Top250为例。 【python】网络爬虫教程 - 教你用python爬取豆瓣电影 Top…

OpenCV图像缩放:resize

图像缩放是图像处理中的基础操作之一。无论是图像预处理、数据增强还是图像金字塔构建&#xff0c;cv::resize 都是我们最常用的函数之一。但你是否注意到&#xff0c;在 OpenCV 中同时还存在一个名为 cv::Mat::resize 的方法&#xff1f;这两个函数虽然名字类似&#xff0c;但…

汽车、航空航天、适用工业虚拟装配解决方案

一、现状在制造业数字化转型浪潮中&#xff0c;传统装配过程仍面临诸多挑战&#xff1a;物理样机试错成本高、装配周期冗长、工艺优化依赖经验、跨部门协作效率低下……如何打破“试错-返工”的恶性循环&#xff1f;目前总装工艺通过DELMIA、NX、Creo等工程软件进行工艺装配验证…

页面跳转和前端路由的区别

传统方式&#xff1a;通过改变浏览器地址栏的 URL 来实现window.location.href /new-page<a href"/new-page">跳转到新页面</a>会导致整个页面重新加载会触发浏览器向服务器发送新的请求页面状态不会保留&#xff0c;所有资源重新加载可以避免新上线的内…

C/C++核心知识点详解

C/C核心知识点详解 1. 变量的声明与定义&#xff1a;内存分配的本质区别 核心概念 在C/C中&#xff0c;变量的声明和定义是两个完全不同的概念&#xff1a; 声明&#xff08;Declaration&#xff09;&#xff1a;告诉编译器变量的名称和类型&#xff0c;但不分配内存空间定义&a…

物联网发展:从概念到应用的演变历程

物联网的发展历程是一部技术革新与社会需求共同驱动的进化史&#xff0c;其演变可划分为概念萌芽、技术积累、应用拓展和智能融合四个阶段&#xff0c;每个阶段均以关键技术突破或社会需求变革为标志&#xff0c;最终形成万物互联的智能生态。以下是具体演变历程&#xff1a;一…

一个人开发一个App(数据库)

后端要保存数据&#xff0c;我还是选择了关系型数据库Mysql, 因为其它的不熟悉。 flutter端这次我选择的是ObjectBox&#xff0c;以前都是直接用的sqlite3&#xff0c;看对比ObjectBox效率比sqlite3高许多&#xff0c;这次前端为了用户体验&#xff0c;我需要缓存数据&#xff…