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

piątek, 19 sierpnia 2016

Xamarin.iOS kontra Swift odc.3 (własna kontrolka, designer)

Dobry wieczór, Xamarin.iOS kontra Swift, witam serdecznie. Dziś na warsztat weźmiemy pisanie własnej kontrolki na cześć posta z czasów DSP poświęconemu tej tematyce.

IMG_0048

Jak wygląda w C# kod CircleView? Ano tak:

   [Register("CircleView"), DesignTimeVisible(true)]
   public class CircleView: UIView
   {
       private UIColor circleColor;

       [Export("CircleColor"), Browsable(true)]
       public UIColor CircleColor
       {
           get { return circleColor; }
           set
           {
               circleColor = value;
               SetNeedsDisplay();
           }
       }       

       public CircleView(IntPtr p): base(p)
       {
           Initialize();
       }

       public CircleView()
       {
           Initialize();
       }

       private void Initialize()
       {
           circleColor = UIColor.Red;
           ContentMode = UIViewContentMode.Redraw;

           SetNeedsDisplay();
       }

       public override void Draw(CGRect rect)
       {
           base.Draw(rect);

           DrawCircle(CircleColor);
       }

       private void DrawCircle(UIColor color)
       {
           using (var context = UIGraphics.GetCurrentContext())
           {
               var a = Math.Min(Bounds.Size.Width, Bounds.Size.Height);
               var leftX = Bounds.GetMidX() - a / 2;
               var topY = Bounds.GetMidY() - a / 2;
               var rectangle = new CGRect(leftX, topY, a, a);

               context.SetFillColor(CircleColor.CGColor);
               context.FillEllipseInRect(rectangle);
           }
       }
   }

Jakie odczucia w stosunku do Swift?  Inne atrybuty, inny konstruktor (z IntPtr) dla designera (pachnie trochę Visual Studio i Windows a nie Xcode i OS X). Pojawia się metoda SetNeedsDisplay, której w oryginale nie musiałem używać. Metoda Draw ma nieco inną nazwę. Z kolei operacje takie jak np. GetMidX czy FillEllipseInRect prezentują się znacznie zgrabniej w C#. Co z designerem?  Też działa, to znaczy prawie. W prostszym projekcie “na boku” działa wszystko jak należy:

image

Jednak w projekcie aplikacji widnieją paski zamiast kółek Sad smile I tak dziś jest lepiej, bo wczoraj w nocy nic się w designerze nie pokazywało. Dodam, że nie zmieniałem nic w kodzie…

image

W Xcode taki sam storyboard wyświetlany jest poprawnie z kółkami. Wygląda na to, że jakiś bardziej złożony scenariusz nie został do końca obsłużony w designerze Visual Studio… Może to kwestia auto layoutu i dużej liczby constraint-ów?

Na koniec ciekawostka. Otóż uświadomiłem sobie dopiero przy pisaniu w C#, że UIColor ma gotową metodę do uzyskania koloru z zadaną wartością kanału alfa na podstawie podanego koloru. Metoda SetLight może być taka prosta:

       private void SetLight(CircleView light, float ratio)
       {
              light.CircleColor = light.CircleColor.ColorWithAlpha(ratio);
       }

Yupi! Wszystko co dziś pokazywałem jest oczywiście na githubie.  Dobranoc Państwu Winking smile

środa, 17 sierpnia 2016

Xamarin.iOS kontra Swift odc.2 (UISearchController, UISearchBar, stan aplikacji, stylowanie)

Dobry wieczór, Xamarin.iOS kontra Swift, zaczynamy –Winking smileDziś dzień pamięci [DSP2016] iOS prosto z poligonu odc.3, tak więc będzie o wyszukiwarce, a właściwie o zielonej wyszukiwarce takiej jak ta:

IMG_0046

Wybierzmy co bardziej interesujące kawałki kodu z ostatnich wbitek na github.

Zacznijmy od delegatów. Temat był już poruszany w poprzednim odcinku. O ile ostatnio wdrożyłem podejście silnie typowane, ale okupione tworzeniem dodatkowej klasy TableSource, o tyle tym razem wybrałem drogę na skróty bez silnego typowania, ale bardziej w stosunku 1:1 do oryginału w Swift.

      [Export("searchBarSearchButtonClicked:")]
      public virtual void SearchButtonClicked(UISearchBar searchBar)
      {
          searchBar.ResignFirstResponder();
      }

      private void ConfigureSearchController()
      {
          searchController = new CustomSearchController((UIViewController)null)
          {
              WeakSearchResultsUpdater = this
          };
          searchController.DimsBackgroundDuringPresentation = false;
          searchController.SearchBar.SizeToFit();
          UISearchBar.Appearance.TintColor = UIColor.FromRGB(197, 225, 165);
          searchController.SearchBar.Placeholder = NSBundle.MainBundle.LocalizedString("searchMusic", "Search Music");
         searchController.SearchBar.WeakDelegate = this;
          DefinesPresentationContext = true;
          NavigationItem.TitleView = searchController.SearchBar;
          searchController.HidesNavigationBarDuringPresentation = false;
      }

       [Export("updateSearchResultsForSearchController:")]
       public virtual void UpdateSearchResultsForSearchController(UISearchController searchController)
       {
           FilterContentForSearchText(searchController.SearchBar.Text);
       }

I tak  cała klasa  FileListViewController implementuje protokół UISearchResultsUpdating poprzez oznaczoną atrybutem metodę UpdateSearchResultsForSearchController. W obiekcie klasy CustomSearchController dziedziczącej po UISearchController ustawiamy ogólnie typowaną właściwość WeakSearchResultsUpdater na this. Analogicznie postąpiłem dla UISearchBarDelegate tworząc metodę SearchButtonClicked i ustawiając searchController.SearchBar.WeakDelegate na this.

A jak pokolorować kursor w UISearchBar?  Załatwia to linijka: 

UISearchBar.Appearance.TintColor = UIColor.FromRGB(197, 225, 165);

Wspomnijmy jeszcze o odtwarzaniu stanu. Generalnie jest podobnie jak w Swift. Na poziomie kontrolera są pewne różnice w metodach kodujących i dekodujących parametry. W C# przedstawia się to w następujący sposób:

        public override void EncodeRestorableState(NSCoder coder)
        {
            base.EncodeRestorableState(coder);

            coder.Encode(searchController.Active, RestorationKeys.SearchControllerIsActive.ToString());
            coder.Encode(searchController.SearchBar.IsFirstResponder, RestorationKeys.SearchBarIsFirstResponder.ToString());
            coder.Encode(new NSString(searchController.SearchBar.Text), RestorationKeys.SearchBarText.ToString());
        }

        public override void DecodeRestorableState(NSCoder coder)
        {
            base.DecodeRestorableState(coder);

            restoredState.WasActive = coder.DecodeBool(RestorationKeys.SearchControllerIsActive.ToString());
            restoredState.WasFirstResponder = coder.DecodeBool(RestorationKeys.SearchBarIsFirstResponder.ToString());
            searchController.SearchBar.Text = (NSString)coder.DecodeObject(RestorationKeys.SearchBarText.ToString());
        }

W przypadku kodowania mamy metody Encode z przeładowaniem zamiast np. EncodeBool. Nie ma wariantu dla string, dlatego tworzę odpowiedni obiekt NSString. Przy dekodowaniu jest bardziej podobnie jak w oryginale, mamy osobne metody dla różnych typów, ale o krótszych nazwach (np. zamiast decodeBoolForKey jest po prostu DecodeBool).

Na dziś się żegnamy, dobranoc Państwu Winking smile

ś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ę.

środa, 3 sierpnia 2016

Xamarin.Android kontra Java odc.4 (rozwiązanie problemu ze słabym mruganiem świateł w C#)

Hurra! Zagadka #9 Zagadka z wizualizacji z odcinka 2 rozwiązana! Zielona Xamarinowa edycja app-ki mruga już tak samo jak oryginał w Javie. Poprawka wbita na githubie.

WP_20160803_22_35_05_Pro

Co nie grało? Czemu w Javie dokładnie ten sam kod inaczej działał niż w C#?  Zrobiłem sobie logowanie tablicy bajtów z FFT dostarczanych do mojej klasy LightOrganProcessor przez obiekt klasy Visualizer z Android API w projekcie w Javie oraz w C#.

Java:

08-03 19:23:35.166 19561-19561/com.apps.kruszyn.lightorganapp I/sample_LightOrganProce: processFftData 1470245015166: [-8, 0, -5, -1, -8, 0, -14, 2, -13, 17, 24, -13, 11, 10, 13, -3, 4, -8, -1, -4, 3, 3, 4, -3, 0, -5, -1, -2, 1, 0, 3, -2, -2, -4, -2, 0, 0, 1, 2, -1, -1, -3, -2, -2, -1, 0, 1, -1, 0, -3, -3, -1, -1, 0, 0, -1, 0, -2, -2, -1, 0, 0, 0, -1, -1, -2, -2, -1, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 1, -1, 0, -2, -1, -1, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 0, -1, 0, -1, -1, -1, 0, -1, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, 0, -2, -1, -1, 0, 0, 0, -1, -1, -2, -2, -2, -3, 0, -1, 0, -2, 2, 0, 0, 0, 0, 0, 1, 0, -1, -1, 0, 0, 0, 1, -1, 0, -1, -1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, -1, 0, -2, -3, -2, -2, -3, -1, 1, 0, 1, 0, -1, 0, -2, 0, -1, 0, -1, -1, -1, -2, -1, 0, -1, 0, 0, 0, -1, -1, -1, -1, 0, -2, 0, -1, 0, 0, 0, -1, 2, 0, -5, -2, -2, -2, 0, 0, 1, 0, -1, -1, 0, -2, -1, 1, -1, 1, -1, 0, -2, -2, 1, -1, 0, 1, -1, 2, -2, -1, -3, -2, 0, -1, 0, -1, -2, 1, -3, -2, -1, -3, -2, -1, 2, -1, -2, -3, 0, 2, 0, 0, 1, -1, -1, 0, 0, 1, 0, 1, -2, 0, -2, -2, -1, 0, -4, -2, -2, 0, -3, -2, -1, -4, -1, -2, 1, 0, -1, -1, 0, -1, 0, -1, -1, -1, 1, -3, 2, 0, 2, 3, 0, 0, 0, 0, -1, 0, 1, -1, -2, 1, -3, -2, -2, 1, 0, -1, -1, -2, -2, -2, -1, -2, -1, -2, 1, -2, -1, 1, -1, 0, -1, -1, -1, -2, 0, -1, -2, -1, 0, 1, 0, 0, -2, -3, 0, -2, 0, -1, 0, 0, 0, -1, -2, 0, -3, -1, -3, -3, -1, -2, 0, -1, 1, 0, 1, 1, 0, -2, 1, 0, 1, 0, -1, 0, -2, 0, -1, -4, -3, -2, 1, -1, 1, 0, 2, -2, 1, -1, -1, 0, -2, -2, 0, -1, 0, -1, -1, -1, -1, -2, -1, -2, -1, 0, 0, -1, 0, -1, 0, -4, 0, -1, 1, -1, 2, 0, 1, 0, 0, -1, 0, -1, 1, 0, -1, -1, 0, 0, 0, -1, -1, -3, -1, 0, 2, -1, 1, 0, -1, -1, 0, 0, 0, 0, 1, -1, -1, -1, -1, -2, 2, 0, 0, 0, -2, -2, -1, -2, 1, 0, -1, -1, -1, -2, -1, 1, 0, -1, 0, -1, 0, 0, 0, -1, 1, -2, 0, -1, 1, 0, 2, -1, 0, -1, 1, 0, -1, -1, -1, -2, 1, 0, 1, 0, 1, 1, 0, -1, 1, 0, -1, -1, -2, -1, 0, -1, 0, -1, 0, 0, 1, 2, 1, -2, 2, 2, -2, 0, -2, -2, -1, -1, 0, 0, -1, 0, 0, 0, -1, 0, -1, -2, 1, 0, -1, 0, 0, 1, 0, -1, 1, -2, 0, -1, 0, -1, 2, 1, 0, -1, -1, -1, 0, 1, 0, 1, 0, 0, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, 1, 2, 0, 0, 0, -1, -1, 1, 0, 1, -1, 0, -2, -1, -2, -2, 0, -1, 2, 1, 1, 2, -1, 0, -2, -3, -1, -1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, -1, 0, -1, 0, 0, -2, -1, -1, 0, 0, 0, -1, 0, -1, 1, 1, 0, 1, 0, 1, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, 1, -1, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, 1, -1, 0, -2, -2, -2, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 1, -1, -2, -2, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, -1, 0, -2, 0, 1, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, -1, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 0]

C#:

08-03 19:47:04.625 I/sample_LightOrganProce(22658): processFftData 1470246424618: [2, 0, 0, 251, 249, 1, 1, 15, 19, 14, 241, 215, 12, 242, 254, 241, 250, 249, 253, 0, 2, 249, 0, 252, 255, 254, 1, 255, 2, 254, 2, 255, 0, 250, 254, 253, 0, 254, 0, 254, 0, 253, 255, 253, 255, 254, 255, 254, 255, 255, 0, 255, 0, 255, 0, 254, 255, 254, 255, 255, 0, 255, 0, 254, 0, 254, 255, 254, 255, 255, 255, 254, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 254, 1, 254, 0, 255, 0, 254, 255, 255, 0, 254, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 255, 255, 0, 255, 0, 255, 0, 0, 255, 254, 0, 255, 1, 255, 2, 1, 254, 1, 0, 0, 255, 1, 0, 1, 0, 1, 255, 0, 254, 0, 255, 0, 255, 0, 0, 255, 0, 1, 255, 0, 1, 0, 0, 3, 255, 0, 253, 255, 0, 253, 255, 1, 2, 0, 255, 0, 255, 3, 0, 255, 1, 1, 255, 0, 254, 254, 0, 0, 255, 255, 0, 1, 2, 3, 1, 2, 253, 2, 254, 255, 255, 1, 255, 0, 255, 254, 250, 0, 254, 255, 254, 2, 254, 254, 253, 252, 2, 2, 2, 1, 254, 0, 1, 1, 0, 1, 254, 1, 0, 1, 253, 0, 0, 0, 2, 4, 250, 0, 253, 254, 254, 2, 255, 0, 253, 255, 252, 254, 252, 251, 253, 254, 8, 255, 255, 255, 1, 255, 0, 4, 254, 2, 253, 255, 252, 252, 253, 253, 254, 0, 255, 0, 0, 254, 252, 252, 0, 0, 3, 2, 0, 0, 0, 1, 255, 3, 2, 3, 255, 2, 254, 254, 254, 0, 254, 255, 254, 255, 2, 253, 255, 254, 253, 2, 0, 2, 0, 0, 1, 1, 255, 1, 0, 255, 252, 254, 254, 0, 255, 0, 253, 253, 254, 254, 0, 255, 0, 255, 0, 254, 255, 252, 255, 254, 255, 0, 0, 253, 1, 255, 0, 1, 255, 0, 255, 254, 0, 254, 1, 4, 1, 1, 0, 0, 1, 1, 254, 255, 253, 253, 254, 253, 2, 1, 5, 5, 1, 2, 254, 1, 252, 0, 1, 2, 253, 255, 1, 0, 1, 0, 255, 3, 255, 2, 1, 1, 0, 1, 0, 2, 2, 3, 254, 1, 252, 0, 254, 1, 254, 2, 251, 255, 254, 254, 253, 253, 255, 254, 4, 1, 255, 0, 255, 255, 255, 2, 254, 255, 254, 0, 0, 0, 0, 0, 3, 1, 254, 1, 0, 253, 255, 255, 0, 255, 0, 254, 255, 1, 0, 0, 1, 2, 255, 1, 0, 255, 255, 254, 1, 253, 1, 0, 3, 2, 2, 1, 255, 255, 254, 255, 0, 255, 2, 2, 253, 2, 253, 0, 254, 255, 255, 254, 254, 255, 254, 2, 1, 0, 255, 254, 255, 254, 0, 255, 1, 1, 0, 1, 255, 0, 3, 1, 255, 255, 255, 254, 255, 254, 0, 2, 254, 0, 255, 255, 0, 0, 1, 1, 0, 255, 1, 2, 254, 0, 254, 253, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 254, 0, 0, 0, 255, 0, 253, 0, 2, 3, 255, 254, 1, 253, 1, 0, 0, 1, 1, 1, 1, 255, 255, 255, 0, 1, 1, 0, 2, 255, 0, 253, 255, 254, 255, 0, 255, 255, 0, 255, 255, 255, 0, 255, 0, 254, 1, 2, 0, 254, 254, 0, 0, 0, 255, 1, 2, 1, 2, 255, 0, 253, 254, 253, 253, 255, 255, 1, 1, 0, 2, 254, 1, 253, 253, 0, 2, 255, 0, 254, 255, 1, 0, 1, 1, 0, 1, 255, 1, 254, 0, 254, 255, 255, 255, 2, 1, 1, 3, 255, 255, 254, 254, 255, 255, 0, 1, 255, 0, 253, 255, 255, 0, 2, 1, 2, 1, 255, 2, 254, 0, 0, 0, 255, 1, 255, 253, 254, 254, 255, 255, 0, 1, 1, 2, 255, 0, 253, 0, 253, 255, 255, 255, 254, 0, 255, 0, 254, 254, 0, 1, 254, 255, 255, 0, 0, 0, 254, 255, 255, 253, 1, 254, 1, 0, 1, 1, 0, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255

Podczas portowania wkradła się szkolna pułapka (kiedyś na studiach gdy Java rozmawiała z C++ przez sockety stało się podobnie….). Pułapką była zbytnia wierność kodu w C# w stosunku do kodu w Javie, w sumie to mapowanie API w Xamarinie z Javy na C# ma trochę za uszami. Przecież byte w Javie to NIE TO SAMO co byte w C#, po dwakroć, po trzykroć!  W Javie byte jest signed, podczas gdy w C# jest unsigned.

image

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

image

https://msdn.microsoft.com/en-us/library/5bdb6693.aspx

W C# bajtem unsigned jest sbyte.

image

https://msdn.microsoft.com/en-us/library/d86he86x.aspx

Xamarin zwraca nam z Android API tablicę byte[] zamiast sbyte[]. Jak dokonam rzutowania bajtu w C# (powstałego z bajtu w Javie) na sbyte, to powrócę do tego, co było w Javie.

Po zastosowaniu poprawki:

    public class LightOrganProcessor
    {
        …

        public void ProcessFftData(Visualizer visualizer, byte[] fft, int samplingRate)
        {

            …
             //bass

            …
            while (nextFrequency < LowFrequency)
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
            …


            //mid
            …

            while (nextFrequency < MidFreguency)
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
            …


            //treble
            …

            while ((nextFrequency < HighFrequency) && (k < fft.Length))
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
           …
        }

        private static double GetAmplitude(sbyte r, sbyte i)
        {
            return Math.Sqrt(r * r + i * i);
        }

       …

    }

mruganie świateł wraca w Xamarinie do normy Winking smile