fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
19 minut

“Swift okiem programisty C#” okiem programisty Swift


29.03.2018

Nazywam się Piotr Tobolski, pracuję w intive i mam doświadczenie w pisaniu aplikacji na platformę iOS i pochodne. Zarówno w Swift, jak i Objective-C.

Od dłuższego czasu czytam bloga Macieja i po przeczytaniu Swift okiem programisty C# stwierdziłem, że chciałbym mu się trochę odwdzięczyć i wspomóc swoim doświadczeniem. Odrzucił jednak moją propozycję (ponieważ chce pokazać proces uczenia się samodzielnie) ale wyszedł z inną propozycją – opublikowania mojej odpowiedzi na jego wpis. Jak widać doszło to do skutku.

Autorem tekstu jest Piotr Tobolski, który użycza swojego doświadczenia, komentując moje pierwsze wrażenia z pracy z Swift i Xcode. Dzięki!

Na wstępie chciałbym zaznaczyć, że bardzo cieszę się z nowego członka iOSowej społeczności! Im więcej wśród nas fajnych i kompetentnych ludzi tym lepiej. Uważam wybór technologii za bardzo dobry ;-) Z względu na dość zamkniętą platformę odpada wiele nieprzyjemnych zajęć (jak support dla egzotycznych urządzeń) i można skupić się na tworzeniu wysokiej jakości aplikacji co pozytywnie wpływa na warunki pracy. Estetyka narzędzi dostarczanych przez Apple również, choć z ich działaniem nie jest już aż tak dobrze, co dało się już zauważyć…

Trochę kontekstu

Trzeba pamiętać, że Swift jest bardzo młodym językiem. Został udostępniony w połowie roku 2014 ale trzeba mieć na uwadze kilka rzeczy:

  • Przechodził bardzo duże zmiany, np. składnia drastycznie zmieniła się wraz z wersją 3.0 czyli w zaledwie 1,5 roku temu.
  • Dopiero od wersji 3.2 ma stabilne źródła (czyli nowa wersja kompilatora będzie potrafiła skompilować kod starszej wersji języka), a wersja ta wyszła zaledwie pół roku temu (wraz z 4.0)!
  • Nadal nie ma stabilnego ABI choć ma się to zmienić na jesień, niestety nadal bez stabilności modułu co nadal nie pozwoli na dystrybuowanie skompilowanych bibliotek Swifta.
  • Musi on doskonale współpracować z Objective-C, ponieważ każde API udostępniane przez Apple nadal ma interfejs w tym języku. Co ciekawe – na pierwszy rzut oka tego nie widać – właśnie dzięki pracy włożonej w świetną kooperację tych języków. Do API Objective-C można dodawać odpowiednie adnotacje, które pozwalają na konwersję enumów, nazw metod, a nawet np. zamianę zwykłych funkcji C na metody w strukturach Swifta. Z uwagi na to, że Objective-C jest nadzbiorem C, kooperacja z C jest równie prosta.
  • Pomimo wymagań dotyczących Objective-C, język ten został stworzony praktycznie OD ZERA. W przeciwieństwie np. do Kotlina czy Scali, które opierają się na Javie. Prace nad nim wymagają więc dużo więcej pracy i czasu.

Nadal warto czasem pisać nowe aplikacje w Objective-C, a pisząc SDK jest to wręcz konieczność. Podchodząc poważnie do programowania na iOS trzeba się tego języka nauczyć.

Warto mieć na uwadze, że programowanie na iOS to nie tylko Swift, a wiele pojawiających się problemów to właśnie jego choroby wieku dziecięcego.

Pamiętajmy też, że używając Swifta w kontekście iOS lub macOS mamy dostępną również całą moc Objective-C ale np. na Linux już nie.

Inferencja typów

Bardzo ciekawa funkcjonalność Swifta ale w praktyce nie jest tak różowo. Niestety zabija złożonością obliczeniową. Nie jestem pewien czy zostało to już poprawione ale jeszcze niedawno sprawdzałem, że stworzenie tak prostej konstrukcji jak przypisanie do zmiennej (bez deklarowania typu) tablicy kilkudziesięciu Stringów może się kompilować godzinami!

Dlaczego? String implementuje wiele protokołów i kompilator musi jakoś wykombinować, że chcemy aby wynikowa referencja była typu [String], a nie np. [Equatable]albo [StringLiteralConvertible]. Z inferencją typów jest właśnie największy problem i najlepszym jego rozwiązaniem jest tworzenie krótkich funkcji lub dodawanie typów tam gdzie inferencja może sprawiać problemy.

