Articles

Jak przekonwertować swoje wtyczki Xcode na rozszerzenia Xcode

by Khoa Pham

Źródło: Imgur

Xcode to niezastąpione IDE dla programistów iOS i macOS. Od wczesnych dni, możliwość budowania i instalowania niestandardowych wtyczek dała nam ogromny wzrost produktywności. Nie minęło wiele czasu, zanim Apple wprowadził rozszerzenie Xcode z powodu obaw o prywatność.

Zbudowałem kilka wtyczek i rozszerzeń Xcode, takich jak XcodeWay, XcodeColorSense, XcodeColorSense2 i Xmas. To było satysfakcjonujące doświadczenie. Wiele się nauczyłem, a wydajność jaką uzyskałem była znaczna. W tym poście przedstawiam, jak przekonwertowałem moje wtyczki do Xcode na rozszerzenia i jakie miałem przy tym doświadczenie.

Mój pierwszy plugin do Xcode: XcodeWay

Wybieram leniwą osobę do wykonania ciężkiej pracy. Ponieważ leniwa osoba znajdzie łatwy sposób, aby to zrobić

Naprawdę podoba mi się powyższy cytat z Billa Gatesa. Staram się unikać powtarzających się i nudnych zadań. Ilekroć spotykam się z wykonywaniem tych samych zadań po raz kolejny, piszę skrypty i narzędzia, aby to zautomatyzować. Robienie tego zajmuje trochę czasu, ale będę trochę bardziej leniwy w najbliższej przyszłości.

Poza zainteresowaniem budowaniem frameworków i narzędzi open source, lubię rozszerzać IDE, którego używam – głównie Xcode.

Po raz pierwszy zacząłem rozwój iOS w 2014 roku. Chciałem mieć szybki sposób na nawigację do wielu miejsc bezpośrednio z Xcode z kontekstem bieżącego projektu. Jest wiele razy, kiedy chcemy:

  • otworzyć bieżący folder projektu w „Finderze”, aby zmienić niektóre pliki
  • otworzyć Terminal, aby uruchomić niektóre polecenia
  • otworzyć bieżący plik w GitHub, aby szybko podać link do kolegi z pracy
  • lub otworzyć inne foldery, takie jak motywy, wtyczki, fragmenty kodu, dzienniki urządzeń.

Każdy mały kawałek czasu, który oszczędzamy każdego dnia, liczy się.

Pomyślałem, że fajnym pomysłem byłoby napisanie wtyczki do Xcode, dzięki której moglibyśmy robić wszystkie powyższe rzeczy bezpośrednio w Xcode. Zamiast czekać aż inni ludzie to zrobią, podciągnąłem rękaw i napisałem swój pierwszy plugin do Xcode – XcodeWay- i udostępniłem go jako open source.

XcodeWay działa poprzez stworzenie menu pod Editor z wieloma opcjami nawigacji do innych miejsc bezpośrednio z Xcode. Wygląda to prosto, ale wymagało trochę ciężkiej pracy.

Czym są wtyczki Xcode?

Wtyczki Xcode nie są oficjalnie wspierane przez Xcode ani zalecane przez Apple. Nie ma żadnych dokumentów na ich temat. Najlepszym miejscem, w którym możemy się o nich dowiedzieć jest kod źródłowy istniejących wtyczek oraz kilka tutoriali.

Wtyczka Xcode jest po prostu pakietem typu xcplugin i jest umieszczona pod adresem ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins . Xcode, podczas uruchamiania, załaduje wszystkie wtyczki Xcode znajdujące się w tym folderze. Wtyczki są uruchamiane w tym samym procesie co Xcode, więc mogą robić wszystko to, co Xcode. Błąd w dowolnej wtyczce może spowodować awarię Xcode.

Aby stworzyć wtyczkę Xcode, stwórz macOS Bundle z jedną klasą, która rozszerza się z NSObject i ma inicjalizator, który akceptuje NSBundle , na przykład w Xmas:

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

Wewnątrz Info.plist, musimy:

  • deklarować tę klasę jako główną klasę wejściową dla wtyczki, oraz
  • że ten bundle nie ma UI, ponieważ tworzymy kontrolki UI i dodajemy do interfejsu Xcode podczas runtime
