Skip to content

Commit ef58291

Browse files
authored
docs: add Avatar Image to UserManagement example (#162)
* docs: add Avatar Image to UserManagement example * docs: add example of download/uploading image to Storage * Fix image upload * Fix tests
1 parent 42e94b2 commit ef58291

File tree

8 files changed

+166
-50
lines changed

8 files changed

+166
-50
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ jobs:
1919
name: Test Library
2020
steps:
2121
- uses: actions/checkout@v3
22-
- name: Select Xcode 14.3
23-
run: sudo xcode-select -s /Applications/Xcode_14.3.app
22+
- name: Select Xcode 15.0.1
23+
run: sudo xcode-select -s /Applications/Xcode_15.0.1.app
2424
- name: Run tests
2525
run: make test-library
2626

@@ -29,8 +29,8 @@ jobs:
2929
name: Build Examples
3030
steps:
3131
- uses: actions/checkout@v3
32-
- name: Select Xcode 14.3
33-
run: sudo xcode-select -s /Applications/Xcode_14.3.app
32+
- name: Select Xcode 15.0.1
33+
run: sudo xcode-select -s /Applications/Xcode_15.0.1.app
3434
- name: Build examples
3535
run: make build-examples
3636

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
79FEFFC32B078CD800D36347 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC22B078CD800D36347 /* ProfileView.swift */; };
4040
79FEFFC52B078D7900D36347 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC42B078D7900D36347 /* Models.swift */; };
4141
79FEFFC72B078FB000D36347 /* SwiftUIHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */; };
42+
79FEFFC92B0797F600D36347 /* AvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC82B0797F600D36347 /* AvatarImage.swift */; };
4243
/* End PBXBuildFile section */
4344

4445
/* Begin PBXFileReference section */
@@ -76,6 +77,7 @@
7677
79FEFFC22B078CD800D36347 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
7778
79FEFFC42B078D7900D36347 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
7879
79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelpers.swift; sourceTree = "<group>"; };
80+
79FEFFC82B0797F600D36347 /* AvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarImage.swift; sourceTree = "<group>"; };
7981
/* End PBXFileReference section */
8082

8183
/* Begin PBXFrameworksBuildPhase section */
@@ -190,17 +192,18 @@
190192
79FEFFAD2B07873600D36347 /* UserManagement */ = {
191193
isa = PBXGroup;
192194
children = (
193-
79FEFFC12B078B6100D36347 /* Info.plist */,
194-
79FEFFAE2B07873600D36347 /* UserManagementApp.swift */,
195195
79FEFFB02B07873600D36347 /* AppView.swift */,
196196
79FEFFB22B07873700D36347 /* Assets.xcassets */,
197-
79FEFFB42B07873700D36347 /* UserManagement.entitlements */,
198-
79FEFFB52B07873700D36347 /* Preview Content */,
199-
79FEFFBD2B07894700D36347 /* Supabase.swift */,
200197
79FEFFBF2B07895900D36347 /* AuthView.swift */,
201-
79FEFFC22B078CD800D36347 /* ProfileView.swift */,
198+
79FEFFC82B0797F600D36347 /* AvatarImage.swift */,
199+
79FEFFC12B078B6100D36347 /* Info.plist */,
202200
79FEFFC42B078D7900D36347 /* Models.swift */,
201+
79FEFFB52B07873700D36347 /* Preview Content */,
202+
79FEFFC22B078CD800D36347 /* ProfileView.swift */,
203+
79FEFFBD2B07894700D36347 /* Supabase.swift */,
203204
79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */,
205+
79FEFFB42B07873700D36347 /* UserManagement.entitlements */,
206+
79FEFFAE2B07873600D36347 /* UserManagementApp.swift */,
204207
);
205208
path = UserManagement;
206209
sourceTree = "<group>";
@@ -393,6 +396,7 @@
393396
79FEFFC52B078D7900D36347 /* Models.swift in Sources */,
394397
79FEFFC72B078FB000D36347 /* SwiftUIHelpers.swift in Sources */,
395398
79FEFFC02B07895900D36347 /* AuthView.swift in Sources */,
399+
79FEFFC92B0797F600D36347 /* AvatarImage.swift in Sources */,
396400
79FEFFAF2B07873600D36347 /* UserManagementApp.swift in Sources */,
397401
);
398402
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// AvatarImage.swift
3+
// UserManagement
4+
//
5+
// Created by Guilherme Souza on 17/11/23.
6+
//
7+
8+
import SwiftUI
9+
10+
#if canImport(UIKit)
11+
typealias PlatformImage = UIImage
12+
extension Image {
13+
init(platformImage: PlatformImage) {
14+
self.init(uiImage: platformImage)
15+
}
16+
}
17+
18+
#elseif canImport(AppKit)
19+
typealias PlatformImage = NSImage
20+
extension Image {
21+
init(platformImage: PlatformImage) {
22+
self.init(nsImage: platformImage)
23+
}
24+
}
25+
#endif
26+
27+
struct AvatarImage: Transferable, Equatable {
28+
let image: Image
29+
let data: Data
30+
31+
static var transferRepresentation: some TransferRepresentation {
32+
DataRepresentation(importedContentType: .image) { data in
33+
guard let image = AvatarImage(data: data) else {
34+
throw TransferError.importFailed
35+
}
36+
37+
return image
38+
}
39+
}
40+
}
41+
42+
extension AvatarImage {
43+
init?(data: Data) {
44+
guard let uiImage = PlatformImage(data: data) else {
45+
return nil
46+
}
47+
48+
let image = Image(platformImage: uiImage)
49+
self.init(image: image, data: data)
50+
}
51+
}
52+
53+
enum TransferError: Error {
54+
case importFailed
55+
}

