Skip to content

Commit

Permalink
WIP Add SwiftUI Quickstart for Storage
Browse files Browse the repository at this point in the history
  • Loading branch information
codeblooded committed Apr 20, 2021
1 parent 5694c29 commit b3a1d88
Show file tree
Hide file tree
Showing 9 changed files with 851 additions and 15 deletions.
24 changes: 19 additions & 5 deletions storage/Podfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
# StorageExample

use_frameworks!
platform :ios, '10.0'

pod 'Firebase/Analytics'
pod 'Firebase/Auth'
pod 'Firebase/Storage'

target 'StorageExample' do
platform :ios, '10.0'

pod 'Firebase/Analytics'
pod 'Firebase/Auth'
pod 'Firebase/Storage'
end
target 'StorageExampleSwift' do
platform :ios, '10.0'

pod 'Firebase/Analytics'
pod 'Firebase/Auth'
pod 'Firebase/Storage'
pod 'FirebaseStorageSwift', "~> 7.0-beta"
end
target 'StorageExampleSwiftUI' do
platform :ios, '13.0'

pod 'Firebase/Analytics'
pod 'Firebase/Auth'
pod 'Firebase/Storage'
pod 'FirebaseStorageSwift', "~> 7.0-beta"
pod 'FirebaseStorageSwift', "~> 7.0-beta"
end
target 'StorageExampleTests' do
Expand Down
2 changes: 1 addition & 1 deletion storage/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,6 @@ SPEC CHECKSUMS:
nanopb: 59221d7f958fb711001e6a449489542d92ae113e
PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97

PODFILE CHECKSUM: 68e67a7f5b716247bf88b244cb2b3cc55d2e53ec
PODFILE CHECKSUM: d7464278d730121dc09bc1404e6cbe6af4459ccc

COCOAPODS: 1.10.1
474 changes: 465 additions & 9 deletions storage/StorageExample.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions storage/StorageExampleSwiftUI/ImagePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2021 Google LLC
//
// 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 SwiftUI
import Firebase

/// ImagePickerRepresentable wraps a UIImagePickerController, so it is accessible through SwiftUI.
struct ImagePickerRepresentable {
enum Source {
case camera
case photoLibrary
}

/// Denotes whether the user is taking a photo or selecting one.
var source: Source

/// Persistent storage which retains the image.
@ObservedObject var store: ImageStore

/// Binds to whether the image picker is visible.
@Binding var visible: Bool

/// Completion handler that is invoked when the image picker dismisses.
var completion: () -> Void

/// Coordinator is an internal class that acts as a delegate for the image picker.
class Coordinator: NSObject {
private var representable: ImagePickerRepresentable
private var store: ImageStore

init(representable: ImagePickerRepresentable, store: ImageStore) {
self.representable = representable
self.store = store
}
}
}

extension ImagePickerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = UIImagePickerController

/// Invoked by the system to setup a coordinator that the UIImagePickerViewController can use.
/// - Returns: The coordinator.
func makeCoordinator() -> Coordinator {
Coordinator(representable: self, store: self.store)
}

func makeUIViewController(context: Context) -> UIImagePickerController {
let imagePicker = UIImagePickerController()

switch self.source {
case .camera:
imagePicker.sourceType = .camera
imagePicker.cameraCaptureMode = .photo
case .photoLibrary:
imagePicker.sourceType = .photoLibrary
}

imagePicker.delegate = context.coordinator
return imagePicker
}

/// Required to implement, but unnecessary. We do not need to invalidate the SwiftUI canvas.
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
}

extension ImagePickerRepresentable.Coordinator: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }

let imagePath = "\(Auth.auth().currentUser!.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
self.store.uploadImage(image, atPath: imagePath)
// TODO: Move UserDefaults and imagePath logic into store.
UserDefaults.standard.setValue(imagePath, forKey: self.store.imagePathKey)

self.store.image = image
self.representable.visible = false
picker.dismiss(animated: true, completion: self.representable.completion)
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.representable.visible = false
picker.dismiss(animated: true, completion: self.representable.completion)
}
}