<key>NSPrincipalClass</key><string>Xmas</string><key>XCPluginHasUI</key><false/>

Kolejnym problemem z wtyczkami Xcode jest to, że musimy stale aktualizować DVTPluginCompatibilityUUIDs . Zmienia się to za każdym razem, gdy pojawia się nowa wersja Xcode. Bez aktualizacji, Xcode odmówi załadowania wtyczki.

Co potrafią wtyczki Xcode

Wielu programistów buduje wtyczki Xcode, ponieważ brakuje im specyficznych funkcji, które można znaleźć w innych IDE, takich jak Sublime Text, AppCode czy Atom.

Ponieważ wtyczki Xcode są ładowane w tym samym procesie co Xcode, mogą one robić wszystko to, co Xcode. Jedynym ograniczeniem jest nasza wyobraźnia. Możemy wykorzystać Objective C Runtime do odkrywania prywatnych frameworków i funkcji. Następnie LLDB i symboliczny punkt przerwania mogą być dalej używane do inspekcji działającego kodu i zmiany jego zachowania. Możemy również użyć swizzlingu do zmiany implementacji dowolnego działającego kodu. Pisanie wtyczek do Xcode jest trudne – dużo zgadywania, a czasami wymagana jest dobra znajomość montażu.

W złotej erze wtyczek istniał popularny menedżer wtyczek, który sam był wtyczką, zwany Alcatraz. Mógł on instalować inne wtyczki, które w zasadzie po prostu pobierają plik xcplugin i przenoszą go do folderu Plug Ins.

Aby zorientować się, co mogą zrobić wtyczki, spójrzmy na kilka popularnych wtyczek.

Xvim

Pierwszą na liście jest Xvim, który dodaje przypisania klawiszy Vim bezpośrednio w Xcode. Obsługuje on w większości wszystkie przypisania klawiszy, które mieliśmy wcześniej w Terminalu.

SCXcodeMiniMap

Jeśli brakuje Ci trybu MiniMap w Sublime Text, możesz użyć SCXcodeMiniMap, aby dodać panel prawej mapy wewnątrz edytora Xcode.

FuzzyAutocompletePlugin

Przed wersją 9, Xcode nie posiadał prawidłowego autouzupełniania – było ono po prostu oparte na prefiksie. Tutaj właśnie błyszczał FuzzyAutocompletePlugin. Wykonuje on rozmyte autouzupełnianie w oparciu o ukrytą w Xcode funkcję IDEOpenQuicklyPattern.

KSImageNamed-Xcode

Aby wyświetlić obrazek w pakiecie UIImageView, często używamy metody imageNamed. Jednak zapamiętanie dokładnej nazwy pliku z obrazem jest trudne. KSImageNamed-Xcode jest tutaj, aby pomóc. Otrzymasz listę automatycznie sugerowanych nazw obrazków, gdy zaczniesz pisać.

ColorSense-for-Xcode

Kolejnym swędzeniem podczas rozwoju jest praca z UIColor , która wykorzystuje przestrzeń kolorów RGBA. Nie otrzymujemy wizualnego wskaźnika koloru, który określamy, a ręczne sprawdzanie może być czasochłonne. Na szczęście jest ColorSense-for-Xcode, który pokazuje używany kolor oraz panel color picker, dzięki któremu łatwo wybrać odpowiedni kolor.

LinkedConsole

W AppCode możemy przeskoczyć do konkretnej linii w pliku, która jest logowana wewnątrz konsoli. Jeśli brakuje Ci tej funkcji w Xcode, możesz użyć LinkedConsole. Umożliwia to klikalne linki wewnątrz konsoli Xcode, dzięki czemu możemy natychmiast przeskoczyć do tego pliku.

Ciężka praca za wtyczkami Xcode

Utworzenie wtyczki Xcode nie jest łatwe. Nie tylko musimy znać się na programowaniu w systemie macOS, ale także zagłębić się w hierarchię widoków Xcode. Musimy zbadać prywatne frameworki i API, aby wstrzyknąć funkcję, którą chcemy.

Jest bardzo mało tutoriali, jak tworzyć wtyczki, ale na szczęście większość wtyczek jest open source, więc możemy zrozumieć, jak działają. Ponieważ stworzyłem kilka wtyczek, mogę podać kilka szczegółów technicznych na ich temat.