Może w tym pomóc opcja kompilacji (Other Swift Flags) -Xfrontend -warn-long-function-bodies=<time_in_ms>, która spowoduje wygenerowanie ostrzeżenia gdy czas kompilacji metody będzie zbyt wysoki.

Implicitly unwrapped optional variable

Krótkie wyjaśnienie dla osoby z małym doświadczeniem w programowaniu: zwykła referencja, jak w Javie. Jeśli jest nullem i wywołasz na niej metodę to będzie crash.

Krótkie wyjaśnienie dla osoby nie znającej innego języka: taki Optional, który automatycznie i niejawnie (implicitly) wstawia po sobie wykrzyknik (unwrapping) podczas użycia. Co się stanie jeśli spróbujemy na siłę rozpakować Optionala, a on okaże się być nilem? Crash.

Z tego typu nie zaleca się korzystać, choć czasem jest to dużo wygodniejsze i może być akceptowalne, np. w przypadku outletów Interface Buildera (ale o tym może innym razem).

Funkcje

Argument labels – to nie do końca to i nie jest to lukier składniowy. Tutaj warto znać Objective-C żeby wiedzieć skąd takie, a nie inne sygnatury metod. Tak, etykiety przed parametrami są po prostu częścią sygnatury metody, które w Objective-C wyglądały na przykład tak:

(BOOL)insertString:(NSString *)someString atIndex:(int)index {}
// sygnatura to `insertString:atIndex:`
// wywołanie: [myContainer insertString:myString atIndex:myIndex];

A w Swift odpowiednik napisalibyśmy tak:

func insert(string: String, at index: Int) -> Bool {}
// sygnatura to `insert(string:at:)`
// wywołanie: myContainer.insert(string: myString, at: myIndex)

Swift jest pod tym względem bardziej elastyczny ale nadal dzięki wplataniu sygnatury metody pomiędzy parametry kod lepiej się czyta – jak powieść. Jest to chyba moja ulubiona cecha Swifta (a także Objective-C). W powyższym przykładzie można też dyskutować o zasadności użycia słowa “string” w metodzie Swiftowej ale założyłem, że jest to mój własny kontener posiadający również metodę insert(number:at:), która jest już zupełnie inną metodą.

Jeżeli chodzi o method dispatch w Swifcie to jest to dość skomplikowany temat. Napiszę tylko, że występuje wiele wariantów (message, table, static) i w rzadkich przypadkach może się wywołać nie to czego moglibyśmy się spodziewać (referencja typu interfejsu, który ma zaimplementowaną metodę w rozszerzeniu ale sam interfejs nie deklaruje, że obiekt taką metodę może posiadać…). Zainteresowanych szczegółami zapraszam tutaj.

Enumy

Moim zdaniem akurat bardziej dziwne jest to, że właśnie enumy mogą “dziedziczyć” po jakimś typie :-) Enumy w Swifcie są typem samym w sobie, a wręcz kolekcją typów… ciężko to określić. Powiedzmy, że są to takie wyjątkowe pogrupowane struktury, a do referencji można przypisać jedną ze struktur w ramach swojej grupy. Dlaczego akurat struktury, a nie wartości? Bo enumy mogą mieć powiązane wartości!

enum StringDataOperation {
    case update(String)
    case delete
}
switch(incomingOperation) {
    case .update(let newValue): // update record with newValue
    case .delete: // delete record
}

Interfejsy

W Swifcie zamiast tworzenia jednego protokołu implementującego kilka innych tworzy się po prostu alias. Przykład z biblioteki standardowej:

public typealias Codable = Decodable & Encodable

Metody opcjonalne to oczywiście korzenie Objective-C. Objective-C jest na tyle dynamicznym językiem, że można w nim zadeklarować metodę lub implementację protokołu… a następnie tego nie zaimplementować. Już od dawna kompilator wyrzuca błędy w takiej sytuacji ale konwencje nadal żyją. Na przykład: protokół źródła danych tabeli wymaga opcjonalnie metody zwracającej wysokość nagłówka jako double. Tabela zanim odpyta źródło danych sprawdzi czy została ona zaimplementowana poprzez użycie respondsToSelector:, a jeśli nie to przyjmie domyślną wartość.

Chciałbym zwrócić jeszcze jedną uwagę – czyste Swiftowe protokoły na to nie pozwalają. Tylko w protokołach z atrybutem @objc czyli są to w sumie protokoły Objective-C ale używane z poziomu Swifta.

