Articles

Como converter seus plugins Xcode para extensões Xcode

by Khoa Pham

>
Source: Imgur

Xcode é uma IDE indispensável para desenvolvedores de iOS e macOS. Desde os primeiros tempos, a capacidade de construir e instalar plugins personalizados deu-nos um enorme impulso na produtividade. Não demorou muito para a Apple introduzir a extensão Xcode devido a preocupações com privacidade.

Eu construí alguns plugins Xcode e extensões como XcodeWay, XcodeColorSense, XcodeColorSense2, e Xmas. Foi uma experiência gratificante. Eu aprendi muito, e a produtividade que ganhei foi considerável. Neste post eu passo por como converti meus plugins Xcode em extensões, e a experiência que tive ao fazê-lo.

Meu primeiro plugin Xcode: XcodeWay

Eu escolho uma pessoa preguiçosa para fazer um trabalho duro. Porque uma pessoa preguiçosa vai encontrar uma maneira fácil de fazê-lo

Eu realmente gosto da citação acima de Bill Gates. Eu tento evitar tarefas repetitivas e enfadonhas. Sempre que me encontro fazendo as mesmas tarefas novamente, eu escrevo scripts e ferramentas para automatizar isso. Fazer isso leva algum tempo, mas serei um pouco mais preguiçoso num futuro próximo.

Além do interesse em construir frameworks e ferramentas de código aberto, eu gosto de estender a IDE que estou usando – principalmente Xcode.

Iniciei o desenvolvimento do iOS em 2014. Eu queria uma maneira rápida de navegar para muitos lugares diretamente do Xcode com o contexto do projeto atual. Há muitas vezes que queremos:

  • abrir a pasta do projeto atual no “Finder” para alterar alguns arquivos
  • abrir o Terminal para executar alguns comandos
  • abrir o arquivo atual no GitHub para rapidamente dar o link para um colega de trabalho
  • ou para abrir outras pastas como temas, plugins, trechos de código, logs de dispositivos.

Todos os dias poupamos um pouco de tempo.

Pensei que seria uma boa ideia escrever um plugin Xcode que pudéssemos fazer todas as coisas acima dentro do Xcode. Em vez de esperar que outras pessoas o fizessem, puxei a manga para cima e escrevi o meu primeiro plugin Xcode – XcodeWay- e partilhei como código aberto.

XcodeWay funciona criando um menu sob Editor com muitas opções para navegar para outros sítios a partir do Xcode. Parece simples mas foi necessário algum trabalho duro.

O que são plugins Xcode?

Xcode plugins não são oficialmente suportados pelo Xcode ou recomendados pela Apple. Não há documentos sobre eles. Os melhores lugares que podemos aprender sobre eles são através do código fonte dos plugins existentes e alguns tutoriais.

Um plugin Xcode é apenas um pacote do tipo xcplugin e é colocado em ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins . Xcode, ao iniciar, irá carregar qualquer plugin Xcode presente nesta pasta. Os plugins são executados no mesmo processo que o Xcode, portanto pode fazer qualquer coisa como Xcode. Um bug em qualquer plugin pode fazer com que o Xcode trave.

Para fazer um plugin Xcode, crie um macOS Bundle com uma classe que se estenda de NSObject , e tenha um inicializador que aceite NSBundle , por exemplo em Xmas:

class Xmas: NSObject {
 var bundle: NSBundle
 init(bundle: NSBundle) { self.bundle = bundle super.init() }}

Dentro de Info.plist, precisamos de o fazer:

  • declarar esta classe como a principal classe de entrada para o plugin, e
  • que este pacote não tem IU, porque nós criamos controles de IU e adicionamos à interface do Xcode durante o tempo de execução
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>

Outro problema com os plugins do Xcode é que nós temos que atualizar continuamente DVTPluginCompatibilityUUIDs . Isto muda cada vez que uma nova versão do Xcode é lançada. Sem atualizar, o Xcode se recusará a carregar o plugin.

O que os plugins Xcode podem fazer

Muitos desenvolvedores constroem plugins Xcode porque perdem recursos específicos encontrados em outros IDEs como Sublime Text, AppCode ou Atom.

