Articles

Cómo convertir tus plugins de Xcode en extensiones de Xcode

por Khoa Pham

Fuente: Imgur

Xcode es un IDE indispensable para los desarrolladores de iOS y macOS. Desde los primeros días, la capacidad de construir e instalar plugins personalizados nos había dado un gran impulso a la productividad. No pasó mucho tiempo antes de que Apple introdujera la extensión de Xcode debido a la preocupación por la privacidad.

He construido algunos plugins y extensiones de Xcode como XcodeWay, XcodeColorSense, XcodeColorSense2 y Xmas. Fue una experiencia gratificante. Aprendí mucho y la productividad que gané fue considerable. En este post recorro cómo convertí mis plugins de Xcode en extensiones, y la experiencia que tuve al hacerlo.

Mi primer plugin de Xcode: XcodeWay

Elijo una persona perezosa para hacer un trabajo difícil. Porque una persona perezosa encontrará una manera fácil de hacerlo

Me gusta mucho la cita anterior de Bill Gates. Intento evitar las tareas repetitivas y aburridas. Siempre que me encuentro haciendo las mismas tareas, escribo scripts y herramientas para automatizarlas. Hacer esto lleva algo de tiempo, pero seré un poco más perezoso en el futuro cercano.

Además del interés en construir frameworks y herramientas de código abierto, me gusta extender el IDE que estoy usando – principalmente Xcode.

Comencé a desarrollar iOS en 2014. Quería una forma rápida de navegar a muchos lugares directamente desde Xcode con el contexto del proyecto actual. Hay muchas veces que queremos:

  • abrir la carpeta del proyecto actual en el «Finder» para cambiar algunos archivos
  • abrir Terminal para ejecutar algunos comandos
  • abrir el archivo actual en GitHub para dar rápidamente el enlace a un compañero de trabajo
  • o abrir otras carpetas como temas, plugins, fragmentos de código, registros de dispositivos.

Cada poco de tiempo que ahorramos cada día cuenta.

Pensé que sería una idea genial escribir un plugin de Xcode con el que podamos hacer todas las cosas anteriores directamente dentro de Xcode. En lugar de esperar a que otras personas lo hagan, me saqué de la manga y escribí mi primer plugin de Xcode – XcodeWay- y lo compartí como código abierto.

XcodeWay funciona creando un menú bajo Editor con muchas opciones para navegar a otros lugares directamente desde Xcode. Parece simple, pero hubo algo de trabajo duro requerido.

¿Qué son los plugins de Xcode?

Los plugins de Xcode no son soportados oficialmente por Xcode o recomendados por Apple. No hay documentos sobre ellos. Los mejores lugares donde podemos aprender sobre ellos son a través del código fuente de los plugins existentes y algunos tutoriales.

Un plugin de Xcode no es más que un paquete de tipo xcplugin y se coloca en ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins . Xcode, al iniciarse, cargará cualquier plugin de Xcode presente en esta carpeta. Los plugins se ejecutan en el mismo proceso que Xcode, por lo que podría hacer cualquier cosa como Xcode. Un error en cualquier plugin puede hacer que Xcode se bloquee.

Para hacer un plugin de Xcode, crear un macOS Bundle con una clase que extienda de NSObject , y tener un inicializador que acepte NSBundle , por ejemplo en Xmas:

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

Dentro de Info.plist, necesitamos:

  • declarar esta clase como la clase de entrada principal del plugin, y
  • que este bundle no tenga UI, porque creamos controles UI y los añadimos a la interfaz de Xcode durante el tiempo de ejecución
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>

Otro problema de los plugins de Xcode es que tenemos que actualizar continuamente DVTPluginCompatibilityUUIDs . Esto cambia cada vez que sale una nueva versión de Xcode. Si no se actualiza, Xcode se negará a cargar el plugin.

Lo que pueden hacer los plugins de Xcode

Muchos desarrolladores construyen plugins de Xcode porque echan de menos características específicas que se encuentran en otros IDEs como Sublime Text, AppCode o Atom.

Dado que los plugins de Xcode se cargan en el mismo proceso que Xcode, pueden hacer todo lo que puede hacer Xcode. El único límite es nuestra imaginación. Podemos aprovechar Objective C Runtime para descubrir frameworks y funciones privadas. Luego se puede utilizar LLDB y Symbolic breakpoint para inspeccionar el código en ejecución y alterar sus comportamientos. También podemos usar swizzling para cambiar la implementación de cualquier código en ejecución. Escribir plugins en Xcode es difícil – muchas conjeturas, y a veces se requiere un buen conocimiento de ensamblaje.

En la época dorada de los plugins, había un popular gestor de plugins, que a su vez era un plugin, llamado Alcatraz. Podía instalar otros plugins, que básicamente solo descargaba el archivo xcplugin y lo movía a la carpeta Plug Ins.

Para tener una idea de lo que pueden hacer los plugins, echemos un vistazo a algunos plugins populares.

Xvim

El primero de la lista es Xvim, que añade enlaces de teclas de Vim justo dentro de Xcode. Soporta la mayoría de los keybindings que solíamos tener en Terminal.

SCXcodeMiniMap

Si echas de menos el modo MiniMap en Sublime Text, puedes usar SCXcodeMiniMap para añadir un panel de mapa derecho dentro del editor de Xcode.

FuzzyAutocompletePlugin

Antes de la versión 9, Xcode no tenía un autocompletado adecuado – sólo se basaba en el prefijo. Ahí es donde FuzzyAutocompletePlugin brilló. Realiza un autocompletado difuso basado en la característica oculta IDEOpenQuicklyPattern de Xcode.

KSImageNamed-Xcode

Para mostrar una imagen de un paquete dentro de UIImageView, a menudo utilizamos el método imageNamed. Pero recordar exactamente el nombre del archivo de imagen es difícil. KSImageNamed-Xcode está aquí para ayudar. Obtendrá una lista de nombres de imagen auto-sugeridos cuando comience a escribir.

ColorSense-for-Xcode

Otro prurito durante el desarrollo es trabajar con UIColor , que utiliza el espacio de color RGBA. No tenemos un indicador visual del color que especificamos, y realizar la comprobación manualmente puede llevar mucho tiempo. Por suerte existe ColorSense-for-Xcode que muestra el color que se está utilizando y el panel de selección de color para seleccionar fácilmente el color correcto.

LinkedConsole

En AppCode, podemos saltar a una línea específica del archivo que se registra dentro de la consola. Si echas de menos esta característica en Xcode, puedes utilizar LinkedConsole. Esto permite enlaces clicables dentro de la consola de Xcode para que podamos saltar a ese archivo al instante.

El duro trabajo detrás de los plugins de Xcode

Hacer un plugin de Xcode no es fácil. No sólo tenemos que conocer la programación de macOS, sino que también tenemos que bucear profundamente en la jerarquía de vistas de Xcode. Necesitamos explorar frameworks y APIs privadas para poder inyectar la característica que queremos.

Hay muy pocos tutoriales sobre cómo hacer plugins pero, por suerte, la mayoría de los plugins son de código abierto para que podamos entender cómo funcionan. Como he hecho unos cuantos plugins, puedo dar algunos detalles técnicos sobre ellos.

Los plugins de Xcode se hacen normalmente con dos frameworks privados: DVTKit y IDEKit . Los frameworks del sistema están en /System/Library/PrivateFrameworks pero los frameworks que usa Xcode exclusivamente están en /Applications/Xcode.app/Contents/ , ahí puedes encontrar Frameworks , OtherFrameworks y SharedFrameworks.

Hay una herramienta class-dump que puede generar cabeceras del paquete de aplicaciones de Xcode. Con los nombres de las clases y los métodos, puedes llamar a NSClassFromString para obtener la clase a partir del nombre.

Swizzling DVTBezelAlertPanel framework in Xmas

La Navidad siempre me ha dado un sentimiento especial, así que decidí hacer Xmas, que muestra una imagen navideña al azar en lugar de la vista de alerta por defecto. La clase utilizada para renderizar esa vista es DVTBezelAlertPanel dentro del framework DVTKit. Mi artículo sobre la construcción de ese plugin está aquí.

Con Objective C Runtime, hay una técnica llamada swizzling, que puede cambiar y conmutar la implementación y la firma del método de cualquier clase y método en ejecución.

Aquí, para cambiar el contenido de esa vista de alerta, tenemos que cambiar el inicializador initWithIcon:message:parentWindow:duration: con nuestro propio método. Lo hacemos antes escuchando a NSApplicationDidFinishLaunchingNotification que se notifica cuando un plugin de macOS, en este caso Xcode, se lanza.

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") }}

Al principio me gustaba hacerlo todo en Swift. Pero es complicado usar el método swizzle init en Swift, así que la forma más rápida es hacerlo en Objective C. Entonces simplemente atravesamos la jerarquía de vistas para encontrar el NSVisualEffectView dentro de NSPanel para actualizar la imagen.

Interactuando con DVTSourceTextView en XcodeColorSense

Trabajo principalmente con colores hexadecimales y quiero una forma rápida de ver el color. Así que construí XcodeColorSense – soporta el color hexadecimal, RGBA, y el color con nombre.

La idea es simple. Analizar la cadena para ver si el usuario está escribiendo algo relacionado con UIColor, y mostrar una pequeña vista superpuesta con ese color como fondo. La vista de texto que utiliza Xcode es de tipo DVTSourceTextView en DVTKit framework. También tenemos que escuchar a NSTextViewDidChangeSelectionNotification que se dispara cada vez que cualquier contenido NSTextView se cambia.

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}

Tenía una arquitectura Matcher para que podamos detectar diferentes tipos de construcciones UIColor – por ejemplo 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 la superposición, usamos NSColorWell que es bueno para mostrar una vista con fondo. La posición se determina llamando a firstRectForCharacterRange y algunas conversiones de puntos con convertRectFromScreen y convertRect .

Usando NSTask e IDEWorkspaceWindowController en XcodeWay

Finalmente, mi querido XcodeWay.

Me encontré con la necesidad de ir a diferentes lugares desde Xcode con el contexto del proyecto actual. Así que construí XcodeWay como un plugin que añade un montón de prácticas opciones de menú bajo Window.

Como el plugin se ejecuta en el mismo proceso de Xcode, tiene acceso al menú principal NSApp.mainMenu?.itemWithTitle("Window"). Allí podemos modificar el menú. XcodeWay está diseñado para extender fácilmente las funcionalidades a través de su protocolo Navigator.

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

Para carpetas con una ruta estática como Provisioning Profile ~/Library/MobileDevice/Provisioning Profiles o User data Developer/Xcode/UserData , podemos simplemente construir el URL y llamar a NSWorkspace.sharedWorkspace().openURL . Para las carpetas dinámicas que varían dependiendo del proyecto actual, hay que hacer más trabajo.

¿Cómo abrimos la carpeta del proyecto actual en el Finder? La información para la ruta del proyecto actual se mantiene dentro de IDEWorkspaceWindowController . Esta es una clase que gestiona las ventanas del espacio de trabajo en Xcode. Mira EnvironmentManager donde utilizamos objc_getClass para obtener la definición de la clase a partir de una cadena.

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

Por último, podemos utilizar valueForKey para obtener el valor de cualquier propiedad que creamos que existe. De esta manera no sólo obtenemos la ruta del proyecto, sino también la ruta del archivo de apertura. Así que podemos llamar a activateFileViewerSelectingURLs en NSWorkspace para abrir el Finder con ese archivo seleccionado. Esto es útil ya que los usuarios no necesitan buscar ese archivo en el Finder.

Muchas veces queremos ejecutar algunos comandos de la Terminal en la carpeta actual del proyecto. Para ello, podemos utilizar NSTask con la plataforma de lanzamiento /usr/bin/open y los argumentos . iTerm, si se configura probablemente, abrirá esto en una nueva pestaña.

Los documentos de las apps de iOS 7 se colocan en la ubicación fija iPhone Simulator dentro de Application Support. Pero, a partir de iOS 8, cada app tiene un UUID único y sus carpetas de documentos son difíciles de predecir.

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

Podemos construir un mapa y realizar un rastreo para encontrar el ID generado para el proyecto actual, o comprobar el plist dentro de cada carpeta para comparar el identificador del bundle.

