Skip to content

Commit

Permalink
docs: add Avatar Image to UserManagement example (#162)
Browse files Browse the repository at this point in the history
* docs: add Avatar Image to UserManagement example

* docs: add example of download/uploading image to Storage

* Fix image upload

* Fix tests
  • Loading branch information
grdsdev committed Nov 17, 2023
1 parent 42e94b2 commit ef58291
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 50 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ jobs:
name: Test Library
steps:
- uses: actions/checkout@v3
- name: Select Xcode 14.3
run: sudo xcode-select -s /Applications/Xcode_14.3.app
- name: Select Xcode 15.0.1
run: sudo xcode-select -s /Applications/Xcode_15.0.1.app
- name: Run tests
run: make test-library

Expand All @@ -29,8 +29,8 @@ jobs:
name: Build Examples
steps:
- uses: actions/checkout@v3
- name: Select Xcode 14.3
run: sudo xcode-select -s /Applications/Xcode_14.3.app
- name: Select Xcode 15.0.1
run: sudo xcode-select -s /Applications/Xcode_15.0.1.app
- name: Build examples
run: make build-examples

16 changes: 10 additions & 6 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
79FEFFC32B078CD800D36347 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC22B078CD800D36347 /* ProfileView.swift */; };
79FEFFC52B078D7900D36347 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC42B078D7900D36347 /* Models.swift */; };
79FEFFC72B078FB000D36347 /* SwiftUIHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */; };
79FEFFC92B0797F600D36347 /* AvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFC82B0797F600D36347 /* AvatarImage.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -76,6 +77,7 @@
79FEFFC22B078CD800D36347 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
79FEFFC42B078D7900D36347 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelpers.swift; sourceTree = "<group>"; };
79FEFFC82B0797F600D36347 /* AvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarImage.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -190,17 +192,18 @@
79FEFFAD2B07873600D36347 /* UserManagement */ = {
isa = PBXGroup;
children = (
79FEFFC12B078B6100D36347 /* Info.plist */,
79FEFFAE2B07873600D36347 /* UserManagementApp.swift */,
79FEFFB02B07873600D36347 /* AppView.swift */,
79FEFFB22B07873700D36347 /* Assets.xcassets */,
79FEFFB42B07873700D36347 /* UserManagement.entitlements */,
79FEFFB52B07873700D36347 /* Preview Content */,
79FEFFBD2B07894700D36347 /* Supabase.swift */,
79FEFFBF2B07895900D36347 /* AuthView.swift */,
79FEFFC22B078CD800D36347 /* ProfileView.swift */,
79FEFFC82B0797F600D36347 /* AvatarImage.swift */,
79FEFFC12B078B6100D36347 /* Info.plist */,
79FEFFC42B078D7900D36347 /* Models.swift */,
79FEFFB52B07873700D36347 /* Preview Content */,
79FEFFC22B078CD800D36347 /* ProfileView.swift */,
79FEFFBD2B07894700D36347 /* Supabase.swift */,
79FEFFC62B078FB000D36347 /* SwiftUIHelpers.swift */,
79FEFFB42B07873700D36347 /* UserManagement.entitlements */,
79FEFFAE2B07873600D36347 /* UserManagementApp.swift */,
);
path = UserManagement;
sourceTree = "<group>";
Expand Down Expand Up @@ -393,6 +396,7 @@
79FEFFC52B078D7900D36347 /* Models.swift in Sources */,
79FEFFC72B078FB000D36347 /* SwiftUIHelpers.swift in Sources */,
79FEFFC02B07895900D36347 /* AuthView.swift in Sources */,
79FEFFC92B0797F600D36347 /* AvatarImage.swift in Sources */,
79FEFFAF2B07873600D36347 /* UserManagementApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
55 changes: 55 additions & 0 deletions Examples/UserManagement/AvatarImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// AvatarImage.swift
// UserManagement
//
// Created by Guilherme Souza on 17/11/23.
//

import SwiftUI

#if canImport(UIKit)
typealias PlatformImage = UIImage
extension Image {
init(platformImage: PlatformImage) {
self.init(uiImage: platformImage)
}
}

#elseif canImport(AppKit)
typealias PlatformImage = NSImage
extension Image {
init(platformImage: PlatformImage) {
self.init(nsImage: platformImage)
}
}
#endif

struct AvatarImage: Transferable, Equatable {
let image: Image
let data: Data

static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
guard let image = AvatarImage(data: data) else {
throw TransferError.importFailed
}

return image
}
}
}

extension AvatarImage {
init?(data: Data) {
guard let uiImage = PlatformImage(data: data) else {
return nil
}

let image = Image(platformImage: uiImage)
self.init(image: image, data: data)
}
}

enum TransferError: Error {
case importFailed
}
16 changes: 3 additions & 13 deletions Examples/UserManagement/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,16 @@

import Foundation

struct Profile: Decodable {
struct Profile: Codable {
let username: String?
let fullName: String?
let website: String?
let avatarURL: String?

enum CodingKeys: String, CodingKey {
case username
case fullName = "full_name"
case website
}
}

struct UpdateProfileParams: Encodable {
let username: String
let fullName: String
let website: String

enum CodingKeys: String, CodingKey {
case username
case fullName = "full_name"
case website
case avatarURL = "avatar_url"
}
}
84 changes: 77 additions & 7 deletions Examples/UserManagement/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Created by Guilherme Souza on 17/11/23.
//

import PhotosUI
import Supabase
import SwiftUI

struct ProfileView: View {
Expand All @@ -14,9 +16,35 @@ struct ProfileView: View {

@State var isLoading = false

@State var imageSelection: PhotosPickerItem?
@State var avatarImage: AvatarImage?

var body: some View {
NavigationStack {
Form {
Section {
HStack {
Group {
if let avatarImage {
avatarImage.image.resizable()
} else {
Color.clear
}
}
.scaledToFit()
.frame(width: 80, height: 80)

Spacer()

PhotosPicker(selection: $imageSelection, matching: .images) {
Image(systemName: "pencil.circle.fill")
.symbolRenderingMode(.multicolor)
.font(.system(size: 30))
.foregroundColor(.accentColor)
}
}
}

Section {
TextField("Username", text: $username)
.textContentType(.username)
Expand Down Expand Up @@ -54,6 +82,10 @@ struct ProfileView: View {
}
}
})
.onChange(of: imageSelection) { _, newValue in
guard let newValue else { return }
loadTransferable(from: newValue)
}
}
.task {
await getInitialProfile()
Expand All @@ -76,6 +108,10 @@ struct ProfileView: View {
fullName = profile.fullName ?? ""
website = profile.website ?? ""

if let avatarURL = profile.avatarURL, !avatarURL.isEmpty {
try await downloadImage(path: avatarURL)
}

} catch {
debugPrint(error)
}
Expand All @@ -86,24 +122,58 @@ struct ProfileView: View {
isLoading = true
defer { isLoading = false }
do {
let imageURL = try await uploadImage()

let currentUser = try await supabase.auth.session.user

let updatedProfile = Profile(
username: username,
fullName: fullName,
website: website,
avatarURL: imageURL
)

try await supabase.database
.from("profiles")
.update(
UpdateProfileParams(
username: username,
fullName: fullName,
website: website
)
)
.update(updatedProfile)
.eq("id", value: currentUser.id)
.execute()
} catch {
debugPrint(error)
}
}
}

private func loadTransferable(from imageSelection: PhotosPickerItem) {
Task {
do {
avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self)
} catch {
debugPrint(error)
}
}
}

private func downloadImage(path: String) async throws {
let data = try await supabase.storage.from("avatars").download(path: path)
avatarImage = AvatarImage(data: data)
}

private func uploadImage() async throws -> String? {
guard let data = avatarImage?.data else { return nil }

let filePath = "\(UUID().uuidString).jpeg"

try await supabase.storage
.from("avatars")
.upload(
path: filePath,
file: data,
options: FileOptions(contentType: "image/jpeg")
)

return filePath
}
}

#if swift(>=5.9)
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
PLATFORM_IOS = iOS Simulator,name=iPhone 14 Pro
PLATFORM_IOS = iOS Simulator,name=iPhone 15 Pro
PLATFORM_MACOS = macOS
PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst
PLATFORM_TVOS = tvOS Simulator,name=Apple TV
PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 8 (41mm)
PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 9 (41mm)
EXAMPLE = Examples

test-library:
Expand Down
26 changes: 13 additions & 13 deletions Sources/Storage/StorageFileApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public class StorageFileApi: StorageApi {
method: Request.Method,
path: String,
file: Data,
fileOptions: FileOptions
options: FileOptions
) async throws -> String {
let contentType = fileOptions.contentType
let contentType = options.contentType
var headers = [
"x-upsert": "\(fileOptions.upsert)",
"x-upsert": "\(options.upsert)",
]

headers["duplex"] = fileOptions.duplex
headers["duplex"] = options.duplex

let fileName = fileName(fromPath: path)

Expand All @@ -57,7 +57,7 @@ public class StorageFileApi: StorageApi {
path: "/object/\(bucketId)/\(path)",
method: method,
formData: form,
options: fileOptions,
options: options,
headers: headers
)
)
Expand All @@ -68,26 +68,26 @@ public class StorageFileApi: StorageApi {
/// - Parameters:
/// - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The
/// bucket must already exist before attempting to upload.
/// - file: The File object to be stored in the bucket.
/// - fileOptions: HTTP headers. For example `cacheControl`
/// - file: The Data to be stored in the bucket.
/// - options: HTTP headers. For example `cacheControl`
@discardableResult
public func upload(path: String, file: File, fileOptions: FileOptions = FileOptions())
public func upload(path: String, file: Data, options: FileOptions = FileOptions())
async throws -> String
{
try await uploadOrUpdate(method: .post, path: path, file: file.data, fileOptions: fileOptions)
try await uploadOrUpdate(method: .post, path: path, file: file, options: options)
}

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

/// Moves an existing file, optionally renaming it at the same time.
Expand Down
7 changes: 2 additions & 5 deletions Tests/StorageTests/StorageClientIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,13 @@ final class StorageClientIntegrationTests: XCTestCase {

try await storage.from(bucketId).update(
path: "README.md",
file: File(name: "README.md", data: dataToUpdate ?? Data(), fileName: nil, contentType: nil)
file: dataToUpdate ?? Data()
)
}

private func uploadTestData() async throws {
let file = File(
name: "README.md", data: uploadData ?? Data(), fileName: "README.md", contentType: "text/html"
)
_ = try await storage.from(bucketId).upload(
path: "README.md", file: file, fileOptions: FileOptions(cacheControl: "3600")
path: "README.md", file: uploadData ?? Data(), options: FileOptions(cacheControl: "3600")
)
}
}

0 comments on commit ef58291

Please sign in to comment.