Was ändert sich, wenn man Objective-C Code in Swift umschreibt?

In meinem letzten Artikel Von Objective-C zu Swift: Table Views, habe ich den umgeschriebenen Code eines Table View Menüsystems verglichen.

In diesem Artikel nehme ich das Einlesen von JSON Daten als Beispiel und verwende folgende Daten:

{
    "works":
    [
        {
            "title": "Lemon Tree",
            "producer": "Fools Garden",
            "year": 1995,
            "length": "3:11",
            "medium": "SONG"
        },
        {
            "title": "The Lion King",
            "producer": "Don Hahn",
            "year": 1994,
            "length": "1:28:00",
            "medium": "MOVIE"
        },
        {
            "title": "The Lord of the Rings",
            "producer": "J. R. R. Tolkien",
            "year": 1994,
            "medium": "BOOK"
        }
    ]
}

Diese Daten könnten aus einem Web-Request stammen, aber ich werde sie aus einer Datei einlesen, um es möglichst einfach zu halten.

Objective-C Code

#import <Foundation/Foundation.h>

@interface WorkObjc : NSObject

+ (NSArray*)parseAllFromFileWithName:(NSString*)fileName;

@end
#import "WorkObjc.h"

@interface WorkObjc ()

typedef NS_ENUM(NSInteger, Medium) {
  MediumNone,
  
  MediumSong,
  MediumMovie,
  MediumBook,
};

@property (nonatomic) NSString* title;
@property (nonatomic) NSString* producer;
@property (nonatomic) NSInteger year;
@property (nonatomic) NSInteger lengthInSeconds;
@property (nonatomic) Medium medium;

@end

@implementation WorkObjc

+ (NSString*)stringFromMedium:(Medium)medium{
  if(medium) {
    switch((Medium)medium) {
      case MediumBook: return @"BOOK";
      case MediumMovie: return @"MOVIE";
      case MediumSong: return @"SONG";
      default: return nil;
    }
  } else {
    return nil;
  }
}

+ (Medium)mediumFromString:(NSString*)string{
  if([string isEqualToString:@"BOOK"]) {
    return MediumBook;
  } else if([string isEqualToString:@"MOVIE"]) {
    return MediumMovie;
  } else if([string isEqualToString:@"SONG"]) {
    return MediumSong;
  } else {
    return MediumNone;
  }
}

+ (NSArray*)parseAllFromFileWithName:(NSString*)fileName{
  NSString* documentPath = NSSearchPathForDirectoriesInDomains(
    NSDocumentDirectory, NSUserDomainMask, YES)[0];
  
  NSString* path =
    [documentPath stringByAppendingPathComponent:fileName];
  
  NSData* data =
    [NSData dataWithContentsOfFile:path options:0 error:nil];

  id json = data ? [NSJSONSerialization JSONObjectWithData:
    data options:0 error:nil] : nil;
  
  if([json isKindOfClass:[NSDictionary class]]) {
    return [self parseAllFromDict:json];
  } else {
    return [NSArray array];
  }
}

+ (NSArray*)parseAllFromDict:(NSDictionary*)dict{
  NSMutableArray* works = [NSMutableArray array];
  NSArray* jsonWorks = dict[@"works"];
  if([jsonWorks isKindOfClass:[NSArray class]]) {
    for(NSDictionary* jsonWork in jsonWorks) {
      if([jsonWork isKindOfClass:[NSDictionary class]]) {
        [works addObject:[self parseFromDict:jsonWork]];
      }
    }
  }
  return works;
}

+ (instancetype)parseFromDict:(NSDictionary*)dict{
  WorkObjc* work = [[WorkObjc alloc] init];
  
  NSString* title = dict[@"title"];
  if([title isKindOfClass:[NSString class]]) {
    work.title = title;
  }
  
  NSString* producer = dict[@"producer"];
  if([producer isKindOfClass:[NSString class]]) {
    work.producer = producer;
  }
  
  NSNumber* year = dict[@"year"];
  if([year isKindOfClass:[NSNumber class]]) {
    work.year = year.integerValue;
  }
  
  NSString* lengthString = dict[@"length"];
  if([lengthString isKindOfClass:[NSString class]]) {
    NSArray* components =
      [lengthString componentsSeparatedByString:@":"];
    int base = 1, sum = 0;
    for(NSString* stringComponent in
        [components reverseObjectEnumerator]) {
      int component = stringComponent.intValue;
      sum += component * base;
      base *= 60;
    }
    work.lengthInSeconds = sum;
  }
  
  NSString* mediumString = dict[@"medium"];
  if([mediumString isKindOfClass:[NSString class]]) {
    work.medium = [self mediumFromString:mediumString];
  }
  
  return work;
}

- (NSString *)description{
  NSMutableString* description =
    [NSMutableString stringWithFormat:@"%@: %@ by %@",
      [WorkObjc stringFromMedium:self.medium],self.title,self.producer];
  
  if(self.year != 0) {
    [description appendFormat:@" (%i)", self.year];
  }
  if(self.lengthInSeconds != 0) {
    [description appendFormat:@", %i seconds", self.lengthInSeconds];
  }
  return description;
}

@end

Die Klasse WorkObjc repräsentiert ein Werk. Die fünf Properties entsprechen jeweils einer Eigenschaft des Werks.

Für die Eigenschaft des Mediums habe ich ein Enum definiert, um die möglichen Werte auf Song, Film und Buch einzuschränken. Der erste Enum-Wert MediumNone steht für ein unbekanntes oder nicht definiertes Medium. Er steht an erster Stelle, damit er implizit den Wert 0 bekommt. Auf diese Weise hat die medium Property den Wert MediumNone, wenn sie keinen Wert zugewiesen bekommt.

Die ersten beiden Methoden, stringFromMedium: und mediumFromString: machen eine Umwandlung von Enum zu String und umgekehrt. Sie werden für das Parsen und für die textuelle Darstellung eines Werks verwendet.

Die Methode parseAllFromFileWithName: liest zunächst eine Datei aus dem Documents Verzeichnis der App-Sandbox aus. Dann wandelt sie den Inhalt der Datei in ein Dictionary um, die einem JSON Objekt entspricht und ruft schließlich die weiter unten definierte Methode parseAllFromDict: auf.

Das JSON Objekt in Form einer Dictionary wird von der Methode parseAllFromDict: in ein Array von WorkObjc Objekten umgewandelt. Um jedes einzelne Werk zu parsen, wird jeweils die Methode parseFromDict: aufgerufen. Beim Parsen werden unerwartete, ungültige oder nicht vorhandene Werte wie folgt behandelt:
Die Eigenschaften title und producer sind Strings und können auf nil gesetzt werden. year und lengthInSeconds sind beide Integer. Sie bekommen den Wert 0, wenn etwas schief läuft. medium ist ein Enum und bekommt den Wert MediumNone zugewiesen, wenn es keinem anderen Wert zugeordnet werden kann.

Die letzte Methode description stellt das WorkObjc-Objekt als String dar. year und lengthInSeconds werden jeweils auf den Wert 0 geprüft um zu entscheiden ob sie in den description String aufgenommen werden oder nicht. Damit sieht das Ergebnis schöner aus, wenn beispielsweise das Jahr unbekannt ist und deswegen in den JSON-Daten weggelassen wurde.

Ausgabe der description für die drei Werke:

SONG: Lemon Tree by Fools Garden (1995), 191 seconds
MOVIE: The Lion King by Don Hahn (1994), 5280 seconds
BOOK: The Lord of the Rings by J. R. R. Tolkien (1994)

Swift Code

Der in Swift um geschriebene Code sieht wie folgt aus:

import Foundation

class WorkSwift : Printable {
  
  enum Medium : String, Printable {
    case Song = "SONG"
    case Movie = "MOVIE"
    case Book = "BOOK"
    
    var description: String {
      return toRaw()
    }
  }
  
  var title: String?
  var producer: String?
  var year: Int?
  var lengthInSeconds: Int?
  var medium: Medium?
  
  typealias JsonDict = Dictionary<NSString, NSObject>
  
  class func parseAllFromFileWithName(fileName: String) -> [WorkSwift] {
    let documentPath = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory, .UserDomainMask, true)[0] as String

    let path = documentPath.stringByAppendingPathComponent(fileName)
    
    let data = NSData.dataWithContentsOfFile(
      path, options: nil, error: nil)
    
    let json:AnyObject! = data ? NSJSONSerialization.JSONObjectWithData(
      data, options: nil, error: nil) : nil
    
    if let jsonDictionary = json as? JsonDict {
      return parseAllFromDict(jsonDictionary)
    } else {
      return []
    }
  }
  
  class func parseAllFromDict(dict: JsonDict) -> [WorkSwift] {
    var works = [WorkSwift]()
    if let jsonObjects = dict["works"] as? NSArray {
      for jsonObject in jsonObjects {
        if let jsonWork = jsonObject as? JsonDict {
          works += WorkSwift.parseFromDict(jsonWork)
        }
      }
    }
    return works
  }
  
  class func parseFromDict(dict: JsonDict) -> WorkSwift {
    let work = WorkSwift()
    
    work.title = dict["title"] as? NSString
    work.producer = dict["producer"] as? NSString
    work.year = dict["year"] as? NSNumber as? Int
    
    if let components = ((dict["length"] as? NSString as? String)?
            .componentsSeparatedByString(":"))?.map({ $0.toInt() }) {
      var base = 1, sum = 0
      for possibleComponent in components.reverse() {
        let component = possibleComponent ? possibleComponent! : 0
        sum += component * base
        base *= 60
      }
      work.lengthInSeconds = sum
    }
    
    if let mediumString = dict["medium"] as? NSString {
      work.medium = Medium.fromRaw(mediumString)
    }
    
    return work
  }
  
  var description: String {
    var description = "(medium): (title) by (producer)"
    if year {
      description += " ((year))"
    }
    if lengthInSeconds {
      description += ", (lengthInSeconds) seconds"
    }
    return description
  }
}

In der Swift-Version des Beispiels definiere ich das Enum mit String als „raw type“ und weise daher jedem Enum-Wert einen String anstelle eines Integers zu. Die zugewiesenen String Werte ensprechen den möglichen Werten aus den JSON Daten. Auf diese Weise spare ich mir die beiden Mapping-Methoden, die ich im Objective-C Code benötigt habe.

Alle fünf Properties habe ich als Optionals definiert. Für year und lengthInSeconds bedeutet das, dass ich den Wert 0 nicht als besonderen Wert für nicht vorhandene oder ungültige Werte behandeln muss. Für das Enum Medium bedeutet das, dass ich den Enum-Wert MediumNone nicht mehr brauche. Alle nicht vorhandenen oder ungültigen Werte werden einheitlich mit nil abgehandelt, egal ob es sich um ein String, ein Integer oder ein Enum handelt.

Ich definiere ein typealias für den speziellen Dictionary Type Dictionary<NSString, NSObject>, da ich ihn innerhalb der Klasse mehrmals verwenden möchte.

Die nächsten beiden Methoden parseAllFromFileWithName und parseAllFromDict haben abgesehen von der Swift-typischen Syntax keine wesentlichen Unterschiede zum Objective-C Code.

Die Methode parseFromDict fällt jedoch in der Swift-Variante wesentlich kürzer aus. Das liegt daran, dass ich in Swift den as? Operator verwenden kann um gleichzeitig den Typ zu prüfen und zu casten.

Die description Methode ist auf der Swift-Seite zwar nicht unbedingt kürzer, wenn man nach der Zeilenzahl geht. Aber sie wirkt dank des += Operators sauberer und ist mit der Verwendung der „String Interpolation“ (Einfügen der Variablen direkt im String Literal) leichter zu lesen.

Fazit

Aus 135 Objective-C Zeilen sind 89 Swift Zeilen geworden. Zum kürzeren Code hat am meisten das String-Enum und der as? Operator beigetragen.
Die Properties bilden die Werte der JSON-Daten dank der Optionals sauber und einheitlich ab. Syntaktische Hilfsmittel wie der += Operator für Arrays und Strings verringern die Länge einiger Codezeilen und machen den Code lesbarer.

Bewerte diesen Beitrag
0 Kommentare

Dein Kommentar

An Diskussion beteiligen?
Hinterlasse uns Deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.