Wyjątki

Tutaj powinniśmy sobie wyjaśnić jedną ważną rzecz. Obsługa błędów w Swifcie to NIE SĄ wyjątki!

Ale jak to? Przecież rzuca się je przy użyciu throw! W jaki sposób nie są więc wyjątkami?!

Składniowo tak, wyglądają dokładnie jak wyjątki ale pod spodem dzieje się coś zupełnie innego niż np. w C++. Nie jest to jednak prosty temat. Wyjątki wiążą się z odwijaniem stosu, które może wyraźnie wpłynąć na wydajność w momencie rzucenia wyjątku. W Swift tego nie ma, jest za to niewielkie pogorszenie wydajności w momencie wywoływania funkcji mogącej zaraportować błąd (czego nie ma w C++).

Działa to trochę tak jakby pod spodem metoda zwracała wartość LUB błąd, a przy każdym wywołaniu był warunek – jeśli wartość to działaj dalej, a jeśli błąd to przejdź do bloku catch lub zwróć go dalej gdy bloku nie ma.

Generyki

Temat rzeka. Chyba najtrudniejszy w całym Swifcie. Są bardzo elastyczne, pozwalają np. na stworzenie generycznego rozszerzenia innego obiektu z dodanymi warunkami (np. generyczny typ musi implementować protokół) i jeszcze sprawić, że rozszerzany obiekt będzie implementował jakiś protokół. Co do generycznych enumów to dobrym przykładem ze standardowej biblioteki może być… Optional. Tak, te znaki zapytania to tak naprawdę tylko lukier składniowy :-)

// Tak to z grubsza wygląda w bibliotece standardowej
enum Optional<T> {
case some(T)
case none
}

