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!

Brak komentarzy: