The fewer sources of truth we have for a design system, the more efficient we are.——Jon Gold

设计零碎的真谛起源越少,效率就越高。——Jon Gold,出名全栈设计师

背景

1. 积木工具链体系

前段时间咱们在美团技术团队公众号上发表了《积木Sketch Plugin:设计同学的贴心搭档》一文,未曾想到,这个仅在美团外卖C端应用的插件受到了更多的关注,美团多个业务团队纷纷向咱们抛出“橄榄枝”,示意想要接入以及并表白了违心共同开发的动向,其它互联网同行也纷纷询问相干的技术,一时让咱们有些“受宠若惊”。回想起写第一篇文章的时候,咱们的心田还是有些不安的。作为UI同学的一个设计工具,有些RD甚至没有据说过Sketch这个名字,咱们很认真地批改过上一篇文章的每一句措辞,争取让内容更丰盛乏味,过后还很放心不会被读者承受。

积木Sketch插件的“意外走红”,的确有些出乎咱们的预料,但正是如此,才让咱们晓得UI一致性是绝大部分开发团队面临的共性问题,大家对落地设计规范,进步UI中台能力,晋升产研效率都有着强烈的诉求。为了帮忙更多团队晋升产研效率,咱们成立了袋鼠UI共建项目组,将门户建设、工具链建设以及组件建设对立治理统一规划,并将工具链的品牌确定为“积木”,而积木Sketch插件便是其中重要的一环。

咱们通过建设蕴含雷同设计元素的对立物料市场,PM通过Axure插件拾取物料市场中的组件产出原型稿;UI/UE通过Sketch插件落地物料市场中的设计规范,产出符合要求的设计稿;而物料市场中的组件又与RD代码仓库中的组件一一对应,从而造成了一个闭环。将来,咱们心愿通过高保真原型输入,能够给中后盾我的项目、非依赖体验我的项目提供更好的服务体验,赋予产品同学间接向技术侧输入原型稿的能力。

2. 积木插件平台化

随同着“积木”品牌的确立,越来越多的团队心愿能够接入积木Sketch插件,其中局部团队也在和咱们探讨技术单干的可能性。UI设计语言与本身业务关联性很强,不同业务的色调零碎、图形、栅格零碎、投影零碎、图文关系千差万别,其中任意一环的缺失都会导致一致性被毁坏,业务方都心愿通过积木插件实现设计规范的落地。为了帮忙更多团队的UI同学晋升设计效率,节约RD同学页面调整的工夫,同时也让App界面具备一致性,从而更好地传播品牌主张和设计理念,咱们决定对积木插件进行平台化革新。平台化是指积木插件能够接入各个业务团队的整套设计规范,通过平台化革新,能够使积木插件提供的设计元素与业务强关联,满足不同业务团队的设计需要。

积木插件本来只是外卖晋升UI/RD合作效率的一次尝试,最后的指标仅是UI一致性,然而当初曾经作为全面晋升产研效率的媒介,承载了越来越多的性能。围绕设计日常工作,提供高效的设计元素获取形式,让工作变得更轻松,是积木的外围使命。如何推动设计规范落地,并且输入到各个业务零碎灵便应用,是咱们继续追寻的答案。而探寻研发和设计更为高效的合作模式也是咱们始终致力的方向。

通过一段时间的平台化建设,目前美团曾经有7个设计团队接入了积木插件,笼罩了美团到家事业部大部分设计同学,将来咱们会继续推动积木插件的平台化建设,不断完善性能,冀望能将积木插件打造成业界一流的品牌。

3. Sketch插件开发进阶

第一篇文章可能是为数不多的入门教程,而本篇可能是你能找到的惟一一篇进阶开发文章。进阶开发次要波及如何切换业务方数据,即抉择所属业务方后,对应的组件、色彩等设计素材切换为以后业务方在物料市场中上传的元素;将承载组件库的Library文件转化为插件能够辨认的格局,并在插件上展现,以供设计师在绘制设计稿时抉择应用;一些优化运行效率,晋升用户体验的办法。

