sobota, 24 września 2016

Xamarin.Forms kontra Xamarin odc.2 (XAML, lista, search, własna kontrolka)

Dobry wieczór, Xamarin.Forms kontra Xamarin, witam serdecznie Winking smile Zrobiła się nieco dłuższa przerwa od ostatniego razu, ale powinno zaowocować to mocniejszym materiałem. XAML to język znany od lat dziesięciu. Czy jednak XAML w Xamarin.Forms w zestawieniu z XAML UWP czy WPF nie jest ich dość odległym kuzynem niczym w przyrodzie wilk i wilk workowaty?  Zaczynamy!

Polem do dyskusji tradycyjnie będzie już porcja nowego kodu z repozytorium LightOrgan/Xamarin.Forms/. Od ostatniego odcinka na dwóch stronkach XAML przybyło całkiem sporo elementów. Prezentują się one nieco inaczej na platformie Android:

Screenshot_20160923-020818  Screenshot_20160923-020951

i platformie iOS:

IMG_0059  IMG_0060

 

#1  Pasek odtwarzacza

Mówimy o tym pasku na dole strony MainPage. Widzimy że dość różni się pomiędzy Android i iOS. Tymczasem zrobiłem go za pomocą wspólnego kawałka XAML:

<RelativeLayout Grid.Row="1" BackgroundColor="#e57373">     
      <RelativeLayout.Resources>
        <ResourceDictionary>
            <OnPlatform x:TypeArguments="ConstraintType" Android="RelativeToParent" iOS="Constant" x:Key="XType" />
            <OnPlatform x:TypeArguments="x:Double" Android="-52" iOS="8" x:Key="XConstant" />
            <OnPlatform x:TypeArguments="x:Double" Android="22" iOS="8" x:Key="YConstant" />
            <OnPlatform x:TypeArguments="x:String" Android="AlbumArt" iOS="PlayPauseIcon" x:Key="TitleRelativeElement" />
        </ResourceDictionary>
    </RelativeLayout.Resources>   
      <RelativeLayout.HeightRequest>
        <OnPlatform Android="80" iOS="48" x:TypeArguments="x:Double"/>
      </RelativeLayout.HeightRequest>
      <ContentView x:Name="AlbumArt"
                   Content="{StaticResource AlbumArt}"
                   RelativeLayout.XConstraint="{ConstraintExpression Type=Constant, Constant=8}"
                   RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=8}"
                   RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=1, Constant=-16}">       
      </ContentView>     
      <Label x:Name="Title" TextColor="White"
             RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, ElementName={StaticResource TitleRelativeElement}, Property=Width, Factor=1, Constant=16}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=16}">
        <Label.FontSize>
          <OnPlatform x:TypeArguments="x:Double" Android="18" iOS="14"/>
        </Label.FontSize>     
      </Label>     
      <Label x:Name="Artist" FontSize="14" TextColor="#ffebee"
             RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AlbumArt, Property=Width, Factor=1, Constant=16}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant=40}">
        <Label.IsVisible>
          <OnPlatform x:TypeArguments="x:Boolean" Android="True" iOS="False"/>
        </Label.IsVisible>     
      </Label>   
      <Image x:Name="PlayPauseIcon"
             RelativeLayout.XConstraint="{ConstraintExpression Type={StaticResource XType}, Property=Width, Factor=1, Constant={StaticResource XConstant}}"
             RelativeLayout.YConstraint="{ConstraintExpression Type=Constant, Constant={StaticResource YConstant}}">
        <Image.Source>
          <OnPlatform x:TypeArguments="ImageSource" Android="ic_play_arrow_white_36dp.png" iOS="Play.png"/>
        </Image.Source>     
      </Image>     
    </RelativeLayout>   

Aha, zasób AlbumArt jest zdefiniowany w całym MainPage jako:

      <OnPlatform x:Key="AlbumArt" x:TypeArguments="View">       
        <OnPlatform.Android>
          <Image BackgroundColor="#9ccc65" Source="ic_audiotrack_white_36dp.png" Aspect="AspectFit" WidthRequest="64" HeightRequest="64"/>
        </OnPlatform.Android>       
      </OnPlatform> 

Stosując ContentView z zasobem zdefiniowanym wariantowo poprzez OnPlatform udało mi się pokazywać obrazek tylko w Android. Teraz trochę uwag. Czemu ContentView a nie ContentControl? Czemu BackgroundColor a nie Background? Aspect? WidthRequest?  Takich rzeczy nie znajdziemy w żadnym innym gatunku XAML. A RelativeLayout?  Niemal zupełnie inna koncepcja niż tego w XAML UWP z Windows 10!  ConstraintExpression występuje tylko w Xamarin.Forms. Brakuje mi formy znanej nie tylko z Windows 10, ale i natywnego .axml z Android. W dodatku czuję tu pewne ograniczenia. Centrowanie i układanie w stosunku do sąsiadów nie wydaje się dostatecznie wygodne.

 

#2 lista

Znów widzimy różnice. W XAML to jeden kawałek wspólny dla wszystkich platform:

<ListView x:Name="listView" BackgroundColor="{StaticResource PageBackgroundColor}" SeparatorColor="#e57373" ItemTapped="listView_ItemTapped" ItemSelected="listView_ItemSelected">
      <ListView.SeparatorVisibility>
        <OnPlatform x:TypeArguments="SeparatorVisibility" Android="None" iOS="Default"/>
      </ListView.SeparatorVisibility>
      <ListView.RowHeight>
        <OnPlatform x:TypeArguments="x:Int32" Android="68"/>
      </ListView.RowHeight>
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout HorizontalOptions="StartAndExpand" Orientation="Horizontal">
              <StackLayout.Padding>
                <OnPlatform x:TypeArguments="Thickness" Android="12,0,0,0" iOS="6,0,0,0"/>
              </StackLayout.Padding>
              <Image BackgroundColor="#9ccc65" Aspect="AspectFit" WidthRequest="36" HeightRequest="36" VerticalOptions="Center">
                <Image.Source>
                  <OnPlatform x:TypeArguments="ImageSource" Android="ic_audiotrack_white_36dp.png" iOS="audioIcon.png"/>
                </Image.Source>
              </Image>           
              <StackLayout VerticalOptions="Center" Orientation="Vertical">
                <StackLayout.Padding>
                  <OnPlatform x:TypeArguments="Thickness" Android="12,0,0,0" iOS="0"/>
                </StackLayout.Padding>
                <Label Text="{Binding Title}" VerticalTextAlignment="Center" TextColor="White">
                  <Label.FontSize>
                    <OnPlatform x:TypeArguments="x:Double" Android="18" iOS="16"/>
                  </Label.FontSize>
                </Label>
                <StackLayout Padding="0,-5,0,0" HorizontalOptions="StartAndExpand" Orientation="Horizontal">
                  <Label Text="{Binding Artist}" VerticalTextAlignment="Center" TextColor="#ffebee">
                    <Label.FontSize>
                      <OnPlatform x:TypeArguments="x:Double" Android="14" iOS="12"/>
                    </Label.FontSize>
                  </Label>
                  <Label Text="{Binding Duration}" VerticalTextAlignment="Center" TextColor="#ffebee">
                    <Label.FontSize>
                      <OnPlatform x:TypeArguments="x:Double" Android="14" iOS="12"/>
                    </Label.FontSize>
                  </Label>
                </StackLayout>             
              </StackLayout>
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView> 