Desde que os plugins Xcode sejam carregados no mesmo processo que o Xcode, eles podem fazer tudo o que o Xcode pode. O único limite é a nossa imaginação. Nós podemos aproveitar o Objective C Runtime para descobrir frameworks e funções privadas. Então LLDB e Symbolic breakpoint podem ser usados para inspecionar o código em execução e alterar seus comportamentos. Também podemos usar swizzling para alterar a implementação de qualquer código em execução. Escrever plugins Xcode é difícil – muitas suposições, e às vezes um bom conhecimento de assembly é necessário.

Na era dourada dos plugins, havia um plugin manager popular, que por si só era um plugin, chamado Alcatraz. Ele podia instalar outros plugins, que basicamente só baixava o arquivo xcplugin e o movia para a pasta Plug Ins.

Para ter uma idéia do que os plugins podem fazer, vamos dar uma olhada em alguns plugins populares.

Xvim

Primeiro na lista é o Xvim, que adiciona keybindings Vim bem dentro do Xcode. Ele suporta principalmente todos os keybindings que costumávamos ter no Terminal.

SCXcodeMiniMap

Se você perder o modo MiniMap em Sublime Text, você pode usar SCXcodeMiniMap para adicionar um painel de mapa direito dentro do editor Xcode.

FuzzyAutocompletePlugin

Antes da versão 9, o Xcode não tinha auto-completamento adequado – ele era apenas baseado em prefixo. Era aí que o FuzzyAutocompletePlugin brilhava. Ele executa o autocompletar fuzzy baseado no recurso oculto IDEOpenQuicklyPattern em Xcode.

KSImageNamed-Xcode

Para exibir uma imagem de pacote dentro de UIImageView, nós usamos frequentemente o método imageNamed. Mas lembrar exatamente o nome do arquivo de imagem é difícil. O KSImageNamed-Xcode está aqui para ajudar. Você obterá uma lista de nomes de imagens auto-suguradas quando começar a digitar.

ColorSense-for-Xcode

Uma outra coceira durante o desenvolvimento é trabalhar com UIColor , que usa o espaço de cor RGBA. Não obtemos um indicador visual da cor que especificamos, e a verificação manual pode ser demorada. Felizmente existe o ColorSense-for-Xcode que mostra a cor que está sendo usada e o painel de seleção de cores para selecionar facilmente a cor certa.

LinkedConsole

In AppCode, podemos pular para uma linha específica no arquivo que está logado dentro do console. Se você perder este recurso no Xcode, você pode usar o LinkedConsole. Isso habilita links clicáveis dentro do console do Xcode para que possamos pular para aquele arquivo instantaneamente.

O trabalho duro por trás dos plugins Xcode

Fazer um plugin Xcode não é fácil. Não só precisamos de saber programação MacOS, mas também de mergulhar fundo na hierarquia de visualização do Xcode. Precisamos explorar frameworks privados e APIs para injetar o recurso que queremos.

Existem muito poucos tutoriais sobre como fazer plugins mas, felizmente, a maioria dos plugins são de código aberto para que possamos entender como eles funcionam. Como eu fiz alguns plugins, posso dar alguns detalhes técnicos sobre eles.

Xcode plugins são feitos normalmente com dois frameworks privados: DVTKit e IDEKit . Os frameworks do sistema estão em /System/Library/PrivateFrameworks mas os frameworks que o Xcode usa exclusivamente estão em /Applications/Xcode.app/Contents/ , lá você pode encontrar Frameworks , OtherFrameworks e SharedFrameworks.

Existe uma classe-dump que pode gerar cabeçalhos a partir do pacote de maçã do Xcode. Com os nomes das classes e métodos, você pode chamar NSClassFromString para obter a classe do nome.

Swizzling DVTBezelAlertPanel framework em Xmas

Christmas sempre me deu uma sensação especial, então eu decidi fazer o Xmas, que mostra uma imagem de Natal aleatória ao invés da vista de alerta padrão. A classe usada para renderizar essa vista é DVTBezelAlertPanel dentro da estrutura do DVTKit. Meu artigo sobre a construção desse plugin está aqui.

Com o Objective C Runtime, existe uma técnica chamada swizzling, que pode mudar e mudar a implementação e a assinatura do método de qualquer classe e método em execução.

Aqui, para mudar o conteúdo dessa visualização de alerta, precisamos trocar o inicializador initWithIcon:message:parentWindow:duration: pelo nosso próprio método. Fazemos isso cedo ouvindo NSApplicationDidFinishLaunchingNotification que é notificado quando um plugin MacOS, neste caso Xcode, lança.