La solución rápida que se me ocurrió fue buscar la carpeta actualizada más reciente. Cada vez que construimos el proyecto, o hacemos cambios dentro de la app, su carpeta de documentos se actualiza. Ahí es donde podemos hacer uso de NSFileModificationDate para encontrar la carpeta del proyecto actual.

Hay muchos hacks cuando se trabaja con los plugins de Xcode, pero los resultados son gratificantes. Cada pocos minutos que ahorramos cada día acaban ahorrando mucho tiempo en general.

Seguridad y libertad

Con un gran poder viene una gran responsabilidad. El hecho de que los plugins puedan hacer lo que quieran supone una alerta para la seguridad. A finales de 2015, hubo un ataque de malware distribuyendo una versión modificada de Xcode, llamada XcodeGhost, que inyecta código malicioso en cualquier app construida con Xcode Ghost. Se cree que el malware utiliza, entre otras cosas, el mecanismo de plugins.

Al igual que las apps de iOS que descargamos de la Appstore, las apps de macOS como Xcode son firmadas por Apple cuando las descargamos de la Mac Appstore o a través de los enlaces oficiales de descarga de Apple.

La firma de código de tu app asegura a los usuarios que proviene de una fuente conocida y que la app no ha sido modificada desde la última vez que se firmó. Antes de que tu app pueda integrar servicios de aplicaciones, ser instalada en un dispositivo o ser enviada a la App Store, debe ser firmada con un certificado emitido por Apple

Para evitar posibles programas maliciosos como este, en la WWDC 2016 Apple anunció la Extensión del Editor de Fuentes de Xcode como la única forma de cargar extensiones de terceros en Xcode. Esto significa que, a partir de Xcode 8, no se pueden cargar plugins.

Extensión del Editor de Fuentes

La extensión es el enfoque recomendado para añadir funcionalidades de forma restringida de forma segura.

Las extensiones de apps dan acceso a los usuarios a la funcionalidad y contenido de tu app en todo iOS y macOS. Por ejemplo, tu app puede aparecer ahora como un widget en la pantalla «Hoy», añadir nuevos botones en la hoja de acciones, ofrecer filtros de fotos dentro de la app Fotos o mostrar un nuevo teclado personalizado en todo el sistema.

Por ahora, la única extensión de Xcode es Source Editor, que nos permite leer y modificar el contenido de un archivo fuente, así como leer y modificar la selección de texto actual dentro del editor.

La extensión es un nuevo objetivo y se ejecuta en un proceso diferente al de Xcode. Esto es bueno en el sentido de que no puede alterar Xcode en cualquier forma que no sea conforme a XCSourceEditorCommand para modificar el contenido del documento actual.

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

Xcode 8 tiene un montón de mejoras como las nuevas características de finalización de código, imagen Swift y literales de color, y fragmentos. Esto condujo a la desaprobación de muchos plugins de Xcode. Para algunos plugins indispensables como XVim, esto es insoportable para algunas personas. Algunas características de los antiguos plugins no se pueden conseguir con el actual sistema de Extensión del Editor de Fuentes.

A menos que renuncies a Xcode

Una solución para saltarse la restricción de Xcode 8 para los plugins, es reemplazar la firma existente de Xcode por una técnica llamada resign. Renunciar es muy fácil – sólo tenemos que crear un certificado autofirmado y llamar al comando codesign. Después de esto, Xcode debería ser capaz de cargar plugins.

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

Sin embargo, no es posible enviar aplicaciones construidas con Xcode resignado, ya que la firma no coincide con la versión oficial de Xcode. Una forma es usar dos Xcodes: uno oficial para la distribución y otro resignado para el desarrollo.

Mover a la extensión de Xcode

La extensión de Xcode es el camino a seguir, así que empecé a mover mis plugins a la extensión. Para Xmas, ya que modifica la jerarquía de la vista, no puede convertirse en una extensión.

Literal de color en XcodeColorSense2

Para el sentido del color, reescribí la extensión desde cero, y la llamé XcodeColorSense2. Esto, por supuesto, no puede mostrar una superposición sobre la vista del editor actual. Así que opté por utilizar el nuevo Color literal que se encuentra en Xcode 8+.

