środa, 10 sierpnia 2016

Xamarin.iOS kontra Swift odc.1 (narzędzia, składnia, nawigacja, toolbar, Music Library, async, SystemMusicPlayer, lokalizacja, ikony, UITableViewController, stylowanie)

Witam wszystkich serdecznie. Kolekcjonerskich edycji aplikacji Kolorofon z czasów DSP ciąg dalszy. Tym razem powstaje jeszcze bardziej kolekcjonerski klon niż poprzednio, w Xamarinie na system z jabłkiem, zaczynamy!

Obecnie mamy na github stan odpowiadający odcinkom 1, 2 oraz  5, 6 originału pisanego w Xcode i Swift. Poniżej tradycyjnie już screenshoty z działającej nieukończonej jeszcze app-ki (można przeglądać i wybierać pliki do odtwarzania i je odtwarzać), tym razem prosto z iphona:

IMG_0044  IMG_0045

Teraz wypada podzielić się wrażeniami.

 

#1 Narzędzia

Zainstalowałem na Mac mini Xamarina, dołączyłem się do niego z poziomu Visual Studio zgodnie z instrukcją. Co do certyfikatów, to póki co skorzystałem z generowanych za darmo przez Xcode 7 (jak wygeneruje w nim provisioning profile dla jakiejś nazwy, to potem mogę go wybrać w Visual Studio w ustawieniach projektu iOS Bundle Signing, oczywiście wcześniej wybierając tą samą osobę co na Xcode, nazwa identyfikatora w iOS Aplication też powinna się zgadzać). Podłączam telefon do Mac mini i hula deployment z Visual Studio.

Designer. Zdalne wykonywanie powoduje, że przy otwieraniu storyboard mamy przez chwilę pasek postępu, zanim wyświetli się nam zawartość. Designer obsługuje tworzenie przejść między kontrolerami, warunki layoutu czy klasy size, nie wspominam o ustawianiu właściwości obiektu, bo to oczywiste. Do działania nie musimy mieć uruchomionego na Mac-u Xcode, OS X może w ogóle wygasić ekran podczas pracy w Visual Studio i to w sumie chodzi.

vs_storyboard

Jak łączymy XML storyboard/xib z C#?  Nie jest tak jak w Xcode, przypomina to raczej pracę ze starszymi Windows Forms niż nawet XAML… O ile w XAML część partial okna/strony/kontrolki wynikająca z samego XAML nie jest widoczna na pierwszy rzut oka dla dewelopera, o tyle tutaj partial klasę jawnie generowaną i modyfikowaną przez designer mamy w projekcie, tak jak 11 lat temu po premierze VS 2005 i C# 2.0. Ktoś z czytających pamięta?

O designerze można poczytać sobie tutaj.

Odsłońmy kulisy kodu designera. Jak podamy nazwę klasy, a jej nie ma to zostanie wygenerowana z częścia dla nas i dla designera. Jak uzupełnimy pole Name na widgecie, to powstanie nam pole outleta w części designera np:

