Pokazywanie postów oznaczonych etykietą Swift. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Swift. Pokaż wszystkie posty

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!

czwartek, 23 czerwca 2016

BUILD 2016 - odc.10 (iOS–> UWP, UWP design, robot)

4 następne filmiki z BUILD’a. Trochę interesujących informacji odnajdziemy w pokazie na temat portowania aplikacji z iOS do UWP. W designie raczej nic nowego. Robot pokazany na zasadzie pokazu bez kodu.

 

Build the UWP App You Already Wrote: Getting Started with the Windows Bridge for iOS

image

image

WinRT API po albańsku Smile, a dokładniej w projekcji na Objective-C

image

lepszy debugging

image

image

image

image

image

będzie Swift w UWP

image

 

Designing Universal Windows Apps: Creating Beautiful and Engaging Experiences on Windows 10

image

image

image

image

 

Design: Real World Design and Development to Accelerate Your Universal Windows App

image

 

How to Train Your Robot with Sensors and Bluetooth

image

image

wtorek, 31 maja 2016

[DSP2016] iOS prosto z poligonu odc.9 (próba audio w tle, z playlistą i wizualizacją FFT; CocoaPods; Swift + Objective-C/C)

Ten post będzie nieco inny od poprzednich. Do tej pory bywały dość szybkie piłki z od razu gotową implementacją. Tym razem będzie niemal na żywo o swoich zmaganiach z budową odtwarzacza audio w iOS połączonego z wizualizacją, co udało mi się dowiedzieć, co sprawdziłem, na jakie natrafiłem problemy… 

live

Co chcę mieć? To co wcześniej stworzyłem na Android w dość szybki i prosty sposób.  Chcę funkcjonalny odpowiednik systemowego odtwarzacza (odczytywanie z iPod Library, obsługa kolejki utworów, działanie w tle, sterowanie z lock screen) wzbogacony o możliwość analizy strumienia audio celem wizualizacji za pomocą FFT.

Na dzień dobry powiedzmy, jakie mamy zasadnicze możliwości odtwarzania audio w iOS. Są to:

  • systemowy odtwarzacz (używany przeze mnie w dotychczasowych źródłach, wysokopoziomowy full wypas)
  • wysokopoziomowe AVFoundation (AVAudioPlayer - dedykowany dla audio, ale bez kolejkowania utworów, AVPlayer - bardziej ogólny do audio i wideo, AVQueuePlayer - wersja AVPlayer z kolejkowaniem utworów)
  • Core Audio (mało przystępne niskopoziomowe API w C, dostęp do strumienia audio, możliwość analizy i przetwarzania, efektów itp.)
  • niskopoziomowe AVFoundation (AVAudioEngine, AVPlayerNode - opakowanie Core Audio w nowoczesną znacznie bardziej wygodną postać, nie zawsze satysfakcjonująca dokumentacja i przykłady)

W dokumentacji Apple warto rzucić okiem na Multimedia Programming Guide, AVFoundation Programming Guide czy Core Audio Overview. By robić wizualizację potrzebujemy dostępu do strumienia audio w trakcie jego odtwarzania, to jest możliwe albo za pomocą Core Audio albo za pomocą AVAudioEngine (pomijam tu proste wizualizacje w oparciu o siłę sygnału jak na stronie How To Make a Music Visualizer in iOS). A co Apple daje nam w zakresie transformaty FFT?  Mamy funkcje w C w ramach vDSP API w Acceleration Framework (są też oczywiście na świecie biblioteki, ale standardowe API może nam tutaj wystarczyć).

Eksperymenty zdecydowałem się przeprowadzać poza głównym projektem aplikacji iOS, na boku w osobnym “surowym” projekcie AudioPoligono (poligon doświadczalny). Po osiągnieciu stabilnego rozwiązania z założonymi funkcjonalnościami przeniosę go do aplikacji. 