Wtyczki do Xcode są wykonywane zazwyczaj za pomocą dwóch prywatnych frameworków: DVTKit i IDEKit . Systemowe frameworki znajdują się pod /System/Library/PrivateFrameworks, ale frameworki, których Xcode używa wyłącznie są pod /Applications/Xcode.app/Contents/ , tam można znaleźć Frameworks , OtherFrameworks i SharedFrameworks.

Istnieje narzędzie class-dump, które może generować nagłówki z pakietu aplikacji Xcode. Mając nazwy klas i metod, możesz wywołać NSClassFromString, aby uzyskać klasę z nazwy.

Swizzling DVTBezelAlertPanel framework w Xmas

Święta Bożego Narodzenia zawsze dawały mi specjalne uczucie, więc postanowiłem zrobić Xmas, który pokazuje losowy świąteczny obrazek zamiast domyślnego widoku alertu. Klasą używaną do renderowania tego widoku jest DVTBezelAlertPanel wewnątrz frameworka DVTKit. Mój artykuł na temat budowania tego pluginu jest tutaj.

W Objective C Runtime, istnieje technika zwana swizzling, która może zmienić i przełączyć implementację i sygnaturę metody dowolnej działającej klasy i metody.

Tutaj, aby zmienić zawartość tego widoku alertu, musimy zamienić inicjalizator initWithIcon:message:parentWindow:duration: z naszą własną metodą. Robimy to wcześnie, słuchając NSApplicationDidFinishLaunchingNotification, który jest powiadamiany, gdy wtyczka macOS, w tym przypadku Xcode, uruchamia się.

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

Początkowo lubiłem robić wszystko w Swift. Ale trudno jest użyć metody swizzle init w Swift, więc najszybszym sposobem jest zrobienie tego w Objective C. Następnie po prostu przemierzamy hierarchię widoków, aby znaleźć NSVisualEffectView wewnątrz NSPanel, aby zaktualizować obraz.

Interakcja z DVTSourceTextView w XcodeColorSense

Pracuję głównie z kolorami heksadecymalnymi i chcę mieć szybki sposób na zobaczenie koloru. Więc zbudowałem XcodeColorSense – obsługuje kolor heksadecymalny, RGBA i nazwany kolor.

Pomysł jest prosty. Parsuje ciąg znaków, aby sprawdzić, czy użytkownik wpisuje coś związanego z UIColor, i pokazuje mały widok nakładki z tym kolorem jako tłem. Widok tekstowy, którego używa Xcode, jest typu DVTSourceTextView w DVTKit framework. Musimy również słuchać NSTextViewDidChangeSelectionNotification, który jest wyzwalany za każdym razem, gdy dowolna zawartość NSTextView jest zmieniana.

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}

Miałem architekturę Matcher, więc możemy wykryć różne rodzaje konstrukcji UIColor – na przykład 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) }}

Aby wyrenderować nakładkę, używamy NSColorWell, który jest dobry do pokazywania widoku z tłem. Pozycja jest określana przez wywołanie firstRectForCharacterRange i niektóre konwersje punktów za pomocą convertRectFromScreen i convertRect .

Używanie NSTask i IDEWorkspaceWindowController w XcodeWay

W końcu mój ukochany XcodeWay.

Znalazłem się w potrzebie przejścia do różnych miejsc z Xcode z kontekstem bieżącego projektu. Zbudowałem więc XcodeWay jako wtyczkę, która dodaje wiele przydatnych opcji menu pod Window.

Ponieważ wtyczka działa w tym samym procesie Xcode, ma dostęp do głównego menu NSApp.mainMenu?.itemWithTitle("Window") . Tam możemy zmieniać menu. XcodeWay został zaprojektowany w celu łatwego rozszerzenia funkcjonalności za pomocą swojego protokołu Navigator.

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

Dla folderów ze statyczną ścieżką, takich jak Profil Provisioning ~/Library/MobileDevice/Provisioning Profiles lub Dane użytkownika Developer/Xcode/UserData , możemy po prostu skonstruować URL i zadzwonić NSWorkspace.sharedWorkspace().openURL . W przypadku folderów dynamicznych, które zmieniają się w zależności od bieżącego projektu, należy wykonać więcej pracy.

Jak otworzyć folder dla bieżącego projektu w Finderze? Informacje dotyczące ścieżki bieżącego projektu są przechowywane wewnątrz IDEWorkspaceWindowController . Jest to klasa, która zarządza oknami obszaru roboczego w Xcode. Spójrzmy na EnvironmentManager, gdzie używamy objc_getClass, aby uzyskać definicję klasy z łańcucha znaków.

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

W końcu możemy wykorzystać valueForKey, aby uzyskać wartość dla dowolnej właściwości, która naszym zdaniem istnieje. W ten sposób nie tylko otrzymamy ścieżkę projektu, ale także ścieżkę do pliku otwierającego. Możemy więc wywołać activateFileViewerSelectingURLs na NSWorkspace, aby otworzyć Findera z wybranym plikiem. Jest to przydatne, ponieważ użytkownicy nie muszą szukać tego pliku w Finderze.

Wiele razy chcemy wykonać pewne polecenia Terminala na bieżącym folderze projektu. Aby to osiągnąć, możemy użyć NSTask z launch pad /usr/bin/open i argumentami . iTerm, jeśli skonfigurowany prawdopodobnie, otworzy to w nowej karcie.

Dokumenty dla aplikacji iOS 7 są umieszczone w stałej lokalizacji iPhone Simulator wewnątrz Application Support. Ale, od iOS 8, każda aplikacja ma unikalny identyfikator UUID, a ich foldery dokumentów są trudne do przewidzenia.

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

Możemy zbudować mapę i wykonać śledzenie, aby znaleźć wygenerowany identyfikator dla bieżącego projektu lub sprawdzić plist wewnątrz każdego folderu, aby porównać identyfikator bundle.

Szybkim rozwiązaniem, które wymyśliłem, było wyszukiwanie ostatnio zaktualizowanego folderu. Za każdym razem, gdy budujemy projekt lub wprowadzamy zmiany wewnątrz aplikacji, ich folder dokumentów jest aktualizowany. To właśnie tam możemy skorzystać z NSFileModificationDate, aby znaleźć folder dla bieżącego projektu.

Podczas pracy z wtyczkami Xcode jest wiele hacków, ale wyniki są satysfakcjonujące. Każde kilka minut, które oszczędzamy każdego dnia, w końcu oszczędza dużo czasu.

Bezpieczeństwo i wolność

Z wielką mocą przychodzi wielka odpowiedzialność. Fakt, że wtyczki mogą robić co chcą, alarmuje o bezpieczeństwie. Pod koniec 2015 roku miał miejsce atak złośliwego oprogramowania poprzez dystrybucję zmodyfikowanej wersji Xcode, zwanej XcodeGhost, która wstrzykuje złośliwy kod do wszelkich aplikacji zbudowanych za pomocą Xcode Ghost. Uważa się, że złośliwe oprogramowanie wykorzystuje między innymi mechanizm wtyczek.

Podobnie jak aplikacje iOS, które pobieramy z Appstore, aplikacje macOS, takie jak Xcode, są podpisywane przez Apple, gdy pobieramy je z Mac Appstore lub za pośrednictwem oficjalnych linków do pobierania Apple.

Podpisywanie kodu Twojej aplikacji zapewnia użytkowników, że pochodzi ona ze znanego źródła, a aplikacja nie została zmodyfikowana od czasu jej ostatniego podpisania. Zanim Twoja aplikacja może zintegrować usługi aplikacji, zostać zainstalowana na urządzeniu lub zostać zgłoszona do App Store, musi zostać podpisana certyfikatem wydanym przez Apple

Aby uniknąć potencjalnego złośliwego oprogramowania, takiego jak to, na WWDC 2016 Apple ogłosiło Xcode Source Editor Extension jako jedyny sposób na załadowanie rozszerzeń firm trzecich do Xcode. Oznacza to, że od Xcode 8 wtyczki nie mogą być ładowane.

Source Editor Extension

Rozszerzenie jest zalecanym podejściem do bezpiecznego dodawania funkcjonalności w ograniczony sposób.

Rozszerzenia aplikacji dają użytkownikom dostęp do funkcjonalności i zawartości Twojej aplikacji w całym systemie iOS i macOS. Na przykład, Twoja aplikacja może teraz pojawić się jako widżet na ekranie Today, dodać nowe przyciski w arkuszu Action, zaoferować filtry fotograficzne w aplikacji Photos lub wyświetlić nową systemową klawiaturę niestandardową.

Na razie jedynym rozszerzeniem do Xcode jest Source Editor, który pozwala nam czytać i modyfikować zawartość pliku źródłowego, a także czytać i modyfikować bieżący wybór tekstu w edytorze.

Rozszerzenie jest nowym celem i działa w innym procesie niż Xcode. To jest dobre, ponieważ nie może zmienić Xcode w żaden inny sposób niż zgodny z XCSourceEditorCommand, aby zmodyfikować bieżącą zawartość dokumentu.

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

Xcode 8 ma wiele ulepszeń, takich jak nowe funkcje uzupełniania kodu, literały obrazów i kolorów Swift oraz snippets. Doprowadziło to do rezygnacji z wielu wtyczek do Xcode. Dla niektórych niezastąpionych wtyczek, takich jak XVim, jest to nie do zniesienia dla niektórych osób. Niektóre stare funkcje wtyczek nie mogą być osiągnięte przy pomocy obecnego systemu rozszerzeń edytora źródeł.

Bez rezygnacji z Xcode

Obejściem pozwalającym ominąć ograniczenie z Xcode 8 dla wtyczek, jest zastąpienie istniejącej sygnatury Xcode techniką zwaną resign. Rezygnacja jest bardzo prosta – wystarczy utworzyć samopodpisany certyfikat i wywołać polecenie codesign. Po tej operacji Xcode powinien być w stanie ładować wtyczki.

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

Nie jest jednak możliwe przesyłanie aplikacji zbudowanych przy użyciu zrezygnowanego Xcode, ponieważ podpis nie odpowiada oficjalnej wersji Xcode. Jednym ze sposobów jest użycie dwóch Xcode: jednego oficjalnego do dystrybucji i jednego zrezygnowanego do rozwoju.

Przejście do rozszerzenia Xcode

Rozszerzenie Xcode jest drogą do zrobienia, więc zacząłem przenosić moje wtyczki do rozszerzenia. Dla Xmas, ponieważ modyfikuje hierarchię widoku, nie może stać się rozszerzeniem.

Color literal w XcodeColorSense2

Dla zmysłu koloru, przepisałem rozszerzenie od zera i nazwałem je XcodeColorSense2. To, oczywiście, nie może pokazać nakładki na bieżący widok edytora. Zdecydowałem się więc na wykorzystanie nowego Color literal, który można znaleźć w Xcode 8+.

Kolor jest wyświetlany w małej ramce. Może być trudno odróżnić podobne kolory, więc dlatego dołączam również nazwę. Kod jest po prostu o inspekcji selections i parsowaniu, aby znaleźć deklarację koloru.

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

Większość funkcjonalności jest osadzona wewnątrz mojego frameworka Farge, ale nie mogę znaleźć sposobu na użycie frameworka wewnątrz rozszerzenia Xcode.

Ponieważ funkcja rozszerzenia jest dostępna tylko poprzez menu Editor, możemy dostosować wiązanie klawiszy, aby wywołać ten element menu. Na przykład wybieram Cmd+Ctrl+S, aby pokazać i ukryć informacje o kolorze.

To oczywiście nie jest intuicyjne w porównaniu z oryginalną wtyczką, ale lepsze to niż nic.

Jak debugować rozszerzenia Xcode

Praca i debugowanie rozszerzeń jest proste. Możemy użyć Xcode do debugowania Xcode. Debugowana wersja Xcode ma szarą ikonę.

Jak instalować rozszerzenia Xcode

Rozszerzenie musi mieć dołączoną aplikację na macOS. Może to być dystrybuowane do Mac Appstore lub samodzielnie podpisane. Napisałem artykuł o tym, jak to zrobić.

Wszystkie rozszerzenia dla aplikacji muszą być jawnie włączone poprzez „Preferencje systemowe”.

Rozszerzenie Xcode na razie działa tylko z edytorem, więc musimy otworzyć plik źródłowy, aby menu Editor zadziałało.

