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:
Gdy potrzebujemy szybko wgryźć się w temat, mogę polecić obejrzenie kawałka któregoś ze szkoleń:
- Introduction to iOS for .NET Developers (Persistance)
- iOS 7 Fundamentals (Local Persistance)
- Building a Real World iOS Application with Swift (Persisting Data)
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:
- http://www.codingexplorer.com/nsuserdefaults-a-swift-introduction/
- http://www.ioscreator.com/tutorials/use-settings-nsuserdefaults-ios8-swift
- http://www.accella.net/knowledgebase/nsuserdefaults-some-pretty-good-practices/
- https://makeapppie.com/2016/03/14/using-settings-bundles-with-swift/
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).
Brak komentarzy:
Prześlij komentarz