czwartek, 26 maja 2016

[DSP2016] iOS prosto z poligonu odc.8 (sockety)

Dziś święto, początek długiego weekendu, ale DSP to DSP i rządzi się swoimi prawami. Sockety. Pewnie wszyscy znamy, ale nie wszystkim kojarzą się z interesującymi rzeczami. Aby choć trochę to zmienić, pokażemy “przejęcie władzy” nad diodami podłączonymi do Raspberry Pi. W roli “pilota” tym razem wystąpi iPhone (wcześniej pisałem o samej aplikacji Windows 10 na Raspberry oraz komunikacji socketowej z Android).

WP_20160526_09_54_54_Pro

Najpierw szczypta edukacji. W dokumentacji Apple całkiem przydatne okazują się strony Using Sockets and Socket Streams i Introduction to Stream Programming Guide for Cocoa. Można zerknąć na sampla SimpleNetworkStreams. Jak ktoś lubi patrzeć szerzej bardziej przekrojowe Networking Overview ukaże różne formy i oblicza komunikacji sieciowej…  Jednak najbardziej do gustu przypadł mi blog z postem Communication between iOS device (Client) and Raspberry Pi (Server), a zwłaszcza Sending RSA encrypted message - From iOS device to Python socket server (iOS Part).

Te źródła informacji przyczyniły się do powstania poniższego kodu:

class ViewController: UIViewController, NSStreamDelegate  {

    …        
    var outStream: NSOutputStream?

override func viewWillAppear(animated: Bool) {
       super.viewWillAppear(animated)
      
       self.defaultsChanged()
      
       NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.defaultsChanged), name: NSUserDefaultsDidChangeNotification, object: nil)
  
       //test
       onLightOrganDataUpdated(0.3, midLevel: 1, trebleLevel: 0)
   }

func onLightOrganDataUpdated(bassLevel: CGFloat, midLevel: CGFloat, trebleLevel: CGFloat) {
        setLight(bassLight, ratio: bassLevel)
        setLight(midLight, ratio: midLevel)
        setLight(trebleLight, ratio: trebleLevel)
       
        let bassValue = UInt8(round(255 * bassLevel))
        let midValue = UInt8(round(255 * midLevel))
        let trebleValue = UInt8(round(255 * trebleLevel))
        let bytes:[UInt8] = [bassValue, midValue, trebleValue]
       
        sendCommand(bytes)
    }
   
    func sendCommand(bytes: [UInt8]) {       
        let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
       
        dispatch_async(queue) {
            self.outStream?.write(UnsafePointer<UInt8>(bytes), maxLength: bytes.count)
        }
    }
   
    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()
        }
    }
   
    func releaseOutStream() {
        outStream?.delegate = nil
        outStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
        outStream?.close()
        outStream = nil
    }
   
    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")
        }
    }
   
    func defaultsChanged() {
        let defaults = NSUserDefaults.standardUserDefaults()
        let useRemoteDevice = defaults.boolForKey("use_remote_device_preference")
       
        if outStream != nil {
            releaseOutStream()
        }
       
        if useRemoteDevice {
            createNewSocket(defaults)
        }
    }
}

Kod ten możecie sprawdzić, jest już na github. Wyjaśnijmy go teraz!

Co trzeba zrobić, by przygotować łączność z danym adresem i portem hosta? 

U mnie funkcja createNewSocket wywołuje absolutnie najważniejszą systemową funkcję NSStream.getStreamsToHostWithName, która zwraca dla danego adresu hosta i portu strumień wejściowy i wyjściowy, przy pomocy których możemy coś odczytywać lub zapisywać (zdaje się, że dopiero od iOS8 nie trzeba robić przejściówek do bardziej corowego API w C). Interesuje mnie tylko wysyłanie do serwera, dlatego obsłużyłem tylko strumień wyjściowy. Kolejną ważną rzeczą jest odbiór zdarzeń ze strumienia. Tu wystarczy zaimplementować protokół NSStreamDelegate z funkcją func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent). Trzecim ważnym aspektem jest przetwarzanie strumienia w pętli (za pomocą scheduleInRunLoop).

Jak wysłać bajty z klienta na serwer?