Sketch插件代码因为和业务强相干,且实现形式较为简单,可能存在局部敏感信息,所以根本没有成熟的插件开源。在进行一些简单性能开发时,咱们也经常“丈二和尚摸不到头脑”,“要不这个性能算了吧”的想法也不止一次呈现,可是每当散会看到旁边的设计同学在应用“积木”插件认真作图时,又一次次动摇了咱们的信念,要不加班再试试吧,没准就能实现了呢?一次“忍辱负重”,前面可能导致整个我的项目缓缓崩塌,所以咱们始终以将积木插件打造成为业界当先的插件为信念。如果说看过了第一篇文章你曾经晓得了如何开发一款插件,那么通过本篇文章的学习你就能够真正实现一款能够与业务强关联且性能可定制的成熟工具,与其说是介绍如何开发一个进阶版的Sketch插件,不如说是分享给大家实现一个商业化我的项目的教训。

反对多业务切换

为了当面对“咱们能够接入积木插件吗”这种灵魂拷问时不再不知所措,平台化过程迅速启动。平台化的外围其实就是当产生业务线变更时,物料市场中的素材整体同步切换,因而咱们须要进行如下几个操作:首先建设全局变量,存储以后用户所述业务方信息及鉴权信息;用户抉择功能模块后,依据用户所述业务方,拉取对应素材;解决Library等素材并渲染页面展现;依据素材信息变更画板中的相干Layer。这部分次要介绍如何依附长久化存储来实现业务切换的性能,就像在第一篇启蒙文档中说的那样,这里不会贴大段的代码,只会帮你梳理最外围的流程,置信你亲自实际一次之后,当前的艰难都能够轻松解决。

1. 定义通用变量

功能模块展现的素材与以后抉择的业务相干,因而须要在每个功能模块的Redux初始化状态中减少一些全局状态变量。比方所有模块都须要应用businessType来确定以后抉择的业务,应用theme进行主题切换,应用commonRequestParams获取用户鉴权信息等。

export const ACTION_TYPE = 'roo/colorData/index';const intialState = {  id: 'color',  title: '色彩库',  theme: 'black',  businessType: 'waimai-c',  commonRequestParams: {    userInfo: '',  },};export default reducerCreator(intialState, ACTION_TYPE);

2. 实现数据交换

第一步:WebView侧获取用户抉择,将所选的业务方数据通过window.postMessage办法传递至插件侧。

window.postMessage('onBusinessSelected', item);

第二步:Plugin侧通过webViewContents.on( )办法接管从WebView侧传递过去的数据。Sketch官网通过Settings API提供了一些类的办法来解决用户的参数设置,这些设置在Sketch敞开后仍然会保留,除了存储一段JSON数据外,Layer、Document甚至是Session variable都是反对的。

 webViewContents.on('onBusinessSelected', item => {    Settings.setSettingForKey('onBusinessSelected', JSON.stringify(item));  });// 除此之外,插件侧也能够通过localStorage向WebView注入数据browserWindow.webContents.executeJavaScript(      `localStorage.setItem("${key}",'${data}')`);

第三步:当用户通过工具栏抉择某一功能模块(例如“插画库”)时,会回调NSButton的点击事件监听,此时除了须要要让WebView展现(Show)以及获取焦点(Focus)外,还须要将第二步存储的业务方信息传入,并以此加载以后业务方的物料数据。

//用户关上的功能模块const button = NSButton.alloc().initWithFrame(rect)   button.setCOSJSTargetFunction(() => {    initWebviewData(browserWindow);    browserWindow.focus();    browserWindow.show();  });// 注入全局初始化信息function initWebviewData(browserWindow) {  const businessItem = Settings.settingForKey('onBusinessSelected');  browserWindow.webContents.executeJavaScript(`initBusinessData(${businessItem})`);}

WebView侧功能模块收到初始化信息,开始进行页面渲染前的数据筹备。Object.keys()办法会返回一个由给定对象的本身可枚举属性组成的数组,遍历这个数组即可拿到所有被注入的初始化数据,之后通过redux的store.dispatch办法更新state即可。至此实现业务切换性能的流程就全副完结了,是不是感觉非常简单,忍不住想亲自动手试一下呢?

ReactDOM.render(<Provider store={store}><App /></Provider>,  document.getElementById('root'));window.initBusinessData = data => {  const businessItem = {};  Object.keys(data).forEach(key => {    businessItem[key] = { $set: initialData[key] };  });  store.dispatch(update(businessItem));};const update = payload => ({  type: ACTION_TYPE,  payload,});

3. 小结

有小伙伴会问,为什么WebView与Plugin侧须要数据传递呢,它们不都属于插件的一部分么?根本原因是咱们的界面是通过WebView展现的,然而对Layer的各种操作是通过Sketch的API实现的,WebView只是一个网页,自身与Sketch并无关系,因而必须应用bridge在两者之间进行数据传递。别放心,这里再带你把整个流程梳理一遍:①在插件启动后会从服务端拉取业务方列表;②用户在WebView中抉择本人所属的业务方;③将业务方数据通过bridge传递至Plugin侧,并通过Sketch的Settings API进行长久化存储,这样就能够保障每次启动Sketch的时候无需再次抉择所属业务方;④用户点击插件工具栏的按钮抉择所需性能(例如色板库、组件库等),从长久化数据中读取以后所属业务方,并告诉WebView侧拉取以后业务方数据。至此,整个流程完结。

Library库文件自动化解决

这部分将介绍如何将Library库文件转化为插件能够辨认的JSON格局,并在插件上展现。

如果要问Sketch插件最重要的性能是什么,组件库相对是无可争议的C位。在长期的版本迭代中,随着性能的一直减少以及UI的继续改版,新旧款式混淆,保护极为艰难。设计师通过将页面走查后果演绎梳理,制订设计规范,从而选取复用性高的组件进行组件库搭建。通过搭建组件库能够进行标准管制,防止控件的随便组合,缩小页面差别;组件库中组件满足业务特色,同时具备云端动静调整能力,能够在标准更新时进行对立调整。

目前,咱们将组件集成进Sketch供UI应用大抵分为两个流派:一个是基于Sketch官网的Library库文件,设计师通过将业务中复用性高的Symbol组件演绎整顿生成库文件(后缀.sketch),并上传至云端,插件拉取库文件转化为JSON并在操作面板展现供选取应用;另一个则是采纳相似Airbnb开源的React-Sketchapp这样的框架,它能够让你应用React代码来制作和治理视觉稿及相干设计资源,官网把它称作“用代码来绘画”,这种计划的施行难度较大,因为实质上设计是理性和感性的联合,设计师应用Sketch是画,而非带有逻辑和层级关系的写,他们对于页面的树形构造很难了解,上手老本较高,而且代码保护老本绝对较大。咱们不去评估哪种计划的好坏,只是第一种计划能够更好地满足咱们的外围诉求。

1. 订阅近程组件库

Library库文件实际上是一个蕴含components的文档,components包含了Symbols、Text Styles以及Layer Styles三类,将Library存储在云端就能够在不同文档甚至不同团队间共享这些components。因为组件库实时指向最新,因而当其维护者更新库中的components时,应用了这些components的文档将会收到告诉,这能够保障设计稿永远指向最新的设计规范。

订阅云端组件库的形式很简略,首先创立一个云端组件库,具体能够参照上一篇文章,如果须要服务多个设计部门,则须要创立多个库,每个库有惟一的RSS地址;在插件中获取到这些RSS地址后,能够通过Library.getRemoteLibraryWithRSS办法对其进行订阅。

// 启动插件时增加近程组件库export const addRemoteLibrary = context => {  fetch(LibraryListURL, {    method: 'POST',    headers: {      'Content-Type': 'application/json',    },  })    .then(res => res.json())    .then(response => response.data)    .then(json => {      const { remoteLibraryList } = json;      _.forEach(remoteLibraryList, fileName => {        Library.getRemoteLibraryWithRSS(fileName, (err, library) => {        });      });      return list;    });};

2. Library库文件转换JSON数据

将Sketch的Library文件转换为JSON的过程,实际上就是转换为WebView能够辨认格局的过程。因为积木插件是将组件依照肯定分组展现在面板中供设计师选取,因而须要依据组件分类组织其构造。Sketch原生反对采纳 "/" 符号对其进行分组:Group-name/Symbol-name,比方命名为Button/Normal和Button/Pressed的两个Symbols会成为Button Group的一部分。

