Opór czy upór. Co wygra? Zaczynam wreszcie kodować w Swift

Rafał Piekarski
Smarter Life
Published in
5 min readMar 26, 2017

Muszę się przyznać, że nie jest mi łatwo z projektem, który mam stworzyć w ramach wyzwania “Daj się poznać”, czyli aplikacji pod roboczym tytułem “Domowa Apteczka”. Mam jakiś wewnętrzny opór, aby odpalić XCode i zacząć pisać coś w Swift.

Próbowałem go najpierw nieco oszukać i opisałem, jaki tutorial przerobię, aby się nauczyć pisać programy na iOS. Myślałem, że gdy opiszę to publicznie, pomoże mi to naprawdę go rozpocząć, bo przecież inni będą widzieli, że powinienem coś robić! Potem, dalej odwlekając kodowanie, stworzyłem nawet papierowy prototyp przyszłej aplikacji. Ale to wszystko na nic, bo przecież moim celem jest nauczenie się czegoś nowego i napisanie pierwszego projektu w Swift. Gdy nadszedł trzeci tydzień, uznałem, że koniec ściemniania. Trzeba wreszcie coś zakodować.

Jak zhakować wewnętrzny opór?

Pomysł, na który wpadłem, polegał na nie rzucaniu się od razu na głęboką wodę i pisaniu w nowym środowisku (XCode), na nowej platformie (iOS) i nowym języku (Swift). Ale na stworzeniu czegoś prostego, co będzie w okolicach projektu, ale pozwoli wykorzystać też moje kompetencje i umiejętności. Tym pomysłem było poznanie podstaw środowiska i nowego języka, ale podczas pisania miniprojektu, którego zadaniem będzie:

Pobranie z serwera pliku XML z listą leków dopuszczonych do obrotu w PL, oraz przeparsowanie go do konkretnej struktury danych zawierającej dane niezbędne dla przyszłej aplikacji, względnie wyszukanie w tym zbiorze konkretnego leku.

Brzmi prosto, oraz w technologiach, jakie znam, byłoby to trywialne zadanie. Więc poziomem wejścia będzie tylko nauczenie się składni języka, kilku metod z biblioteki standardowej i podstaw środowiska.

Mój pierwszy kod

Naiwne podejście sugerowało, aby wpierw pobrać plik z serwera, czymś à la open(url).read w Ruby. A następnie przeparsowanie jego treści i przemapowanie tagów na listę obiektów przedstawiających leki. Tymczasem środowiski Playground w XCode zrobiło mi psikusa, bo gdy wreszcie doszedłem do tego, że aby pobrać plik z serwera w Swift, potrzebuję instancji URLSession oraz wykonać na niej dataTask wraz z blokiem, to nie byłem w stanie wyświetlić żadnej treści z pobranego pliku. Okazało się, że ów blok (completionHandler) jest wykonywany asynchronicznie i Playground nie wie, że powinien zaczekać na jego wykonanie. Tak więc kod, owszem, zadziałałby w zwykłej aplikacji, ale próby uruchomienia poniższego w Playground kończyły się fiaskiem:

import Cocoavar fileUrl = "http://pub.rejestrymedyczne.csioz.gov.pl/..." 
let theUrl = URL(string: fileUrl)
let sess = URLSession.shared
let task = sess.dataTask(with: theUrl!) { (data, response, error) in
if let error = error {
print(error)
} else {
if let content = data {
print(content)
}
}
}
task.resume()

To, czego zabrakło, to poinstruowanie XCode, aby nie kończył wykonywania, gdy dojdzie do ostatniej linijki:

import PlaygroundPage
PlaygroundPage.current.needsIndefiniteExecution = true

oraz w momencie zakończenia callbacka dataTask, że cały kod się wykonał:

PlaygroundPage.current.finishExecution()

Inny sposób to dodanie w ostatniej linijce po prostu sleep(3), bo 3 sekundy powinny wystarczyć, aby plik pobrał się z serwera 😜. Ale nie polecam. Cały kod poprawnego rozwiązania dostępne na GitHub.

A sama nauka ekstremalnie ciekawa, czyli efekt osiągnięty, wkręciłem się.

Parser parserowi nierówny

Druga część zadania to przeparsowanie zawartości pliku i otrzymanie na wyjściu listy leków. Okazało się, że drogi do tego rozwiązania są co najmniej trzy:

  1. wykorzystać powyższy kod i po pobraniu pliku uruchomić funkcję parsującą na zmiennej content,
  2. przekazać adres zasobu do pobrania ( theUrl ) do XMLDocument.init(contentsOf:) i potem operując na drzewie XMLNode‘ów zamienić go na listę,
  3. przekazać adres zasobu do pobrania ( theUrl ) do XMLParser i poprzez własną implementację delegata XMLParserDelegate przetworzyć plik i zbudować wynikową listę.

Pierwsza opcja brzmiała najbardziej naiwnie, ale też nieco jak wynajdywanie koła na nowo, bo po co pisać swoje pobieranie zasobu z sieci, skoro inne obiekty już to potrafią. Z kolei trzecia możliwość brzmiała zbyt skomplikowanie jak na początek. Oraz była dostosowana do przetwarzania wielkich plików. Bo XMLParserDelegate pozwala zaimplementować dość niskopoziomowe metody takie jak: didStartElement, parserDidEndDocument, czy foundCharacters. Które, owszem, pozwalają również osiągnąć cel, ale komplikują finalne rozwiązanie.

Zdecydowałem się więc na pójście drogą numer 2. Czyli załadowanie całego dokumentu do pamięci, zbudowanie drzewa elementów i następnie przemapowanie go na docelową listę. To, co mi z tego wyszło, można podejrzeć o tu na GitHub.

Typ Optional, czyli jak się pozbyć błędów wykonywania i wartości nil

To, czego ważnego nauczyłem się podczas implementowania tego przetwarzania, to jak pracować z opcjonalnymi typami w Swift. Że jest kilka możliwości:

  • wykonać metodę tylko, jeśli obiekt istnieje. Ale trzeba pamiętać, że efekt również jest opcjonalny: list.first?.elements
  • zapodać wartość domyślną, gdyby nie było obiektu: element.attribute(forName: "kodATC")?.stringValue ?? "brak"
  • gdy jest się pewnym, że obiekt zawsze istnieje, to można go po prostu odpakować z Optionala: document.rootElement()!
  • zapodać instrukcję if let lub guard let, która rozpakuje go za nas i wykona kod wtedy, gdy wartość będzie istniała: if let content = data { ... }

Było to dla mnie coś nowego, bo do tej pory, pisząc w językach dynamicznych, takich jak Ruby czy JavaScript, zupełnie nie musiałem się przejmować typami. Podczas przetwarzania pliku XML było to podwójne wyzwanie, bo nawet z podejściem: jestem pewien, że ten atrybut to zawsze będzie, dopiero po przeprocesowaniu całego pliku mogłem się przekonać, czy coś jest, a czego może nie być. Takie: learning the hard way.

Aplikacja w konsoli terminala

Byłem zaskoczony, jak proste okazało się przeniesienie kodu z piaskownicy (Playground) do projektu budowanego jako aplikacja konsolowa. Ten prosty tutorial Raya Wenderlicha daje odpowiedzi na większość pytań - jak zacząć, jak operować wejściem/wyjściem czy skąd wziąć parametry wejściowe, a na końcu co skonfigurować, aby na końcu otrzymać binarkę do uruchomienia. I to zdumiewająco szybką, bo przetwarzanie pliku z wszystkimi ponad 60 tysiącami leków trwa na moim komputerze 4 sekundy.

Cały kod dostępny w repozytorium Home-First-Aid-Kit/PL meds DB. A ciąg dalszy z pewnością nastąpi, bo zamierzam poprawić jeszcze co nieco i dopisać wyszukiwanie leków. 👋

--

--

Rafał Piekarski
Smarter Life

MacUser, Programmer, Entrepreneur, Amateur photographer and someone who will be rich. Soon…