Xamarin – Customers Manager – #4 – iOS

Poprzednie części serii

I część czwarta, a w niej zaktualizujemy projekt iOS o nowe widoki. Wykorzystamy gotową już warstwę Core, którą przygotowaliśmy w części trzeciej kursu. Do dzieła!

Dzień dobry panie iOS

Tak jak pisałem w części drugiej kursu, widoki na iOS możemy tworzyć za pomocą Interface Buildera lub kreować je z kodu. W tym kursie będziemy korzystać z pierwszej opcji. Zanim do tego  przejdziemy, tak jak w przypadku Androida i tutaj musimy skonfigurować projekt do pracy z MvvmCross. Stwórz klasę Setup. I umieść następującą definicję:

public class Setup : MvxIosSetup
{
    protected override void InitializeFirstChance()
    {
        base.InitializeFirstChance();
    }

    protected override IMvxIocOptions CreateIocOptions()
    {
        return new MvxIocOptions
        {
            PropertyInjectorOptions = MvxPropertyInjectorOptions.MvxInject
        };
    }
}

W przyszłości wszelkie rejestracje komponentów w kontenerze IoC wykonasz w metodzie InitializeFirstChance lub InitializeSecondChance. Dwie główne różnice pomiędzy tymi metodami są następujące:

  • Pierwsza z nich jest wywoływana na głównym wątku aplikacji zaraz po inicjalizacji kontenera IoC (co wydaje się logiczne, bo przecież z niego korzystamy).  
  • Druga z nich działa w tle i jest wywoływana na końcu inicjalizacji frameworka MvvmCross. Jest to świetne miejsce dla konfiguracji mechanizmów, których nie potrzebujemy zaraz po starcie aplikacji.

W drzewie projektu znajdź AppDelegate. Obecna definicja tej klasy wyglądać będzie prawdopodobnie tak jak na listingu poniżej. Czym jest AppDelegate? Jest to ogólnie mówiąc miejsce, w którym jesteśmy w stanie odebrać różnego rodzaju powiadomienia z systemu iOS. Może to być choćby standardowa notyfikacja, ale też przechwycenie informacji o przejściu naszej aplikacji do tła, czy jej powrotu. Nic nie stoi na przeszkodzie, aby przechwycić również informację o zamknięciu aplikacji. 

// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to application events from iOS.
[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    // class-level declarations

    public override UIWindow Window
    {
        get;
        set;
    }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        // Override point for customization after application launch.
        // If not required for your application you can safely delete this method

        return true;
    }

    public override void OnResignActivation(UIApplication application)
    {
        // Invoked when the application is about to move from active to inactive state.
        // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) 
        // or when the user quits the application and it begins the transition to the background state.
        // Games should use this method to pause the game.
    }

    public override void DidEnterBackground(UIApplication application)
    {
        // Use this method to release shared resources, save user data, invalidate timers and store the application state.
        // If your application supports background exection this method is called instead of WillTerminate when the user quits.
    }

    public override void WillEnterForeground(UIApplication application)
    {
        // Called as part of the transiton from background to active state.
        // Here you can undo many of the changes made on entering the background.
    }

    public override void OnActivated(UIApplication application)
    {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. 
        // If the application was previously in the background, optionally refresh the user interface.
    }

    public override void WillTerminate(UIApplication application)
    {
        // Called when the application is about to terminate. Save data, if needed. See also DidEnterBackground.
    }
}

Wprowadzimy zmiany w tej klasie. Zmienimy dziedziczenie na MvxApplicationDelegate, a jako parametry podamy wcześniej wspomniany typ Setup oraz pochodzący z projektu Core, typ App.

[Register("AppDelegate")]
public partial class AppDelegate : MvxApplicationDelegate<Setup, App>
{
    public override UIWindow Window { get; set; }

    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        var result = base.FinishedLaunching(application, launchOptions);

        return result;
    }
}

Do projektu iOS dodaj paczkę NuGet AutoMapper oraz FFImageLoading. Usuń widok Main.storyboard. Nie będziemy z niego korzystać, ponieważ dodamy za chwilę dedykowany widok z listą klientów. W folderze Views, dodaj nowy folder o nazwie Customers. Z mojego doświadczenia wynika, że tylko Visual Studio jest w stanie poprawnie stworzyć widok i jego wymagane zasoby, dlatego skorzystaj właśnie z niego i dodaj nowy ViewController CustomersListView w folderze Customers. 

