Przejdź do głównej zawartości

Swift. Dzielimy kod między aplikacjami


Czy można architekturę aplikacji mobilnej przekształcić na budowę bardziej modułową? W backendzie to zawsze się zwraca ale czy zwraca się w aplikacji mobilnej? Czy warto funkcjonalności pociąć na niezależne moduły skoro używamy ich i tak w jednej aplikacji? Czy ma to sens, żeby chować je za kolejną warstwą abstrakcji? Czy, w końcu, da się to zrobić w sposób wygodny, który nam pomaga zamiast stawać na drodze?
Tradycyjne podejście zakłada użycie CocoaPods. Każdy framework ma swoje repozytorium z którego dociągamy źródła i tworzymy projekt z podsami. Wszystko fajnie poza jednym szczegółem, chciałbym móc edytować źródła moich modułów w miejscu, gdzie ich używam i komitować bez dodatkowych czynności, tzn. chciałbym, żeby z punktu widzenia używania kodu fakt, że komponenty są w innym repozytorium, był niewidzialny. Chciałem użyć Swift Package Managera i jego pakietów. SPM w kwestii używania komponentów w sposób przezroczysty również przypomina cocoapodsy, bo mimo, że możesz mieć swój local copy package, to jednak w repozytorium źródłowym jest oryginał. Przymknąłbym na to oko ale w kodzie, którym się opiekuję, są też zewnętrzne biblioteki, które również chcę wepchnąć do niezależnego modułu. Ile bym nie kombinował, żeby je opakować w SPM, zawsze gdzieś mnie to ugryzło. W końcu odpuściłem i postanowiłem używać zwykłych Frameworks. Wydzielenie kodu do frameworka nie wydaje się trudne, ale biblioteka, o której wspomniałem sprawiła mi trochę kłopotu. 
Biblioteka przychodzi jako plik .a + pliki headerów. W aplikacji na iOS headery zaimportowałbym w bridging-headerze i problem by się sam rozwiązał. Niestety frameworki nie wspierają bridging-headerów. Tutaj trzeba posłużyć się modułem, a nawet jego mapą. 
Dodawanie modułu wygląda mniej więcej tak: dodajemy plik (który kreatywnie nazwałem module.map) będący właśnie mapą naszego modułu.



module MojModul {
    header "pierwszy_plik.h"
    header "kolejny.h"
    export *
}

Taki bajer sprawia, że mamy dostęp do zadeklarowanych w nagłówkach rzeczy po zrobieniu "import MojModul" (piszemy w swifcie, racja?). Nie wystarczy to jednak do poprawnego zbudowania naszego frameworka. Trzeba też dodać w build settings projektu w sekcji "Swift compiler - custom flags" w polu "Other swift flags" coś takiego: -Xcc -fmodule-map-file='/sciezka/do/pliku/module.map'.

Nie było łatwo to znaleźć, dlatego wolę opisać, ja na to zmarnowałem trochę czasu - Ty już nie musisz :-). 

To wystarcza, żeby zbudować bibliotekę.
Bardzo chciałem mieć projekt, który mógłbym rozwijać podczas normalnej pracy z aplikacją, która go używa. Bardzo bym nie chciał sytuacji w której żeby zrobić i sprawdzić prostą zmianę, musiałbym otworzyć jeden projekt, zrobić zmianę, zbudować, podpiąć framework do innego projektu, zorientować się, że źle zrobiłem swoją zmianę, poprawić, zbudować, podłączyć do apki...
Chciałbym zjeść cukierek i mieć cukierek. Edytować jak jedną aplikację a mieć wydzieloną paczkę gotową do użycia w innym projekcie. 
Zamiast budować więc binarkę i dołączać ją niezależnie do apki, umieściłem projekt frameworka w projekcie apki, co dało mi możliwość edycji wszystkiego na raz. (w finderze znajdujesz .xcodeproj frameworka i przeciągasz go niczym plik źródłowy gdzieś do otwartego głównego projektu apki w xcode, o tym mówię). To niestety przyniosło też pewne problemy. Mój projekt apki ma kilka konfiguracji builda. Niektóre z tych konfiguracji się budowały, inne nie. Konfigurowanie każdej osobno byłoby męczarnią, używamy w projekcie cocoapodsów i chciałbym, żeby pewne rzeczy robiły się jednak same. Od tego mamy przecież komputery. Tak jak w cocoapodsach, bo tam komponenty jednak dawały radę bez względu na to jaki nowy build config bym stworzył.  Skoro cocoapodsy działają, może jednak za szybko je odrzuciłem. Ostatecznie do pracy mogę importować podsy z :path => albo nawet robić poprawki w lokalnie ściągniętej kopii którą potem tylko skopiuję do oryginalnego repozytorium. Da się zrobić. 