Core Audio. Generalnie nie jest to bardzo przyjazne API, aczkolwiek po lekturze bardzo dobrej książki Learning Core Audio: A Hands-On Guide to Audio Programming for Mac and iOS przejaśni się co nieco w głowie i zobaczymy, że wilk nie jest taki straszny. W temacie przetwarzania FFT Apple oferuje całkiem niezły przykład aurioTouch, szkoda tylko że źródłem dźwięku jest w nim mikrofon, a nie plik audio. Jego nieoficjalną wersję w Swift znajdziemy pod adresem https://github.com/ooper-shlab/aurioTouch2.0-Swift.

Wilk jednak zawsze pozostanie wilkiem, nowoczesne AVAudioEngine było znacznie bardziej kuszące, więc praktyczną zabawę zacząłem od niego. Zachęciła mnie do tego prezentacja Building Modern Apps with AVAudioEngine. W efekcie powstała najpierw klasa CustomPlayer, a potem ją rozbudowałem do klasy CustomAudioPlayer, w której zmiksowałem obsługę kolejkowania wzorując się na samplu iOS Audio Player with Lock Screen Controls (gdzie playerem jest AVAudioPlayer). Metoda odtwarzająca wygląda tutaj tak:

func playItem(item: MPMediaItem) {
       
        guard let url = item.assetURL, let file = try? AVAudioFile(forReading: url) else {
            self.endPlayback()
            return
        }
               
        let audioEngine = AVAudioEngine()
        let audioPlayer = AVAudioPlayerNode()
       
        audioEngine.attachNode(audioPlayer)
        audioEngine.connect(audioPlayer, to: audioEngine.mainMixerNode, format: file.processingFormat)
       
        audioPlayer.scheduleFile(file, atTime: nil, completionHandler: {
            if self.nextItem == nil {
                self.endPlayback()
            }
            else {
                self.nextTrack()
            }
        })
        audioEngine.prepare()
       
        do {
            try audioEngine.start()
            audioPlayer.play()
           
            self.audioEngine = audioEngine
            self.audioPlayer = audioPlayer
           
            self.nowPlayingItem = item
           
            self.updateNowPlayingInfoForCurrentPlaybackItem()
           
            self.notifyOnTrackChanged()
           
        } catch {
            print("Error")
        }
    }

Na samym początku muzyka mi się nie odtwarzała. Okazało się, że system zwalniał obiekt odtwarzacza zanim rozpoczął granie, musiałem związać go z klasą (np.ViewController).

Jakimś cudem wpadłem na pomysł, by przekazać assetURL z MPMediaItem do AVAudioFile, co pozwoliło odtwarzać mi muzykę z iPod Library przez AVAudioEngine. Nie natrafiłem nigdzie na taki przykład, nawet było gdzieś o to pytanie na forum Apple’a i stackoverflow pozostawione bez odpowiedzi.

Zachęcony pierwszymi sukcesemi zrobiłem sobie audio w tle. Jak to się robi? Pomocna okazało się Q&A z dokumentacji (Playing media while in the background using AV Foundation on iOS) oraz kilka sampli (np. Playing audio in the background czy kapitalny iOS Audio Player with Lock Screen Controls). W capabilities projektu zaznaczamy pracę w tle i wybieramy audio. Dodatkowo w kodzie musimy ustawić sesję audio, bez tego to nie zadziała:

self.audioSession = AVAudioSession.sharedInstance()

try! self.audioSession.setCategory(AVAudioSessionCategoryPlayback)
try! self.audioSession.setActive(true)

W kodzie nie musimy nic robić, to używane przez nas API powoduje, że system podtrzymuje rozpoczętą aktywność odtwarzania po opuszczeniu foreground przez app-kę, a nawet po wyłączeniu ekranu telefonu. Kolejny sukces.