Niestety można natrafić na kłopoty z ostylowaniem bardziej wysublimowanych elementów w porównaniu do natywnych interfesjów z Android i iOS. W dodatku znów używany tutaj XAML różni od innych gatunków. Ale po kolei.

W Android lista przyjmuje tło strony, ale już w iOS domyślnie jest biała jak oryginał i trzeba jawnie jej ustawić kolor tła. Dużą niegodnością są kolory przyciśniętego i zaznaczonego elementu listy. O ile w Android udało mi się w zrobić tak, by element zaznaczony nie miał innego koloru, a przytrzymany nie miał go prawie w ogóle, o tyle w iOS w pewnym momencie machnąłem już ręką. Tak wiem, że jak mi się niepodoba, to mogę sobie napisać własny natywny renderer dla każdej z platform każdego niemal elementu z Xamarin.Forms, ale czy framework nie mógłby po prostu w ujednoliconym XAML udostępnić nieco więcej właściwości niż teraz? Np. kolor zaznaczonego elementu? Na razie uciekam się do sztuczki, by lista nie miała nigdy zaznaczonego elementu:

private void listView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
        listView.SelectedItem = null;
}

A czemu w DataTemplate rootem czynimy ViewCell?  Tak wiem, kojarzy się to choćby z iOS, ale dla ludzi wywodzących się z innych, bardziej popularnych odmian XAML, jest to twór obcy. A czemu StackLayout a nie StackPanel? Może dlatego że można go rozciągać za pomocą HorizontalOptions (czemu nie HorizontalAlignment?) z wartością StartAndExpand? TextColor miast Foreground? Eh…

 

#3 search

Spora porażka, że SearchBar ze stabilnej wersji nie renderuje się w Android 7!  Znajdziemy taki wątek na forum Xamarina (o widzę pojawiła się odpowiedź: jest obejście w postaci jawnego HeighRequest, może wypróbuję). Nie mając jeszcze odpowiedzi zrobiłem póki co podobnie jak w jednym przykładzie od Xamarina, użyłem zwykłego pola tekstowego. Jest nim kontrolka Entry (zamiast TextBox):

    <Frame Padding="8, 0" OutlineColor="Transparent"  BackgroundColor="Transparent" HasShadow="False" HorizontalOptions="FillAndExpand">
      <Frame.Padding>
        <OnPlatform x:TypeArguments="Thickness" Android="8,0" iOS="6,10,6,4"/>
      </Frame.Padding>
      <Entry Placeholder="{i18n:Translate SearchSongs}" TextChanged="SearchBar_OnTextChanged">
        <Entry.FontSize>
          <OnPlatform x:TypeArguments="x:Double" Android="20" iOS="16"/>
        </Entry.FontSize>
        <Entry.TextColor>
          <OnPlatform x:TypeArguments="Color" Android="White" iOS="#e57373"/>
        </Entry.TextColor>
        <Entry.PlaceholderColor>
          <OnPlatform x:TypeArguments="Color" Android="#ffcdd2" iOS="#f8bbd0"/>
        </Entry.PlaceholderColor>
      </Entry>
    </Frame>

Nie wygląda oczywiście to tak super, jak oryginalny search, ale stosując różne ustawienia trochę popracowałem, by wyglądało to w miarę przyzwoicie.

 

#4 własna kontrolka

Narysować kółko, elipsę cóż trudnego powie prawie każdy użytkownik XAML… No właśnie, prawie robi różnicę. Na pewno nie powie tego ktoś, kto pisze w Xamarin.Forms. Trzeba tutaj opakować natywne implementacje pisane na każdą z platform. W moim przypadku opakowałem kontrolkę CircleView, którą napisałem wcześniej dla Android i iOS tworząc klon app-ki w Xamarin. W projekcie portable napisałem klasę:

    public class CircleView: View
    {
        public CircleView()
        {
        }

        public static readonly BindableProperty CircleColorProperty =
            BindableProperty.Create<CircleView, Color>(
                p => p.CircleColor, Color.Red);
       
        public Color CircleColor
        {
            get { return (Color)GetValue(CircleColorProperty); }
            set { SetValue(CircleColorProperty, value); }
        }
    }

Tutaj twórcy Xamarin.Forms dali kolejny upust chęci odróżnienia się od wszystkich innych gatunków XAML mianując propercję bindowalną jako BindableProperty, a nie DependencyProperty. Może to był wyraz szczerości, że jej funkcjonalność nie jest tak potężna jak we frameworkach stworzonych od początku przez Microsoft. Następnie w każdym projekcie na daną platformę stworzyłem sobie renderer, który łączy klasę “na wszystko” z natywną kontrolką. W Android wygląda to tak:

using Xamarin.Forms;
using LightOrganApp.Droid.Renderers;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(LightOrganApp.Controls.CircleView), typeof(CircleViewRenderer))]
namespace LightOrganApp.Droid.Renderers
{
    public class CircleViewRenderer: ViewRenderer<LightOrganApp.Controls.CircleView, LightOrganApp.Droid.UI.CircleView>
    {
        public CircleViewRenderer()
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<LightOrganApp.Controls.CircleView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || this.Element == null)
                return;

            var circle = new LightOrganApp.Droid.UI.CircleView(Forms.Context)
            {               
                CircleColor = Element.CircleColor.ToAndroid()               
            };

            SetNativeControl(circle);
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control == null || Element == null)
                return;

            if (e.PropertyName == LightOrganApp.Controls.CircleView.CircleColorProperty.PropertyName)
            {
                Control.CircleColor = Element.CircleColor.ToAndroid();
            }
        }
    }
}

a w iOS tak:

using Xamarin.Forms;
using LightOrganApp.iOS.Renderers;
using Xamarin.Forms.Platform.iOS;
using UIKit;

[assembly: ExportRenderer(typeof(LightOrganApp.Controls.CircleView), typeof(CircleViewRenderer))]
namespace LightOrganApp.iOS.Renderers
{
    public class CircleViewRenderer: ViewRenderer<LightOrganApp.Controls.CircleView, LightOrganApp.iOS.CircleView>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<LightOrganApp.Controls.CircleView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null || this.Element == null)
                return;

            var circle = new LightOrganApp.iOS.CircleView
            {
                CircleColor = Element.CircleColor.ToUIColor(),
                BackgroundColor = UIColor.Clear           
            };

            SetNativeControl(circle);
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control == null || Element == null)
                return;

            if (e.PropertyName == LightOrganApp.Controls.CircleView.CircleColorProperty.PropertyName)
            {
                Control.CircleColor = Element.CircleColor.ToUIColor();
            }
        }
    }
}

Po tym wszystkim (nie licząc czasami potrzeby pisania natywnych kontrolek) możemy cieszyć się własną kontrolką w XAML:

<controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>

