├── Gemfile ├── app-screenshot.png ├── eCommerce ├── Assets.xcassets │ ├── Contents.json │ ├── live.imageset │ │ ├── Image.png │ │ └── Contents.json │ ├── home_icon.imageset │ │ ├── Image.png │ │ └── Contents.json │ └── AppIcon.appiconset │ │ ├── iOS App Icon_20pt.png │ │ ├── iOS App Icon_29pt.png │ │ ├── iOS App Icon_40pt.png │ │ ├── iOS App Icon_76pt.png │ │ ├── iOS App Icon_20pt@2x.png │ │ ├── iOS App Icon_20pt@3x.png │ │ ├── iOS App Icon_29pt@2x.png │ │ ├── iOS App Icon_29pt@3x.png │ │ ├── iOS App Icon_40pt@2x.png │ │ ├── iOS App Icon_40pt@3x.png │ │ ├── iOS App Icon_60pt@2x.png │ │ ├── iOS App Icon_60pt@3x.png │ │ ├── iOS App Icon_76pt@2x.png │ │ ├── iOS App Icon_83.5@2x.png │ │ ├── iOS App Icon_1024pt@1x.png │ │ ├── iOS App Icon_20pt@2x 1.png │ │ ├── iOS App Icon_29pt@2x 1.png │ │ ├── iOS App Icon_40pt@2x 1.png │ │ └── Contents.json ├── Custom Fonts │ ├── AmazonEmber-Bold.ttf │ ├── AmazonEmber-Heavy.ttf │ ├── AmazonEmber-Medium.ttf │ └── AmazonEmber-Regular.ttf ├── eCommerce.entitlements ├── Constants.swift ├── AppDelegate.swift ├── Models │ ├── Products.swift │ └── Image.swift ├── Views │ ├── Product │ │ ├── ProductView.swift │ │ └── ProductView.xib │ └── Player │ │ ├── PlayerView.swift │ │ └── PlayerView.xib ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── Products.json └── Controllers │ └── ProductsViewController.swift ├── eCommerce.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── eCommerce.xcscheme └── project.pbxproj ├── eCommerce.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CODE_OF_CONDUCT.md ├── Podfile.lock ├── Podfile ├── LICENSE ├── .gitignore ├── Gemfile.lock ├── CONTRIBUTING.md ├── README.md └── THIRD-PARTY-LICENSES.txt /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | 5 | -------------------------------------------------------------------------------- /app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/app-screenshot.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /eCommerce/Custom Fonts/AmazonEmber-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Custom Fonts/AmazonEmber-Bold.ttf -------------------------------------------------------------------------------- /eCommerce/Custom Fonts/AmazonEmber-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Custom Fonts/AmazonEmber-Heavy.ttf -------------------------------------------------------------------------------- /eCommerce/Custom Fonts/AmazonEmber-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Custom Fonts/AmazonEmber-Medium.ttf -------------------------------------------------------------------------------- /eCommerce/Custom Fonts/AmazonEmber-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Custom Fonts/AmazonEmber-Regular.ttf -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/live.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/live.imageset/Image.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/home_icon.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/home_icon.imageset/Image.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_76pt.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@3x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@3x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@3x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_60pt@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_60pt@3x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_76pt@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_83.5@2x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_1024pt@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_1024pt@1x.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_20pt@2x 1.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_29pt@2x 1.png -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-ecommerce-ios-demo/HEAD/eCommerce/Assets.xcassets/AppIcon.appiconset/iOS App Icon_40pt@2x 1.png -------------------------------------------------------------------------------- /eCommerce.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /eCommerce/eCommerce.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.in-app-payments 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /eCommerce.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /eCommerce.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /eCommerce.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AmazonIVSPlayer (1.40.0) 3 | 4 | DEPENDENCIES: 5 | - AmazonIVSPlayer (~> 1.40.0) 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - AmazonIVSPlayer 10 | 11 | SPEC CHECKSUMS: 12 | AmazonIVSPlayer: b1dbf89d0067d016ab734dc6e7ae799ea52101cd 13 | 14 | PODFILE CHECKSUM: d4eedbde72703590cd550010242bace272a68b0e 15 | 16 | COCOAPODS: 1.16.2 17 | -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/live.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/home_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | 3 | target 'eCommerce' do 4 | pod 'AmazonIVSPlayer', '~> 1.40.0' 5 | end 6 | 7 | # Allow building for arm64e architecture, which AmazonIVSPlayer supports. 8 | # See https://developer.apple.com/documentation/security/preparing_your_app_to_work_with_pointer_authentication 9 | post_install do |installer| 10 | installer.pods_project.build_configurations.each do |configuration| 11 | configuration.build_settings['ARCHS[sdk=iphoneos*]'] = ['$(ARCHS_STANDARD)','arm64e'] 12 | end 13 | end -------------------------------------------------------------------------------- /eCommerce/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // eCommerce 4 | // 5 | // Created by Uldis Zingis on 01/10/2021. 6 | // Copyright © 2021 Twitch. All rights reserved. 7 | 8 | import Foundation 9 | 10 | enum Constants { 11 | // MARK: Stream url 12 | static let streamUrl = "https://4c62a87c1810.us-west-2.playback.live-video.net/api/video/v1/us-west-2.049054135175.channel.onToXRHIurEP.m3u8" 13 | 14 | // MARK: Product image source url 15 | static let productImageBaseUrl = "https://ecommerce.ivsdemos.com" 16 | } 17 | -------------------------------------------------------------------------------- /eCommerce/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // eCommerce 4 | // 5 | // Created by Zingis, Uldis on 5/29/20. 6 | // Copyright © 2020 Twitch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | do { 20 | try AVAudioSession.sharedInstance().setCategory(.playback) 21 | try AVAudioSession.sharedInstance().setActive(true) 22 | } catch { 23 | print("‼️ Could not setup AVAudioSession: \(error)") 24 | } 25 | 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /eCommerce/Models/Products.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Product.swift 3 | // eCommerce 4 | // 5 | // Created by Zingis, Uldis on 6/8/20. 6 | // Copyright © 2020 Twitch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct Products: Decodable { 12 | var items: [Product] = [] 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case items = "products" 16 | } 17 | } 18 | 19 | struct Product: Decodable, Equatable { 20 | var id: String 21 | var name: String 22 | var imageUrl: String 23 | var imageLargeUrl: String 24 | var price: Int 25 | var discountedPrice: Int 26 | var longDescription: String 27 | 28 | func getImage(completion: @escaping (UIImage?) -> Void) { 29 | if var url = URL(string: Constants.productImageBaseUrl) { 30 | url.appendPathComponent(imageUrl) 31 | Image.getFrom(url) { image in 32 | completion(image) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /eCommerce/Models/Image.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image.swift 3 | // eCommerce 4 | // 5 | // Created by Uldis Zingis on 05/10/2021. 6 | // Copyright © 2021 Twitch. All rights reserved. 7 | 8 | import UIKit 9 | 10 | enum Image { 11 | static func getFrom(_ url: URL, completion: @escaping (UIImage?) -> Void) { 12 | URLSession.shared.dataTask(with: url, completionHandler: { (data, _, error) in 13 | guard let data = data, error == nil else { 14 | print("❌ Error getting image from \(url.absoluteString): \(error!)") 15 | DispatchQueue.main.async { completion(nil) } 16 | return 17 | } 18 | if let image = UIImage(data: data) { 19 | DispatchQueue.main.async { completion(image) } 20 | } else { 21 | print("❌ Could not get UIImage from data \(data)") 22 | DispatchQueue.main.async { completion(nil) } 23 | } 24 | }).resume() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT No Attribution 3 | 4 | Copyright 2020 Amazon.com, Inc. or its affiliates. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xccheckout 19 | *.xcscmblueprint 20 | 21 | ## Obj-C/Swift specific 22 | *.hmap 23 | *.ipa 24 | *.dSYM.zip 25 | *.dSYM 26 | 27 | ## Playgrounds 28 | timeline.xctimeline 29 | playground.xcworkspace 30 | 31 | # Swift Package Manager 32 | # 33 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 34 | # Packages/ 35 | # Package.pins 36 | # Package.resolved 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots/**/*.png 64 | fastlane/test_output 65 | 66 | *.DS_Store 67 | -------------------------------------------------------------------------------- /eCommerce/Views/Product/ProductView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductView.swift 3 | // eCommerce 4 | // 5 | // Created by Uldis Zingis on 05/10/2021. 6 | // Copyright © 2021 Twitch. All rights reserved. 7 | 8 | import UIKit 9 | 10 | class ProductView: UIView { 11 | @IBOutlet weak var productImageView: UIImageView! 12 | @IBOutlet weak var titleLabel: UILabel! 13 | @IBOutlet weak var priceLabel: UILabel! 14 | @IBOutlet weak var discountLabel: UILabel! 15 | @IBOutlet weak var separatorLine: UIView! 16 | 17 | func setup(with product: Product, in frame: CGRect) { 18 | self.frame = frame 19 | 20 | product.getImage { image in 21 | DispatchQueue.main.async { 22 | self.productImageView.image = image 23 | } 24 | } 25 | productImageView.layer.cornerRadius = 10 26 | titleLabel.text = product.name 27 | if product.discountedPrice != product.price { 28 | let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: "$\(product.price)") 29 | attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 1, range: NSMakeRange(0, attributeString.length)) 30 | priceLabel.attributedText = attributeString 31 | } else { 32 | priceLabel.text = "$\(product.price)" 33 | priceLabel.textColor = .white 34 | } 35 | 36 | discountLabel.text = "$\(product.discountedPrice)" 37 | discountLabel.isHidden = product.discountedPrice == product.price 38 | 39 | setNeedsLayout() 40 | layoutIfNeeded() 41 | } 42 | 43 | func showBottomSeparator(_ isVisible: Bool) { 44 | separatorLine.isHidden = !isVisible 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /eCommerce/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /eCommerce/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 | UIAppFonts 24 | 25 | AmazonEmber-Bold.ttf 26 | AmazonEmber-Heavy.ttf 27 | AmazonEmber-Medium.ttf 28 | AmazonEmber-Regular.ttf 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UIStatusBarStyle 39 | UIStatusBarStyleLightContent 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | UIViewControllerBasedStatusBarAppearance 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.3) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | algoliasearch (1.27.4) 11 | httpclient (~> 2.8, >= 2.8.3) 12 | json (>= 1.5.1) 13 | atomos (0.1.3) 14 | claide (1.0.3) 15 | cocoapods (1.9.3) 16 | activesupport (>= 4.0.2, < 5) 17 | claide (>= 1.0.2, < 2.0) 18 | cocoapods-core (= 1.9.3) 19 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 20 | cocoapods-downloader (>= 1.2.2, < 2.0) 21 | cocoapods-plugins (>= 1.0.0, < 2.0) 22 | cocoapods-search (>= 1.0.0, < 2.0) 23 | cocoapods-stats (>= 1.0.0, < 2.0) 24 | cocoapods-trunk (>= 1.4.0, < 2.0) 25 | cocoapods-try (>= 1.1.0, < 2.0) 26 | colored2 (~> 3.1) 27 | escape (~> 0.0.4) 28 | fourflusher (>= 2.3.0, < 3.0) 29 | gh_inspector (~> 1.0) 30 | molinillo (~> 0.6.6) 31 | nap (~> 1.0) 32 | ruby-macho (~> 1.4) 33 | xcodeproj (>= 1.14.0, < 2.0) 34 | cocoapods-core (1.9.3) 35 | activesupport (>= 4.0.2, < 6) 36 | algoliasearch (~> 1.0) 37 | concurrent-ruby (~> 1.1) 38 | fuzzy_match (~> 2.0.4) 39 | nap (~> 1.0) 40 | netrc (~> 0.11) 41 | typhoeus (~> 1.0) 42 | cocoapods-deintegrate (1.0.4) 43 | cocoapods-downloader (1.6.3) 44 | cocoapods-plugins (1.0.0) 45 | nap 46 | cocoapods-search (1.0.0) 47 | cocoapods-stats (1.1.0) 48 | cocoapods-trunk (1.5.0) 49 | nap (>= 0.8, < 2.0) 50 | netrc (~> 0.11) 51 | cocoapods-try (1.2.0) 52 | colored2 (3.1.2) 53 | concurrent-ruby (1.1.7) 54 | escape (0.0.4) 55 | ethon (0.12.0) 56 | ffi (>= 1.3.0) 57 | ffi (1.13.1) 58 | fourflusher (2.3.1) 59 | fuzzy_match (2.0.4) 60 | gh_inspector (1.1.3) 61 | httpclient (2.8.3) 62 | i18n (0.9.5) 63 | concurrent-ruby (~> 1.0) 64 | json (2.3.1) 65 | minitest (5.14.2) 66 | molinillo (0.6.6) 67 | nanaimo (0.3.0) 68 | nap (1.1.0) 69 | netrc (0.11.0) 70 | ruby-macho (1.4.0) 71 | thread_safe (0.3.6) 72 | typhoeus (1.4.0) 73 | ethon (>= 0.9.0) 74 | tzinfo (1.2.10) 75 | thread_safe (~> 0.1) 76 | xcodeproj (1.18.0) 77 | CFPropertyList (>= 2.3.3, < 4.0) 78 | atomos (~> 0.1.3) 79 | claide (>= 1.0.2, < 2.0) 80 | colored2 (~> 3.1) 81 | nanaimo (~> 0.3.0) 82 | 83 | PLATFORMS 84 | ruby 85 | 86 | DEPENDENCIES 87 | cocoapods 88 | 89 | BUNDLED WITH 90 | 2.1.4 91 | -------------------------------------------------------------------------------- /eCommerce/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iOS App Icon_20pt@2x 1.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "iOS App Icon_20pt@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "iOS App Icon_29pt@2x 1.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "iOS App Icon_29pt@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "iOS App Icon_40pt@2x 1.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "iOS App Icon_40pt@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iOS App Icon_60pt@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iOS App Icon_60pt@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "iOS App Icon_20pt.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "iOS App Icon_20pt@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "iOS App Icon_29pt.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "iOS App Icon_29pt@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "iOS App Icon_40pt.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "iOS App Icon_40pt@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "iOS App Icon_76pt.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "iOS App Icon_76pt@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "iOS App Icon_83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "iOS App Icon_1024pt@1x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /eCommerce.xcodeproj/xcshareddata/xcschemes/eCommerce.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /eCommerce/Products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "1000567890", 5 | "name": "Eyeshadow Palette - Set of 3 Palettes", 6 | "imageUrl": "images/1000567890.jpg", 7 | "imageLargeUrl": "images/1000567890-lg.jpg", 8 | "price": 125, 9 | "discountedPrice": 99, 10 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 11 | }, { 12 | "id": "1000567891", 13 | "name": "Waterproof Eyeliner and Mascara", 14 | "imageUrl": "images/1000567891.jpg", 15 | "imageLargeUrl": "images/1000567891-lg.jpg", 16 | "price": 27, 17 | "discountedPrice": 27, 18 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 19 | }, { 20 | "id": "1000567892", 21 | "name": "Divine Shine - Satin Rose Lipstick set", 22 | "imageUrl": "images/1000567892.jpg", 23 | "imageLargeUrl": "images/1000567892-lg.jpg", 24 | "price": 35, 25 | "discountedPrice": 28, 26 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 27 | }, { 28 | "id": "1000567893", 29 | "name": "Gloss Bomb Universal Lip Luminizer", 30 | "imageUrl": "images/1000567893.jpg", 31 | "imageLargeUrl": "images/1000567893-lg.jpg", 32 | "price": 19, 33 | "discountedPrice": 15, 34 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 35 | }, { 36 | "id": "1000567894", 37 | "name": "7-in-1 daily wear palette essentials", 38 | "imageUrl": "images/1000567894.jpg", 39 | "imageLargeUrl": "images/1000567894-lg.jpg", 40 | "price": 103, 41 | "discountedPrice": 95, 42 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 43 | }, { 44 | "id": "1000567895", 45 | "name": "EYE CARE SET - sparkle gloss eyes and lashes", 46 | "imageUrl": "images/1000567895.jpg", 47 | "imageLargeUrl": "images/1000567895-lg.jpg", 48 | "price": 59, 49 | "discountedPrice": 45, 50 | "longDescription": "By combining potent natural ingredients, ancient rituals, and modern science, ACME creates products that perform with experiences that transform. Discovery drives innovation. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vulputate volutpat rhoncus arcu porttitor in." 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon IVS eCommerce iOS Demo 2 | 3 | A demo iOS application intended as an educational tool for demonstrating how Amazon IVS can be used to build a compelling customer experience for eCommerce use-cases. 4 | 5 | Amazon IVS eCommerce iOS demo screenshots 6 | 7 | **This project is intended for education purposes only and not for production usage.** 8 | 9 | This is an iOS application that uses [Amazon IVS TimedMetadata](https://docs.aws.amazon.com/ivs/latest/userguide/SEM.html) to show products. The demo is written in [Swift](https://developer.apple.com/swift/) and showcases how customers can load and play an Amazon IVS stream and display browsable product information using TimedMetadata. 10 | 11 | This demo uses a 24/7 [looping stream](https://4c62a87c1810.us-west-2.playback.live-video.net/api/video/v1/us-west-2.049054135175.channel.onToXRHIurEP.m3u8) which emits a TimedMetadata event every few seconds. These TimedMetadata events describe product information in json format, which is used by the app to show a carousel of products, and to highlight the product being shown on stream. 12 | 13 | ## Getting Started 14 | 15 | To run this demo, you will need the following: 16 | 17 | 1. [Xcode](https://apps.apple.com/us/app/xcode/id497799835) 11 or greater. You can build this application for devices running iOS 10 or later. 18 | 2. [Cocoapods](https://cocoapods.org/), installed and up-to-date. You can quickly install cocoapods from Terminal with `sudo gem install cocoapods`. 19 | 3. [Bundler](https://bundler.io/), installed and up-to-date. 20 | 4. [Git](https://git-scm.com/) installed and up-to-date. You can quickly install git from Terminal with `xcode-select --install`. 21 | 22 | To run the demo in the iOS Simulator: 23 | 24 | 1. Clone the project from this repository 25 | 2. Navigate to the project directory using _Terminal_ 26 | 3. Run `pod install` 27 | 4. Open `eCommerce.xcworkspace` in Xcode. 28 | 5. Click `Run` in the toolbar, or press `Cmd(⌘)-R` 29 | 30 | You should see the iOS Simulator boot up and launch the demo app. This may take a few moments to complete. 31 | 32 | ## Modifying this Example 33 | 34 | ### Prerequisites 35 | 36 | **IMPORTANT NOTE:** Using your own stream will create and consume AWS resources, which will cost money. 37 | 38 | 1. Create and set up an Amazon IVS channel. [Getting started with Amazon IVS](https://docs.aws.amazon.com/ivs/latest/userguide/GSIVS.html). 39 | 40 | ### Using your own Live video 41 | 42 | 1. Open the [Amazon IVS Console](https://console.aws.amazon.com/ivs) and navigate to the channel you would like to use. 43 | 2. Copy the _Playback URL_ for the channel. The URL should end in `.m3u8`. (For example: `https://4c62a87c1810.us-west-2.playback.live-video.net/api/video/v1/us-west-2.049054135175.channel.onToXRHIurEP.m3u8`). 44 | 3. In Xcode, open `eCommerce/Constants.swift`. 45 | 4. Replace the string on line `12` with the _Playback URL_ from step 2. 46 | 5. Save and build the application. Navigate to the leftmost image in the `LIVE` carousel to view landscape video, or the second-to-leftmost image to view portrait video. 47 | 48 | ### Using your own TimedMetadata events 49 | 50 | Amazon IVS TimedMetadata provides a way to embed metadata in an Amazon IVS stream. It ensures that your users receive the metadata at the same time as the video stream, regardless of stream latency or geographic location. Learn how to embed TimedMetadata in stream: [Embedding Metadata within a Video Stream](https://docs.aws.amazon.com/ivs/latest/userguide/SEM.html). 51 | 52 | This example expects a `productId` that represents the unique identifier for a project in the `streams.json` file. 53 | 54 | ``` 55 | "metadata" : { 56 | "productId": "1000567892" 57 | } 58 | ``` 59 | 60 | ## Additional Notes 61 | 62 | For production applications, we recommend using TimedMetadata alongside services like [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon API Gateway](https://aws.amazon.com/api-gateway/), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). These services will let you store and retrieve product information in a more scalable way. See the [Amazon IVS eCommerce Web Demo](https://github.com/aws-samples/amazon-ivs-ecommerce-web-demo) for example code using these services. 63 | 64 | ## Documentation 65 | 66 | - [Amazon IVS Amazon Interactive Video Service (Amazon IVS)](https://aws.amazon.com/ivs/) is a managed live streaming solution that is quick and easy to set up, and ideal for creating interactive video experiences. Simply send your live streams to Amazon IVS and the service does everything you need to make ultra-low latency live video available to any viewer around the world, letting you focus on building interactive experiences alongside the live video. [Learn more](https://aws.amazon.com/ivs/). 67 | - [Amazon IVS docs](https://docs.aws.amazon.com/ivs/) 68 | - [User Guide](https://docs.aws.amazon.com/ivs/latest/userguide/) 69 | - [API Reference](https://docs.aws.amazon.com/ivs/latest/APIReference/) 70 | 71 | ## Known Issues 72 | 73 | - The application was written for demonstration purposes and not for production use. 74 | - Currently only tested in the us-west-2 (Oregon) region. Additional regions may be supported depending on service availability. 75 | 76 | ## License 77 | 78 | This sample code is made available under a modified MIT license. See the LICENSE file. 79 | -------------------------------------------------------------------------------- /eCommerce/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES.txt: -------------------------------------------------------------------------------- 1 | **The following images are derivatives of "flowers Icon #1382329" by Made, used under CC BY 3.0 (available at https://creativecommons.org/licenses/by/3.0/us/legalcode). 2 | 3 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-1024@1x.png 4 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-83.5@2x.png 5 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-76@2x.png 6 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png 7 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png 8 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png 9 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png 10 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png 11 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png 12 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png 13 | eCommerce/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png 14 | 15 | 16 | **The following images are used, or derivatives of images used, under the Pexels License (available at https://www.pexels.com/terms-of-service/): 17 | 18 | eCommerce/Assets.xcassets/avatar_1.imageset 19 | eCommerce/Assets.xcassets/avatar_2.imageset 20 | eCommerce/Assets.xcassets/avatar_3.imageset 21 | eCommerce/Assets.xcassets/avatar_4.imageset 22 | eCommerce/Assets.xcassets/daily_offer_1.imageset 23 | eCommerce/Assets.xcassets/daily_offer_2.imageset 24 | eCommerce/Assets.xcassets/daily_offer_3.imageset 25 | 26 | 27 | **The following fonts are used and licensed under the SIL Open Font License, Version 1.1. 28 | This license is copied below, and is also available with a FAQ at: 29 | http://scripts.sil.org/OFL 30 | 31 | eCommerce/Custom Fonts/DMSans-Bold.ttf 32 | eCommerce/Custom Fonts/DMSans-Medium.ttf 33 | eCommerce/Custom Fonts/DMSans-Regular.ttf 34 | eCommerce/Custom Fonts/DMSerifDisplay-Regular.ttf 35 | 36 | ----------------------------------------------------------- 37 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 38 | ----------------------------------------------------------- 39 | 40 | PREAMBLE 41 | The goals of the Open Font License (OFL) are to stimulate worldwide 42 | development of collaborative font projects, to support the font creation 43 | efforts of academic and linguistic communities, and to provide a free and 44 | open framework in which fonts may be shared and improved in partnership 45 | with others. 46 | 47 | The OFL allows the licensed fonts to be used, studied, modified and 48 | redistributed freely as long as they are not sold by themselves. The 49 | fonts, including any derivative works, can be bundled, embedded, 50 | redistributed and/or sold with any software provided that any reserved 51 | names are not used by derivative works. The fonts and derivatives, 52 | however, cannot be released under any other type of license. The 53 | requirement for fonts to remain under this license does not apply 54 | to any document created using the fonts or their derivatives. 55 | 56 | DEFINITIONS 57 | "Font Software" refers to the set of files released by the Copyright 58 | Holder(s) under this license and clearly marked as such. This may 59 | include source files, build scripts and documentation. 60 | 61 | "Reserved Font Name" refers to any names specified as such after the 62 | copyright statement(s). 63 | 64 | "Original Version" refers to the collection of Font Software components as 65 | distributed by the Copyright Holder(s). 66 | 67 | "Modified Version" refers to any derivative made by adding to, deleting, 68 | or substituting -- in part or in whole -- any of the components of the 69 | Original Version, by changing formats or by porting the Font Software to a 70 | new environment. 71 | 72 | "Author" refers to any designer, engineer, programmer, technical 73 | writer or other person who contributed to the Font Software. 74 | 75 | PERMISSION & CONDITIONS 76 | Permission is hereby granted, free of charge, to any person obtaining 77 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 78 | redistribute, and sell modified and unmodified copies of the Font 79 | Software, subject to the following conditions: 80 | 81 | 1) Neither the Font Software nor any of its individual components, 82 | in Original or Modified Versions, may be sold by itself. 83 | 84 | 2) Original or Modified Versions of the Font Software may be bundled, 85 | redistributed and/or sold with any software, provided that each copy 86 | contains the above copyright notice and this license. These can be 87 | included either as stand-alone text files, human-readable headers or 88 | in the appropriate machine-readable metadata fields within text or 89 | binary files as long as those fields can be easily viewed by the user. 90 | 91 | 3) No Modified Version of the Font Software may use the Reserved Font 92 | Name(s) unless explicit written permission is granted by the corresponding 93 | Copyright Holder. This restriction only applies to the primary font name as 94 | presented to the users. 95 | 96 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 97 | Software shall not be used to promote, endorse or advertise any 98 | Modified Version, except to acknowledge the contribution(s) of the 99 | Copyright Holder(s) and the Author(s) or with their explicit written 100 | permission. 101 | 102 | 5) The Font Software, modified or unmodified, in part or in whole, 103 | must be distributed entirely under this license, and must not be 104 | distributed under any other license. The requirement for fonts to 105 | remain under this license does not apply to any document created 106 | using the Font Software. 107 | 108 | TERMINATION 109 | This license becomes null and void if any of the above conditions are 110 | not met. 111 | 112 | DISCLAIMER 113 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 114 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 115 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 116 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 117 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 118 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 119 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 120 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 121 | OTHER DEALINGS IN THE FONT SOFTWARE. 122 | -------------------------------------------------------------------------------- /eCommerce/Controllers/ProductsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // eCommerce 4 | // 5 | // Created by Zingis, Uldis on 5/29/20. 6 | // Copyright © 2020 Twitch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AmazonIVSPlayer 11 | 12 | class ProductsViewController: UIViewController { 13 | 14 | // MARK: IBOutlet 15 | @IBOutlet weak var tableView: UITableView! 16 | 17 | private var playerView: PlayerView? 18 | private var headerTitleLabel: UILabel? 19 | 20 | // MARK: View Lifecycle 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | tableView.delegate = self 26 | tableView.dataSource = self 27 | tableView.layer.cornerRadius = 30 28 | 29 | loadProducts() 30 | } 31 | 32 | override func viewDidAppear(_ animated: Bool) { 33 | super.viewDidAppear(animated) 34 | createPlayerView() 35 | playerView?.startPlayback() 36 | playerView?.addApplicationLifecycleObservers() 37 | } 38 | 39 | override func viewDidDisappear(_ animated: Bool) { 40 | super.viewDidDisappear(animated) 41 | playerView?.pausePlayback() 42 | playerView?.removeApplicationLifecycleObservers() 43 | } 44 | 45 | // MARK: Custom actions 46 | 47 | private var products: [Product] = [] 48 | 49 | private func loadProducts() { 50 | if let path = Bundle.main.path(forResource: "Products", ofType: "json") { 51 | do { 52 | let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) 53 | do { 54 | self.products = try JSONDecoder().decode(Products.self, from: data).items 55 | } catch { 56 | print("‼️ Error decoding products: \(error)") 57 | } 58 | } catch { 59 | print("‼️ Error: \(error)") 60 | } 61 | } 62 | } 63 | 64 | private func createPlayerView() { 65 | playerView = Bundle.main.loadNibNamed("PlayerView", owner: self, options: nil)?[0] as? PlayerView 66 | playerView?.collapsedSize = CGRect(x: 0, y: 0, width: 100, height: 205) 67 | playerView?.frame = CGRect(x: 0, y: 0, width: 100, height: 205) 68 | playerView?.expandedSize = tableView.frame 69 | playerView?.collapsedCenterPosition = CGPoint( 70 | x: tableView.frame.width - (playerView?.frame.width ?? 0) * 0.6, 71 | y: tableView.frame.height - (playerView?.frame.height ?? 0) * 0.5 72 | ) 73 | 74 | if let playerView = playerView { 75 | view.addSubview(playerView) 76 | view.bringSubviewToFront(playerView) 77 | playerView.setup() 78 | playerView.setNeedsLayout() 79 | } 80 | 81 | playerView?.state = .expanded 82 | playerView?.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureHandler))) 83 | playerView?.products = products 84 | 85 | UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) { 86 | self.view.layoutSubviews() 87 | } 88 | } 89 | 90 | @objc private func panGestureHandler(gesture: UIPanGestureRecognizer) { 91 | guard let playerView = playerView, playerView.state == .collapsed else { 92 | return 93 | } 94 | let newLocation = gesture.location(in: view) 95 | playerView.center = newLocation 96 | 97 | if gesture.state == .ended { 98 | let isCloserToViewTop = playerView.frame.midY <= self.view.frame.height / 2 99 | 100 | if playerView.frame.midX >= self.view.frame.width / 2 { 101 | UIView.animate( 102 | withDuration: 0.5, 103 | delay: 0, 104 | usingSpringWithDamping: 1, 105 | initialSpringVelocity: 1, 106 | options: .curveEaseIn, 107 | animations: { 108 | playerView.center.x = self.tableView.frame.width - playerView.frame.width * 0.6 109 | playerView.center.y = isCloserToViewTop ? 110 | playerView.frame.height * 0.8 : 111 | self.tableView.frame.height - playerView.frame.height * 0.5 112 | }, 113 | completion: {_ in 114 | playerView.collapsedCenterPosition = playerView.center 115 | } 116 | ) 117 | } else { 118 | UIView.animate( 119 | withDuration: 0.5, 120 | delay: 0, 121 | usingSpringWithDamping: 1, 122 | initialSpringVelocity: 1, 123 | options: .curveEaseIn, 124 | animations: { 125 | playerView.center.x = playerView.frame.width * 0.6 126 | playerView.center.y = isCloserToViewTop ? 127 | playerView.frame.height * 0.8 : 128 | self.tableView.frame.height - playerView.frame.height * 0.5 129 | }, 130 | completion: {_ in 131 | playerView.collapsedCenterPosition = playerView.center 132 | } 133 | ) 134 | } 135 | } 136 | } 137 | } 138 | 139 | // MARK: UITableViewDelegate 140 | 141 | extension ProductsViewController: UITableViewDelegate { 142 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 143 | let headerView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.width, height: 120)) 144 | let title = UILabel() 145 | title.frame = CGRect.init(x: 16, y: 0, width: headerView.frame.width, height: headerView.frame.height) 146 | title.textColor = .white 147 | title.font = UIFont(name: "AmazonEmber-Bold", size: 24) 148 | title.text = "All Products" 149 | headerTitleLabel = title 150 | headerView.addSubview(title) 151 | return headerView 152 | } 153 | 154 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 155 | return 120 156 | } 157 | 158 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 159 | let offset = scrollView.contentOffset.y 160 | var alpha: CGFloat = 1 161 | if offset > 70 { 162 | alpha = (100 - offset) / 100 163 | } 164 | headerTitleLabel?.textColor = UIColor(red: 1, green: 1, blue: 1, alpha: alpha) 165 | } 166 | } 167 | 168 | // MARK: UITableViewDataSource 169 | 170 | extension ProductsViewController: UITableViewDataSource { 171 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 172 | return products.count 173 | } 174 | 175 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 176 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell") else { 177 | return UITableViewCell() 178 | } 179 | 180 | if let productView = Bundle.main.loadNibNamed("ProductView", owner: self, options: nil)?[0] as? ProductView { 181 | productView.setup(with: products[indexPath.row], in: cell.bounds) 182 | productView.showBottomSeparator(indexPath.row != products.count - 1) 183 | cell.addSubview(productView) 184 | cell.layoutSubviews() 185 | } 186 | 187 | return cell 188 | } 189 | } 190 | 191 | // MARK: PlayerViewDelegate 192 | 193 | extension ProductsViewController: PlayerViewDelegate { 194 | func show(_ alert: UIAlertController, animated: Bool) { 195 | DispatchQueue.main.async { 196 | self.present(alert, animated: animated) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /eCommerce/Views/Product/ProductView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | AmazonEmber-Medium 13 | 14 | 15 | AmazonEmber-Regular 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /eCommerce/Views/Player/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // eCommerce 4 | // 5 | // Created by Uldis Zingis on 04/10/2021. 6 | // Copyright © 2021 Twitch. All rights reserved. 7 | 8 | import UIKit 9 | import AmazonIVSPlayer 10 | 11 | enum PlayerViewState { 12 | case collapsed, expanded 13 | } 14 | 15 | protocol PlayerViewDelegate { 16 | func show(_ alert: UIAlertController, animated: Bool) 17 | } 18 | 19 | class PlayerView: UIView { 20 | var delegate: PlayerViewDelegate? 21 | var collapsedCenterPosition = CGPoint(x: 0, y: 0) 22 | var collapsedSize = CGRect(x: 0, y: 0, width: 120, height: 200) 23 | var expandedSize = UIScreen.main.bounds 24 | var products: [Product] = [] 25 | 26 | private let jsonDecoder = JSONDecoder() 27 | private var ivsView: IVSPlayerView? 28 | private var currentProduct: Product? 29 | private var controlsViewCollapsed: Bool = false 30 | private var timer: Timer? 31 | private var currentSeconds: Int = 11 32 | private var receivedProductsLine: [Product] = [] { 33 | didSet { 34 | if oldValue.count == 0 && receivedProductsLine.count == 1 { 35 | // Start showing the products 36 | showNextProductInLine() 37 | } 38 | } 39 | } 40 | 41 | // MARK: - IBOutlet 42 | 43 | @IBOutlet weak var controlsView: UIView! { 44 | didSet { 45 | controlsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(controlsViewTapped))) 46 | } 47 | } 48 | @IBOutlet weak var homeButton: UIButton! 49 | @IBOutlet weak var bufferIndicator: UIActivityIndicatorView! 50 | @IBOutlet weak var streamInfoPill: UIView! 51 | @IBOutlet weak var streamInfoPillImageView: UIImageView! 52 | @IBOutlet weak var productPopup: UIView! 53 | @IBOutlet weak var productAddToCartButton: UIButton! 54 | @IBOutlet weak var productBuyNowButton: UIButton! 55 | @IBOutlet weak var productHolderView: UIView! 56 | @IBOutlet weak var bottomGradientView: UIView! 57 | @IBOutlet weak var timerView: UIView! 58 | @IBOutlet weak var timerLabel: UILabel! 59 | @IBOutlet weak var productsPopupBottomConstraint: NSLayoutConstraint! 60 | @IBOutlet weak var homeButtonTopConstraint: NSLayoutConstraint! 61 | @IBOutlet weak var streamInfoPillTopConstraint: NSLayoutConstraint! 62 | 63 | var state: PlayerViewState? { 64 | didSet { 65 | setSizeForState() 66 | } 67 | } 68 | 69 | // MARK: View Lifecycle 70 | 71 | override init(frame: CGRect) { 72 | super.init(frame: frame) 73 | commonInit() 74 | } 75 | 76 | required init?(coder aDecoder: NSCoder) { 77 | super.init(coder: aDecoder) 78 | commonInit() 79 | } 80 | 81 | func commonInit() { 82 | ivsView = IVSPlayerView(frame: expandedSize) 83 | guard let ivsView = ivsView else { 84 | return 85 | } 86 | ivsView.backgroundColor = .black 87 | ivsView.layer.masksToBounds = true 88 | ivsView.clipsToBounds = true 89 | ivsView.videoGravity = AVLayerVideoGravity.resizeAspectFill 90 | ivsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapsedPlayerTapped))) 91 | self.addSubview(ivsView) 92 | 93 | if let url = URL(string: Constants.streamUrl) { 94 | loadStream(from: url) 95 | startPlayback() 96 | } 97 | } 98 | 99 | deinit { 100 | timer?.invalidate() 101 | } 102 | 103 | func addApplicationLifecycleObservers() { 104 | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil) 105 | NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) 106 | } 107 | 108 | func removeApplicationLifecycleObservers() { 109 | NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) 110 | NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) 111 | } 112 | 113 | @objc private func applicationDidEnterBackground(notification: Notification) { 114 | if player?.state == .playing || player?.state == .buffering { 115 | pausePlayback() 116 | } 117 | } 118 | 119 | @objc private func applicationWillEnterForeground(notification: Notification) { 120 | startPlayback() 121 | } 122 | 123 | func setup() { 124 | streamInfoPill.backgroundColor = .white 125 | streamInfoPill.layer.cornerRadius = 25 126 | streamInfoPillImageView.layer.cornerRadius = streamInfoPillImageView.frame.size.width / 2 127 | if let imageUrl = URL(string: "https://ecommerce.ivsdemos.com/images/profile.png") { 128 | Image.getFrom(imageUrl) { [weak self] (image) in 129 | DispatchQueue.main.async { 130 | self?.streamInfoPillImageView.image = image 131 | } 132 | } 133 | } 134 | productAddToCartButton.layer.cornerRadius = 4 135 | productBuyNowButton.layer.cornerRadius = 4 136 | productPopup.layer.cornerRadius = 16 137 | timerView.layer.cornerRadius = 16 138 | timerView.isHidden = true 139 | homeButton.layer.cornerRadius = homeButton.layer.bounds.width / 2 140 | homeButton.titleLabel?.text = "" 141 | bringSubviewToFront(controlsView) 142 | 143 | let gradient = CAGradientLayer() 144 | gradient.colors = [ 145 | UIColor(red: 0, green: 0, blue: 0, alpha: 0).cgColor, 146 | UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor 147 | ] 148 | gradient.locations = [0.1, 1.0] 149 | gradient.frame = bottomGradientView.bounds 150 | bottomGradientView.layer.insertSublayer(gradient, at: 0) 151 | } 152 | 153 | // MARK: Custom actions 154 | 155 | private func setSizeForState() { 156 | switch state { 157 | case .collapsed: 158 | self.frame = collapsedSize 159 | self.layer.cornerRadius = 10 160 | ivsView?.frame = self.bounds 161 | ivsView?.layer.cornerRadius = 10 162 | self.center = collapsedCenterPosition 163 | toggleControlsView(true) 164 | 165 | case .expanded: 166 | self.frame = expandedSize 167 | self.layer.cornerRadius = 30 168 | ivsView?.frame = self.bounds 169 | ivsView?.layer.cornerRadius = 30 170 | toggleControlsView(false) 171 | 172 | case .none: 173 | break 174 | } 175 | 176 | setNeedsLayout() 177 | layoutIfNeeded() 178 | } 179 | 180 | private func toggleControlsView(_ hide: Bool? = nil) { 181 | if let hide = hide { 182 | controlsView.isHidden = hide 183 | } else { 184 | controlsView.isHidden.toggle() 185 | } 186 | } 187 | 188 | private func showNextProductInLine() { 189 | if let nextProductInLine = receivedProductsLine.first { 190 | show(nextProductInLine) 191 | if !receivedProductsLine.isEmpty { 192 | receivedProductsLine.remove(at: 0) 193 | } 194 | } 195 | } 196 | 197 | private func show(_ product: Product) { 198 | guard state != .collapsed, currentProduct == nil else { 199 | return 200 | } 201 | 202 | self.layoutIfNeeded() 203 | if productPopup.isHidden { 204 | if let productView = Bundle.main.loadNibNamed("ProductView", owner: self, options: nil)?[0] as? ProductView { 205 | productView.setup(with: product, in: productHolderView.bounds) 206 | productHolderView.addSubview(productView) 207 | productHolderView.layoutSubviews() 208 | } 209 | self.productPopup.isHidden = false 210 | self.timerView.isHidden = self.controlsViewCollapsed 211 | 212 | UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { 213 | self.productsPopupBottomConstraint.constant = self.controlsViewCollapsed ? -30 : 50 214 | self.layoutIfNeeded() 215 | } completion: { _ in 216 | self.startCountdown() 217 | } 218 | currentProduct = product 219 | if !receivedProductsLine.isEmpty { 220 | receivedProductsLine.remove(at: 0) 221 | } 222 | } else { 223 | UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) { 224 | self.productsPopupBottomConstraint.constant = -self.productPopup.frame.height 225 | self.layoutIfNeeded() 226 | } completion: { _ in 227 | self.currentProduct = nil 228 | self.productHolderView.subviews[0].removeFromSuperview() 229 | self.productPopup.isHidden = true 230 | self.timerView.isHidden = true 231 | self.show(product) 232 | } 233 | } 234 | } 235 | 236 | private func startCountdown() { 237 | if let timer = timer { 238 | timer.invalidate() 239 | } 240 | currentSeconds = 11 241 | timerUpdated() 242 | timer = Timer.scheduledTimer(timeInterval: 1, 243 | target: self, 244 | selector: #selector(timerUpdated), 245 | userInfo: nil, 246 | repeats: true) 247 | } 248 | 249 | @objc private func timerUpdated() { 250 | currentSeconds -= 1 251 | timerLabel.text = "0:\(currentSeconds < 10 ? "0" : "")\(currentSeconds)" 252 | if currentSeconds == 0 { 253 | timerView.isHidden = true 254 | currentProduct = nil 255 | timer?.invalidate() 256 | showNextProductInLine() 257 | } 258 | } 259 | 260 | // MARK: - Player 261 | 262 | var player: IVSPlayer? { 263 | didSet { 264 | if oldValue != nil { 265 | removeApplicationLifecycleObservers() 266 | } 267 | ivsView?.player = player 268 | if player != nil { 269 | addApplicationLifecycleObservers() 270 | } 271 | } 272 | } 273 | 274 | // MARK: Playback Control 275 | 276 | private func loadStream(from streamURL: URL) { 277 | let player: IVSPlayer 278 | if let existingPlayer = self.player { 279 | player = existingPlayer 280 | } else { 281 | player = IVSPlayer() 282 | player.delegate = self 283 | self.player = player 284 | print("ℹ️ Player initialized: version \(player.version)") 285 | } 286 | player.load(streamURL) 287 | } 288 | 289 | func startPlayback() { 290 | player?.play() 291 | } 292 | 293 | func pausePlayback() { 294 | player?.pause() 295 | } 296 | 297 | // MARK: - IBAction 298 | 299 | @IBAction func didTapHomeButton(_ sender: Any) { 300 | state = .collapsed 301 | } 302 | 303 | @objc private func collapsedPlayerTapped() { 304 | if state == .collapsed { 305 | state = .expanded 306 | showNextProductInLine() 307 | // self.timerView.isHidden = false 308 | } else { 309 | controlsViewTapped() 310 | } 311 | } 312 | 313 | @objc private func controlsViewTapped() { 314 | controlsViewCollapsed.toggle() 315 | UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { 316 | self.streamInfoPillTopConstraint.constant = self.controlsViewCollapsed ? -100 : 8 317 | self.homeButtonTopConstraint.constant = self.controlsViewCollapsed ? -100 : 8 318 | self.productsPopupBottomConstraint.constant = self.controlsViewCollapsed ? -30 : 50 319 | self.timerView.isHidden = self.currentProduct == nil || self.controlsViewCollapsed 320 | self.bottomGradientView.isHidden = self.currentProduct == nil || !self.controlsViewCollapsed 321 | self.layoutIfNeeded() 322 | } 323 | } 324 | 325 | 326 | // MARK: State display 327 | 328 | private func presentError(_ error: Error, componentName: String) { 329 | let alert = UIAlertController(title: "\(componentName) Error", message: String(reflecting: error), preferredStyle: .alert) 330 | alert.addAction(UIAlertAction(title: "Close", style: .cancel)) 331 | delegate?.show(alert, animated: true) 332 | } 333 | 334 | private func presentAlert(_ message: String, componentName: String) { 335 | let alert = UIAlertController(title: "\(componentName)", message: message, preferredStyle: .alert) 336 | alert.addAction(UIAlertAction(title: "Close", style: .cancel)) 337 | delegate?.show(alert, animated: true) 338 | } 339 | 340 | private func updateForState(_ state: IVSPlayer.State) { 341 | if state == .buffering { 342 | bufferIndicator?.startAnimating() 343 | } else { 344 | bufferIndicator?.stopAnimating() 345 | } 346 | } 347 | } 348 | 349 | // MARK: - IVSPlayer.Delegate 350 | 351 | extension PlayerView: IVSPlayer.Delegate { 352 | func player(_ player: IVSPlayer, didChangeState state: IVSPlayer.State) { 353 | updateForState(state) 354 | } 355 | 356 | func player(_ player: IVSPlayer, didFailWithError error: Error) { 357 | presentError(error, componentName: "Player") 358 | } 359 | 360 | func player(_ player: IVSPlayer, didOutputCue cue: IVSCue) { 361 | switch cue { 362 | case let textMetadataCue as IVSTextMetadataCue: 363 | print("ℹ Received Timed Metadata (\(textMetadataCue.textDescription)): \(textMetadataCue.text)") 364 | guard let jsonData = textMetadataCue.text.data(using: .utf8) else { 365 | return 366 | } 367 | do { 368 | let json = try jsonDecoder.decode([String: String].self, from: jsonData) 369 | if let id = json["productId"], let product = products.first(where: { $0.id == id }) { 370 | if receivedProductsLine.last != product { 371 | receivedProductsLine.append(product) 372 | } 373 | } 374 | } catch { 375 | print("Could not decode productId: \(error)") 376 | } 377 | case let textCue as IVSTextCue: 378 | print("ℹ Received Text Cue: “\(textCue.text)”") 379 | default: 380 | print("ℹ Received unknown cue (type \(cue.type))") 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /eCommerce/Views/Player/PlayerView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | AmazonEmber-Bold 14 | 15 | 16 | AmazonEmber-Medium 17 | 18 | 19 | AmazonEmber-Regular 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 150 | 151 | 152 | 153 | 159 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /eCommerce.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4664E33F2481198C00B75A8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4664E33E2481198C00B75A8B /* AppDelegate.swift */; }; 11 | 4664E3432481198C00B75A8B /* ProductsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4664E3422481198C00B75A8B /* ProductsViewController.swift */; }; 12 | 4664E3462481198C00B75A8B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4664E3442481198C00B75A8B /* Main.storyboard */; }; 13 | 4664E3482481198E00B75A8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4664E3472481198E00B75A8B /* Assets.xcassets */; }; 14 | 4664E34B2481198E00B75A8B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4664E3492481198E00B75A8B /* LaunchScreen.storyboard */; }; 15 | 46B18B77248E262C00BA0AF0 /* Products.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B18B76248E262C00BA0AF0 /* Products.swift */; }; 16 | 5C3BD472270AFCF3004BCE01 /* PlayerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5C3BD471270AFCF3004BCE01 /* PlayerView.xib */; }; 17 | 5C3BD474270AFD15004BCE01 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3BD473270AFD15004BCE01 /* PlayerView.swift */; }; 18 | 5C56C5F5270C826700473E52 /* ProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C56C5F4270C826700473E52 /* ProductView.swift */; }; 19 | 5C56C5F7270C82AF00473E52 /* ProductView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5C56C5F6270C82AF00473E52 /* ProductView.xib */; }; 20 | 5C56C5FA270C8E3500473E52 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C56C5F9270C8E3500473E52 /* Image.swift */; }; 21 | 5CEC613F2707426400E1C472 /* AmazonEmber-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5CEC61392707426300E1C472 /* AmazonEmber-Regular.ttf */; }; 22 | 5CEC61402707426400E1C472 /* AmazonEmber-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5CEC613A2707426300E1C472 /* AmazonEmber-Bold.ttf */; }; 23 | 5CEC61412707426400E1C472 /* AmazonEmber-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5CEC613B2707426300E1C472 /* AmazonEmber-Medium.ttf */; }; 24 | 5CEC61422707426400E1C472 /* AmazonEmber-Heavy.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5CEC613C2707426300E1C472 /* AmazonEmber-Heavy.ttf */; }; 25 | 5CEC61472707521900E1C472 /* Products.json in Resources */ = {isa = PBXBuildFile; fileRef = 5CEC61382707422200E1C472 /* Products.json */; }; 26 | 5CEC61492707553600E1C472 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEC61482707553600E1C472 /* Constants.swift */; }; 27 | E29B5C62A0CC8F1F22C7E1DE /* libPods-eCommerce.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B0604796E9C72975B9EAC568 /* libPods-eCommerce.a */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 2AEAF43F3A53E170ED4F9EAB /* Pods-IVSClients-eCommerce.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IVSClients-eCommerce.debug.xcconfig"; path = "../eCommerce/Pods/Target Support Files/Pods-IVSClients-eCommerce/Pods-IVSClients-eCommerce.debug.xcconfig"; sourceTree = ""; }; 32 | 460A58F52490E2F6009B5B7B /* eCommerce.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = eCommerce.entitlements; sourceTree = ""; }; 33 | 4664E33B2481198C00B75A8B /* eCommerce.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = eCommerce.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 4664E33E2481198C00B75A8B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 4664E3422481198C00B75A8B /* ProductsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsViewController.swift; sourceTree = ""; }; 36 | 4664E3452481198C00B75A8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | 4664E3472481198E00B75A8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 4664E34A2481198E00B75A8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 4664E34C2481198E00B75A8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 46B18B76248E262C00BA0AF0 /* Products.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Products.swift; sourceTree = ""; }; 41 | 5C3BD471270AFCF3004BCE01 /* PlayerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PlayerView.xib; sourceTree = ""; }; 42 | 5C3BD473270AFD15004BCE01 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; 43 | 5C56C5F4270C826700473E52 /* ProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductView.swift; sourceTree = ""; }; 44 | 5C56C5F6270C82AF00473E52 /* ProductView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProductView.xib; sourceTree = ""; }; 45 | 5C56C5F9270C8E3500473E52 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 46 | 5CEC61382707422200E1C472 /* Products.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Products.json; sourceTree = ""; }; 47 | 5CEC61392707426300E1C472 /* AmazonEmber-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AmazonEmber-Regular.ttf"; sourceTree = ""; }; 48 | 5CEC613A2707426300E1C472 /* AmazonEmber-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AmazonEmber-Bold.ttf"; sourceTree = ""; }; 49 | 5CEC613B2707426300E1C472 /* AmazonEmber-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AmazonEmber-Medium.ttf"; sourceTree = ""; }; 50 | 5CEC613C2707426300E1C472 /* AmazonEmber-Heavy.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "AmazonEmber-Heavy.ttf"; sourceTree = ""; }; 51 | 5CEC61482707553600E1C472 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 52 | 77B6A9BA9BE03B89EBC75B09 /* Pods-eCommerce.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-eCommerce.debug.xcconfig"; path = "../amazon-ivs-ecommerce-ios-demo/Pods/Target Support Files/Pods-eCommerce/Pods-eCommerce.debug.xcconfig"; sourceTree = ""; }; 53 | 83D51A534FF3FC792E25D0BC /* Pods-eCommerce.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-eCommerce.release.xcconfig"; path = "../amazon-ivs-ecommerce-ios-demo/Pods/Target Support Files/Pods-eCommerce/Pods-eCommerce.release.xcconfig"; sourceTree = ""; }; 54 | B0604796E9C72975B9EAC568 /* libPods-eCommerce.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-eCommerce.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | BE5D00185D8E188697B92D43 /* Pods-IVSClients-eCommerce.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IVSClients-eCommerce.release.xcconfig"; path = "../eCommerce/Pods/Target Support Files/Pods-IVSClients-eCommerce/Pods-IVSClients-eCommerce.release.xcconfig"; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 4664E3382481198C00B75A8B /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | E29B5C62A0CC8F1F22C7E1DE /* libPods-eCommerce.a in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 460A58F6249228CE009B5B7B /* Custom Fonts */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 5CEC613A2707426300E1C472 /* AmazonEmber-Bold.ttf */, 74 | 5CEC613C2707426300E1C472 /* AmazonEmber-Heavy.ttf */, 75 | 5CEC613B2707426300E1C472 /* AmazonEmber-Medium.ttf */, 76 | 5CEC61392707426300E1C472 /* AmazonEmber-Regular.ttf */, 77 | ); 78 | path = "Custom Fonts"; 79 | sourceTree = ""; 80 | }; 81 | 4664E3322481198C00B75A8B = { 82 | isa = PBXGroup; 83 | children = ( 84 | 4664E33D2481198C00B75A8B /* eCommerce */, 85 | 4664E33C2481198C00B75A8B /* Products */, 86 | 91C8E2F2338A7F4553FD88D0 /* Pods */, 87 | A15985E20FD044D012EB2E45 /* Frameworks */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | 4664E33C2481198C00B75A8B /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 4664E33B2481198C00B75A8B /* eCommerce.app */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 4664E33D2481198C00B75A8B /* eCommerce */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 460A58F52490E2F6009B5B7B /* eCommerce.entitlements */, 103 | 4664E33E2481198C00B75A8B /* AppDelegate.swift */, 104 | 5CEC61382707422200E1C472 /* Products.json */, 105 | 46B18B78248E29BC00BA0AF0 /* Models */, 106 | 4674B023248E322B0047FD37 /* Views */, 107 | 46B18B7B248E29E500BA0AF0 /* Controllers */, 108 | 4664E3472481198E00B75A8B /* Assets.xcassets */, 109 | 5CEC61482707553600E1C472 /* Constants.swift */, 110 | 4664E3442481198C00B75A8B /* Main.storyboard */, 111 | 4664E3492481198E00B75A8B /* LaunchScreen.storyboard */, 112 | 4664E34C2481198E00B75A8B /* Info.plist */, 113 | 460A58F6249228CE009B5B7B /* Custom Fonts */, 114 | ); 115 | path = eCommerce; 116 | sourceTree = ""; 117 | }; 118 | 4674B023248E322B0047FD37 /* Views */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 5C56C5F8270C82B500473E52 /* Product */, 122 | 5C3BD475270B000A004BCE01 /* Player */, 123 | ); 124 | path = Views; 125 | sourceTree = ""; 126 | }; 127 | 46B18B78248E29BC00BA0AF0 /* Models */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 46B18B76248E262C00BA0AF0 /* Products.swift */, 131 | 5C56C5F9270C8E3500473E52 /* Image.swift */, 132 | ); 133 | path = Models; 134 | sourceTree = ""; 135 | }; 136 | 46B18B7B248E29E500BA0AF0 /* Controllers */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 4664E3422481198C00B75A8B /* ProductsViewController.swift */, 140 | ); 141 | path = Controllers; 142 | sourceTree = ""; 143 | }; 144 | 5C3BD475270B000A004BCE01 /* Player */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 5C3BD473270AFD15004BCE01 /* PlayerView.swift */, 148 | 5C3BD471270AFCF3004BCE01 /* PlayerView.xib */, 149 | ); 150 | path = Player; 151 | sourceTree = ""; 152 | }; 153 | 5C56C5F8270C82B500473E52 /* Product */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 5C56C5F4270C826700473E52 /* ProductView.swift */, 157 | 5C56C5F6270C82AF00473E52 /* ProductView.xib */, 158 | ); 159 | path = Product; 160 | sourceTree = ""; 161 | }; 162 | 91C8E2F2338A7F4553FD88D0 /* Pods */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 2AEAF43F3A53E170ED4F9EAB /* Pods-IVSClients-eCommerce.debug.xcconfig */, 166 | BE5D00185D8E188697B92D43 /* Pods-IVSClients-eCommerce.release.xcconfig */, 167 | 77B6A9BA9BE03B89EBC75B09 /* Pods-eCommerce.debug.xcconfig */, 168 | 83D51A534FF3FC792E25D0BC /* Pods-eCommerce.release.xcconfig */, 169 | ); 170 | name = Pods; 171 | path = ../Pods; 172 | sourceTree = ""; 173 | }; 174 | A15985E20FD044D012EB2E45 /* Frameworks */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | B0604796E9C72975B9EAC568 /* libPods-eCommerce.a */, 178 | ); 179 | name = Frameworks; 180 | sourceTree = ""; 181 | }; 182 | /* End PBXGroup section */ 183 | 184 | /* Begin PBXNativeTarget section */ 185 | 4664E33A2481198C00B75A8B /* eCommerce */ = { 186 | isa = PBXNativeTarget; 187 | buildConfigurationList = 4664E34F2481198E00B75A8B /* Build configuration list for PBXNativeTarget "eCommerce" */; 188 | buildPhases = ( 189 | 82FFFFD775BD0E61B119FD5F /* [CP] Check Pods Manifest.lock */, 190 | 4664E3372481198C00B75A8B /* Sources */, 191 | 4664E3382481198C00B75A8B /* Frameworks */, 192 | 4664E3392481198C00B75A8B /* Resources */, 193 | D6A3B8A895ACD01F9D1E3776 /* [CP] Embed Pods Frameworks */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | ); 199 | name = eCommerce; 200 | productName = eCommerce; 201 | productReference = 4664E33B2481198C00B75A8B /* eCommerce.app */; 202 | productType = "com.apple.product-type.application"; 203 | }; 204 | /* End PBXNativeTarget section */ 205 | 206 | /* Begin PBXProject section */ 207 | 4664E3332481198C00B75A8B /* Project object */ = { 208 | isa = PBXProject; 209 | attributes = { 210 | LastSwiftUpdateCheck = 1130; 211 | LastUpgradeCheck = 1130; 212 | ORGANIZATIONNAME = Twitch; 213 | TargetAttributes = { 214 | 4664E33A2481198C00B75A8B = { 215 | CreatedOnToolsVersion = 11.3; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 4664E3362481198C00B75A8B /* Build configuration list for PBXProject "eCommerce" */; 220 | compatibilityVersion = "Xcode 9.3"; 221 | developmentRegion = en; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | en, 225 | Base, 226 | ); 227 | mainGroup = 4664E3322481198C00B75A8B; 228 | productRefGroup = 4664E33C2481198C00B75A8B /* Products */; 229 | projectDirPath = ""; 230 | projectRoot = ""; 231 | targets = ( 232 | 4664E33A2481198C00B75A8B /* eCommerce */, 233 | ); 234 | }; 235 | /* End PBXProject section */ 236 | 237 | /* Begin PBXResourcesBuildPhase section */ 238 | 4664E3392481198C00B75A8B /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 4664E34B2481198E00B75A8B /* LaunchScreen.storyboard in Resources */, 243 | 5CEC61412707426400E1C472 /* AmazonEmber-Medium.ttf in Resources */, 244 | 5C3BD472270AFCF3004BCE01 /* PlayerView.xib in Resources */, 245 | 5CEC613F2707426400E1C472 /* AmazonEmber-Regular.ttf in Resources */, 246 | 4664E3482481198E00B75A8B /* Assets.xcassets in Resources */, 247 | 5CEC61402707426400E1C472 /* AmazonEmber-Bold.ttf in Resources */, 248 | 5CEC61422707426400E1C472 /* AmazonEmber-Heavy.ttf in Resources */, 249 | 4664E3462481198C00B75A8B /* Main.storyboard in Resources */, 250 | 5C56C5F7270C82AF00473E52 /* ProductView.xib in Resources */, 251 | 5CEC61472707521900E1C472 /* Products.json in Resources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXResourcesBuildPhase section */ 256 | 257 | /* Begin PBXShellScriptBuildPhase section */ 258 | 82FFFFD775BD0E61B119FD5F /* [CP] Check Pods Manifest.lock */ = { 259 | isa = PBXShellScriptBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | inputFileListPaths = ( 264 | ); 265 | inputPaths = ( 266 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 267 | "${PODS_ROOT}/Manifest.lock", 268 | ); 269 | name = "[CP] Check Pods Manifest.lock"; 270 | outputFileListPaths = ( 271 | ); 272 | outputPaths = ( 273 | "$(DERIVED_FILE_DIR)/Pods-eCommerce-checkManifestLockResult.txt", 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | shellPath = /bin/sh; 277 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 278 | showEnvVarsInLog = 0; 279 | }; 280 | D6A3B8A895ACD01F9D1E3776 /* [CP] Embed Pods Frameworks */ = { 281 | isa = PBXShellScriptBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | inputFileListPaths = ( 286 | "${PODS_ROOT}/Target Support Files/Pods-eCommerce/Pods-eCommerce-frameworks-${CONFIGURATION}-input-files.xcfilelist", 287 | ); 288 | name = "[CP] Embed Pods Frameworks"; 289 | outputFileListPaths = ( 290 | "${PODS_ROOT}/Target Support Files/Pods-eCommerce/Pods-eCommerce-frameworks-${CONFIGURATION}-output-files.xcfilelist", 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | shellPath = /bin/sh; 294 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-eCommerce/Pods-eCommerce-frameworks.sh\"\n"; 295 | showEnvVarsInLog = 0; 296 | }; 297 | /* End PBXShellScriptBuildPhase section */ 298 | 299 | /* Begin PBXSourcesBuildPhase section */ 300 | 4664E3372481198C00B75A8B /* Sources */ = { 301 | isa = PBXSourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | 5C56C5FA270C8E3500473E52 /* Image.swift in Sources */, 305 | 5C56C5F5270C826700473E52 /* ProductView.swift in Sources */, 306 | 5CEC61492707553600E1C472 /* Constants.swift in Sources */, 307 | 4664E3432481198C00B75A8B /* ProductsViewController.swift in Sources */, 308 | 5C3BD474270AFD15004BCE01 /* PlayerView.swift in Sources */, 309 | 4664E33F2481198C00B75A8B /* AppDelegate.swift in Sources */, 310 | 46B18B77248E262C00BA0AF0 /* Products.swift in Sources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXSourcesBuildPhase section */ 315 | 316 | /* Begin PBXVariantGroup section */ 317 | 4664E3442481198C00B75A8B /* Main.storyboard */ = { 318 | isa = PBXVariantGroup; 319 | children = ( 320 | 4664E3452481198C00B75A8B /* Base */, 321 | ); 322 | name = Main.storyboard; 323 | sourceTree = ""; 324 | }; 325 | 4664E3492481198E00B75A8B /* LaunchScreen.storyboard */ = { 326 | isa = PBXVariantGroup; 327 | children = ( 328 | 4664E34A2481198E00B75A8B /* Base */, 329 | ); 330 | name = LaunchScreen.storyboard; 331 | sourceTree = ""; 332 | }; 333 | /* End PBXVariantGroup section */ 334 | 335 | /* Begin XCBuildConfiguration section */ 336 | 4664E34D2481198E00B75A8B /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CLANG_ANALYZER_NONNULL = YES; 341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 343 | CLANG_CXX_LIBRARY = "libc++"; 344 | CLANG_ENABLE_MODULES = YES; 345 | CLANG_ENABLE_OBJC_ARC = YES; 346 | CLANG_ENABLE_OBJC_WEAK = YES; 347 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 348 | CLANG_WARN_BOOL_CONVERSION = YES; 349 | CLANG_WARN_COMMA = YES; 350 | CLANG_WARN_CONSTANT_CONVERSION = YES; 351 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 352 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 353 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 354 | CLANG_WARN_EMPTY_BODY = YES; 355 | CLANG_WARN_ENUM_CONVERSION = YES; 356 | CLANG_WARN_INFINITE_RECURSION = YES; 357 | CLANG_WARN_INT_CONVERSION = YES; 358 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 360 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 361 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 363 | CLANG_WARN_STRICT_PROTOTYPES = YES; 364 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | COPY_PHASE_STRIP = NO; 369 | DEBUG_INFORMATION_FORMAT = dwarf; 370 | ENABLE_STRICT_OBJC_MSGSEND = YES; 371 | ENABLE_TESTABILITY = YES; 372 | GCC_C_LANGUAGE_STANDARD = gnu11; 373 | GCC_DYNAMIC_NO_PIC = NO; 374 | GCC_NO_COMMON_BLOCKS = YES; 375 | GCC_OPTIMIZATION_LEVEL = 0; 376 | GCC_PREPROCESSOR_DEFINITIONS = ( 377 | "DEBUG=1", 378 | "$(inherited)", 379 | ); 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 382 | GCC_WARN_UNDECLARED_SELECTOR = YES; 383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 384 | GCC_WARN_UNUSED_FUNCTION = YES; 385 | GCC_WARN_UNUSED_VARIABLE = YES; 386 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 387 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 388 | MTL_FAST_MATH = YES; 389 | ONLY_ACTIVE_ARCH = YES; 390 | SDKROOT = iphoneos; 391 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 392 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 393 | }; 394 | name = Debug; 395 | }; 396 | 4664E34E2481198E00B75A8B /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ALWAYS_SEARCH_USER_PATHS = NO; 400 | CLANG_ANALYZER_NONNULL = YES; 401 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 402 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 403 | CLANG_CXX_LIBRARY = "libc++"; 404 | CLANG_ENABLE_MODULES = YES; 405 | CLANG_ENABLE_OBJC_ARC = YES; 406 | CLANG_ENABLE_OBJC_WEAK = YES; 407 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 408 | CLANG_WARN_BOOL_CONVERSION = YES; 409 | CLANG_WARN_COMMA = YES; 410 | CLANG_WARN_CONSTANT_CONVERSION = YES; 411 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 412 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 413 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 414 | CLANG_WARN_EMPTY_BODY = YES; 415 | CLANG_WARN_ENUM_CONVERSION = YES; 416 | CLANG_WARN_INFINITE_RECURSION = YES; 417 | CLANG_WARN_INT_CONVERSION = YES; 418 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 420 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 422 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 423 | CLANG_WARN_STRICT_PROTOTYPES = YES; 424 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 425 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 426 | CLANG_WARN_UNREACHABLE_CODE = YES; 427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 428 | COPY_PHASE_STRIP = NO; 429 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 430 | ENABLE_NS_ASSERTIONS = NO; 431 | ENABLE_STRICT_OBJC_MSGSEND = YES; 432 | GCC_C_LANGUAGE_STANDARD = gnu11; 433 | GCC_NO_COMMON_BLOCKS = YES; 434 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 435 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 436 | GCC_WARN_UNDECLARED_SELECTOR = YES; 437 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 438 | GCC_WARN_UNUSED_FUNCTION = YES; 439 | GCC_WARN_UNUSED_VARIABLE = YES; 440 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 441 | MTL_ENABLE_DEBUG_INFO = NO; 442 | MTL_FAST_MATH = YES; 443 | SDKROOT = iphoneos; 444 | SWIFT_COMPILATION_MODE = wholemodule; 445 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 446 | VALIDATE_PRODUCT = YES; 447 | }; 448 | name = Release; 449 | }; 450 | 4664E3502481198E00B75A8B /* Debug */ = { 451 | isa = XCBuildConfiguration; 452 | baseConfigurationReference = 77B6A9BA9BE03B89EBC75B09 /* Pods-eCommerce.debug.xcconfig */; 453 | buildSettings = { 454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 455 | CODE_SIGN_ENTITLEMENTS = eCommerce/eCommerce.entitlements; 456 | CODE_SIGN_STYLE = Automatic; 457 | DEVELOPMENT_TEAM = ""; 458 | INFOPLIST_FILE = eCommerce/Info.plist; 459 | LD_RUNPATH_SEARCH_PATHS = ( 460 | "$(inherited)", 461 | "@executable_path/Frameworks", 462 | ); 463 | PRODUCT_BUNDLE_IDENTIFIER = tv.twitch.starfruit.demo.eCommerce; 464 | PRODUCT_NAME = "$(TARGET_NAME)"; 465 | SWIFT_VERSION = 5.0; 466 | TARGETED_DEVICE_FAMILY = "1,2"; 467 | }; 468 | name = Debug; 469 | }; 470 | 4664E3512481198E00B75A8B /* Release */ = { 471 | isa = XCBuildConfiguration; 472 | baseConfigurationReference = 83D51A534FF3FC792E25D0BC /* Pods-eCommerce.release.xcconfig */; 473 | buildSettings = { 474 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 475 | CODE_SIGN_ENTITLEMENTS = eCommerce/eCommerce.entitlements; 476 | CODE_SIGN_STYLE = Automatic; 477 | DEVELOPMENT_TEAM = ""; 478 | INFOPLIST_FILE = eCommerce/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = ( 480 | "$(inherited)", 481 | "@executable_path/Frameworks", 482 | ); 483 | PRODUCT_BUNDLE_IDENTIFIER = tv.twitch.starfruit.demo.eCommerce; 484 | PRODUCT_NAME = "$(TARGET_NAME)"; 485 | SWIFT_VERSION = 5.0; 486 | TARGETED_DEVICE_FAMILY = "1,2"; 487 | }; 488 | name = Release; 489 | }; 490 | /* End XCBuildConfiguration section */ 491 | 492 | /* Begin XCConfigurationList section */ 493 | 4664E3362481198C00B75A8B /* Build configuration list for PBXProject "eCommerce" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 4664E34D2481198E00B75A8B /* Debug */, 497 | 4664E34E2481198E00B75A8B /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | 4664E34F2481198E00B75A8B /* Build configuration list for PBXNativeTarget "eCommerce" */ = { 503 | isa = XCConfigurationList; 504 | buildConfigurations = ( 505 | 4664E3502481198E00B75A8B /* Debug */, 506 | 4664E3512481198E00B75A8B /* Release */, 507 | ); 508 | defaultConfigurationIsVisible = 0; 509 | defaultConfigurationName = Release; 510 | }; 511 | /* End XCConfigurationList section */ 512 | }; 513 | rootObject = 4664E3332481198C00B75A8B /* Project object */; 514 | } 515 | --------------------------------------------------------------------------------