Skip to content
Permalink
Browse files
code change: 5. add file storage
  • Loading branch information
Stormacq, Sebastien committed Jun 11, 2020
1 parent ad92ead commit 3e77d8a992d6600ba8bee3169c2ff30f5122c608
Showing 4 changed files with 149 additions and 66 deletions.
@@ -15,13 +15,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
public let userData = UserData()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {



do {
Amplify.Logging.logLevel = .info
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))

try Amplify.add(plugin: AWSS3StoragePlugin())

try Amplify.configure()
print("Amplify initialized")

@@ -67,7 +67,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} catch {
print("Failed to configure Amplify \(error)")
}

return true
}

@@ -87,9 +87,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
// MARK: Amplify - Authentication

// MARK: -- Authentication code
// change our internal state, this triggers an UI update on the main thread
func updateUI(forSignInStatus : Bool) {
DispatchQueue.main.async() {
@@ -118,7 +118,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

}
}

// signin with Cognito web user interface
public func authenticateWithHostedUI() {

@@ -175,4 +175,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
}
}

// MARK: AWS S3 & Image Loading
func image(_ name: String, callback: @escaping (Data) -> Void ) {

print("Downloading image : \(name)")

_ = Amplify.Storage.downloadData(key: "\(name).jpg",
progressListener: { progress in
// in case you want to monitor progress
// print("Progress: \(progress)")
}, resultListener: { (event) in
switch event {
case let .success(data):
print("Image \(name) loaded")
callback(data)
case let .failure(storageError):
print("Can not download image: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
}
}
)
}
}
@@ -9,9 +9,10 @@ import UIKit
import SwiftUI
import CoreLocation

// this is just used for the previews. At runtime, data are now taken from UserData and loaded through AppDelegate
let landmarkData: [Landmark] = load("landmarkData.json")

func load<T: Decodable>(_ filename: String) -> T {
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
let data: Data

guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
@@ -33,36 +34,74 @@ func load<T: Decodable>(_ filename: String) -> T {
}
}

final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]
// allow to create image with uniform color
// https://gist.github.com/isoiphone/031da3656d69c0d85805
extension UIImage {
class func imageWithColor(color: UIColor, size: CGSize=CGSize(width: 1, height: 1)) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(CGRect(origin: CGPoint.zero, size: size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}

fileprivate static var scale = 2
// manage iage cache and download
final class ImageStore {
typealias _ImageDictionary = [String: Image]

static var shared = ImageStore()
fileprivate let placeholderName = "PLACEHOLDER"
fileprivate var images: _ImageDictionary
static var scale = 2

func image(name: String) -> Image {
let index = _guaranteeImage(name: name)

return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
}
static var shared = ImageStore()

static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
init() {
images = [:]
images[self.placeholderName] = Image(uiImage: UIImage.imageWithColor(color: UIColor.white, size: CGSize(width:300, height: 300)))
}

func image(name: String, landmark: Landmark) -> Image {
var result : Image?
if let img = images[name] {
result = img
} else {

DispatchQueue.main.async {
// trigger asynchronous download
let app = UIApplication.shared.delegate as! AppDelegate
_ = app.image(name) { (data) in

guard
let i = UIImage(data: data)
else {
fatalError("Couldn't convert image data \(name)")
}
let img = Image(i.cgImage!, scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))

// update UI on the main thread
DispatchQueue.main.async {
// update landmark object, this will trigger the UI refresh because image is Published
// and Landmark is Observable in LandmarkRow UI component
landmark.image = img
}
}
}
result = self.placeholder()
}
return image
return result!
}

fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }

images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
func placeholder() -> Image {
if let img = images[self.placeholderName] {
return img
} else {
fatalError("Image cache is incorrectly initialized")
}
}
}

func addImage(name: String, image : Image) {
images[name] = image
}
}
@@ -8,7 +8,8 @@ The model for an individual landmark.
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable {
// migrated Landmark from struct to class to make it Observable
class Landmark: Decodable, Identifiable, ObservableObject {
var id: Int
var name: String
fileprivate var imageName: String
@@ -17,7 +18,44 @@ struct Landmark: Hashable, Codable, Identifiable {
var park: String
var category: Category
var isFavorite: Bool

// consequence is that I need to add the constructor from decoder
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LandmarkKeys.self) // defining our (keyed) container
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
imageName = try container.decode(String.self, forKey: .imageName)
state = try container.decode(String.self, forKey: .state)
park = try container.decode(String.self, forKey: .park)
isFavorite = try container.decode(Bool.self, forKey: .isFavorite)
category = try container.decode(Category.self, forKey: .category)
coordinates = try container.decode(Coordinates.self, forKey: .coordinates)

// trigger image download & set placeholder
image = ImageStore.shared.image(name: imageName, landmark: self)
}

// construct from API Data
init(from : LandmarkData) {

guard let i = Int(from.id) else {
preconditionFailure("Can not create Landmark, Invalid ID : \(from.id) (expected Int)")
}

self.id = i
name = from.name
imageName = from.imageName!
coordinates = Coordinates(latitude: from.coordinates!.latitude!, longitude: from.coordinates!.longitude!)
state = from.state!
park = from.park!
category = Category(rawValue: from.category!)!
isFavorite = from.isFavorite!

// trigger image download & set placeholder
image = ImageStore.shared.image(name: imageName, landmark: self)

}

var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
@@ -30,40 +68,24 @@ struct Landmark: Hashable, Codable, Identifiable {
case rivers = "Rivers"
case mountains = "Mountains"
}
}

extension Landmark {
var image: Image {
ImageStore.shared.image(name: imageName)

// part of Decodable protocol, I need to declare all keys from the jSON file
enum LandmarkKeys: String, CodingKey {
case id = "id"
case name = "name"
case imageName = "imageName"
case category = "category"
case isFavorite = "isFavorite"
case park = "park"
case state = "state"
case coordinates = "coordinates"
}

// advertise changes on this property. This will allow Views to refresh when image is changed.
@Published var image : Image = Image("temp")
}

struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}

// assume all fields are non null.
// real life project must spend more time thinking about null values and
// maybe convert the above code (original Landmark class) to optionals
// I am not doing it for this workshop as this would imply too many changes in UI code
// MARK: - TODO
extension Landmark {
init(from : LandmarkData) {

guard let i = Int(from.id) else {
preconditionFailure("Can not create Landmark, Invalid ID : \(from.id) (expected Int)")
}

id = i
name = from.name
imageName = from.imageName!
coordinates = Coordinates(latitude: from.coordinates!.latitude!, longitude: from.coordinates!.longitude!)
state = from.state!
park = from.park!
category = Category(rawValue: from.category!)!
isFavorite = from.isFavorite!

}
}
@@ -8,7 +8,7 @@ A single row to be displayed in a list of landmarks.
import SwiftUI

struct LandmarkRow: View {
var landmark: Landmark
@ObservedObject var landmark: Landmark

var body: some View {
HStack {

0 comments on commit 3e77d8a

Please sign in to comment.