Zapisujemy po prostu bajty do strumienia wyjściowego. Hurra! Działa. Nie zaprzątam sobie na razie głowy zabezpieczaniem komunikacji, jak czyni to autor polecanego przeze mnie postu. Zacząłem jednak troche to testować przy różnej konfiguracji ustawień i wyszło kilka przypadłości. Trzeba będzie w niedalekiej przyszłości zmierzyć się z utrzymaniem socketów przy przejściu aplikacji do background (przy okazji jak w ogóle będę odkrywać tajniki pracy w tle na iOS). Z tego co patrzyłem nie bedzie to łatwe. Dziś natomiast poprawiłem coś bardziej trywialnego. W przypadku podania błędnych namiarów na serwer w konfiguracji app-ka mi się przywieszała, pokazując na jakiś czas biały ekran. Aby tak się nie działo, w funkcji sendCommand zacząłem korzystać z innego wątku do wysyłania. Teraz działa wszystko jak należy.

Na razie jedynie dla celów testowych wywołuję sobie funkcję onLightOrganDataUpdated. Jej implementację wzorowałem na uprzednio stworzonej wersji dla Android. To tyle na teraz.

poniedziałek, 23 maja 2016

[DSP2016] iOS prosto z poligonu odc.7 (ustawienia aplikacji)

Od ostatniego razu jestem bogatszy o kolejne informacje związane z przetwarzaniem dźwięku w iOS, ale… zostawimy jeszcze na jakiś czas ten temat i dziś pokażemy jak robi się ustawienia mobilnej appki u Apple’a. W aplikacji dla systemu Android jakiś czas temu zrobiłem panel ustawień do komunikacji z Raspberry Pi. Dziś opowiemy jak udało mi się w iOS osiągnąć poniższą zakładkę, dostępną z poziomu systemowej aplikacji Settings / Ustawienia:

IMG_0034

Gdy potrzebujemy szybko wgryźć się w temat, mogę polecić obejrzenie kawałka któregoś ze szkoleń:

Jeśli chcemy bardziej całościowo zapoznać się z tematem warto przeczytać oficjalny przewodnik Preferences and Settings Programming Guide. Jest całkiem przystępny, a jego część Implementing an iOS Settings Bundle nawet bardzo przystępna. Poza tym natrafiłem na kilka innych przydatnych linków w sieci:

Generalnie jednak nie jest to wszystko bardzo złożone. Tworzymy w projekcie Xcode nowy plik typu Settings Bundle, co generuje nam plik Root.plist opisujący strukturę UI zakładki (dostępnej z poziomu systemowej appki Settings). Za budowę graficznego interfejsu odpowiada sam system. Format pliku jak wskazuje rozszerzenie jest taki sam jak w przypadku innych *.plist w XCode, możemy tam przechowywać elementy kilku typów prostych, jak również słowniki, co pozwala tworzyć struktury hierarchiczne.  Poprzez klikanie w edytorze Xcode doprowadziłem zawartość Root.plist do postaci:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>StringsTable</key>
    <string>Root</string>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Type</key>
            <string>PSToggleSwitchSpecifier</string>
            <key>Title</key>
            <string>UseExternalLighting</string>
            <key>Key</key>
            <string>use_remote_device_preference</string>
            <key>DefaultValue</key>
            <false/>
        </dict>
        <dict>
            <key>AutocorrectionType</key>
            <string>No</string>
            <key>KeyboardType</key>
            <string>URL</string>
            <key>Type</key>
            <string>PSTextFieldSpecifier</string>
            <key>Title</key>
            <string>Host</string>
            <key>Key</key>
            <string>remote_device_host_preference</string>
        </dict>
        <dict>
            <key>KeyboardType</key>
            <string>NumberPad</string>
            <key>DefaultValue</key>
            <string>8181</string>
            <key>Type</key>
            <string>PSTextFieldSpecifier</string>
            <key>Title</key>
            <string>Port</string>
            <key>Key</key>
            <string>remote_device_port_preference</string>
        </dict>
    </array>
</dict>
</plist>

Zapomniałem dodać, że kreator wygenerował też plik językowy Root.strings (w Settings.bundle/en.lproj). Doprowadziłem jego zawartość do postaci:

"UseExternalLighting" = "Use external lighting";
"Host" = "Host";
"Port" = "Port";

Pytanie za dwanaście groszy. Jak dodać tłumaczenia tego pliku dla innego języka np. polskiego?  XCode jakoś nam specjalnie swoim interfejsem nie pomaga, tak jak pomagał przy lokalizacji innych plików. Skorzystałem z rady ze stackoverflow i po prostu utworzyłem za pomocą Xcode folder pl.lproj w Settings.bundle. Następnie już bez Xcode, a przy użyciu zwykłego Findera (odpowiednik Windows Eksploator) wszedłem do pakietu Settings.bundle (nie klikamy w niego, bo wyskakuje jakaś konsola, szukamy odpowiedniej opcji w menu podręcznym) i skopiowałem plik Root.strings z en.lproj do pl.lproj, po czym przetłumaczyłem frazy na polski.

Patrząc na systemowe ustawienia sieci Wi-Fi myślałem, że zrobię podobnie, czyli bedę ukrywał/deaktywował host i port przy wyłączonym switch. Niestety wg. jednego wątku na stackoverflow czy innego nie jest to udokumentowana funkcjonalność. W Android mamy wygodną konfigurację na poziomie XML, w iOS nie mamy takiej otwartości. Nie mamy też otwartości, jeśli chodzi o umieszczanie własnych komponentów (wg. wątku na stackoverflow), jak to jest możliwe w Android. W przypadku liczbowego portu w iOS korzystam z pola tekstowego, tylko rodzaj klawiatury ustawiłem na Number Pad (patrz stackoverflow).

Wróćmy jednak to bardziej zasadniczych rzeczy. Okazuje się, że w iOS nawet domyślne wartości nie działają do końca jakbyśmy tego oczekiwali i musimy napisać całkiem spory kawałek kodu, by zainicjować nimi na dzień dobry ustawienia  aplikacji. Wzorując się na oficjalnym samplu AppPrefs: Storing and Retrieving User Preferences w Objective-C stworzyłem następujący kod Swift w AppDelegate:

   func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
       
        self.populateRegistrationDomain()
       
        // Override point for customization after application launch.
        return true
    }

   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()
    }    
    
    func loadDefaultsFromSettingsPage(plistName: String, inSettingsBundleAtURL settingsBundleURL: NSURL) -> [String:AnyObject]? {
        let settingsDict = NSDictionary(contentsOfURL: settingsBundleURL.URLByAppendingPathComponent(plistName))
       
        if settingsDict == nil {
            return nil;
        }
       
        let prefSpecifierArray = settingsDict!.valueForKey("PreferenceSpecifiers") as? [[String:AnyObject]]
       
        if prefSpecifierArray == nil {
            return nil;
        }
       
        var keyValuePairs: [String:AnyObject] = [:]
       
        for prefItem in prefSpecifierArray! {
            let prefItemType = prefItem["Type"] as? String
            let prefItemKey = prefItem["Key"] as? String
            let prefItemDefaultValue = prefItem["DefaultValue"] as? String
           
            if prefItemType == "PSChildPaneSpecifier" {
                let prefItemFile = prefItem["File"] as? String
                if let childPageKeyValuePairs = loadDefaultsFromSettingsPage(prefItemFile!, inSettingsBundleAtURL: settingsBundleURL) {
                    keyValuePairs += childPageKeyValuePairs
                }
            }
            else if prefItemKey != nil && prefItemDefaultValue != nil {
                keyValuePairs[prefItemKey!] = prefItemDefaultValue
            }
        }
       
        return keyValuePairs
    }

Pasuje dorzucić nieco komentarza. Generalnie zczytujemy plik XML do słownika, który potem przekazujemy już do metody registerDefaults z API. Wywoływana jest też metoda synchronize (fizyczne wymuszenie zapisu w pliku bez czekania aż system to sam zrobi za jakiś czas), której nie powinniśmy nadużywać. W kodzie można zobaczyć, że słowniki są mergowane za pomocą operatora +=. Jego definicja przedstawia się następująco:

func += <K, V> (inout left: [K:V], right: [K:V]) {
    for (k, v) in right {
        left.updateValue(v, forKey: k)
    }
}

A gdzie w ViewController czytamy wartości ustawień i jak dowiemy się, że zostały zmienione po powrocie aplikacji z tła?  Poniższy kod stanowi na to odpowiedź:

override func viewWillAppear(animated: Bool) {
       super.viewWillAppear(animated)
      
       self.defaultsChanged()
      
       NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.defaultsChanged),

       name: NSUserDefaultsDidChangeNotification, object: nil)
   }
   
override func viewWillDisappear(animated: Bool) {
       super.viewWillDisappear(animated)
      
       NSNotificationCenter.defaultCenter().removeObserver(self, name: NSUserDefaultsDidChangeNotification, object: nil)
   }

func defaultsChanged() {
        let defaults = NSUserDefaults.standardUserDefaults()
        let useRemoteDevice = defaults.boolForKey("use_remote_device_preference")
        let host = defaults.stringForKey("remote_device_host_preference")
        let port = defaults.integerForKey("remote_device_port_preference")

        //test
        //self.song.text = "\(useRemoteDevice) \(host) \(port)"
    }

Korzystamy tutaj z NSNotificationCenter subskrybując się na komunikat NSUserDefaultsDidChangeNotification. Metoda defaultsChanged na obecny moment została stworzona tylko w celach testowych (w miejscu tytułu aktualnie odtwarzanej piosenki wyświetlałem sobie wartości parametrów z konfiguracji).

czwartek, 19 maja 2016

[DSP2016] iOS prosto z poligonu odc.6 (ikony aplikacji)

Wypada coś napisać, aby tradycji DSP stało się zadość. Od dwóch wieczorów rozkminiam dość trudne sprawy związane z przetwarzaniem i analizą dźwięku w iOS, ale póki co napiszę o czymś bardziej banalnym, a mianowicie o ikonach mobilnej appki w systemie Apple. Wrzuciłem je niedawno na github.

IMG_0033

Jak przygotować odpowiednie ikony i umieścić je w projekcie w Xcode?  Bardzo zgrabnie przedstawia to początek drugiej części szkolenia.  Generalnie jednak nie jest to trudne. W Xcode zaznaczamy Images.xcassets i wchodzimy w edycję grupy AppIcon. Wszystkie potrzebne ikony (u mnie dla iPhone i iPad’a) wygenerujemy szybko w darmowy sposób za pomocą stronki https://makeappicon.com/.  Następnie przeciągamy je na odpowiednie miejsca na zakładce Xcode. Wygenerowane pliki nazewniczo z reguły pasują do nazw w Xcode (pamiętajmy jedynie że *-Small-*.png odpowiadają najmniejszym, czyli 29pt). O całej operacji można też poczytać na oficjalnej stronie https://developer.apple.com/library/ios/recipes/xcode_help-image_catalog-1.0/chapters/AddingLaunchImagestoanAssetCatalog.html.

poniedziałek, 16 maja 2016

[DSP2016] iOS prosto z poligonu odc.5 (lokalizacja)

Tym razem bardziej lightowo, czasami tak trzeba, jeśli ma się w dalszych planach mocniejszy i trudniejszy materiał. Dziś opiszę, jak przygotowałem wersje anglo- i polskojęzyczną mojej dotychczasowej app-ki na iOS. Możecie to sprawdzić jak zawsze na github.

IMG_0031  IMG_0032

Lokalizacja w iOS nie jest przesadnie trudna, aczkolwiek wymaga kilku kroków. Oto linki z oficjalnej dokumentacji Apple, z którymi możemy się zapoznać:

Najbardziej pomógł mi jednak tutorial ze strony http://www.appcoda.com/localization-tutorial-ios8/. Znalazłem też inny na https://www.raywendeadanrlich.com/64401/internationalization-tutorial-for-ios-2014. Ten pierwszy generalnie jest lepszy, pokazuje więcej zagadnień i jest w Swift. Nie mówi jednak wszystkiego i ten drugi może być dla niego pewnym uzupełnieniem. Żaden z nich nie powiedział jednak o jednej rzeczy, a mianowicie jak… otworzyć w Xcode odpowiednią zakładkę z ustawieniami projektu, od której wszystko się zaczyna. Jeśli ktoś nie będzie mógł trafić, polecam link http://stackoverflow.com/questions/33614747/ios-localization-in-xcode-7.

Wiedza zawarta w powyższych linkach jest w zupełności wystarczająca do wykonania naszego zadania. Nie będziemy się więc przesadnie rozwodzić, dokonamy tylko krótkiego podsumowania z fragmentami plików zasobów i kodu.

Krok 1 - włączenie lokalizacji w całym projekcie, a konkretnie u nas dla plików storyboard. Projekt natywnie został stworzony w języku angielskim, więc dodałem tylko do niego kulturę polską. Zaowocowało to odpowiednią strukturą w projekcie i powstaniem plików na tłumaczenia polskie. Zrobiłem to dla każdego storyboard, natomiast aktualnie praktyczne znaczenie ma Main.storyboard. W wygenerowanym pliku Main.strings w miejscu angielskich napisów powstawiałem polskie, np:

/* Class = "UINavigationItem"; title = "Light Organ"; ObjectID = "G1V-9d-2Ho"; */
"G1V-9d-2Ho.title" = "Kolorofon";

Krok 2 - tłumaczenia dla napisów w kodzie. W projekcie możemy stworzyć plik tekstowy o nazwie Localizable.strings i włączamy jego lokalizację dla kultur angielskiej i polskiej. Następnie wstawiamy tłumaczenia w postaci klucz = wartość, np:

/* Unknown artist */

"unknownArtist" = "Nieznany wykonawca";

W kodzie aplikacji sztywne stringi zastępujemy wywołaniem funkcji NSLocalizedString z podaniem klucza i opcjonalnie komentarza. Przykładowo w FileListViewController mamy linijkę:

var artist = NSLocalizedString("unknownArtist", comment: "Unknown Artist")

Krok 3 - przetłumaczenie nazwy aplikacji. Podobnie jak w Android chcemy, by w języku polskim app-ka nazywała się “Kolorofon”, a po angielsku “Light Organ”. Jak tego dokonać? Tworzymy plik InfoPlist.strings (dla kultur en i pl) w taki sam sposób jak wcześniej Localizable.strings.  Ustawiamy w nim klucz CFBundleDisplayName na pożądaną wartość. Dla kultury polskiej wygląda to tak:

CFBundleDisplayName = "Kolorofon";

Następnym razem może wspomnimy o jeszcze jednej drobnej rzeczy, ale generalnie już czas na więcej hardcore’u –Winking smile

piątek, 13 maja 2016

BUILD 2016 - odc.5 (Project Rome, centrum notyfikacji, notyfikacje, rozszerzenia)

Mała przerwa od DSP i powrót do BUILD 2016. O czym mówią wybrane przeze mnie kolejne cztery prezentacje?

Project Rome - czas experience jednego produktu się kończy, będzie się liczyć experience współpracy między produktami. Wyświetlanie wybranych stron internetowych przez aplikacje wydaje się miłym dodatkiem, natomiast pomysł współpracy pomiędzy różnymi urządzeniami niezależnie od platformy w oparciu z zdalne odpowiedniki serwisów aplikacyjnych jest kapitalny. Owszem znamy od wielu lat ideę zdalnej współpracy między różnymi maszynami opartej na serwerze i kliencie, ale tutaj mamy wprost zdalne odpowiedniki serwisów aplikacyjnych, a więc takich które nie muszą pracować cały czas i mogą być uruchamiane przez klienta w razie potrzeby na czas korzystania (tak jak na jednym urządzeniu mobilnym w Android czy Windows 10)

Centrum notyfikacji w chmurze - kolejna idea, by odchodzić od konkretnego systemu i postawić na współpracę różnych platform ponad podziałami. Na co dzień korzystamy z różnych urządzeń, a to smartfon, statystycznie najczęściej z Androidem, a to laptop z Windows 10 itd.  Dostajemy co dzień różne notyfikacje od aplikacji, jeśli mamy tą samą app-kę na różnych urządzeniach to czy nie byłoby miło by te wszystkie notyfikacje były zsynchronizowane ze sobą?  Dla nas istotne jest dostać informację niezależnie od aktualnie używanego przez nas urządzenia. Taki mirroring będziemy dostawać za darmo, a na urządzeniach z Android wystarczy zainstalować Cortanę i być zalogowanym na konto Microsoft. Oczywiście są też sposoby, by nie zawsze z tego korzystać.  Inna sprawa, to usunięcie notyfikacji. Jak zrobiliśmy to na jednym urządzeniu, to czy nie byłoby miło, by nie trzeba było tego robić na każdym urządzeniu z osobna?  To również dostajemy, tym razem jako opcję.

Rozszerzalność aplikacji uniwersalnych - pewnie dużo z nas słyszało o wprowadzeniu obsługi rozszerzeń do Edge. Okazuje się, że wykorzystywany jest tutaj nowy, w pełni uniwersalny mechanizm. Każde rozszerzenie do osobna aplikacja. Zarówno host, jak i rozszerzenie zawierają stosowne informacje w swoich manifestach. Mamy API do listowania rozszerzeń i monitorowania zachodzących zmian (np. instalacja, deinstalacja). Możemy odczytać właściwości rozszerzenia z jego manifestu. Jeśli to będzie deklaracja serwisu aplikacyjnego, to możemy z takiego serwisu hostowanego w rozszerzeniu sobie skorzystać. Oczywiście nowo wprowadzone rozszerzenia to nie jedyna możliwość rozszerzenia możliwości uniwersalnych aplikacji w Windows 10, do tej pory dysponowaliśmy wywołaniem zewnętrznej aplikacji dla wyniku oraz (oczywiście) serwisami aplikacyjnymi.

Nowości w kaflach i notyfikacjach - w kaflach nie ma ich za wiele (jedno demo), w notyfikacjach toast otrzymamy wsparcie dla struktur znanych z uniwersalnego szablonu kafli, co skutkuje tym, że notyfikacja toast może wyglądać bogato niczym kafel, z pewnymi różnicami oczywiście (m.in jest zwijana). Poza tym w notyfikacjach toast otrzymujemy wiele drobnych wizualnych udogodnień, z reguły nie wymagających zmian w kodzie. Wsparcie przez Edge standardu W3C Web Notifications jest bardzo dobrą wiadomością. Poza tym deweloperzy dostają ułatwienia w tworzeniu notyfikacji i synchronizacji między kaflami, a notyfikacjami toast w ramach pakietów NotificationExtensions i NotificationMate. To nie wszystko. Możemy programowo nasłuchiwać zdarzeń związanymi z notyfikacjami. W związku z wprowadzeniem Windows 10 na Xbox pojawia się notyfikacja dla wielu użytkowników. Całość dopełnia możliwość korzystania z kafli i notyfikacji przez tradycyjne aplikacje desktopowe w ramach projektu Centennial.

 

Project Rome: Driving User Engagement with Connected Apps and Devices

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

możliwość utworzenia watchera z filtrem

image

image

image

image

image

image

image

SDK także dla Android i iOS

image

 

Notification Futures: Action Center in the Cloud and the Windows Notification Platform

image

notyfikacje w chmurze

symbol roku 2016 w MS:  telefon … z Androidem plus tablet z Windows 10

image

image

image

image

image

image

image

dlaczego warto mieć Cortanę na Androidzie?

image

image

przycisk Get up na notyfikacji w Windows 10 pozwala użytkownikowi Androida odkryć, że aplikacja na Windows 10 też istnieje - zostanie przeniesiony do sklepu

image

image

image

image

image

image

image

universal dismiss

image

image

image

image

 

App Extensibility: Build an Ecosystem of Apps

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

http://aka.ms/appextensibility

 

What’s New for Tiles and Toast Notifications

image

image

image

image

image

wypasiona notyfikacja toast (Anniversary Update)

image

image  image

rekomendowana przynajmniej jedna linia tekstu jako podsumowanie (zwijanie w centrum notyfikacji)

image

image image

image

image

image

image

image

image

Anniversary Update: Edge ma wspierać Web Notifications!  (W3C)

akcja w menu kontekstowym notyfikacji

image

image

image

image

NotificationsExtensions

image

image

NotificationMate

image

image

notifications listener

image

image

image

image

image

image

image

image

image

image