// Kolejny przykład z biblioteki standardowej
extension Array : Encodable where Element : Encodable {
public func encode(to encoder: Encoder) throws { // ... }
}

Xcode

Wiem dobrze co ludzie mówią o Xcode. Nie jestem jednak jednym z narzekaczy. Może to przez to, że do niego jestem najbardziej przyzwyczajony i to inne IDE są dla mnie nieintuicyjne, problematyczne i źle działają? W rzeczywistości zapewne po prostu Xcode umiem używać, umiem rozwiązać jego problemy, a inne IDE do niego porównuję i uważam za gorsze tylko przez to, że są inne… Nie znam wyników żadnych badań w tej kwestii więc powstrzymam się od silnych opinii ale z pewnością można stwierdzić, że Xcode wyraźnie różni się od innych IDE.

Osobiście najbardziej lubię w nim jego interfejs. Jest ładny, czysty i nie zaśmiecony. Dlaczego tak mało twórców programów narzędziowych przykłada się do estetyki? Czy wszystkie profesjonalne aplikacje muszą wyglądać jak MS Office 97?

Crashe

Po drugiej stronie barykady jest stabilność. Ciężko zaprzeczyć temu czego doświadczył Maciej. Xcode się wywala i to często. Zdecydowanej większości tych błędów winny jest Swift. Jako, że jest to język pisany od podstaw – narzędzia z nim związane są równie młode, ulegają równie dynamicznym zmianom i są jeszcze niedopracowane.

Gorsze jednak jest to, że nawet sample potrafią być nieaktualne! Gdy coś było pokazywane na konferencji Apple w Swift 2.0 to aktualna wersja IDE nie tylko nie skompiluje tego kodu ale nawet nie udostępnia migracji kodu do nowej wersji Swift bo wspiera w tej kwestii tylko jedną wersję wstecz.

Osobiście nie doświadczam crashy tak często ale może być to związane z większym doświadczeniem w Swifcie. Crashe pojawiają się szczególnie wtedy gdy napiszemy niepoprawny składniowo kod w wariancie, na który kompilator nie był przygotowany. Wiadomo, że ludzka kreatywność nie zna granic. Nie uważam jednak żeby to była odpowiednia wymówka dla programistów Apple. Dałoby się to rozwiązać lepiej, tak aby nie przerywać pracy programisty.

Warto wspomnieć, że przy używaniu Objective-C crashe nie pojawiają się tak często, a wręcz bardzo rzadko. Choć nadal Xcode ma tak dużo funkcji, że ciężko ich całkiem uniknąć. Np. podczas korzystania z bardzo rozbudowanego Interface Buildera.

Pobieranie

Mac App Store nie cieszy się dobrą sławą wśród użytkowników macOS, a szczególnie w przypadku gdy trzeba pobrać duży program lub co gorsza – zaktualizować go. Wielokrotnie zdarzało się, że aktualizacja, która miała mieć np. 300MB nie powiodła się, a Mac App Store zdecydował aby pobrać program ponownie. Cały. Kilka GB. I tak kilka razy…

Problem pojawia się też gdy potrzebujemy mieć kilka wersji Xcode. Xcode jest ściśle powiązany z SDK jakie jest z nim dostarczane. Pobierając Xcode 9.2 możemy używać tylko SDK iOS 11.2 (deploy oczywiście też na starsze systemy). Ale jeśli chcemy użyć np. SDK iOS 10.3 to musimy pobrać np. Xcode 8.3.3.

Do tego celu polecam użyć narzędzia xcode-install. Pozwala na łatwe zarządzanie wersjami Xcode z linii komend. Szczególnie przydaje się na CI ale lokalnie też warto z niego skorzystać. Dodatkowo upewnia się, że każda instalacja ma dostęp do odpowiednich pozycji w Pęku Kluczy i zaakceptowaną licencję aby uniknąć późniejszych żądań o wpisanie hasła.

Przy okazji wspomnę też o innych konsolowych narzędziach będących aktualnie praktycznie obowiązkowymi dla każdego programisty iOS:

Polecam też użyć jakiegoś menedżera wersji ruby (3/4 ze wspomnianych narzędzi jest w ruby). Osobiście korzystam z rbenv.

Playground

Zgadzam się, że jest to świetna koncepcja. Niestety wykonanie jest trochę gorsze, a ich domyślny tryb pracy (automatyczna kompilacja co chwilę, po wpisaniu kodu) potęguje tylko ilość crashy.

Warto wspomnieć, że są również na iPada choć w nieco innej formie.

AppCode

AppCode to chyba jedyne inne IDE, które może służyć do pisania aplikacji na iOS z użyciem Swifta. Niestety nie jestem w stanie go polecić. Szczególnie nie polecam go początkującym. Do wielu rzeczy i tak potrzebne jest Xcode (choćby Interface Builder!).

Często jest też w tyle za nowymi funkcjonalnościami iOSa lub Swifta. W świecie iOS nie można być w tyle. Gdy wychodzi nowe SDK to używa się go niemal natychmiast, a często nawet jak jeszcze jest w becie. Apple narzuca duże tempo ale ułatwia też radzenie sobie z nim. Po paru miesiącach od wyjścia nowego SDK aplikacje wysyłane do App Store muszą z niego korzystać ale użycie nowego SDK zwykle nie generuje problemów lub są one stosunkowo małe.

Dla osób mających doświadczenie z innymi IDE od JetBrains może się ono wydawać bardziej przyjazne i łatwiejsze do obsługi. Nie zamierzam się sprzeciwiać. Sam osobiście uważam takie Android Studio za bardzo dobre IDE. Ba! AppCode niektóre rzeczy robi lepiej od Xcode (np. refactoring). Niestety ze względu na opisane braki docelowo można z niego korzystać jak z uzupełnienia Xcode, a nie zastępstwa.

Usability

Jeśli chodzi o code completion to ctrl+spacja u mnie działa i nigdy nie miałem z tym problemów. Może inny program przechwycił ten skrót? Nie wiem. Musiałem u siebie specjalnie sprawdzić bo naprawdę rzadko z tego korzystam. Zwykle po prostu wpisuję fragment metody. Od niedawna wspierane jest też fuzzy search. Wystarczy wpisać kilka liter wchodzących w skład nazwy metody aby aby ją znaleźć. Czyli pisując “myViewController.vwiapp” podpowie nam metody viewWillAppear(animated:) oraz viewWillDisappear(animated:).

Warto też zapamiętać skrót Command+Shift+O gdyż służy on do przeszukiwania projektu i zależności (w tym też np. UIKit czy biblioteka standardowa Swift). Można znaleźć w ten sposób zarówno pliki jak i symbole (metody, klasy).

Możliwości refactoringu rzeczywiście są ograniczone. Szczególnie w przypadku Swifta – refactoring pojawił się ok. pół roku temu. Wraz z Xcode 9 i pierwszą wersją Swifta, która miała stabilne źródła. Co ciekawe te możliwości są trochę większe niż może się wydawać na pierwszy rzut oka, ponieważ w Xcode prawie wszystkie funkcje pojawiają się kontekstowo.

Kontekstowość funkcji Xcode może odpowiadać za wrażenie małych możliwości Xcode. W istocie tak jednak nie jest. Dla początkującego programisty to nawet dobrze bo nie przeraża go ogrom funkcjonalności i przeróżnych paneli. Ale w sumie to po co nam przyciski zatrzymywania i debugowania aplikacji gdy nie jest ona uruchomiona? Pojawią się dopiero jak się ją odpali. Wystarczy kliknąć na plik storyboard aby pojawił się bardzo zaawansowany Interface Builder służący do graficznej edycji widoków. W podobny sposób możemy odkryć, że Xcode posiada wbudowane np. View Debugger, edytor modeli baz danych Core Data, kompilator i generator kodu modeli CoreML, a nawet edytor scen 2D i 3D! Do tego bardzo rozbudowane Instruments służące do profilowania (dostarczane z Xcode jednak jako osobna aplikacja, podobnie jak symulator iOS).

Symulator iOS

Warto zwrócić uwagę na nazewnictwo. Symulator, nie emulator.

Na czym polega różnica? macOS oraz iOS są bardzo podobnymi systemami. Na tyle podobnymi, że podczas uruchamiania aplikacji iOS na symulatorze są one kompilowane na architekturę x86_64 i uruchamiane natywnie. Nie ma tam wirtualizacji. Symulowane jest środowisko uruchomieniowe, np. menedżer okien czy niektóre usługi systemowe.

Sprawia to, że aplikacje na symulatorze działają bardzo szybko i na fizycznym urządzeniu uruchamia się je względnie rzadko (choć oczywiście zależy tu dużo od specyfiki aplikacji). Dla mnie osobiście w większości przypadków wygodniejszy jest symulator, ponieważ nie muszę co chwilę odrywać rąk od klawiatury i myszy. Niestety symulacja OpenGL ES jest dość obciążająca, więc aplikacje, które z tego korzystają (gry, mapy) nie działają równie szybko (choć i tak radzą sobie całkiem sprawnie).

Deploy

To też jest jedna z rzeczy, która ostatnimi laty mocno się poprawiła. Łatwo jest podłączyć urządzenie, wybrać konto deweloperskie, kliknąć (raz lub kilka razy) Fix issue” i będzie działać. Niestety na dłuższą metę może to bardziej zaszkodzić niż pomóc bo nie wiemy co namiesza w profilach. Albo wiemy – ale wtedy nie jest nam ta funkcja potrzebna.

Apple bardzo mocno stawia na bezpieczeństwo. Na tyle bardzo, że staje się to uciążliwe. Niestety na to nie ma mocnych. Aby sobie z tym szybko i sprawnie radzić trzeba dobrze zrozumieć te mechanizmy bezpieczeństwa. Ale z drugiej strony może to i dobrze? Myślę, że nadal wielu programistów za mało przykłada się do kwestii bezpieczeństwa.

Długi okres oczekiwania na “Preparing debugger support” jest związany z koniecznością skopiowania symboli debugowania z konkretnej wersji systemu urządzenia na komputer. Symbole te potrafią zająć ~2 GB dla jednej wersji, więc warto co jakiś czas stare pliki kasować z ~/Library/Developer/Xcode/iOS DeviceSupport.

Płacić za konto deweloperskie trzeba gdy nasze wymagania rosną lub gdy chcemy wrzucić aplikację do sklepu. To również jest w miarę świeża zmiana, a informacja o konieczności płacenia nadal bywa błędnie rozpowszechniana. Poza tym co możemy znaleźć tutaj, limity narzucone na darmowe konta to:

  • 7 dniowy czas wygasania Provisioning Profile – czyli aplikacja na urządzeniu działa przez 7 dni od instalacji przez Xcode
  • 10 zmian w App ID w ciągu 7 dni – czyli w ciągu 7 dniach można zarejestrować do App ID 10 nowych aplikacji lub wykonać 10 aktualizacji App ID jednej aplikacji. Nic nie zabrania używania tego samego App ID w kilku projektach podczas pisania aplikacji. Aplikację można też bez problemu budować i instalować wielokrotnie

Podsumowanie

Bardzo ciekawe było dla mnie czytanie doświadczeń osoby wchodzącej teraz do środowiska Apple. Moje doświadczenia kilka lat temu trochę się różniły. Teraz wszystko działa zauważalnie sprawniej i wiele obiegowych opinii zamieniło się w mity. Jako programistę iOS bardzo mnie cieszy obecna sytuacja i dobrze wróży na przyszłość.

To kto jeszcze chce zostać programistą iOS? :-)

Powodzenia!

Comments are closed.

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również