class func swizzleMethods() { guard let originalClass = NSClassFromString("DVTBezelAlertPanel") as? NSObject.Type else { return }
do { try originalClass.jr_swizzleMethod("initWithIcon:message:parentWindow:duration:", withMethod: "xmas_initWithIcon:message:parentWindow:duration:") } catch { Swift.print("Swizzling failed") }}

Gostei inicialmente de fazer tudo no Swift. Mas é complicado usar o método swizzle init no Swift, então a maneira mais rápida é fazer isso no Objective C. Então nós simplesmente atravessamos a hierarquia de visualização para encontrar o NSVisualEffectView dentro de NSPanel para atualizar a imagem.

Interagindo com DVTSourceTextView no XcodeColorSense

Eu trabalho principalmente com cores hexadecimais e eu quero uma maneira rápida de ver a cor. Então construí o XcodeColorSense – ele suporta cores hexadecimais, RGBA, e nomeado cor.

A ideia é simples. Analise a string para ver se o usuário está digitando algo relacionado a UIColor, e mostre uma pequena vista de overlay com essa cor como fundo. A vista de texto que o Xcode usa é do tipo DVTSourceTextView em DVTKit framework. Também precisamos ouvir NSTextViewDidChangeSelectionNotification que é acionado sempre que qualquer NSTextView conteúdo é alterado.

func listenNotification() { NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(handleSelectionChange(_:)), name: NSTextViewDidChangeSelectionNotification, object: nil)}
func handleSelectionChange(note: NSNotification) { guard let DVTSourceTextView = NSClassFromString("DVTSourceTextView") as? NSObject.Type, object = note.object where object.isKindOfClass(DVTSourceTextView.self), let textView = object as? NSTextView else { return }
self.textView = textView}

Eu tinha uma arquitetura Matcher para que possamos detectar diferentes tipos de UIColor construções – por exemplo HexMatcher .

public struct HexMatcher: Matcher {
func check(line: String, selectedText: String) -> (color: NSColor, range: NSRange)? { let pattern1 = "\"#?{6}\"" let pattern2 = "0x{6}"
let ranges = .flatMap { return Regex.check(line, pattern: ) }
guard let range = ranges.first else { return nil }
let text = (line as NSString).substringWithRange(range).replace("0x", with: "").replace("\"", with: "") let color = NSColor.hex(text)
return (color: color, range: range) }}

Para renderizar a overlay, usamos NSColorWell o que é bom para mostrar uma vista com fundo. A posição é determinada chamando firstRectForCharacterRange e algumas conversões de pontos com convertRectFromScreen e convertRect .

Usando NSTask e IDEWorkspaceWindowController no XcodeWay

Finalmente, meu amado XcodeWay.

Encontrei-me a precisar ir a lugares diferentes do Xcode com o contexto do projecto actual. Então eu construí o XcodeWay como um plugin que adiciona muitas opções de menu úteis em Window.

Desde que o plugin corre no mesmo processo do Xcode, ele tem acesso ao menu principal NSApp.mainMenu?.itemWithTitle("Window") . Aí podemos alterar o menu. XcodeWay foi projetado para estender facilmente as funcionalidades através de seu protocolo Navigator.

@objc protocol Navigator: NSObjectProtocol { func navigate() var title: String { get }}

Para pastas com um caminho estático como Perfil de Provisionamento ~/Library/MobileDevice/Provisioning Profiles ou Dados do Usuário Developer/Xcode/UserData , podemos apenas construir o URL e chamar NSWorkspace.sharedWorkspace().openURL . Para pastas dinâmicas que variam dependendo do projeto atual, mais trabalho precisa ser feito.

Como abrimos a pasta para o projeto atual no Finder? As informações para o caminho do projeto atual são mantidas dentro de IDEWorkspaceWindowController . Esta é uma classe que gerencia janelas de espaço de trabalho em Xcode. Dê uma olhada no EnvironmentManager onde usamos objc_getClass para obter a definição da classe a partir de uma string.

self.IDEWorkspaceWindowControllerClass = objc_getClass("IDEWorkspaceWindowController");
NSArray *workspaceWindowControllers = ;
id workSpace = nil;
for (id controller in workspaceWindowControllers) { if ( isEqual:]) { workSpace = ; }}
NSString * path = valueForKey:@"_pathString"];