W dalszej kolejności zamierzałem skorzystać z tap-a na węźle końcowym, by pobierać co jakiś czas strumień audio trafiający do głośników, a później obrabiać go vDSP. Pomijając już niezbyt dobrą dokumentację (miałem kilka wpisów, jakiś slajd z prezentacji), o zaprzestaniu tej drogi zadecydowały pewne problemy z przejmowaniem odtwarzania audio, gdy inna aplikacja już coś odtwarzała. Taka obsługa wznawiania sesji jak w przypadku AVPlayer nie była możliwa, ale to w sumie detal. Najbardziej rozwalił mnie current time odtwarzacza i problem z prawidłową obsługą panelu odtwarzającego na lock-screen. AVAudioEngine nie ma takiego pola jak current time, które ma AVPlayer. Co prawda jest sposób, by sobie je obliczyć, jednak te wartości użyte do odświeżania informacji o aktualnie odtwarzanym utworze na lock-screen powodowały, że nie mogłem wznawiać odtwarzania po spauzowaniu, a czas leciał nawet po spauzowaniu. Co prawda znalazłem sposób, by go zatrzymać, ale powodowało to powrót do początku utworu.

Postanowiłem pójść w biblioteki. EZAudio wydawało się bardzo obiecujące. Nie dość, że mamy banalnie prostą obsługę odtwarzania, to jeszcze mamy też dość prosty przykład z analizą FFT:

5621705a-2971-11e5-88ed-9a865e422ade

Jak skorzystać z tej biblioteki?  Najprościej użyć CocoaPods, takiego odpowiednika nugeta. Polecam filmik, by zacząć go szybko używać.

Kolejnym problemem jest fakt, że EZAudio nie jest napisane w Swift, tylko w Objective-C. Ale problem nie okazał się duży. Apple bardzo elegancko to rozwiązał. Polecam szybką lekturę Using Swift with Cocoa and Objective-C (Swift 2.2). W jednym projekcie możemy mieć pliki zarówno w Swift, jak i w Objective-C. Mogą się wywoływać wzajemnie. Bajka. Tylko tworzymy plik pomostowy plik nagłówkowy .h z importem odpowiedniego nagłówka .h z Objective-C. XCode sam go zaproponuje stworzyć, jak zaczniemy tworzyć dowolny plik Objective-C w projekcie Swift (plik Objective-C możemy potem wyrzucić). W ten sposób stworzyłem plik AudioPoligono-Bridging-Header.h , którego treść zmodyfikowałem do postaci:

#import <EZAudio/EZAudio.h>

W źródłach EZAudio 1.4 był problem i zgodnie z wątkiem na github poprawiłem tam import w dwóch plikach .h. Zobaczyłem BTW że jest już wersja 1.5 i może ją wypróbuję następnym razem (1.4 zainstalowałem kierując się dokumentacją na głównej stronie github).  Dla porównania przykładowy projekt będący taką integracją, ale sprzed 11 miesięcy możemy znaleźć na https://github.com/syedhali/EZAudio-Swift.

Stworzyłem kod odtwarzający audio (aha, aby używać obiektów z biblioteki dodałem jeszcze u góry pliku:  import EZAudio):

func playItem(item: MPMediaItem) {       
      
       let file = EZAudioFile(URL: item.assetURL)
      
       let player = EZAudioPlayer(delegate: self)
      
       player.playAudioFile(file)
      
       self.audioPlayer = player
      
       self.sampleRate = Float(file.clientFormat.mSampleRate)
       self.fft = EZAudioFFTRolling(windowSize: FFTWindowSize, sampleRate: self.sampleRate, delegate: self)
      
       self.nowPlayingItem = item
      
       self.updateNowPlayingInfoForCurrentPlaybackItem()
      
       self.notifyOnTrackChanged()
   }

Okazało się jednak, że oryginalne EZAudio (przynajmniej to 1.4) rzuca błedem przy przekazaniu do EZAudioFile (czy EZAudioPlayer) url-a z iPod Library. Lektura wątku na github spowodowała, że wgrałem zapronowaną przez kogoś poprawkę warunku i odtwarzanie ruszyło. Hurra!