El color se muestra en un pequeño cuadro. Puede ser difícil distinguir colores similares, por eso también incluyo el nombre. El código es simplemente acerca de la inspección de selections y el análisis sintáctico para encontrar la declaración de color.

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) }}

La mayor parte de la funcionalidad está incrustado dentro de mi marco Farge, pero no puedo encontrar una manera de utilizar el marco dentro de la extensión de Xcode.

Dado que la función de extensión sólo es accesible a través del menú Editor, podemos personalizar un enlace de teclas para invocar este elemento de menú. Por ejemplo, elijo Cmd+Ctrl+S para mostrar y ocultar la información de color.

Esto, por supuesto, no es intuitivo en comparación con el plugin original, pero es mejor que nada.

Cómo depurar las extensiones de Xcode

Trabajar y depurar las extensiones es sencillo. Podemos usar Xcode para depurar Xcode. La versión depurada de Xcode tiene un icono gris.

Cómo instalar extensiones de Xcode

La extensión debe tener una app para macOS que la acompañe. Esta puede ser distribuida en Mac Appstore o autofirmada. He escrito un artículo sobre cómo hacerlo.

Todas las extensiones de una app tienen que estar explícitamente habilitadas a través de «Preferencias del Sistema».

La extensión de Xcode sólo funciona con el editor por ahora, así que debemos abrir un archivo fuente para que el menú Editor tenga efecto.

AppleScript en XcodeWay

En las extensiones de Xcode, NSWorkspace, NSTask y la construcción de clases privadas ya no funcionan. Desde que he utilizado Finder Sync Extension en FinderGo, pensé que podría probar el mismo AppleScript scripting para la extensión de Xcode.

AppleScript es un lenguaje de scripting creado por Apple. Permite a los usuarios controlar directamente las aplicaciones Macintosh scriptables, así como partes del propio macOS. Puede crear scripts -conjuntos de instrucciones escritas- para automatizar tareas repetitivas, combinar funciones de varias aplicaciones scriptables y crear flujos de trabajo complejos.

Para probar AppleScript, puede utilizar la app Script Editor integrada en macOS para escribir prototipos de funciones. La declaración de la función comienza con on y termina con end . Para evitar posibles conflictos con las funciones del sistema, suelo utilizar my como prefijo. Así es como me baso en Eventos del Sistema para obtener el directorio de inicio.

La terminología de scripting de interfaz de usuario se encuentra en la «Suite de Procesos» del diccionario de scripting «Eventos del Sistema». Esta suite incluye terminología para interactuar con la mayoría de los tipos de elementos de la interfaz de usuario, incluyendo:

  • ventanas
  • botones
  • casillas de verificación
  • menús
  • botones de radio
  • campos de texto.

En Eventos del Sistema, la clase processrepresenta una app en ejecución.

Muchas apps de buen ciudadano soportan AppleScript exponiendo algunas de sus funcionalidades, para que éstas puedan ser utilizadas por otras apps. Así es como obtengo la canción actual de Spotify en 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 obtener todos los posibles comandos de una determinada app, podemos abrir el diccionario en Script Editor. Allí podemos conocer qué funciones y parámetros están soportados.

Si crees que Objective C es difícil, AppleScript lo es mucho más. La sintaxis es verbosa y propensa a errores. Para su referencia, aquí está todo el archivo de script que impulsa XcodeWay.

Para abrir una determinada carpeta, dile a Finder usando POSIX file. Refactorizo cada funcionalidad en función para una mejor reutilización del código.

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

Entonces, para ejecutar AppleScript dentro de una app o extensión de macOS, necesitamos construir un descriptor AppleScript con el número de serie del proceso correcto y los 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}

Otras tareas, como comprobar el Git remoto actual, son un poco más complicadas. Muchas veces quiero compartir el enlace del archivo que estoy depurando con mi compañero de equipo remoto, para que sepan a qué archivo estoy haciendo referencia. Esto se puede hacer utilizando 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 utilizar quoted y la concatenación de cadenas para formar cadenas. Por suerte podemos exponer Foundation framework y ciertas clases. Aquí es cómo expongo NSString para aprovechar todas las funcionalidades existentes. Escribir la manipulación de cadenas desde cero usando AppleScript simple tomará mucho tiempo.

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