AppleScript w XcodeWay

W rozszerzeniach Xcode, NSWorkspace, NSTask i konstrukcja klasy prywatnej już nie działają. Ponieważ użyłem Finder Sync Extension w FinderGo, pomyślałem, że mogę spróbować tego samego skryptowania AppleScript dla rozszerzenia Xcode.

AppleScript to język skryptowy stworzony przez Apple. Pozwala użytkownikom bezpośrednio kontrolować skryptowalne aplikacje Macintosha, a także części samego macOS. Można tworzyć skrypty – zestawy pisemnych instrukcji – w celu zautomatyzowania powtarzających się zadań, łączenia funkcji z wielu aplikacji skryptowych i tworzenia złożonych przepływów pracy.

Aby wypróbować AppleScript, można użyć aplikacji Edytor skryptów wbudowanej w macOS do pisania prototypów funkcji. Deklaracja funkcji zaczyna się od on i kończy na end . Aby uniknąć potencjalnych konfliktów z funkcjami systemowymi, zwykle używam my jako prefiksu. Oto jak polegam na Zdarzeniach systemowych, aby uzyskać katalog domowy.

Terminologię skryptową interfejsu użytkownika można znaleźć w pakiecie „Processes Suite” słownika skryptowego „Zdarzenia systemowe”. Ten zestaw zawiera terminologię dotyczącą interakcji z większością typów elementów interfejsu użytkownika, w tym:

  • okna
  • przyciski
  • kontraktory
  • menu
  • przyciski radiowe
  • pola tekstowe.

W Zdarzeniach systemowych klasa process reprezentuje działającą aplikację.

Wiele dobrych aplikacji obywatelskich wspiera AppleScript przez ujawnienie niektórych swoich funkcji, więc mogą one być używane przez inne aplikacje. Oto jak pobieram bieżącą piosenkę ze Spotify w 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

Aby poznać wszystkie możliwe komendy danej aplikacji, możemy otworzyć jej słownik w Edytorze Skryptów. Tam możemy dowiedzieć się, które funkcje i parametry są obsługiwane.

Jeśli myślisz, że Objective C jest trudny, AppleScript jest o wiele trudniejszy. Składnia jest obszerna i podatna na błędy. Dla odniesienia, oto cały plik skryptu, który zasila XcodeWay.

Aby otworzyć określony folder, powiedz Finder używając POSIX file. Każdą funkcjonalność refaktoryzuję do funkcji dla lepszego ponownego użycia kodu.

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

Aby uruchomić AppleScript wewnątrz aplikacji macOS lub rozszerzenia, musimy skonstruować deskryptor AppleScript z poprawnym numerem seryjnym procesu i identyfikatorami zdarzeń.

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}

Inne zadania, takie jak sprawdzanie bieżącego pilota Git, są nieco trudniejsze. Wiele razy chcę podzielić się linkiem do pliku, który debuguję z moim zdalnym kolegą z zespołu, aby wiedzieli do jakiego pliku się odnoszę. Jest to możliwe dzięki użyciu shell script wewnątrz 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

Możemy użyć quoted i konkatenacji łańcuchów do tworzenia łańcuchów. Na szczęście możemy wyeksponować Foundation framework i pewne klasy. Oto jak ja eksponuję NSString aby wykorzystać wszystkie istniejące funkcjonalności. Pisanie manipulacji ciągami od zera przy użyciu zwykłego AppleScript zajmie dużo czasu.

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

Dzięki temu możemy zbudować nasze inne funkcje do obsługi ciągów.

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

Jedną z fajnych funkcji, które obsługuje XcodeWay, jest możliwość przejścia do katalogu dokumentów dla bieżącej aplikacji w symulatorze. Jest to przydatne, gdy musimy sprawdzić dokument, aby sprawdzić zapisane lub zbuforowane dane. Katalog jest dynamiczny, więc trudno go wykryć. Możemy jednak posortować katalog pod kątem ostatnio aktualizowanych. Poniżej znajduje się sposób, w jaki łączymy wiele poleceń shell scripts, aby znaleźć folder.

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

Ta funkcja bardzo mi pomogła podczas tworzenia Galerii, aby sprawdzić, czy filmy i pobrane obrazy są zapisywane we właściwym miejscu.