Środowisko powinno stworzyć 3 pliki. Ten z końcówką designer jest autogenerowany przez środowisko podczas edycji widoku i nie powinniśmy go zmieniać ręcznie. Jego zawartość jest zmieniana podczas dodawania tzw. outletów. W uproszczeniu jest to połączenie pomiędzy widokiem a kodem aplikacji. Tworzymy je w Interface Builderze, a na ich podstawie Visual Studio utworzy nam referencje do nich właśnie w pliku designerUmożliwi nam to odwołanie się do konkretnych kontrolek widoku. 

Kliknij prawym na pliku z rozszerzeniem xib i uruchom go za pomocą Interface Buildera. Podstawy edycji za pomocą tego narzędzia przedstawiłem w części drugiej kursu. 

Xcode Interface Builder

W trakcie tworzenia widoku, będzie nam potrzebna grafika. Pokaże Ci, w jaki sposób dodawać zasoby graficzne do projektu. Pobierz najpierw zasoby graficzne, a następnie w folderze Resources, dodaj nowy zasób. W tym celu kliknij prawym na folderze i dodaj nowy plik. Z listy wybierz Asset Catalog i nazwij go Images. Kliknij dwukrotnie, pojawi się edytor zasobów. Na lewej jego części kliknij prawym przyciskiem myszki i wybierz New Image Set. Zostanie dodany zasób graficzny. Zmień jego nazwę na add, a następnie z prawej strony kliknij znajdujący się na samej górze kwadrat o nazwie 1x i wybierz plik graficzny o nazwie baseline_add_white_48dp.png. 

Lista klientów

Krok po kroku przedstawię Ci jak stworzyć widok listy klientów. 

  1. Podczas konstruowania widoków należy pamiętać o prawidłowej obsłudze Status Bar’a(o ile nasza aplikacja nie jest pełnoekranowa). Chcemy odpowiednio obsłużyć wcięcie (notch) w nowych iPhone oraz jednocześnie zachować odpowiednie zachowanie dla poprzednich iPhone’ów, dlatego skorzystamy z Safe Area – Obraz Ad.1.Przejdź do zakładki File Inspector w prawym panelu i zaznacz Use Safe Area Layout Guides w jego dolnej części.W drzewie projektu pojawi się nam obiekt Safe Area, który od tej pory będziemy uznawać za odnośnik dla górnej części naszego widoku. Dzięki niemu, nasz widok nie będzie znajdował się pod Status Bar’em. 
  2. Dodaj na widok roboczy standardowy widok View. Znajdziesz go na końcu listy z dostępnymi widokami. W drzewie projektu nazwij go Header. Zmień jego tło na #402143. Lewy i prawy constraint dodaj względem jego rodzica (leading, trailing space). Dodaj constraint (top space) w stosunku do Safe Area. Ich wartości ustaw na 0. Nasz Header powinien ustawić się w górnej części widoku. Obraz Ad.2.
  3. Dodaj etykietę tekstową Label, do wnętrza przed chwilą dodanego Headera. Nazwij ją CustomersHeader, a wartości jego constraintów ustaw na 20 jednostek od każdego z boków rodzica. Znajdzie się wtedy po środku. Zmień kolor czcionki za pomocą właściwości Color na wartość #E4E4E4.
  4. Dodaj na widok roboczy widok o nazwie Table View. Zmień nazwę na Customers. Dodaj Leading, Trailing oraz Bottom Space to Container w stosunku do głównego widoku. I ustaw wartości constraintów na 0. Następnie dodaj Vertical Spacing w stosunku do widoku Header i także ustaw wartość na 0. Obraz Ad.4.
  5. Usuń separatory i efekt zaznaczania komórek.Obraz Ad.5.
  6. Obraz poniżej Pozostało dodanie FAB. Dodaj na widok roboczy Button. Zmień jego nazwę na Add Button. Dodaj Trailing oraz Bottom Space to Container w stosunku do głównego widoku. Wartości tych constraintów ustaw na 15 jednostek. Musimy dodać, także aspect ratio dla rozmiaru przycisku. W tym celu dodaj constraint tak jakbyś tworzył go jak dla pozostałych widoków, ale jako cel wskaż nasz przycisk. We właściwościach tego constrainta zmień wartość Muliplier na 1:1. Następnie w identyczny sposób ustaw długość przycisku, ale zamiast Aspect Ratio, wybierz Width i ustaw wartość tego constraintu na 50 jednostek. 
  7. Wykorzystamy teraz grafikę, którą dodaliśmy wcześniej. Wybierz nasz przycisk i we właściwości Image wybierz grafikę add. Ustaw jeszcze kolor tła przycisku na #402143, zrobisz to zmieniając wł
