SwiftUI – Přichází zemětřesení

Úvodní přednáška Worldwide Developer conference 2019 (WWDC) pro nás byla opravdovým překvapením letošního června. Představila nám SwiftUI. Největší objev od dob objevení jazyka Swift. A protože už máme hotový první projekt kompletně realizovaný ve SwitchUI, rád bych se s vámi podělil o naše zkušenosti.

SwiftUI: Alternativa pro vytváření uživatelských prostředí

To, že je UIKit respektive AppKit trochu za zenitem a moc se nehodí k jazyku Swift asi tušíme všichni. Včetně společnosti Apple, která se rozhodla jít dál a představila SwiftUI: alternativu pro vytváření uživatelských rozhraní. SwiftUI je podle jejích tvůrců deklarativní rychlý způsob pro tvorbu bohatých uživatelských rozhraní

Což je jistě pravda, ale po delším používání bych ještě dodal, že přináší kompletní změnu postupů a způsobu jak tvoříme aplikace pro iOS a iPadOS. 

Přechod na SwiftUI nevyžaduje pouze změnu UI frameworku, ale změnu téměř celé architektury aplikace a způsobu, jak řešíme různé problémy. Pokud máte aplikaci například v MVVM nebo Clean Swift architektuře, pak případné nasazení nebo přechod na SwiftUI bude celkem snadný. Pokud se však snažíte držet konceptu MVC, pak doporučuji spíše celý přepis. 

Obdobně se SwiftUI vyloženě nehodí na staré věci a projekty, kde je ještě velká část kódu v Objective-C. Není možné jej použít ani tam, kde je nutná podpora starších systémů. Knihovna je totiž dostupná jen v aktuálních systémech, tedy pro iOS je minimálním požadavkem verze 13.

 

Proč ji používat, když máme UIKit?

Protože je to budoucnost. Po realizaci a zkušenostech z projektu ve SwiftUI jsem o tom ještě více přesvědčen. Nová knihovna i přes své nedostatky už teď řeší obrovské množství problémů, které jsme s UIKit dříve měli. V některých oblastech dramaticky zrychluje dobu vývoje. 

Přímé zrychlení vývoje UI je zatím vykoupeno delší učební křivkou a nutností více řešit vlastní architekturu aplikace. Později se však investovaný čas vrátí zrychlením celkového vývoje. Když vám jde UI snadněji, máte pak více času vyladit architekturu a věci v pozadí. Samotná idea Apple by se dala vyjádřit větou: Snadno vytvářejte UI, abyste měli dost času řešit důležité věci.

UIKit tu určitě ještě dlouho bude, ale do budoucna bude už hrát jen druhé housle, na uchvacující sólo to už nikdy nebude. Vzhledem k běhu času je také dobré přemýšlet nad tím, že příští rok už podpora systému min. verze iOS 13 nebude limitující. Většina uživatelů jej bude mít dávno nainstalovaný a zákazníci budou ochotnější z případných požadavků na kompatibilitu mírně ustoupit.

 

Vývoj v Beta technologii je občas napínavý

Protože máme jako firma skvělé zákazníky, nebyl problém realizovat po dohodě se zákazníkem celý nový projekt ve SwiftUI. S projektem jsme začínali jen pár dnů po uvedení SwiftUI. Takže jsem začínal od první bety iOS 13.

První komplikace spočívala v tom, že na starém systému macOS sice můžete psát aplikace se SwiftUI, ale neuvidíte živý preview designovaného prvku nebo obrazovky. Pro rozumnou práci vás tedy nemine instalace macOS Catalina. Dalším trochu nepříjemným aspektem bylo, že jednotlivé Beta verze Xcode vyžadují odpovídající Beta verzi macOS. Beta verze alespoň na mém MacBook Pro nebyly ze začátku úplně vyladěné a spousta drobných chyb neustále zkoušela moji trpělivost.

Apple v rámci jednotlivých Beta vydání celkem zásadním způsobem měnil dostupná API ve SwiftUI. Některé postupy přestávaly s další verzí fungovat, upravené prvky se začaly chovat jinak, a tak bylo potřeba kód neustále upravovat. Naštěstí to nikdy nebyla operace na dny ale spíše na hodiny. Většina změn však byla k lepšímu a zdá se, že tým SwiftUI hlasu komunity dost naslouchal.

Nemalým problémem, se kterým budeme ještě pár měsíců bojovat, je celkový nedostatek informací. Což se sice rychle zlepšuje, ale pořád je to na míle daleko od situace kolem knihovny UIKit. Dokumentace ke knihovně je zatím trochu nedopečená a bylo by potřeba ji dost zlepšit. Spoustu věcí jsem musel hledat v interface knihovny průzkumem jejího rozhraní. Některá složitější řešení nejsou dobře popsaná a existuje k nim jen minimum zdrojů na internetu. Než se pustím do dalšího popisu, ukáži vám několik zajímavých aspektů této knihovny.

 

Já jsem View. Kdo je víc než modifikátor?

Základem SwiftUI je View, tedy vlastně protokol View. Jeho význam je podobný jako v UIKit. Jedná se o základní stavební kámen všech částí SwiftUI. Druhým zásadním aspektem je, že protokol aplikujeme na strukturu. Prvkem grafického rozhraní ve SwiftUI je tedy jakákoli struktura, která splňuje vlastnosti protokolu View. 

Počkat, počkat … tady něco nehraje. Struct je přece hodnotový typ. A ten se chová jinak než třída? Ano je to přesně tak, Apple se rozhodl pro prvky UI používat struktury. Odpadají tak některé problémy spojené s referenčními typy, naopak to však přináší některá drobná omezení a již na začátku zmiňovanou nutnost změnit způsob uvažování. 

Například v následujícím případu:

SampleView()
    .background(Color.green)
    .cornerRadius(8)
    .padding()

Nejprve vykreslíme SampleView a na něj aplikujeme modifikátor .background(Color.green), čímž nastavíme podbarvení View. Příkaz pozmění původní strukturu, přičemž do dalšího modifikátoru .cornerRadius(8) jde už změněná kopie původního SampleView a tak dále. Každý modifikátor nám vytvoří změněnou kopii z původních dat a aplikuje na ni svoje úpravy.

Apple říká, že toto je výsledek kterého chtěl dosáhnout a knihovna je na uvedené chování připravena. Nevadí větší množství aplikovaných modifikátorů ani zanořování jednotlivých View do sebe. Vše bude fungovat stejně rychle a podle očekávání.

 

Na pořadí záleží nejen ve sportu

Když v uvedeném příkladu prohodíme první dva modifikátory:

SampleView()
    .cornerRadius(8)
    .background(Color.green)
    .padding()

Dostaneme úplně jiný výsledek, neboť v tomto případě se zakulacení rohů aplikuje na nepodbarvené View, ale tato změna je ihned překryta použitím modifikátoru pro nastavením podbarvení, což odpovídá výše popsanému chování. 

Při designu UI je tak rozhodující pořadí jednotlivých modifikátorů. Občas to vyžaduje trochu zkoušení. S určitou mírou zkušeností však rychle budete vědět, jak mají jít modifikátory za sebou pro dosažení požadovaného zobrazení. Výhodou proti designu v UIKit je, že si dokážete lépe představit dopad jednotlivých modifikátorů na výslednou podobu a máte větší variabilitu v dosahování požadované vizuální podoby.

 

Shape. Později ukáže, co umí

Trochu opomíjený, přesto důležitý a zajímavý je Shape. Jak je dobrým zvykem ve SwiftUI, je také řešený protokolem. Primárně slouží k vytvoření nějakého tvaru, symbolu, ikony, atd. Vše navíc s podporou animování. Můžete si tak vytvořit například kruh, který se vám animovaně mění na čtverec. Níže ukazuji například jednoduchou obdobu proužku v UIProgressView:

struct ProgressLine: Shape {
    var value: Double

    private var relativeWidth: CGFloat {
        return CGFloat(min(value, 100)) / 100.0
    }

    func path(in rect: CGRect) -> Path {
        let start = CGPoint(x: 0, y: 0)
        let end = CGPoint(x: rect.size.width * relativeWidth, y: 0)

        var path = Path()
        path.move(to: start)
        path.addLine(to: end)

        return path.strokedPath(.init(lineWidth: 8, lineCap: .round, lineJoin: .round))
    }

    var animatableData: Double {
        get { return value }
        set { value = newValue }
    }
}

Je to tak, Path má nějak povědomou syntaxi. Nemýlíte se. Syntax a použití jsou podobné jejich obdobě z Core Graphics frameworku. Pokud tedy znáte způsob kreslení v Core Graphics, tady jej můžete lehce použít. Při vzorovém exportu z vektorového grafického editoru, který umí exportovat grafiku jako kód do Core Graphics, jsem nenarazil na nic, co by při vhodně nastaveném exportu nešlo rychle upravit a použít také ve SwiftUI.

 

Za vším hledej Model

Z výše uvedeného vyplývá nutnost někde uchovávat stavová data pro UI. Protože struktura nemůže sama sebe modifikovat (kromě použití mutating func, což tady není rozumně použitelné), musel tým SwiftUI přinést nějaký jiný dostatečně snadný způsob modifikace. Ve SwiftUI tak máme několik nových výrazů jako např. @State nebo @ObservedObject, které slouží jako property wrapper. V následujícím případu tak můžeme přímo v rámci struktury měnit hodnotu její property loading.