Finalmente, podemos utilizar valueForKey para obter o valor para qualquer propriedade que pensamos existir. Desta forma não só obtemos o caminho do projecto, como também o caminho para o ficheiro de abertura. Assim podemos chamar activateFileViewerSelectingURLs em NSWorkspace para abrir o Finder com esse ficheiro seleccionado. Isto é útil pois os utilizadores não precisam de procurar esse ficheiro no Finder.

em

Muitas vezes queremos executar alguns comandos do Terminal na pasta do projecto actual. Para conseguir isso, podemos usar NSTask com launch pad /usr/bin/open e argumentos . O iTerm, se configurado provavelmente, abrirá isso em uma nova aba.

Os documentos para aplicativos iOS 7 são colocados no local fixo iPhone Simulator dentro do Application Support. Mas, a partir do iOS 8, cada aplicativo tem um UUID único e suas pastas de documentos são difíceis de prever.

~/Library/Developer/CoreSimulator/Devices/1A2FF360-B0A6-8127-95F3-68A6AB0BCC78/data/Container/Data/Application/

Podemos construir um mapa e realizar o rastreamento para encontrar o ID gerado para o projeto atual, ou verificar o plist dentro de cada pasta para comparar o identificador do pacote.

A solução rápida que eu encontrei foi procurar pela pasta atualizada mais recente. Toda vez que construímos o projeto, ou fazemos alterações dentro do aplicativo, sua pasta de documentos é atualizada. É onde podemos fazer uso de NSFileModificationDate para encontrar a pasta do projeto atual.

Existem muitos hacks quando se trabalha com plugins Xcode, mas os resultados são gratificantes. A cada poucos minutos que salvamos a cada dia acabamos economizando muito tempo no geral.

Segurança e liberdade

Com grande poder vem grande responsabilidade. O fato de que os plugins podem fazer o que quiserem, alerta para a segurança. No final de 2015, houve um ataque de malware ao distribuir uma versão modificada do Xcode, chamada XcodeGhost, que injeta código malicioso em qualquer aplicativo construído com o Xcode Ghost. Acredita-se que o malware usa o mecanismo de plugin entre outras coisas.

Como os aplicativos iOS que baixamos da Appstore, aplicativos macOS como o Xcode são assinados pela Apple quando os baixamos da Mac Appstore ou através de links oficiais de download da Apple.

Code assinando seu aplicativo garante aos usuários que ele é de uma fonte conhecida e que o aplicativo não foi modificado desde que foi assinado pela última vez. Antes que seu aplicativo possa integrar serviços de aplicativos, ser instalado em um dispositivo ou ser submetido à App Store, ele deve ser assinado com um certificado emitido pela Apple

Para evitar malware potencial como este, na WWDC 2016 a Apple anunciou a Extensão Xcode Source Editor Extension como a única forma de carregar extensões de terceiros no Xcode. Isto significa que, a partir do Xcode 8, os plugins não podem ser carregados.

Source Editor Extension

Extension é a abordagem recomendada para adicionar com segurança funcionalidades de formas restritas.

As extensões do aplicativo dão aos usuários acesso à funcionalidade e ao conteúdo do seu aplicativo através do iOS e do macOS. Por exemplo, seu aplicativo agora pode aparecer como um widget na tela Hoje, adicionar novos botões na folha de ação, oferecer filtros de fotos dentro do aplicativo Fotos, ou exibir um novo teclado personalizado em todo o sistema.

Por enquanto, a única extensão para Xcode é o Editor de código fonte, que nos permite ler e modificar o conteúdo de um arquivo fonte, bem como ler e modificar a seleção de texto atual dentro do editor.

Extension é um novo alvo e roda em um processo diferente do Xcode. Isto é bom na medida em que não pode alterar o Xcode de nenhuma outra forma a não ser em conformidade com XCSourceEditorCommand para modificar o conteúdo do documento atual.

protocol XCSourceEditorCommand {
 func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -&gt; Void)}

Xcode 8 tem muitas melhorias como as novas funcionalidades de completamento de código, imagem Swift e literal de cores, e snippets. Isto levou à desvalorização de muitos plugins Xcode. Para alguns plugins indispensáveis como o XVim, isto é insuportável para algumas pessoas. Alguns recursos antigos de plugins não podem ser alcançados com o sistema Source Editor Extension atual.

A menos que você resignar o Xcode

Uma alternativa para contornar a restrição do Xcode 8 para plugins, é substituir a assinatura do Xcode existente por uma técnica chamada resignar. Resignar é muito fácil – basta criar um certificado autoassinado e chamar o comando codesign. Depois disto, o Xcode deve ser capaz de carregar plugins.

codesign -f -s MySelfSignedCertificate /Applications/Xcode.app

Não é possível, no entanto, submeter aplicações construídas com o Xcode resignado, pois a assinatura não corresponde à versão oficial do Xcode. Uma maneira é usar dois Xcodes: um oficial para distribuição e outro resignado para desenvolvimento.

Movendo para extensão Xcode

A extensão Xcode é o caminho a seguir, então eu comecei a mover meus plugins para extensão. Para o Xmas, como ele modifica a hierarquia de visualização, não pode se tornar uma extensão.

Color literalmente em XcodeColorSense2

Para o sentido de cor, eu reescrevi a extensão do zero, e a chamei de XcodeColorSense2. Isto, é claro, não pode mostrar uma sobreposição sobre a vista actual do editor. Então eu escolhi utilizar o novo Color literal encontrado no código X 8+.

A cor é mostrada em uma pequena caixa. Pode ser difícil distinguir cores semelhantes, por isso também incluo o nome. O código é simplesmente sobre inspecionar selections e analisar para encontrar a declaração de cor.

func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void { guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { completionHandler(nil) return }
let lineNumber = selection.start.line
guard lineNumber < invocation.buffer.lines.count, let line = invocation.buffer.lines as? String else { completionHandler(nil) return }
guard let hex = findHex(string: line) else { completionHandler(nil) return }
let newLine = process(line: line, hex: hex)
invocation.buffer.lines.replaceObject(at: lineNumber, with: newLine)
completionHandler(nil) }}

A maior parte da funcionalidade está embutida dentro do meu framework Farge, mas não consigo encontrar uma maneira de usar o framework dentro da extensão Xcode.

Desde que o recurso de extensão só é acessível através do menu Editor, podemos personalizar uma ligação de teclas para invocar este item de menu. Por exemplo, eu escolho Cmd+Ctrl+S para mostrar e ocultar informações de cor.

Isso, é claro, não é intuitivo comparado ao plugin original, mas é melhor do que nada.

Como depurar extensões Xcode

Trabalhar e depurar extensões é simples. Podemos usar o Xcode para depurar o Xcode. A versão depurada do Xcode tem um ícone cinza.

Como instalar extensões Xcode

A extensão deve ter um aplicativo macOS de acompanhamento. Este pode ser distribuído para a Mac Appstore ou auto-sinalizado. Escrevi um artigo sobre como fazer isto.

Todas as extensões para um aplicativo precisam ser explicitamente habilitadas através de “Preferências do Sistema”.

A extensão Xcode só funciona com editor por enquanto, por isso devemos abrir um arquivo fonte para o menu Editor para ter efeito.

AppleScript in XcodeWay

Em extensões Xcode, NSWorkspace, NSTask e a construção de classes privadas não funcionam mais. Como já usei a extensão Finder Sync no FinderGo, pensei que poderia tentar o mesmo script AppleScript para a extensão Xcode.

AppleScript é uma linguagem de script criada pela Apple. Ela permite aos usuários controlar diretamente aplicativos Macintosh com scriptable, assim como partes do próprio macOS. Você pode criar scripts – conjuntos de instruções escritas – para automatizar tarefas repetitivas, combinar funcionalidades de múltiplas aplicações com scripts e criar fluxos de trabalho complexos.

Para tentar AppleScript, você pode usar o Script Editor de scripts da aplicação criado dentro do macOS para escrever funções de protótipos. A declaração de funções começa com on e termina com end . Para evitar possíveis conflitos com as funções do sistema, eu normalmente uso my como prefixo. Aqui é como eu confio nos Eventos do Sistema para obter o diretório home.