Jednakże żaden ze skryptów nie wydaje się działać. Skryptowanie zawsze było częścią macOS od 1993 roku. Ale wraz z pojawieniem się Mac Appstore i obawami dotyczącymi bezpieczeństwa, AppleScript został ostatecznie ograniczony w połowie 2012 roku. To było, gdy App Sandbox został egzekwowany.

App Sandbox

App Sandbox jest technologią kontroli dostępu dostarczoną w macOS, egzekwowaną na poziomie jądra. Ma ona na celu ograniczenie szkód w systemie i danych użytkownika w przypadku kompromitacji aplikacji. Aplikacje dystrybuowane za pośrednictwem Mac App Store muszą obsługiwać App Sandbox.

Aby rozszerzenie Xcode mogło zostać załadowane przez Xcode, musi również obsługiwać App Sandbox.

Na początku egzekwowania App Sandbox mogliśmy użyć App Sandbox Temporary Exception, aby tymczasowo przyznać naszej aplikacji dostęp do Apple Script.

Teraz nie jest to możliwe.

Jedynym sposobem na uruchomienie AppleScript jest jego obecność w folderze ~/Library/Application Scripts.

Jak zainstalować niestandardowe skrypty

Aplikacje lub rozszerzenia dla systemu macOS nie mogą same instalować skryptów w Application Scripts. Potrzebują zgody użytkownika.

Jednym z możliwych sposobów jest włączenie Read/Write i pokazanie okna dialogowego przy użyciu NSOpenPanel, aby poprosić użytkownika o wybranie folderu do zainstalowania naszych skryptów.

Dla XcodeWay, wybieram zapewnienie skryptu powłoki instalacyjnej, aby użytkownik miał szybki sposób na zainstalowanie skryptów.

#!/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 jest bardzo potężny. Wszystko to jest jawne, więc użytkownik ma pełną kontrolę nad tym, co można zrobić.

Jak rozszerzenie, skrypt jest wykonywany asynchronicznie w innym procesie przy użyciu XPC do komunikacji między procesami. Zwiększa to bezpieczeństwo, ponieważ skrypt nie ma dostępu do przestrzeni adresowej do naszej aplikacji lub rozszerzenia.

Więcej bezpieczeństwa w macOS Mojave

W tym roku, na WWDC 2018, Apple zaprezentowało macOS Mojave, który skupia się na wielu poprawkach bezpieczeństwa. W sekcji Your Apps and the Future of macOS Security możemy dowiedzieć się więcej o nowych wymogach bezpieczeństwa dla aplikacji macOS. Jednym z nich jest opis użycia dla AppleEvents.

unable to load info.plist exceptions (egpu overrides)

Zwykliśmy deklarować opis użycia dla wielu uprawnień w iOS, takich jak biblioteka zdjęć, aparat i powiadomienia push. Teraz musimy zadeklarować opis użycia dla AppleEvents.

Źródło: https://www.felix-schwarz.org/blog/2018/08/new-apple-event-apis-in-macos-mojave

Pierwszym razem, gdy nasze rozszerzenie próbuje wykonać jakieś polecenia AppleScript, wyświetlane jest powyższe okno dialogowe, aby zapytać o zgodę użytkownika. Użytkownik może udzielić lub odmówić pozwolenia, ale dla Xcode proszę powiedzieć tak ?

Poprawką dla nas jest zadeklarowanie NSAppleEventsUsageDescription w naszym celu aplikacji. Musimy tylko zadeklarować w celu aplikacji, a nie w celu rozszerzenia.

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

Gdzie iść stąd

Huff huff, whew! Dzięki za śledzenie tak długiej podróży. Tworzenie frameworków i narzędzi zajmuje mnóstwo czasu, szczególnie wtyczek i rozszerzeń – musimy ciągle zmieniać, aby dostosować je do nowych systemów operacyjnych i wymagań bezpieczeństwa. Ale jest to satysfakcjonujący proces, ponieważ nauczyliśmy się więcej i mamy kilka narzędzi, które oszczędzają nasz cenny czas.

Dla twojego odniesienia, oto moje rozszerzenia, które są w pełni open source.

  • XcodeWay
  • XcodeColorSense2