Aby dopełnić pełni obrazu dodam, że w wersji preview Xamarin.Forms Microsoft wprowadził możliwość używania bezpośrednio w XAML natywnych elementów typowych dla danej platformy bez konieczności pisania klas typu renderer. Znacząco to uprości pisanie interfejsu. Nie testowałem tego jeszcze, ale z pewnością wrócimy do tego tematu.

 

#5 orientacja ekranu i jeszcze renderer

Xamarin.Forms w stosunku do XAML UWP Windows 10, CSS w Web, natywnego Androida czy nawet natywnego iOS brakuje deklaratywnego sposobu definiowania innych wariantów interfejsu użytkownika w zależności od orientacji ekranu, rodzaju urządzenia czy rozmiarów ekranu. Aby obracać światła kolorofonu skorzystałem najpierw z kodu, który zmieniał orientację StackLayout. StackLayout z Xamarin.Forms w przeciwieństwie do StackPanelu z każdego innego XAML ma kilka fajnych właściwości, jak możliwość wypełniania dostępnej przestrzeni (niczym Grid), a także ustawianie odległości między elementami. W dodatku domyślna odległość między elementami nie jest zero jak w StackPanel, tylko wynosi 10!  To duża niespodzianka dla osób wywodzących się z XAML innego niż w Xamarin. Pomysł ze zmianą orientacji działał ładnie na Androidzie, ale na iOS (chyba że tylko na iOS 10?) spotkała mnie przykra niespodzianka. Po obrocie ekranu kółka się … rozciągnęły, tak że urosły i były widoczne zasadniczo dwa z małym ogonkiem, a nie trzy. Czemu nie wiadomo.

Przypomniałem sobie, że na iOS w natywnym ujęciu wyłączałem animacje tranzycji strony przy obrocie ekranu. Ale jak to zrobić w cross-platformowym Xamarin.Forms? Jest na to sposób. Możemy napisać własny renderer strony. Tak zrobiłem teraz w projekcie tylko dla iOS:

using CoreAnimation;
using CoreGraphics;
using UIKit;
using Xamarin.Forms.Platform.iOS;
using LightOrganApp.iOS.Renderers;
using Xamarin.Forms;
using LightOrganApp;

[assembly: ExportRenderer(typeof(MainPage), typeof(MainPageRenderer))]
namespace LightOrganApp.iOS.Renderers
{
    public class MainPageRenderer: PageRenderer
    {
        public override void ViewWillTransitionToSize(CGSize toSize, IUIViewControllerTransitionCoordinator coordinator)
        {
            base.ViewWillTransitionToSize(toSize, coordinator);
            CATransaction.Begin();
            CATransaction.DisableActions = true;

            coordinator.AnimateAlongsideTransition(ctx => { }, ctx => CATransaction.Commit());
        }
    }
}

Ale to nie rozwiązało sprawy. Jak kółka na iOS po obrocie ekranu się rozwalały, tak rozwalały się dalej. Intuicja programisty nie piszącego od wczoraj podpowiedziała mi rozwiązanie. Może by przy obrocie ekranu ładować DataTemplate do ContentControl?  Ale hola. W Xamarin.Forms nie ma… ContentControl. Grr.. Natrafiłem jednak na świetny post ContentPresenter for Xamarin.Forms, który wskazał mi zaktualizowane repozytorium https://github.com/XLabs/Xamarin-Forms-Labs. Z niego wziąłem implementację ContentControl:

   public class ContentControl : ContentView
   {               
       public static readonly BindableProperty ContentTemplateProperty = BindableProperty.Create<ContentControl, DataTemplate>(x => x.ContentTemplate, null, propertyChanged:  OnContentTemplateChanged);

       private static void OnContentTemplateChanged(BindableObject bindable, object oldvalue, object newvalue)
       {
           var cp = (ContentControl)bindable;

           var template = cp.ContentTemplate;
           if (template != null)
           {
               var content = (View)template.CreateContent();
               cp.Content = content;
           }
           else
           {
               cp.Content = null;
           }
       }
      
       public DataTemplate ContentTemplate
       {
           get
           {
               return (DataTemplate)GetValue(ContentTemplateProperty);
           }
           set
           {
               SetValue(ContentTemplateProperty, value);
           }
       }
   }

Czy to jest trudny kod? Nie mógł być częścią Xamarin.Forms? Następnie zdefiniowałem sobie w zasobach MainPage szablony:

    <DataTemplate x:Key="VerticalLights">       
        <StackLayout Spacing="20">            
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding MidColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding TrebleColor}"/>             
        </StackLayout>       
      </DataTemplate>
      <DataTemplate x:Key="HorizontalLights">       
        <StackLayout Spacing="20" Orientation="Horizontal">            
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding BassColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding MidColor}"/>
          <controls:CircleView VerticalOptions="FillAndExpand"
            HorizontalOptions="FillAndExpand" CircleColor="{Binding TrebleColor}"/>              
        </StackLayout>       
      </DataTemplate>

Przynajmniej DataTemplate wygląda dość standardowo. A teraz kod przełączający szablon po obrocie ekranu:

        private double width = 0;
        private double height = 0;

        …

        protected override void OnSizeAllocated(double width, double height)
        {
            base.OnSizeAllocated(width, height);
            if (width != this.width || height != this.height)
            {
                this.width = width;
                this.height = height;

                if (width > height)
                {
                    lights.ContentTemplate = Resources["HorizontalLights"] as DataTemplate;
                }
                else
                {
                    lights.ContentTemplate = Resources["VerticalLights"] as DataTemplate;                                
                }
            }
        }

      

#6 binding

Jeszcze słowo o data bindingu. Xaml w Xamarin nie byłby sobą, gdyby było jak w innych postaciach Xaml. Zamiast właściwości DataContext mamy BindingContext. Dobrze, że INotifyPropertyChanged jest i działa tak, jak należałoby się tego spodziewać. W moim przypadku steruję kolorami świateł właśnie przez binding. Szczegóły w MainPage.xaml.cs.

 

Morał jest taki, że Xaml Xaml-owi nierówny. Andy Wigley mówił kiedyś, że może ustandaryzują XAML. Przydałoby się! Muszę przyznać, że w obecnej sytuacji znajomość innych odmian XAML czasami zamiast mi pomagać, to mi wręcz przeszkadzała. Czy tak powinno być? Po drugie mimo pisania w XAML czasami potrzebujemy dość często zrobić coś za pomocą natywnego podejścia, czy to przez brak dostępu do ostylowania, brak zachowania znanego z natywnego komponentu czy własna kontrolkę. Upada wtedy nieco atut, że nie trzeba znać natywnych platform, mamy za to największą elastyczność. Dodając fakt, że aplikacja Xamarin.Forms działa (przynajmniej bez wysilonych zabiegów) gołym okiem wolniej od aplikacji z natywnym w pełni interfejsem (w oryginalnych narzędziach i SDK czy też w Xamarin.Android i Xamarin.iOS), ten sposób tworzenia aplikacji mobilnych mniej mnie przekonuje niż pozostałe, z którymi miałem doczynienia. Dodatkowa warstwa abstrakcji powoduje narzut przez potrzebę jej parsowania. Visual Studio 2015 nie jest też dopracowane dla XAML w Xamarin.Forms. Od strony dewelopera może się zdarzyć, że coś się skompiluje, uruchomi, a potem dowiemy się, że obiekt nie ma propercji Margin, Padding czy Background (albo w ogóle się nie dowiemy jak u mnie na iOS, gdzie aplikacja od razu się zamykała). A, zapomniałem dodać, że Margin w ogóle prawie nie istnieje w Xamarin.Forms. Trzeba to obchodzić stosując wszędzie Padding, o ile to możliwe. W rozwojowej wersji Xamarin.Forms mamy podgląd w designerze dla różnych platform. Ja póki co pracuję na stabilnej w czystym edytorze XML. Microsoft ma jeszcze dużo do zrobienia, by korzystanie z XAML w Xamarin.Forms dorównało korzystaniu z XAML w UWP czy WPF. Działają jednak cały czas, oprócz natywnych kontrolek bezpośrednio w XAML pojawiło się też preview kompilowanego bindingu.

Mimo pewnych problemów z Xamarin.Forms, nadal będę się przyglądał tej technologii,  by wyrobić sobie o niej jeszcze pełniejszy obraz. Obecnie kolekcjonerskie “różowe złoto” ma UI w XAML pozwalające na robienie wszystkiego poza konfiguracją, teraz pora na próbę sprzężenia go z natywnym API, by listować muzykę, odtwarzać ją i by światła mrugały. Ale to już w następnym odcinku. Stay tuned!

środa, 7 września 2016

Xamarin.Forms kontra Xamarin odc.1 (toolbar, nawigacja, stylowanie, lokalizacja, ikony aplikacji)

Dobry wieczór, w nowym cyklu tematycznym Xamarin.Forms kontra Xamarin witam serdecznie. Z XAML w Xamarin musiałem się po prostu zmierzyć, to była tylko kwestia czasu.

Z XAML jestem od początku tzn. początku 2005, kiedy były wersje CTP ówczesnego .NET 3.0. Po zachwycie nad WPF miałem wręcz najlepszy dev-okres w czasach Silverlight. Byłem też wielkim entuzjastą Silverlight w Windows Phone 7.x. Z chwilą pojawienia się pierwszego wydania Windows 8 Developer Preview uważnie zapoznałem się z XAML WinRT, śledziłem też rozwój wypadków w Windows Phone 8.x. Na ulepszonym nieco XAML WinRT opierają się z kolei aplikacje uniwersalne XAML w Windows 10.

Z XAML jestem naprawdę długo. Nie chcę popadać w jakąś megalomanię, ale zaczynam chyba skalą czasową doganiać takie osobowości jak Andy Wigley, Charles Petzold, Jessy Liberty czy Pete Brown. Był jeszcze John Papa, ale on na 100% przechcił się na HTML 5, a ja tylko częściowo. Co te osoby dziś wszystkie robią (poza JP)?  Zajmują się platformą Xamarin, a zwłaszcza Xamarin.Forms, który obsługuje XAML. Nie wypadało wręcz tego odpuścić.

Aby nie było zbyt prosto dodam, że mamy doczynienia z kolejna odmiana XAML, z zauważalnymi różnicami w stosunku do wszystkich technologii jakie tutaj dziś wymieniłem. Jakie są to różnice?  A jak ma się to do Xamarin.Android i Xamarin.iOS czy natywnych rozwiązań w oryginale?  To przykładowe pytania, na które będziemy udzielać odpowiedzi. Zrobimy to tworząc jeszcze bardziej kolekcjonerską wersję app-ki z czasów DSP, tym razem o nazwie Light Organ XF / Kolorofon XF (XF od Xamarin Forms!). Nie wiem czemu, ale od początku miałem wizję, że jej kolor będzie w kolorze “różowego złota” . Nawet nadałem jej kryptonim “różowe złoto”. Nie jestem oryginalny, jak wiadomo to jeden z kolorów iPhone (btw dziś zgodnie ze spiskowymi teoriami odbyła się premiera iPhone 7,  jak skończę tego posta wskakuję komentować na twitterze).

Po dość długim wstępie, mogę w końcu powiedzieć zaczynamy!

image

Na razie jesteśmy w fazie początkowej, na github dwa niemal puste ekrany,  póki co na Android i iOS.  Oto jeden z nich:

Screenshot_20160907-205510  IMG_0057

Jak to wygląda w XAML?

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:i18n="clr-namespace:LightOrganApp"
             xmlns:local="clr-namespace:LightOrganApp"
             x:Class="LightOrganApp.MainPage" Title="{i18n:Translate AppName}"
             BackgroundColor="{StaticResource PageBackgroundColor}">
  <ContentPage.ToolbarItems>
    <ToolbarItem Text="{i18n:Translate ActionMediaFiles}" Clicked="OnMediaFilesClicked">     
      <ToolbarItem.Icon>
        <OnPlatform x:TypeArguments="FileImageSource" Android="ic_library_music_white_48dp.png" iOS="Search.png"/>
      </ToolbarItem.Icon>     
    </ToolbarItem>   
  </ContentPage.ToolbarItems> 

</ContentPage>

Zasoby przydatne dla wielu stron zdefiniowałem w App.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LightOrganApp.App">
  <Application.Resources>
    <ResourceDictionary>
      <Color x:Key="PageBackgroundColor">#ff8a80</Color>
      <Style TargetType="NavigationPage">
        <Setter Property="BackgroundColor" Value="{StaticResource PageBackgroundColor}"/>
        <Setter Property="BarBackgroundColor" Value="#e57373"/>
        <Setter Property="BarTextColor" Value="White"/>
      </Style>     
    </ResourceDictionary>   
  </Application.Resources>
</Application>

A code-behind strony?

    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            if (Device.OS == TargetPlatform.Android)
            {
                var toolbarItem = new ToolbarItem(AppResources.ActionSettings, null, () => { }, ToolbarItemOrder.Secondary, 0);
                ToolbarItems.Add(toolbarItem);
            }
        }

        async void OnMediaFilesClicked(object sender, EventArgs e)
        {
            var fileListPage = new FileListPage();
                
            await Navigation.PushAsync(fileListPage);
        }
    }

Strona FileListPage jest póki co pusta, tylko z pustym toolbarem. A teraz uwagi. Będzie ich sporo, pewnie nie dam rady podzielić się wszystkim, co mam w głowie, ale traktuję tego posta tylko jako swoisty skrótowy ekspres z ciekawostkami.

#1  XAML

Mamy XAML z roku 2009. Brawo. XAML-e tworzone od początku przez Microsoft nadal bazują na specyfikacji z 2006 (za wyjątkiem WF). Czemu muszę pisać jawnie ResourceDictionary w Resources?  Czemu mam pisać BackgroundColor zamiast Background?  Andy Wigley powiedział pół słowem w Warszawie, że może Microsoft ustandaryzuje XAML w Xamarin. Przydałoby się, bo pewną klątwą nad tym językiem jest, że każdy framework ma swoją własną jego odmianę.

#2  Status bar

Nie pokolorujemy go w XAML, ani nawet w cross-platformowym projekcie!  W Android w projekcie na tę platformę pilnujemy ustawiania:

<item name="colorPrimaryDark">#e57373</item>

w Resources/values/styles.xml. W iOS zdaje się dopilnowałem, by w Info.plist było:

