Se você perder o modo MiniMap em Sublime Text, você pode usar SCXcodeMiniMap para adicionar um painel de mapa direito dentro do editor Xcode.
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.
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.
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.
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.
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.
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.
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?) -> 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