import SwiftUI

struct SampleView: View {
    @State var model = SampleModel()
    @State private var loading = false

    var body: some View {
        if loading {
            LoadingView()
        }

        VStack {
            Text(model.title)
                .font(.headline)
                .fontWeight(.bold)

            Text(model.description)
                .font(.subheadline)
            }
        }
        .onAppear {
            self.loadData()
        }
    }

    // MARK: - UI Actions

    private func loadData() {
        if !self.loading {
            self.loading = true
            ....
        }
    }
    ....
}

Už po pár chvilkách zkoušení SwiftUI dojdete k poznání, že bude dobré téměř nic ve View nemít. Knihovna vás svojí podstatou sama tlačí, abyste měli ve View jen samotné zobrazení a jakoukoli práci realizovali jinde. Sami se tak budete snažit přesunut co nejvíce dat a k nim vztažené logiky do modelu a ve View mít jen nezbytně nutné stavové proměnné.

Což ale obnáší již zmiňovanou nutnost změnit myšlení a přístup k řešení běžných problémů. Dobré je také před větším projektem připravit architekturu a otestovat jednotlivé způsoby propojení logických částí aplikace.

 

Je architektura opravdu nutná?

Bohužel je, bez vhodné architektury bude kód výsledné aplikace ve SwiftUI podobně nepřehledný jako kód aplikace v UIKit. Jen s tím rozdílem, že místo “Massive View Controller” budete mít “Massive Model”.

Projekt jsme realizovali v kombinaci s Clean Swift architekturou, která jde po menších úpravách dobře propojit také se SwiftUI. Hlavní změnou je rozdělení ViewController na View ve SwiftUI a přidružený Controller. Počet objektů popisujících jednu scénu nám tak vzrostl z původních šesti na sedm.

Z povahy věci není dobré mít ve View přímo zaveden objekt pro Controller. Jako lepší řešení se ukázalo použít jinou metodu jak z View odkazovat na příslušný kontrolér scény. Řešení spočívá ve vytvoření kontroléru pro scénu mimo View. Ten tak vlastní aplikace a spravuje samostatný objekt, který slouží jako správce a majitel všech kontrolérů v aplikaci. 

Jednotlivá View pro scény se pak tohoto objektu dotazují na odpovídající přidružený kontrolér. Není tím narušený životní cyklus View ani se nevyrábí pevná vazba mezi View a jeho kontrolérem. Dále jsme rozšířili novou třídu UIHostingController o mechanismus, který to za nás zautomatizuje. Stačí tak vytvořit nový hosting controller, ten si pak sám vytvoří přidružený kontrolér pro scénu a také jej sám uvolní, pokud jej už nebude potřeba.

 

Jak na navigaci v aplikaci?

SwiftUI přichází s novým způsobem jak propojovat jednotlivé obrazovky a provést přesun na další obrazovku. Nicméně my jsme se rozhodli tento mechanismus vůbec nevyužívat. Problémem je hlavně celková neslučitelnost s většinou běžných architektur. API pro NavigationLink využívaný ve SwiftUI není dle mého dobře navrženo a jde proti čistotě SwiftUI. API je sice snadné pro nováčky a docela dobře použitelné v menších aplikací, ale pro větší aplikace nebo ve spojení např. se zmiňovaným Clean Swift je nevyhovující. 

V našem projektu jsme použili standardní navigaci pomocí směrovačů, tak jak je definuje Clean Swift. Ale s rozdílem, že nesměrujeme na UIViewController ale na UIHostingController. Uvedené řešení nám umožnilo snadno používat také staré kontroléry z UIKit jako je např. UIImagePickerController. Dává nám také větší volnost při řešení složitějších případů navigace.

 

Interakce, interakce, interakce

Jednou z nejlepších vlastností SwiftUI je podpora plynulých animací. Ty fungují všude, ať už ve spojení s tvary Shape nebo se samotnými View. Mají extrémně jednoduchou syntax díky aplikování pomocí modifikátoru .animation a také předdefinované běžné výchozí styly animace. 

Naprosto geniální je pak předdefinovaná animace Spring, která vypadá velmi dobře a přitom nenásilně. Jako téměř vše ve SwiftUI ji aplikujeme přidáním modifikátoru .animation(.spring()). Podívejte se, jak to vypadá v praxi: 

Podporována jsou také běžná gesta a základní události, jako je zobrazení View. Jejich obsluhu SwiftUI řeší stejně jako jiné vlastnosti přidáním odpovídajícího modifikátoru. Co se týká samotných událostí na View je jich sice poskromnu, ale na běžné úkoly stačí více než dobře. Ve SwiftUI si na metody jako viewWillAppear z UIViewControlleru už ani nevzpomenete.

 

Jak do toho všeho zapadá Auto Layout?

Nezapadá. Po letech trénování, přesvědčování a mnohdy lítých bojů s Auto Layout ve snaze jej přimět k tomu, co jako vývojáři aplikací potřebujeme, přichází Apple s jeho odstraněním. Ve chvíli, kdy to už všichni nějak umíme, se tým SwiftUI rozhodl jít jinou cestou. Znalosti a zkušenosti s tvorbou složitějších konstrukcí vám tak budou úplně k ničemu a musíte začínat myslet jinak, znovu a snad radostněji. 

Základem layoutu ve SwiftUI je stack, tedy konkrétně VStack a HStack, jejichž význam je asi z názvu celkem jasný. Tuto dvojku pak doplňuje třetí ZStack, který slouží k vrstvení jednotlivých View nad sebe. Stack slouží jako kontejner pro další prvky a je také možné je do sebe vnořovat. Výsledný layout tak skládáte jejich kombinací.

Někdy jen samotný stack nestačí a budete si muset vypomoci modifikátorem .frame. Ten má několik variant a umožňuje vám poradit knihovně SwiftUI, jak by měla prvek vykreslit. Například pomocí .frame(minWidth: 0, maxWidth: .infinity) řeknete, že se má View roztahovat na největší dostupnou šířku. 

A když už selže úplně všechno, pořád je možné určit priority pro vykreslení jednotlivých prvků pomocí modifikátoru .layoutPriority. Zní to dobře, že? Ale co když budu chtít vytvořit nějaký složitý, nejlépe interaktivní layout?

 

GeometryRender pomůže

Občas jsem se dostal do situace, kdy jsem potřeboval vědět,  jak je konkrétní View na aktuálním layoutu velké, tedy jaká je jeho skutečná velikost, abych ji mohl někde dále použít. V UIKit by to nebyl problém a uměli bychom si snadno poradit. Ve SwiftUI se situace ale zásadně mění, neboť nejsme schopni přistupovat například ze svého View na jiné View, ve kterém je naše View umístěno. Ve SwiftUI o sobě View celkem nic neví a je to tak správně.

Pro tyto případy má knihovna speciální View nazvané GeometryRender, jehož jedinou funkcí je zpřístupňovat tyto údaje pro v něm obsažené View. GeometryRender si můžete představit jako kontejner pro View, který vám řekne, jak je obsažené View velké, kde se nachází na obrazovce, nebo třeba jaké jsou aktuální hodnoty pro Safe Area.

Údaje pak zpřístupňuje průběžně, lze je tak navázat na gesta a animace. GeometryRender je při správném a chytrém použití nesmírně mocný nástroj a myslím, že se teprve v budoucnu ukáže, co všechno lze s jeho pomocí vytvořit.

 

Jaká je tedy vlastně práce se SwiftUI?

Rozhodně velmi osvěžující. Přeci jenom v trochu unaveném světě UIKit je to čerstvý vánek a ono pomyslné otevřené okno do jinak zadýchané kanceláře. Jako všechno má i SwiftUI svoje nedostatky a na některé projekty se zatím vyloženě nehodí, ostatně jak jsem zmiňoval už na začátku. Naopak na jiné bych knihovnu určitě doporučil. 

Obecně se zatím i přes dobrou podporu formulářů moc nehodí na aplikace využívající klasický grouped styl UITableView. Zatím zde totiž není dobře zvládnutá podpora vysouvání klávesnice ani práce s poli pro zadávání textu. Snad se dočkáme v nějaké z příštích velkých aktualizací knihovny. Dále je knihovna vyloženě nevhodná, pokud jsou designem požadovány nějaké složitější kousky, obvykle nestandardního a pro iOS netypického chování.

Naopak se velmi hodí tam, kde chcete spíš prezentovat získaná data, požadujete různé speciální animované prvky a interakce. Tedy všude tam, kde chcete moderně vypadající aplikaci, která má vhodně použité mikro interakce a vypadá příjemně živě.

Pokud se výše uvedeného vyvarujete práce odsýpá po pár týdnech rychle a co je zásadní, spolehlivě. Stejně tak pokud je pro vás vývoj pro Apple platformy už tak trochu rutina, SwiftUI vás znova nakopne a budete si užívat všech nových postupů a řešení starých problémů novým a snad také lepším způsobem.

Přeji veselé kódování a objevování SwiftUI! 🙂

Václav Halík
iOS Developer

RSS