A terminologia do script de interface do usuário é encontrada no “Conjunto de Processos” do dicionário de scripts de “Eventos do Sistema”. Essa suíte inclui terminologia para interagir com a maioria dos tipos de elementos da interface do usuário, incluindo:

  • janelas
  • buttons
  • checkboxes
  • menus
  • radio buttons
  • text fields.
  • Em System Events, a classe process representa uma aplicação em execução.

    Muitas boas aplicações cidadãs suportam AppleScript expondo algumas das suas funcionalidades, pelo que estas podem ser utilizadas por outras aplicações. Aqui está como eu obtenho a música atual do Spotify em Lyrics.

tell application "Spotify" set trackId to id of current track as string set trackName to name of current track as string set artworkUrl to artwork url of current track as string set artistName to artist of current track as string set albumName to album of current track as string return trackId & "---" & trackName & "---" & artworkUrl & "---" & artistName & "---" & albumNameend tell

Para obter todos os comandos possíveis de um determinado aplicativo, nós podemos abrir o dicionário no Script Editor. Lá podemos aprender quais funções e parâmetros são suportados.

Se você acha que o Objective C é difícil, o AppleScript é muito mais difícil. A sintaxe é verbosa e propensa a erros. Para sua referência, aqui está todo o arquivo de script que alimenta XcodeWay.

Para abrir uma determinada pasta, diga Finder usando POSIX file. Eu refactoriz cada funcionalidade em função para melhor reutilização do código.

on myOpenFolder(myPath)tell application "Finder"activateopen myPath as POSIX fileend tellend myOpenFolder

Então, para executar AppleScript dentro de uma aplicação macOS ou extensão, precisamos de construir um descritor AppleScript com o número de série do processo correcto e identificadores de eventos.

func eventDescriptior(functionName: String) -> NSAppleEventDescriptor { var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess)) let target = NSAppleEventDescriptor( descriptorType: typeProcessSerialNumber, bytes: &psn, length: MemoryLayout<ProcessSerialNumber>.size )
let event = NSAppleEventDescriptor( eventClass: UInt32(kASAppleScriptSuite), eventID: UInt32(kASSubroutineEvent), targetDescriptor: target, returnID: Int16(kAutoGenerateReturnID), transactionID: Int32(kAnyTransactionID) )
let function = NSAppleEventDescriptor(string: functionName) event.setParam(function, forKeyword: AEKeyword(keyASSubroutineName))
return event}

Outras tarefas, como verificar o comando Git actual, são um pouco mais complicadas. Muitas vezes eu quero compartilhar o link do arquivo que estou depurando com meu colega de equipe remoto, para que eles saibam qual arquivo estou referenciando. Isto pode ser feito usando shell script dentro de AppleScript .

on myGitHubURL()set myPath to myProjectPath()set myConsoleOutput to (do shell script "cd " & quoted form of myPath & "; git remote -v")set myRemote to myGetRemote(myConsoleOutput)set myUrl to (do shell script "cd " & quoted form of myPath & "; git config --get remote." & quoted form of myRemote & ".url")set myUrlWithOutDotGit to myRemoveSubString(myUrl, ".git")end myGitHubURL

Podemos usar quoted e concatenação de strings para formar strings. Por sorte podemos expor Foundation framework e certas classes. Aqui está como eu exponho NSString para tirar vantagem de todas as funcionalidades existentes. Escrever manipulação de strings do zero usando AppleScript simples levará muito tempo.

use scripting additionsuse framework "Foundation"property NSString : a reference to current application's NSString

Com isto podemos construir nossas outras funções para manipulação de strings.

on myRemoveLastPath(myPath)set myString to NSString's stringWithString:myPathset removedLastPathString to myString's stringByDeletingLastPathComponentremovedLastPathString as textend myRemoveLastPath

Uma funcionalidade legal que o XcodeWay suporta é a habilidade de ir para o diretório de documentos da aplicação atual no simulador. Isto é útil quando precisamos inspecionar um documento para verificar dados salvos ou em cache. O diretório é dinâmico, por isso é difícil de detectar. Podemos, no entanto, ordenar o directório para a mais recente actualização. Abaixo está como nós encadeamos múltiplos comandos shell scripts para encontrar a pasta.

on myOpenDocument()set command1 to "cd ~/Library/Developer/CoreSimulator/Devices/;"set command2 to "cd `ls -t | head -n 1`/data/Containers/Data/Application;"set command3 to "cd `ls -t | head -n 1`/Documents;"set command4 to "open ."do shell script command1 & command2 & command3 & command4end myOpenDocument

