W ostatnim czasie możemy zaobserwować, że tempo rozwoju technologii znacząco przyspieszyło. Apple nie przestaje nas zadziwiać szybkością wprowadzania coraz to nowszych i ciekawszych API do swojego ekosystemu, a sam Swift UI jest bardzo mocno promowany przez Apple jako zalecany sposób tworzenia aplikacji mobilnych.
W krótkim artykule nie sposób opisać wszystkich bibliotek, które pojawiały się w iOS od wersji 15 do 18, dlatego chciałbym się skupić zaledwie na kilku ciekawych zagadnieniach, na które udało mi się natrafić zarówno w codziennej pracy jak i w trakcie przeglądania najnowszych materiałów szkoleniowych oraz oficjalnej dokumentacji Apple.
W dzisiejszym wpisie opiszę pokrótce takie zagadnienia jak AGA (Automatic Grammar Agreement) i String Catalog, czyli nowe podejście Apple do lokalizacji swoich aplikacji, oraz jak stworzyć i wykorzystać w praktyce swój własny EnvironmentKey().
Zatem – nie przedłużając przydługiego już i tak wstępu, przejdźmy do meritum. Link do repozytorium znajdziecie na końcu artykułu.
AGA – Automatic Grammar Agreement
Myślę, że każdy z developerów spotkał się z problemem poprawnego odmieniania rzeczowników w zależności od ilości lub przynajmniej zastanawiał się nad takim problemem. Łatwo wyobrazić sobie przypadek, kiedy mamy za zadanie stworzyć widok, w którym, korzystając przykładowo ze Stepper(), zwiększamy ilość np. filiżanek kawy, które klient może zamówić przy użyciu tworzonej przez nas aplikacji.
Nawet w języku angielskim, gdzie sprawa sprowadza się najczęściej do dodania literki „s” do danego słowa, w określonych przypadkach poprawna odmiana może się mocno skomplikować. Przykładowo z one goose robi się two geese, a z one mouse – two mice.
W języku polskim sprawa wyglądałaby jeszcze ciekawiej, chociażby na przykładzie pączków – jeden pączek, dwa pączki, 11 pączków, 23 pączki… i prawdopodobnie z tego prostego powodu, Apple nie zdecydowało się na objęciem AGA naszego ojczystego języka 🧐
AGA w praktyce
Odstawmy na bok zawiłości gramatyczne i spróbujmy zobrazować na prostym przykładzie kodu, z czym przychodzi nam się mierzyć. Poniżej mamy zdefiniowane dwa przyciski do zwiększania i zmniejszania wartości oraz prosty tekst ze słowem „Bottle” powiązanym z ilością:

Efekt omawianego przykładu nietrudno przewidzieć – bez względu na wybraną ilość butelek otrzymamy dokładnie ten sam String – „Bottle” poprzedzony oczywiście właściwą ilością, którą wybraliśmy, korzystając z dwóch prostych przycisków.
W zrzucie ekranu prezentowanym poniżej możecie zobaczyć, jak to prezentuje się wizualnie – raczej nie jest to coś, co chcielibyśmy pokazać w czasie prezentacji na koniec sprintu naszym product ownerom.

W aplikacji chcielibyśmy uzyskać taki efekt, gdzie zdefiniowane przez nas słowo „Bottle” lub jakiekolwiek inne zostanie poprawnie odmienione zgodnie z obowiązującymi zasadami gramatyki.
Pojawia się zatem pytanie: W jaki sposób możemy osiągnąć nasz cel bez konieczności tworzenia dodatkowych funkcji zwracających odpowiednią wartość String dla odpowiedniej wartości liczbowej?
Tutaj z pomocą przychodzi nam właśnie Automatic Grammar Agreement, które działa na ten moment dla 6 najbardziej popularnych języków. Dla waszej wygody zebrałem całość w tabelę zbiorczą zaprezentowanej w dalszej części wpisu.
Wracając do tematu w przedmiotowej dyskusji oraz możliwości rozwiązania problemu, to w naszym przypadku wystarczy, że zamienimy poprzedni zapis na:
Text("^[\(count) Bottle](inflect: true)")
I uzyskamy efekt, jaki chcieliśmy uzyskać – angielskie słowo „BOTTLE” zostanie poprawnie odmienione w zależności od wybranej ilości.
AGA w praktyce – kolejne przykłady
Zmieńmy nieco nasz kod, aby pokazać więcej przykładów i przy okazji wystylizujmy nieco widok, korzystając z natywnych elementów Swift UI, dodając m.in. gradientowe tło, stylizując kontener, w którym znajdują się przykłady oraz wykorzystując Stepper() zamiast wcześniej wykorzystanego Button().

Uzyskany efekt:

W tym miejscu warto wspomnieć, że w warunkach produkcyjnych często otrzymujemy wartości z innych miejsc w aplikacji np. z View Modelu. W tym przypadku konieczne jest wskazanie typu LocalizedStringKey, czyli dla naszego przykładu, jeśli chcielibyśmy zdefiniować wartość tekstową np. w view modelu, to musielibyśmy zapisać nasz parametr w ten sposób:
var test: LocalizedStringKey {("^[\(count) Bottle](inflect: true)")}
}
AGM jest dostępne dla iOS wg tabeli poniżej i może okazać się pomocne przy lokalizacji aplikacji, nad którą pracujecie lub będziecie pracować w przyszłości. Nawet jeśli nie przyjdzie Wam skorzystać z tego rozwiązania, to przynajmniej już będziecie wiedzieć, jak zbudować poprawnie liczbę mnogą od słowa Goose w języku angielskim 🙂
| iOS 15 | Angielski |
| Hiszpański | |
| iOS 16 | Francuski |
| Włoski | |
| Portugalski (Brazylia) | |
| iOS 17 | Portugalski (Europa) |
| Niemiecki |
String Catalog – nowe podejście do lokalizacji w XCode
W ostatnim czasie Apple wprowadziło nowocześniejszą wersję lokalizacji, która można wykorzystać od wersji XCode 15.0. Zmianie uległo podejście do samej lokalizacji – wcześniej wykorzystywaliśmy Localizable.strings, w którym mozolnie wpisywaliśmy kolejne klucze lokalizacji.
W nowym wydaniu pojawiło się narzędzie, którego możemy użyć jako alternatywy do wymienionych powyżej Localizable.strings, jeśli oczywiście chcielibyśmy, aby nasza aplikacja obsługiwała więcej niż jeden język. Rozwiązanie to działa Out of the box, jeśli język aplikacji jest zgodny z ustawieniami systemowymi.
String Catalog w praktyce
Pozwólcie zatem przedstawić Wam, jak to rozwiązanie działa w praktyce. Przejdźmy zatem do rzeczy i na przykładzie kilku prostych tłumaczeń dodajmy lokalizację aplikacji oraz obsługę zmiany języka wykorzystującą natywne narzędzia SwiftUI.
W pierwszym kroku musimy dodać nowy katalog stringów do naszego projektu, który domyślnie dostaje nazwę Localizable.xcstring

Standardowo przechodzimy dalej i wybieramy nazwę dla naszego katalogu, a ja, na potrzeby artykułu, zostawiam wartość domyślną.
Jak możecie zauważyć – lista słów jest początkowo pusta.


W kolejnym kroku możemy dodać dodatkowe języki, które chcemy, aby były objęte lokalizacją. W moim przypadku dodałem język polski i hiszpański.


W przeciwieństwie do Localized.strings poszczególne wpisy uzupełniają się automatycznie w fazie budowania się naszego projektu. Jeśli wprowadzona wartość tekstowa będzie zgodna z typem LocalizedStringKey, już przy pierwszym uruchomieniu projektu, nawet w wersji z jednym widokiem, który zrobiliśmy do tej pory, uzyskamy coś takiego:

Jak możecie zauważyć, do tłumaczenia po uruchomieniu projektu automatycznie pojawiły się wszystkie wpisy z pól Text(). W moim przypadku nie widzę potrzeby tłumaczenia dwóch pierwszych rekordów, które pojawiły się na tej liście, więc z menu kontekstowego mogę wybrać opcję „Don’t translate”, co z automatu wyłączy lokalizację dla wszystkich dodanych języków w katalogu.
Dla przykładu przetłumaczmy sobie na początek przynajmniej ten jeden string odpowiednio dla języka polskiego i hiszpańskiego. Po wprowadzeniu tej wartości możemy zauważyć, że XCode oznaczył nam wszystkie tłumaczenia jako uzupełnione, natomiast jeśli nie jesteśmy pewni, czy tłumaczenie jest ok lub chcemy poprosić o sprawdzenie, możemy oznaczyć dany wiersz jako „Mark for review”.
Na tym etapie warto wspomnieć, że String Catalog posiada funkcjonalność różnicowania wpisów w zależności od wariantu urządzenia, czy też dodawania odpowiedniej formy dla liczby mnogiej danego wpisu.

Implementacja lokalizacji w aplikacji
Skoro już wiemy, jak posługiwać się katalogiem, to możemy przejść do implementacji lokalizacji
w aplikacji. W tym celu wykorzystamy @AppStorage oraz zmienne środowiskowe environment(). Dodamy też NavigationStack() oraz proste menu kontekstowe pozwalające na zmianę języka aplikacji w czasie rzeczywistym.
Na początek stwórzmy klasę AppData, gdzie będziemy przechować ustawienia użytkownika.
W naszym przypadku na tym etapie będzie to tylko jedna zmienna „language”, którą dodatkowo oznaczymy jako @AppStorage.
@AppStorage to nic innego jak Property Wrapper, który działa w sposób podobny do User Defaults, przechowując podstawowe ustawienia użytkownika.

Ten krok nie jest wymagany, jeśli nie chcemy mieć pełnej kontroli nad ustawieniami języka w aplikacji, ponieważ domyślnie aplikacja będzie działała w oparciu o ustawienia systemowe.
Aby osiągnąć zamierzony cel, dodatkowo wykorzystamymodyfikatory environment() oraz environmentObject(), które należy dodać do root view naszej aplikacji.

Następnie tworzymy widok pomocniczy zawierający Menu() do wyboru odpowiedniego języka:

Potrzebujemy jeszcze jeden dodatkowy widok pomocniczy, gdzie zaprezentujemy kilka prostych słów. Podobnie jak poprzednio po zbudowaniu projektu te słowa od razu pojawią się w katalogu stringów, gdzie będzie można je przetłumaczyć analogicznie do zrzutów ekranu pokazanych wcześniej.
Przykładowy kod dla omawianego widoku:

W przykładzie powyżej użyłem typu LocalizedStringKey. Nie jest to jednak niezbędne, aby zachować oczekiwaną przez nas funkcjonalność. Chciałem jedynie pokazać, jaki typ wykorzystywany jest przez Swift do procesowania lokalizacji.
Ostatnim etapem jest umieszczenie naszego widoku w NavigationStack(), oraz dodanie przygotowanego wcześniej przycisku menu z opcjami zmiany języka. W przykładzie poniżej dodatkowo umieściłem widoki w TabView, aby urozmaicić prezentację i zobrazować wykorzystanie tego API w aplikacjach mobilnych.

Ważne:
Zwróćcie uwagę, że jeśli chcemy korzystać z obiektów środowiskowych i Preview, koniecznie musimy dodać environmentObject() również w Preview. W innym przypadku nie będzie działać i nie będziemy mogli skorzystać z tej funkcjonalności przy projektowaniu widoku, który wykorzystuje dany element środowiskowy.
Efekt końcowy prezentuję się następująco – zmiana języka następuje natychmiastowo po kliknięciu w wybrany przycisk menu. Zmieniają się jednocześnie wszystkie słowa, które zostały zdefiniowane wcześniej w katalogu, uwzględniając również elementy menu.

Podsumowanie
W mojej ocenie nowe rozwiązanie to czytelna alternatywa dla poprzedniego rozwiązania Apple, która ma szereg ciekawych funkcjonalności. Dodatkowo, nawet jeśli w Waszym projekcie wykorzystywana jest poprzednia wersja lokalizacji, to migracja do katalogu stringów jest bardzo prosta.
Na sam koniec tej sekcji warto wspomnieć, że jeśli zajrzycie głębiej do projektu, to zauważycie, że String Catalog jest generowany w formacie JSON, co daje dodatkowe możliwości na etapie developmentu samej aplikacji, a sam katalog można łatwo wygenerować w XCode (Xcode, Product -> Export Localizable) i przesłać do tłumaczenia.
Environment Key – jak stworzyć i wykorzystać customowe klucze środowiskowe
Pracując ze SwiftUI, zapewne spotkaliście się z wykorzystaniem różnego rodzaju obiektów środowiskowych, które definiujemy jako @Environment(\.key) var yourKeyName. W Swift UI znajdziemy ich mnóstwo, wystarczy zacząć wpisywanie i pojawi się cała lista:

Przykładem użycia może być chociażby colorScheme:
@Environment(\.colorScheme) var colorScheme
Który możemy wykorzystać do np. zdefiniowania koloru teksu czy jego wielkość w zależności od ustawień preferencji systemowych:
.foregroundStyle(colorScheme == .dark ? .primary : .secondary)
Oczywiście, w tym przypadku możemy zdefiniować, jaki kolor zostanie użyty odpowiednio dla dark mode i light mode w Assetach. Niemniej, warto mieć w swoim arsenale umiejętności również taką wiedzę, zwłaszcza, że wykorzystanie innych zmiennych środowiskowych, jak np. .horizontalSizeClass, może okazać się już nie tak samo oczywiste, ale równie pomocne.
W mojej ocenie umiejętność wykorzystanie zmiennych środowiskowych obok tworzenia własnych styli oraz Extension do praktycznie dowolnego typu jest czymś, co podnosi umiejętności Swift UI na wyższy poziom.
Tworzenie nowych kluczy
Ale co możemy zrobić w sytuacji, kiedy interesujący nas klucz nie istnieje lub jego funkcjonalność nie spełnia naszych oczekiwań?
O ile tworzenie nowych styli dla elementów UI w SwiftUI opiera się na stworzeniu struct zgodnego z odpowiednim protokołem, jak np. ButtonStyle(), DatePickeStyle(), ProgressBarStyle() TextFieldStyle(), to w przypadku kluczy środowiskowych po prostu musimy stworzyć nowy.
Na potrzeby niniejszego wpisu omówimy sobie przykład pola, które jest wymagane do wprowadzenia, aby pójść dalej w danym formularzu, czyli coś, co nietrudno nam będzie sobie wyobrazić w codziennym użyciu.
Zacznijmy od stworzenia nowego klucza o nazwie isRequired:

Następnie, jako że chcemy wykorzystać nasz klucz przy tworzeniu nowych pól tekstowych i zakładamy, że będziemy wykorzystywać ten styl więcej niż raz, zdefiniujmy sobie CustomTextFieldStyle(), który wykorzystamy do stworzenia naszego widoku. Jak już wspomniałem wcześniej wykorzystanie styli to dosyć częsta praktyka przy tworzeniu aplikacji mobilnych w Swift UI, więc warto wiedzieć, jak można to zrobić.

Przy okazji tego widoku wykorzystałem dodatkowy Extension do View z funkcją – .if(), który ułatwia wykorzystanie różnych elementów UI. Bardzo łatwo można go znaleźć w sieci, ale pokażę go również tutaj. Jednocześnie dodaję funkcję isRequired, która ułatwi nam użycie zmiennej środowiskowej isRequired:

Następnie tworzymy widok pomocniczy CustomInputField(), w którym wykorzystamy nasz customowy styl oraz nasz nowy klucz środowiskowy, który przekażemy do stylu
@Environment(\.isRequired) var isRequired

Na sam koniec pozostaje nam jedynie zbudowanie widoku, który możemy wykorzystać w aplikacji. Dodatkowo w przykładzie pokazałem, jak można wykorzystać @FocusState do zarządzaniem focusem dla poszczególnych pól, co jest bardzo przydatne, jeśli mamy więcej jak jedno pole tekstowe.

Efekt końcowy naszych prac przedstawia się następująco:


Podsumowanie
Na zakończenie powyższej sekcji warto zaznaczyć, że przy wykorzystaniu istniejących czy stworzonych przez nas kluczy środowiskowych należy zachować czujność i rozwagę. Może się bowiem okazać, że aplikacja zacznie nam generować trudne do zdiagnozowania problemy, jeśli zapomnieliśmy użyć odpowiedniego klucza w odpowiednim miejscu i we właściwy sposób. Do minusów tego rozwiązania można zaliczyć trudniejsze testowanie naszej aplikacji.
Mam nadzieję, że udało mi się choćby w niewielkim stopniu przykuć Waszą uwagę i że omawiane tematy przydadzą się Wam w codziennej pracy.
Poniżej znajdziecie link do repozytorium z omawianymi przykładami oraz bibliografię, która pomogła mi w napisaniu tego artykułu.
***
Link do repozytorium z przykładami przestawianymi w blogu: https://github.com/Theordius/SwiftUIBlogExamples
***
Bibliografia
- Hudson P., „Pro SwiftUI”, Hacking with Swift, 2022
- Automatic grammar agreement
- How to use Automatic Grammar Agreement in Swift
- Grammar Agreement in Swift
Zostaw komentarz