-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP Add SwiftUI Quickstart for Storage
- Loading branch information
1 parent
5694c29
commit b3a1d88
Showing
9 changed files
with
851 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)") | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
6 changes: 6 additions & 0 deletions
6
storage/StorageExampleSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
Oops, something went wrong.