Ad. 1. Safe Area
Obraz Ad. 2.
Obraz Ad. 4.

Świetnie, nasz widok jest gotowy, ale brakuje nam jeszcze połączenia z naszym kodem. Dodamy wcześniej wspomniane outlety. W prawym górnym rogu programu Xcode poszukaj ikony dwóch kółek. Po kliknięciu, pojawi się nam edytor. Ponad edytorem jest sekcja breadcrumbs. Kliknij na CustomersListView.m i z listy wybierz drugi plik – screeny poniżej.

Edytor
W pliku z rozszerzeniem *.h dodamy outlety
Ad. 5.Ustawienia tabeli

Nasz FAB, nie wygląda jeszcze tak jak powinien, ale naprawimy to za pomocą kodu. Teraz musimy dodać outlet dla tabeli i przycisku. W tym celu zaznacz tabelę w drzewie widoku i z wciśniętym przyciskiem CTRL przeciągnij ją po prostu do edytora. W oknie, które się pojawi w polu Name, wpisz Customers i kliknij Connect. To samo zrób z przyciskiem, jemu jednak nadaj nazwę AddButton. Świetnie, teraz wróć do Visual Studio i pozwól mu na przebudowanie zasobów. Możesz sprawdzić zawartość pliku designer. Powinna pojawić się tam właściwość Customers oraz AddButton.

Rozwinięcie tabeli

Tabela została dodana, ale nie mamy jeszcze widoku dla pojedynczego jej elementu. W tym celu dodamy komórki (ang. cell). W folderze Customer, stwórz folder Cells. I dodaj plik CustomerItemCellView, wykorzystując szablon Table View Cell. 

Dodanie nowej komórki

Zdefiniujemy wygląd komórki. Będzie to trudniejsze zadanie niż stworzenie widoku samej listy, ze względu na większą liczbę elementów. Będzie nam potrzebna grafika telefonu. Dodaj ją tak jak poprzednio i nazwij phone.

  1. Dodaj do widoku roboczego standardowe View. Nazwij go jako Separator Left. Dodaj constrainty względem rodzica (space), a ich wartości ustaw odpowiednio na 0,15,15 dla lewego, górnego i dolnego constrainta. Identycznie jak przy ustawianiu szerokości przycisku FAB, ustaw szerokość separatora na 5 jednostek. Kolor ustaw na #FF4789
  2. Dodaj ImageView i nazwij je Avatar. Ustaw aspect ratio na wartość 1:1. Dodaj Horizontal Spacing względem Separator Left i zmień wartość na 20 jednostek. Dodaj, także Top, Bottom Space względem rodzica – tutaj zmień wartości na 10 jednostek. 
  3. Dodaj etykietę tekstową Label i nazwij ją Name. Ustaw ją względem top Avatar i w odległości 20 jednostek względem prawej krawędzi Avatar. Wielkość czcionki ustaw na 20 jednostek i dodaj pogrubienie. Kolor czcionki możesz wybrać z listy, ja wybrałem Dark Gray Color
  4. Dodaj ImageView nazwij ją TelephoneImage.
    1. W tym miejscu chcemy skorzystać z wcześniej wykorzystywanej biblioteki FFImage, dlatego wejdź do zakładki Identity Inspector i w polu Class podaj nazwę MvxCachedImageView. Dzięki temu zabiegowi faktycznie będziemy korzystać z zalet biblioteki.
    2. Ustaw odpowiednią grafikę we właściwości Image kontrolki. Ustaw aspect ratio na 1:1, a szerokość na 16 jednostek. Dodaj constraint do lewego boku etykiety Name. Wyrównaj do dolnego boku etykiety Name, w tym celu podczas dodawania constrainta, wybierz Vertical Spacing i ustaw wartość na 10 jednostek.
  5. Dodaj etykietę tekstową Label i nazwij ją Number. Dodaj constraint to prawego boku obrazka Telephone Image, a wartość ustaw na 10 jednostek. Chcemy wyrównać ją względem obrazu telefonu. Podczas dodawania constrainta, wybierz Center Vertically.