理论中能够依据业务须要采纳三级以上分组命名的形式,通过split办法将Symbol名称通过 "/" 符号拆分为数组,第一级名称、第二级名称等各级名称作为JSON构造的不同层级即可,具体操作能够参照如下示例代码:

 const document = library.getDocument();    const symbols = [];    _.forEach(document.pages, page => {      _.forEach(page.layers, l => {        if (l.type && l.type === 'SymbolMaster') {          symbols.push(l);        }      });    }); // 对symbol进行分组解决,并生成json数据 for (let i = 0; i < symbols.length; i++) {      const name = symbols[i].name;      const subNames = name.split('/');      // 去掉所有空格      const groupName = subNames[0].replace(/\s/g, '');      const typeName = subNames[1].replace(/\s/g, '');      const symbolName = subNames.join('/').replace(/\s/g, '');      result[groupName] = result[groupName] || {};      result[groupName][typeName] = result[groupName][typeName] || [];      result[groupName][typeName].push({        symbolID:symbolID,        name: symbolName,      }); }

通过以上操作后,一个简化版的JSON文件如下方所示:

{    "美团外卖C端组件库": {        "icon": [{            "symbolID": "E35D2CE8-4276-45A1-972D-E14A06B0CD23",            "name": "28/问号"        },{            "symbolID": "E57D2CE8-4276-45A1-962D-E14A06B0CD61",            "name": "27/花朵"        }]    }}

3. Symbol缩略图解决

WebView默认是不反对间接显示Symbol供用户拖拽应用的,解决该问题的计划有两种:(1)通过dump剖析Sketch的头文件发现,能够采纳MSSymbolPreviewGenerator的imageForSymbolAncestry办法导出缩略图,该办法反对图片大小、色彩空间等多种属性设置,劣势是较为灵便,能够依据须要进行任意配置,不过要承当前期API变更的危险;(2)间接采纳sketchDOM提供的export办法,将Symbol组件导出为缩略图,之后在WebView中显示缩略图,当拖拽缩略图至画板时,再将其替换为Library中对应的Symbol即可。

import sketchDOM from 'sketch/dom';sketchDOM.export(symbolMaster, {     overwriting: true,     'use-id-for-name': true,     output: path,     scales: '1',     formats: 'png',     compression: 1,});

4. 小结

以上就是实现平台化的一个根本流程了,不晓得此时你有没有听得“云里雾里”。在这里,再把外围点带大家温习一下。本节次要讲了两件事件:第一,插件如何能力反对多个业务方,即在插件的业务方列表中抉择相干业务方,就能够切换对应的设计资源;第二,如何解决Library文件,将其转换为JSON供WebView展现应用。具体流程如下:

  1. 不同设计组的UI同学制作实现蕴含各种components的Library后,通过后盾上传至云端。
  2. RD同学依据以后使用者所属的设计团队拉取对应的包含Library在内的设计素材,色彩、图片,iconFont等设计素材能够间接展现,可是Library文件不反对在WebView中间接显示,须要进行解决。
  3. 依据和UI同学约定组件的命名规定,通过应用“/”宰割,将第一级名称、第二级名称等各级名称作为JSON构造的不同层级,再通过sketchDOM提供的export办法将Symbol转换为png格局的缩略图即可在插件中显示。
  4. 将选中的缩略图拖拽至Sketch的画板时,再将缩略图替换为Library中的实在Symbol即可。

操作体验优化

实现了上述步骤后,就能够实现一款反对多业务方的插件了。然而随着积木Sketch插件接入的业务方越来越多,除了听到能够显著晋升效率的贬责外,对插件的吐槽声也经常传入咱们的耳边。市面上成熟的插件也有很多,咱们无奈限度他人的抉择,所以只能让积木变得更好用,本节次要介绍插件的优化办法。为了更好地聆听大家意见,积木插件通过各种措施理解用户的实在想法。首先积木插件接入美团外部的TT(Trouble Tracker)零碎,相比公司很多业余零碎,TT不带任何业余流程和定制化,只做纯流转,是一套实用于公司外部的、通用的问题发动、响应和追踪零碎,用户反馈的问题主动创立工单并与对应RD关联,Bug能够最疾速修复;插件外部减少反馈渠道,用户反馈及时发送给相干PM,作为下次性能排期的权重指标;插件外部减少多维度埋点统计,从设计渗透到高频应用两个方面理解UI同学的外围诉求。以下介绍了依据反馈整顿的局部高优先级问题的解决方案。

1. 操作界面优化