/// The coordinator must implement the UINavigationControllerDelegate protocol in order to be the UIImagePickerController's delegate.
extension ImagePickerRepresentable.Coordinator: UINavigationControllerDelegate { }
77 changes: 77 additions & 0 deletions storage/StorageExampleSwiftUI/ImageStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2021 Google LLC
//
// 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 Firebase
import FirebaseStorageSwift

/// ImageStore facilitates saving and downloading images.
public class ImageStore: ObservableObject {
/// Reference to Firebase storage.
private var storage: Storage

/// Quality for JPEG images where 1.0 is the best quality and 0.0 is the worst.
public var compressionQuality: CGFloat = 0.8

/// UserDefaults key that will have a value containing the path of the last image.
public let imagePathKey = "imagePath"

/// Binds to the current image.
@Published var image: UIImage?

lazy var localImageFileDirectory: String = {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
return "file:\(documentsDirectory)"
}()

public init(storage: Storage) {
self.storage = storage
}

/// Uploads an image to Firebase storage.
/// - Parameters:
/// - image: Image to upload.
/// - imagePath: Path of the image in Firebase storage.
public func uploadImage(_ image: UIImage, atPath imagePath: String) {
guard let imageData = image.jpegData(compressionQuality: compressionQuality) else { return }

let imageMetadata = StorageMetadata()
imageMetadata.contentType = "image/jpeg"

let storageRef = storage.reference(withPath: imagePath)
storageRef.putData(imageData, metadata: imageMetadata) { result in
switch result {
case .success:
break
case let .failure(error):
_ = error
break
}
}
}

/// Downloads an image from Firebase storage.
/// - Parameter imagePath: Path of the image in Firebase storage.
public func downloadImage(atPath imagePath: String) {
guard let imageURL = URL(string: "\(self.localImageFileDirectory)/\(imagePath)") else { return }
self.storage.reference().child(imagePath).write(toFile: imageURL) { result in
switch result {
case let .success(downloadedFileURL):
self.image = UIImage(contentsOfFile: downloadedFileURL.path)
case let .failure(error):
print("Error downloading: \(error)")
}
}
}
}
95 changes: 95 additions & 0 deletions storage/StorageExampleSwiftUI/ImageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2021 Google LLC
//
// 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 SwiftUI
import Firebase

/// ImageView provides the main content for the app. It displays a current image and provides controls to change
/// it by taking a new one with the camera, selecting one from the photo library or downloading one from Firebase
/// storage.
struct ImageView: View {
/// Manages retrieval and persistence of the current image.
@StateObject private var photoStore = ImageStore(storage: Storage.storage())

/// Indicates whether the user is selecting an image from the photo library.
@State var isSelectingImage = false

/// Indicates whether the user is taking an image using the camera.
@State var isTakingPhoto = false

/// Indicates whether a submenu that allows the user to choose whether to select or take a photo should be
/// visible.
@State var showUploadMenu = false

var body: some View {
NavigationView {
VStack {
Image(uiImage: photoStore.image ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fit)
}
.navigationTitle("Firebase Storage")
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
if showUploadMenu {
Button("") {
showUploadMenu = false
}

Spacer()

Button("Take Photo") {
isTakingPhoto = true
}.sheet(isPresented: $isTakingPhoto) {
ImagePickerRepresentable(source: .camera, store: photoStore, visible: $isTakingPhoto, completion: {
showUploadMenu = false
})
}.disabled(!UIImagePickerController.isSourceTypeAvailable(.camera))

Button("Select Image") {
isSelectingImage = true
}.sheet(isPresented: $isSelectingImage) {
ImagePickerRepresentable(source: .photoLibrary, store: photoStore, visible: $isSelectingImage, completion: {
showUploadMenu = false
})
}
} else {
Button("Upload") {
showUploadMenu = true
}
Spacer()
Button("Download") {
self.downloadImage()
}
}
}
}
}.onAppear {
downloadImage()
}
}

// TODO: Move this logic into the store, relying on its binding alone.
func downloadImage() {
if let imagePath = UserDefaults.standard.string(forKey: self.photoStore.imagePathKey) {
self.photoStore.downloadImage(atPath: imagePath)
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ImageView()
}
}
54 changes: 54 additions & 0 deletions storage/StorageExampleSwiftUI/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Take and upload photos to cloud storage</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Upload photos to cloud storage</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading

0 comments on commit b3a1d88

Please sign in to comment.