<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>

#3 Toolbar

Zrobiłem odpowiedni styl do NavigationPage. Musi być do NavigationPage, jak zrobimy do ContentPage nie zadziała!  Widzę kilka mankamentów. Nie uciekając się do natywnych renderingów jestem obowiązywany podawać bitmapowe ikony, a natywnie np. w iOS mogłem podać słowo “search” i to wystarczało. No, ale mam unifikację w XAML. Inna rzecz, to nie udało mi się póki co ostylować koloru popupu dla akcji secondary w przypadku Android. Nawet natywny sposób jaki stosowałem w Xamarin.Android nie chce działać z Xamarin.Forms!!!  O tym, że w XAML nie ma gdzie tego zrobić nawet nie mówię. Jeśli chodzi o Android to kolejne pytanie, co się stało z cieniem toolbara. Xamarin niby wspiera Material Design poprzez FormsAppCompatActivity, ale czemu domyślnie tego cienia nie widać?  Nie mam już siły, uderzę z pytaniami na forum Xamarina.

#4  Nawigacja

Widać fajne nowoczesne podejście z async.  Odpalają się domyślnie jakieś tranzycje między stronami, inne jednak niż w domyślne w moich natywnych rozwiązaniach.

#5  Lokalizacja

Generalnie wykonałem to co na stronie Localizing Xamarin.Forms Apps with RESX Resource Files. Jak w przypadku całego Xamarin.Forms widać, że pewną małą część pozostawia się rozwiązaniom natywnym.

#6  Ikony aplikacji

Podmieniłem generalnie te defaultowe na swoje z wcześniejszych solucji. Bez zaskoczenia, pamiętać tylko że Xamarin.Forms wspiera szerszy zakres wersji systemów niż robiłem to tworząc rozwiązania natywne.

To tyle na dziś. Stay tuned!

wtorek, 30 sierpnia 2016

iOS 10 beta - migracja w Xamarin

iOS 10 beta, ciąg dalszy, witam serdecznie. Dziś parę słów o Xamarin.

Wszystkie potrzebne informacje znajdziemy na stronie Introduction to iOS 10.

W przeciwieństwie do Swift w C# nie trzeba było nic zmieniać! Dodałem jedynie obsługę nowego modelu uprawnień. Podobnie jak ostatnim razem w pliku Info.plist utworzyłem klucz NSAppleMusicUsageDescription. W przeciwieństwie do Xcode 8 musiałem zrobić to ręcznie edytując XML, gdzie wstawiłem:

<key>NSAppleMusicUsageDescription</key>
<string>This app uses your media library in order to track changes</string>

W klasie FileListViewController stworzyłem następujący kod:

        public async override void ViewDidLoad()
        {
            …

            if (UIDevice.CurrentDevice.CheckSystemVersion(9, 3))
            {
                var status = await MPMediaLibrary.RequestAuthorizationAsync();

                if (status == MPMediaLibraryAuthorizationStatus.Authorized)
                    LoadMediaItemsForMediaTypeAsync(MPMediaType.Music);
                else
                    DisplayMediaLibraryError();               
            }
            else
            {
                LoadMediaItemsForMediaTypeAsync(MPMediaType.Music);
            }     
        }

        private void DisplayMediaLibraryError()
        {
            string error;

            switch (MPMediaLibrary.AuthorizationStatus)
            {
                case MPMediaLibraryAuthorizationStatus.Restricted:
                    error = "Media library access restricted by corporate or parental settings";
                    break;
                case MPMediaLibraryAuthorizationStatus.Denied:
                    error = "Media library access denied by user";
                    break;
                default:
                    error = "Unknown error";
                    break;
            }

            var controller = UIAlertController.Create("Error", error, UIAlertControllerStyle.Alert);
            controller.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));

            PresentViewController(controller, true, null);              
        }

W porównaniu nawet do Swift 3  C# ma większą elegancję jeśli chodzi o async. Widzimy już metody z API w tej postaci, jak choćby RequestAuthorizationAsync. Przy enumach wygrywa Swift. W C# musiałem za każdym razem pisać MPMediaLibraryAuthorizationStatus.*.  Podczas tłumaczenia kodu wyszło, że klasy UIAlertController i UIAlertAction dla oczekiwanego zestawu parametrów zamiast typowego konstruktora mają w C# statyczne metody Create.

Wszystko działa jak w oryginale w Swift 3 i Xcode 8 beta. Na designerze w Visual Studio zaobserwowałem jedynie rysowanie linii łączych w innym miejscu niż łączonych przez nie kontrolerów, ale w niczym to nie przeszkodziło.

niedziela, 28 sierpnia 2016

iOS 10 beta - migracja do Swift 3 i Xcode 8

Witam serdecznie. Zachęcony premierą nowego Androida postanowiłem przyjrzeć się bliżej, co szykuje Apple w iOS 10 od strony użytkownika oraz języka, platformy i narzędzi.  Efektem tego jest zmigrowany projekt Xcode z czasów DSP do Swift 3 udostępniony na github w gałęzi iOS10.  Zaczynamy!

Zróbmy na początek małe podsumowanie odnośnie samego systemu operacyjnego iOS 10. Na obecny moment dostępna jest publiczna beta 7, a deweloperska beta 8. Póki co jadę na publicznych edycjach. Po zarejestrowaniu urządzenia w programie beta bez najmniejszych problemów udało mi się całkiem szybko cieszyć się betą 6, a dzień później betą 7.  Kilka linków o tym co nowego, jak zainstalować, jak powrócić do wersji produkcyjnej itp:

Nowy system jakoś specjalnie mnie nie powalił, ale zachowuje się jak na razie bezbłędnie.  Wizualne zmiany dostrzeżemy w widgetach i niektórych systemowych aplikacjach. Wiele z nich może być dyskusyjna. Najbardziej podoba mi się app-ka Clock z ciemną themą. Od strony API system zawiera nowe funkcjonalności, które są nieraz odpowiedzią na możliwości innych platform (np. integracja z Siri, interaktywne notyfikacje).

Instalacja Xcode 8 beta obok dotychczasowej stabilnej wersji 7.3 też przebiegła sprawnie i bezproblemowo. Następnie przystąpiłem do migracji kopii projektu na Swift 3.

 

Swift 3

Nieprawdopodobna jest ilość kodu, który uległ zmianie po (automatycznej) migracji. To potwierdza świeżość nowego języka Apple, w którym wciąż dokonywane są znaczące zmiany składni, nazw typów i metod z API. Oto wybrane przykłady:

 

AppDelegate

Swift 2.2

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { … }

Swift 3

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { … }

Swift 2.2

func populateRegistrationDomain() {
    let settingsBundleURL = NSBundle.mainBundle().URLForResource("Settings", withExtension: "bundle")
   
    let appDefaults = loadDefaultsFromSettingsPage("Root.plist", inSettingsBundleAtURL: settingsBundleURL!)
   
    let defaults = NSUserDefaults.standardUserDefaults()
    defaults.registerDefaults(appDefaults!)
    defaults.synchronize()
}

Swift 3

func populateRegistrationDomain() {
    let settingsBundleURL = Bundle.main.url(forResource: "Settings", withExtension: "bundle")
   
    let appDefaults = loadDefaultsFromSettingsPage("Root.plist", inSettingsBundleAtURL: settingsBundleURL!)
   
    let defaults = UserDefaults.standard
    defaults.register(defaults: appDefaults!)
    defaults.synchronize()
}

Swift 2.2

func += <K, V> (inout left: [K:V], right: [K:V]) { … }

Swift 3

func += <K, V> (left: inout [K:V], right: [K:V]) { … }

 

CircleView

Swift 2.2

@IBInspectable var circleColor: UIColor = UIColor.redColor()

Swift 3

@IBInspectable var circleColor: UIColor = UIColor.red

Swift 2.2

override func drawRect(rect: CGRect) {
    drawCircle(circleColor)
}   

Swift 3

override func draw(_ rect: CGRect) {
    drawCircle(circleColor)
}   

Swift 2.2

func drawCircle(color: UIColor) {
   
    let context = UIGraphicsGetCurrentContext()
   
    let a = min(bounds.size.width, bounds.size.height)
    let leftX = CGRectGetMidX(self.bounds) - a / 2
    let topY = CGRectGetMidY(self.bounds) - a / 2
    let rectangle = CGRectMake(leftX, topY, a, a)
   
    CGContextSetFillColorWithColor(context, circleColor.CGColor)
    CGContextFillEllipseInRect(context, rectangle)
}

Swift 3

func drawCircle(_ color: UIColor) {
   
    let context = UIGraphicsGetCurrentContext()
   
    let a = min(bounds.size.width, bounds.size.height)
    let leftX = self.bounds.midX - a / 2
    let topY = self.bounds.midY - a / 2
    let rectangle = CGRect(x: leftX, y: topY, width: a, height: a)
   
    context?.setFillColor(circleColor.cgColor)
    context?.fillEllipse(in: rectangle)
}

 

CustomSearchBar

Swift 2.2

if searchBarView.subviews[i].isKindOfClass(UITextField) {

Swift 3

if searchBarView.subviews[i].isKind(of: UITextField.self) {

Swift 2.2

glassIconView.image = glassIconView.image?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)

Swift 3

glassIconView.image = glassIconView.image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)

Swift 2.2

let textFieldInsideSearchBarLabel = searchField.valueForKey("placeholderLabel") as? UILabel

Swift 3

let textFieldInsideSearchBarLabel = searchField.value(forKey: "placeholderLabel") as? UILabel

Swift 2.2

clearButton.setImage(clearButton.imageView?.image?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate), forState: .Normal)
clearButton.setImage(clearButton.imageView?.image?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate), forState: .Highlighted)

Swift 3

clearButton.setImage(clearButton.imageView?.image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate), for: UIControlState())
clearButton.setImage(clearButton.imageView?.image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate), for: .highlighted)

 

CustomSearchController

Swift 2.2

let result = CustomSearchBar(frame: CGRectZero)

Swift 3

let result = CustomSearchBar(frame: CGRect.zero)

 

FileListViewController

Swift 2.2

searchController.active = restoredState.wasActive

Swift 3

searchController.isActive = restoredState.wasActive

Swift 2.2

func loadMediaItemsForMediaType(mediaType: MPMediaType){
   
    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
   
    dispatch_async(queue) {
        let query = MPMediaQuery()
        let mediaTypeNumber =  Int(mediaType.rawValue)
        let predicate = MPMediaPropertyPredicate(value: mediaTypeNumber,
                                                 forProperty: MPMediaItemPropertyMediaType)
        query.addFilterPredicate(predicate)
       
        self.allMediaItems = query.items
       
        dispatch_async(dispatch_get_main_queue()) {
            self.tableView.reloadData()
        }
    }
}

Swift 3

func loadMediaItemsForMediaType(_ mediaType: MPMediaType) {
   
    let queue = DispatchQueue.global(qos: .default)
   
    queue.async {
        let query = MPMediaQuery()
        let mediaTypeNumber =  Int(mediaType.rawValue)
        let predicate = MPMediaPropertyPredicate(value: mediaTypeNumber,
                                                 forProperty: MPMediaItemPropertyMediaType)
        query.addFilterPredicate(predicate)
       
        self.allMediaItems = query.items
       
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
}

Swift 2.2

private func getMediaItems() -> [MPMediaItem]? {

Swift 3

fileprivate func getMediaItems() -> [MPMediaItem]? {

Swift 2.2

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {

Swift 3

override func numberOfSections(in tableView: UITableView) -> Int {

Swift 2.2

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    cell.textLabel?.textColor = .whiteColor()
    cell.detailTextLabel?.textColor = .lightGrayColor()
}

Swift 3

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cell.textLabel?.textColor = .white
    cell.detailTextLabel?.textColor = .lightGray
}

Swift 2.2

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
    let row = indexPath.row
    let mediaItems = getMediaItems()
   
   
    let item = mediaItems![row] as MPMediaItem
    cell.textLabel?.text = item.valueForProperty(MPMediaItemPropertyTitle) as! String?
   
    var artist = NSLocalizedString("unknownArtist", comment: "Unknown Artist")
    if let artistVal = item.valueForProperty(MPMediaItemPropertyArtist) as? String {
        artist = artistVal
    }
   
    let length = item.valueForProperty(MPMediaItemPropertyPlaybackDuration) as! Int

    …

}

Swift 3

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
    let row = (indexPath as NSIndexPath).row
    let mediaItems = getMediaItems()
   
   
    let item = mediaItems![row] as MPMediaItem
    cell.textLabel?.text = item.value(forProperty: MPMediaItemPropertyTitle) as! String?
   
    var artist = NSLocalizedString("unknownArtist", comment: "Unknown Artist")
    if let artistVal = item.value(forProperty: MPMediaItemPropertyArtist) as? String {
        artist = artistVal
    }
   
    let length = item.value(forProperty: MPMediaItemPropertyPlaybackDuration) as! Int

    …

}

Swift 2.2

if let index = selectedMediaItems!.indexOf(item) {
    selectedMediaItems!.removeAtIndex(index)
}

Swift 3

if let index = selectedMediaItems!.index(of: item) {
    selectedMediaItems!.remove(at: index)
}

Swift 2.2

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

Swift 3

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

Swift 2.2

b1 = title.lowercaseString.containsString(searchText.lowercaseString)

Swift 3

b1 = title.lowercased().contains(searchText.lowercased())

Swift 2.2

func updateSearchResultsForSearchController(searchController: UISearchController) {

Swift 3

func updateSearchResults(for searchController: UISearchController) {

Swift 2.2

override func encodeRestorableStateWithCoder(coder: NSCoder) {
    super.encodeRestorableStateWithCoder(coder)
   
   
    coder.encodeBool(searchController.active, forKey:RestorationKeys.searchControllerIsActive.rawValue)
   
    coder.encodeBool(searchController.searchBar.isFirstResponder(), forKey:RestorationKeys.searchBarIsFirstResponder.rawValue)
   
    coder.encodeObject(searchController.searchBar.text, forKey:RestorationKeys.searchBarText.rawValue)
}

Swift 3

override func encodeRestorableState(with coder: NSCoder) {
    super.encodeRestorableState(with: coder)
   
   
    coder.encode(searchController.isActive, forKey:RestorationKeys.searchControllerIsActive.rawValue)
   
    coder.encode(searchController.searchBar.isFirstResponder, forKey:RestorationKeys.searchBarIsFirstResponder.rawValue)
   
    coder.encode(searchController.searchBar.text, forKey:RestorationKeys.searchBarText.rawValue)
}

 

ViewController

Swift 2.2

NSStreamDelegate

Swift 3

StreamDelegate

Swift 2.2

NSOutputStream

Swift 3

OutputStream

Swift 2.2

let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: self.player)
notificationCenter.addObserver(self, selector: #selector(ViewController.playbackStateChanged(_:)), name: MPMusicPlayerControllerPlaybackStateDidChangeNotification, object: self.player)

Swift 3

let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: NSNotification.Name.MPMusicPlayerControllerNowPlayingItemDidChange, object: self.player)
notificationCenter.addObserver(self, selector: #selector(ViewController.playbackStateChanged(_:)), name: NSNotification.Name.MPMusicPlayerControllerPlaybackStateDidChange, object: self.player)

Swift 2.2

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    …    
    coordinator.animateAlongsideTransition({ … })
   
}

Swift 3:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    …    
    coordinator.animate(alongsideTransition: { … })
   
}

Swift 2.2

if let sourceViewController = sender.sourceViewController as? FileListViewController,
    mediaItemCollection = sourceViewController.didPickMediaItems {

Swift 3

if let sourceViewController = sender.source as? FileListViewController,
    let mediaItemCollection = sourceViewController.didPickMediaItems {

Swift 2.2

self.player.setQueueWithItemCollection(self.collection)

Swift 3

self.player.setQueue(with: self.collection)

Swift 2.2

func nowPlayingItemChanged(notification: NSNotification) {

Swift 3

func nowPlayingItemChanged(_ notification: Notification) {

Swift 2.2

self.toolbar.hidden = playbackState != .Playing && playbackState != .Paused

Swift 3

self.toolbar.isHidden = playbackState != .playing && playbackState != .paused

Swift 2.2

func createNewSocket(defaults: NSUserDefaults) {
    let host = defaults.stringForKey("remote_device_host_preference")
    let port = defaults.integerForKey("remote_device_port_preference")
   
    if host != nil && port > 0 {
        NSStream.getStreamsToHostWithName(host!, port: port, inputStream: nil, outputStream: &outStream)
       
        outStream?.delegate = self
        outStream?.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
        outStream?.open()
    }
}

Swift 3

func createNewSocket(_ defaults: UserDefaults) {
    let host = defaults.string(forKey: "remote_device_host_preference")
    let port = defaults.integer(forKey: "remote_device_port_preference")
   
    if host != nil && port > 0 {
        Stream.getStreamsToHost(withName: host!, port: port, inputStream: nil, outputStream: &outStream)
       
        outStream?.delegate = self
        outStream?.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
        outStream?.open()
    }
}

Swift 2.2

outStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)

Swift 3

outStream?.remove(from: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

Swift 2.2

func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
    switch eventCode {
    case NSStreamEvent.EndEncountered:
        print("EndEncountered")
        releaseOutStream()
    case NSStreamEvent.ErrorOccurred:
        print("ErrorOccurred")
        releaseOutStream()
    case NSStreamEvent.HasSpaceAvailable:
        print("HasSpaceAvailable")
    case NSStreamEvent.None:
        print("None")
    case NSStreamEvent.OpenCompleted:
        print("OpenCompleted")
    default:
        print("Unknown")
    }
}

Swift 3

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case Stream.Event.endEncountered:
        print("EndEncountered")
        releaseOutStream()
    case Stream.Event.errorOccurred:
        print("ErrorOccurred")
        releaseOutStream()
    case Stream.Event.hasSpaceAvailable:
        print("HasSpaceAvailable")
    case Stream.Event():
        print("None")
    case Stream.Event.openCompleted:
        print("OpenCompleted")
    default:
        print("Unknown")
    }
}


Na podstawie tych przykładów widzimy, że generalnie składnia Swift 3 staje się z reguły bardziej naturalna, nazwy metod ulegają skróceniu, globalne elementy trafiają do klas (co bardziej przypomina ich postać w C#). Zwróćmy uwagę na zmiany w korzystaniu z kolejek systemowych. Tutaj dodatkowo dotychczasowa metoda stała się deprecated i należy korzystać z nowej. Uwagę zwraca też nieco inny sposób definiowania zmiennych w warunku if.

To nie koniec niespodzianek. Aplikacja mimo skompilowania podczas wykonywania rzucała błąd o braku deklaracji uprawnienia w Info.plist. Powodem był nowy system uprawnień, który pojawił się od iOS 9.3.

 

Nowy model uprawnień

Na początek przydatny artykuł z przykładem:  Media Library privacy flaw fixed in iOS 10.  Posiłkując się nim zadeklarowałem w Info.plist pozycję o kluczu “Privacy - Media Library Usage Description” z wartością, którą system wstawi do komunikatu, jakim użytkownik zostanie poproszony o zgodę na uprawnienie. Mógłbym w sumie poprzestać na tym, ale lepiej jest zapewnić wymagane uprawnienie w miejscu korzystania z Media Library. Zaowocowało to kodem w FileListViewController:

override func viewDidLoad() {
    …    
    if #available(iOS 9.3, *) {
        MPMediaLibrary.requestAuthorization { (status) in
            if status == .authorized {
                self.loadMediaItemsForMediaType(.music)
            } else {
                self.displayMediaLibraryError()
            }
        }
    }
}

@available(iOS 9.3, *)
func displayMediaLibraryError() {
    var error: String
    switch MPMediaLibrary.authorizationStatus() {
    case .restricted:
        error = "Media library access restricted by corporate or parental settings"
    case .denied:
        error = "Media library access denied by user"
    default:
        error = "Unknown error"
    }
   
    let controller = UIAlertController(title: "Error", message: error, preferredStyle: .alert)
    controller.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    present(controller, animated: true, completion: nil)
}

Nowy model uprawnień ma też odzwierciedlenie w interfejsie. Użytkownik - podobnie jak w Android 6 -  może zarządzać uprawnieniami aplikacji w jej ustawieniach:

IMG_0053

Na tym dziś zakończymy, ale nie jest to koniec, jeśli chodzi o iOS 10. Stay tuned!

wtorek, 23 sierpnia 2016

Android 7.0 - migracja projektów w Android Studio i Visual Studio

Dokonajmy krótkiego podsumowania dotychczasowych faktów odnośnie Android 7.0 z punktu widzenia dewelopera.

Screenshot_20160823-005513

Po pierwsze wczoraj została wypuszczona finalna wersja. Nie każdy ją jednak od razu otrzymał. Aby pomóc nieco losowi można było zapisać swoje urządzenie do programu beta (wczoraj przyniosło mi to natychmiastowe pojawienie się pożądanego updatu).

Co nowego dla dev-a?  Najlepiej przeczytać stronkę Android 7.0 for Developers.

Android Studio

Najpierw wczytałem swój projekt z DSP do najnowszego stabilnego Android Studio 2.1.3.  Nie było tak źle, skorzystałem z automatycznego upgrade’u gradle. Zainstalowałem API 24, odpowiednie build tools itp. w SDK managerze:

image

Wcześniej już miałem Javę 8 na maszynie. W zasadzie tylko zmieniłem wersje do kompilacji w pliku build.gradle:

apply plugin: 'com.android.application'

android {
compileSdkVersion 24
buildToolsVersion "24.0.1"

defaultConfig {
applicationId "com.apps.kruszyn.lightorganapp"
minSdkVersion 19
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.+'
compile 'com.android.support:design:24.+'
compile 'com.android.support:cardview-v7:24.+'
}

W manifeście też dałem targetSdkVersion na 24, choć z podpowiedzi narzędzia wynika nawet że i tak gradle to nadpisuje podczas kompilacji na podstawie swoich ustawień.

Xamarin & Visual Studio

Po pierwsze postąpiłem według Introduction to the Android N Developer Preview. Instaluje się specjalną wersję Xamarina dla VS (nie instalowałem nawet Xamarin Studio czy gtk sharp). W VS w konfiguracji Xamarin ustawiłem ścieżkę do JDK 1.8. W projekcie pozmieniałem ustawienia co do wersji API:

image

Nie ustawiałem wersji minimalnej na 24, a i tak zdaje się wszystko działa. Pakiety nuget jednak automatycznie się nie zaktualizowały, więc je usunąłem i dodałem na nowo. O ile ich wersje nadal pozostają takie same, o tyle teraz są dedykowane dla frameworka 7.0 (przy API 23 dotychczas był to 6.0). 
Deplyment. Tu miałem problem… Dostawałem problem z niemożliwością usunięcia app-ki, mimo że ją wcześniej odinstalowałem na telefonie. Taka przypadłość – jak wyczytałem m.in na stackoverflow – przydarza się czasami w Xamarin. Usuwamy wtedy niby usuniętą już app-kę poprzez linię komend adb:
adb uninstall com.apps.kruszyn.lightorganapp.droid

Czyści to scachowane biblioteki Xamarina. Pomogło. Wgrałem, działa podobnie jak oryginał w Android Studio.

Wsparcia Xamarina wygląda chyba jednak nadal jako preview, designer przestał mi póki co działać:

image

W Android Studio designer działa bez problemu. Niewykluczone, że u mnie w Xamarinie coś przestało działać na skutek instalowania różnych jego wydań (stabilne, beta, Android N Preview, …). Póki co w zasadzie cisza o Android 7 na jego forum, a pewnie w jakiejś stabilnej wersji pojawi się oficjalne wsparcie dla 7.0.

To tyle póki co w temacie siódemki. Aha, jak mamy app-kę targetowaną na API starsze niż 24 (np. 23) to przy dokowaniu app-ki na części ekranu pojawia się info, że aplikacja ta może nie wspierać takiego trybu pracy. Po ztargetowaniu na 24 taka notyfikacja znika. Przedstawione tutaj zmiany trafiły tradycyjnie już jako wbitki do mojego githuba.

niedziela, 21 sierpnia 2016

Xamarin.iOS kontra Swift odc.4 (ustawienia aplikacji, sockety)

Xamarin kontra Swift, witam serdecznie. Dziś upamiętnimy odcinki o iOS z czasów DSP nr. 7 i 8. To sprawadza w zasadzie aplikację w C# do poziomu funkcjonalności w Swift, jaką osiągnąłem na koniec maja (i DSP).

IMG_0049  

Zacznijmy od ustawień. Taki efekt jak na powyższym screenie osiągniemy jak wgramy aplikację skompilowaną jako Release, w Debug Xamarin dodatkowo dodaje swoją sekcję w ustawieniach. W Visual Studio nie ma takiego kreatora do ustawień jak w Xcode, pliki trzeba tworzyć ręcznie w folderze Settings.bundle (ja je skopiowałem z projektu Xcode):

image

W AppDelegate stworzyłem odpowiedniki metod do wczytywania domyślnych wartości:

        private void PopulateRegistrationDomain()
        {
            var appDefaults = LoadDefaultsFromSettingsPage();

            var defaults = NSUserDefaults.StandardUserDefaults;
            defaults.RegisterDefaults(appDefaults);
            defaults.Synchronize();
        }

        private NSDictionary LoadDefaultsFromSettingsPage()
        {
            var settingsDict = new NSDictionary(NSBundle.MainBundle.PathForResource("Settings.bundle/Root.plist", null));

            var prefSpecifierArray = settingsDict["PreferenceSpecifiers"] as NSArray;

            if (prefSpecifierArray == null)
                return null;

            var keyValuePairs = new NSMutableDictionary();

            foreach (var prefItem in NSArray.FromArray<NSDictionary>(prefSpecifierArray))
            {
                var prefItemType = prefItem["Type"] as NSString;
                var prefItemKey = prefItem["Key"] as NSString;
                var prefItemDefaultValue = prefItem["DefaultValue"] as NSString;

                if (prefItemType.ToString() == "PSChildPaneSpecifier")
                {
                   
                }
                else if (prefItemKey != null && prefItemDefaultValue != null)
                {
                    keyValuePairs[prefItemKey] = prefItemDefaultValue;
                }
            }

            return keyValuePairs;
        }

Jest dużo podobieństwa, choć są też pewne różnice np. w metodzie ładującej zasoby.

Największą niespodzianką był jednak fakt, że metoda ViewWillAppear w ViewController nie jest w ogóle wywoływana! (przynajmniej na fizycznym urządzeniu). Patrzyłem w necie i jest całkiem sporo tego typu zgłoszeń, w tym na forum Xamarin. Nasłuchiwanie zmian w konfiguracji umieściłem więc w metodzie ViewDidLoad, podobnie jak w przykładzie od Xamarin, a także innych notyfikacji w mojej aplikacji.

NSObject notificationToken3;

        public override void ViewDidLoad()
        {

              …
              notificationToken3 = notificationCenter.AddObserver(NSUserDefaults.DidChangeNotification, DefaultsChanged);

              …
        }  

        public override void DidReceiveMemoryWarning()
        {
               …

               notificationToken3.Dispose();
        }

        private async void DefaultsChanged(NSNotification notification)
        {
            try
            {
                var defaults = NSUserDefaults.StandardUserDefaults;
                var useRemoteDevice = defaults.BoolForKey("use_remote_device_preference");

                if (remoteController != null)
                    await ReleaseRemoteController();

                if (useRemoteDevice)
                {
                    await CreateNewRemoteController(defaults);                  
                }

             }
            catch (Exception)
            {
               
            }
        }

Kod w ostatniej zaprezentowanej tu metodzie skłania do przejścia do drugiego zagadnienia tego odcinka, a mianowicie socketów. W kontrolerze ViewController wykorzystałem klasę LightsRemoteController z projektu LightOrganApp.Shared używaną już wcześniej przez aplikację w Xamarin.Android. To właśnie jest przykład na siłę i moc Xamarina, jaką jest współdzielenie kodu między platformami. Komunikacja z Raspberry Pi działa bez problemu:

IMG_20160821_120331

I tym sprzętowym akcentem zakończymy ten odcinek Winking smile