From a6f08eed309725864c47e262fb24c116d5f20c9f Mon Sep 17 00:00:00 2001 From: Nils Fischer Date: Fri, 20 May 2016 13:24:11 +0200 Subject: [PATCH 1/6] implemented pokedex --- APIClient.xcodeproj/project.pbxproj | 32 +++ APIClient/AppDelegate.swift | 14 +- APIClient/Base.lproj/Main.storyboard | 283 ++++++++++++++++++- APIClient/Info.plist | 11 + APIClient/PokeAPI.swift | 124 ++++++++ APIClient/Pokedex.swift | 40 +++ APIClient/PokedexEntriesViewController.swift | 99 +++++++ APIClient/PokedexEntryCell.swift | 41 +++ APIClient/PokedexViewController.swift | 133 +++++++++ APIClient/Pokemon.swift | 72 +++++ APIClient/PokemonSpeciesCell.swift | 66 +++++ APIClient/PokemonViewController.swift | 166 +++++++++++ 12 files changed, 1070 insertions(+), 11 deletions(-) create mode 100644 APIClient/PokeAPI.swift create mode 100644 APIClient/Pokedex.swift create mode 100644 APIClient/PokedexEntriesViewController.swift create mode 100644 APIClient/PokedexEntryCell.swift create mode 100644 APIClient/PokedexViewController.swift create mode 100644 APIClient/Pokemon.swift create mode 100644 APIClient/PokemonSpeciesCell.swift create mode 100644 APIClient/PokemonViewController.swift diff --git a/APIClient.xcodeproj/project.pbxproj b/APIClient.xcodeproj/project.pbxproj index d57524a..633cbe8 100644 --- a/APIClient.xcodeproj/project.pbxproj +++ b/APIClient.xcodeproj/project.pbxproj @@ -8,21 +8,37 @@ /* Begin PBXBuildFile section */ 872A275C1CF0D87100A988C4 /* APIResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872A275B1CF0D87100A988C4 /* APIResource.swift */; }; + 8744AA581CF2199F001B0470 /* PokemonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA571CF2199F001B0470 /* PokemonViewController.swift */; }; + 8744AA5A1CF22716001B0470 /* PokedexEntriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */; }; + 8744AA5C1CF22955001B0470 /* PokedexEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */; }; 874D06591CEF295E009A494D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D06581CEF295E009A494D /* AppDelegate.swift */; }; + 874D065B1CEF295E009A494D /* PokedexViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D065A1CEF295E009A494D /* PokedexViewController.swift */; }; 874D065E1CEF295E009A494D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 874D065C1CEF295E009A494D /* Main.storyboard */; }; 874D06601CEF295E009A494D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 874D065F1CEF295E009A494D /* Assets.xcassets */; }; 874D06631CEF295E009A494D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 874D06611CEF295E009A494D /* LaunchScreen.storyboard */; }; + 874D066B1CEF2BC0009A494D /* PokeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066A1CEF2BC0009A494D /* PokeAPI.swift */; }; + 874D066D1CEF2F49009A494D /* Pokemon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066C1CEF2F49009A494D /* Pokemon.swift */; }; + 874D066F1CEF372D009A494D /* Pokedex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066E1CEF372D009A494D /* Pokedex.swift */; }; + 874D06711CEF6877009A494D /* PokemonSpeciesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */; }; A526FDEE5BF2D1BC3C97B9FA /* Pods_APIClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD8A5B3E1266D5298CABE44A /* Pods_APIClient.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 872A275B1CF0D87100A988C4 /* APIResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIResource.swift; sourceTree = ""; }; + 8744AA571CF2199F001B0470 /* PokemonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonViewController.swift; sourceTree = ""; }; + 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokedexEntriesViewController.swift; sourceTree = ""; }; + 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokedexEntryCell.swift; sourceTree = ""; }; 874D06551CEF295E009A494D /* APIClient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = APIClient.app; sourceTree = BUILT_PRODUCTS_DIR; }; 874D06581CEF295E009A494D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 874D065A1CEF295E009A494D /* PokedexViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokedexViewController.swift; sourceTree = ""; }; 874D065D1CEF295E009A494D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 874D065F1CEF295E009A494D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 874D06621CEF295E009A494D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 874D06641CEF295E009A494D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 874D066A1CEF2BC0009A494D /* PokeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokeAPI.swift; sourceTree = ""; }; + 874D066C1CEF2F49009A494D /* Pokemon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pokemon.swift; sourceTree = ""; }; + 874D066E1CEF372D009A494D /* Pokedex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pokedex.swift; sourceTree = ""; }; + 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonSpeciesCell.swift; sourceTree = ""; }; 93EFAB13AE62F1B6FB2683B2 /* Pods-APIClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIClient.debug.xcconfig"; path = "Pods/Target Support Files/Pods-APIClient/Pods-APIClient.debug.xcconfig"; sourceTree = ""; }; BD8A5B3E1266D5298CABE44A /* Pods_APIClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FFE13D543A95D28F27CFBDA4 /* Pods-APIClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIClient.release.xcconfig"; path = "Pods/Target Support Files/Pods-APIClient/Pods-APIClient.release.xcconfig"; sourceTree = ""; }; @@ -43,6 +59,9 @@ 872A27571CF0D4E200A988C4 /* View Controller */ = { isa = PBXGroup; children = ( + 8744AA571CF2199F001B0470 /* PokemonViewController.swift */, + 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */, + 874D065A1CEF295E009A494D /* PokedexViewController.swift */, ); name = "View Controller"; sourceTree = ""; @@ -50,6 +69,8 @@ 872A27581CF0D4EB00A988C4 /* View */ = { isa = PBXGroup; children = ( + 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */, + 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */, ); name = View; sourceTree = ""; @@ -57,7 +78,10 @@ 872A27591CF0D64A00A988C4 /* Model */ = { isa = PBXGroup; children = ( + 874D066A1CEF2BC0009A494D /* PokeAPI.swift */, 872A275B1CF0D87100A988C4 /* APIResource.swift */, + 874D066E1CEF372D009A494D /* Pokedex.swift */, + 874D066C1CEF2F49009A494D /* Pokemon.swift */, ); name = Model; sourceTree = ""; @@ -242,8 +266,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 874D06711CEF6877009A494D /* PokemonSpeciesCell.swift in Sources */, + 874D066B1CEF2BC0009A494D /* PokeAPI.swift in Sources */, + 8744AA5A1CF22716001B0470 /* PokedexEntriesViewController.swift in Sources */, + 874D065B1CEF295E009A494D /* PokedexViewController.swift in Sources */, + 8744AA5C1CF22955001B0470 /* PokedexEntryCell.swift in Sources */, + 874D066F1CEF372D009A494D /* Pokedex.swift in Sources */, 872A275C1CF0D87100A988C4 /* APIResource.swift in Sources */, 874D06591CEF295E009A494D /* AppDelegate.swift in Sources */, + 8744AA581CF2199F001B0470 /* PokemonViewController.swift in Sources */, + 874D066D1CEF2F49009A494D /* Pokemon.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/APIClient/AppDelegate.swift b/APIClient/AppDelegate.swift index 630c3ed..b3684a6 100644 --- a/APIClient/AppDelegate.swift +++ b/APIClient/AppDelegate.swift @@ -7,14 +7,26 @@ // import UIKit +import Moya @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - + + /// The PokeAPI Provider representing the Server + let pokeAPI = MoyaProvider() + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + + if let pokemonViewController = (window?.rootViewController as? UINavigationController)?.topViewController as? PokemonViewController { + + // Pass the PokeAPI Provider on to the root view controller + pokemonViewController.pokeAPI = pokeAPI + + } + return true } diff --git a/APIClient/Base.lproj/Main.storyboard b/APIClient/Base.lproj/Main.storyboard index 69b2fbf..3174287 100644 --- a/APIClient/Base.lproj/Main.storyboard +++ b/APIClient/Base.lproj/Main.storyboard @@ -3,8 +3,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -14,31 +194,114 @@ - + - - + + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + diff --git a/APIClient/Info.plist b/APIClient/Info.plist index 40c6215..5669918 100644 --- a/APIClient/Info.plist +++ b/APIClient/Info.plist @@ -22,6 +22,17 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSExceptionDomains + + pokeapi.co + + NSExceptionAllowsInsecureHTTPLoads + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/APIClient/PokeAPI.swift b/APIClient/PokeAPI.swift new file mode 100644 index 0000000..364bae8 --- /dev/null +++ b/APIClient/PokeAPI.swift @@ -0,0 +1,124 @@ +// +// PokeAPI.swift +// APIClient +// +// Created by Nils Fischer on 20.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import Foundation +import Moya +import Freddy + + +/// The abstraction of the [Poke API](http://pokeapi.co) REST API. +enum PokeAPI: Moya.TargetType, Cacheable { + + + /// MARK: Endpoints + + case pokedex(NamedResource) + case pokemonSpecies(NamedResource) + case pokemon(NamedResource) + + + // MARK: Network Abstraction + + var baseURL: NSURL { return NSURL(string: "http://pokeapi.co/api/v2")! } + + var path: String { + switch self { + case .pokedex(let namedResource): return "/pokedex/\(namedResource.name)" + case .pokemonSpecies(let namedResource): return "/pokemon-species/\(namedResource.name)" + case .pokemon(let namedResource): return "/pokemon/\(namedResource.name)" + } + } + + var method: Moya.Method { return .GET } + + var parameters: [String : AnyObject]? { + switch self { + default: return nil + } + } + + // TODO: Provide sample data for testing + var sampleData: NSData { + switch self { + default: return "".dataUsingEncoding(NSUTF8StringEncoding)! + } + } + + var cacheIdentifier: String { + return self.path + } +} + + +/// Represents a resource provided by the Poke API by its name +struct NamedResource: Freddy.JSONDecodable { + + let name: String + + init(name: String) { + self.name = name + } + + init(json: JSON) throws { + self.name = try json.string("name") + } + +} + +/// Represents an image resource provided by its URL +struct ImageResource: JSONDecodable { + + let url: NSURL + + init(json: JSON) throws { + guard let url = NSURL(string: try String(json: json)) else { + throw DecodeError.unexpectedValue(json, expected: "URL") + } + self.url = url + } +} + +struct Language: JSONDecodable { + + let name: String + + init(json: JSON) throws { + self.name = try json.decode("name") + } + +} + + +// MARK: - Decoding Utility + +enum DecodeError: ErrorType { + case unexpectedValue(JSON, expected: String) + case emptyLanguageList +} + +extension JSON { + + /// From a list of JSON objects at `path`, that each contains an associated `language` (given by a `NamedResource` representation), select the one most appropriate for the current device locale. + func localized(path: JSONPathType) throws -> JSON { + let preferredLanguages = NSLocale.preferredLanguages() + guard let localized = try self.array(path).sort({ lhs, rhs in + guard let lhsLanguage: NamedResource = try? lhs.decode("language"), lhsPriority = preferredLanguages.indexOf({ $0.hasPrefix(lhsLanguage.name) }) else { + return false + } + guard let rhsLanguage: NamedResource = try? rhs.decode("language"), rhsPriority = preferredLanguages.indexOf({ $0.hasPrefix(rhsLanguage.name) }) else { + return true + } + return lhsPriority < rhsPriority + }).first else { + throw DecodeError.emptyLanguageList + } + return localized + } + +} + diff --git a/APIClient/Pokedex.swift b/APIClient/Pokedex.swift new file mode 100644 index 0000000..e63c716 --- /dev/null +++ b/APIClient/Pokedex.swift @@ -0,0 +1,40 @@ +// +// Pokedex.swift +// APIClient +// +// Created by Nils Fischer on 20.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import Foundation +import Freddy + + +/// A Pokedex contains the Pokemon species encountered in a specific region of the Pokemon world +struct Pokedex: JSONDecodable { + + /// The name of the resource, such as "kanto" + let name: String + /// The localized name of the Pokedex, containing the name of the region such as "Kanto" + let localizedName: String + /// The numbered entries of the Pokedex + let entries: [Entry] + + struct Entry: JSONDecodable { + + let number: Int + let pokemonSpecies: NamedResource + + init(json: JSON) throws { + self.number = try json.int("entry_number") + self.pokemonSpecies = try json.decode("pokemon_species") + } + } + + init(json: JSON) throws { + self.name = try json.string("name") + self.localizedName = try json.localized("names").string("name") + self.entries = try json.arrayOf("pokemon_entries") + } + +} diff --git a/APIClient/PokedexEntriesViewController.swift b/APIClient/PokedexEntriesViewController.swift new file mode 100644 index 0000000..b24e691 --- /dev/null +++ b/APIClient/PokedexEntriesViewController.swift @@ -0,0 +1,99 @@ +// +// PokedexEntriesViewController.swift +// APIClient +// +// Created by Nils Fischer on 22.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit +import Moya + +class PokedexEntriesViewController: UITableViewController { + + /// The Poke API provider that handles requests for server resources + var pokeAPI: MoyaProvider! + + /// The Pokemon species whose Pokedex entries to show here + var pokemonSpecies: PokemonSpecies! { + didSet { + self.title = pokemonSpecies.localizedName + self.pokedexes = pokemonSpecies.pokedexEntries.map { entry in + return .notLoaded(.pokedex(entry.pokedex)) + } + } + } + /// Holds the resource for each row of the table view that may or may not be loaded yet + var pokedexes: [APIResource]! + + + // MARK: User Interaction + + override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { + switch identifier { + case "showPokedex": + if let indexPath = tableView.indexPathForSelectedRow, case .loaded = pokedexes[indexPath.row] { + return true + } else { + return false + } + default: + return true + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + switch segue.identifier! { + case "showPokedex": + guard let pokedexViewController = segue.destinationViewController as? PokedexViewController else { + return + } + guard let indexPath = tableView.indexPathForSelectedRow, case .loaded(let pokedex) = pokedexes[indexPath.row] else { + return + } + pokedexViewController.pokeAPI = pokeAPI + pokedexViewController.pokedex = pokedex + default: + break + } + } + +} + + +// MARK: - Table View Datasource + +extension PokedexEntriesViewController { + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 1 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return pokemonSpecies.pokedexEntries.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + + // Load the resource for this row if necessary + if case .notLoaded(let target) = pokedexes[indexPath.row] { + pokedexes[indexPath.row] = pokeAPI.request(target) { result in + self.pokedexes[indexPath.row] = result + tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) + } + } + + // Obtain a cell and configure it + let cell = tableView.dequeueReusableCellWithIdentifier("PokedexEntryCell", forIndexPath: indexPath) as! PokedexEntryCell + cell.configureForEntry(pokemonSpecies.pokedexEntries[indexPath.row], pokedex: pokedexes[indexPath.row]) + if case .loaded = pokedexes[indexPath.row] { + cell.selectionStyle = .Default + cell.accessoryType = .DisclosureIndicator + } else { + cell.selectionStyle = .None + cell.accessoryType = .None + } + return cell + } + +} diff --git a/APIClient/PokedexEntryCell.swift b/APIClient/PokedexEntryCell.swift new file mode 100644 index 0000000..54faf64 --- /dev/null +++ b/APIClient/PokedexEntryCell.swift @@ -0,0 +1,41 @@ +// +// PokedexCell.swift +// APIClient +// +// Created by Nils Fischer on 22.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit + +class PokedexEntryCell: UITableViewCell { + + @IBOutlet var numberLabel: UILabel! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var entriesCountLabel: UILabel! + @IBOutlet var loadingIndicator: UIActivityIndicatorView! + + func configureForEntry(entry: PokemonSpecies.PokedexEntry, pokedex: APIResource) { + numberLabel.text = String(entry.number) + switch pokedex { + case .loaded(let pokedex): + loadingIndicator.stopAnimating() + nameLabel.text = pokedex.localizedName + entriesCountLabel.hidden = false + entriesCountLabel.text = String(pokedex.entries.count) + case .loading: + loadingIndicator.startAnimating() + nameLabel.text = nil + entriesCountLabel.hidden = true + case .notLoaded: + loadingIndicator.stopAnimating() + nameLabel.text = nil + entriesCountLabel.hidden = true + case .failed(let error): + loadingIndicator.stopAnimating() + nameLabel.text = String(error) + entriesCountLabel.hidden = true + } + } + +} diff --git a/APIClient/PokedexViewController.swift b/APIClient/PokedexViewController.swift new file mode 100644 index 0000000..e36ba6e --- /dev/null +++ b/APIClient/PokedexViewController.swift @@ -0,0 +1,133 @@ +// +// ViewController.swift +// APIClient +// +// Created by Nils Fischer on 20.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit +import Moya + + +class PokedexViewController: UITableViewController, PokemonSpeciesProvider { + + /// The Poke API provider that handles requests for server resources + var pokeAPI: MoyaProvider! + + /// The Pokedex to show here + var pokedex: Pokedex! { + didSet { + // Obtain the entries of the Pokedex and prepare the `pokemonSpecies` and `sprites` arrays to hold loaded resources + self.pokemonSpecies = pokedex.entries.map({ .notLoaded(.pokemonSpecies($0.pokemonSpecies)) }) + self.sprites = pokedex.entries.map({ _ in nil }) + // Configure the view + self.title = pokedex.localizedName + self.tableView.reloadData() + } + } + /// Holds the resource for each row of the table view that may or may not be loaded yet + private var pokemonSpecies: [APIResource] = [] + /// Holds the image information for each row of the table view that may or may not be loaded yet + private var sprites: [APIResource?] = [] + + /// Holds the selected Pokemon species to provide to segue destination + var providedPokemonSpecies: PokemonSpecies? + + + // MARK: Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 44 + } + + + // MARK: User Interaction + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + // Retry to load a resource if it failed before + if case .failed(let target, _) = pokemonSpecies[indexPath.row] { + self.pokemonSpecies[indexPath.row] = .notLoaded(target) + tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) + } + if let sprite = sprites[indexPath.row], case .failed(let target, _) = sprite { + self.sprites[indexPath.row] = .notLoaded(target) + tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) + } + } + + override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { + switch identifier { + case "showPokemonSpecies": + if let indexPath = tableView.indexPathForSelectedRow, case .loaded = pokemonSpecies[indexPath.row] { + return true + } else { + return false + } + default: + return true + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + switch segue.identifier! { + case "showPokemonSpecies": + guard let indexPath = tableView.indexPathForSelectedRow, case .loaded(let pokemonSpecies) = pokemonSpecies[indexPath.row] else { + return + } + self.providedPokemonSpecies = pokemonSpecies + default: + break + } + } +} + + +// MARK: - Table View Datasource + +extension PokedexViewController { + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 1 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return pokemonSpecies.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + + // Load the resource for this row if necessary + if case .notLoaded(let target) = pokemonSpecies[indexPath.row] { + pokemonSpecies[indexPath.row] = pokeAPI.request(target) { result in + self.pokemonSpecies[indexPath.row] = result + // Obtain the image information from a loaded resource to be loaded subsequentially + if case .loaded(let pokemonSpecies) = result { + self.sprites[indexPath.row] = pokemonSpecies.varieties.first.flatMap({ .notLoaded(.pokemon($0)) }) + } + tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) + } + } + + // Load the image information for this row if necessary + if let sprite = sprites[indexPath.row], case .notLoaded(let target) = sprite { + sprites[indexPath.row] = pokeAPI.request(target) { result in + self.sprites[indexPath.row] = result + tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) + } + } + + // Obtain a cell and configure it + let cell = tableView.dequeueReusableCellWithIdentifier("PokemonSpeciesCell", forIndexPath: indexPath) as! PokemonSpeciesCell + cell.configureForEntry(pokedex.entries[indexPath.row], pokemonSpecies: pokemonSpecies[indexPath.row], sprite: sprites[indexPath.row]) + if case .loaded = pokemonSpecies[indexPath.row] { + cell.selectionStyle = .Default + } else { + cell.selectionStyle = .None + } + return cell + } + +} diff --git a/APIClient/Pokemon.swift b/APIClient/Pokemon.swift new file mode 100644 index 0000000..890fa19 --- /dev/null +++ b/APIClient/Pokemon.swift @@ -0,0 +1,72 @@ +// +// Pokemon.swift +// APIClient +// +// Created by Nils Fischer on 20.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import Foundation +import Freddy +import UIKit + + +/// A Pokemon species such as "Bulbasaur" +struct PokemonSpecies: JSONDecodable { + + /// The name of the resource, such as "bulbasaur" + let name: String + /// The localized name of the Pokemon species, such as "Bisasam" + let localizedName: String + /// A short descriptive text about the Pokemon species + let flavorText: String? + /// The varieties this Pokemon species can occur in + let varieties: [NamedResource] + /// The Pokedex Entries associated to this Pokemon + let pokedexEntries: [PokedexEntry] + + struct PokedexEntry: JSONDecodable { + + let number: Int + let pokedex: NamedResource + + init(json: JSON) throws { + self.number = try json.int("entry_number") + self.pokedex = try json.decode("pokedex") + } + + } + + init(json: JSON) throws { + self.name = try json.string("name") + self.localizedName = try json.localized("names").string("name") + self.flavorText = try json.localized("flavor_text_entries").string("flavor_text").stringByReplacingOccurrencesOfString("\n", withString: " ") + self.varieties = try json.array("varieties").map({ try $0.decode("pokemon") }) + self.pokedexEntries = try json.arrayOf("pokedex_numbers") + } + +} + +/// A specific Pokemon variety of a species +struct Pokemon: JSONDecodable { + + /// The visual depictions of this Pokemon + let sprites: PokemonSprites + + init(json: JSON) throws { + self.sprites = try json.decode("sprites") + } + +} + + +/// The visual depictions of a Pokemon +struct PokemonSprites: JSONDecodable { + + let frontDefault: ImageResource + + init(json: JSON) throws { + self.frontDefault = try json.decode("front_default") + } + +} diff --git a/APIClient/PokemonSpeciesCell.swift b/APIClient/PokemonSpeciesCell.swift new file mode 100644 index 0000000..de5da0f --- /dev/null +++ b/APIClient/PokemonSpeciesCell.swift @@ -0,0 +1,66 @@ +// +// PokemonSpeciesCell.swift +// APIClient +// +// Created by Nils Fischer on 20.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit +import AlamofireImage + + +class PokemonSpeciesCell: UITableViewCell { + + @IBOutlet var numberLabel: UILabel! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var flavorTextLabel: UILabel! + @IBOutlet var spriteImageview: UIImageView! + @IBOutlet var loadingIndicator: UIActivityIndicatorView! + + func configureForEntry(entry: Pokedex.Entry, pokemonSpecies: APIResource, sprite: APIResource?) { + + numberLabel.text = String(entry.number) + + // The resource may or may not be loaded, so configure the content accordingly + switch pokemonSpecies { + + case .loaded(let pokemonSpecies): + loadingIndicator.stopAnimating() + nameLabel.text = pokemonSpecies.localizedName + flavorTextLabel.text = pokemonSpecies.flavorText + + case .loading: + loadingIndicator.startAnimating() + nameLabel.text = nil + flavorTextLabel.text = nil + + case .failed(_, let error): + loadingIndicator.stopAnimating() + nameLabel.text = String(error) + flavorTextLabel.text = nil + + case .notLoaded: + loadingIndicator.stopAnimating() + nameLabel.text = nil + flavorTextLabel.text = nil + + } + + // Try to obtain an image for the pokemon + if let sprite = sprite { + switch sprite { + case .loaded(let sprite): + spriteImageview.image = nil + spriteImageview.hidden = false + // Use `AlamofireImage` to load the image from the URL and display it in the image view + spriteImageview.af_setImageWithURL(sprite.sprites.frontDefault.url) + default: + spriteImageview.hidden = true + } + } else { + spriteImageview.hidden = true + } + } + +} diff --git a/APIClient/PokemonViewController.swift b/APIClient/PokemonViewController.swift new file mode 100644 index 0000000..56dbaf0 --- /dev/null +++ b/APIClient/PokemonViewController.swift @@ -0,0 +1,166 @@ +// +// PokemonViewController.swift +// APIClient +// +// Created by Nils Fischer on 22.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit +import Freddy +import Moya +import AlamofireImage + + +class PokemonViewController: UIViewController { + + /// The Poke API provider that handles requests for server resources + var pokeAPI: MoyaProvider! + + /// The Pokemon species to show here + var pokemonSpecies: PokemonSpecies? { + didSet { + // Configure view + self.searchTextfield.text = pokemonSpecies?.name + self.nameLabel.text = pokemonSpecies?.localizedName + self.flavorTextLabel.text = pokemonSpecies?.flavorText + self.showPokedexEntriesButton.hidden = pokemonSpecies == nil + self.sprite = nil + if let sprite = pokemonSpecies?.varieties.first { + self.loadSprite(sprite) + } + } + } + /// The image information for the pokemon species if loaded + private var sprite: Pokemon? { + didSet { + if let spriteURL = sprite?.sprites.frontDefault.url { + self.spriteImageview.hidden = true + // Use `AlamofireImage` to load the image from the URL and display it in the image view + self.spriteImageview.af_setImageWithURL(spriteURL) { response in + if case .Success = response.result { + self.spriteImageview.hidden = false + } + } + } else { + self.spriteImageview.hidden = true + } + } + } + + + // MARK: Interface Elements + + @IBOutlet var searchTextfield: UITextField! + @IBOutlet var loadingIndicator: UIActivityIndicatorView! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var spriteImageview: UIImageView! + @IBOutlet var spriteLoadingIndicator: UIActivityIndicatorView! + @IBOutlet var flavorTextLabel: UILabel! + @IBOutlet var showPokedexEntriesButton: UIButton! + + + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + self.pokemonSpecies = nil + } + + + // MARK: Loading Resources + + func loadPokemonSpecies(pokemonSpecies: NamedResource) { + loadingIndicator.startAnimating() + pokeAPI.request(.pokemonSpecies(pokemonSpecies)) { result in + self.loadingIndicator.stopAnimating() + switch result { + case .Success(let response): + do { + try response.filterSuccessfulStatusCodes() + // Try to parse the response to JSON + let json = try JSON(data: response.data) + // Try to decode the JSON to the required type + let pokemonSpecies = try PokemonSpecies(json: json) + // Configure view according to model + self.pokemonSpecies = pokemonSpecies + } catch { + print(error) + } + case .Failure(let error): + print(error) + } + } + } + + func loadSprite(pokemon: NamedResource) { + spriteLoadingIndicator.startAnimating() + pokeAPI.request(.pokemon(pokemon)) { result in + self.spriteLoadingIndicator.stopAnimating() + switch result { + case .Success(let response): + do { + try response.filterSuccessfulStatusCodes() + // Try to parse the response to JSON + let json = try JSON(data: response.data) + // Try to decode the JSON to the required type + let pokemon = try Pokemon(json: json) + // Configure view according to model + self.sprite = pokemon + } catch { + print(error) + } + case .Failure(let error): + print(error) + } + } + } + + + // MARK: User Interaction + + func textFieldShouldReturn(textField: UITextField) -> Bool { + guard let name = textField.text else { + return true + } + self.loadPokemonSpecies(NamedResource(name: name)) + return true + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + switch segue.identifier! { + case "showPokedexEntry": + guard let pokedexEntriesViewController = (segue.destinationViewController as? UINavigationController)?.topViewController as? PokedexEntriesViewController else { + return + } + guard let pokemonSpecies = self.pokemonSpecies else { + return + } + pokedexEntriesViewController.pokeAPI = pokeAPI + pokedexEntriesViewController.pokemonSpecies = pokemonSpecies + default: + break + } + } + + @IBAction func unwindToPokemon(segue: UIStoryboardSegue) { + switch segue.identifier! { + case "showPokemonSpecies": + guard let pokemonSpeciesProvider = segue.sourceViewController as? PokemonSpeciesProvider else { + return + } + self.pokemonSpecies = pokemonSpeciesProvider.providedPokemonSpecies + default: + break + } + } +} + + +// MARK: - Pokemon Species Provider + +protocol PokemonSpeciesProvider { + + var providedPokemonSpecies: PokemonSpecies? { get } + +} From 332c1b83d3ab8c812280fb44d6b96628472d1bcd Mon Sep 17 00:00:00 2001 From: felix Date: Mon, 30 May 2016 11:49:46 +0200 Subject: [PATCH 2/6] Fast_fertig: nur Problem bei PerformRequest --- APIClient.xcodeproj/project.pbxproj | 48 +-- APIClient/APIResource.swift | 4 +- APIClient/AppDelegate.swift | 10 +- APIClient/Base.lproj/Main.storyboard | 306 +++--------------- APIClient/PokeAPI.swift | 124 ------- APIClient/Pokedex.swift | 40 --- APIClient/PokedexEntriesViewController.swift | 99 ------ APIClient/PokedexEntryCell.swift | 41 --- APIClient/PokedexViewController.swift | 133 -------- APIClient/Pokemon.swift | 72 ----- APIClient/PokemonSpeciesCell.swift | 66 ---- APIClient/PokemonViewController.swift | 166 ---------- APIClient/SWAPI.swift | 70 ++++ APIClient/SWView.swift | 16 + APIClient/SWViewController.swift | 68 ++++ APIClient/StarWars.swift | 33 ++ Pods/Pods.xcodeproj/project.pbxproj | 14 +- .../Target Support Files/Alamofire/Info.plist | 40 +-- .../AlamofireImage/Info.plist | 40 +-- .../AwesomeCache/Info.plist | 40 +-- Pods/Target Support Files/Freddy/Info.plist | 40 +-- Pods/Target Support Files/Moya/Info.plist | 40 +-- Pods/Target Support Files/Result/Info.plist | 40 +-- 23 files changed, 379 insertions(+), 1171 deletions(-) delete mode 100644 APIClient/PokeAPI.swift delete mode 100644 APIClient/Pokedex.swift delete mode 100644 APIClient/PokedexEntriesViewController.swift delete mode 100644 APIClient/PokedexEntryCell.swift delete mode 100644 APIClient/PokedexViewController.swift delete mode 100644 APIClient/Pokemon.swift delete mode 100644 APIClient/PokemonSpeciesCell.swift delete mode 100644 APIClient/PokemonViewController.swift create mode 100644 APIClient/SWAPI.swift create mode 100644 APIClient/SWView.swift create mode 100644 APIClient/SWViewController.swift create mode 100644 APIClient/StarWars.swift diff --git a/APIClient.xcodeproj/project.pbxproj b/APIClient.xcodeproj/project.pbxproj index 633cbe8..5c58a56 100644 --- a/APIClient.xcodeproj/project.pbxproj +++ b/APIClient.xcodeproj/project.pbxproj @@ -8,39 +8,31 @@ /* Begin PBXBuildFile section */ 872A275C1CF0D87100A988C4 /* APIResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872A275B1CF0D87100A988C4 /* APIResource.swift */; }; - 8744AA581CF2199F001B0470 /* PokemonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA571CF2199F001B0470 /* PokemonViewController.swift */; }; - 8744AA5A1CF22716001B0470 /* PokedexEntriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */; }; - 8744AA5C1CF22955001B0470 /* PokedexEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */; }; + 8744AA581CF2199F001B0470 /* SWViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744AA571CF2199F001B0470 /* SWViewController.swift */; }; 874D06591CEF295E009A494D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D06581CEF295E009A494D /* AppDelegate.swift */; }; - 874D065B1CEF295E009A494D /* PokedexViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D065A1CEF295E009A494D /* PokedexViewController.swift */; }; 874D065E1CEF295E009A494D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 874D065C1CEF295E009A494D /* Main.storyboard */; }; 874D06601CEF295E009A494D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 874D065F1CEF295E009A494D /* Assets.xcassets */; }; 874D06631CEF295E009A494D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 874D06611CEF295E009A494D /* LaunchScreen.storyboard */; }; - 874D066B1CEF2BC0009A494D /* PokeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066A1CEF2BC0009A494D /* PokeAPI.swift */; }; - 874D066D1CEF2F49009A494D /* Pokemon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066C1CEF2F49009A494D /* Pokemon.swift */; }; - 874D066F1CEF372D009A494D /* Pokedex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066E1CEF372D009A494D /* Pokedex.swift */; }; - 874D06711CEF6877009A494D /* PokemonSpeciesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */; }; + 874D066B1CEF2BC0009A494D /* SWAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874D066A1CEF2BC0009A494D /* SWAPI.swift */; }; A526FDEE5BF2D1BC3C97B9FA /* Pods_APIClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD8A5B3E1266D5298CABE44A /* Pods_APIClient.framework */; }; + DCE5C8C61CFB621C0025C611 /* StarWars.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE5C8C51CFB621C0025C611 /* StarWars.swift */; }; + DCE5C8C81CFB66320025C611 /* SWView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE5C8C71CFB66320025C611 /* SWView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 872A275B1CF0D87100A988C4 /* APIResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIResource.swift; sourceTree = ""; }; - 8744AA571CF2199F001B0470 /* PokemonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonViewController.swift; sourceTree = ""; }; - 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokedexEntriesViewController.swift; sourceTree = ""; }; - 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokedexEntryCell.swift; sourceTree = ""; }; + 8744AA571CF2199F001B0470 /* SWViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWViewController.swift; sourceTree = ""; }; 874D06551CEF295E009A494D /* APIClient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = APIClient.app; sourceTree = BUILT_PRODUCTS_DIR; }; 874D06581CEF295E009A494D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 874D065A1CEF295E009A494D /* PokedexViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PokedexViewController.swift; sourceTree = ""; }; 874D065D1CEF295E009A494D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 874D065F1CEF295E009A494D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 874D06621CEF295E009A494D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 874D06641CEF295E009A494D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 874D066A1CEF2BC0009A494D /* PokeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokeAPI.swift; sourceTree = ""; }; - 874D066C1CEF2F49009A494D /* Pokemon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pokemon.swift; sourceTree = ""; }; - 874D066E1CEF372D009A494D /* Pokedex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pokedex.swift; sourceTree = ""; }; - 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PokemonSpeciesCell.swift; sourceTree = ""; }; + 874D066A1CEF2BC0009A494D /* SWAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWAPI.swift; sourceTree = ""; }; 93EFAB13AE62F1B6FB2683B2 /* Pods-APIClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIClient.debug.xcconfig"; path = "Pods/Target Support Files/Pods-APIClient/Pods-APIClient.debug.xcconfig"; sourceTree = ""; }; BD8A5B3E1266D5298CABE44A /* Pods_APIClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DCE5C8C51CFB621C0025C611 /* StarWars.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StarWars.swift; sourceTree = ""; }; + DCE5C8C71CFB66320025C611 /* SWView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SWView.swift; sourceTree = ""; }; FFE13D543A95D28F27CFBDA4 /* Pods-APIClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIClient.release.xcconfig"; path = "Pods/Target Support Files/Pods-APIClient/Pods-APIClient.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -59,9 +51,7 @@ 872A27571CF0D4E200A988C4 /* View Controller */ = { isa = PBXGroup; children = ( - 8744AA571CF2199F001B0470 /* PokemonViewController.swift */, - 8744AA591CF22716001B0470 /* PokedexEntriesViewController.swift */, - 874D065A1CEF295E009A494D /* PokedexViewController.swift */, + 8744AA571CF2199F001B0470 /* SWViewController.swift */, ); name = "View Controller"; sourceTree = ""; @@ -69,8 +59,7 @@ 872A27581CF0D4EB00A988C4 /* View */ = { isa = PBXGroup; children = ( - 8744AA5B1CF22955001B0470 /* PokedexEntryCell.swift */, - 874D06701CEF6877009A494D /* PokemonSpeciesCell.swift */, + DCE5C8C71CFB66320025C611 /* SWView.swift */, ); name = View; sourceTree = ""; @@ -78,10 +67,9 @@ 872A27591CF0D64A00A988C4 /* Model */ = { isa = PBXGroup; children = ( - 874D066A1CEF2BC0009A494D /* PokeAPI.swift */, + 874D066A1CEF2BC0009A494D /* SWAPI.swift */, + DCE5C8C51CFB621C0025C611 /* StarWars.swift */, 872A275B1CF0D87100A988C4 /* APIResource.swift */, - 874D066E1CEF372D009A494D /* Pokedex.swift */, - 874D066C1CEF2F49009A494D /* Pokemon.swift */, ); name = Model; sourceTree = ""; @@ -266,16 +254,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 874D06711CEF6877009A494D /* PokemonSpeciesCell.swift in Sources */, - 874D066B1CEF2BC0009A494D /* PokeAPI.swift in Sources */, - 8744AA5A1CF22716001B0470 /* PokedexEntriesViewController.swift in Sources */, - 874D065B1CEF295E009A494D /* PokedexViewController.swift in Sources */, - 8744AA5C1CF22955001B0470 /* PokedexEntryCell.swift in Sources */, - 874D066F1CEF372D009A494D /* Pokedex.swift in Sources */, + 874D066B1CEF2BC0009A494D /* SWAPI.swift in Sources */, 872A275C1CF0D87100A988C4 /* APIResource.swift in Sources */, 874D06591CEF295E009A494D /* AppDelegate.swift in Sources */, - 8744AA581CF2199F001B0470 /* PokemonViewController.swift in Sources */, - 874D066D1CEF2F49009A494D /* Pokemon.swift in Sources */, + 8744AA581CF2199F001B0470 /* SWViewController.swift in Sources */, + DCE5C8C81CFB66320025C611 /* SWView.swift in Sources */, + DCE5C8C61CFB621C0025C611 /* StarWars.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/APIClient/APIResource.swift b/APIClient/APIResource.swift index 6bb455f..fa7ce9c 100644 --- a/APIClient/APIResource.swift +++ b/APIClient/APIResource.swift @@ -24,7 +24,7 @@ import AwesomeCache You are free to use this utility or implement your own way to keep track of loaded and not loaded resources. - seealso: MoyaProvider.request(_:completion:) -*/ + */ enum APIResource { case notLoaded(Target) @@ -91,4 +91,4 @@ protocol Cacheable { /// A key used for identification in a cache var cacheIdentifier: String { get } -} +} \ No newline at end of file diff --git a/APIClient/AppDelegate.swift b/APIClient/AppDelegate.swift index b3684a6..76fb22f 100644 --- a/APIClient/AppDelegate.swift +++ b/APIClient/AppDelegate.swift @@ -14,16 +14,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - /// The PokeAPI Provider representing the Server - let pokeAPI = MoyaProvider() + /// The SWAPI Provider representing the Server + let swAPI = MoyaProvider() func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - if let pokemonViewController = (window?.rootViewController as? UINavigationController)?.topViewController as? PokemonViewController { + if let starwarsViewController = (window?.rootViewController as? UINavigationController)?.topViewController as? SWViewController { - // Pass the PokeAPI Provider on to the root view controller - pokemonViewController.pokeAPI = pokeAPI + // Pass the SWAPI Provider on to the root view controller + starwarsViewController.swAPI = swAPI } diff --git a/APIClient/Base.lproj/Main.storyboard b/APIClient/Base.lproj/Main.storyboard index 3174287..bbf8afd 100644 --- a/APIClient/Base.lproj/Main.storyboard +++ b/APIClient/Base.lproj/Main.storyboard @@ -1,307 +1,73 @@ - + - - - + + - - + + - + - - + + - - - - - - - - - + + - - - - - - - - - - - - - + - - + + - - - - - - - + - - + - + - + - - + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/APIClient/PokeAPI.swift b/APIClient/PokeAPI.swift deleted file mode 100644 index 364bae8..0000000 --- a/APIClient/PokeAPI.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// PokeAPI.swift -// APIClient -// -// Created by Nils Fischer on 20.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import Foundation -import Moya -import Freddy - - -/// The abstraction of the [Poke API](http://pokeapi.co) REST API. -enum PokeAPI: Moya.TargetType, Cacheable { - - - /// MARK: Endpoints - - case pokedex(NamedResource) - case pokemonSpecies(NamedResource) - case pokemon(NamedResource) - - - // MARK: Network Abstraction - - var baseURL: NSURL { return NSURL(string: "http://pokeapi.co/api/v2")! } - - var path: String { - switch self { - case .pokedex(let namedResource): return "/pokedex/\(namedResource.name)" - case .pokemonSpecies(let namedResource): return "/pokemon-species/\(namedResource.name)" - case .pokemon(let namedResource): return "/pokemon/\(namedResource.name)" - } - } - - var method: Moya.Method { return .GET } - - var parameters: [String : AnyObject]? { - switch self { - default: return nil - } - } - - // TODO: Provide sample data for testing - var sampleData: NSData { - switch self { - default: return "".dataUsingEncoding(NSUTF8StringEncoding)! - } - } - - var cacheIdentifier: String { - return self.path - } -} - - -/// Represents a resource provided by the Poke API by its name -struct NamedResource: Freddy.JSONDecodable { - - let name: String - - init(name: String) { - self.name = name - } - - init(json: JSON) throws { - self.name = try json.string("name") - } - -} - -/// Represents an image resource provided by its URL -struct ImageResource: JSONDecodable { - - let url: NSURL - - init(json: JSON) throws { - guard let url = NSURL(string: try String(json: json)) else { - throw DecodeError.unexpectedValue(json, expected: "URL") - } - self.url = url - } -} - -struct Language: JSONDecodable { - - let name: String - - init(json: JSON) throws { - self.name = try json.decode("name") - } - -} - - -// MARK: - Decoding Utility - -enum DecodeError: ErrorType { - case unexpectedValue(JSON, expected: String) - case emptyLanguageList -} - -extension JSON { - - /// From a list of JSON objects at `path`, that each contains an associated `language` (given by a `NamedResource` representation), select the one most appropriate for the current device locale. - func localized(path: JSONPathType) throws -> JSON { - let preferredLanguages = NSLocale.preferredLanguages() - guard let localized = try self.array(path).sort({ lhs, rhs in - guard let lhsLanguage: NamedResource = try? lhs.decode("language"), lhsPriority = preferredLanguages.indexOf({ $0.hasPrefix(lhsLanguage.name) }) else { - return false - } - guard let rhsLanguage: NamedResource = try? rhs.decode("language"), rhsPriority = preferredLanguages.indexOf({ $0.hasPrefix(rhsLanguage.name) }) else { - return true - } - return lhsPriority < rhsPriority - }).first else { - throw DecodeError.emptyLanguageList - } - return localized - } - -} - diff --git a/APIClient/Pokedex.swift b/APIClient/Pokedex.swift deleted file mode 100644 index e63c716..0000000 --- a/APIClient/Pokedex.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Pokedex.swift -// APIClient -// -// Created by Nils Fischer on 20.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import Foundation -import Freddy - - -/// A Pokedex contains the Pokemon species encountered in a specific region of the Pokemon world -struct Pokedex: JSONDecodable { - - /// The name of the resource, such as "kanto" - let name: String - /// The localized name of the Pokedex, containing the name of the region such as "Kanto" - let localizedName: String - /// The numbered entries of the Pokedex - let entries: [Entry] - - struct Entry: JSONDecodable { - - let number: Int - let pokemonSpecies: NamedResource - - init(json: JSON) throws { - self.number = try json.int("entry_number") - self.pokemonSpecies = try json.decode("pokemon_species") - } - } - - init(json: JSON) throws { - self.name = try json.string("name") - self.localizedName = try json.localized("names").string("name") - self.entries = try json.arrayOf("pokemon_entries") - } - -} diff --git a/APIClient/PokedexEntriesViewController.swift b/APIClient/PokedexEntriesViewController.swift deleted file mode 100644 index b24e691..0000000 --- a/APIClient/PokedexEntriesViewController.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// PokedexEntriesViewController.swift -// APIClient -// -// Created by Nils Fischer on 22.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import UIKit -import Moya - -class PokedexEntriesViewController: UITableViewController { - - /// The Poke API provider that handles requests for server resources - var pokeAPI: MoyaProvider! - - /// The Pokemon species whose Pokedex entries to show here - var pokemonSpecies: PokemonSpecies! { - didSet { - self.title = pokemonSpecies.localizedName - self.pokedexes = pokemonSpecies.pokedexEntries.map { entry in - return .notLoaded(.pokedex(entry.pokedex)) - } - } - } - /// Holds the resource for each row of the table view that may or may not be loaded yet - var pokedexes: [APIResource]! - - - // MARK: User Interaction - - override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { - switch identifier { - case "showPokedex": - if let indexPath = tableView.indexPathForSelectedRow, case .loaded = pokedexes[indexPath.row] { - return true - } else { - return false - } - default: - return true - } - } - - override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { - switch segue.identifier! { - case "showPokedex": - guard let pokedexViewController = segue.destinationViewController as? PokedexViewController else { - return - } - guard let indexPath = tableView.indexPathForSelectedRow, case .loaded(let pokedex) = pokedexes[indexPath.row] else { - return - } - pokedexViewController.pokeAPI = pokeAPI - pokedexViewController.pokedex = pokedex - default: - break - } - } - -} - - -// MARK: - Table View Datasource - -extension PokedexEntriesViewController { - - override func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return 1 - } - - override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return pokemonSpecies.pokedexEntries.count - } - - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - - // Load the resource for this row if necessary - if case .notLoaded(let target) = pokedexes[indexPath.row] { - pokedexes[indexPath.row] = pokeAPI.request(target) { result in - self.pokedexes[indexPath.row] = result - tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) - } - } - - // Obtain a cell and configure it - let cell = tableView.dequeueReusableCellWithIdentifier("PokedexEntryCell", forIndexPath: indexPath) as! PokedexEntryCell - cell.configureForEntry(pokemonSpecies.pokedexEntries[indexPath.row], pokedex: pokedexes[indexPath.row]) - if case .loaded = pokedexes[indexPath.row] { - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - } else { - cell.selectionStyle = .None - cell.accessoryType = .None - } - return cell - } - -} diff --git a/APIClient/PokedexEntryCell.swift b/APIClient/PokedexEntryCell.swift deleted file mode 100644 index 54faf64..0000000 --- a/APIClient/PokedexEntryCell.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PokedexCell.swift -// APIClient -// -// Created by Nils Fischer on 22.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import UIKit - -class PokedexEntryCell: UITableViewCell { - - @IBOutlet var numberLabel: UILabel! - @IBOutlet var nameLabel: UILabel! - @IBOutlet var entriesCountLabel: UILabel! - @IBOutlet var loadingIndicator: UIActivityIndicatorView! - - func configureForEntry(entry: PokemonSpecies.PokedexEntry, pokedex: APIResource) { - numberLabel.text = String(entry.number) - switch pokedex { - case .loaded(let pokedex): - loadingIndicator.stopAnimating() - nameLabel.text = pokedex.localizedName - entriesCountLabel.hidden = false - entriesCountLabel.text = String(pokedex.entries.count) - case .loading: - loadingIndicator.startAnimating() - nameLabel.text = nil - entriesCountLabel.hidden = true - case .notLoaded: - loadingIndicator.stopAnimating() - nameLabel.text = nil - entriesCountLabel.hidden = true - case .failed(let error): - loadingIndicator.stopAnimating() - nameLabel.text = String(error) - entriesCountLabel.hidden = true - } - } - -} diff --git a/APIClient/PokedexViewController.swift b/APIClient/PokedexViewController.swift deleted file mode 100644 index e36ba6e..0000000 --- a/APIClient/PokedexViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// ViewController.swift -// APIClient -// -// Created by Nils Fischer on 20.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import UIKit -import Moya - - -class PokedexViewController: UITableViewController, PokemonSpeciesProvider { - - /// The Poke API provider that handles requests for server resources - var pokeAPI: MoyaProvider! - - /// The Pokedex to show here - var pokedex: Pokedex! { - didSet { - // Obtain the entries of the Pokedex and prepare the `pokemonSpecies` and `sprites` arrays to hold loaded resources - self.pokemonSpecies = pokedex.entries.map({ .notLoaded(.pokemonSpecies($0.pokemonSpecies)) }) - self.sprites = pokedex.entries.map({ _ in nil }) - // Configure the view - self.title = pokedex.localizedName - self.tableView.reloadData() - } - } - /// Holds the resource for each row of the table view that may or may not be loaded yet - private var pokemonSpecies: [APIResource] = [] - /// Holds the image information for each row of the table view that may or may not be loaded yet - private var sprites: [APIResource?] = [] - - /// Holds the selected Pokemon species to provide to segue destination - var providedPokemonSpecies: PokemonSpecies? - - - // MARK: Lifecycle - - override func awakeFromNib() { - super.awakeFromNib() - tableView.rowHeight = UITableViewAutomaticDimension - tableView.estimatedRowHeight = 44 - } - - - // MARK: User Interaction - - override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { - // Retry to load a resource if it failed before - if case .failed(let target, _) = pokemonSpecies[indexPath.row] { - self.pokemonSpecies[indexPath.row] = .notLoaded(target) - tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) - } - if let sprite = sprites[indexPath.row], case .failed(let target, _) = sprite { - self.sprites[indexPath.row] = .notLoaded(target) - tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) - } - } - - override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool { - switch identifier { - case "showPokemonSpecies": - if let indexPath = tableView.indexPathForSelectedRow, case .loaded = pokemonSpecies[indexPath.row] { - return true - } else { - return false - } - default: - return true - } - } - - override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { - switch segue.identifier! { - case "showPokemonSpecies": - guard let indexPath = tableView.indexPathForSelectedRow, case .loaded(let pokemonSpecies) = pokemonSpecies[indexPath.row] else { - return - } - self.providedPokemonSpecies = pokemonSpecies - default: - break - } - } -} - - -// MARK: - Table View Datasource - -extension PokedexViewController { - - override func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return 1 - } - - override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return pokemonSpecies.count - } - - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - - // Load the resource for this row if necessary - if case .notLoaded(let target) = pokemonSpecies[indexPath.row] { - pokemonSpecies[indexPath.row] = pokeAPI.request(target) { result in - self.pokemonSpecies[indexPath.row] = result - // Obtain the image information from a loaded resource to be loaded subsequentially - if case .loaded(let pokemonSpecies) = result { - self.sprites[indexPath.row] = pokemonSpecies.varieties.first.flatMap({ .notLoaded(.pokemon($0)) }) - } - tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) - } - } - - // Load the image information for this row if necessary - if let sprite = sprites[indexPath.row], case .notLoaded(let target) = sprite { - sprites[indexPath.row] = pokeAPI.request(target) { result in - self.sprites[indexPath.row] = result - tableView.reloadRowsAtIndexPaths([ indexPath ], withRowAnimation: .Fade) - } - } - - // Obtain a cell and configure it - let cell = tableView.dequeueReusableCellWithIdentifier("PokemonSpeciesCell", forIndexPath: indexPath) as! PokemonSpeciesCell - cell.configureForEntry(pokedex.entries[indexPath.row], pokemonSpecies: pokemonSpecies[indexPath.row], sprite: sprites[indexPath.row]) - if case .loaded = pokemonSpecies[indexPath.row] { - cell.selectionStyle = .Default - } else { - cell.selectionStyle = .None - } - return cell - } - -} diff --git a/APIClient/Pokemon.swift b/APIClient/Pokemon.swift deleted file mode 100644 index 890fa19..0000000 --- a/APIClient/Pokemon.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// Pokemon.swift -// APIClient -// -// Created by Nils Fischer on 20.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import Foundation -import Freddy -import UIKit - - -/// A Pokemon species such as "Bulbasaur" -struct PokemonSpecies: JSONDecodable { - - /// The name of the resource, such as "bulbasaur" - let name: String - /// The localized name of the Pokemon species, such as "Bisasam" - let localizedName: String - /// A short descriptive text about the Pokemon species - let flavorText: String? - /// The varieties this Pokemon species can occur in - let varieties: [NamedResource] - /// The Pokedex Entries associated to this Pokemon - let pokedexEntries: [PokedexEntry] - - struct PokedexEntry: JSONDecodable { - - let number: Int - let pokedex: NamedResource - - init(json: JSON) throws { - self.number = try json.int("entry_number") - self.pokedex = try json.decode("pokedex") - } - - } - - init(json: JSON) throws { - self.name = try json.string("name") - self.localizedName = try json.localized("names").string("name") - self.flavorText = try json.localized("flavor_text_entries").string("flavor_text").stringByReplacingOccurrencesOfString("\n", withString: " ") - self.varieties = try json.array("varieties").map({ try $0.decode("pokemon") }) - self.pokedexEntries = try json.arrayOf("pokedex_numbers") - } - -} - -/// A specific Pokemon variety of a species -struct Pokemon: JSONDecodable { - - /// The visual depictions of this Pokemon - let sprites: PokemonSprites - - init(json: JSON) throws { - self.sprites = try json.decode("sprites") - } - -} - - -/// The visual depictions of a Pokemon -struct PokemonSprites: JSONDecodable { - - let frontDefault: ImageResource - - init(json: JSON) throws { - self.frontDefault = try json.decode("front_default") - } - -} diff --git a/APIClient/PokemonSpeciesCell.swift b/APIClient/PokemonSpeciesCell.swift deleted file mode 100644 index de5da0f..0000000 --- a/APIClient/PokemonSpeciesCell.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// PokemonSpeciesCell.swift -// APIClient -// -// Created by Nils Fischer on 20.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import UIKit -import AlamofireImage - - -class PokemonSpeciesCell: UITableViewCell { - - @IBOutlet var numberLabel: UILabel! - @IBOutlet var nameLabel: UILabel! - @IBOutlet var flavorTextLabel: UILabel! - @IBOutlet var spriteImageview: UIImageView! - @IBOutlet var loadingIndicator: UIActivityIndicatorView! - - func configureForEntry(entry: Pokedex.Entry, pokemonSpecies: APIResource, sprite: APIResource?) { - - numberLabel.text = String(entry.number) - - // The resource may or may not be loaded, so configure the content accordingly - switch pokemonSpecies { - - case .loaded(let pokemonSpecies): - loadingIndicator.stopAnimating() - nameLabel.text = pokemonSpecies.localizedName - flavorTextLabel.text = pokemonSpecies.flavorText - - case .loading: - loadingIndicator.startAnimating() - nameLabel.text = nil - flavorTextLabel.text = nil - - case .failed(_, let error): - loadingIndicator.stopAnimating() - nameLabel.text = String(error) - flavorTextLabel.text = nil - - case .notLoaded: - loadingIndicator.stopAnimating() - nameLabel.text = nil - flavorTextLabel.text = nil - - } - - // Try to obtain an image for the pokemon - if let sprite = sprite { - switch sprite { - case .loaded(let sprite): - spriteImageview.image = nil - spriteImageview.hidden = false - // Use `AlamofireImage` to load the image from the URL and display it in the image view - spriteImageview.af_setImageWithURL(sprite.sprites.frontDefault.url) - default: - spriteImageview.hidden = true - } - } else { - spriteImageview.hidden = true - } - } - -} diff --git a/APIClient/PokemonViewController.swift b/APIClient/PokemonViewController.swift deleted file mode 100644 index 56dbaf0..0000000 --- a/APIClient/PokemonViewController.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// PokemonViewController.swift -// APIClient -// -// Created by Nils Fischer on 22.05.16. -// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. -// - -import UIKit -import Freddy -import Moya -import AlamofireImage - - -class PokemonViewController: UIViewController { - - /// The Poke API provider that handles requests for server resources - var pokeAPI: MoyaProvider! - - /// The Pokemon species to show here - var pokemonSpecies: PokemonSpecies? { - didSet { - // Configure view - self.searchTextfield.text = pokemonSpecies?.name - self.nameLabel.text = pokemonSpecies?.localizedName - self.flavorTextLabel.text = pokemonSpecies?.flavorText - self.showPokedexEntriesButton.hidden = pokemonSpecies == nil - self.sprite = nil - if let sprite = pokemonSpecies?.varieties.first { - self.loadSprite(sprite) - } - } - } - /// The image information for the pokemon species if loaded - private var sprite: Pokemon? { - didSet { - if let spriteURL = sprite?.sprites.frontDefault.url { - self.spriteImageview.hidden = true - // Use `AlamofireImage` to load the image from the URL and display it in the image view - self.spriteImageview.af_setImageWithURL(spriteURL) { response in - if case .Success = response.result { - self.spriteImageview.hidden = false - } - } - } else { - self.spriteImageview.hidden = true - } - } - } - - - // MARK: Interface Elements - - @IBOutlet var searchTextfield: UITextField! - @IBOutlet var loadingIndicator: UIActivityIndicatorView! - @IBOutlet var nameLabel: UILabel! - @IBOutlet var spriteImageview: UIImageView! - @IBOutlet var spriteLoadingIndicator: UIActivityIndicatorView! - @IBOutlet var flavorTextLabel: UILabel! - @IBOutlet var showPokedexEntriesButton: UIButton! - - - // MARK: Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - self.pokemonSpecies = nil - } - - - // MARK: Loading Resources - - func loadPokemonSpecies(pokemonSpecies: NamedResource) { - loadingIndicator.startAnimating() - pokeAPI.request(.pokemonSpecies(pokemonSpecies)) { result in - self.loadingIndicator.stopAnimating() - switch result { - case .Success(let response): - do { - try response.filterSuccessfulStatusCodes() - // Try to parse the response to JSON - let json = try JSON(data: response.data) - // Try to decode the JSON to the required type - let pokemonSpecies = try PokemonSpecies(json: json) - // Configure view according to model - self.pokemonSpecies = pokemonSpecies - } catch { - print(error) - } - case .Failure(let error): - print(error) - } - } - } - - func loadSprite(pokemon: NamedResource) { - spriteLoadingIndicator.startAnimating() - pokeAPI.request(.pokemon(pokemon)) { result in - self.spriteLoadingIndicator.stopAnimating() - switch result { - case .Success(let response): - do { - try response.filterSuccessfulStatusCodes() - // Try to parse the response to JSON - let json = try JSON(data: response.data) - // Try to decode the JSON to the required type - let pokemon = try Pokemon(json: json) - // Configure view according to model - self.sprite = pokemon - } catch { - print(error) - } - case .Failure(let error): - print(error) - } - } - } - - - // MARK: User Interaction - - func textFieldShouldReturn(textField: UITextField) -> Bool { - guard let name = textField.text else { - return true - } - self.loadPokemonSpecies(NamedResource(name: name)) - return true - } - - override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { - switch segue.identifier! { - case "showPokedexEntry": - guard let pokedexEntriesViewController = (segue.destinationViewController as? UINavigationController)?.topViewController as? PokedexEntriesViewController else { - return - } - guard let pokemonSpecies = self.pokemonSpecies else { - return - } - pokedexEntriesViewController.pokeAPI = pokeAPI - pokedexEntriesViewController.pokemonSpecies = pokemonSpecies - default: - break - } - } - - @IBAction func unwindToPokemon(segue: UIStoryboardSegue) { - switch segue.identifier! { - case "showPokemonSpecies": - guard let pokemonSpeciesProvider = segue.sourceViewController as? PokemonSpeciesProvider else { - return - } - self.pokemonSpecies = pokemonSpeciesProvider.providedPokemonSpecies - default: - break - } - } -} - - -// MARK: - Pokemon Species Provider - -protocol PokemonSpeciesProvider { - - var providedPokemonSpecies: PokemonSpecies? { get } - -} diff --git a/APIClient/SWAPI.swift b/APIClient/SWAPI.swift new file mode 100644 index 0000000..3fca1df --- /dev/null +++ b/APIClient/SWAPI.swift @@ -0,0 +1,70 @@ +//SWAPI +// APIClient + +import Foundation +import Moya +import Freddy + + + +/// The abstraction of the [SWAPI](http://swapi.co) REST API. +enum SWAPI: Moya.TargetType { + + + /// MARK: Endpoints + + case planets (NamedResource) + case planets_id (NamedResource) + case planets_schema (NamedResource) + + + // MARK: Network Abstraction + + var baseURL: NSURL { return NSURL(string: "http://swapi.co/api")! } + + var path: String { + switch self { + case .planets(let namedResource): return "/planets/\(namedResource.name)" + case .planets_id(let namedResource): return "/planets_id/\(namedResource)" + case .planets_schema(let namedResource): return "/planets_schema/\(namedResource)" + } + } + + var method: Moya.Method { return .GET } + + var parameters: [String : AnyObject]? { + switch self { + default: return nil + } + } + + // TODO: Provide sample data for testing + var sampleData: NSData { + switch self { + default: return "".dataUsingEncoding(NSUTF8StringEncoding)! + } + } +} + +struct NamedResource: Freddy.JSONDecodable { + + let name: String + + init(name: String) { + self.name = name + } + + init(json: JSON) throws { + self.name = try json.string("name") + } + +} + + + + + + + + + diff --git a/APIClient/SWView.swift b/APIClient/SWView.swift new file mode 100644 index 0000000..1969183 --- /dev/null +++ b/APIClient/SWView.swift @@ -0,0 +1,16 @@ +// +// SWView.swift +// APIClient +// +// Created by Felix Meissner on 5/29/16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import Foundation +import UIKit + +class SWPlanetsTableViewCell: UITableViewCell { + + + +} \ No newline at end of file diff --git a/APIClient/SWViewController.swift b/APIClient/SWViewController.swift new file mode 100644 index 0000000..d758242 --- /dev/null +++ b/APIClient/SWViewController.swift @@ -0,0 +1,68 @@ + +// Created by Nils Fischer on 22.05.16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import UIKit +import Moya + + + +class SWViewController: UITableViewController, UITextFieldDelegate { + + /// The SWAPI provider that handles requests for server resources + var swAPI: MoyaProvider! + + + @IBOutlet var Laden: UIButton! + @IBOutlet var Textfeld: UITextField! + + + override func viewDidLoad() { + super.viewDidLoad() + + // Handle the text field’s user input through delegate callbacks. + Textfeld.delegate = self + } + + + // MARK: Actions + + @IBAction func PerformRequest(sender: UIButton) { + swAPI.request(swAPI) {result in + switch result { + case .Success(let response): + do { + try response.filterSuccessfulStatusCodes() + print(response) + let json = try JSON (data: reponse.data) + } catch { + print(error) + } + case .Failure(let error): + print(error) + } + } + + + + + } +} + +extension SWViewController { + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 0 + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) + return cell + } + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + + } +} + + diff --git a/APIClient/StarWars.swift b/APIClient/StarWars.swift new file mode 100644 index 0000000..56c232b --- /dev/null +++ b/APIClient/StarWars.swift @@ -0,0 +1,33 @@ +// +// StarWars.swift +// APIClient +// +// Created by Felix Meissner on 5/29/16. +// Copyright © 2016 iOS Dev Kurs Universität Heidelberg. All rights reserved. +// + +import Foundation +import Freddy +import UIKit + +struct SWPlanet: JSONDecodable { + + /// The name of the resource, such as "Tatooine" + let name: String + let diameter: String + let rotation_period: String + let orbital_period: String + let gravity: String + + + + + init(json: JSON) throws { + self.name = try json.string("name") + self.diameter = try json.string("diameter") + self.rotation_period = try json.string("rotation_period") + self.orbital_period = try json.string("orbital_period") + self.gravity = try json.string("gravity") + } + +} \ No newline at end of file diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 41c4758..283916b 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -789,7 +789,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0730; }; buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */; compatibilityVersion = "Xcode 3.2"; @@ -1041,6 +1041,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Result/Result.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Result; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1070,6 +1071,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Freddy/Freddy.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Freddy; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1100,6 +1102,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Alamofire; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1129,6 +1132,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/AlamofireImage/AlamofireImage.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = AlamofireImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1158,6 +1162,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Result/Result.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Result; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1188,6 +1193,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/AwesomeCache/AwesomeCache.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = AwesomeCache; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1218,6 +1224,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Alamofire/Alamofire.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Alamofire; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1248,6 +1255,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Freddy/Freddy.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Freddy; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1277,6 +1285,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Moya/Moya.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Moya; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1379,6 +1388,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/AlamofireImage/AlamofireImage.modulemap"; MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = AlamofireImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1409,6 +1419,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/AwesomeCache/AwesomeCache.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = AwesomeCache; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1438,6 +1449,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MODULEMAP_FILE = "Target Support Files/Moya/Moya.modulemap"; MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${PRODUCT_BUNDLE_IDENTIFIER}"; PRODUCT_NAME = Moya; SDKROOT = iphoneos; SKIP_INSTALL = YES; diff --git a/Pods/Target Support Files/Alamofire/Info.plist b/Pods/Target Support Files/Alamofire/Info.plist index d4b3c29..2edde6e 100644 --- a/Pods/Target Support Files/Alamofire/Info.plist +++ b/Pods/Target Support Files/Alamofire/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 3.3.1 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 3.3.1 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Pods/Target Support Files/AlamofireImage/Info.plist b/Pods/Target Support Files/AlamofireImage/Info.plist index e526849..8734f42 100644 --- a/Pods/Target Support Files/AlamofireImage/Info.plist +++ b/Pods/Target Support Files/AlamofireImage/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.4.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 2.4.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Pods/Target Support Files/AwesomeCache/Info.plist b/Pods/Target Support Files/AwesomeCache/Info.plist index 0a12077..b548963 100644 --- a/Pods/Target Support Files/AwesomeCache/Info.plist +++ b/Pods/Target Support Files/AwesomeCache/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.0.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 2.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Pods/Target Support Files/Freddy/Info.plist b/Pods/Target Support Files/Freddy/Info.plist index 7f71fff..06b8339 100644 --- a/Pods/Target Support Files/Freddy/Info.plist +++ b/Pods/Target Support Files/Freddy/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.1.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 2.1.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Pods/Target Support Files/Moya/Info.plist b/Pods/Target Support Files/Moya/Info.plist index 2b2e520..30de714 100644 --- a/Pods/Target Support Files/Moya/Info.plist +++ b/Pods/Target Support Files/Moya/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 6.4.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 6.4.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + diff --git a/Pods/Target Support Files/Result/Info.plist b/Pods/Target Support Files/Result/Info.plist index 0a12077..b548963 100644 --- a/Pods/Target Support Files/Result/Info.plist +++ b/Pods/Target Support Files/Result/Info.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.0.0 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 2.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + From d5d8e944ba0366d3067860d6c5d0f47ebe909901 Mon Sep 17 00:00:00 2001 From: felix Date: Mon, 30 May 2016 12:02:26 +0200 Subject: [PATCH 3/6] Fast_fertig: nur Problem bei PerformRequest --- APIClient/Base.lproj/Main.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIClient/Base.lproj/Main.storyboard b/APIClient/Base.lproj/Main.storyboard index 69b2fbf..23ee413 100644 --- a/APIClient/Base.lproj/Main.storyboard +++ b/APIClient/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + From efaa5b44ad166dcf94d140d98887575f4c109cfa Mon Sep 17 00:00:00 2001 From: felix Date: Mon, 30 May 2016 12:24:34 +0200 Subject: [PATCH 4/6] in Master --- APIClient/Base.lproj/Main.storyboard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIClient/Base.lproj/Main.storyboard b/APIClient/Base.lproj/Main.storyboard index bbf8afd..55a2ac8 100644 --- a/APIClient/Base.lproj/Main.storyboard +++ b/APIClient/Base.lproj/Main.storyboard @@ -25,7 +25,7 @@ - +