This repository has been archived by the owner on Sep 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4bcc0c3
commit e0ceae7
Showing
5 changed files
with
222 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
Sources/SAPCAI/UI/Common/SwiftUI/ImageViewWrapper.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import SDWebImage | ||
import SwiftUI | ||
|
||
class ImageManager: ObservableObject { | ||
enum ImageState { | ||
case initial | ||
case error | ||
case completed(image: UIImage) | ||
} | ||
|
||
@Published var state: ImageState = .initial | ||
@Published var contentMode: UIView.ContentMode = .scaleAspectFit | ||
@Published var box: BoundingBox? = nil | ||
|
||
var displaySize: CGSize { | ||
switch state { | ||
case .completed(image: let image): | ||
if let box = box, image.size != .zero { | ||
return applySizeConstraints(from: image.size, to: box) | ||
} else { | ||
return .zero | ||
} | ||
case .initial, .error: | ||
return .zero | ||
} | ||
} | ||
} | ||
|
||
struct ImageViewWrapper: View { | ||
@ObservedObject var imageManager: ImageManager | ||
|
||
let url: URL? | ||
let imageView: ImageViewRepresentable | ||
|
||
init(url: URL?) { | ||
self.init(url: url, manager: ImageManager()) | ||
} | ||
|
||
private init(url: URL?, manager: ImageManager) { | ||
self.url = url | ||
self.imageManager = manager | ||
self.imageView = ImageViewRepresentable(url: url, imageManager: manager) | ||
} | ||
|
||
var body: some View { | ||
switch self.imageManager.state { | ||
case .initial, .error: | ||
imageView | ||
case .completed(image: _): | ||
if imageManager.box != nil { | ||
imageView | ||
.preference(key: ImageSizeInfoPrefKey.self, value: imageManager.displaySize) | ||
} else { | ||
imageView | ||
} | ||
} | ||
} | ||
|
||
func scaledToFit() -> Self { | ||
self.imageManager.contentMode = .scaleAspectFit | ||
return self | ||
} | ||
|
||
func scaledToFill() -> Self { | ||
self.imageManager.contentMode = .scaleAspectFill | ||
return self | ||
} | ||
|
||
func sizeInto(box: BoundingBox) -> Self { | ||
self.imageManager.box = box | ||
return self | ||
} | ||
|
||
func placeholder(_ placeholder: Image) -> Self { | ||
// TODO: add placeholder | ||
self | ||
} | ||
} | ||
|
||
struct ImageViewRepresentable: UIViewRepresentable { | ||
var url: URL? | ||
@ObservedObject var imageManager: ImageManager | ||
|
||
func makeUIView(context: Self.Context) -> UIImageViewWrapper { | ||
let wrapper = UIImageViewWrapper() | ||
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 | ||
} | ||
} | ||
|
||
return wrapper | ||
} | ||
|
||
func updateUIView(_ uiView: UIImageViewWrapper, context: UIViewRepresentableContext<ImageViewRepresentable>) { | ||
uiView.imageView.contentMode = self.imageManager.contentMode | ||
} | ||
} | ||
|
||
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 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 | ||
} | ||
} | ||
|
||
extension ImageManager { | ||
func applySizeConstraints(from size: CGSize, to box: BoundingBox) -> CGSize { | ||
let minWidth = box.minWidth | ||
let minHeight = box.minHeight | ||
let maxWidth = box.maxWidth | ||
let maxHeight = box.maxHeight | ||
|
||
guard size.width > 0, size.height > 0 else { | ||
assertionFailure("size is zero") | ||
return size | ||
} | ||
|
||
// all sides in range | ||
if size.width >= minWidth, size.width <= maxWidth, | ||
size.height >= minHeight, size.height <= maxHeight | ||
{ | ||
return size | ||
} | ||
// only width in range | ||
else if size.width >= minWidth, size.width <= maxWidth { | ||
if size.height < minHeight { | ||
return CGSize(width: min(maxWidth, size.width * (minHeight / size.height)), height: minHeight) | ||
} else if size.height > maxHeight { | ||
return CGSize(width: max(minWidth, size.width * (maxHeight / size.height)), height: maxHeight) | ||
} | ||
} | ||
// only height in range | ||
else if size.height >= minHeight, size.height <= maxHeight { | ||
if size.width < minWidth { | ||
return CGSize(width: minWidth, height: min(maxHeight, size.height * (minWidth / size.width))) | ||
} else if size.width > maxWidth { | ||
return CGSize(width: maxWidth, height: max(minHeight, size.height * (maxWidth / size.width))) | ||
} | ||
} | ||
|
||
// then no sides are in range | ||
|
||
else if size.width < minWidth, size.height > maxHeight { | ||
return CGSize(width: minWidth, height: maxHeight) | ||
} else if size.width > maxWidth, size.height < minHeight { | ||
return CGSize(width: maxWidth, height: minHeight) | ||
} | ||
|
||
// probably the most common use case, both sides are too large | ||
else if size.width > maxWidth, size.height > maxHeight { | ||
let r = max(size.width / maxWidth, size.height / maxHeight) | ||
return CGSize(width: size.width / r, height: size.height / r) | ||
} | ||
|
||
// both sides are too small | ||
else if size.width < minWidth, size.height < minHeight { | ||
let r = min(size.width / minWidth, size.height / minHeight) | ||
return CGSize(width: size.width / r, height: size.height / r) | ||
} | ||
|
||
assertionFailure("it should never end up here, check conditions.") | ||
return size | ||
} | ||
} |