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:
Na tym dziś zakończymy, ale nie jest to koniec, jeśli chodzi o iOS 10. Stay tuned!
Brak komentarzy:
Prześlij komentarz