Odkryłem jednak, że EZAudio zawiesza odtwarzanie, jak wyłączy się ekran telefonu. Jak się okazało na kolejnym wątku w github, to znany problem. Tymczasowo można rozwiązać go poprzez ustawienie rodzaju sesji na odtwarzanie i nagrywanie, zamiast tylko na odtwarzanie. Dało to oczekiwany efekt. Zauważyłem jednak, że czasami odtwarzanie na malutką chwilkę może spowodować lekką chrypkę podczas przechodzenia aplikacji do background. Nie zawsze, ale może zdarzyć się. Dochodzi do tego fakt, że dotychczasowa implementacja kolejkowania utworów, o ile dobrze działała w tle w poprzednim przypadku z AVFoundation, o tyle w EZAudio następuje co prawda przejście na następnego kawałka, ale po kilku sekundach odtwarzacz milknie. Plusem natomiast jest to, że EZAudio ma current time tak jak AVAudioPlayer, co daje nam poprawne sterowanie odtwarzaniem z poziomu lock-screen. Mogę pauzować, wznawiać, przechodzić między utworami bez żadnych problemów, czas dobrze się pokazuje i zatrzymuje kiedy potrzeba.

Pewne problemy z EZAudio może byłbym w stanie jakoś przeboleć (i szukać nieco później dla nich rozwiazania nawet analizując kod EZAudio), by cieszyć się wizualizacją FFT. Tutaj sukces jest jednak tylko częściowy. Co prawda implementując metodę protokołu EZAudioPlayerDelegate:

public func audioPlayer(audioPlayer: EZAudioPlayer!, playedAudio buffer: UnsafeMutablePointer<UnsafeMutablePointer<Float>>, withBufferSize bufferSize: UInt32, withNumberOfChannels  numberOfChannels: UInt32, inAudioFile audioFile: EZAudioFile!) {
       
        self.fft.computeFFTWithBuffer(buffer[0], withBufferSize: bufferSize)
    }

i protokołu EZAudioFFTDelegate:

public func fft(fft: EZAudioFFT!, updatedWithFFTData fftData: UnsafeMutablePointer<Float>, bufferSize: vDSP_Length) { … }

udało mi się podczas odtwarzania dostawać regularnie wyliczane tablice float-ów, ale czasami przy starcie odtwarzania czy tuż po jego zakończeniu miałem EXC_BAD_ACCESS na UnsafeMutablePointer<Float>, który jest odpowiednikiem tablicy z Objective-C w Swift. To powinno dać się poprawić. Sporej poprawki z pewnością wymaga jednak logika przetwarzajaca dane otrzymywane w ostatniej metodzie. Próba adaptacji kodu z Androida, gdzie otrzymywałem bajty zamiast floatów po uwzględnieniu tego faktu nie daje w pełni satysfakcjonujących wyników. Na kanale basu mam w miarę sensowne wartości od 0 do 1, o tyle w kanale środkowym to Nan, a tony wysokie zawsze oscylują przy zerze. Może winna jest klasa EZAudioFFTRolling używana w wykresach i może zamiast niej bezpośrednio korzystać z EZAudioFFT czy systemowego vDSP?

A może znajdzie są coś lepszego i bardziej niezawodnego od EZAudio? W każdym razie ta tematyka w iOS nie jest tak prosta jak w przypadku Androida, gdzie mamy bardzo dobry przykład od Google i gotowe wsparcie dla FFT przy odtwarzaniu na poziomie systemowego API.

Myślę, że to zagadnienie będę dalej kontynuować, już poza Daj Się Poznać 2016, bo jesteśmy na finishu tej zacnej inicjatywy. To oczywiście niejedyna rzecz, jaką zajmiemy się w okresie post-DSP –Winking smile

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).

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