Articles

Comment convertir vos plugins Xcode en extensions Xcode

par Khoa Pham

Source : Imgur

Xcode est un IDE indispensable pour les développeurs iOS et macOS. Dès les premiers jours, la possibilité de construire et d’installer des plugins personnalisés nous avait permis de gagner énormément en productivité. Il n’a pas fallu longtemps avant qu’Apple n’introduise l’extension Xcode en raison de préoccupations en matière de confidentialité.

J’ai construit quelques plugins et extensions Xcode comme XcodeWay, XcodeColorSense, XcodeColorSense2 et Xmas. Ce fut une expérience enrichissante. J’ai beaucoup appris, et la productivité que j’ai gagnée a été considérable. Dans ce post, je walkthrough comment j’ai converti mes plugins Xcode en extensions, et l’expérience que j’ai eue en le faisant.

Mon premier plugin Xcode : XcodeWay

Je choisis une personne paresseuse pour faire un travail difficile. Parce qu’une personne paresseuse trouvera un moyen facile de le faire

J’aime beaucoup la citation ci-dessus de Bill Gates. J’essaie d’éviter les tâches répétitives et ennuyeuses. Chaque fois que je me retrouve à refaire les mêmes tâches, j’écris des scripts et des outils pour automatiser cela. Faire cela prend un certain temps, mais je serai un peu plus paresseux dans un avenir proche.

En plus de l’intérêt pour la construction de frameworks et d’outils open source, j’aime étendre l’IDE que j’utilise – principalement Xcode.

J’ai commencé le développement iOS en 2014. Je voulais un moyen rapide de naviguer vers de nombreux endroits directement depuis Xcode avec le contexte du projet en cours. Il y a de nombreuses fois où nous voulons :

  • ouvrir le dossier du projet en cours dans le « Finder » pour modifier certains fichiers
  • ouvrir le Terminal pour exécuter certaines commandes
  • ouvrir le fichier en cours dans GitHub pour donner rapidement le lien à un collègue
  • ou ouvrir d’autres dossiers comme les thèmes, les plugins, les extraits de code, les journaux des appareils.

Chaque petit peu de temps que nous économisons chaque jour compte.

J’ai pensé que ce serait une idée cool d’écrire un plugin Xcode que nous pouvons faire toutes les choses ci-dessus directement à l’intérieur de Xcode. Au lieu d’attendre que d’autres personnes le fassent, j’ai remonté ma manche et j’ai écrit mon premier plugin Xcode – XcodeWay- et je l’ai partagé en open source.

XcodeWay fonctionne en créant un menu sous Editor avec beaucoup d’options pour naviguer vers d’autres endroits directement depuis Xcode. Cela semble simple mais il y a eu un travail difficile à réaliser.

Qu’est-ce que les plugins Xcode ?

Les plugins Xcode ne sont pas officiellement supportés par Xcode ou recommandés par Apple. Il n’existe aucun document à leur sujet. Les meilleurs endroits où nous pouvons apprendre à leur sujet sont via le code source des plugins existants et quelques tutoriels.

Un plugin Xcode est juste un bundle de type xcplugin et est placé à ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins . Xcode, au démarrage, chargera tous les plugins Xcode présents dans ce dossier. Les plugins sont exécutés dans le même processus que Xcode, donc pourraient faire tout comme Xcode. Un bug dans un plugin peut provoquer un crash de Xcode.

Pour faire un plugin Xcode, créer un macOS Bundle avec une classe qui s’étend de NSObject , et avoir un initialisateur qui accepte NSBundle , par exemple dans Xmas:

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

Inside Info.plist, nous devons :

  • déclarer cette classe comme la classe d’entrée principale pour le plugin, et
  • que ce bundle n’a pas d’interface utilisateur, car nous créons des contrôles d’interface utilisateur et ajoutons à l’interface Xcode pendant l’exécution
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>

Un autre problème avec les plugins Xcode est que nous devons continuellement mettre à jour DVTPluginCompatibilityUUIDs . Celle-ci change à chaque fois qu’une nouvelle version de Xcode sort. Sans mise à jour, Xcode refusera de charger le plugin.

Ce que les plugins Xcode peuvent faire

De nombreux développeurs construisent des plugins Xcode parce que des fonctionnalités spécifiques trouvées dans d’autres IDE comme Sublime Text, AppCode ou Atom leur manquent.

Puisque les plugins Xcode sont chargés dans le même processus que Xcode, ils peuvent faire tout ce que Xcode peut faire. La seule limite est notre imagination. Nous pouvons tirer parti d’Objective C Runtime pour découvrir des frameworks et des fonctions privées. Ensuite, LLDB et Symbolic breakpoint peuvent être utilisés pour inspecter le code en cours d’exécution et modifier leurs comportements. Nous pouvons également utiliser le swizzling pour modifier l’implémentation de tout code en cours d’exécution. Écrire des plugins Xcode est difficile – beaucoup de devinettes, et parfois une bonne connaissance de l’assemblage est nécessaire.

À l’âge d’or des plugins, il y avait un gestionnaire de plugins populaire, qui était lui-même un plugin, appelé Alcatraz. Il pouvait installer d’autres plugins, ce qui, en gros, ne fait que télécharger le fichier xcplugin et déplacer celui-ci dans le dossier Plug Ins.

Pour avoir une idée de ce que les plugins peuvent faire, jetons un coup d’œil à certains plugins populaires.

Xvim

Premier de la liste est Xvim, qui ajoute des liaisons de touches Vim directement à l’intérieur de Xcode. Il supporte presque toutes les liaisons de touches que nous avions dans Terminal.

SCXcodeMiniMap

Si le mode MiniMap de Sublime Text vous manque, vous pouvez utiliser SCXcodeMiniMap pour ajouter un panneau de carte droit à l’intérieur de l’éditeur Xcode.

FuzzyAutocompletePlugin

Avant la version 9, Xcode n’avait pas de complétion automatique correcte – elle était juste basée sur le préfixe. C’est là que FuzzyAutocompletePlugin a brillé. Il effectue une auto-complétion floue basée sur la fonctionnalité IDEOpenQuicklyPattern cachée dans Xcode.

KSImageNamed-Xcode

Pour afficher une image groupée à l’intérieur de UIImageView, nous utilisons souvent la méthode imageNamed. Mais se souvenir exactement du nom du fichier image est difficile. KSImageNamed-Xcode est là pour vous aider. Vous obtiendrez une liste de noms d’images auto-suggérés lorsque vous commencerez à taper.

ColorSense-for-Xcode

Une autre démangeaison pendant le développement est de travailler avec UIColor , qui utilise l’espace couleur RGBA. Nous n’avons pas d’indicateur visuel de la couleur que nous spécifions, et effectuer manuellement la vérification peut prendre du temps. Heureusement, il y a ColorSense-for-Xcode qui montre la couleur utilisée et le panneau de sélection des couleurs pour sélectionner facilement la bonne couleur.

LinkedConsole

Dans AppCode, nous pouvons sauter à une ligne spécifique du fichier qui est enregistrée à l’intérieur de la console. Si cette fonctionnalité vous manque dans Xcode, vous pouvez utiliser LinkedConsole. Cela permet des liens cliquables à l’intérieur de la console Xcode afin que nous puissions sauter à ce fichier instantanément.

Le travail difficile derrière les plugins Xcode

Faire un plugin Xcode n’est pas facile. Non seulement nous devons connaître la programmation macOS, mais nous devons également plonger profondément dans la hiérarchie des vues de Xcode. Nous devons explorer des cadres et des API privés afin d’injecter la fonctionnalité que nous voulons.