Uff, jakoś poszło. Teraz pozostało dodać outlety. Stwórz outlet CustomerAvatar, CustomerTelephoneNumber, CustomerName. Z pewnością bez problemu to zrobisz. Kiedy się z tym uporasz, wróć do Visual Studio i pozwól mu przebudować zasoby. 

Czas na mały kodzik

Wróć do klasy CustomerCellView. Pamiętając, że do pojedynczej komórki, będziemy bindować obiekt CustomerListItemPO, dodamy odpowiednią implementację. Zamiast domyślnego dziedziczenia, skorzystamy z MvxTableViewCell, jest to jedna ze standardowych typów komórek dostarczanych przez MvvmCross. 

public partial class CustomersCellView : MvxTableViewCell
{
    public static readonly NSString Key = new NSString("CustomersCellView");
    public static readonly UINib Nib;

    static CustomersCellView()
    {
        Nib = UINib.FromName("CustomersCellView", NSBundle.MainBundle);
    }

    protected CustomersCellView(IntPtr handle) : base(handle)
    {
    }

    public override void AwakeFromNib()
    {
        base.AwakeFromNib();

        this.DelayBind(() =>
        {
            CustomerAvatar.LoadingPlaceholderImagePath = "res:user_placeholder.jpg";
            CustomerAvatar.ErrorPlaceholderImagePath = "res:user_placeholder.jpg";
        
            var set = this.CreateBindingSet<CustomersCellView, CustomerListItemPO>();

            set.Bind(CustomerName).For(v => v.Text).To(vm => vm.FullName);
            set.Bind(CustomerAvatar).For(v => v.ImagePath).To(vm => vm.AvatarUrl);
            set.Bind(CustomerTelephoneNumber).For(v => v.Text).To(vm => vm.Number);
            set.Apply();
        });
    }
}

Kilka słów wyjaśnienia. Zaczniemy od góry. Właściwość Key oraz Nib będą potrzebne podczas konfigurowania tabeli i mają na celu identyfikacje komórki. Moglibyśmy te wartości na sztywno podać podczas konfiguracji tabeli, ale ze względów czystego kodu lepiej jest to zrobić w ten sposób.  

Kiedy komórka zostanie zainicjalizowana tj. outlety są dostępne, uruchamiana jest metoda AwakeFromNib. To w niej dodajemy binding danych, który wygląda inaczej niż do tej pory przedstawiałem. Warto zaznaczyć, że w wersji Android, także możemy skorzystać z tego sposobu bindowania. No dobrze, ale co tu się dzieje?

Na samym początku, na podobnej zasadzie jak w wersji Android, dodajemy placeholder dla kontrolki avatara klienta. Dodaj teraz grafikę placeholder.jpg do zasobów projektu i zmień nazwę nowemu zasobowi na user_placeholder. Biblioteka FFImage samodzielnie odnajdzie odpowiedni zasób

Tworzymy obiekt bindowania, pomiędzy obecną komórką (widokiem) a modelem widoku co dokładnie odzwierciedlają użyte typy. W tym przypadku modelem widoku będzie obiekt PO, który zostanie tutaj dostarczony. Ten konkretny obiekt będzie dostępny dla mechanizmu bindowania na podstawie kontekstu, który zostanie niejawnie dostarczony. Już wkrótce pokaże Ci w jaki sposób się to dzieje. Za pomocą Fluent Api uzupełniamy co z czym chcemy połączyć. I tak na przykład ImagePath avataru klienta łączymy z adresem grafiki z modelu widoku. Na koniec wywołujemy metodę Apply na obiekcie reprezentującego bindowanie i w ten sposób binding dojdzie do skutku – oczywiście, jeżeli nie wystąpi jakiś błąd 🙂 

Do metody rozszerzającej DelayBind przekazujemy akcję. Dlaczego korzystamy z tej metody? Daje ona nam pewność, że bindowanie zostanie wykonane dopiero wtedy kiedy komórki tabeli będą w pełni gotowe do użycia.