Con esto podemos construir nuestras otras funciones para el manejo de cadenas.

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

Una característica interesante que soporta XcodeWay es la capacidad de ir al directorio de documentos para la aplicación actual en el simulador. Esto es útil cuando necesitamos inspeccionar un documento para comprobar los datos guardados o en caché. El directorio es dinámico por lo que es difícil de detectar. Sin embargo, podemos ordenar el directorio por lo más recientemente actualizado. A continuación se muestra cómo encadenamos varios comandos shell scripts para encontrar la carpeta.

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

Esta característica me ayudó mucho cuando desarrollé Gallery para comprobar si los vídeos y las imágenes descargadas se guardan en el lugar correcto.

Sin embargo, ninguno de los scripts parece funcionar. Los scripts siempre han formado parte de macOS desde 1993. Pero, con la llegada de la Mac Appstore y las preocupaciones de seguridad, AppleScript finalmente se restringió a mediados de 2012. Fue entonces cuando se aplicó App Sandbox.

App Sandbox

App Sandbox es una tecnología de control de acceso proporcionada en macOS, aplicada a nivel del kernel. Está diseñada para contener el daño al sistema y a los datos del usuario si una app se ve comprometida. Las aplicaciones distribuidas a través de la Mac App Store deben adoptar App Sandbox.

Para que una extensión de Xcode sea cargada por Xcode, también debe ser compatible con App Sandbox.

Al principio de la aplicación de App Sandbox, podríamos utilizar App Sandbox Temporary Exception para conceder temporalmente a nuestra aplicación acceso a Apple Script.

Ahora esto no es posible.

La única manera de que AppleScript se ejecute es si reside dentro de la carpeta ~/Library/Application Scripts.

Cómo instalar scripts personalizados

Las apps o extensiones de macOS no pueden instalar scripts en la Aplicación Scripts por sí mismas. Necesitan el consentimiento del usuario.

Una posible forma de hacerlo es habilitar Read/Write y mostrar un diálogo usando NSOpenPanel para pedir al usuario que seleccione la carpeta para instalar nuestros scripts.

Para XcodeWay, elijo proporcionar un script de shell de instalación para que el usuario tenga una forma 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 es muy potente. Todo esto se hace explícito para que el usuario tenga un control total sobre qué cosas se pueden hacer.

Al igual que una extensión, un script se realiza de forma asíncrona en un proceso diferente utilizando XPC para la comunicación entre procesos. Esto mejora la seguridad ya que un script no tiene acceso al espacio de direcciones a nuestra app o extensión.

Más seguridad en macOS Mojave

Este año, en la WWDC 2018, Apple presentó macOS Mojave que se centra en muchas mejoras de seguridad. En el artículo Tus apps y el futuro de la seguridad de macOS podemos conocer más sobre los nuevos requisitos de seguridad para las apps de macOS. Uno de ellos es la descripción de uso para AppleEvents.

Incapacidad de cargar excepciones info.plist (egpu overrides)

Solíamos declarar la descripción de uso para muchos permisos en iOS, como la fototeca, la cámara y las notificaciones push. Ahora tenemos que declarar la descripción de uso para AppleEvents.

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

La primera vez que nuestra extensión intenta ejecutar algunos comandos AppleScript, se muestra el diálogo anterior para pedir el consentimiento del usuario. El usuario puede conceder o denegar el permiso, pero para Xcode por favor diga que sí

La solución para nosotros es declarar NSAppleEventsUsageDescription en el objetivo de nuestra app. Sólo tenemos que declarar en el objetivo de la aplicación, no en el objetivo de la extensión.

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

A dónde ir desde aquí

¡Huff huff, whew! Gracias por seguir un viaje tan largo. Hacer frameworks y herramientas lleva mucho tiempo, especialmente los plugins y extensiones – tenemos que cambiar continuamente para adaptarlos a nuevos sistemas operativos y requisitos de seguridad. Pero es un proceso gratificante, ya que hemos aprendido más y tenemos algunas herramientas para ahorrar nuestro precioso tiempo.

Para tu referencia, aquí están mis extensiones que son totalmente de código abierto.

  • XcodeWay
  • XcodeColorSense2