[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIToolbar toolbar { get; set; }

plus kod w metodzie ReleaseDesignerOutlets. Do tego pola można się odwołać w pisanej przez nas części kontrolera. A jak jest ze zdarzeniami?  Można dwa razy kliknąć na przycisk lub skorzystać z zakładki Events na obiekcie. Wygenerowane zostanie metoda partial w części designera:

[Action ("playPausePressed:")]
[GeneratedCode ("iOS Designer", "1.0")]
partial void playPausePressed (UIKit.UIBarButtonItem sender);

oraz w części dewelopera:

partial void playPausePressed(UIKit.UIBarButtonItem sender)
{

}

Udało mi się reużytkować plik .storyboard z oryginalnego projektu w Xcode. Co ciekawe nie musiałem każdego pola czy metody w XML generować w C# osobno. Skasowałem literę w Name jednego z pożądanych w code-behind obiektów, zapisałem, potem dodałem, znów zapisałem. Całość się przegenerowała, dostałem wszystkie potrzebne definicje refencji na pola i oczekiwane metody w C# danego kontrolera.

 

2# Składnia

W Xamarin protokoły Objective-C/Swift są reprezentowane w postaci klas abstrakcyjnych. Dlaczego nie interfejsy? Protokoły mogą mieć opcjonalne elementy, które w klasach abstrakcyjnych są odwzorowane w postaci metod wirtualnych. Przejdźmy do delegatów. W C# możemy odnaleźć odpowiednie zdarzenia, a jeśli są metody z wynikiem to albo implementujemy klasy wewnętrzne dziedziczące po klasach abstrakcyjnych protokołów albo stosujemy podejście mniej type-safe, ale bez tworzenia dodatkowej klasy z opatrzeniem implementowanych metod odpowiednim atrybutem. Można o tym poczytać tutaj.

W moim przypadku źródło danych dla FileListViewController dziedziczącego po UITableViewController wygląda tak:

public partial class FileListViewController : UITableViewController
    {
        static NSString cellId = new NSString("reuseIdentifier");

        List<MPMediaItem> filteredMediaItems;
        …

        public FileListViewController (IntPtr handle) : base (handle)
        {
        }

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

            TableView.Source = new TableSource(this);

            TableView.TableFooterView = new UIView();
            TableView.BackgroundView = new UIView();

            …
        }

        …

        class TableSource : UITableViewSource
        {
            FileListViewController controller;

            public TableSource(FileListViewController controller)
            {
                this.controller = controller;
            }

            public override nint RowsInSection(UITableView tableView, nint section)
            {
                var mediaItems = controller.GetMediaItems();

                if (mediaItems != null)
                    return mediaItems.Count;
                else
                    return 0;               
            }

            public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
            {
                cell.TintColor = UIColor.White;

                if (cell.TextLabel != null)
                    cell.TextLabel.TextColor = UIColor.White;

                if (cell.DetailTextLabel != null)
                    cell.DetailTextLabel.TextColor = UIColor.FromRGB(197, 225, 165);
            }

            public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
            {
                var cell = tableView.DequeueReusableCell(cellId, indexPath);
                var row = indexPath.Row;
                var mediaItems = controller.GetMediaItems();

                var item = mediaItems[row];
                if (cell.TextLabel != null)
                    cell.TextLabel.Text = item.Title;

                var artist = NSBundle.MainBundle.LocalizedString("unknownArtist", "Unknown Artist");
                var artistVal = item.Artist;
                if (artistVal != null)
                    artist = artistVal;

                var length = (int)item.PlaybackDuration;

                if (cell.DetailTextLabel != null)
                    cell.DetailTextLabel.Text = $"{artist} {GetDisplayTime(length)}";

                if (controller.selectedMediaItems != null && controller.selectedMediaItems.Contains(item))
                    cell.Accessory = UITableViewCellAccessory.Checkmark;
                else
                    cell.Accessory = UITableViewCellAccessory.None;

                cell.Tag = row;

                return cell;
            }

            …

            public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
            {
                var row = indexPath.Row;
                var mediaItems = controller.GetMediaItems();
                var item = mediaItems[row];

                if (controller.selectedMediaItems == null || !controller.selectedMediaItems.Contains(item))               
                    controller.selectedMediaItems?.Add(item);               
                else               
                    controller.selectedMediaItems.Remove(item);             

                tableView.ReloadData();
            }
        }
    }

Przechodząc ze Swift na C# brakuje mi w tym ostatnim skrótowego zapisywania enum-ów (znów piszę UITableViewCellAccessory.Checkmark zamiast .Checkmark) oraz rozbudowanej składni w if-ach czy guardów (znów muszę zapisywać najpierw coś do zmiennej, by potem korzystać z niej w warunku i dalszym kodzie zamiast zdefiniować ją wewnątrz if-a).

Na koniec punktu #2 dodatkowa ogólna uwaga odnośnie składni przy migracji Objective-C/Swift na C#.  Więcej mamy drobnych różnic niż podczas migracji z Javy na Android. Wynika to pewnie z większych różnic pomiędzy Objective-C/Swift a C# niż pomiędzy C# a Javą. Twórcy jednak zrobili wydaje się co mogli, by otrzymać jak najczystszą i prostą formę, czasami decydując się na pewne kompromisy. Warto przeczytać o API Design.

Plusem pisania w C# jest async. Dziś odciążyłem wątek UI podobnie jak kiedyś w Swift od ładowania utworów z Media Library:

        private async void LoadMediaItemsForMediaTypeAsync(MPMediaType mediaType)
        {
            await Task.Run(() =>
            {
                var query = new MPMediaQuery();
                var mediaTypeNumber = NSNumber.FromInt32((int)mediaType);
                var predicate = MPMediaPropertyPredicate.PredicateWithValue(mediaTypeNumber, MPMediaItem.MediaTypeProperty);

                query.AddFilterPredicate(predicate);

                allMediaItems = query.Items.ToList();
            });

            TableView.ReloadData();                      
        }

Wygląda to nieco zgrabniej niż kolejki iOS nawet i w Swift, choć do ideału brakuje asynchronicznych metod w samym systemowym API, a co jest na porządku dziennym w Windows Runtime API. O wątkach można poczytać tutaj.

W miarę rozwoju Xamarin też ewoluuje w mapowaniu pewnych rzeczy. Weźmy np. obsługę notyfikacji. W Swift piszemy wywołania AddObserver i RemoveObserver. W C# kiedyś też, ale od jakiejś wersji można nieco krócej:

NSObject notificationToken1;

notificationToken1 = NSNotificationCenter.DefaultCenter.AddObserver(MPMusicPlayerController.NowPlayingItemDidChangeNotification, NowPlayingItemChanged, player);

notificationToken1.Dispose();

Wołamy Dispose na tokenie zamiast długiego w zapisie wywołania RemoveObserver.

 

#3 Lokalizacja

Najlepiej przeczytać sobie jej opis i posiłkować się samplem. Xamarin trochę porządkuje pewną anarchię projektu Xcode, który trzyma pewne zasoby poza folderem całego projektu. W Xamarin możemy umieścić je w folderze Resources, a potem trafią w odpowiednie dla nich miejsce.

image

W dodatku zasoby trzech rodzajów: napisy w kodzie (Localizable.strings), napisy pakietu całej aplikacji np. jej nazwa (InfoPlist.strings) oraz napisy danego storyboard/xib (u mnie jest to Main.strings do Main.storyboard) są umieszczane w Xamarinie w tym samym folderze danego języka. Takiego porządku nie ma w projekcie Xcode. Co do samych plików to wziąłem je wprost z projektu Xcode, zmian co do ich formatu nie ma. W kodzie odczytujemy napis w następujący sposób:

var artist = NSBundle.MainBundle.LocalizedString("unknownArtist", "Unknown Artist");

Nieco inaczej, ale nazwa metody i jej parametry takie same jak w oryginale.

 

#4 Ikony

O ikonach dobrze rzucić sobie okiem tu oraz tu. Mamy ładne wsparcie dla asset-ów w Visual Studio:

image

Opis ikon nieco inny, ale dokładnie odpowiadają temu, co jest w Xcode.

 

#5 Stylowanie

Teraz sobie styluję w tonacji zielonej, potrzebowałem więc kilka rzeczy ostylować po raz pierwszy zamiast zadowolić się ich domyślną kolorystyką.

Tło paska nazwigacji i toolbara:  Bar Tint

Kolor ikony/napisu przycisku: Tint

Kolor Accessory w komórce UITableView:  Tint (u mnie checkmark)

Nieprzezroczysty toolbar: domyślnie jest przezroczysty, programowo trzeba ustawić:   toolbar.Translucent = false; 

W ostatnim przypadku designer we właściwościach nie zawsze podaje prawdę, bo przezroczystość była  na dzień dobry odznaczona, a domyślnie jak wiemy jest. Podobnie nie wiem czemu po ustawieniu niestandardowego koloru we właściwościach Visual Studio pokazywane są później inne wartości z jakiegoś predefiniowanego koloru…

 

Na tym dziś kończymy. W najbliższym czasie zamierzam przenieść wyszukiwarkę.

Brak komentarzy: