Hoe converteer je Xcode-plugins naar Xcode-extensies
door Khoa Pham
Xcode is een onmisbare IDE voor iOS- en macOS-ontwikkelaars. Al vanaf de begindagen zorgde de mogelijkheid om aangepaste plugins te bouwen en te installeren voor een enorme productiviteitsstijging. Het duurde niet lang voordat Apple Xcode-extensie introduceerde vanwege privacyzorgen.
Ik heb een paar Xcode-plugins en -extensies gebouwd, zoals XcodeWay, XcodeColorSense, XcodeColorSense2, en Xmas. Het was een lonende ervaring. Ik heb veel geleerd, en de productiviteit die ik heb opgedaan was aanzienlijk. In dit bericht laat ik zien hoe ik mijn Xcode plugins heb omgezet naar extensies, en welke ervaring ik daarbij heb opgedaan.
Mijn eerste Xcode plugin: XcodeWay
Ik kies een lui iemand om een moeilijke klus te klaren. Want een lui iemand vindt wel een makkelijke manier om het te doen
Ik vind de bovenstaande quote van Bill Gates erg mooi. Ik probeer repetitieve en saaie taken te vermijden. Als ik merk dat ik dezelfde taken opnieuw doe, schrijf ik scripts en hulpmiddelen om dat te automatiseren. Dit doen kost wat tijd, maar ik zal in de nabije toekomst wat luier zijn.
Naast de interesse in het bouwen van open source frameworks en tools, vind ik het leuk om de IDE die ik gebruik uit te breiden – meestal Xcode.
Ik begon voor het eerst met iOS-ontwikkeling in 2014. Ik wilde een snelle manier om direct vanuit Xcode naar veel plaatsen te navigeren met de context van het huidige project. Er zijn veel momenten dat we willen:
- open de huidige projectmap in “Finder” om enkele bestanden te wijzigen
- open Terminal om enkele commando’s uit te voeren
- open het huidige bestand in GitHub om snel de link aan een collega te geven
- of om andere mappen te openen zoals thema’s, plugins, code snippets, apparaat logs.
Elk beetje tijd dat we elke dag besparen telt.
Ik dacht dat het een cool idee zou zijn om een Xcode plugin te schrijven waarmee we alle bovenstaande dingen direct in Xcode kunnen doen. In plaats van te wachten tot anderen het doen, heb ik de stoute schoenen aangetrokken en mijn eerste Xcode-plugin geschreven – XcodeWay – en deze als open source gedeeld.
Wat zijn Xcode-plugins?
Xcode-plugins worden niet officieel ondersteund door Xcode en worden ook niet aanbevolen door Apple. Er zijn geen documenten over. We kunnen er het beste over leren via de broncode van bestaande plugins en een paar tutorials.
Een Xcode plugin is gewoon een bundel van het type xcplugin
en is geplaatst op ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins
. Als Xcode wordt gestart, worden alle Xcode-plugins in deze map geladen. Plugins worden uitgevoerd in hetzelfde proces als Xcode, dus kunnen alles doen als Xcode. Een bug in een plugin kan Xcode laten crashen.
Om een Xcode plugin te maken, maakt u een macOS Bundle
met een klasse die uitbreidt van NSObject
, en heeft u een initialiser die NSBundle
accepteert, bijvoorbeeld in Xcode:
class Xmas: NSObject {
var bundle: NSBundle
init(bundle: NSBundle) { self.bundle = bundle super.init() }}
Binnen Info.plist
, moeten we:
- declarare this class as the main entry class for the plugin, en
- dat deze bundel geen UI heeft, omdat we UI controls maken en toevoegen aan de Xcode interface tijdens runtime
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>
Een ander probleem met Xcode plugins is dat we voortdurend DVTPluginCompatibilityUUIDs
moeten updaten. Dit verandert elke keer als er een nieuwe versie van Xcode uitkomt. Zonder updaten zal Xcode weigeren de plugin te laden.
Wat Xcode plugins kunnen
Veel ontwikkelaars bouwen Xcode plugins omdat ze specifieke functies missen die in andere IDE’s zoals Sublime Text, AppCode, of Atom te vinden zijn.
Omdat Xcode plugins in hetzelfde proces als Xcode worden geladen, kunnen ze alles wat Xcode kan. De enige limiet is onze verbeelding. We kunnen Objective C Runtime gebruiken om private frameworks en functies te ontdekken. Dan kunnen LLDB en Symbolic breakpoint verder worden gebruikt om lopende code te inspecteren en hun gedrag te veranderen. We kunnen ook swizzling gebruiken om de implementatie van een draaiende code te veranderen. Het schrijven van Xcode plugins is moeilijk – veel giswerk, en soms is een goede kennis van assemblage vereist.
In de gouden eeuw van plugins, was er een populaire plugin manager, die zelf een plugin was, genaamd Alcatraz. Het kon andere plugins installeren, die in principe gewoon het xcplugin
bestand download en dit verplaatst naar de Plug Ins
map.
Om een idee te krijgen van wat plugins kunnen doen, laten we eens kijken naar een aantal populaire plugins.
Xvim
De eerste in de lijst is Xvim, die Vim toetscombinaties direct in Xcode toevoegt. Het ondersteunt bijna alle toetsenbindingen die we vroeger in Terminal hadden.
SCXcodeMiniMap
Als u de MiniMap-modus in Sublime Text mist, kunt u SCXcodeMiniMap gebruiken om een kaartpaneel in de Xcode-editor toe te voegen.
FuzzyAutocompletePlugin
Vóór versie 9 had Xcode geen goede auto completion – het was gewoon gebaseerd op prefix. Dat was waar FuzzyAutocompletePlugin schitterde. Het voert fuzzy auto completion uit op basis van de verborgen IDEOpenQuicklyPattern
-functie in Xcode.
KSImageNamed-Xcode
Om een bundelafbeelding binnen UIImageView
weer te geven, gebruiken we vaak de imageNamed
-methode. Maar het is moeilijk om de naam van het afbeeldingsbestand precies te onthouden. KSImageNamed-Xcode is hier om te helpen. U krijgt een lijst met automatisch gesuggereerde afbeeldingsnamen wanneer u begint te typen.
ColorSense-for-Xcode
Een andere kriebel tijdens de ontwikkeling is het werken met UIColor
, dat gebruik maakt van RGBA kleurruimte. We krijgen geen visuele indicator van de kleur die we specificeren, en het handmatig uitvoeren van controles kan tijdrovend zijn. Gelukkig is er ColorSense-for-Xcode die de gebruikte kleur toont en het color picker panel om gemakkelijk de juiste kleur te selecteren.
LinkedConsole
In AppCode kunnen we naar een specifieke regel in het bestand springen die wordt gelogd in de console. Als u deze functie in Xcode mist, kunt u LinkedConsole gebruiken. Hiermee kunnen we in de Xcode-console op aanklikbare links klikken, zodat we direct naar dat bestand kunnen springen.
Het harde werk achter Xcode-plugins
Het maken van een Xcode-plugin is niet eenvoudig. Niet alleen moeten we macOS kunnen programmeren, maar we moeten ook diep in de Xcode view hiërarchie duiken. We moeten private frameworks en API’s verkennen om de functie die we willen te injecteren.
Er zijn maar weinig tutorials over hoe je plugins maakt, maar gelukkig zijn de meeste plugins open source, zodat we kunnen begrijpen hoe ze werken. Aangezien ik een paar plugins heb gemaakt, kan ik er wat technische details over geven.
Xcode plugins worden meestal gedaan met twee eigen frameworks: DVTKit
en IDEKit
. Systeem frameworks staan bij /System/Library/PrivateFrameworks
maar de frameworks die Xcode exclusief gebruikt staan onder /Applications/Xcode.app/Contents/
, daar vind je Frameworks
, OtherFrameworks
en SharedFrameworks
.
Er is een tool class-dump die headers kan genereren uit de Xcode app bundel. Met de klasse namen en methoden, kunt u NSClassFromString
aanroepen om de klasse uit de naam te halen.
Swizzling DVTBezelAlertPanel framework in Xmas
Christmas heeft me altijd een speciaal gevoel gegeven, dus ik besloot om Xmas te maken, die een willekeurige kerstfoto laat zien in plaats van de standaard alert view. De klasse die gebruikt wordt om die weergave te renderen is DVTBezelAlertPanel binnen het DVTKit raamwerk. Mijn artikel over het bouwen van die plugin is hier.
Met Objective C Runtime is er een techniek die swizzling heet, waarmee de implementatie en methodesignatuur van alle actieve klassen en methoden kan worden gewijzigd en veranderd.
Hier, om de inhoud van die waarschuwingsweergave te wijzigen, moeten we de initialiser initWithIcon:message:parentWindow:duration:
verwisselen met onze eigen methode. Dat doen we vroegtijdig door te luisteren naar NSApplicationDidFinishLaunchingNotification
die een melding krijgt als een macOS-plugin, in dit geval Xcode, opstart.
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") }}
Ik vond het aanvankelijk leuk om alles in Swift te doen. Maar het is lastig om de swizzle init methode in Swift te gebruiken, dus de snelste manier is om dat in Objective C te doen. Dan gaan we gewoon door de view hiërarchie om de NSVisualEffectView
binnen NSPanel
te vinden om de afbeelding bij te werken.
Interactie met DVTSourceTextView in XcodeColorSense
Ik werk meestal met hex kleuren en ik wil een snelle manier om de kleur te zien. Daarom heb ik XcodeColorSense gebouwd – het ondersteunt hex kleuren, RGBA, en benoemde kleuren.
Het idee is eenvoudig. Parseer de string om te zien of de gebruiker iets typt dat te maken heeft met UIColor
, en toon een kleine overlay-weergave met die kleur als achtergrond. De tekstweergave die Xcode gebruikt is van het type DVTSourceTextView
in DVTKit
framework. We moeten ook luisteren naar NSTextViewDidChangeSelectionNotification
die wordt getriggerd wanneer een NSTextView
inhoud wordt gewijzigd.
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}
Ik had een Matcher architectuur zodat we verschillende soorten UIColor
constructies kunnen detecteren – bijvoorbeeld 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) }}
Om de overlay te renderen, gebruiken we NSColorWell
die goed is voor het tonen van een view met achtergrond. De positie wordt bepaald door firstRectForCharacterRange
aan te roepen en wat puntconversies met convertRectFromScreen
en convertRect
.
Gebruik NSTask en IDEWorkspaceWindowController in XcodeWay
Tot slot, mijn geliefde XcodeWay.
Ik vond dat ik vanuit Xcode naar verschillende plaatsen moest gaan met de context van het huidige project. Dus bouwde ik XcodeWay als een plugin die veel handige menu-opties toevoegt onder Window
.
Omdat de plugin in hetzelfde Xcode proces draait, heeft het toegang tot het hoofdmenu NSApp.mainMenu?.itemWithTitle("Window")
. Daar kunnen we het menu wijzigen. XcodeWay is ontworpen om eenvoudig functionaliteiten uit te breiden via het Navigator protocol.
@objc protocol Navigator: NSObjectProtocol { func navigate() var title: String { get }}
Voor mappen met een statisch pad zoals Provisioning Profile ~/Library/MobileDevice/Provisioning Profiles
of User data Developer/Xcode/UserData
, kunnen we gewoon de URL
construeren en NSWorkspace.sharedWorkspace().openURL
oproepen. Voor dynamische mappen die variëren afhankelijk van het huidige project, moet meer werk worden gedaan.
Hoe openen we de map voor het huidige project in Finder? De informatie voor het huidige project pad wordt bewaard in IDEWorkspaceWindowController
. Dit is een klasse die de werkruimtevensters in Xcode beheert. Kijk eens naar EnvironmentManager waar we objc_getClass gebruiken om de klasse definitie uit een string te halen.
self.IDEWorkspaceWindowControllerClass = objc_getClass("IDEWorkspaceWindowController");
NSArray *workspaceWindowControllers = ;
id workSpace = nil;
for (id controller in workspaceWindowControllers) { if ( isEqual:]) { workSpace = ; }}
NSString * path = valueForKey:@"_pathString"];
Ten slotte kunnen we valueForKey
gebruiken om de waarde te krijgen voor elke eigenschap waarvan we denken dat die bestaat. Op deze manier krijgen we niet alleen het project pad, maar ook het pad naar het te openen bestand. Zo kunnen we activateFileViewerSelectingURLs
aanroepen op NSWorkspace
om Finder te openen met dat bestand geselecteerd. Dit is handig omdat gebruikers dan niet naar dat bestand hoeven te zoeken in Finder.
Vaak willen we wat Terminal commando’s uitvoeren op de huidige project map. Om dat te bereiken, kunnen we NSTask
gebruiken met launchpad /usr/bin/open
en argumenten . iTerm, indien waarschijnlijk geconfigureerd, zal dit openen in een nieuw tabblad.
De documenten voor iOS 7 apps worden geplaatst op de vaste locatie iPhone Simulator
binnen Application Support. Maar, vanaf iOS 8, heeft elke app een unieke UUID en hun document mappen zijn moeilijk te voorspellen.
~/Library/Developer/CoreSimulator/Devices/1A2FF360-B0A6-8127-95F3-68A6AB0BCC78/data/Container/Data/Application/
We kunnen een map bouwen en tracking uitvoeren om de gegenereerde ID voor het huidige project te vinden, of om de plist in elke map te controleren om de bundel identifier te vergelijken.
De snelle oplossing die ik bedacht was om te zoeken naar de meest recente bijgewerkte map. Elke keer als we het project bouwen, of wijzigingen aanbrengen in de app, wordt hun document map bijgewerkt. Dat is waar we gebruik kunnen maken van NSFileModificationDate
om de map voor het huidige project te vinden.
Er zijn veel hacks bij het werken met Xcode plugins, maar de resultaten zijn lonend. Elke paar minuten die we per dag besparen, leveren uiteindelijk een hoop tijdwinst op.
Veiligheid en vrijheid
Met grote macht komt grote verantwoordelijkheid. Het feit dat plugins kunnen doen wat ze willen, doet een alarmbel rinkelen voor de beveiliging. Eind 2015 was er een malware-aanval door het verspreiden van een aangepaste versie van Xcode, genaamd XcodeGhost, die kwaadaardige code injecteert in alle apps die met Xcode Ghost zijn gebouwd. De malware zou onder andere gebruikmaken van het plugin-mechanisme.
Net als de iOS-apps die we downloaden uit de Appstore, worden macOS-apps zoals Xcode ondertekend door Apple wanneer we ze downloaden uit de Mac Appstore of via officiële Apple-downloadlinks.
Code-ondertekening van uw app verzekert gebruikers dat deze afkomstig is van een bekende bron en dat de app niet is gewijzigd sinds deze voor het laatst is ondertekend. Voordat uw app app-diensten kan integreren, op een apparaat kan worden geïnstalleerd of bij de App Store kan worden ingediend, moet deze worden ondertekend met een certificaat dat door Apple is uitgegeven
Om potentiële malware zoals deze te voorkomen, heeft Apple tijdens WWDC 2016 de Xcode Source Editor Extension aangekondigd als de enige manier om extensies van derden in Xcode te laden. Dit betekent dat, vanaf Xcode 8, plug-ins niet kunnen worden geladen.
Source Editor Extension
Extensie is de aanbevolen aanpak om veilig functionaliteiten toe te voegen op beperkte manieren.
App-extensies geven gebruikers toegang tot de functionaliteit en inhoud van uw app in heel iOS en macOS. Uw app kan nu bijvoorbeeld verschijnen als een widget op het scherm Vandaag, nieuwe knoppen toevoegen in het Actieblad, fotofilters aanbieden binnen de Foto’s-app, of een nieuw systeembreed aangepast toetsenbord weergeven.
Voorlopig is de enige extensie voor Xcode Source Editor, waarmee we de inhoud van een bronbestand kunnen lezen en wijzigen, evenals de huidige tekstselectie binnen de editor kunnen lezen en wijzigen.
Extensie is een nieuw doel en draait in een ander proces dan Xcode. Dit is goed omdat het Xcode op geen enkele andere manier kan veranderen dan door te voldoen aan XCSourceEditorCommand
om de huidige documentinhoud te wijzigen.
protocol XCSourceEditorCommand {
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void)}
Xcode 8 heeft veel verbeteringen, zoals de nieuwe functies voor het aanvullen van de code, Swift-afbeeldingen en kleurenliteralen, en snippets. Dit heeft geleid tot de deprecatie van veel Xcode-plugins. Voor sommige onmisbare plugins, zoals XVim, is dit ondraaglijk voor sommige mensen. Sommige oude plugin-functies kunnen niet worden bereikt met het huidige Source Editor Extension systeem.
Niet Xcode opzeggen
Een workaround om de beperking van Xcode 8 voor plugins te omzeilen, is om de bestaande Xcode-handtekening te vervangen door een techniek die opzeggen heet. Ontslag nemen is heel eenvoudig – we hoeven alleen maar een zelfondertekend certificaat aan te maken en het codesign
commando op te roepen. Hierna zou Xcode in staat moeten zijn om plugins te laden.
codesign -f -s MySelfSignedCertificate /Applications/Xcode.app
Het is echter niet mogelijk om apps in te dienen die met resigned Xcode zijn gebouwd, omdat de handtekening niet overeenkomt met de officiële versie van Xcode. Een manier is om twee Xcodes te gebruiken: een officiële voor distributie en een afgetreden voor ontwikkeling.
Verhuizen naar Xcode-extensie
Xcode-extensie is de manier om te gaan, dus ik ben begonnen met het verplaatsen van mijn plug-ins naar extensie. Voor Xmas, omdat het de view hiërarchie wijzigt, kan het geen extensie worden.
Color literal in XcodeColorSense2
Voor de color sense, herschreef ik de extensie helemaal opnieuw, en noemde het XcodeColorSense2. Dit kan natuurlijk geen overlay tonen over de huidige editor weergave. Daarom heb ik ervoor gekozen om de nieuwe Color literal
te gebruiken die in Xcode 8+.
De kleur wordt in een klein vakje getoond. Het kan moeilijk zijn om gelijksoortige kleuren te onderscheiden, dus daarom zet ik ook de naam erbij. De code gaat gewoon over het inspecteren van selections
en parsing om de kleur declaratie te vinden.
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) }}
De meeste van de functionaliteit is ingebed in mijn framework Farge, maar ik kan geen manier vinden om het framework binnen Xcode extensie te gebruiken.
Omdat de extensie functie alleen toegankelijk is via het Editor menu, kunnen we een toets binding aanpassen om dit menu item aan te roepen. Ik kies bijvoorbeeld Cmd+Ctrl+S
om kleurinformatie te tonen en te verbergen.
Dit is natuurlijk niet intuïtief vergeleken met de oorspronkelijke plugin, maar het is beter dan niets.
Hoe Xcode-extensies te debuggen
Het werken met en debuggen van extensies is rechttoe rechtaan. We kunnen Xcode gebruiken om Xcode te debuggen. De gedebugte versie van Xcode heeft een grijs pictogram.
Hoe installeert u Xcode-extensies
De extensie moet een bijbehorend macOS-programma hebben. Deze kan worden gedistribueerd via de Mac Appstore of zelf worden ondertekend. Ik heb een artikel geschreven over hoe u dit kunt doen.
Alle extensies voor een app moeten expliciet worden ingeschakeld via “Systeemvoorkeuren”.
De Xcode-extensie werkt voorlopig alleen met editor, dus we moeten een bronbestand openen om het Editor
-menu effect te laten hebben.
AppleScript in XcodeWay
In Xcode-extensies werken NSWorkspace
, NSTask
en private class-constructie niet meer. Aangezien ik Finder Sync Extension heb gebruikt in FinderGo, dacht ik dat ik dezelfde AppleScript scripting kon proberen voor Xcode extension.
AppleScript is een scripttaal gemaakt door Apple. Het stelt gebruikers in staat om direct scriptable Macintosh-toepassingen te besturen, evenals delen van macOS zelf. U kunt scripts maken – sets van geschreven instructies – om repetitieve taken te automatiseren, functies van meerdere scriptbare applicaties te combineren en complexe workflows te maken.
Om AppleScript uit te proberen, kunt u de app Script Editor gebruiken die in macOS is ingebouwd om prototype functies te schrijven. Functiedeclaratie begint met on
en eindigt met end
. Om mogelijke conflicten met systeemfuncties te vermijden, gebruik ik meestal my
als voorvoegsel. Hier is hoe ik vertrouw op System Events om de home directory te krijgen.
Gebruiker interface scripting terminologie is te vinden in de “Processes Suite” van het “System Events” scripting woordenboek. Deze suite bevat terminologie voor interactie met de meeste typen gebruikersinterface-elementen, waaronder:
- vensters
- knoppen
- checkboxes
- menus
- radioknoppen
- tekstvelden.
In Systeemgebeurtenissen vertegenwoordigt de process
-klasse een draaiende app.
Veel goede burger-apps ondersteunen AppleScript door enkele van hun functionaliteiten bloot te geven, zodat deze door andere apps gebruikt kunnen worden. Hier is hoe ik het huidige nummer van Spotify in Lyrics krijg.
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
Om alle mogelijke commando’s van een bepaalde app te krijgen, kunnen we het woordenboek openen in Script Editor. Daar kunnen we leren welke functies en parameters worden ondersteund.
Als je denkt dat Objective C moeilijk is, dan is AppleScript nog veel moeilijker. De syntaxis is omslachtig en foutgevoelig. Ter referentie, hier is het hele scriptbestand dat XcodeWay aanstuurt.
Om een bepaalde map te openen, vertel je Finder
met POSIX file
. Ik refactor elke functionaliteit in functie voor een beter code hergebruik.
on myOpenFolder(myPath)tell application "Finder"activateopen myPath as POSIX fileend tellend myOpenFolder
Dan, om AppleScript binnen een macOS app of extensie uit te voeren, moeten we een AppleScript descriptor maken met het juiste proces serienummer en event identifiers.
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}
Andere taken, zoals het controleren van de huidige Git remote, zijn een beetje lastiger. Vaak wil ik de link van het bestand dat ik aan het debuggen ben delen met mijn teamgenoot op afstand, zodat ze weten naar welk bestand ik aan het verwijzen ben. Dit is mogelijk door shell script
te gebruiken binnen 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
We kunnen quoted
en string concatenation gebruiken om strings te vormen. Gelukkig kunnen we Foundation
framework en bepaalde klassen blootstellen. Hier is hoe ik NSString
exposeer om te profiteren van alle bestaande functionaliteiten. Het vanaf nul schrijven van string manipulatie met gewone AppleScript kost veel tijd.
use scripting additionsuse framework "Foundation"property NSString : a reference to current application's NSString
Met deze kunnen we onze andere functies voor string handling bouwen.
on myRemoveLastPath(myPath)set myString to NSString's stringWithString:myPathset removedLastPathString to myString's stringByDeletingLastPathComponentremovedLastPathString as textend myRemoveLastPath
Een coole functie die XcodeWay ondersteunt is de mogelijkheid om naar de document directory voor de huidige app in de simulator te gaan. Dit is handig wanneer we een document moeten inspecteren om opgeslagen of gecachete gegevens te controleren. De directory is dynamisch, dus het is moeilijk te detecteren. We kunnen echter de directory sorteren op de meest recent bijgewerkte. Hieronder zie je hoe we meerdere shell scripts
commando’s aan elkaar rijgen om de map te vinden.
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
Deze functie heeft me erg geholpen bij het ontwikkelen van Gallery om te controleren of video’s en gedownloade afbeeldingen op de juiste plaats zijn opgeslagen.
Hoewel, geen van de scripts lijkt te werken. Scripting heeft altijd deel uitgemaakt van macOS sinds 1993. Maar met de komst van de Mac Appstore en zorgen over de veiligheid werd AppleScript halverwege 2012 eindelijk aan banden gelegd. Dat was toen App Sandbox werd afgedwongen.
App Sandbox
App Sandbox is een toegangscontroletechnologie die wordt geleverd in macOS, afgedwongen op kernelniveau. Het is ontworpen om schade aan het systeem en de gegevens van de gebruiker te beperken als een app gecompromitteerd raakt. Apps die via de Mac App Store worden gedistribueerd, moeten App Sandbox ondersteunen.
Als een Xcode-extensie door Xcode moet worden geladen, moet deze ook App Sandbox ondersteunen.
Aan het begin van de App Sandbox-handhaving konden we App Sandbox Temporary Exception gebruiken om onze app tijdelijk toegang te geven tot Apple Script.
Dit is nu niet meer mogelijk.
De enige manier waarop AppleScript kan worden uitgevoerd, is als het zich in de map ~/Library/Application Scripts
bevindt.
Hoe installeer ik aangepaste scripts
MacOS-apps of -extensies kunnen niet zomaar zelf scripts in de applicatiescripts installeren. Ze hebben toestemming van de gebruiker nodig.
Een mogelijke manier om dat te doen is om Read/Write
in te schakelen en een dialoogvenster te tonen met NSOpenPanel
om de gebruiker te vragen de map te selecteren waarin onze scripts moeten worden geïnstalleerd.
Voor XcodeWay heb ik ervoor gekozen om een shellscript te installeren zodat de gebruiker een snelle manier heeft om scripts te installeren.
#!/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 is erg krachtig. Dit alles wordt expliciet gemaakt zodat de gebruiker volledige controle heeft over welke dingen gedaan kunnen worden.
Net als een extensie wordt een script asynchroon in een ander proces uitgevoerd met behulp van XPC voor inter-proces communicatie. Dit verbetert de beveiliging, omdat een script geen toegang heeft tot de adresruimte van onze app of extensie.
Meer beveiliging in macOS Mojave
Dit jaar, tijdens WWDC 2018, introduceerde Apple macOS Mojave dat zich richt op veel beveiligingsverbeteringen. In de Your Apps and the Future of macOS Security kunnen we meer te weten komen over nieuwe beveiligingseisen voor macOS-apps. Een daarvan is de gebruiksbeschrijving voor AppleEvents.
unable to load info.plist exceptions (egpu overrides)
We waren gewend om gebruiksbeschrijving te verklaren voor veel machtigingen in iOS, zoals fotobibliotheek, camera en pushmeldingen. Nu moeten we de gebruiksomschrijving opgeven voor AppleEvents.
De eerste keer dat onze extensie een aantal AppleScript-opdrachten probeert uit te voeren, wordt het bovenstaande dialoogvenster weergegeven om de gebruiker om toestemming te vragen. De gebruiker kan toestemming geven of weigeren, maar voor Xcode zegt u alstublieft ja ?
De oplossing voor ons is om NSAppleEventsUsageDescription
te declareren in onze app target. We hoeven alleen te declareren in de app target, niet in de extensie target.
<key>NSAppleEventsUsageDescription</key><string>Use AppleScript to open folders</string>
Waar te gaan vanaf hier
Huff huff, whew! Bedankt voor het volgen van zo’n lange reis. Het maken van frameworks en tools kost veel tijd, vooral plugins en extensies – we moeten voortdurend veranderen om ze aan te passen aan nieuwe besturingssystemen en beveiligingseisen. Maar het is een lonend proces, omdat we meer hebben geleerd en een aantal tools hebben om onze kostbare tijd te besparen.
Voor uw referentie, hier zijn mijn extensies die volledig open source zijn.
- XcodeWay
- XcodeColorSense2