Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add Avatar Image to UserManagement example #162

Merged
merged 5 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
)
}
}