
简介面向苹果移动端开发者的UIA抽奖转盘效果资源包聚焦手机应用中实现类似建设银行客户端大转盘的互动界面方案适合初、中级开发者在自定义动画、触摸交互与界面适配方面参考学习。资源一共包含二十八个文件以源码文件、界面布局文件、图片素材及工程配置文件为主体压缩后仅三百四十六千字节轻量紧凑便于快速下载与部署。已有二百二十五人浏览学习。压缩包内代码示例覆盖了自动布局与尺寸类别适配、核心动画关键帧旋转、视图动画、贝塞尔曲线绘制转盘形状、拖动手势识别以及基于数据模型驱动界面更新等核心知识点同时涉及音效反馈与显示刷新同步的性能优化可直接提取图片与界面结构快速搭建可复用的抽奖转盘功能模块便于开发者打造流畅且富有吸引力的互动界面。1. UIA.zip 不是压缩包那么简单它是 UIAutomation 的遗产与门禁拿到一个名为UIA.zip_UIA_iphone的压缩包第一反应是解压、找 README、跑脚本。但如果你在 iOS 自动化测试这个领域待过几年看到 UIA 这个前缀就该意识到这个包大概率不是 Appium 或 XCUITest 的产物而是 Apple 在 Xcode 8 之前主推的 UIAutomation 框架留下的老代码——Instruments 里的 Automation 模板、.js格式的测试脚本、UIATarget和UIAApplication的 API 调用以及一堆只有真机调试时才会遇到的签名和部署配置。这个标题本身透露的信息是有人把整套 UIA 测试工程打成了 zip拿到 iPhone 上跑。今天这篇文章就从 UIAutomation 的核心机制讲起落到怎么把这个包里的东西真正在 iPhone 上跑通以及跑不通时看哪里。2. UIA 的骨架UIAElement 树与脚本运行机制2.1 UIA 脚本不是普通 JS它绑定的是 Instruments 的运行时UIAutomation 的脚本语法看起来像 JavaScript但它的对象模型完全由 Instruments 的 Automation 模板注入。一个最小可跑的脚本长这样var target UIATarget.localTarget(); var app target.frontMostApp(); var window app.mainWindow(); target.logElementTree();这个脚本在 Instruments 的 Automation 模板里执行时UIATarget代表物理设备或模拟器本身frontMostApp()返回当前前台 App 的句柄logElementTree()把当前界面的控件树打印到日志面板。你用 Node.js 去跑这段代码会直接报UIATarget is not defined因为这套 API 只存在于 Instruments 的 JS 运行时里。2.2 UIAElement 树的层级从 UIATarget 到 UIAButtonUIA 脚本的核心操作是沿着 UIAElement 树做遍历和点击。树的大致层级是UIATarget - UIAApplication - UIAWindow - UIANavigationBar - UIAButton - UIATableView - UIATableCell - UIAStaticText每层对象都有elements()方法返回子元素数组也有firstWithName()和firstWithPredicate()做查找。firstWithPredicate()接受的是 NSPredicate 语法这是 UIA 里最实用的过滤手段var cell app.mainWindow().tableViews()[0].cells() .firstWithPredicate(name BEGINSWITH 订单 AND visible 1); cell.tap();BEGINSWITH和visible 1是两个高频过滤条件前者处理动态拼接的控件名后者避开屏幕外但存在于树里的元素。很多 UIA 脚本跑着跑着就点错位置多数是因为没用visible 1点到了离屏元素上。2.3 命令行执行 UIA 脚本instruments 工具的完整参数在 Xcode 8 之前跑 UIA 脚本的标准姿势是instruments -t /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/AutomationInstrument.xrplugin/Contents/Resources/Automation.tracetemplate \ -D /tmp/trace_output \ -w iPhone 6s \ /path/to/YourApp.app \ -e UIARESULTSPATH /tmp/uia_results \ -e UIASCRIPT /path/to/test.js参数拆解参数作用常见坑-t指定 Automation 模板路径Xcode 版本升级后路径变化最常见报错来源-D输出 trace 文件路径目录不存在时 Instruments 不会自动创建-w指定目标设备支持设备名或 UDID设备名带空格会解析失败优先用 UDID-e UIARESULTSPATH指定结果输出目录目录需要预先创建否则结果写不进去-e UIASCRIPT指定 JS 脚本路径相对路径在 Instruments 的工作目录下解析建议用绝对路径跑完后到/tmp/uia_results目录看AutomationResult.plist里面记录了每条用例的通过/失败状态和截图路径。2.4 延时与同步UIA 脚本的时序控制UIA 脚本操作 UI 时Instruments 会等 App 空闲后才执行下一步。但tap()之后的界面跳转是异步的如果脚本紧接着查找下一个元素经常查不到。常见做法是target.delay(2);delay()的单位是秒但它是硬等待2 秒就是 2 秒不够智能。更稳的方式是轮询等待某个元素出现function waitForElement(element, timeout) { var deadline new Date().getTime() timeout * 1000; while (new Date().getTime() deadline) { if (element.isValid() element.isVisible()) { return true; } target.delay(0.5); } return false; } var okButton app.mainWindow().buttons()[确定]; if (waitForElement(okButton, 10)) { okButton.tap(); } else { throw new Error(确定按钮在 10 秒内未出现); }isValid()判断的是当前脚本快照里元素是否还挂在这棵树上isVisible()判断是否在屏幕上可见。这两个方法配合轮询比裸delay()能扛住更多网络波动导致的页面卡顿。提示UIA 脚本里的throw会导致整个用例中断这个行为可以用来做失败断言。Instruments 会把异常信息记录到结果 plist 里。3. UIA.zip 的包内结构从解压到能在 iPhone 上执行3.1 典型目录结构长什么样一个完整的UIA.zip解压后内部目录通常长这样UIA/ ├── Automation/ │ └── test.js ├── UIDemo.app ├── UIDemo.app.dSYM ├── config.plist └── run_uia.shAutomation/test.js是测试脚本本体UIDemo.app是被测 App 的 bundle.dSYM是符号文件用于 crash 时的符号化。config.plist里一般存设备 UDID、脚本路径、结果输出路径等配置。run_uia.sh是把第二节提到的长命令封装成一行执行的入口脚本。3.2 run_uia.sh 这个入口脚本要检查什么拿到别人的 UIA.zip最忌讳直接双击 run_uia.sh。先打开这个脚本文件检查里面的三件事模板路径是否与当前 Xcode 版本匹配。-w后面的 UDID 是否跟当前连接的 iPhone 一致。-e UIASCRIPT指向的 JS 文件是否存在。如果脚本里的路径还是Xcode 5.x或Xcode 6.x时代的旧路径需要手动改到当前 Xcode 的 Automation 模板位置。我一般会在脚本开头加一段自动探测AUTOMATION_TEMPLATE$(find /Applications/Xcode.app \ -name Automation.tracetemplate -type d 2/dev/null | head -1) if [ -z $AUTOMATION_TEMPLATE ]; then echo 找不到 Automation 模板请确认 Xcode 版本是否支持 UIAutomation exit 1 fifind从 Xcode 应用包根目录往下找匹配到第一个名为Automation.tracetemplate的目录就作为模板路径。这样 Xcode 小版本升级后路径变了也不影响运行但 Xcode 8 及以上版本里这套模板已经彻底移除find会返回空脚本直接报错退出这本身就是有用的诊断信息。3.3 config.plist 里的参数与真机部署的关系config.plist的内容用plutil命令可以查看plutil -p config.plist里面常见的键值keydeviceUDID/key stringxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/string keybundleID/key stringcom.example.uidemo/string keytestScriptPath/key stringAutomation/test.js/string keyresultPath/key string/tmp/uia_results/stringdeviceUDID决定-w参数如果解压后设备的 UDID 变了需要更新这个值。resultPath建议设成一个绝对路径下已存在的目录因为 Instruments 不会自动递归创建目录。3.4 手把手在 iPhone 上跑通一条 UIA 用例前提条件一台 iPhoneiOS 9 或更低版本、一台 Mac装有 Xcode 7 或更低版本、一根数据线、一个已签名的 App。步骤是# 1. 解压 UIA.zip unzip UIA.zip -d ~/Desktop/UIA_project cd ~/Desktop/UIA_project # 2. 查看当前连接的设备 UDID idevice_id -l # 3. 修改 config.plist 里的 deviceUDID /usr/libexec/PlistBuddy -c Set :deviceUDID 你的UDID config.plist # 4. 给脚本加执行权限并运行 chmod x run_uia.sh ./run_uia.shidevice_id -l来自 libimobiledevice 工具集没装的话用 Xcode 的Window Devices and Simulators界面也能看到 UDID只是命令行更直接。第 3 步用PlistBuddy改配置比手动编辑 plist 文件更可靠因为它能保持二进制 plist 的格式避免手误损坏文件。4. 跑 UIA 的 5 个高频失败点和对应修复4.1 Xcode 版本不兼容Automation 模板路径变了Xcode 从 5 到 7 的演进中Automation 模板的路径一直在变。报错信息类似Template does not exist或Instruments crashed。修复方式前面已经说过用find动态搜索是最方便的。如果find也找不到说明当前 Xcode 版本已经移除了 UIAutomation这时候只有两条路换回 Xcode 7或者放弃 UIA 改用 XCTest。4.2 设备系统版本过高iOS 10 以上跑不了 UIAiOS 10 开始Apple 移除了 UIAutomation 的运行时支持。如果你手里的 iPhone 系统版本是 iOS 10 或更高instruments 命令虽然能启动但脚本会卡在UIATarget.localTarget()这一步然后报一个没有明确描述的 timeout 错误。这是一个硬边界不是参数能解决的。你只能找一台系统版本在 iOS 9 或更低的设备来跑 UIA。4.3 JS 脚本语法错误UIA 兼容的是旧版 JS 引擎UIA 脚本运行在 Instruments 内置的 JavaScriptCore 上用的是 ES5 标准。如果你在脚本里写了let、const、箭头函数会直接语法报错。把源码里的 ES6 写法全部换成var和function。这块没有捷径只能逐行排查报错信息里会给出行号。4.4 签名与证书问题App 装不上设备-w指定了设备却启动不了 App去Window Devices and Simulators里看这个 App 的安装状态。如果显示灰色说明签名已经失效。常见做法是重新用 Xcode 跑一次工程装上设备确保设备上已经有可启动的 App 实例。UIA 脚本启动 App 的方式是直接拉起已安装的应用如果设备上根本没有这个 App脚本会等待超时。4.5 UIASCRIPT 路径指向错误用了相对路径Instruments 执行脚本时的工作目录不确定相对路径经常解析不到文件报File not found。修复方式是把脚本路径写绝对路径或者在 run_uia.sh 里先cd到 UIA 工程根目录再执行cd $(dirname $0)dirname $0拿到当前脚本所在目录cd进去之后相对路径就有了稳定的参照点。这个技巧在config.plist里写Automation/test.js这种相对路径时特别好用。4.6 捕获崩溃日志当 App 在执行中点崩了脚本跑了一半 App crash这时候需要从设备上拉取 crash 日志idevicecrashreport -e /tmp/crash_reports-e参数表示提取后从设备上删除日志避免堆积。拉取下来的.ips文件用 symbolicatecrash 做符号化./symbolicatecrash /tmp/crash_reports/xxx.ips /path/to/UIDemo.app.dSYM提示symbolicatecrash 在 Xcode 的Contents/Developer/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/目录下找到后直接调用即可。符号化之后看到的堆栈才是人能看懂的类名和方法名。5. 从 UIA 到 XCTest保留脚本资产的过渡技巧5.1 UIA 脚本转 XCTest 的映射思路UIAutomation 虽然已经被淘汰但UIA.zip里的测试资产并非毫无价值。.js文件里的测试步骤——点击某个按钮、等待某个元素出现、断言某个文本存在——这些业务逻辑可以一对一映射到 XCTest 的XCUIApplicationAPI 上UIA JS 代码XCTest 等价写法target.frontMostApp().mainWindow()XCUIApplication()buttons()[确定]app.buttons[确定]tap().tap()target.delay(2)sleep(2)element.isVisible()element.isHittablethrow new Error(断言)XCTAssertTrue(...)5.2 保留 UIA 轮询逻辑的一个思路UIA 脚本里最值得借鉴的是轮询等待的写法。XCTest 里没有target.delay()这种全局等待但可以自己封装一个func waitForElement(_ element: XCUIElement, timeout: TimeInterval) - Bool { let predicate NSPredicate(format: exists true) let expectation XCTNSPredicateExpectation(predicate: predicate, object: element) return XCTWaiter().wait(for: [expectation], timeout: timeout) .completed }5.3 把 UIA 的元素路径导出成 XCTest 的辅助函数UIA 脚本里大量出现window.tableViews()[0].cells()[2].buttons()[0]这种索引链。对应到 XCTest 里可以保持同样的层级访问let cell app.tables.element(boundBy: 0).cells.element(boundBy: 2) let button cell.buttons.element(boundBy: 0) button.tap()element(boundBy:)按索引取元素cell.buttons返回 cell 下所有按钮的查询集合。这样把 UIA 脚本里的操作步骤逐行翻译不需要重新梳理业务逻辑只改 API 调用方式迁移成本比完全重写低很多。5.4 验证迁移正确性用同一台 iPhone 对比执行结果迁移完成后把 UIA 脚本和 XCTest 用例放在同一台 iPhone 上跑一遍看两条执行路径的最终停靠页面是否一致。UIA 脚本的最后一个步骤通常是target.logElementTree()打印出当前界面的完整元素树XCTest 端用print(app.debugDescription)输出同样的树结构。两个输出做文本比对除开系统自动追加的 frame 信息业务控件的层级和名称应该完全吻合。这个对比方法不依赖任何外部工具一条 diff 命令就能看出迁移有没有漏控件。本文还有配套的精品资源点击获取