Este recurso me ajudou muito ao desenvolver a Galeria para verificar se os vídeos e imagens baixadas estão salvos no lugar correto.

No entanto, nenhum dos scripts parece funcionar. O scripting sempre fez parte do macOS desde 1993. Mas, com o advento da Mac Appstore e preocupações com a segurança, o AppleScript finalmente ficou restrito em meados de 2012. Isso foi quando a App Sandbox foi aplicada.

App Sandbox

App Sandbox é uma tecnologia de controle de acesso fornecida no MacOS, aplicada ao nível do kernel. Ele é projetado para conter danos ao sistema e aos dados do usuário se um aplicativo for comprometido. Os aplicativos distribuídos através do Mac App Store devem adotar o App Sandbox.

Para que uma extensão Xcode seja carregada pelo Xcode, ele também deve suportar o App Sandbox.

No início da aplicação do App Sandbox, nós poderíamos usar a exceção temporária do App Sandbox para conceder temporariamente o acesso do nosso aplicativo ao Apple Script.

Isso agora não é possível.

A única maneira do AppleScript ser executado é se ele reside dentro da pasta ~/Library/Application Scripts pasta.

Como instalar scripts personalizados

Aplicativos ou extensões de macOS não podem simplesmente instalar scripts nos Scripts de Aplicativos por si mesmos. Eles precisam do consentimento do usuário.

Uma maneira possível de fazer isso é habilitar Read/Write e mostrar um diálogo usando NSOpenPanel para pedir ao usuário que selecione a pasta para instalar nossos scripts.

Para XcodeWay, eu escolho fornecer um script shell de instalação para que o usuário tenha uma maneira rápida de instalar scripts.

#!/bin/bash
set -euo pipefail
DOWNLOAD_URL=https://raw.githubusercontent.com/onmyway133/XcodeWay/master/XcodeWayExtensions/Script/XcodeWayScript.scptSCRIPT_DIR="${HOME}/Library/Application Scripts/com.fantageek.XcodeWayApp.XcodeWayExtensions"
mkdir -p "${SCRIPT_DIR}"curl $DOWNLOAD_URL -o "${SCRIPT_DIR}/XcodeWayScript.scpt"

AppleScript é muito poderoso. Tudo isso é explicitado para que o usuário tenha controle completo sobre quais coisas podem ser feitas.

Como uma extensão, um script é feito de forma assíncrona em um processo diferente usando XPC para comunicação entre processos. Isto melhora a segurança como um script não tem acesso ao espaço de endereço da nossa aplicação ou extensão.

Mais segurança em macOS Mojave

Este ano, na WWDC 2018, a Apple introduziu o macOS Mojave que se concentra em muitas melhorias de segurança. Nas Suas Aplicações e o Futuro da Segurança MacOS podemos aprender mais sobre os novos requisitos de segurança para as aplicações MacOS. Uma delas é a descrição de uso para AppleEvents.

unable to load info.plist exceptions (egpu overrides)

We used to declare use description for many permissions in iOS, like photo library, camera, and push notifications. Agora nós precisamos declarar a descrição de uso para AppleEvents.

Source: https://www.felix-schwarz.org/blog/2018/08/new-apple-event-apis-in-macos-mojave

A primeira vez que a nossa extensão tenta executar alguns comandos AppleScript, o diálogo acima é mostrado para pedir o consentimento do usuário. O usuário pode conceder ou negar permissão, mas para Xcode por favor diga sim ?

A correção para nós é declarar NSAppleEventsUsageDescription em nosso alvo de aplicação. Só precisamos de declarar no alvo da aplicação, não na extensão alvo.

<key>NSAppleEventsUsageDescription</key><string>Use AppleScript to open folders</string>

Onde ir a partir daqui

Huff huff, whew! Obrigado por seguir uma viagem tão longa. Fazer estruturas e ferramentas levam muito tempo, especialmente plugins e extensões – temos que mudar continuamente para adaptá-las aos novos sistemas operacionais e requisitos de segurança. Mas é um processo gratificante, pois aprendemos mais e temos algumas ferramentas para economizar nosso precioso tempo.

Para sua referência, aqui estão minhas extensões que são totalmente de código aberto.

  • XcodeWay
  • XcodeColorSense2