Examples/UserManagement/Models.swift

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,16 @@
77

88
import Foundation
99

10-
struct Profile: Decodable {
10+
struct Profile: Codable {
1111
let username: String?
1212
let fullName: String?
1313
let website: String?
14+
let avatarURL: String?
1415

1516
enum CodingKeys: String, CodingKey {
1617
case username
1718
case fullName = "full_name"
1819
case website
19-
}
20-
}
21-
22-
struct UpdateProfileParams: Encodable {
23-
let username: String
24-
let fullName: String
25-
let website: String
26-
27-
enum CodingKeys: String, CodingKey {
28-
case username
29-
case fullName = "full_name"
30-
case website
20+
case avatarURL = "avatar_url"
3121
}
3222
}

Examples/UserManagement/ProfileView.swift

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Created by Guilherme Souza on 17/11/23.
66
//
77

8+
import PhotosUI
9+
import Supabase
810
import SwiftUI
911

1012
struct ProfileView: View {
@@ -14,9 +16,35 @@ struct ProfileView: View {
1416

1517
@State var isLoading = false
1618

19+
@State var imageSelection: PhotosPickerItem?
20+
@State var avatarImage: AvatarImage?
21+
1722
var body: some View {
1823
NavigationStack {
1924
Form {
25+
Section {
26+
HStack {
27+
Group {
28+
if let avatarImage {
29+
avatarImage.image.resizable()
30+
} else {
31+
Color.clear
32+
}
33+
}
34+
.scaledToFit()
35+
.frame(width: 80, height: 80)
36+
37+
Spacer()
38+
39+
PhotosPicker(selection: $imageSelection, matching: .images) {
40+
Image(systemName: "pencil.circle.fill")
41+
.symbolRenderingMode(.multicolor)
42+
.font(.system(size: 30))
43+
.foregroundColor(.accentColor)
44+
}
45+
}
46+
}
47+
2048
Section {
2149
TextField("Username", text: $username)
2250
.textContentType(.username)
@@ -54,6 +82,10 @@ struct ProfileView: View {
5482
}
5583
}
5684
})
85+
.onChange(of: imageSelection) { _, newValue in
86+
guard let newValue else { return }
87+
loadTransferable(from: newValue)
88+
}
5789
}
5890
.task {
5991
await getInitialProfile()
@@ -76,6 +108,10 @@ struct ProfileView: View {
76108
fullName = profile.fullName ?? ""
77109
website = profile.website ?? ""
78110

111+
if let avatarURL = profile.avatarURL, !avatarURL.isEmpty {
112+
try await downloadImage(path: avatarURL)
113+
}
114+
79115
} catch {
80116
debugPrint(error)
81117
}
@@ -86,24 +122,58 @@ struct ProfileView: View {
86122
isLoading = true
87123
defer { isLoading = false }
88124
do {
125+
let imageURL = try await uploadImage()
126+
89127
let currentUser = try await supabase.auth.session.user
90128

129+
let updatedProfile = Profile(
130+
username: username,
131+
fullName: fullName,
132+
website: website,
133+
avatarURL: imageURL
134+
)
135+
91136
try await supabase.database
92137
.from("profiles")
93-
.update(
94-
UpdateProfileParams(
95-
username: username,
96-
fullName: fullName,
97-
website: website
98-
)
99-
)
138+
.update(updatedProfile)
100139
.eq("id", value: currentUser.id)
101140
.execute()
102141
} catch {
103142
debugPrint(error)
104143
}
105144
}
106145
}
146+
147+
private func loadTransferable(from imageSelection: PhotosPickerItem) {
148+
Task {
149+
do {
150+
avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self)
151+
} catch {
152+
debugPrint(error)
153+
}
154+
}
155+
}
156+
157+
private func downloadImage(path: String) async throws {
158+
let data = try await supabase.storage.from("avatars").download(path: path)
159+
avatarImage = AvatarImage(data: data)
160+
}
161+
162+
private func uploadImage() async throws -> String? {
163+
guard let data = avatarImage?.data else { return nil }
164+
165+
let filePath = "\(UUID().uuidString).jpeg"
166+
167+
try await supabase.storage
168+
.from("avatars")
169+
.upload(
170+
path: filePath,
171+
file: data,
172+
options: FileOptions(contentType: "image/jpeg")
173+
)
174+
175+
return filePath
176+
}
107177
}
108178

109179
#if swift(>=5.9)

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
PLATFORM_IOS = iOS Simulator,name=iPhone 14 Pro
1+
PLATFORM_IOS = iOS Simulator,name=iPhone 15 Pro
22
PLATFORM_MACOS = macOS
33
PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst
44
PLATFORM_TVOS = tvOS Simulator,name=Apple TV
5-
PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 8 (41mm)
5+
PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 9 (41mm)
66
EXAMPLE = Examples
77

88
test-library:

Sources/Storage/StorageFileApi.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ public class StorageFileApi: StorageApi {
3636
method: Request.Method,
3737
path: String,
3838
file: Data,
39-
fileOptions: FileOptions
39+
options: FileOptions
4040
) async throws -> String {
41-
let contentType = fileOptions.contentType
41+
let contentType = options.contentType
4242
var headers = [
43-
"x-upsert": "\(fileOptions.upsert)",
43+
"x-upsert": "\(options.upsert)",
4444
]
4545

46-
headers["duplex"] = fileOptions.duplex
46+
headers["duplex"] = options.duplex
4747

4848
let fileName = fileName(fromPath: path)
4949

@@ -57,7 +57,7 @@ public class StorageFileApi: StorageApi {
5757
path: "/object/\(bucketId)/\(path)",
5858
method: method,
5959
formData: form,
60-
options: fileOptions,
60+
options: options,
6161
headers: headers
6262
)
6363
)
@@ -68,26 +68,26 @@ public class StorageFileApi: StorageApi {
6868
/// - Parameters:
6969
/// - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The
7070
/// bucket must already exist before attempting to upload.
71-
/// - file: The File object to be stored in the bucket.
72-
/// - fileOptions: HTTP headers. For example `cacheControl`
71+
/// - file: The Data to be stored in the bucket.
72+
/// - options: HTTP headers. For example `cacheControl`
7373
@discardableResult
74-
public func upload(path: String, file: File, fileOptions: FileOptions = FileOptions())
74+
public func upload(path: String, file: Data, options: FileOptions = FileOptions())
7575
async throws -> String
7676
{
77-
try await uploadOrUpdate(method: .post, path: path, file: file.data, fileOptions: fileOptions)
77+
try await uploadOrUpdate(method: .post, path: path, file: file, options: options)
7878
}
7979

8080
/// Replaces an existing file at the specified path with a new one.
8181
/// - Parameters:
8282
/// - path: The relative file path. Should be of the format `folder/subfolder`. The bucket
8383
/// already exist before attempting to upload.
84-
/// - file: The file object to be stored in the bucket.
85-
/// - fileOptions: HTTP headers. For example `cacheControl`
84+
/// - file: The Data to be stored in the bucket.
85+
/// - options: HTTP headers. For example `cacheControl`
8686
@discardableResult
87-
public func update(path: String, file: File, fileOptions: FileOptions = FileOptions())
87+
public func update(path: String, file: Data, options: FileOptions = FileOptions())
8888
async throws -> String
8989
{
90-
try await uploadOrUpdate(method: .put, path: path, file: file.data, fileOptions: fileOptions)
90+
try await uploadOrUpdate(method: .put, path: path, file: file, options: options)
9191
}
9292

9393
/// Moves an existing file, optionally renaming it at the same time.

Tests/StorageTests/StorageClientIntegrationTests.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,13 @@ final class StorageClientIntegrationTests: XCTestCase {
144144

145145
try await storage.from(bucketId).update(
146146
path: "README.md",
147-
file: File(name: "README.md", data: dataToUpdate ?? Data(), fileName: nil, contentType: nil)
147+
file: dataToUpdate ?? Data()
148148
)
149149
}
150150

151151
private func uploadTestData() async throws {
152-
let file = File(
153-
name: "README.md", data: uploadData ?? Data(), fileName: "README.md", contentType: "text/html"
154-
)
155152
_ = try await storage.from(bucketId).upload(
156-
path: "README.md", file: file, fileOptions: FileOptions(cacheControl: "3600")
153+
path: "README.md", file: uploadData ?? Data(), options: FileOptions(cacheControl: "3600")
157154
)
158155
}
159156
}

0 commit comments

Comments
 (0)