Skip to content
This repository has been archived by the owner on Sep 24, 2024. It is now read-only.

Commit

Permalink
feat: 🎸 placeholder and failure support alse README updated
Browse files Browse the repository at this point in the history
  • Loading branch information
ShadowTourist committed Sep 2, 2021
1 parent 84ce935 commit 2275708
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 109 deletions.
5 changes: 5 additions & 0 deletions Sources/SAPCAI/UI/AssistantView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Combine
import SDWebImage
import SwiftUI

/// SwiftUI main view. Use this when you want a full screen out-of-the box view that contains:
Expand Down Expand Up @@ -83,6 +84,10 @@ public struct AssistantView: View {
.onAppear {
self.viewModel.load()
}
.onDisappear {
SDImageCache.shared.clearMemory()
SDImageCache.shared.clearDisk(onCompletion: nil)
}
}
}

Expand Down
16 changes: 7 additions & 9 deletions Sources/SAPCAI/UI/Common/SwiftUI/AvatarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ struct AvatarView: View {
var imageUrl: String

var body: some View {
if let url = URL(string: imageUrl) {
ImageViewWrapper(url: url) { $0 }
.scaledToFill()
.placeholder(Image(systemName: "person.crop.circle"))
.frame(width: 32, height: 32, alignment: .center)
.clipped()
} else {
Image(systemName: "icloud.slash")
}
ImageViewWrapper(url: URL(string: imageUrl),
placeholder: { Image(systemName: "person.crop.circle") },
failure: { Text($0.localizedDescription) },
content: { $0 })
.scaledToFill()
.frame(width: 32, height: 32, alignment: .center)
.clipped()
}
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/SAPCAI/UI/Common/SwiftUI/CarouselImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ struct CarouselImageView: View {
var body: some View {
Group {
if let mediaItem = media, let sourceUrl = mediaItem.sourceUrl {
ImageViewWrapper(url: sourceUrl) { $0 }
.placeholder(mediaItem.placeholder)
ImageViewWrapper(url: sourceUrl,
placeholder: { mediaItem.placeholder },
failure: { Text($0.localizedDescription) },
content: { $0 })
.scaledToFill()
.frame(width: self.itemWidth, height: self.vSizeClass == .regular ? 180 : 80)
.clipped()
Expand Down
1 change: 0 additions & 1 deletion Sources/SAPCAI/UI/Common/SwiftUI/ImageUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ struct ImageUIView: View {
})
}
.scaledToFill()
.sizeInto(box: boundingBox)
} else {
fallback
}
Expand Down
7 changes: 5 additions & 2 deletions Sources/SAPCAI/UI/Common/SwiftUI/ImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ struct ImageView: View {
if imageUrl.absoluteString.range(of: "sap-icon") != nil {
IconImageView(iconUrl: imageUrl.absoluteString, iconSize: CGSize(width: 50, height: 50))
} else {
ImageViewWrapper(url: imageUrl) { $0 }
.errorImage(Image(systemName: "photo"))
ImageViewWrapper(url: imageUrl,
failure: { _ in
Image(systemName: "photo")
},
content: { $0 })
.scaledToFit()
.cornerRadius(8)
}
Expand Down
55 changes: 55 additions & 0 deletions Sources/SAPCAI/UI/Common/UIKit/ImageViewRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SDWebImage
import SwiftUI

struct ImageViewRepresentable: UIViewRepresentable {
var url: URL?
var imageManager: ImageManager
let wrapper = UIImageViewWrapper()

func makeUIView(context: Self.Context) -> UIImageViewWrapper {
self.wrapper
}

func updateUIView(_ uiView: UIImageViewWrapper, context: UIViewRepresentableContext<ImageViewRepresentable>) {
uiView.imageView.contentMode = self.imageManager.contentMode
if case ImageManager.ImageState.error = self.imageManager.state, uiView.imageView.image == nil {
self.loadImage()
}
}

func loadImage() {
self.wrapper.imageView.sd_setImage(with: self.url,
placeholderImage: nil) { image, error, _, _ in
if let error = error {
imageManager.state = .error(error)
} else if let image = image {
imageManager.state = .completed(image: image)
} else {
imageManager.state = .error(NSError(domain: "cai.com", code: 1, userInfo: nil))
}
}
}
}

class UIImageViewWrapper: UIView {
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
return imageView
}()

override init(frame: CGRect) {
super.init(frame: frame)
addSubview(self.imageView)
self.imageView.bindFrameToSuperviewBounds()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
addSubview(self.imageView)
self.imageView.bindFrameToSuperviewBounds()
}
}
153 changes: 58 additions & 95 deletions Sources/SAPCAI/UI/Common/UIKit/ImageViewWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import SDWebImage
import SwiftUI

class ImageManager: ObservableObject {
enum ImageState: Equatable {
enum ImageState {
case initial
case error
case error(_ error: Error)
case completed(image: UIImage)
}

@Published var state: ImageState = .initial
var contentMode: UIView.ContentMode = .scaleAspectFit
var box: BoundingBox?
var placeholder: Image?
var errorImage: Image?

var imgSize: CGSize {
switch self.state {
Expand All @@ -24,25 +21,27 @@ class ImageManager: ObservableObject {
}
}

struct ImageViewWrapper<Content>: View where Content: View {
struct ImageViewWrapper<Content: View, Failure: View, Placeholder: View>: View {
@ObservedObject var imageManager: ImageManager

let url: URL?
let imageView: ImageViewRepresentable
let content: (_ imgView: ImageViewRepresentable) -> Content


private let failure: (_ error: Error) -> Failure
private let placeholder: () -> Placeholder

init(url: URL?,
@ViewBuilder placeholder: @escaping () -> Placeholder,
@ViewBuilder failure: @escaping (_ error: Error) -> Failure,
@ViewBuilder content: @escaping (_ imgView: ImageViewRepresentable) -> Content)
{
self.init(url: url, content: content, manager: ImageManager())
}

private init(url: URL?,
@ViewBuilder content: @escaping (_ imgView: ImageViewRepresentable) -> Content,
manager: ImageManager)
{
self.url = url
self.placeholder = placeholder
self.failure = failure
self.content = content

let manager = ImageManager()
self.imageManager = manager
self.imageView = ImageViewRepresentable(url: url, imageManager: manager)
self.imageView.loadImage()
Expand All @@ -52,19 +51,11 @@ struct ImageViewWrapper<Content>: View where Content: View {
ZStack {
switch imageManager.state {
case .initial:
if let placeholder = imageManager.placeholder {
placeholder
} else {
EmptyView()
}
placeholder()
case .completed(image: _):
content(imageView)
case .error:
if let errorImage = imageManager.errorImage {
errorImage
} else {
EmptyView()
}
case .error(let error):
failure(error)
}
}
}
Expand All @@ -78,85 +69,57 @@ struct ImageViewWrapper<Content>: View where Content: View {
self.imageManager.contentMode = .scaleAspectFill
return self
}

func sizeInto(box: BoundingBox) -> Self {
self.imageManager.box = box
return self
}

func placeholder(_ placeholder: Image) -> Self {
self.imageManager.placeholder = placeholder
return self
}

func errorImage(_ errorImage: Image) -> Self {
self.imageManager.errorImage = errorImage
return self
}
}

struct ImageViewRepresentable: UIViewRepresentable {
var url: URL?
var imageManager: ImageManager
let wrapper = UIImageViewWrapper()

func makeUIView(context: Self.Context) -> UIImageViewWrapper {
self.wrapper
extension ImageViewWrapper where Placeholder == EmptyView, Failure == EmptyView {
init(url: URL?,
@ViewBuilder content: @escaping (_ imgView: ImageViewRepresentable) -> Content)
{
self.init(url: url,
placeholder: { EmptyView() },
failure: { _ in EmptyView() },
content: content)
}
}

func updateUIView(_ uiView: UIImageViewWrapper, context: UIViewRepresentableContext<ImageViewRepresentable>) {
uiView.imageView.contentMode = self.imageManager.contentMode
/// retry strategy missed
if uiView.imageView.image == nil, self.imageManager.state != .error {
self.loadImage()
}
}

func loadImage() {
self.wrapper.imageView.sd_setImage(with: self.url,
placeholderImage: nil) { image, error, _, _ in
if error == nil, let image = image {
imageManager.state = .completed(image: image)
} else {
imageManager.state = .error
}
}
extension ImageViewWrapper where Failure == EmptyView {
init(url: URL?,
@ViewBuilder placeholder: @escaping () -> Placeholder,
@ViewBuilder content: @escaping (_ imgView: ImageViewRepresentable) -> Content)
{
self.init(url: url,
placeholder: placeholder,
failure: { _ in EmptyView() },
content: content)
}
}

class UIImageViewWrapper: UIView {
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
return imageView
}()

override init(frame: CGRect) {
super.init(frame: frame)
addSubview(self.imageView)
self.imageView.bindFrameToSuperviewBounds()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
addSubview(self.imageView)
self.imageView.bindFrameToSuperviewBounds()
extension ImageViewWrapper where Placeholder == EmptyView {
init(url: URL?,
@ViewBuilder failure: @escaping (_ error: Error) -> Failure,
@ViewBuilder content: @escaping (_ imgView: ImageViewRepresentable) -> Content)
{
self.init(url: url,
placeholder: { EmptyView() },
failure: failure,
content: content)
}
}

extension UIView {
func bindFrameToSuperviewBounds() {
guard let superview = self.superview else {
print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.")
return
#if DEBUG
struct ImageViewWrapper_Previews: PreviewProvider {
static var previews: some View {
let url = URL(string: "https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/2019-mustang-shelby-gt350-101-1528733363.jpg?crop=0.817xw:1.00xh;0.149xw,0&resize=640:*")
let gifURL = URL(string: "http://assets.sbnation.com/assets/2512203/dogflops.gif")
VStack {
ImageViewWrapper(url: url, content: { $0.frame(width: 100, height: 100) })

ImageViewWrapper(url: gifURL, content: { $0.frame(width: 300, height: 300) })

ImageViewWrapper(url: nil,
failure: { _ in Text("load image failed") },
content: { $0.frame(width: 300, height: 300) })
}
}
self.translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true
}
}
#endif
15 changes: 15 additions & 0 deletions Sources/SAPCAI/UI/Common/UIKit/UIViewExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import UIKit

extension UIView {
func bindFrameToSuperviewBounds() {
guard let superview = self.superview else {
print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.")
return
}
self.translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true
}
}

0 comments on commit 2275708

Please sign in to comment.