Świetnie! Nasza celka (piękna nazwa dla komórki) jest już gotowa. Wykorzystajmy ją w tabeli klientów.

Lista klientów

Przejdź do klasy CustomersListView. Standardowo zmienimy dziedziczenie z domyślnego typu UIViewController, na MvxViewController, który pozwala na podanie modelu widoku. W kontrolerach widoków binding danych jest przeprowadzany w metodzie ViewDidLoad, ze względu na to, że wtedy widok jest gotowy do użycia i mamy dostęp do poszczególnych widoków składowych.

[MvxRootPresentation()]    
public partial class CustomersListView : MvxViewController<CustomersListViewModel>
{
    private MvxSimpleTableViewSource _source;

    public CustomersListView() : base("CustomersListView", null)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        Customers.RegisterNibForCellReuse(CustomersCellView.Nib, CustomersCellView.Key);
        Customers.RowHeight = 100;
 
        _source = new MvxSimpleTableViewSource(Customers, CustomersCellView.Key);
        Customers.Source = _source;

        var set = this.CreateBindingSet<CustomersListView, CustomersListViewModel>();
        set.Bind(_source).To(vm => vm.Customers);
        set.Bind(_source).For(v => v.SelectionChangedCommand).To(vm => vm.CustomerItemClickCommand);
        set.Apply();
    }
}

Customers jest referencja do tabeli klientów. Za pomocą RegisterNibForCellReuse przekazujemy informację jakiej komórki ma używać tabela. Potrzebujemy jeszcze stworzyć źródło dla tabeli. Można powiedzieć, że jest to odpowiednik adaptera dla RecyclerView w Androidzie. Dla standardowych rozwiązań, gdzie nie potrzebujemy personalizować tabeli MvvmCross dostarcza nam gotowe implementacje źródeł do wykorzystania. Jednym z nich jest MvxSimpleTableViewSource, zgodnie z nazwą jego użycie jest bardzo proste. Pierwszym parametrem jest referencja do tabeli, drugim identyfikator komórki (klucz). Następnie wystarczy przypisać gotowe źródło do właściwości Source tabeli. 

Binding danych jest standardowy, przypisujemy listę klientów z modelu widoku do źródła tabeli. Zauważ, że nie korzystamy z metody For, a to dlatego, że w tym przypadku MvvmCross domyślnie wykorzysta właściwość ItemsSource. Drugie bindowanie wykorzystujemy do poinformowanie modelu widoku o kliknięciu, w któregoś z klientów. 

Floating Action Button

Pozostało nam odpowiednio przygotować kształt FABa. Narazie jest kwadratowy, co nie wygląda zachęcająco, aby to zmienić została przygotowana poniższa metoda. Dodaj ją do widoku listy klientów, a następnie wywołaj zaraz po bindingu danych w metodzie ViewDidLoad.

Wprowadzamy modyfikacje do Layer przycisku. Layer odpowiada za wizualną zawartość kontrolki i możemy np. dodać do niej obramowanie, cień czy zmienić jej kształt. Zmieniamy kolor cienia, dodajemy także przesunięcie cienia względem obszaru przycisku, aby wzmocnić efekt 3D. Ustawiamy promień warstwy z cieniem i dodajemy przezroczystość. Zależy nam, aby przycisk był okrągły, więc zwiększamy zaokrąglenie jego rogów.

private void SetupFabButton()
{
    AddButton.Layer.ShadowColor = UIColor.Black.CGColor;
    AddButton.Layer.ShadowOffset = new CGSize(width: 0.0, height: 2.0);
    AddButton.Layer.MasksToBounds = false;
    AddButton.Layer.ShadowRadius = 1.0f;
    AddButton.Layer.ShadowOpacity = 0.5f;
    AddButton.Layer.CornerRadius = AddButton.Frame.Width / 2;
}

Sprawdź to

Teraz sprawdź rozwiązanie. Uruchom aplikację na rzeczywistym urządzeniu lub symulatorze. Zwróć uwagę, że zanim awatary zostaną pobrane przez chwilę będzie widoczny placeholder. W następnej części rozwiniemy naszą aplikację i dodamy możliwość podglądu wybranego klienta. 

Do następnego!

Github: https://github.com/krzbbaranowski/customer_manager_pb

Wynik końcowy

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu