├── .DS_Store ├── .gitignore ├── Backend ├── .DS_Store └── TodoAPI │ ├── .DS_Store │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Dockerfile │ ├── Package.resolved │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── TodoAPI │ │ ├── Extensions │ │ └── APIGateway+Extension.swift │ │ ├── Model │ │ ├── APIError.swift │ │ ├── Handler.swift │ │ ├── Todo+DynamoDB.swift │ │ └── Todo.swift │ │ ├── Services │ │ └── TodoService.swift │ │ ├── TodoLambdaHandler.swift │ │ ├── Utils.swift │ │ └── main.swift │ ├── Tests │ ├── LinuxMain.swift │ └── TodoAPITests │ │ ├── TodoAPITests.swift │ │ └── XCTestManifests.swift │ ├── scripts │ └── package.sh │ └── serverless.yml ├── Client └── Todo │ ├── .DS_Store │ ├── Todo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── Todo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Bindings │ ├── TodoFormViewModel.swift │ └── TodoListViewModel.swift │ ├── ContentView.swift │ ├── Info.plist │ ├── Model │ ├── Todo.swift │ └── TodoError.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Services │ └── TodoProvider.swift │ ├── Todo.xcdatamodeld │ └── Todo.xcdatamodel │ │ └── contents │ ├── TodoApp.swift │ ├── TodoFormView.swift │ └── Utils.swift ├── README.md └── promo.jpeg /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .serverless 2 | node_modules 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ -------------------------------------------------------------------------------- /Backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Backend/.DS_Store -------------------------------------------------------------------------------- /Backend/TodoAPI/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Backend/TodoAPI/.DS_Store -------------------------------------------------------------------------------- /Backend/TodoAPI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swiftlang/swift:nightly-amazonlinux2 2 | 3 | RUN yum -y install \ 4 | git \ 5 | libuuid-devel \ 6 | libicu-devel \ 7 | libedit-devel \ 8 | libxml2-devel \ 9 | sqlite-devel \ 10 | python-devel \ 11 | ncurses-devel \ 12 | curl-devel \ 13 | openssl-devel \ 14 | tzdata \ 15 | libtool \ 16 | jq \ 17 | tar \ 18 | zip 19 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "037b70291941fe43de668066eb6fb802c5e181d2", 10 | "version": "1.1.1" 11 | } 12 | }, 13 | { 14 | "package": "aws-sdk-swift", 15 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "0f5a40c1331f497dd5cc29c873d9f0ee4ff0db60", 19 | "version": "5.0.0-alpha.4" 20 | } 21 | }, 22 | { 23 | "package": "aws-sdk-swift-core", 24 | "repositoryURL": "https://github.com/swift-aws/aws-sdk-swift-core.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "e0f70315488c23ea64ab63ef7b8530ce01783024", 28 | "version": "5.0.0-alpha.4" 29 | } 30 | }, 31 | { 32 | "package": "swift-aws-lambda-runtime", 33 | "repositoryURL": "https://github.com/swift-server/swift-aws-lambda-runtime.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "2bac89639fffd7b1197ab597473a4d10c459a230", 37 | "version": "0.2.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-backtrace", 42 | "repositoryURL": "https://github.com/swift-server/swift-backtrace.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "f2fd8c4845a123419c348e0bc4b3839c414077d5", 46 | "version": "1.2.0" 47 | } 48 | }, 49 | { 50 | "package": "swift-log", 51 | "repositoryURL": "https://github.com/apple/swift-log.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 55 | "version": "1.2.0" 56 | } 57 | }, 58 | { 59 | "package": "swift-metrics", 60 | "repositoryURL": "https://github.com/apple/swift-metrics.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "708b960b4605abb20bc55d65abf6bad607252200", 64 | "version": "2.0.0" 65 | } 66 | }, 67 | { 68 | "package": "swift-nio", 69 | "repositoryURL": "https://github.com/apple/swift-nio.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "120acb15c39aa3217e9888e515de160378fbcc1e", 73 | "version": "2.18.0" 74 | } 75 | }, 76 | { 77 | "package": "swift-nio-extras", 78 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "7cd24c0efcf9700033f671b6a8eaa64a77dd0b72", 82 | "version": "1.5.1" 83 | } 84 | }, 85 | { 86 | "package": "swift-nio-ssl", 87 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "f0b118d9af6c4e78bc4f3f4fbb464020172b0bf4", 91 | "version": "2.7.5" 92 | } 93 | }, 94 | { 95 | "package": "swift-nio-transport-services", 96 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "2ac8fde712c1b1a147ecb7065824a40d2c09d0cb", 100 | "version": "1.6.0" 101 | } 102 | } 103 | ] 104 | }, 105 | "version": 1 106 | } 107 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TodoAPI", 7 | platforms: [ 8 | .macOS(.v10_14) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.2.0"), 12 | .package(url: "https://github.com/swift-aws/aws-sdk-swift.git", from: "5.0.0-alpha.4") 13 | ], 14 | targets: [ 15 | .target( 16 | name: "TodoAPI", 17 | dependencies: [ 18 | .product(name: "AWSDynamoDB", package: "aws-sdk-swift"), 19 | .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), 20 | .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime") 21 | ]), 22 | .testTarget( 23 | name: "TodoAPITests", 24 | dependencies: ["TodoAPI"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Backend/TodoAPI/README.md: -------------------------------------------------------------------------------- 1 | # TodoAPI 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Extensions/APIGateway+Extension.swift: -------------------------------------------------------------------------------- 1 | // Created by Alfian Losari on 02/07/20. 2 | // 3 | 4 | import Foundation 5 | import AWSLambdaEvents 6 | 7 | extension APIGateway.Request { 8 | 9 | private static let jsonDecoder: JSONDecoder = { 10 | let decoder = JSONDecoder() 11 | decoder.dateDecodingStrategy = .formatted(Utils.iso8601Formatter) 12 | return decoder 13 | }() 14 | 15 | func bodyObject() throws -> D { 16 | guard let jsonData = body?.data(using: .utf8) else { 17 | throw APIError.requestError 18 | } 19 | let object = try APIGateway.Request.jsonDecoder.decode(D.self, from: jsonData) 20 | return object 21 | } 22 | } 23 | 24 | extension APIGateway.Response { 25 | 26 | private static let jsonEncoder: JSONEncoder = { 27 | let encoder = JSONEncoder() 28 | encoder.dateEncodingStrategy = .formatted(Utils.iso8601Formatter) 29 | return encoder 30 | }() 31 | 32 | public static let defaultHeaders = [ 33 | "Content-Type": "application/json", 34 | "Access-Control-Allow-Origin": "*", 35 | "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE", 36 | "Access-Control-Allow-Credentials": "true", 37 | ] 38 | 39 | public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { 40 | self.init( 41 | statusCode: statusCode, 42 | headers: APIGateway.Response.defaultHeaders, 43 | multiValueHeaders: nil, 44 | body: "{\"error\":\"\(String(describing: error))\"}", 45 | isBase64Encoded: false 46 | ) 47 | } 48 | 49 | public init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { 50 | var body: String = "{}" 51 | if let data = try? Self.jsonEncoder.encode(object) { 52 | body = String(data: data, encoding: .utf8) ?? body 53 | } 54 | self.init( 55 | statusCode: statusCode, 56 | headers: APIGateway.Response.defaultHeaders, 57 | multiValueHeaders: nil, 58 | body: body, 59 | isBase64Encoded: false 60 | ) 61 | } 62 | } 63 | 64 | struct EmptyResponse: Encodable {} 65 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Model/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 30/06/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum APIError: Error { 11 | case decodingError 12 | case requestError 13 | case todoNotFound 14 | } 15 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Model/Handler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import AWSLambdaRuntime 9 | 10 | enum Handler: String { 11 | 12 | case create 13 | case update 14 | case delete 15 | case read 16 | case list 17 | 18 | static var current: Handler? { 19 | guard let handler = Lambda.env("_HANDLER") else { 20 | return nil 21 | } 22 | return Handler(rawValue: handler) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Model/Todo+DynamoDB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 29/06/20. 6 | // 7 | 8 | import AWSDynamoDB 9 | import Foundation 10 | 11 | extension Todo { 12 | 13 | public var dynamoDbDictionary: [String: DynamoDB.AttributeValue] { 14 | var dictionary = [ 15 | DynamoDBField.id: DynamoDB.AttributeValue(s: id), 16 | DynamoDBField.name: DynamoDB.AttributeValue(s: name), 17 | DynamoDBField.isCompleted: DynamoDB.AttributeValue(bool: isCompleted) 18 | ] 19 | 20 | if let dueDate = dueDate { 21 | dictionary[DynamoDBField.dueDate] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: dueDate)) 22 | } 23 | 24 | if let createdAt = createdAt { 25 | dictionary[DynamoDBField.createdAt] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: createdAt)) 26 | } 27 | 28 | if let updatedAt = updatedAt { 29 | dictionary[DynamoDBField.updatedAt] = DynamoDB.AttributeValue(s: Utils.iso8601Formatter.string(from: updatedAt)) 30 | } 31 | 32 | return dictionary 33 | } 34 | 35 | public init(dictionary: [String: DynamoDB.AttributeValue]) throws { 36 | guard let id = dictionary[DynamoDBField.id]?.s, 37 | let name = dictionary[DynamoDBField.name]?.s, 38 | let isCompleted = dictionary[DynamoDBField.isCompleted]?.bool, 39 | let dueDateValue = dictionary[DynamoDBField.dueDate]?.s, 40 | let dueDate = Utils.iso8601Formatter.date(from: dueDateValue), 41 | let createdAtValue = dictionary[DynamoDBField.createdAt]?.s, 42 | let createdAt = Utils.iso8601Formatter.date(from: createdAtValue), 43 | let updatedAtValue = dictionary[DynamoDBField.updatedAt]?.s, 44 | let updatedAt = Utils.iso8601Formatter.date(from: updatedAtValue) else { 45 | throw APIError.decodingError 46 | } 47 | 48 | self.id = id 49 | self.name = name 50 | self.isCompleted = isCompleted 51 | self.dueDate = dueDate 52 | self.createdAt = createdAt 53 | self.updatedAt = updatedAt 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Model/Todo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 29/06/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Todo: Codable { 11 | public let id: String 12 | public let name: String 13 | public let isCompleted: Bool 14 | public var dueDate: Date? 15 | public var createdAt: Date? 16 | public var updatedAt: Date? 17 | 18 | public struct DynamoDBField { 19 | static let id = "id" 20 | static let name = "name" 21 | static let isCompleted = "isCompleted" 22 | static let dueDate = "dueDate" 23 | static let createdAt = "createdAt" 24 | static let updatedAt = "updatedAt" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Services/TodoService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 30/06/20. 6 | // 7 | 8 | import AWSDynamoDB 9 | import Foundation 10 | 11 | public class TodoService { 12 | 13 | let db: DynamoDB 14 | let tableName: String 15 | 16 | public init(db: DynamoDB, tableName: String) { 17 | self.db = db 18 | self.tableName = tableName 19 | } 20 | 21 | public func getAllTodos() -> EventLoopFuture<[Todo]> { 22 | let input = DynamoDB.ScanInput(tableName: tableName) 23 | 24 | return db.scan(input).flatMapThrowing { (output) -> [Todo] in 25 | try output.items?.compactMap { try Todo(dictionary: $0) } ?? [] 26 | } 27 | } 28 | 29 | public func getTodo(id: String) -> EventLoopFuture { 30 | let input = DynamoDB.GetItemInput(key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: id)], tableName: tableName) 31 | 32 | return db.getItem(input).flatMapThrowing { (output) -> Todo in 33 | if output.item == nil { throw APIError.todoNotFound } 34 | return try Todo(dictionary: output.item ?? [:]) 35 | } 36 | } 37 | 38 | public func createTodo(todo: Todo) -> EventLoopFuture { 39 | var todo = todo 40 | let currentDate = Date() 41 | 42 | todo.updatedAt = currentDate 43 | todo.createdAt = currentDate 44 | 45 | let input = DynamoDB.PutItemInput(item: todo.dynamoDbDictionary, tableName: tableName) 46 | 47 | return db.putItem(input).map { (_) -> Todo in 48 | todo 49 | } 50 | } 51 | 52 | public func updateTodo(todo: Todo) -> EventLoopFuture { 53 | var todo = todo 54 | 55 | todo.updatedAt = Date() 56 | 57 | let input = DynamoDB.UpdateItemInput( 58 | expressionAttributeNames: [ 59 | "#name": Todo.DynamoDBField.name, 60 | "#isCompleted": Todo.DynamoDBField.isCompleted, 61 | "#dueDate": Todo.DynamoDBField.dueDate, 62 | "#updatedAt": Todo.DynamoDBField.updatedAt 63 | ], 64 | expressionAttributeValues: [ 65 | ":name": DynamoDB.AttributeValue(s: todo.name), 66 | ":isCompleted": DynamoDB.AttributeValue(bool: todo.isCompleted), 67 | ":dueDate": DynamoDB.AttributeValue(s: todo.dueDate?.iso8601 ?? ""), 68 | ":updatedAt": DynamoDB.AttributeValue(s: todo.updatedAt?.iso8601 ?? ""), 69 | 70 | ], 71 | key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: todo.id)], 72 | returnValues: DynamoDB.ReturnValue.allNew, 73 | tableName: tableName, 74 | updateExpression: "SET #name = :name, #isCompleted = :isCompleted, #dueDate = :dueDate, #updatedAt = :updatedAt" 75 | ) 76 | 77 | return db.updateItem(input).flatMap { (output) in 78 | self.getTodo(id: todo.id) 79 | } 80 | } 81 | 82 | public func deleteTodo(id: String) -> EventLoopFuture { 83 | let input = DynamoDB.DeleteItemInput( 84 | key: [Todo.DynamoDBField.id: DynamoDB.AttributeValue(s: id)], 85 | tableName: tableName 86 | ) 87 | 88 | return db.deleteItem(input).map { _ in } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/TodoLambdaHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import Foundation 9 | import AWSLambdaEvents 10 | import AWSLambdaRuntime 11 | import AsyncHTTPClient 12 | import NIO 13 | import AWSDynamoDB 14 | 15 | struct TodoLamdaHandler: EventLoopLambdaHandler { 16 | 17 | typealias In = APIGateway.Request 18 | typealias Out = APIGateway.Response 19 | 20 | let db: AWSDynamoDB.DynamoDB 21 | let todoService: TodoService 22 | let httpClient: HTTPClient 23 | 24 | init(context: Lambda.InitializationContext) throws { 25 | let timeout = HTTPClient.Configuration.Timeout( 26 | connect: .seconds(30), 27 | read: .seconds(30) 28 | ) 29 | 30 | let httpClient = HTTPClient( 31 | eventLoopGroupProvider: .shared(context.eventLoop), 32 | configuration: HTTPClient.Configuration(timeout: timeout) 33 | ) 34 | 35 | let tableName = Lambda.env("TODOS_TABLE_NAME") ?? "" 36 | let region: Region 37 | if let envRegion = Lambda.env("AWS_REGION") { 38 | region = Region(rawValue: envRegion) 39 | } else { 40 | region = .uswest2 41 | } 42 | 43 | let db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(httpClient)) 44 | let todoService = TodoService(db: db, tableName: tableName) 45 | 46 | self.httpClient = httpClient 47 | self.db = db 48 | self.todoService = todoService 49 | } 50 | 51 | func handle(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 52 | guard let handler = Handler.current else { 53 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest)) 54 | } 55 | 56 | switch handler { 57 | case .create: 58 | return handleCreate(context: context, event: event) 59 | case .read: 60 | return handleRead(context: context, event: event) 61 | case .update: 62 | return handleUpdate(context: context, event: event) 63 | case .delete: 64 | return handleDelete(context: context, event: event) 65 | case .list: 66 | return handleList(context: context, event: event) 67 | } 68 | } 69 | 70 | func handleCreate(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 71 | guard let newTodo: Todo = try? event.bodyObject() else { 72 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest)) 73 | } 74 | return todoService.createTodo(todo: newTodo) 75 | .map { todo in 76 | APIGateway.Response(with: todo, statusCode: .ok) 77 | }.flatMapError { 78 | self.catchError(context: context, error: $0) 79 | } 80 | } 81 | 82 | func handleRead(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 83 | guard let id = event.pathParameters?[Todo.DynamoDBField.id] else { 84 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .notFound)) 85 | } 86 | return todoService.getTodo(id: id) 87 | .map { todo in 88 | APIGateway.Response(with: todo, statusCode: .ok) 89 | }.flatMapError { 90 | self.catchError(context: context, error: $0) 91 | } 92 | } 93 | 94 | func handleUpdate(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 95 | guard let updatedTodo: Todo = try? event.bodyObject() else { 96 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest)) 97 | } 98 | return todoService.updateTodo(todo: updatedTodo) 99 | .map { todo in 100 | APIGateway.Response(with: todo, statusCode: .ok) 101 | }.flatMapError { 102 | self.catchError(context: context, error: $0) 103 | } 104 | } 105 | 106 | func handleDelete(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 107 | guard let id = event.pathParameters?[Todo.DynamoDBField.id] else { 108 | return context.eventLoop.makeSucceededFuture(APIGateway.Response(with: APIError.requestError, statusCode: .badRequest)) 109 | } 110 | return todoService.deleteTodo(id: id) 111 | .map { 112 | APIGateway.Response(with: EmptyResponse(), statusCode: .ok) 113 | }.flatMapError { 114 | self.catchError(context: context, error: $0) 115 | } 116 | } 117 | 118 | func handleList(context: Lambda.Context, event: APIGateway.Request) -> EventLoopFuture { 119 | return todoService.getAllTodos() 120 | .map { todos in 121 | APIGateway.Response(with: todos, statusCode: .ok) 122 | }.flatMapError { 123 | self.catchError(context: context, error: $0) 124 | } 125 | } 126 | 127 | func catchError(context: Lambda.Context, error: Error) -> EventLoopFuture { 128 | let response = APIGateway.Response(with: error, statusCode: .notFound) 129 | return context.eventLoop.makeSucceededFuture(response) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alfian Losari on 29/06/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Utils { 11 | 12 | public static let iso8601Formatter: DateFormatter = { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 15 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 16 | return formatter 17 | }() 18 | 19 | } 20 | 21 | extension Date { 22 | 23 | var iso8601: String { 24 | Utils.iso8601Formatter.string(from: self) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Sources/TodoAPI/main.swift: -------------------------------------------------------------------------------- 1 | import AWSLambdaRuntime 2 | 3 | Lambda.run(TodoLamdaHandler.init) 4 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import TodoAPITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += TodoAPITests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Tests/TodoAPITests/TodoAPITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class TodoAPITests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("TodoAPI") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample), 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Backend/TodoAPI/Tests/TodoAPITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TodoAPITests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Backend/TodoAPI/scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | executable=$1 6 | 7 | target=.build/lambda/$executable 8 | rm -rf "$target" 9 | mkdir -p "$target" 10 | cp ".build/release/$executable" "$target/" 11 | cp -Pv \ 12 | /usr/lib/swift/linux/libBlocksRuntime.so \ 13 | /usr/lib/swift/linux/libFoundation*.so \ 14 | /usr/lib/swift/linux/libdispatch.so \ 15 | /usr/lib/swift/linux/libicu* \ 16 | /usr/lib/swift/linux/libswiftCore.so \ 17 | /usr/lib/swift/linux/libswiftDispatch.so \ 18 | /usr/lib/swift/linux/libswiftGlibc.so \ 19 | "$target" 20 | cd "$target" 21 | ln -s "$executable" "bootstrap" 22 | zip --symlinks lambda.zip * -------------------------------------------------------------------------------- /Backend/TodoAPI/serverless.yml: -------------------------------------------------------------------------------- 1 | service: alf-todoapi 2 | 3 | package: 4 | artifact: .build/lambda/TodoAPI/lambda.zip 5 | 6 | custom: 7 | todosTableName: todos-${self:provider.stage} 8 | 9 | provider: 10 | name: aws 11 | runtime: provided 12 | stage: ${opt:stage, 'dev'} 13 | region: ${opt:region, 'us-west-2'} 14 | environment: 15 | TODOS_TABLE_NAME: "${self:custom.todosTableName}" 16 | iamRoleStatements: 17 | - Effect: Allow 18 | Action: 19 | - logs:CreateLogGroup 20 | - logs:CreateLogStream 21 | - logs:PutLogEvents 22 | Resource: "*" 23 | - Effect: Allow 24 | Action: 25 | - dynamodb:UpdateItem 26 | - dynamodb:PutItem 27 | - dynamodb:GetItem 28 | - dynamodb:DeleteItem 29 | - dynamodb:Query 30 | - dynamodb:Scan 31 | - dynamodb:DescribeTable 32 | Resource: 33 | - { Fn::GetAtt: [TodosTable, Arn] } 34 | 35 | functions: 36 | createTodo: 37 | handler: create 38 | memorySize: 256 39 | events: 40 | - http: 41 | path: /todos 42 | method: post 43 | cors: true 44 | readTodo: 45 | handler: read 46 | memorySize: 256 47 | events: 48 | - http: 49 | path: /todos/{id} 50 | method: get 51 | cors: true 52 | updateTodo: 53 | handler: update 54 | memorySize: 256 55 | events: 56 | - http: 57 | path: /todos/{id} 58 | method: put 59 | cors: true 60 | deleteTodo: 61 | handler: delete 62 | memorySize: 256 63 | events: 64 | - http: 65 | path: /todos/{id} 66 | method: delete 67 | cors: true 68 | listTodos: 69 | handler: list 70 | memorySize: 256 71 | events: 72 | - http: 73 | path: /todos 74 | method: get 75 | cors: true 76 | 77 | resources: 78 | Resources: 79 | TodosTable: 80 | Type: AWS::DynamoDB::Table 81 | Properties: 82 | TableName: ${self:custom.todosTableName} 83 | AttributeDefinitions: 84 | - AttributeName: id 85 | AttributeType: S 86 | KeySchema: 87 | - AttributeName: id 88 | KeyType: HASH 89 | BillingMode: PAY_PER_REQUEST 90 | -------------------------------------------------------------------------------- /Client/Todo/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/Client/Todo/.DS_Store -------------------------------------------------------------------------------- /Client/Todo/Todo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8B7FBA9724AD8D4700167A90 /* TodoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */; }; 11 | 8B7FBA9924AD8D4700167A90 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9824AD8D4700167A90 /* ContentView.swift */; }; 12 | 8B7FBA9B24AD8D4800167A90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */; }; 13 | 8B7FBA9E24AD8D4800167A90 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */; }; 14 | 8B7FBAA724AD8D6600167A90 /* Todo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */; }; 15 | 8B7FBAA924AD8E3500167A90 /* TodoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */; }; 16 | 8B7FBAAD24AD8EF300167A90 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAAC24AD8EF300167A90 /* Todo.swift */; }; 17 | 8B7FBAAF24AD8F4600167A90 /* TodoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */; }; 18 | 8B7FBAB124AD8FE500167A90 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAB024AD8FE500167A90 /* Utils.swift */; }; 19 | 8B7FBAB424ADB7C700167A90 /* TodoListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */; }; 20 | 8BC92DD524ADDF1900FFE329 /* TodoFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */; }; 21 | 8BC92DD724ADF8B400FFE329 /* TodoFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 8B7FBA9324AD8D4700167A90 /* Todo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoApp.swift; sourceTree = ""; }; 27 | 8B7FBA9824AD8D4700167A90 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 28 | 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 30 | 8B7FBA9F24AD8D4800167A90 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Todo.xcdatamodel; sourceTree = ""; }; 32 | 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoProvider.swift; sourceTree = ""; }; 33 | 8B7FBAAC24AD8EF300167A90 /* Todo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; 34 | 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoError.swift; sourceTree = ""; }; 35 | 8B7FBAB024AD8FE500167A90 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 36 | 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListViewModel.swift; sourceTree = ""; }; 37 | 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoFormView.swift; sourceTree = ""; }; 38 | 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoFormViewModel.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 8B7FBA9024AD8D4700167A90 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 8B7FBA8A24AD8D4700167A90 = { 53 | isa = PBXGroup; 54 | children = ( 55 | 8B7FBA9524AD8D4700167A90 /* Todo */, 56 | 8B7FBA9424AD8D4700167A90 /* Products */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | 8B7FBA9424AD8D4700167A90 /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 8B7FBA9324AD8D4700167A90 /* Todo.app */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | 8B7FBA9524AD8D4700167A90 /* Todo */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 8B7FBA9624AD8D4700167A90 /* TodoApp.swift */, 72 | 8B7FBA9824AD8D4700167A90 /* ContentView.swift */, 73 | 8BC92DD424ADDF1900FFE329 /* TodoFormView.swift */, 74 | 8B7FBAB024AD8FE500167A90 /* Utils.swift */, 75 | 8B7FBAB224ADB7B900167A90 /* Bindings */, 76 | 8B7FBAAB24AD8E9300167A90 /* Services */, 77 | 8B7FBAAA24AD8E8A00167A90 /* Model */, 78 | 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */, 79 | 8B7FBA9A24AD8D4800167A90 /* Assets.xcassets */, 80 | 8B7FBA9F24AD8D4800167A90 /* Info.plist */, 81 | 8B7FBA9C24AD8D4800167A90 /* Preview Content */, 82 | ); 83 | path = Todo; 84 | sourceTree = ""; 85 | }; 86 | 8B7FBA9C24AD8D4800167A90 /* Preview Content */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 8B7FBA9D24AD8D4800167A90 /* Preview Assets.xcassets */, 90 | ); 91 | path = "Preview Content"; 92 | sourceTree = ""; 93 | }; 94 | 8B7FBAAA24AD8E8A00167A90 /* Model */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 8B7FBAAC24AD8EF300167A90 /* Todo.swift */, 98 | 8B7FBAAE24AD8F4600167A90 /* TodoError.swift */, 99 | ); 100 | path = Model; 101 | sourceTree = ""; 102 | }; 103 | 8B7FBAAB24AD8E9300167A90 /* Services */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 8B7FBAA824AD8E3500167A90 /* TodoProvider.swift */, 107 | ); 108 | path = Services; 109 | sourceTree = ""; 110 | }; 111 | 8B7FBAB224ADB7B900167A90 /* Bindings */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 8B7FBAB324ADB7C700167A90 /* TodoListViewModel.swift */, 115 | 8BC92DD624ADF8B400FFE329 /* TodoFormViewModel.swift */, 116 | ); 117 | path = Bindings; 118 | sourceTree = ""; 119 | }; 120 | /* End PBXGroup section */ 121 | 122 | /* Begin PBXNativeTarget section */ 123 | 8B7FBA9224AD8D4700167A90 /* Todo */ = { 124 | isa = PBXNativeTarget; 125 | buildConfigurationList = 8B7FBAA224AD8D4800167A90 /* Build configuration list for PBXNativeTarget "Todo" */; 126 | buildPhases = ( 127 | 8B7FBA8F24AD8D4700167A90 /* Sources */, 128 | 8B7FBA9024AD8D4700167A90 /* Frameworks */, 129 | 8B7FBA9124AD8D4700167A90 /* Resources */, 130 | ); 131 | buildRules = ( 132 | ); 133 | dependencies = ( 134 | ); 135 | name = Todo; 136 | productName = Todo; 137 | productReference = 8B7FBA9324AD8D4700167A90 /* Todo.app */; 138 | productType = "com.apple.product-type.application"; 139 | }; 140 | /* End PBXNativeTarget section */ 141 | 142 | /* Begin PBXProject section */ 143 | 8B7FBA8B24AD8D4700167A90 /* Project object */ = { 144 | isa = PBXProject; 145 | attributes = { 146 | LastSwiftUpdateCheck = 1200; 147 | LastUpgradeCheck = 1200; 148 | TargetAttributes = { 149 | 8B7FBA9224AD8D4700167A90 = { 150 | CreatedOnToolsVersion = 12.0; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = 8B7FBA8E24AD8D4700167A90 /* Build configuration list for PBXProject "Todo" */; 155 | compatibilityVersion = "Xcode 9.3"; 156 | developmentRegion = en; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | en, 160 | Base, 161 | ); 162 | mainGroup = 8B7FBA8A24AD8D4700167A90; 163 | productRefGroup = 8B7FBA9424AD8D4700167A90 /* Products */; 164 | projectDirPath = ""; 165 | projectRoot = ""; 166 | targets = ( 167 | 8B7FBA9224AD8D4700167A90 /* Todo */, 168 | ); 169 | }; 170 | /* End PBXProject section */ 171 | 172 | /* Begin PBXResourcesBuildPhase section */ 173 | 8B7FBA9124AD8D4700167A90 /* Resources */ = { 174 | isa = PBXResourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | 8B7FBA9E24AD8D4800167A90 /* Preview Assets.xcassets in Resources */, 178 | 8B7FBA9B24AD8D4800167A90 /* Assets.xcassets in Resources */, 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | /* End PBXResourcesBuildPhase section */ 183 | 184 | /* Begin PBXSourcesBuildPhase section */ 185 | 8B7FBA8F24AD8D4700167A90 /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 8B7FBAB424ADB7C700167A90 /* TodoListViewModel.swift in Sources */, 190 | 8B7FBA9924AD8D4700167A90 /* ContentView.swift in Sources */, 191 | 8B7FBAA724AD8D6600167A90 /* Todo.xcdatamodeld in Sources */, 192 | 8B7FBAA924AD8E3500167A90 /* TodoProvider.swift in Sources */, 193 | 8B7FBAB124AD8FE500167A90 /* Utils.swift in Sources */, 194 | 8B7FBA9724AD8D4700167A90 /* TodoApp.swift in Sources */, 195 | 8B7FBAAF24AD8F4600167A90 /* TodoError.swift in Sources */, 196 | 8B7FBAAD24AD8EF300167A90 /* Todo.swift in Sources */, 197 | 8BC92DD724ADF8B400FFE329 /* TodoFormViewModel.swift in Sources */, 198 | 8BC92DD524ADDF1900FFE329 /* TodoFormView.swift in Sources */, 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | /* End PBXSourcesBuildPhase section */ 203 | 204 | /* Begin XCBuildConfiguration section */ 205 | 8B7FBAA024AD8D4800167A90 /* Debug */ = { 206 | isa = XCBuildConfiguration; 207 | buildSettings = { 208 | ALWAYS_SEARCH_USER_PATHS = NO; 209 | CLANG_ANALYZER_NONNULL = YES; 210 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 211 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 212 | CLANG_CXX_LIBRARY = "libc++"; 213 | CLANG_ENABLE_MODULES = YES; 214 | CLANG_ENABLE_OBJC_ARC = YES; 215 | CLANG_ENABLE_OBJC_WEAK = YES; 216 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 217 | CLANG_WARN_BOOL_CONVERSION = YES; 218 | CLANG_WARN_COMMA = YES; 219 | CLANG_WARN_CONSTANT_CONVERSION = YES; 220 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 221 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 222 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 223 | CLANG_WARN_EMPTY_BODY = YES; 224 | CLANG_WARN_ENUM_CONVERSION = YES; 225 | CLANG_WARN_INFINITE_RECURSION = YES; 226 | CLANG_WARN_INT_CONVERSION = YES; 227 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 228 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 229 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 230 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 231 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 232 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 233 | CLANG_WARN_STRICT_PROTOTYPES = YES; 234 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 235 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 236 | CLANG_WARN_UNREACHABLE_CODE = YES; 237 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 238 | COPY_PHASE_STRIP = NO; 239 | DEBUG_INFORMATION_FORMAT = dwarf; 240 | ENABLE_STRICT_OBJC_MSGSEND = YES; 241 | ENABLE_TESTABILITY = YES; 242 | GCC_C_LANGUAGE_STANDARD = gnu11; 243 | GCC_DYNAMIC_NO_PIC = NO; 244 | GCC_NO_COMMON_BLOCKS = YES; 245 | GCC_OPTIMIZATION_LEVEL = 0; 246 | GCC_PREPROCESSOR_DEFINITIONS = ( 247 | "DEBUG=1", 248 | "$(inherited)", 249 | ); 250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 252 | GCC_WARN_UNDECLARED_SELECTOR = YES; 253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 254 | GCC_WARN_UNUSED_FUNCTION = YES; 255 | GCC_WARN_UNUSED_VARIABLE = YES; 256 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 257 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 258 | MTL_FAST_MATH = YES; 259 | ONLY_ACTIVE_ARCH = YES; 260 | SDKROOT = iphoneos; 261 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 262 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 263 | }; 264 | name = Debug; 265 | }; 266 | 8B7FBAA124AD8D4800167A90 /* Release */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ALWAYS_SEARCH_USER_PATHS = NO; 270 | CLANG_ANALYZER_NONNULL = YES; 271 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 273 | CLANG_CXX_LIBRARY = "libc++"; 274 | CLANG_ENABLE_MODULES = YES; 275 | CLANG_ENABLE_OBJC_ARC = YES; 276 | CLANG_ENABLE_OBJC_WEAK = YES; 277 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 278 | CLANG_WARN_BOOL_CONVERSION = YES; 279 | CLANG_WARN_COMMA = YES; 280 | CLANG_WARN_CONSTANT_CONVERSION = YES; 281 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 282 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 283 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 284 | CLANG_WARN_EMPTY_BODY = YES; 285 | CLANG_WARN_ENUM_CONVERSION = YES; 286 | CLANG_WARN_INFINITE_RECURSION = YES; 287 | CLANG_WARN_INT_CONVERSION = YES; 288 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 290 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 291 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 292 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 293 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 294 | CLANG_WARN_STRICT_PROTOTYPES = YES; 295 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 296 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 297 | CLANG_WARN_UNREACHABLE_CODE = YES; 298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 299 | COPY_PHASE_STRIP = NO; 300 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 301 | ENABLE_NS_ASSERTIONS = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu11; 304 | GCC_NO_COMMON_BLOCKS = YES; 305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 307 | GCC_WARN_UNDECLARED_SELECTOR = YES; 308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 309 | GCC_WARN_UNUSED_FUNCTION = YES; 310 | GCC_WARN_UNUSED_VARIABLE = YES; 311 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 312 | MTL_ENABLE_DEBUG_INFO = NO; 313 | MTL_FAST_MATH = YES; 314 | SDKROOT = iphoneos; 315 | SWIFT_COMPILATION_MODE = wholemodule; 316 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 317 | VALIDATE_PRODUCT = YES; 318 | }; 319 | name = Release; 320 | }; 321 | 8B7FBAA324AD8D4800167A90 /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 325 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 326 | CODE_SIGN_STYLE = Automatic; 327 | DEVELOPMENT_ASSET_PATHS = "\"Todo/Preview Content\""; 328 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 329 | ENABLE_PREVIEWS = YES; 330 | INFOPLIST_FILE = Todo/Info.plist; 331 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 332 | LD_RUNPATH_SEARCH_PATHS = ( 333 | "$(inherited)", 334 | "@executable_path/Frameworks", 335 | ); 336 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.Todo; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | SWIFT_VERSION = 5.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | 8B7FBAA424AD8D4800167A90 /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 348 | CODE_SIGN_STYLE = Automatic; 349 | DEVELOPMENT_ASSET_PATHS = "\"Todo/Preview Content\""; 350 | DEVELOPMENT_TEAM = 5C2XD9H2JS; 351 | ENABLE_PREVIEWS = YES; 352 | INFOPLIST_FILE = Todo/Info.plist; 353 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 354 | LD_RUNPATH_SEARCH_PATHS = ( 355 | "$(inherited)", 356 | "@executable_path/Frameworks", 357 | ); 358 | PRODUCT_BUNDLE_IDENTIFIER = com.alfianlosari.Todo; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | SWIFT_VERSION = 5.0; 361 | TARGETED_DEVICE_FAMILY = "1,2"; 362 | }; 363 | name = Release; 364 | }; 365 | /* End XCBuildConfiguration section */ 366 | 367 | /* Begin XCConfigurationList section */ 368 | 8B7FBA8E24AD8D4700167A90 /* Build configuration list for PBXProject "Todo" */ = { 369 | isa = XCConfigurationList; 370 | buildConfigurations = ( 371 | 8B7FBAA024AD8D4800167A90 /* Debug */, 372 | 8B7FBAA124AD8D4800167A90 /* Release */, 373 | ); 374 | defaultConfigurationIsVisible = 0; 375 | defaultConfigurationName = Release; 376 | }; 377 | 8B7FBAA224AD8D4800167A90 /* Build configuration list for PBXNativeTarget "Todo" */ = { 378 | isa = XCConfigurationList; 379 | buildConfigurations = ( 380 | 8B7FBAA324AD8D4800167A90 /* Debug */, 381 | 8B7FBAA424AD8D4800167A90 /* Release */, 382 | ); 383 | defaultConfigurationIsVisible = 0; 384 | defaultConfigurationName = Release; 385 | }; 386 | /* End XCConfigurationList section */ 387 | 388 | /* Begin XCVersionGroup section */ 389 | 8B7FBAA524AD8D6600167A90 /* Todo.xcdatamodeld */ = { 390 | isa = XCVersionGroup; 391 | children = ( 392 | 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */, 393 | ); 394 | currentVersion = 8B7FBAA624AD8D6600167A90 /* Todo.xcdatamodel */; 395 | path = Todo.xcdatamodeld; 396 | sourceTree = ""; 397 | versionGroupType = wrapper.xcdatamodel; 398 | }; 399 | /* End XCVersionGroup section */ 400 | }; 401 | rootObject = 8B7FBA8B24AD8D4700167A90 /* Project object */; 402 | } 403 | -------------------------------------------------------------------------------- /Client/Todo/Todo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Client/Todo/Todo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Bindings/TodoFormViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoFormViewModel.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class TodoFormViewModel: ObservableObject { 11 | 12 | let todoService: TodoProvider 13 | @Published var isUploaded = false 14 | @Published var isUploading = false 15 | @Published var error: Error? 16 | 17 | init(todoService: TodoProvider = TodoProvider.shared) { 18 | self.todoService = todoService 19 | } 20 | 21 | func update(todo: Todo) { 22 | self.error = nil 23 | self.isUploaded = false 24 | self.isUploading = true 25 | 26 | todoService.updateTodo(todo) { [weak self] (result) in 27 | DispatchQueue.main.async { 28 | self?.isUploading = false 29 | switch result { 30 | case .success: 31 | self?.isUploaded = true 32 | case .failure(let error): 33 | self?.error = error 34 | } 35 | } 36 | } 37 | } 38 | 39 | func create(todo: TodoProperties) { 40 | self.error = nil 41 | self.isUploaded = false 42 | self.isUploading = true 43 | 44 | todoService.createTodo(todo) { [weak self] (result) in 45 | DispatchQueue.main.async { 46 | self?.isUploading = false 47 | switch result { 48 | case .success: 49 | self?.isUploaded = true 50 | case .failure(let error): 51 | self?.error = error 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Bindings/TodoListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoListViewModel.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class TodoListViewModel: ObservableObject { 11 | 12 | let service: TodoProvider 13 | @Published var error: Error? 14 | @Published var lastUpdatedAt: Date? 15 | 16 | init(service: TodoProvider = TodoProvider.shared) { 17 | self.service = service 18 | } 19 | 20 | func loadTodoList() { 21 | self.error = nil 22 | service.fetchTodoList { error in 23 | DispatchQueue.main.async { [weak self] in 24 | self?.error = error 25 | self?.service.resetContext() 26 | self?.lastUpdatedAt = error == nil ? Date() : nil 27 | } 28 | } 29 | } 30 | 31 | func deleteTodo(todo: Todo) { 32 | service.deleteTodo(todo: todo) { (_) in 33 | // DispatchQueue.main.async { [weak self] in 34 | // 35 | // } 36 | 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Client/Todo/Todo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum FormType: Identifiable { 11 | 12 | var id: String { 13 | switch self { 14 | case .add: return "add" 15 | case .edit(let todo): return "edit-\(todo.id ?? "")" 16 | } 17 | } 18 | 19 | case add 20 | case edit(Todo) 21 | } 22 | 23 | struct ContentView: View { 24 | 25 | @StateObject var todoList = TodoListViewModel() 26 | @State var formType: FormType? 27 | 28 | var body: some View { 29 | NavigationView { 30 | TodoListView(lastUpdatedAt: self.todoList.lastUpdatedAt, formType: $formType, onDelete: { todo in 31 | todoList.deleteTodo(todo: todo) 32 | }) 33 | .navigationTitle("Todos-DynamoDB") 34 | .navigationBarItems(leading: Button(action: todoList.loadTodoList, label: { 35 | Image(systemName: "arrow.2.circlepath") 36 | }), trailing: Button(action: { 37 | formType = .add 38 | }, label: { 39 | Image(systemName: "plus") 40 | })) 41 | .sheet(item: $formType) { (formType) in 42 | switch formType { 43 | case .add: 44 | TodoFormView(name: "", isCompleted: false, dueDate: Date()) 45 | case .edit(let todoEdit): 46 | TodoFormView(name: todoEdit.name ?? "", isCompleted: todoEdit.isCompleted, dueDate: todoEdit.dueDate ?? Date(), todoEdit: todoEdit) 47 | } 48 | } 49 | .onAppear { 50 | todoList.loadTodoList() 51 | } 52 | } 53 | } 54 | } 55 | 56 | struct TodoListView: View { 57 | 58 | let lastUpdatedAt: Date? 59 | @Binding var formType: FormType? 60 | @State var selection: Todo? 61 | var onDelete: (Todo) -> () 62 | 63 | @FetchRequest( 64 | entity: Todo.entity(), 65 | sortDescriptors: [ 66 | NSSortDescriptor(keyPath: \Todo.isCompleted, ascending: true), 67 | NSSortDescriptor(keyPath: \Todo.dueDate, ascending: false) 68 | ] 69 | ) 70 | var todos: FetchedResults 71 | 72 | var body: some View { 73 | List(selection: $selection) { 74 | ForEach(todos) { (todo: Todo) in 75 | 76 | Section { 77 | Button { 78 | self.formType = .edit(todo) 79 | } label: { 80 | HStack { 81 | VStack(alignment: .leading, spacing: 8) { 82 | Text(todo.name ?? "") 83 | .font(.headline) 84 | Text("Due \(todo.formattedDueDateText)") 85 | .font(.subheadline) 86 | .foregroundColor(Color(UIColor.secondaryLabel)) 87 | } 88 | 89 | Spacer() 90 | 91 | if todo.isCompleted { 92 | Image(systemName: "checkmark.circle.fill") 93 | .foregroundColor(.green) 94 | } else { 95 | Image(systemName: "chevron.right") 96 | .foregroundColor(Color(UIColor.systemBlue)) 97 | 98 | } 99 | } 100 | } 101 | .listRowInsets(EdgeInsets.init(top: 12, leading: 16, bottom: 12, trailing: 16)) 102 | } 103 | 104 | } 105 | 106 | .onDelete { indexSet in 107 | if let index = indexSet.first { 108 | let deletedTodo = todos[index] 109 | self.onDelete(deletedTodo) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | 117 | struct ContentView_Previews: PreviewProvider { 118 | static var previews: some View { 119 | ContentView() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Model/Todo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Todo.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Todo { 11 | 12 | func update(with todoDictionary: [String: Any]) throws { 13 | guard let newId = todoDictionary["id"] as? String, 14 | let newName = todoDictionary["name"] as? String, 15 | let newIsCompleted = todoDictionary["isCompleted"] as? Bool, 16 | let newDueDate = todoDictionary["dueDate"] as? Date, 17 | let newUpdatedAt = todoDictionary["updatedAt"] as? Date, 18 | let newCreatedAt = todoDictionary["createdAt"] as? Date else { 19 | throw TodoError.decodingError 20 | } 21 | 22 | id = newId 23 | name = newName 24 | isCompleted = newIsCompleted 25 | dueDate = newDueDate 26 | createdAt = newCreatedAt 27 | updatedAt = newUpdatedAt 28 | } 29 | } 30 | 31 | extension Todo: Identifiable { 32 | 33 | var formattedDueDateText: String { 34 | Utils.listDateFormatter.string(from: dueDate ?? Date()) 35 | } 36 | 37 | } 38 | 39 | struct TodoProperties: Decodable { 40 | 41 | let id: String 42 | let name: String 43 | let isCompleted: Bool 44 | let dueDate: Date 45 | let createdAt: Date 46 | let updatedAt: Date 47 | 48 | var dictionary: [String: Any] { 49 | return [ 50 | "id": id, 51 | "name": name, 52 | "isCompleted": isCompleted, 53 | "dueDate": dueDate, 54 | "createdAt": createdAt, 55 | "updatedAt": updatedAt 56 | ] 57 | } 58 | 59 | var uploadDictionary: [String: Any] { 60 | return [ 61 | "id": id, 62 | "name": name, 63 | "isCompleted": isCompleted, 64 | "dueDate": Utils.iso8601Formatter.string(from: dueDate) 65 | ] 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Model/TodoError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoError.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TodoError: Error { 11 | case decodingError 12 | case invalidRequest 13 | case noConnection 14 | case importError 15 | case deleteError 16 | } 17 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Services/TodoProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoProvider.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import CoreData 9 | 10 | class TodoProvider { 11 | 12 | static let shared = TodoProvider() 13 | private init() {} 14 | 15 | static let jsonDecoder: JSONDecoder = { 16 | let decoder = JSONDecoder() 17 | decoder.dateDecodingStrategy = .formatted(Utils.iso8601Formatter) 18 | return decoder 19 | }() 20 | 21 | let baseURL = "PASTE_YOUR_ENDPOINT_HERE/dev/todos" 22 | 23 | lazy var persistentContainer: NSPersistentContainer = { 24 | let container = NSPersistentContainer(name: "Todo") 25 | container.loadPersistentStores { storeDesription, error in 26 | guard error == nil else { 27 | fatalError("Unresolved error \(error!)") 28 | } 29 | } 30 | container.viewContext.automaticallyMergesChangesFromParent = false 31 | container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 32 | container.viewContext.undoManager = UndoManager() 33 | container.viewContext.shouldDeleteInaccessibleFaults = true 34 | return container 35 | }() 36 | 37 | private func newTaskContext() -> NSManagedObjectContext { 38 | let taskContext = persistentContainer.newBackgroundContext() 39 | taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 40 | taskContext.undoManager = nil 41 | return taskContext 42 | } 43 | 44 | func createTodo(_ todo: TodoProperties, completionHandler: @escaping (Result) -> ()) { 45 | guard let url = URL(string: baseURL), let data = try? JSONSerialization.data(withJSONObject: todo.uploadDictionary, options: []) else { 46 | completionHandler(.failure(TodoError.invalidRequest)) 47 | return 48 | } 49 | 50 | var request = URLRequest(url: url) 51 | request.httpMethod = "POST" 52 | request.httpBody = data 53 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 54 | 55 | let session = URLSession(configuration: .default) 56 | session.dataTask(with: request) { (data, response, urlSessionError) in 57 | DispatchQueue.main.async { 58 | if let error = urlSessionError { 59 | completionHandler(.failure(error)) 60 | return 61 | } 62 | 63 | guard let data = data else { 64 | completionHandler(.failure(TodoError.noConnection)) 65 | return 66 | } 67 | 68 | do { 69 | let todoProperty = try TodoProvider.jsonDecoder.decode(TodoProperties.self, from: data) 70 | let todo = Todo(entity: Todo.entity(), insertInto: self.persistentContainer.viewContext) 71 | 72 | try todo.update(with: todoProperty.dictionary) 73 | 74 | try? self.persistentContainer.viewContext.save() 75 | completionHandler(.success(todo)) 76 | } catch { 77 | completionHandler(.failure(error)) 78 | } 79 | } 80 | }.resume() 81 | } 82 | 83 | func updateTodo(_ todo: Todo, completionHandler: @escaping (Result) -> ()) { 84 | let todoProperties = TodoProperties(id: todo.id ?? "", name: todo.name ?? "", isCompleted: todo.isCompleted, dueDate: todo.dueDate ?? Date(), createdAt: Date(), updatedAt: Date()) 85 | 86 | guard let url = URL(string: baseURL)?.appendingPathComponent(todoProperties.id), 87 | let data = try? JSONSerialization.data(withJSONObject: todoProperties.uploadDictionary, options: []) else { 88 | completionHandler(.failure(TodoError.invalidRequest)) 89 | return 90 | } 91 | 92 | var request = URLRequest(url: url) 93 | request.httpMethod = "PUT" 94 | request.httpBody = data 95 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 96 | 97 | let session = URLSession(configuration: .default) 98 | session.dataTask(with: request) { (data, response, urlSessionError) in 99 | DispatchQueue.main.async { 100 | if let error = urlSessionError { 101 | self.persistentContainer.viewContext.undo() 102 | completionHandler(.failure(error)) 103 | return 104 | } 105 | 106 | guard let data = data else { 107 | self.persistentContainer.viewContext.undo() 108 | completionHandler(.failure(TodoError.noConnection)) 109 | return 110 | } 111 | 112 | do { 113 | let todoProperty = try TodoProvider.jsonDecoder.decode(TodoProperties.self, from: data) 114 | todo.createdAt = todoProperty.createdAt 115 | todo.updatedAt = todoProperty.updatedAt 116 | try? self.persistentContainer.viewContext.save() 117 | completionHandler(.success(todo)) 118 | } catch { 119 | completionHandler(.failure(error)) 120 | } 121 | } 122 | }.resume() 123 | } 124 | 125 | 126 | func deleteTodo(todo: Todo, completionHandler: @escaping (Result) -> ()) { 127 | guard let url = URL(string: baseURL)?.appendingPathComponent(todo.id ?? "") else { 128 | completionHandler(.failure(TodoError.invalidRequest)) 129 | return 130 | } 131 | 132 | self.persistentContainer.viewContext.delete(todo) 133 | 134 | var request = URLRequest(url: url) 135 | request.httpMethod = "DELETE" 136 | 137 | let session = URLSession(configuration: .default) 138 | session.dataTask(with: request) { (data, response, urlSessionError) in 139 | DispatchQueue.main.async { 140 | if let error = urlSessionError { 141 | self.persistentContainer.viewContext.undo() 142 | completionHandler(.failure(error)) 143 | return 144 | } 145 | 146 | guard let _ = data else { 147 | self.persistentContainer.viewContext.undo() 148 | completionHandler(.failure(TodoError.noConnection)) 149 | return 150 | } 151 | 152 | try? self.persistentContainer.viewContext.save() 153 | completionHandler(.success(())) 154 | } 155 | }.resume() 156 | } 157 | 158 | func fetchTodoList(completionHandler: @escaping (Error?) -> Void) { 159 | guard let jsonURL = URL(string: baseURL) else { 160 | completionHandler(TodoError.invalidRequest) 161 | return 162 | } 163 | let session = URLSession(configuration: .default) 164 | 165 | let task = session.dataTask(with: jsonURL) { data, _, urlSessionError in 166 | guard urlSessionError == nil else { 167 | completionHandler(urlSessionError) 168 | return 169 | } 170 | 171 | guard let data = data else { 172 | completionHandler(TodoError.noConnection) 173 | return 174 | } 175 | 176 | self.deleteAll { (error) in 177 | if let error = error { 178 | completionHandler(error) 179 | return 180 | } 181 | 182 | do { 183 | let todos = try TodoProvider.jsonDecoder.decode([TodoProperties].self, from: data) 184 | try self.importTodos(from: todos) 185 | 186 | } catch { 187 | completionHandler(error) 188 | return 189 | } 190 | completionHandler(nil) 191 | } 192 | } 193 | task.resume() 194 | } 195 | 196 | private func importTodos(from todos: [TodoProperties]) throws { 197 | guard !todos.isEmpty else { return } 198 | 199 | var performError: Error? 200 | let todosDictionaryList = todos.map { $0.dictionary } 201 | 202 | let taskContext = newTaskContext() 203 | taskContext.performAndWait { 204 | let batchInsert = self.newBatchInsertRequest(with: todosDictionaryList) 205 | batchInsert.resultType = .statusOnly 206 | 207 | if let batchInsertResult = try? taskContext.execute(batchInsert) as? NSBatchInsertResult, 208 | let success = batchInsertResult.result as? Bool, success { 209 | return 210 | } 211 | performError = TodoError.importError 212 | } 213 | 214 | if let error = performError { 215 | throw error 216 | } 217 | } 218 | 219 | private func newBatchInsertRequest(with todoDictionaryList: [[String: Any]]) -> NSBatchInsertRequest { 220 | let batchInsert: NSBatchInsertRequest 221 | var index = 0 222 | let total = todoDictionaryList.count 223 | batchInsert = NSBatchInsertRequest(entityName: "Todo", dictionaryHandler: { dictionary in 224 | guard index < total else { return true } 225 | dictionary.addEntries(from: todoDictionaryList[index]) 226 | index += 1 227 | return false 228 | }) 229 | return batchInsert 230 | } 231 | 232 | func deleteAll(completionHandler: @escaping (Error?) -> Void) { 233 | let taskContext = newTaskContext() 234 | taskContext.perform { 235 | let fetchRequest = NSFetchRequest(entityName: "Todo") 236 | let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 237 | batchDeleteRequest.resultType = .resultTypeCount 238 | 239 | if let batchDeleteResult = try? taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult, 240 | batchDeleteResult.result != nil { 241 | completionHandler(nil) 242 | 243 | } else { 244 | completionHandler(TodoError.deleteError) 245 | } 246 | } 247 | } 248 | 249 | func resetContext() { 250 | persistentContainer.viewContext.reset() 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Todo.xcdatamodeld/Todo.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Client/Todo/Todo/TodoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoApp.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TodoApp: App { 12 | var body: some Scene { 13 | let viewContext = TodoProvider.shared.persistentContainer.viewContext 14 | 15 | return WindowGroup { 16 | ContentView() 17 | .environment(\.managedObjectContext, viewContext) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Client/Todo/Todo/TodoFormView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodoFormView.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TodoFormView: View { 11 | 12 | @State var name: String 13 | @State var isCompleted: Bool 14 | @State var dueDate: Date 15 | 16 | var todoEdit: Todo? 17 | 18 | @StateObject var todoFormVM = TodoFormViewModel() 19 | 20 | @Environment(\.presentationMode) var presentationMode 21 | 22 | var body: some View { 23 | NavigationView { 24 | Form { 25 | 26 | Section { 27 | TextEditor(text: $name) 28 | .frame(height: 160) 29 | } 30 | 31 | 32 | 33 | Toggle("Completed", isOn: $isCompleted) 34 | DatePicker("Due date", selection: self.$dueDate) 35 | } 36 | 37 | .navigationBarItems(leading: Button(action: { 38 | presentationMode.wrappedValue.dismiss() 39 | }, label: { 40 | Text("Cancel") 41 | }), trailing: Button(action: { 42 | guard !name.isEmpty else { return } 43 | if let todoEdit = self.todoEdit { 44 | todoEdit.name = name 45 | todoEdit.isCompleted = isCompleted 46 | todoEdit.dueDate = dueDate 47 | todoFormVM.update(todo: todoEdit) 48 | } else { 49 | let todo = TodoProperties(id: UUID().uuidString, name: name, isCompleted: isCompleted, dueDate: dueDate, createdAt: Date(), updatedAt: Date()) 50 | todoFormVM.create(todo: todo) 51 | } 52 | }, label: { 53 | Text("Save") 54 | })) 55 | .navigationTitle(todoEdit == nil ? "Create Todo" : "Update Todo") 56 | .onChange(of: self.todoFormVM.isUploaded) { value in 57 | guard value else { 58 | return 59 | } 60 | presentationMode.wrappedValue.dismiss() 61 | } 62 | .overlay(Group { 63 | if todoFormVM.isUploading { 64 | ProgressView("Submitting...") 65 | } else { 66 | EmptyView() 67 | } 68 | }) 69 | .disabled(todoFormVM.isUploading) 70 | } 71 | } 72 | 73 | } 74 | 75 | struct TodoFormView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | TodoFormView(name: "", isCompleted: false, dueDate: Date()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Client/Todo/Todo/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // Todo 4 | // 5 | // Created by Alfian Losari on 02/07/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Utils { 11 | 12 | public static let iso8601Formatter: DateFormatter = { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 15 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 16 | return formatter 17 | }() 18 | 19 | public static let listDateFormatter: DateFormatter = { 20 | let formatter = DateFormatter() 21 | formatter.dateFormat = "EEE MMM d" 22 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 23 | return formatter 24 | }() 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Todo REST API with AWS Lambda 2 | 3 | ![Alt text](./promo.jpeg?raw=true "Swift Todo REST API with AWS Lambda") 4 | 5 | Source code for Tutorial on building a Swift REST API to perform CRUD operations for Todo Items, learn how to persist data to AWS DynamoDB using AWS Swift SDK, handle events using Swift AWS Lambda Runtime library, and deploy to AWS Lambda using Serverless framework. 6 | 7 | ## Tutorial Video 8 | Youtube link at https://youtu.be/HHg3fVfpj6M 9 | 10 | ## Backend App Requirement 11 | - Xcode 11.5 12 | - AWS Credentials to provision resources 13 | - Serverless Framework for deployment. https://www.serverless.com 14 | - Docker for build and packaging. https://www.docker.com/products/docker-desktop 15 | 16 | ## Getting Started - Backend 17 | - Copy and Clone the project 18 | - Create the container using Dockerfile 19 | - Build the project in release mode using the docker container 20 | - Run the script inside scripts/package.sh to package the app into Lambda.zip inside the build folder 21 | - Update the serverless by providing your own unique service, dynamo db table 22 | - Deploy using sls -v deploy 23 | 24 | ## Backend Endpoints 25 | - List Todos: /todos (GET) 26 | - Read Todo: /todos/{id} (GET) 27 | ``` 28 | // Response JSON Body 29 | { 30 | "id": "String", 31 | "name": "String", 32 | "isCompleted": "Boolean", 33 | "dueDate": "ISO8601 formatted String", 34 | "createdAt": "ISO8601 formatted String", 35 | "updatedAt": "ISO8601 formatted String" 36 | } 37 | ``` 38 | - Create Todo: /todos (POST) 39 | ``` 40 | // Request JSON Body 41 | { 42 | "id": "String", 43 | "name": "String", 44 | "isCompleted": "Boolean", 45 | "dueDate": "ISO8601 formatted String" 46 | } 47 | ``` 48 | - Update Todo: /todos/{id} (PUT) 49 | ``` 50 | // Request JSON Body 51 | { 52 | "name": "String", 53 | "isCompleted": "Boolean", 54 | "dueDate": "ISO8601 formatted String" 55 | } 56 | ``` 57 | - Delete Todo: /todos/{id} (DELETE) 58 | 59 | 60 | ## Front end App Requirement 61 | - Xcode 12 62 | 63 | ## Getting Started - Frontend 64 | - Paste the endpoint url from backend deployment into TodoProvider.swift 65 | -------------------------------------------------------------------------------- /promo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfianlosari/SwiftAWSLamdaRESTAPI/de97a6a1e6db2f25449085b8ca9b847f2a1098ce/promo.jpeg --------------------------------------------------------------------------------