很多RD在开发过程中,对界面丑化往往不屑一顾,“这个性能能用就能够了”经常被挂在嘴边。难道UI的需要真的是中看不中用?一个产品设计师说过,最早的产品仅依附性能就能够在竞品中怀才不遇,能不能用就成为了一个产品是否合格的规范。起初在越来越成熟的互联网环境中,易用性成了一个新的且更重要的规范,这时同类产品间的性能曾经十分靠近,无奈通过一直重叠性能产生显著差别。而当同类产品的易用性也趋于相近时,如何解决产品竞争力的问题就再一次摆在面前,这时就要赋予产品情感,好的产品关注性能,优良的产品关注情感,能够让用户在应用中感触到和煦。

WebView优化

当咱们通过了认真的性能验证,兴高采烈的把第一版积木插件给用户体验时,原本满心欢喜筹备迎接夸赞,没想到却失去了很多交互体验不好的反馈,“仅仅实现性能就及格了”这个实践在一像素都不肯放过的设计师眼中必定行不通。原生WebView给用户的体验往往不够优良,其实只有一些很简略的设置就能够解决,然而这并不代表它不重要。

禁止触摸板拖拽造成页面整体“橡皮筋”成果,禁止用户抉择(user-select),溢出暗藏等操作,使WebView具备“类原生”成果,都会晋升用户的理论应用体验。

html,body,#root {  height: 100%;  overflow: hidden;  user-select: none;  background: transparent;  -webkit-user-select: none;}

除此之外在正式环境中(NODE_ENV为production),咱们并不心愿以后界面响应右键菜单,须要通过给document增加EventListener监听将相干事件处理办法屏蔽。

document.addEventListener('contextmenu', e => {  if (process.env.NODE_ENV === 'production') {    e.preventDefault();  }});

工具栏优化

Sketch对于设计师的意义,就像代码编辑器对于程序员一样,工作中简直无时无刻也离不开。在积木Sketch插件走出美团外卖,被越来越多的设计团队采纳后,为了让它更加赏心悦目,UI同学决定对工具条进行一次全新的视觉降级。原生界面开发指的是通过macOS的AppKit进行用户界面开发,在插件开发中一些须要嵌入Sketch面板的UI模块就须要进行原生界面开发,比方吸附式工具条就属于通过macOS原生API开发的界面。

原生开发既能够应用Objective-C语言,也能够应用CocoaScript通过写JavaScript的形式进行开发。CocoaScript 通过Mocha实现JS到Objective-C的映射,能够让咱们通过JS调用Sketch外部API以及macOS的Framework。在通过CocoaScript原生开发前须要理解一些基础知识:

  1. 在应用相干框架前须要通过framework()办法进行引入,而Foundation以及CoreGraphics是默认内置的,无需再独自操作。
  2. 一些Objective-C的selectors选择器须要指针参数,因为JavaScript不反对通过援用传递对象,因而CocoaScript提供了MOPointer作为变量援用的代理对象。

UI调整个别分为三个局部:布局调整、动效调整、图片替换。上面的章节会进行逐个介绍。

布局调整

这里UI的需要是NSButton的宽度填充斥整个NSStackView,高度自定义。因为此性能看起来过于简略,过后认为估时0.5天入不敷出,可是没想到搭进去了1个工作日加上2天周末的工夫,因为无论如何设置NSStackView中子View尺寸都无奈失效。

在顶住了四周人“UI问题不影响性能应用,当前有工夫再优化吧”的“舆论压力”后,终于在官网文档外面发现了线索:“NSStackView A stack view employs Auto Layout (the system’s constraint-based layout feature) to arrange and align an array of views according to your specification. To use a stack view effectively, you need to understand the basics of Auto Layout constraints as described in Auto Layout Guide.”简而言之,NSStackView应用constraints的形式进行主动布局(能够类比Android中的ConstraintLayout),在进行尺寸批改时,是须要增加锚点的,因而须要通过Anchor的形式进行尺寸批改。

// 创立工具条const toolbar = NSStackView.alloc().initWithFrame(NSMakeRect(0, 0, 45, 400));toolbar.setSpacing(7);// 创立NSButtonconst button = NSButton.alloc().initWithFrame(rect)// 设置NSButton宽高button    .widthAnchor()    .constraintEqualToConstant(rect.size.width)    .setActive(1);button    .heightAnchor()    .constraintEqualToConstant(rect.size.height)    .setActive(1);button.setBordered(false);// 设置回调点击事件button.setCOSJSTargetFunction(onClickListener);button.setAction('onClickListener:');// 增加NSButton至NSStackView中toolbar.addView_inGravity(button, inGravityType);

动效调整

