Skip to content

Commit e05a887

Browse files
committed
WIP Add SwiftUI Quickstart for Storage
1 parent d4d45b4 commit e05a887

File tree

9 files changed

+723
-7
lines changed

9 files changed

+723
-7
lines changed

storage/Podfile

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
# StorageExample
22

33
use_frameworks!
4-
platform :ios, '10.0'
5-
6-
pod 'Firebase/Analytics'
7-
pod 'Firebase/Auth'
8-
pod 'Firebase/Storage'
94

105
target 'StorageExample' do
6+
platform :ios, '10.0'
7+
8+
pod 'Firebase/Analytics'
9+
pod 'Firebase/Auth'
10+
pod 'Firebase/Storage'
1111
end
1212
target 'StorageExampleSwift' do
13+
platform :ios, '10.0'
14+
15+
pod 'Firebase/Analytics'
16+
pod 'Firebase/Auth'
17+
pod 'Firebase/Storage'
18+
pod 'FirebaseStorageSwift', "~> 7.0-beta"
19+
end
20+
target 'StorageExampleSwiftUI' do
21+
platform :ios, '13.0'
22+
23+
pod 'Firebase'
24+
pod 'Firebase/Analytics'
25+
pod 'Firebase/Auth'
26+
pod 'Firebase/Storage'
1327
pod 'FirebaseStorageSwift', "~> 7.0-beta"
1428
end
1529
target 'StorageExampleTests' do

storage/Podfile.lock

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
PODS:
2+
- Firebase (7.4.0):
3+
- Firebase/Core (= 7.4.0)
24
- Firebase/Analytics (7.4.0):
35
- Firebase/Core
46
- Firebase/Auth (7.4.0):
@@ -81,6 +83,7 @@ PODS:
8183
- PromisesObjC (1.2.12)
8284

8385
DEPENDENCIES:
86+
- Firebase
8487
- Firebase/Analytics
8588
- Firebase/Auth
8689
- Firebase/Storage
@@ -119,6 +122,6 @@ SPEC CHECKSUMS:
119122
nanopb: 59221d7f958fb711001e6a449489542d92ae113e
120123
PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97
121124

122-
PODFILE CHECKSUM: 68e67a7f5b716247bf88b244cb2b3cc55d2e53ec
125+
PODFILE CHECKSUM: 25b9e14f35ae5e264d830941bf9f0c8c3b02ce6b
123126

124127
COCOAPODS: 1.10.1

storage/StorageExample.xcodeproj/project.pbxproj

+315-1
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import SwiftUI
16+
import Firebase
17+
18+
/// ImagePickerRepresentable wraps a UIImagePickerController, so it is accessible through SwiftUI.
19+
struct ImagePickerRepresentable {
20+
enum Source {
21+
case camera
22+
case photoLibrary
23+
}
24+
25+
/// Denotes whether the user is taking a photo or selecting one.
26+
var source: Source
27+
28+
/// Persistent storage which retains the image.
29+
@ObservedObject var store: ImageStore
30+
31+
/// Binds to whether the image picker is visible.
32+
@Binding var visible: Bool
33+
34+
/// Completion handler that is invoked when the image picker dismisses.
35+
var completion: () -> Void
36+
37+
/// Coordinator is an internal class that acts as a delegate for the image picker.
38+
class Coordinator: NSObject {
39+
private var representable: ImagePickerRepresentable
40+
private var store: ImageStore
41+
42+
init(representable: ImagePickerRepresentable, store: ImageStore) {
43+
self.representable = representable
44+
self.store = store
45+
}
46+
}
47+
}
48+
49+
extension ImagePickerRepresentable: UIViewControllerRepresentable {
50+
typealias UIViewControllerType = UIImagePickerController
51+
52+
/// Invoked by the system to setup a coordinator that the UIImagePickerViewController can use.
53+
/// - Returns: The coordinator.
54+
func makeCoordinator() -> Coordinator {
55+
Coordinator(representable: self, store: self.store)
56+
}
57+
58+
func makeUIViewController(context: Context) -> UIImagePickerController {
59+
let imagePicker = UIImagePickerController()
60+
61+
switch self.source {
62+
case .camera:
63+
imagePicker.sourceType = .camera
64+
imagePicker.cameraCaptureMode = .photo
65+
case .photoLibrary:
66+
imagePicker.sourceType = .photoLibrary
67+
}
68+
69+
imagePicker.delegate = context.coordinator
70+
return imagePicker
71+
}
72+
73+
/// Required to implement, but unnecessary. We do not need to invalidate the SwiftUI canvas.
74+
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
75+
}
76+
77+
extension ImagePickerRepresentable.Coordinator: UIImagePickerControllerDelegate {
78+
func imagePickerController(
79+
_ picker: UIImagePickerController,
80+
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
81+
) {
82+
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
83+
// TODO: Consider displaying a progress bar or spinner here
84+
store.saveImage(image) { result in
85+
// TODO: Handle the error
86+
self.representable.visible = false
87+
picker.dismiss(animated: true, completion: self.representable.completion)
88+
}
89+
}
90+
}
91+
92+
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
93+
self.representable.visible = false
94+
picker.dismiss(animated: true, completion: self.representable.completion)
95+
}
96+
}
97+
98+
/// The coordinator must implement the UINavigationControllerDelegate protocol in order to be the
99+
/// UIImagePickerController's delegate.
100+
extension ImagePickerRepresentable.Coordinator: UINavigationControllerDelegate { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Firebase
16+
import FirebaseStorageSwift
17+
18+
/// ImageStore facilitates saving and loading an image.
19+
public class ImageStore: ObservableObject {
20+
/// Reference to Firebase storage.
21+
private var storage: Storage
22+
23+
/// Quality for JPEG images where 1.0 is the best quality and 0.0 is the worst.
24+
public var compressionQuality: CGFloat = 0.8
25+
26+
/// UserDefaults key that will have a value containing the path of the last image.
27+
public let imagePathKey = "imagePath"
28+
29+
/// Binds to the current image.
30+
@Published var image: UIImage?
31+
32+
lazy var localImageFileDirectory: String = {
33+
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
34+
let documentsDirectory = paths[0]
35+
return "file:\(documentsDirectory)"
36+
}()
37+
38+
public init(storage: Storage) {
39+
self.storage = storage
40+
}
41+
42+
/// Saves an image in the store.
43+
/// - Parameter image: The image to save.
44+
public func saveImage(_ image: UIImage, completion: @escaping (Result<Void, Error>) -> Void) {
45+
self.image = image
46+
let imagePath = "\(Auth.auth().currentUser!.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
47+
uploadImage(image, atPath: imagePath) { result in
48+
UserDefaults.standard.setValue(imagePath, forKey: self.imagePathKey)
49+
completion(result)
50+
}
51+
}
52+
53+
/// Loads the most recent image.
54+
public func loadImage() {
55+
if let imagePath = UserDefaults.standard.string(forKey: imagePathKey) {
56+
downloadImage(atPath: imagePath)
57+
}
58+
}
59+
60+
/// Uploads an image to Firebase storage.
61+
/// - Parameters:
62+
/// - image: Image to upload.
63+
/// - imagePath: Path of the image in Firebase storage.
64+
private func uploadImage(
65+
_ image: UIImage,
66+
atPath imagePath: String,
67+
completion: @escaping (Result<Void, Error>) -> Void
68+
) {
69+
guard let imageData = image.jpegData(compressionQuality: compressionQuality) else { return }
70+
71+
let imageMetadata = StorageMetadata()
72+
imageMetadata.contentType = "image/jpeg"
73+
74+
let storageRef = storage.reference(withPath: imagePath)
75+
storageRef.putData(imageData, metadata: imageMetadata) { result in
76+
switch result {
77+
case .success:
78+
completion(.success(()))
79+
case let .failure(error):
80+
completion(.failure(error))
81+
break
82+
}
83+
}
84+
}
85+
86+
/// Downloads an image from Firebase storage.
87+
/// - Parameter imagePath: Path of the image in Firebase storage.
88+
private func downloadImage(atPath imagePath: String) {
89+
guard let imageURL = URL(string: "\(self.localImageFileDirectory)/\(imagePath)") else { return }
90+
self.storage.reference().child(imagePath).write(toFile: imageURL) { result in
91+
switch result {
92+
case let .success(downloadedFileURL):
93+
self.image = UIImage(contentsOfFile: downloadedFileURL.path)
94+
case let .failure(error):
95+
print("Error downloading: \(error)")
96+
}
97+
}
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import SwiftUI
16+
import Firebase
17+
18+
/// ImageView provides the main content for the app. It displays a current image and provides
19+
/// controls to change it by taking a new one with the camera, selecting one from the photo library
20+
/// or downloading one from Firebase storage.
21+
struct ImageView: View {
22+
/// Manages retrieval and persistence of the current image.
23+
@StateObject private var photoStore = ImageStore(storage: Storage.storage())
24+
25+
/// Indicates whether the user is selecting an image from the photo library.
26+
@State var isSelectingImage = false
27+
28+
/// Indicates whether the user is taking an image using the camera.
29+
@State var isTakingPhoto = false
30+
31+
/// Indicates whether a submenu that allows the user to choose whether to select or take a photo
32+
/// should be visible.
33+
@State var showUploadMenu = false
34+
35+
var body: some View {
36+
NavigationView {
37+
VStack {
38+
Image(uiImage: photoStore.image ?? UIImage())
39+
.resizable()
40+
.aspectRatio(contentMode: .fit)
41+
}
42+
.navigationTitle("Firebase Storage")
43+
.toolbar {
44+
ToolbarItemGroup(placement: .bottomBar) {
45+
if showUploadMenu {
46+
Button("") {
47+
showUploadMenu = false
48+
}
49+
50+
Spacer()
51+
52+
Button("Take Photo") {
53+
isTakingPhoto = true
54+
}.sheet(isPresented: $isTakingPhoto) {
55+
ImagePickerRepresentable(
56+
source: .camera,
57+
store: photoStore,
58+
visible: $isTakingPhoto
59+
) {
60+
showUploadMenu = false
61+
}
62+
}.disabled(!UIImagePickerController.isSourceTypeAvailable(.camera))
63+
64+
Button("Select Image") {
65+
isSelectingImage = true
66+
}.sheet(isPresented: $isSelectingImage) {
67+
ImagePickerRepresentable(
68+
source: .photoLibrary,
69+
store: photoStore,
70+
visible: $isSelectingImage
71+
) {
72+
showUploadMenu = false
73+
}
74+
}
75+
} else {
76+
Button("Upload") {
77+
showUploadMenu = true
78+
}
79+
Spacer()
80+
Button("Download") {
81+
photoStore.loadImage()
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
}
89+
90+
struct ContentView_Previews: PreviewProvider {
91+
static var previews: some View {
92+
ImageView()
93+
}
94+
}

0 commit comments

Comments
 (0)