Jeśli przenosisz swoje projekty do cocoapodsów, olej tutorial zaczynający się od "pod lib create", to strata czasu. Zwyczajnie stwórz swój .podspec i wyedytuj go ręcznie. Przykładowy podspec (z biblioteką statyczną, żeby uwzględnić problemy, które znalazły się na mojej drodze) wygląda tak:



Pod::Spec.new do |s|
  s.name             = 'NazwaKomponentu'
  s.version          = '0.1.0'
  s.summary          = 'A short description of NazwaKomponentu ;) .'


  s.description      = <<-DESC
Implementation of NazwaKomponentu longer description
                       DESC

  s.homepage         = 'https://adres.hołmpejdża.gdzie.to.w.koncu.wyląduje'
  
 #licencji nie mam ale wpisu nie wyrzuciłem. Dostaję przez to warning
 #podczas "pod install". Przeżyję
  s.license          = { :type => 'CUSTOM', :file => 'LICENSE' }
  
  s.author           = { 'Mła' => 'mą@imejl.com' }
  s.source           = { :git => 'https://adres.gita.gdzie.to.w.koncu.wyladuje.git' }

  s.ios.deployment_target = '11.0' #czy jakiśtam inny

  s.source_files = 'Folder/z/plikami/**/*'
  #systemowe frameworki ktorych uzywamy
  s.frameworks = 'SystemConfiguration','CoreLocation', 'UIKit' 
  #systemowe biblioteki, BEZ _lib_ z początku nazwy!
  s.libraries = 'resolv'
  #masz referencje do innych podów to tu je podasz. 
  #Nie przejmuj się adresem skąd je pobrać, to ustawisz w Podfile a nie w Podspecu
  s.dependency 'Other_Custom_cocoapod'    
  
  #zapamiętamy ścieżkę, bo będziemy jej używać
  s.preserve_path = 'moja/sciezka/do/pliku/module.map' 
  
  #po zaimportowaniu poda okazalo sie że cocoapods dodał mi jeszcze 
  #"NazwaKomponentu" do sciezki, musiałem potem sprawdzić gdzie to wylądowało i poprawić w podspecu
  s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/moja/sciezka/do/pliku/' }
  s.xcconfig = {
  'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/NazwaKomponentu/moja/sciezka/do/pliku/', 
    'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/NazwaKomponentu/moja/sciezka/do/pliku/ }	
 #a tak importujemy static liba
 s.ios.vendored_libraries = '/NazwaKomponentu/moja/sciezka/do/pliku/biblioteka.a' 
end

Wbrew pozorom (przeczytanie treści tego pliku zajęło pewnie mniej niż minutę), odnalezienie niektórych opji zajęło sporo czasu. Dlatego, jeśli chcesz zrobić swojego podsa i napotkasz problemy - masz ściągę (chyba, że trafisz na inne problemy ;) ). W ten sposób udało mi się stworzyć cocoapodsa i drugiego, który zależy od niego, zaimportować oba w moim projekcie iOS i mieć apkę nieco bardziej zmodułowaną i nadal działającą. To było ciekawe zadanie. Jeszcze jedna rzecz. Mam jedno repo na wszystkie komponenty przeznaczone do importu w różnych apkach. Żeby zrobić dwa lub więcej cocoapodsy w tym samym repo, tworzymy pliki podspec dla każdego z projektu. Te pliki umieszczamy w głównym folderze naszego repo a w s.source_files i podobnych właściwościach, w których jest ścieżka - naprowadzamy cocoapodsy na konkretne pliki. Tu też dużo kombinowałem, a okazało się proste. I to tyle :) Pozdrawiam!

Komentarze