Il y a très peu de tutoriels sur la façon de faire des plugins mais, heureusement, la plupart des plugins sont open source afin que nous puissions comprendre comment ils fonctionnent. Comme j’ai fait quelques plugins, je peux donner quelques détails techniques à leur sujet.

Les plugins de Xcode sont faits habituellement avec deux frameworks privés : DVTKit et IDEKit . Les frameworks du système sont à /System/Library/PrivateFrameworks mais les frameworks que Xcode utilise exclusivement sont sous /Applications/Xcode.app/Contents/ , vous y trouverez Frameworks , OtherFrameworks et SharedFrameworks.

Il existe un outil class-dump qui peut générer des en-têtes à partir du bundle d’applications Xcode. Avec les noms de classe et les méthodes, vous pouvez appeler NSClassFromString pour obtenir la classe à partir du nom.

Swizzling DVTBezelAlertPanel framework in Xmas

Noël m’a toujours donné un sentiment spécial, alors j’ai décidé de faire Xmas, qui montre une image de Noël aléatoire au lieu de la vue d’alerte par défaut. La classe utilisée pour rendre cette vue est DVTBezelAlertPanel à l’intérieur du framework DVTKit. Mon article sur la construction de ce plugin est ici.

Avec Objective C Runtime, il existe une technique appelée swizzling, qui peut changer et commuter l’implémentation et la signature de méthode de n’importe quelle classe et méthode en cours d’exécution.

Ici, afin de changer le contenu de cette vue d’alerte, nous devons échanger l’initialisateur initWithIcon:message:parentWindow:duration: avec notre propre méthode. Nous le faisons de manière anticipée en écoutant NSApplicationDidFinishLaunchingNotification qui est notifié lorsqu’un plugin macOS, dans ce cas Xcode, se lance.

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

A l’origine, j’aimais bien tout faire en Swift. Mais il est délicat d’utiliser la méthode swizzle init en Swift, donc le moyen le plus rapide est de le faire en Objective C. Ensuite, nous traversons simplement la hiérarchie des vues pour trouver le NSVisualEffectView à l’intérieur du NSPanel pour mettre à jour l’image.

Interaction avec DVTSourceTextView dans XcodeColorSense

Je travaille principalement avec des couleurs hexagonales et je veux un moyen rapide de voir la couleur. J’ai donc construit XcodeColorSense – il supporte les couleurs hexagonales, RGBA, et les couleurs nommées.

L’idée est simple. Analyser la chaîne de caractères pour voir si l’utilisateur tape quelque chose en rapport avec UIColor, et afficher une petite vue superposée avec cette couleur comme arrière-plan. La vue de texte que Xcode utilise est de type DVTSourceTextView dans le framework DVTKit. Nous devons également écouter NSTextViewDidChangeSelectionNotification qui est déclenché chaque fois qu’un contenu NSTextView est modifié.

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}

J’ai eu une architecture Matcher afin que nous puissions détecter différents types de constructions UIColor – par exemple 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) }}

Pour rendre la superposition, nous utilisons NSColorWell qui est bon pour montrer une vue avec un fond. La position est déterminée en appelant firstRectForCharacterRange et quelques conversions de points avec convertRectFromScreen et convertRect .

Using NSTask and IDEWorkspaceWindowController in XcodeWay

Enfin, mon cher XcodeWay.

Je me suis retrouvé à devoir aller à différents endroits depuis Xcode avec le contexte du projet en cours. J’ai donc construit XcodeWay comme un plugin qui ajoute beaucoup d’options de menu pratiques sous Window.

Puisque le plugin s’exécute dans le même processus Xcode, il a accès au menu principal NSApp.mainMenu?.itemWithTitle("Window") . Là, nous pouvons modifier le menu. XcodeWay est conçu pour étendre facilement les fonctionnalités grâce à son protocole Navigator.

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

Pour les dossiers avec un chemin statique comme Provisioning Profile ~/Library/MobileDevice/Provisioning Profiles ou User data Developer/Xcode/UserData , nous pouvons juste construire le URL et appeler NSWorkspace.sharedWorkspace().openURL . Pour les dossiers dynamiques qui varient en fonction du projet en cours, il faut faire plus de travail.

Comment ouvrir le dossier du projet en cours dans le Finder ? L’information pour le chemin du projet en cours est conservée à l’intérieur de IDEWorkspaceWindowController . Il s’agit d’une classe qui gère les fenêtres de l’espace de travail dans Xcode. Jetez un coup d’œil à EnvironmentManager où nous utilisons objc_getClass pour obtenir la définition de la classe à partir d’une chaîne de caractères.

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

Enfin, nous pouvons utiliser valueForKey pour obtenir la valeur de toute propriété que nous pensons exister. De cette façon, non seulement nous obtenons le chemin du projet, mais nous obtenons également le chemin du fichier d’ouverture. Ainsi, nous pouvons appeler activateFileViewerSelectingURLs sur NSWorkspace pour ouvrir le Finder avec ce fichier sélectionné. C’est pratique car les utilisateurs n’ont pas besoin de chercher ce fichier dans le Finder.

Plusieurs fois, nous voulons exécuter certaines commandes du Terminal sur le dossier actuel du projet. Pour y parvenir, nous pouvons utiliser NSTask avec la rampe de lancement /usr/bin/open et les arguments . iTerm, s’il est configuré probablement, ouvrira ceci dans un nouvel onglet.

Les documents pour les applications iOS 7 sont placés dans l’emplacement fixe iPhone Simulator à l’intérieur du support d’application. Mais, à partir d’iOS 8, chaque application a un UUID unique et leurs dossiers de documents sont difficiles à prédire.

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

Nous pouvons construire une carte et effectuer un suivi pour trouver l’ID généré pour le projet actuel, ou vérifier le plist à l’intérieur de chaque dossier pour comparer l’identifiant du paquet.

La solution rapide que j’ai trouvée est de rechercher le dossier le plus récemment mis à jour. Chaque fois que nous construisons le projet, ou que nous faisons des changements à l’intérieur de l’app, leur dossier de document est mis à jour. C’est là que nous pouvons faire usage de NSFileModificationDate pour trouver le dossier du projet actuel.

Il y a beaucoup de hacks quand on travaille avec les plugins Xcode, mais les résultats sont gratifiants. Les quelques minutes que nous économisons chaque jour finissent par gagner beaucoup de temps dans l’ensemble.

Sécurité et liberté

Avec un grand pouvoir vient une grande responsabilité. Le fait que les plugins puissent faire ce qu’ils veulent sonne comme une alerte à la sécurité. Fin 2015, une attaque de malware a eu lieu en distribuant une version modifiée de Xcode, appelée XcodeGhost, qui injecte du code malveillant dans toutes les apps construites avec Xcode Ghost. Le malware utiliserait notamment le mécanisme de plugin.

Comme les apps iOS que nous téléchargeons depuis l’Appstore, les apps macOS comme Xcode sont signées par Apple lorsque nous les téléchargeons depuis le Mac Appstore ou via les liens de téléchargement officiels d’Apple.

La signature du code de votre app assure aux utilisateurs qu’elle provient d’une source connue et que l’app n’a pas été modifiée depuis sa dernière signature. Avant que votre app puisse intégrer des services d’app, être installée sur un appareil ou être soumise à l’App Store, elle doit être signée avec un certificat émis par Apple

Pour éviter d’éventuels logiciels malveillants comme celui-ci, Apple a annoncé, lors de la WWDC 2016, que l’extension de l’éditeur de source Xcode était le seul moyen de charger des extensions tierces dans Xcode. Cela signifie que, à partir de Xcode 8, les plugins ne peuvent pas être chargés.

Source Editor Extension

L’extension est l’approche recommandée pour ajouter en toute sécurité des fonctionnalités de manière restreinte.

Les extensions d’apps donnent aux utilisateurs un accès aux fonctionnalités et au contenu de votre app à travers iOS et macOS. Par exemple, votre app peut désormais apparaître comme un widget sur l’écran Aujourd’hui, ajouter de nouveaux boutons dans la feuille d’action, proposer des filtres photo dans l’app Photos ou afficher un nouveau clavier personnalisé à l’échelle du système.

Pour l’instant, la seule extension de Xcode est Source Editor, qui nous permet de lire et de modifier le contenu d’un fichier source, ainsi que de lire et de modifier la sélection de texte actuelle dans l’éditeur.

L’extension est une nouvelle cible et s’exécute dans un processus différent de Xcode. C’est une bonne chose dans la mesure où elle ne peut pas altérer Xcode autrement qu’en se conformant à XCSourceEditorCommand pour modifier le contenu actuel du document.

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

Xcode 8 a beaucoup d’améliorations comme les nouvelles fonctionnalités de complétion de code, les littéraux d’images et de couleurs Swift, et les snippets. Cela a conduit à la dépréciation de nombreux plugins Xcode. Pour certains plugins indispensables comme XVim, cela est insupportable pour certaines personnes. Certaines anciennes fonctionnalités des plugins ne peuvent pas être réalisées avec le système actuel d’extension de l’éditeur de sources.

À moins de démissionner de Xcode

Une solution de contournement pour contourner la restriction de Xcode 8 pour les plugins, consiste à remplacer la signature Xcode existante par une technique appelée resign. La démission est très facile – nous devons simplement créer un certificat auto-signé et appeler la commande codesign. Après cela, Xcode devrait être capable de charger les plugins.

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

Il n’est cependant pas possible de soumettre des apps construites avec Xcode démissionné car la signature ne correspond pas à la version officielle de Xcode. Une solution est d’utiliser deux Xcodes : un officiel pour la distribution et un résigné pour le développement.

Migration vers Xcode extension

Xcode extension est la voie à suivre, j’ai donc commencé à déplacer mes plugins vers l’extension. Pour Xmas, puisqu’il modifie la hiérarchie des vues, il ne peut pas devenir une extension.

Color literal in XcodeColorSense2

Pour le sens des couleurs, j’ai réécrit l’extension à partir de zéro, et je l’ai appelée XcodeColorSense2. Ceci, bien sûr, ne peut pas montrer une superposition sur la vue actuelle de l’éditeur. J’ai donc choisi d’utiliser le nouveau Color literal trouvé dans Xcode 8+.

La couleur est affichée dans une petite boîte. Il peut être difficile de distinguer des couleurs similaires, c’est pourquoi j’inclus également le nom. Le code consiste simplement à inspecter selections et à analyser pour trouver la déclaration de la couleur.

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 plupart des fonctionnalités sont intégrées à l’intérieur de mon framework Farge, mais je ne trouve pas de moyen d’utiliser le framework à l’intérieur de l’extension Xcode.

Puisque la fonctionnalité d’extension n’est accessible que par le menu de l’éditeur, nous pouvons personnaliser une liaison de touche pour invoquer cet élément de menu. Par exemple, je choisis Cmd+Ctrl+S pour afficher et masquer les informations de couleur.

Ce n’est, bien sûr, pas intuitif par rapport au plugin original, mais c’est mieux que rien.

Comment déboguer les extensions Xcode

Le travail et le débogage des extensions sont simples. Nous pouvons utiliser Xcode pour déboguer Xcode. La version déboguée de Xcode a une icône grise.

Comment installer les extensions Xcode

L’extension doit avoir une app macOS d’accompagnement. Celle-ci peut être distribuée sur le Mac Appstore ou auto-signée. J’ai écrit un article sur la façon de le faire.

Toutes les extensions pour une app doivent être explicitement activées par le biais des « Préférences système ».

L’extension Xcode ne fonctionne qu’avec l’éditeur pour le moment, donc nous devons ouvrir un fichier source pour que le menu Editor ait un effet.

AppleScript dans XcodeWay

Dans les extensions Xcode, NSWorkspace, NSTask et la construction de classe privée ne fonctionnent plus. Comme j’ai utilisé l’extension Finder Sync dans FinderGo, j’ai pensé que je pourrais essayer le même script AppleScript pour l’extension Xcode.

AppleScript est un langage de script créé par Apple. Il permet aux utilisateurs de contrôler directement les applications Macintosh scriptables, ainsi que des parties de macOS lui-même. Vous pouvez créer des scripts – des ensembles d’instructions écrites – pour automatiser des tâches répétitives, combiner des fonctionnalités de plusieurs applications scriptables et créer des flux de travail complexes.

Pour essayer AppleScript, vous pouvez utiliser l’app Script Editor intégré à macOS pour écrire des prototypes de fonctions. La déclaration des fonctions commence par on et se termine par end . Pour éviter les conflits potentiels avec les fonctions système, j’utilise généralement my comme préfixe. Voici comment je m’appuie sur System Events pour obtenir le répertoire d’origine.

La terminologie des scripts d’interface utilisateur se trouve dans la « Suite de processus » du dictionnaire de scripts « System Events ». Cette suite comprend la terminologie pour interagir avec la plupart des types d’éléments d’interface utilisateur, notamment :

  • fenêtres
  • boutons
  • cases à cocher
  • menus
  • boutons radio
  • champs de texte.

Dans les événements système, la classe process représente une app en cours d’exécution.

De nombreuses apps citoyennes soutiennent AppleScript en exposant certaines de leurs fonctionnalités, afin que celles-ci puissent être utilisées par d’autres apps. Voici comment j’obtiens la chanson actuelle de Spotify dans 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

Pour obtenir toutes les commandes possibles d’une certaine application, nous pouvons ouvrir le dictionnaire dans l’éditeur de script. Nous pouvons y apprendre quelles fonctions et quels paramètres sont pris en charge.

Si vous pensez qu’Objective C est difficile, AppleScript l’est encore plus. La syntaxe est verbeuse et sujette aux erreurs. Pour votre référence, voici le fichier script complet qui alimente XcodeWay.

Pour ouvrir un certain dossier, dites Finder en utilisant POSIX file. Je refactorise chaque fonctionnalité en fonction pour une meilleure réutilisation du code.

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

Puis, pour exécuter AppleScript à l’intérieur d’une application ou d’une extension macOS, nous devons construire un descripteur AppleScript avec le bon numéro de série du processus et les identifiants d’événements.

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}

D’autres tâches, comme la vérification de la télécommande Git actuelle, sont un peu plus délicates. Souvent, je veux partager le lien du fichier que je débogue avec mon coéquipier distant, afin qu’il sache à quel fichier je fais référence. Cela est possible en utilisant shell script à l’intérieur 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

Nous pouvons utiliser quoted et la concaténation de chaînes pour former des chaînes. Heureusement, nous pouvons exposer le cadre Foundation et certaines classes. Voici comment j’expose NSString pour profiter de toutes les fonctionnalités existantes. Écrire la manipulation des chaînes de caractères à partir de zéro en utilisant simplement AppleScript prendra beaucoup de temps.

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

Avec cela, nous pouvons construire nos autres fonctions pour la manipulation des chaînes de caractères.

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

Une fonctionnalité cool que XcodeWay prend en charge est la possibilité d’aller dans le répertoire des documents de l’application actuelle dans le simulateur. C’est pratique lorsque nous avons besoin d’inspecter un document pour vérifier les données enregistrées ou mises en cache. Le répertoire est dynamique, il est donc difficile de le détecter. Nous pouvons, cependant, trier le répertoire pour le plus récemment mis à jour. Voici comment nous enchaînons plusieurs commandes shell scripts pour trouver le répertoire.

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

Cette fonctionnalité m’a beaucoup aidé lors du développement de Gallery pour vérifier si les vidéos et les images téléchargées sont enregistrées au bon endroit.

Cependant, aucun des scripts ne semble fonctionner. Les scripts ont toujours fait partie de macOS depuis 1993. Mais, avec l’avènement du Mac Appstore et les problèmes de sécurité, AppleScript a finalement été restreint au milieu de l’année 2012. C’est à ce moment-là que App Sandbox a été mis en application.

App Sandbox

App Sandbox est une technologie de contrôle d’accès fournie dans macOS, appliquée au niveau du noyau. Elle est conçue pour contenir les dommages causés au système et aux données de l’utilisateur si une app est compromise. Les apps distribuées par le Mac App Store doivent adopter App Sandbox.

Pour qu’une extension Xcode soit chargée par Xcode, elle doit également supporter App Sandbox.

Au début de l’application d’App Sandbox, nous pouvions utiliser App Sandbox Temporary Exception pour accorder temporairement à notre app l’accès à Apple Script.

Ce n’est maintenant pas possible.

La seule façon pour AppleScript de s’exécuter est s’il réside à l’intérieur du dossier ~/Library/Application Scripts.

Comment installer des scripts personnalisés

les apps ou les extensions MacOS ne peuvent pas simplement installer des scripts dans les scripts de l’application par eux-mêmes. Ils ont besoin du consentement de l’utilisateur.

Une façon possible de le faire est d’activer Read/Write et de montrer un dialogue utilisant NSOpenPanel pour demander à l’utilisateur de sélectionner le dossier pour installer nos scripts.

Pour XcodeWay, j’ai choisi de fournir un script shell d’installation afin que l’utilisateur ait un moyen rapide d’installer des 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 est très puissant. Tout cela est rendu explicite afin que l’utilisateur ait un contrôle total sur les choses qui peuvent être faites.

Comme une extension, un script est fait de manière asynchrone dans un processus différent en utilisant XPC pour la communication interprocessus. Cela renforce la sécurité car un script n’a pas accès à l’espace d’adressage de notre app ou de notre extension.

Plus de sécurité dans macOS Mojave

Cette année, lors de la WWDC 2018, Apple a présenté macOS Mojave qui se concentre sur beaucoup d’améliorations de la sécurité. Dans le document Your Apps and the Future of macOS Security, nous pouvons en apprendre davantage sur les nouvelles exigences de sécurité pour les apps macOS. L’une d’elles est la description d’utilisation pour AppleEvents.

unable to load info.plist exceptions (egpu overrides)

Nous avions l’habitude de déclarer la description d’utilisation pour de nombreuses permissions dans iOS, comme la photothèque, l’appareil photo et les notifications push. Maintenant, nous devons déclarer la description d’utilisation pour AppleEvents.

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

La première fois que notre extension tente d’exécuter certaines commandes AppleScript, le dialogue ci-dessus s’affiche pour demander le consentement de l’utilisateur. L’utilisateur peut accorder ou refuser la permission, mais pour Xcode, veuillez dire oui ?

La solution pour nous est de déclarer NSAppleEventsUsageDescription dans notre cible d’application. Nous avons seulement besoin de déclarer dans la cible de l’application, pas dans la cible de l’extension.

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

Où aller d’ici

Huff huff, whew ! Merci d’avoir suivi un si long voyage. Faire des frameworks et des outils prend beaucoup de temps, surtout les plugins et les extensions – nous devons continuellement changer pour les adapter aux nouveaux systèmes d’exploitation et aux exigences de sécurité. Mais c’est un processus enrichissant, car nous avons appris davantage et nous disposons de certains outils pour gagner notre précieux temps.

Pour votre référence, voici mes extensions qui sont entièrement open source.

  • XcodeWay
  • XcodeColorSense2

.