Skip to content

Commit

Permalink
Add SwiftUI CachedAsyncImage and AvatarView (#310)
Browse files Browse the repository at this point in the history
* Add CachedAsyncImage and AvatarView

* Some improvements

* Add demo page

* Format

* Small update

* Remove unused

* Fix typo

* Replace the init parameters with a decorator function

* Format

* Remove DefaultAvatarContent

* Simplification

* One more simplification

* Revert "One more simplification"

This reverts commit 43880de.

* Make swiftformat
  • Loading branch information
pinarol authored Jul 12, 2024
1 parent 458a6b7 commit 961e6fc
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 36 additions & 6 deletions Demo/Demo/Gravatar-SwiftUI-Demo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,45 @@
import SwiftUI

struct ContentView: View {
@State var path: [String] = []

enum Page: Int, CaseIterable, Identifiable {
case avatarView = 0

var id: Int {
self.rawValue
}

var title: String {
switch self {
case .avatarView:
"Avatar View"
}
}
}

var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
NavigationStack(path: $path) {
VStack {
ForEach(Page.allCases) { page in
Button(page.title) {
path.append(page.title)
}
}
}
.navigationDestination(for: String.self) { value in
VStack(spacing: 20) {
switch value {
case Page.avatarView.title:
DemoAvatarView()
default:
Text("-")
}
}
}
}
.padding()
}

}

#Preview {
Expand Down
69 changes: 69 additions & 0 deletions Demo/Demo/Gravatar-SwiftUI-Demo/DemoAvatarView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// DemoAvatarView.swift
// Gravatar-SwiftUI-Demo
//
// Created by Pinar Olguc on 8.07.2024.
//

import SwiftUI
@testable import GravatarUI

struct DemoAvatarView: View {
enum Constants {
static let avatarWidth: CGFloat = 120
static let borderWidth: CGFloat = 2
}

@AppStorage("email") private var email: String = ""
@State var borderWidthDouble: Double? = Constants.borderWidth
@State var borderWidth: CGFloat = Constants.borderWidth
@State var forceRefresh: Bool = false
@State var isAnimated: Bool = true

var avatarURL: AvatarURL? {
AvatarURL(
with: .email(email),
options: .init(
preferredSize: .points(Constants.avatarWidth),
defaultAvatarOption: .status404
)
)
}

var body: some View {
VStack(alignment: .leading, spacing: 20) {
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.disableAutocorrection(true)
TextField("Border width", value: $borderWidthDouble, format: .number)
.disableAutocorrection(true)
Toggle("Force refresh", isOn: $forceRefresh)
Toggle("Animated", isOn: $isAnimated)

AvatarView(
avatarURL: avatarURL,
placeholder: Image("profileAvatar").renderingMode(.template),
forceRefresh: $forceRefresh,
loadingView: {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
},
transaction: Transaction(animation: isAnimated ? .easeInOut(duration: 0.3) : nil)
)
.avatarShape(RoundedRectangle(cornerRadius: 8),
borderColor: .purple,
borderWidth: borderWidth)
.foregroundColor(.purple)
.frame(width: Constants.avatarWidth)
}
.padding()
.onChange(of: borderWidthDouble) { oldValue, newValue in
self.borderWidth = CGFloat(newValue ?? 0)
}
}
}

#Preview {
DemoAvatarView()
}
4 changes: 4 additions & 0 deletions Demo/Gravatar-Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
49C5D6102B5B33E20067C2A8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C5D60F2B5B33E20067C2A8 /* ContentView.swift */; };
49C5D6122B5B33E20067C2A8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49C5D6112B5B33E20067C2A8 /* Assets.xcassets */; };
49C5D6152B5B33E20067C2A8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49C5D6142B5B33E20067C2A8 /* Preview Assets.xcassets */; };
9146A7AE2C3BD8F000E07C63 /* DemoAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9146A7AD2C3BD8F000E07C63 /* DemoAvatarView.swift */; };
914AC0192BD7FF08005DA4A5 /* DemoBaseProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914AC0172BD7FF08005DA4A5 /* DemoBaseProfileViewController.swift */; };
914AC01A2BD7FF08005DA4A5 /* DemoProfilePresentationStylesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914AC0182BD7FF08005DA4A5 /* DemoProfilePresentationStylesViewController.swift */; };
914AC0202BDAAC3C005DA4A5 /* DemoRemoteSVGViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914AC01F2BDAAC3C005DA4A5 /* DemoRemoteSVGViewController.swift */; };
Expand Down Expand Up @@ -51,6 +52,7 @@
49C5D60F2B5B33E20067C2A8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
49C5D6112B5B33E20067C2A8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
49C5D6142B5B33E20067C2A8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
9146A7AD2C3BD8F000E07C63 /* DemoAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAvatarView.swift; sourceTree = "<group>"; };
914AC0172BD7FF08005DA4A5 /* DemoBaseProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoBaseProfileViewController.swift; sourceTree = "<group>"; };
914AC0182BD7FF08005DA4A5 /* DemoProfilePresentationStylesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoProfilePresentationStylesViewController.swift; sourceTree = "<group>"; };
914AC01F2BDAAC3C005DA4A5 /* DemoRemoteSVGViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoRemoteSVGViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -96,6 +98,7 @@
children = (
49C5D60D2B5B33E20067C2A8 /* DemoApp.swift */,
49C5D60F2B5B33E20067C2A8 /* ContentView.swift */,
9146A7AD2C3BD8F000E07C63 /* DemoAvatarView.swift */,
49C5D6112B5B33E20067C2A8 /* Assets.xcassets */,
49C5D6132B5B33E20067C2A8 /* Preview Content */,
);
Expand Down Expand Up @@ -327,6 +330,7 @@
buildActionMask = 2147483647;
files = (
49C5D6102B5B33E20067C2A8 /* ContentView.swift in Sources */,
9146A7AE2C3BD8F000E07C63 /* DemoAvatarView.swift in Sources */,
49C5D60E2B5B33E20067C2A8 /* DemoApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
5 changes: 5 additions & 0 deletions Sources/Gravatar/Network/Services/ImageDownloadService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public struct ImageDownloadService: ImageDownloader, Sendable {
self.imageCache = cache ?? ImageCache()
}

public init(urlSession: URLSession, cache: ImageCaching? = nil) {
self.client = URLSessionHTTPClient(urlSession: urlSession)
self.imageCache = cache ?? ImageCache()
}

public func fetchImage(with url: URL, forceRefresh: Bool = false, processingMethod: ImageProcessingMethod = .common()) async throws -> ImageDownloadResult {
let request = URLRequest.imageRequest(url: url, forceRefresh: forceRefresh)

Expand Down
105 changes: 105 additions & 0 deletions Sources/GravatarUI/SwiftUI/AvatarView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Gravatar
import SwiftUI

@MainActor
struct AvatarView<LoadingView: View>: View {
typealias LoadingViewBlock = () -> LoadingView
@ViewBuilder private let loadingView: LoadingViewBlock?
@Binding private var forceRefresh: Bool
@State private var isLoading: Bool = false
private var avatarURL: AvatarURL?
private let placeholder: Image?
private let cache: ImageCaching
private let urlSession: URLSession
private let transaction: Transaction

init(
avatarURL: AvatarURL?,
placeholder: Image?,
cache: ImageCaching = ImageCache.shared,
urlSession: URLSession = .shared,
forceRefresh: Binding<Bool> = .constant(false),
loadingView: LoadingViewBlock?,
transaction: Transaction = Transaction()
) {
self.avatarURL = avatarURL
self.placeholder = placeholder
self.cache = cache
self.loadingView = loadingView
self.urlSession = urlSession
self._forceRefresh = forceRefresh
self.transaction = transaction
}

var body: some View {
CachedAsyncImage(
url: avatarURL?.url,
cache: cache,
urlSession: urlSession,
forceRefresh: $forceRefresh,
transaction: transaction,
isLoading: $isLoading
) { phase in
ZStack {
content(for: phase)

if isLoading {
if let loadingView = loadingView?() {
loadingView
}
}
}
}
}

@ViewBuilder
private func content(for phase: AsyncImagePhase) -> some View {
switch phase {
case .success(let image):
scaledImage(image)
case .failure, .empty:
if let placeholder {
scaledImage(placeholder)
}
@unknown default:
if let placeholder {
scaledImage(placeholder)
}
}
}

private func scaledImage(_ image: Image) -> some View {
image
.resizable()
.scaledToFit()
}

func avatarShape(_ shape: some Shape, borderColor: Color = .clear, borderWidth: CGFloat = 0) -> some View {
self
.clipShape(shape)
.overlay(
shape
.stroke(borderColor, lineWidth: borderWidth)
)
}
}

#Preview {
let avatarURL = AvatarURL(
with: .email("[email protected]"),
options: .init(preferredSize: .points(100))
)
return AvatarView(
avatarURL: avatarURL,
placeholder: Image(systemName: "person")
.renderingMode(.template)
.resizable(),
loadingView: {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
},
transaction: Transaction(animation: .easeInOut(duration: 1))
)
.avatarShape(RoundedRectangle(cornerRadius: 20), borderColor: Color.accentColor, borderWidth: 2)
.frame(width: 100, height: 100, alignment: .center)
}
Loading

0 comments on commit 961e6fc

Please sign in to comment.