diff options
Diffstat (limited to 'Example/tvOSSample/tvOSSample')
27 files changed, 1109 insertions, 0 deletions
diff --git a/Example/tvOSSample/tvOSSample/AppDelegate.swift b/Example/tvOSSample/tvOSSample/AppDelegate.swift new file mode 100644 index 0000000..9a0d052 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/AppDelegate.swift @@ -0,0 +1,29 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseCore + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + FirebaseApp.configure() + return true + } +} + diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..7f06667 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 0000000..d29f024 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..7f06667 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..7f06667 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..16a370d --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 0000000..d29f024 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..16a370d --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..16a370d --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 0000000..b03ded1 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - App Store.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 0000000..16a370d --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000..16a370d --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/Assets.xcassets/LaunchImage.launchimage/Contents.json b/Example/tvOSSample/tvOSSample/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..d746a60 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/Example/tvOSSample/tvOSSample/AuthLoginViewController.swift b/Example/tvOSSample/tvOSSample/AuthLoginViewController.swift new file mode 100644 index 0000000..dcf72d4 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/AuthLoginViewController.swift @@ -0,0 +1,40 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseAuth + +class AuthLoginViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/Example/tvOSSample/tvOSSample/AuthViewController.swift b/Example/tvOSSample/tvOSSample/AuthViewController.swift new file mode 100644 index 0000000..72351d3 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/AuthViewController.swift @@ -0,0 +1,86 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseAuth + +class AuthViewController: UIViewController { + + // MARK: - User Interface + + /// A stackview containing all of the buttons to providers (Email, OAuth, etc). + @IBOutlet weak var providers: UIStackView! + + /// A stackview containing a signed in label and sign out button. + @IBOutlet weak var signedIn: UIStackView! + + /// A label to display the status for the signed in user. + @IBOutlet weak var signInStatus: UILabel! + + // MARK: - User Actions + + @IBAction func signOutButtonHit(_ sender: UIButton) { + // Sign out via Auth and update the UI. + try? Auth.auth().signOut() + + setUserSignedIn(nil) + } + + // MARK: - View Controller Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Update the UI based on the current user (if there is one). + setUserSignedIn(Auth.auth().currentUser) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + let destination = segue.destination + if let emailVC = destination as? EmailLoginViewController { + emailVC.delegate = self + } + } + + // MARK: - Internal Helpers + + private func setUserSignedIn(_ user: User?) { + if let user = user { + providers.isHidden = true + signedIn.isHidden = false + + signInStatus.text = "User is signed in via \(user.providerID) and the UID \(user.uid)" + } else { + // User is signed out, hide the signed in state and show the providers. + providers.isHidden = false + signedIn.isHidden = true + } + } +} + +// MARK: - EmailLoginDelegate conformance. + +extension AuthViewController: EmailLoginDelegate { + func emailLogin(_ controller: EmailLoginViewController, signedInAs user: User) { + setUserSignedIn(user) + dismiss(animated: true) + } + + func emailLogin(_ controller: EmailLoginViewController, failedWithError error: Error) { + print("Fail..... \(error)") + DispatchQueue.main.async { + controller.presentError(with: "There was an issue logging in. Please try again.") + } + } +} diff --git a/Example/tvOSSample/tvOSSample/Base.lproj/Main.storyboard b/Example/tvOSSample/tvOSSample/Base.lproj/Main.storyboard new file mode 100644 index 0000000..a2539b3 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Base.lproj/Main.storyboard @@ -0,0 +1,355 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="13529" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7zF-dN-6Yw"> + <device id="appleTV" orientation="landscape"> + <adaptation id="light"/> + </device> + <dependencies> + <deployment identifier="tvOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <customFonts key="customFonts"> + <array key="HelveticaNeue.ttc"> + <string>HelveticaNeue</string> + </array> + </customFonts> + <scenes> + <!--Storage--> + <scene sceneID="tne-QT-ifu"> + <objects> + <viewController title="Storage" id="BYZ-38-t0r" customClass="StorageViewController" customModule="tvOSSample" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/> + <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> + <rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="G46-SV-2zY"> + <rect key="frame" x="90" y="60" width="1740" height="960"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Hello, Firebase!" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="g3G-9d-w9c"> + <rect key="frame" x="628" y="0.0" width="484" height="91"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="1Ht-Xf-hyX"> + <rect key="frame" x="0.0" y="111" width="1740" height="677"/> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="State: Pending download" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4sv-2l-7R6"> + <rect key="frame" x="657" y="808" width="427" height="46"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="253" verticalCompressionResistancePriority="749" distribution="fillEqually" spacing="60" translatesAutoresizingMaskIntoConstraints="NO" id="Pcd-oU-DKI" userLabel="Button Stack View"> + <rect key="frame" x="589" y="874" width="562" height="86"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ERN-SX-QsK"> + <rect key="frame" x="0.0" y="0.0" width="251" height="86"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Download"/> + <connections> + <action selector="downloadButtonHit:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="wUX-Vk-IME"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tx1-wP-N8v"> + <rect key="frame" x="311" y="0.0" width="251" height="86"/> + <color key="backgroundColor" red="1" green="0.40000000600000002" blue="0.40000000600000002" alpha="1" colorSpace="calibratedRGB"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Clear"/> + <connections> + <action selector="clearButtonHit:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="s8P-pU-T19"/> + </connections> + </button> + </subviews> + </stackView> + </subviews> + </stackView> + </subviews> + <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="G46-SV-2zY" firstAttribute="leading" secondItem="wu6-TO-1qx" secondAttribute="leading" id="HA1-L9-mG6"/> + <constraint firstItem="wu6-TO-1qx" firstAttribute="trailing" secondItem="G46-SV-2zY" secondAttribute="trailing" id="f82-FP-nqR"/> + <constraint firstItem="G46-SV-2zY" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="gcN-NC-PDe"/> + <constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="G46-SV-2zY" secondAttribute="bottom" id="hb7-C3-J4p"/> + </constraints> + <viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <tabBarItem key="tabBarItem" title="Storage" id="4mO-Ls-vgJ"/> + <connections> + <outlet property="clearButton" destination="tx1-wP-N8v" id="lQA-AQ-sT6"/> + <outlet property="downloadButton" destination="ERN-SX-QsK" id="2Kf-3u-AxO"/> + <outlet property="imageView" destination="1Ht-Xf-hyX" id="MeV-di-b99"/> + <outlet property="stateLabel" destination="4sv-2l-7R6" id="DKm-L2-aiR"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-3032" y="1936"/> + </scene> + <!--Database--> + <scene sceneID="i8D-25-rvO"> + <objects> + <viewController id="gup-Ft-HnK" customClass="DatabaseViewController" customModule="tvOSSample" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="i5o-My-7RI"/> + <viewControllerLayoutGuide type="bottom" id="R20-Wh-bn4"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="1lM-LN-Cey"> + <rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Bah-Ek-V99"> + <rect key="frame" x="90" y="60" width="1740" height="960"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="253" verticalCompressionResistancePriority="1000" text="Magic Syncing Counter" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pjh-c5-D2o"> + <rect key="frame" x="0.0" y="0.0" width="1740" height="91"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="900" text="?" textAlignment="center" lineBreakMode="tailTruncation" minimumScaleFactor="0.20000000000000001" translatesAutoresizingMaskIntoConstraints="NO" id="gcg-TC-hY4"> + <rect key="frame" x="0.0" y="111" width="1740" height="743"/> + <fontDescription key="fontDescription" name="HelveticaNeue" family="Helvetica Neue" pointSize="700"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="253" verticalCompressionResistancePriority="749" distribution="fillEqually" spacing="60" translatesAutoresizingMaskIntoConstraints="NO" id="1hw-2E-6dg" userLabel="Button Stack View"> + <rect key="frame" x="0.0" y="874" width="1740" height="86"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Yc7-OR-WVJ"> + <rect key="frame" x="0.0" y="0.0" width="840" height="86"/> + <color key="backgroundColor" red="0.25098040700000002" green="0.50196081400000003" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="+1"/> + <connections> + <action selector="incrementButtonHit:" destination="gup-Ft-HnK" eventType="primaryActionTriggered" id="zR6-AT-sGq"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ams-4H-2D4"> + <rect key="frame" x="900" y="0.0" width="840" height="86"/> + <color key="backgroundColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="-1"/> + <connections> + <action selector="decrementButton:" destination="gup-Ft-HnK" eventType="primaryActionTriggered" id="KTe-Dn-DgZ"/> + </connections> + </button> + </subviews> + </stackView> + </subviews> + </stackView> + </subviews> + <constraints> + <constraint firstItem="R20-Wh-bn4" firstAttribute="top" secondItem="Bah-Ek-V99" secondAttribute="bottom" id="ECo-G5-TMU"/> + <constraint firstItem="V8l-aL-U1D" firstAttribute="trailing" secondItem="Bah-Ek-V99" secondAttribute="trailing" id="Ikr-TA-Ejk"/> + <constraint firstItem="Bah-Ek-V99" firstAttribute="leading" secondItem="V8l-aL-U1D" secondAttribute="leading" id="L8R-dY-jge"/> + <constraint firstItem="Bah-Ek-V99" firstAttribute="top" secondItem="i5o-My-7RI" secondAttribute="bottom" id="nvx-MN-rDc"/> + </constraints> + <viewLayoutGuide key="safeArea" id="V8l-aL-U1D"/> + </view> + <extendedEdge key="edgesForExtendedLayout" bottom="YES"/> + <tabBarItem key="tabBarItem" title="Database" id="4ry-Ig-tAU"/> + <connections> + <outlet property="currentValue" destination="gcg-TC-hY4" id="1Cs-5w-yPF"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="FDt-0K-Tad" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-812" y="1936"/> + </scene> + <!--Firebase Demo--> + <scene sceneID="Cbs-Et-MXF"> + <objects> + <tabBarController title="Firebase Demo" automaticallyAdjustsScrollViewInsets="NO" id="7zF-dN-6Yw" sceneMemberID="viewController"> + <toolbarItems/> + <tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yhw-Ot-Uul"> + <rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/> + <autoresizingMask key="autoresizingMask"/> + <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/> + </tabBar> + <connections> + <segue destination="BYZ-38-t0r" kind="relationship" relationship="viewControllers" id="he8-oR-peB"/> + <segue destination="XYg-pc-Kbc" kind="relationship" relationship="viewControllers" id="x6X-oT-xPX"/> + <segue destination="gup-Ft-HnK" kind="relationship" relationship="viewControllers" id="CM1-Uf-Vcz"/> + </connections> + </tabBarController> + <placeholder placeholderIdentifier="IBFirstResponder" id="2Mm-5J-pE9" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-814" y="-224"/> + </scene> + <!--Auth--> + <scene sceneID="KOh-Uh-VKO"> + <objects> + <viewController id="XYg-pc-Kbc" customClass="AuthViewController" customModule="tvOSSample" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="q8D-PQ-udq"/> + <viewControllerLayoutGuide type="bottom" id="w5r-DR-UCF"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="8lE-2P-i4i"> + <rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="500" verticalCompressionResistancePriority="1000" text="Auth" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GfO-0W-QKE"> + <rect key="frame" x="885" y="60" width="151" height="91"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="60" translatesAutoresizingMaskIntoConstraints="NO" id="jeZ-ht-4RO" userLabel="Outer Stack"> + <rect key="frame" x="769" y="497" width="383" height="86"/> + <subviews> + <stackView hidden="YES" opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="60" translatesAutoresizingMaskIntoConstraints="NO" id="ljv-p2-x1f" userLabel="Signed In Stack"> + <rect key="frame" x="0.0" y="-497" width="383" height="60"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Signed in: (UID)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-N4-2IJ"> + <rect key="frame" x="57" y="0.0" width="269" height="0.0"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="m3A-YK-ass"> + <rect key="frame" x="78" y="60" width="228" height="0.0"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Sign Out"/> + <connections> + <action selector="signOutButtonHit:" destination="XYg-pc-Kbc" eventType="primaryActionTriggered" id="Hiq-Jx-oNo"/> + </connections> + </button> + </subviews> + </stackView> + <stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="500" verticalCompressionResistancePriority="749" axis="vertical" distribution="fillEqually" alignment="center" spacing="40" translatesAutoresizingMaskIntoConstraints="NO" id="0bY-NY-sEX" userLabel="Other Providers Stack"> + <rect key="frame" x="0.0" y="0.0" width="383" height="86"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FRK-uH-meZ"> + <rect key="frame" x="0.0" y="0.0" width="383" height="86"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Email & Password"/> + <connections> + <segue destination="64h-nN-haS" kind="show" id="OBj-mt-jsE"/> + </connections> + </button> + </subviews> + </stackView> + </subviews> + </stackView> + </subviews> + <constraints> + <constraint firstItem="jeZ-ht-4RO" firstAttribute="top" relation="greaterThanOrEqual" secondItem="GfO-0W-QKE" secondAttribute="bottom" id="HyJ-ip-Q1e"/> + <constraint firstItem="GfO-0W-QKE" firstAttribute="top" secondItem="q8D-PQ-udq" secondAttribute="bottom" id="Icd-az-gH7"/> + <constraint firstItem="GfO-0W-QKE" firstAttribute="centerX" secondItem="8lE-2P-i4i" secondAttribute="centerX" id="q5o-7T-CbO"/> + <constraint firstItem="jeZ-ht-4RO" firstAttribute="centerY" secondItem="8lE-2P-i4i" secondAttribute="centerY" id="tii-97-jog"/> + <constraint firstItem="jeZ-ht-4RO" firstAttribute="centerX" secondItem="8lE-2P-i4i" secondAttribute="centerX" id="u5t-zJ-nRH"/> + </constraints> + <viewLayoutGuide key="safeArea" id="Kvt-PZ-Nk9"/> + </view> + <extendedEdge key="edgesForExtendedLayout"/> + <tabBarItem key="tabBarItem" title="Auth" id="IQh-1s-utZ"/> + <connections> + <outlet property="providers" destination="0bY-NY-sEX" id="8oq-4v-UHT"/> + <outlet property="signInStatus" destination="j4e-N4-2IJ" id="7wt-c1-8N9"/> + <outlet property="signedIn" destination="ljv-p2-x1f" id="4IZ-Bm-XNW"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="7sk-w5-jZ7" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="1350" y="1936"/> + </scene> + <!--Email Login View Controller--> + <scene sceneID="QMw-Qt-hDM"> + <objects> + <viewController id="64h-nN-haS" customClass="EmailLoginViewController" customModule="tvOSSample" customModuleProvider="target" sceneMemberID="viewController"> + <layoutGuides> + <viewControllerLayoutGuide type="top" id="4fg-KG-aTM"/> + <viewControllerLayoutGuide type="bottom" id="RkR-wX-Tdq"/> + </layoutGuides> + <view key="view" contentMode="scaleToFill" id="yhK-2X-Z3p"> + <rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Email Login" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46l-Z7-4KI"> + <rect key="frame" x="778" y="20" width="365" height="91"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="40" translatesAutoresizingMaskIntoConstraints="NO" id="A1D-vt-Yfv"> + <rect key="frame" x="716" y="409" width="490" height="262"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="8Ag-kk-FiI" userLabel="Email Inputs"> + <rect key="frame" x="0.0" y="0.0" width="490" height="136"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" axis="vertical" distribution="fillEqually" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="OMj-ve-Czq" userLabel="Email Fields Stack"> + <rect key="frame" x="0.0" y="0.0" width="490" height="136"/> + <subviews> + <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" horizontalCompressionResistancePriority="800" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Email Address" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Jwa-bJ-Tt6"> + <rect key="frame" x="0.0" y="0.0" width="490" height="58"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle3"/> + <textInputTraits key="textInputTraits" textContentType="email"/> + </textField> + <textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="100" horizontalCompressionResistancePriority="800" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Password" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="GP0-E2-Zjx"> + <rect key="frame" x="0.0" y="78" width="490" height="58"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" style="UICTFontTextStyleTitle3"/> + <textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/> + </textField> + </subviews> + </stackView> + </subviews> + </stackView> + <stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="256" verticalCompressionResistancePriority="1000" distribution="fillEqually" alignment="center" spacing="60" translatesAutoresizingMaskIntoConstraints="NO" id="6pe-KG-Tt8" userLabel="Email Buttons Stack"> + <rect key="frame" x="0.0" y="176" width="490" height="86"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JUJ-6s-duw"> + <rect key="frame" x="0.0" y="0.0" width="215" height="86"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Log In"/> + <connections> + <action selector="logInButtonHit:" destination="64h-nN-haS" eventType="primaryActionTriggered" id="Ea2-dP-bgD"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Dt9-2d-WJM"> + <rect key="frame" x="275" y="0.0" width="215" height="86"/> + <inset key="contentEdgeInsets" minX="40" minY="20" maxX="40" maxY="20"/> + <state key="normal" title="Sign Up"/> + <connections> + <action selector="signUpButtonHit:" destination="64h-nN-haS" eventType="primaryActionTriggered" id="KUW-Xi-zeI"/> + </connections> + </button> + </subviews> + </stackView> + </subviews> + <constraints> + <constraint firstItem="6pe-KG-Tt8" firstAttribute="width" secondItem="8Ag-kk-FiI" secondAttribute="width" id="si6-Rj-cKU"/> + </constraints> + </stackView> + </subviews> + <constraints> + <constraint firstItem="A1D-vt-Yfv" firstAttribute="centerY" secondItem="yhK-2X-Z3p" secondAttribute="centerY" id="AsP-r2-CmU"/> + <constraint firstItem="46l-Z7-4KI" firstAttribute="centerX" secondItem="yhK-2X-Z3p" secondAttribute="centerX" id="FSK-gW-wzc"/> + <constraint firstItem="A1D-vt-Yfv" firstAttribute="centerX" secondItem="46l-Z7-4KI" secondAttribute="centerX" id="SRe-fq-sjl"/> + <constraint firstItem="46l-Z7-4KI" firstAttribute="top" secondItem="yhK-2X-Z3p" secondAttribute="top" constant="20" id="WYg-tl-PPG"/> + <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="A1D-vt-Yfv" secondAttribute="bottom" id="b2e-xU-0xq"/> + <constraint firstItem="A1D-vt-Yfv" firstAttribute="top" relation="greaterThanOrEqual" secondItem="46l-Z7-4KI" secondAttribute="bottom" id="bj1-cy-X9f"/> + </constraints> + <viewLayoutGuide key="safeArea" id="mS6-Be-asr"/> + </view> + <value key="contentSizeForViewInPopover" type="size" width="840" height="385"/> + <connections> + <outlet property="emailAddress" destination="Jwa-bJ-Tt6" id="HOE-at-pka"/> + <outlet property="password" destination="GP0-E2-Zjx" id="X8f-Ew-moe"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="guA-tW-goc" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="447" y="4052"/> + </scene> + </scenes> +</document> diff --git a/Example/tvOSSample/tvOSSample/DatabaseViewController.swift b/Example/tvOSSample/tvOSSample/DatabaseViewController.swift new file mode 100644 index 0000000..712c48f --- /dev/null +++ b/Example/tvOSSample/tvOSSample/DatabaseViewController.swift @@ -0,0 +1,83 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseDatabase + +/// A class to demonstrate the Firebase Realtime Database API. This will show a number read +/// from the Database and increase or decrease it based on the buttons pressed. +class DatabaseViewController: UIViewController { + private enum Counter: Int { + case increment = 1 + case decrement = -1 + + var intValue: Int { + return rawValue + } + } + + // MARK: - Interface + + /// Label to display the current value. + @IBOutlet weak var currentValue: UILabel! + + // MARK: - User Actions + + /// The increment button was hit. + @IBAction func incrementButtonHit(_ sender: UIButton) { changeServerValue(with: .increment) } + + /// the decrement button was hit. + @IBAction func decrementButton(_ sender: UIButton) { changeServerValue(with: .decrement) } + + // MARK: - Internal Helpers + + /// Update the number on the server by a particular value. Note: the number passed in should only + /// be one above or below the current number. + private func changeServerValue(with type: Counter) { + let ref = Database.database().reference(withPath: Constants.databasePath) + // Update the current value of the number. + ref.runTransactionBlock { (currentData) -> TransactionResult in + guard let value = currentData.value as? Int else { + return TransactionResult.abort() + } + + currentData.value = value + type.intValue + return TransactionResult.success(withValue: currentData) + } + } + + // MARK: - View Controller Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Observe the current value, and update the UI every time it changes. + let ref = Database.database().reference(withPath: Constants.databasePath) + + ref.observe(.value) { [weak self] (snapshot) in + guard let value = snapshot.value as? Int else { + print("Error grabbing value from Snapshot!") + return + } + + self?.currentValue.text = "\(value)" + } + } + + // MARK: - Constants + + private struct Constants { + static let databasePath = "magicSyncingCounter" + } +} diff --git a/Example/tvOSSample/tvOSSample/EmailLoginViewController.swift b/Example/tvOSSample/tvOSSample/EmailLoginViewController.swift new file mode 100644 index 0000000..60dfc43 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/EmailLoginViewController.swift @@ -0,0 +1,93 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseAuth + +protocol EmailLoginDelegate { + func emailLogin(_ controller: EmailLoginViewController, signedInAs user: User) + func emailLogin(_ controller: EmailLoginViewController, failedWithError error: Error) +} +class EmailLoginViewController: UIViewController { + + // MARK: - Public Properties + + var delegate: EmailLoginDelegate? + + // MARK: - User Interface + + @IBOutlet private weak var emailAddress: UITextField! + @IBOutlet private weak var password: UITextField! + + // MARK: - User Actions + + @IBAction func logInButtonHit(_ sender: UIButton) { + guard let (email, password) = validatedInputs() else { return } + + Auth.auth().signIn(withEmail: email, password: password) { [unowned self] (user, error) in + guard let user = user else { + print("Error signing in: \(error!)") + self.delegate?.emailLogin(self, failedWithError: error!) + return + } + + print("Signed in as user: \(user.uid)!") + self.delegate?.emailLogin(self, signedInAs: user) + } + } + + @IBAction func signUpButtonHit(_ sender: UIButton) { + guard let (email, password) = validatedInputs() else { return } + + Auth.auth().createUser(withEmail: email, password: password) { [unowned self] (user, error) in + guard let user = user else { + print("Error signing up: \(error!)") + self.delegate?.emailLogin(self, failedWithError: error!) + return + } + + print("Created new user: \(user.uid)!") + self.delegate?.emailLogin(self, signedInAs: user) + } + } + + // MARK: - View Controller Lifecycle + + override func viewDidLoad() { + } + + // MARK: - Helper Methods + + /// Validate the inputs for user email and password, returning the username and password if valid, + /// otherwise nil. + private func validatedInputs() -> (email: String, password: String)? { + guard let userEmail = emailAddress.text, userEmail.count >= 6 else { + presentError(with: "Email address isn't long enough.") + return nil + } + + guard let userPassword = password.text, userPassword.count >= 6 else { + presentError(with: "Password is not long enough!") + return nil + } + + return (userEmail, userPassword) + } + + func presentError(with text: String) { + let alert = UIAlertController(title: "Error", message: text, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Okay", style: .default)) + present(alert, animated: true) + } +} diff --git a/Example/tvOSSample/tvOSSample/Info.plist b/Example/tvOSSample/tvOSSample/Info.plist new file mode 100644 index 0000000..02942a3 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/Info.plist @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UIMainStoryboardFile</key> + <string>Main</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>UIUserInterfaceStyle</key> + <string>Automatic</string> +</dict> +</plist> diff --git a/Example/tvOSSample/tvOSSample/StorageViewController.swift b/Example/tvOSSample/tvOSSample/StorageViewController.swift new file mode 100644 index 0000000..4416649 --- /dev/null +++ b/Example/tvOSSample/tvOSSample/StorageViewController.swift @@ -0,0 +1,148 @@ +// Copyright 2017 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +import FirebaseStorage + +class StorageViewController: UIViewController { + /// An enum describing the different states of the view controller. + private enum UIState: Equatable { + /// No image is being shown, waiting on user action. + case cleared + + /// Currently downloading from Firebase. + case downloading(StorageTask) + + /// The image has downloaded and should be displayed. + case downloaded(UIImage) + + /// Show an error message and stop downloading. + case failed(String) + + /// Equatable support for UIState. + static func ==(lhs: StorageViewController.UIState, rhs: StorageViewController.UIState) -> Bool { + switch (lhs, rhs) { + case (.cleared, .cleared): return true + case (.downloading, .downloading): return true + case (.downloaded, .downloaded): return true + case (.failed, .failed): return true + default: return false + } + } + } + + /// MARK: - Properties + + /// The current internal state of the view controller. + private var state: UIState = .cleared { + didSet { changeState(from: oldValue, to: state) } + } + + // MARK: Interface + + /// Image view to display the downloaded image. + @IBOutlet weak var imageView: UIImageView! + + /// The download button. + @IBOutlet weak var downloadButton: UIButton! + + /// The clear button. + @IBOutlet weak var clearButton: UIButton! + + /// A visual representation of the state. + @IBOutlet weak var stateLabel: UILabel! + + // MARK: - User Actions + + @IBAction func downloadButtonHit(_ sender: UIButton) { + guard case .cleared = state else { return } + + // Start the download. + let storage = Storage.storage() + let ref = storage.reference(withPath: Constants.downloadPath) + // TODO: Show progress bar here using proper API. + let task = ref.getData(maxSize: Constants.maxSize) { [unowned self] (data, error) in + guard let data = data else { + self.state = .failed("Error downloading: \(error!.localizedDescription)") + return + } + + // Create a UIImage from the PNG data. + guard let image = UIImage(data: data) else { + self.state = .failed("Unable to initialize image with data downloaded.") + return + } + + self.state = .downloaded(image) + } + + // The completion block above could be run before this line in some situations. If that's the + // case, we don't need to do anything else and can return. + if case .downloaded = state { return } + + // Set the state to downloading! + state = .downloading(task) + } + + @IBAction func clearButtonHit(_ sender: UIButton) { + guard case .downloaded = state else { return } + + state = .cleared + } + + // MARK: - State Management + + /// Changing from old state to new state. + private func changeState(from oldState: UIState, to newState: UIState) { + if oldState == newState { return } + + switch (oldState, newState) { + // Regular state, start downloading the image. + case (.cleared, .downloading(_)): + // TODO: Update the UI with a spinner? Progress update? + stateLabel.text = "State: Downloading..." + + // Download complete, ensure the download button is still off and enable the clear button. + case (_, .downloaded(let image)): + imageView.image = image + stateLabel.text = "State: Image downloaded!" + + // Clear everything and reset to the original state. + case (_, .cleared): + imageView.image = nil + stateLabel.text = "State: Pending download" + + // An error occurred. + case (_, .failed(let error)): + stateLabel.text = "State: \(error)" + + // For now, as the default, throw a fatal error because it's an unexpected state. This will + // allow us to catch it immediately and add the required action or fix the bug. + default: + fatalError("Programmer error! Tried to go from \(oldState) to \(newState)") + } + } + + // MARK: - Constants + + /// Internal constants for this class. + private struct Constants { + /// The image name to download. Can comment this out and replace it with the other below it as + /// part of the demo. Ensure that Storage has an image uploaded to this path for this to + /// function properly. + static let downloadPath = "YOUR_IMAGE_NAME.jpg" + + static let maxSize: Int64 = 1024 * 1024 * 10 // ~10MB + } +} |