NSButton内置的点击成果大概15种,能够通过NSBezelStyle进行设置。积木插件工具栏并没有采纳点击后icon反色的通用解决形式,而是点击后将背景色置为浅灰。如果想要自定义一些点击成果,只需在NSButton点击事件的回调中设置即可。

onClickListener:sender => {  const threadDictionary = NSThread.mainThread().threadDictionary();  const currentButton = threadDictionary[identifier];   if (currentButton.state() === NSOnState) {      currentButton.setBackgroundColor(NSColor.colorWithHex('#E3E3E3'));   } else {      currentButton.setBackgroundColor(NSColor.windowBackgroundColor());   }}

图片加载

Sketch插件既反对加载本地图片,也反对加载网络图片。加载本地图片时,能够通过context.plugin的办法获取一个MSPluginBundle对象,即以后插件bundle文件,它的url()办法会返回以后插件的门路信息,进而帮忙咱们找到存储在插件中的本地文件;而加载网络图片则更加简略,通过NSURL.URLWithString( )能够取得一个应用图片网址初始化失去的NSURL对象,这里要分外留神的是,对于网络图片请应用https域名。

//本地图片加载const localImageUrl =        context.plugin.url()      .URLByAppendingPathComponent('Contents')      .URLByAppendingPathComponent('Resources')      .URLByAppendingPathComponent(`${imageurl}.png`);//网络图片加载const remoteImageUrl = NSURL.URLWithString(imageUrl);//依据ImageUrl获取NSImage对象const nsImage = NSImage.alloc().initWithContentsOfURL(imageURL);nsImage.setSize(size);nsImage.setScalesWhenResized(true);

2. 执行效率优化

只有在设计稿中尽可能多地应用组件进行设计,并且将已有页面中的内容通过设计师的走查梳理逐步替换成组件,能力真正通过建设组件库来进行提效。随着设计团队逐渐将设计语言积淀为设计规范,并将其量化内置于积木插件中,组件的数量越来越多,积木插件组件库作为UI同学应用最频繁的性能,须要分外关注其运行效率。

前置组件库加载

将组件库的加载逻辑前置,在打开文档时对近程组件库进行订阅操作。Sketch所提供的了Action API能够使插件对应用程序中的事件做出反馈,监听回调只需在插件的manifest.json文件中增加一个handler即可,增加了对于“OpenDocument”的监听,也就是通知插件在新文档被关上时要去执行addRemoteLibrary这个function。

{      "script": "./libraryProcessor.js",      "identifier": "libraryProcessor",      "handlers": {        "actions": {          "OpenDocument": "addRemoteLibrary"        }      }}

减少缓存逻辑

组件库的解决须要将Library文件转换为带有层级信息的JSON文件,并且须要将Symbol导出为缩略图显示。因为这个步骤较为耗时,因而能够将通过解决的Library信息缓存起来,并通过长久化存储记录已缓存的Library版本。若已缓存的版本与最新版本统一,且缩略图与JSON文件均残缺,则能够间接应用缓存信息,极大的进步Library的加载速度。以下非残缺代码,仅作示例:

verifyLibraryCache(businessType, libraryVersion) {    const temp = Settings.settingForKey('libraryJsonCache');    const libraryJsonCache = temp ? JSON.parse(temp) : null;    // 1.验证缓存版本信息    if (libraryJsonCache.version.businessType !== libraryVersion) {      return null;    }    // 2.验证缩略图完整性    const home = getAssertURL(this.mContext, 'libraryImage');    const path = join(home, businessType);    if (!fs.existsSync(path) || !fs.readdirSync(path)) {      return null;    }    // 3.验证业务库Json文件完整性    if (libraryJsonCache[businessType]) {      console.info(`以后${businessType}命中缓存`);      return libraryJsonCache;    } else {      return null;    }  }}

3. 自定义Inspector属性面板

与Objective-C工程混合开发

随着各个设计组的组件库建设不断完善,抽离的组件数量一直增多,不少UI同学反馈Sketch原生组件款式批改面板操作不够便捷,无奈束缚抉择范畴,心愿能够提供一种更无效的组件overrides批改形式,并且当批改“图片”、“图标”、“文字”等图层时,能够和积木插件的这些功能模块进行联动抉择。实现自定义Inspector面板性能既能够使操作更便捷,又能够对批改项进行束缚。

自定义属性面板性能的根本思维,是将组件从组件库拖至Sketch画板中时,组件的可批改属性能够显示在Sketch自身的属性面板上。咱们引入了Objective-C原生开发以实现对Sketch界面的批改,为什么要应用原生开发?尽管官网提供了JS API并承诺继续保护,但这项工作始终处于Doing状态,而且官网文档更新迟缓,没有明确的工夫节点,因而对于自定义Native Inspector Panel这种须要Hook API的性能,应用原生开发较为便捷,而且对于iOS开发者也更加敌对,无需再学习前端界面开发常识。

Xcode工程配置

通过Xcode工程构建自定义属性面板,最终生成一个能够供JS侧调用的Framework。能够参考上一篇文章介绍的办法创立Xcode工程,该工程在每次构建后会主动生成测试Sketch插件并放入对应的文件夹中。须要留神的一点是,这里生成的插件只是为了不便开发和调试,前面会介绍如何将XCode工程构建的Framework集成至JS主工程中。

积木插件的主体性能应用JS代码实现,然而自定义属性抉择面板应用Objective-C代码实现。为了实现积木插件的JS侧功能模块与OC侧模块之间的通信和桥接,这里借助了Mocha框架来实现相干的性能,Mocha框架也被Sketch官网所应用,将原生侧的办法封装为官网API后裸露给JS侧。

组件选中时,Sketch软件会回调onSelectionChanged办法给JS侧,JS侧借助Mocha框架能够实现对OC侧的调用,同时将参数以OC对象的形式传递。JS侧传递给OC侧的Context内容很丰盛,蕴含了选中的组件、相干图层还有Sketch软件自身的信息。尽管Sketch没有提供API,然而Objective-C语言自身具备KVO监听对象属性的能力,咱们通过读取对应的属性值,就能够获取须要的对象数据。

+ (instancetype)onSelectionChanged:(id)context {     [self setSharedCommand:[context valueForKeyPath:@"command"]];        NSString *key = [NSString stringWithFormat:@"%@-RooSketchPluginNativeUI", [document description]];    __block RooSketchPluginNativeUI *instance = [[Mocha sharedRuntime] valueForKey:key];    NSArray *selection = [context valueForKeyPath:@"actionContext.document.selectedLayers"];    [instance onSelectionChange:selection];    return instance;}

Sketch官网没有将属性面板的批改能力裸露给插件侧,通过查问Sketch头文件发现通过reloadWithViewControllers:办法能够实现属性面板刷新,然而在理论开发过程中发现在某些版本的Sketch上会呈现面板闪动的问题,这里借助Objective-C的Method Swizzle个性,间接批改reloadWithViewControllers:的运行时行为解决。

[NSClassFromString(@"MSInspectorStackView") swizzleMethod:@selector(reloadWithViewControllers:)                                        withMethod:@selector(roo_reloadWithViewControllers:)                                                        error:nil];


Swizzle办法会批改原始办法的行为,实际操作中只有在满足特定条件的状况下才应触发Swizzle后的办法。

组件属性批改与替换原理

通过自定义面板能够批改组件的可笼罩项(即override),目前能够利用可笼罩项的affectedLayer有Text/Image/Symbol Instance三种。设计师与开发者在此前对图层的格局进行了约定,保障咱们能够依照对立的形式读取并替换图层的属性值。

替换文本

基于class-dump,咱们能够找出Sketch中申明的所有类的属性和办法,文本处理的策略是,找到图层中的所有MSAvailableOverride对象,这些对象即示意可用的笼罩项,对文本信息的批改实际上是通过批改MSAvailableOverride对象的overridePoint来实现的。

id overridePoint = [availableOverride valueForKeyPath:@"overridePoint"];[symbolInstance setValue:text forOverridePoint:overridePoint];

更改款式

款式设置的策略,是找到以后选中组件对应的Library中相干款式的组件。因为所有的组件都遵循对立的命名格局,因而只有依据组件命名就能筛选出符合要求的组件。

// 命名形式:一级分类/二级分类/组件名称,基于图层获取对应libraryid library = [self getLibraryBySymbol:layer];// 读取组件名称NSString *layerName = [symbol valueForKeyPath:@"name"];// 配置合乎以后业务的PredicateNSPredicate *predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH [cd] %@", prefix];// 筛选合乎Predicate的所有组件NSArray *filterResult = [allSybmols filteredArrayUsingPredicate:predicate];

当使用者选中某一个款式后,插件会将设计稿上的组件替换为选中的组件,这里须要应用MSSymbolInstance中的changeInstanceToSymbol办法来实现。须要留神的是,changeInstanceToSymbol仅仅替换了图层中的组件,然而并没有批改图层上组件的属性,对于地位和大小等信息须要独自进行解决。

// 在更新图层上的组件之前,咱们须要把组件导入到以后document对象中id foreignSymbol = [libraryController importShareableObjectReference:sharedObject intoDocument:documentData];//更新图层上的组件 [symbolInstance changeInstanceToSymbol:localSymbol];

调试技巧

OC侧开发的最大问题,在于没有官网API的反对。因而调试器就显得十分重要,单步调试能够让咱们十分不便地深刻到Sketch外部理解Document外部的数据结构。调试环境须要配置,但足够简略,并且对于开发效率的晋升是指数级的。

1.对构建Scheme的配置。

2.Attach到Sketch软件上,这样就能够实现断点调试。

与以后JS工程混合编译

1.通过skpm中内置的@skpm/xcodeproj-loader编译XCode工程,并将产物framework拷贝至插件文件夹。

const framework = require('../../RooSketchPluginXCodeProject/RooSketchPluginXCodeProject.xcworkspace/contents.xcworkspacedata');

2.通过Mocha提供的loadFrameworkWithName_inDirectory办法,设置Framework的名称及门路即可进行加载。

function() {    var mocha = Mocha.sharedRuntime();    var frameworkName = 'RooSketchPluginXCodeProject';    var directory = frameworkPath;    if (mocha.valueForKey(frameworkName)) {      console.info('JSloadFramework: `' + frameworkName + '` has loaded.');      return true;    } else if (mocha.loadFrameworkWithName_inDirectory(frameworkName, directory)) {      console.info('JSloadFramework: `' + frameworkName + '` success!');      mocha.setValue_forKey_(true, frameworkName);      return true;    } else {      console.error('JSloadFramework load failed');      return false;    }  }

3.调用framework中的办法。

// 找到曾经被加载的framework const frameworkClass = NSClassFromString('RooSketchPluginNativeUI');// 调用裸露的办法frameworkClass.onSelectionChanged(context);

一起拼积木

目前,积木插件曾经在美团到家事业部遍地开花,咱们心愿将来积木品牌产品能够在更大范畴内失去利用,帮忙更多团队落地设计规范,晋升产研效率,也欢送更多团队接入积木工具链。“不忘初心,方得始终”,就像第一篇启蒙文章中说的那样,咱们除了心愿制作一流的产品,也心愿积木插件能够让大家在忙碌的工作中得以喘息。咱们会持续以设计语言为依靠,以积木工具链为抓手,不断完善优化,拓展插件的应用场景,让设计与开发变得更轻松。

总有人在问,积木插件当初好用吗?我想说,还不够好用。然而每次评审需要时看到旁边的设计师在认真地应用咱们的插件作图,看到积木插件爱好者为咱们制作表情包帮忙咱们推广,咱们深知唯有交付最棒的产品,能力不辜负大家的期待。

平台化二期的需要刚刚确定结束,人力调配排期完结,咱们又想了一大波令你拍手称誉的性能,马上就要踏上新的征程。夜深了,看着窗外人家的灯,一个个燃烧,夜空也变得越来越亮堂。咱们的指标,是星辰大海。

致谢

感激外卖技术部晓飞、彦平、瑶哥、云鹏、冰冰对我的项目的大力支持。
感激到家事业部优良的设计师徐徐、昱翰、淼林、雪美、田园、璟琦。
感激闪购技术团队章琦、CRM团队的怡婷、CI王鹏帮助技术开发。

参考文献

  • 百度Sketch插件开发总结
  • 爱奇艺产品工作流优化:搭建组件库做高ROI
  • 阿里重磅开源中后盾UI解决方案Fusion
  • Painting with Code
  • Sketch Developers Discussion

招聘信息

美团外卖长期招聘Android、iOS、FE高级/资深工程师和技术专家,欢送退出外卖App小家庭。感兴趣的同学可投递简历至:tech@meituan.com(邮件主题请注明:美团外卖前端)。

想浏览更多技术文章,请关注美团技术团队(meituantech)官网微信公众号。