Files

781 lines
33 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import Lucide
public protocol ProfilePictureAnimationManagerType: AnyObject {
var shouldAnimateImageSubject: CurrentValueSubject<Bool, Never> { get }
var shouldAnimateImagePublisher: AnyPublisher<Bool, Never> { get }
}
public final class ProfilePictureView: UIView {
public struct Info {
public enum Size {
case navigation
case message
case list
case hero
case modal
case expanded
public var viewSize: CGFloat {
switch self {
case .navigation, .message: return 26
case .list: return 46
case .hero: return 110
case .modal: return 90
case .expanded: return 190
}
}
public var imageSize: CGFloat {
switch self {
case .navigation, .message: return 26
case .list: return 46
case .hero: return 90
case .modal: return 90
case .expanded: return 190
}
}
public var multiImageSize: CGFloat {
switch self {
case .navigation, .message: return 18 // Shouldn't be used
case .list: return 32
case .hero: return 80
case .modal: return 90
case .expanded: return 140
}
}
var iconSize: CGFloat {
switch self {
case .navigation, .message: return 10 // Intentionally not a multiple of 4
case .list: return 16
case .hero: return 24
case .modal: return 24 // Shouldn't be used
case .expanded: return 33
}
}
}
public enum ProfileIcon: Equatable, Hashable {
case none
case crown
case rightPlus
case letter(Character, Bool)
case pencil
case qrCode
func iconVerticalInset(for size: Size) -> CGFloat {
switch (self, size) {
case (.crown, .navigation), (.crown, .message): return 2
case (.crown, .list): return 3
case (.crown, .hero): return 5
case (.rightPlus, _): return 3
default: return 0
}
}
var isLeadingAligned: Bool {
switch self {
case .none, .letter: return true
case .rightPlus, .pencil, .crown, .qrCode: return false
}
}
}
// TODO: [PRO] Should be able to remove the "public" once `MessageInfoScreen.getProFeaturesInfo()` has been updated
public let source: ImageDataManager.DataSource?
let canAnimate: Bool
let renderingMode: UIImage.RenderingMode?
let themeTintColor: ThemeValue?
let inset: UIEdgeInsets
let icon: ProfileIcon
let cropRect: CGRect?
let backgroundColor: ThemeValue?
let forcedBackgroundColor: ForcedThemeValue?
public init(
source: ImageDataManager.DataSource?,
canAnimate: Bool,
renderingMode: UIImage.RenderingMode? = nil,
themeTintColor: ThemeValue? = nil,
inset: UIEdgeInsets = .zero,
icon: ProfileIcon = .none,
cropRect: CGRect? = nil,
backgroundColor: ThemeValue? = nil,
forcedBackgroundColor: ForcedThemeValue? = nil
) {
self.source = source
self.canAnimate = canAnimate
self.renderingMode = renderingMode
self.themeTintColor = themeTintColor
self.inset = inset
self.icon = icon
self.cropRect = cropRect
self.backgroundColor = backgroundColor
self.forcedBackgroundColor = forcedBackgroundColor
}
}
private var dataManager: ImageDataManagerType?
public var size: Info.Size {
didSet {
widthConstraint.constant = (customWidth ?? size.viewSize)
heightConstraint.constant = size.viewSize
profileIconBackgroundWidthConstraint.constant = size.iconSize
profileIconBackgroundHeightConstraint.constant = size.iconSize
additionalProfileIconBackgroundWidthConstraint.constant = size.iconSize
additionalProfileIconBackgroundHeightConstraint.constant = size.iconSize
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
profileIconLabel.font = .boldSystemFont(ofSize: floor(size.iconSize * 0.75))
additionalProfileIconLabel.font = .boldSystemFont(ofSize: floor(size.iconSize * 0.75))
}
}
public var customWidth: CGFloat? {
didSet {
self.widthConstraint.constant = (customWidth ?? self.size.viewSize)
}
}
public override var isHidden: Bool {
didSet {
widthConstraint.constant = (isHidden ? 0 : size.viewSize)
heightConstraint.constant = (isHidden ? 0 : size.viewSize)
}
}
// MARK: - Constraints
private var widthConstraint: NSLayoutConstraint!
private var heightConstraint: NSLayoutConstraint!
private var imageViewTopConstraint: NSLayoutConstraint!
private var imageViewLeadingConstraint: NSLayoutConstraint!
private var imageViewCenterXConstraint: NSLayoutConstraint!
private var imageViewCenterYConstraint: NSLayoutConstraint!
private var imageViewWidthConstraint: NSLayoutConstraint!
private var imageViewHeightConstraint: NSLayoutConstraint!
private var additionalImageViewWidthConstraint: NSLayoutConstraint!
private var additionalImageViewHeightConstraint: NSLayoutConstraint!
private var profileIconTopConstraint: NSLayoutConstraint!
private var profileIconLeadingConstraint: NSLayoutConstraint!
private var profileIconBottomConstraint: NSLayoutConstraint!
private var profileIconBackgroundLeadingAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundTrailingAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundTopAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundBottomAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundWidthConstraint: NSLayoutConstraint!
private var profileIconBackgroundHeightConstraint: NSLayoutConstraint!
private var additionalProfileIconTopConstraint: NSLayoutConstraint!
private var additionalProfileIconBottomConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundLeadingAlignConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundTrailingAlignConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundWidthConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundHeightConstraint: NSLayoutConstraint!
private lazy var imageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
imageView.pin(.top, to: .top, of: imageContainerView, withInset: 0),
imageView.pin(.left, to: .left, of: imageContainerView, withInset: 0),
imageView.pin(.bottom, to: .bottom, of: imageContainerView, withInset: 0),
imageView.pin(.right, to: .right, of: imageContainerView, withInset: 0)
]
private lazy var additionalImageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
additionalImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.left, to: .left, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.right, to: .right, of: additionalImageContainerView, withInset: 0)
]
// MARK: - Components
private lazy var imageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .backgroundSecondary
return result
}()
private lazy var imageView: SessionImageView = {
let result: SessionImageView = SessionImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
if let dataManager = self.dataManager {
result.setDataManager(dataManager)
}
return result
}()
private lazy var additionalImageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .primary
result.themeBorderColor = .backgroundPrimary
result.layer.borderWidth = 1
result.isHidden = true
return result
}()
private lazy var additionalImageView: SessionImageView = {
let result: SessionImageView = SessionImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.themeTintColor = .textPrimary
if let dataManager = self.dataManager {
result.setDataManager(dataManager)
}
return result
}()
private lazy var profileIconBackgroundView: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var profileIconImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.contentMode = .scaleAspectFit
result.isHidden = true
return result
}()
private lazy var profileIconLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: 6)
result.textAlignment = .center
result.themeTextColor = .backgroundPrimary
result.isHidden = true
return result
}()
private lazy var additionalProfileIconBackgroundView: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var additionalProfileIconImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.contentMode = .scaleAspectFit
return result
}()
private lazy var additionalProfileIconLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: 6)
result.textAlignment = .center
result.themeTextColor = .backgroundPrimary
result.isHidden = true
return result
}()
// MARK: - Lifecycle
@MainActor public init(size: Info.Size, dataManager: ImageDataManagerType?) {
self.dataManager = dataManager
self.size = size
super.init(frame: CGRect(x: 0, y: 0, width: size.viewSize, height: size.viewSize))
clipsToBounds = true
setUpViewHierarchy()
}
public required init?(coder: NSCoder) {
preconditionFailure("Use init(size:) instead.")
}
private func setUpViewHierarchy() {
addSubview(imageContainerView)
addSubview(profileIconBackgroundView)
addSubview(additionalImageContainerView)
addSubview(additionalProfileIconBackgroundView)
imageContainerView.addSubview(imageView)
additionalImageContainerView.addSubview(additionalImageView)
profileIconBackgroundView.addSubview(profileIconImageView)
profileIconBackgroundView.addSubview(profileIconLabel)
additionalProfileIconBackgroundView.addSubview(additionalProfileIconImageView)
additionalProfileIconBackgroundView.addSubview(additionalProfileIconLabel)
widthConstraint = self.set(.width, to: self.size.viewSize)
heightConstraint = self.set(.height, to: self.size.viewSize)
.setting(priority: .defaultHigh)
imageViewTopConstraint = imageContainerView.pin(.top, to: .top, of: self)
imageViewLeadingConstraint = imageContainerView.pin(.leading, to: .leading, of: self)
imageViewCenterXConstraint = imageContainerView.center(.horizontal, in: self)
imageViewCenterXConstraint.isActive = false
imageViewCenterYConstraint = imageContainerView.center(.vertical, in: self)
imageViewCenterYConstraint.isActive = false
imageViewWidthConstraint = imageContainerView.set(.width, to: size.imageSize)
imageViewHeightConstraint = imageContainerView.set(.height, to: size.imageSize)
additionalImageContainerView.pin(.trailing, to: .trailing, of: self)
additionalImageContainerView.pin(.bottom, to: .bottom, of: self)
additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: size.multiImageSize)
additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: size.multiImageSize)
// Activate the image edge constraints
imageEdgeConstraints.forEach { $0.isActive = true }
additionalImageEdgeConstraints.forEach { $0.isActive = true }
profileIconTopConstraint = profileIconImageView.pin(
.top,
to: .top,
of: profileIconBackgroundView,
withInset: 0
)
profileIconImageView.pin(.left, to: .left, of: profileIconBackgroundView)
profileIconImageView.pin(.right, to: .right, of: profileIconBackgroundView)
profileIconBottomConstraint = profileIconImageView.pin(
.bottom,
to: .bottom,
of: profileIconBackgroundView,
withInset: 0
)
profileIconLabel.pin(to: profileIconBackgroundView)
profileIconBackgroundLeadingAlignConstraint = profileIconBackgroundView.pin(.leading, to: .leading, of: imageContainerView)
profileIconBackgroundTrailingAlignConstraint = profileIconBackgroundView.pin(.trailing, to: .trailing, of: imageContainerView)
profileIconBackgroundTopAlignConstraint = profileIconBackgroundView.pin(.top, to: .top, of: imageContainerView)
profileIconBackgroundBottomAlignConstraint = profileIconBackgroundView.pin(.bottom, to: .bottom, of: imageContainerView)
profileIconBackgroundWidthConstraint = profileIconBackgroundView.set(.width, to: size.iconSize)
profileIconBackgroundHeightConstraint = profileIconBackgroundView.set(.height, to: size.iconSize)
profileIconBackgroundLeadingAlignConstraint.isActive = false
profileIconBackgroundTrailingAlignConstraint.isActive = false
profileIconBackgroundTopAlignConstraint.isActive = false
profileIconBackgroundBottomAlignConstraint.isActive = false
additionalProfileIconTopConstraint = additionalProfileIconImageView.pin(
.top,
to: .top,
of: additionalProfileIconBackgroundView,
withInset: 0
)
additionalProfileIconImageView.pin(.left, to: .left, of: additionalProfileIconBackgroundView)
additionalProfileIconImageView.pin(.right, to: .right, of: additionalProfileIconBackgroundView)
additionalProfileIconBottomConstraint = additionalProfileIconImageView.pin(
.bottom,
to: .bottom,
of: additionalProfileIconBackgroundView,
withInset: 0
)
additionalProfileIconLabel.pin(to: additionalProfileIconBackgroundView)
additionalProfileIconBackgroundLeadingAlignConstraint = additionalProfileIconBackgroundView.pin(.leading, to: .leading, of: additionalImageContainerView)
additionalProfileIconBackgroundTrailingAlignConstraint = additionalProfileIconBackgroundView.pin(.trailing, to: .trailing, of: additionalImageContainerView)
additionalProfileIconBackgroundView.pin(.bottom, to: .bottom, of: additionalImageContainerView)
additionalProfileIconBackgroundWidthConstraint = additionalProfileIconBackgroundView.set(.width, to: size.iconSize)
additionalProfileIconBackgroundHeightConstraint = additionalProfileIconBackgroundView.set(.height, to: size.iconSize)
additionalProfileIconBackgroundLeadingAlignConstraint.isActive = false
additionalProfileIconBackgroundTrailingAlignConstraint.isActive = false
}
// MARK: - Functions
public func setDataManager(_ dataManager: ImageDataManagerType) {
self.dataManager = dataManager
self.imageView.setDataManager(dataManager)
self.additionalImageView.setDataManager(dataManager)
}
// MARK: - Content
@MainActor private func updateIconView(
icon: Info.ProfileIcon,
imageView: UIImageView,
label: UILabel,
backgroundView: UIView,
topConstraint: NSLayoutConstraint,
leadingAlignConstraint: NSLayoutConstraint,
trailingAlignConstraint: NSLayoutConstraint,
bottomConstraint: NSLayoutConstraint
) {
backgroundView.isHidden = (icon == .none)
leadingAlignConstraint.isActive = icon.isLeadingAligned
trailingAlignConstraint.isActive = !icon.isLeadingAligned
topConstraint.constant = icon.iconVerticalInset(for: size)
bottomConstraint.constant = -icon.iconVerticalInset(for: size)
switch icon {
case .none:
imageView.image = nil
imageView.isHidden = true
label.isHidden = true
case .crown:
imageView.image = UIImage(named: "ic_crown")?.withRenderingMode(.alwaysTemplate)
imageView.contentMode = .scaleAspectFit
imageView.themeTintColor = .dynamicForPrimary(
.green,
use: .profileIcon_greenPrimaryColor,
otherwise: .profileIcon
)
backgroundView.themeBackgroundColor = .profileIcon_background
imageView.isHidden = false
label.isHidden = true
profileIconBackgroundTopAlignConstraint.isActive = false
profileIconBackgroundBottomAlignConstraint.isActive = true
case .rightPlus:
imageView.image = UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold))
imageView.contentMode = .scaleAspectFit
imageView.themeTintColor = .black
backgroundView.themeBackgroundColor = .primary
imageView.isHidden = false
label.isHidden = true
profileIconBackgroundTopAlignConstraint.isActive = false
profileIconBackgroundBottomAlignConstraint.isActive = true
case .letter(let character, let dangerMode):
label.themeTextColor = (dangerMode ? .textPrimary : .backgroundPrimary)
backgroundView.themeBackgroundColor = (dangerMode ? .danger : .textPrimary)
label.isHidden = false
label.text = "\(character)"
profileIconBackgroundTopAlignConstraint.isActive = true
profileIconBackgroundBottomAlignConstraint.isActive = false
case .pencil:
imageView.image = Lucide.image(icon: .pencil, size: 14)?.withRenderingMode(.alwaysTemplate)
imageView.contentMode = .center
imageView.themeTintColor = .black
backgroundView.themeBackgroundColor = .primary
imageView.isHidden = false
label.isHidden = true
profileIconBackgroundTopAlignConstraint.isActive = false
profileIconBackgroundBottomAlignConstraint.isActive = true
case .qrCode:
imageView.image = Lucide.image(icon: .qrCode, size: (size == .expanded ? 20 : 14))?.withRenderingMode(.alwaysTemplate)
imageView.contentMode = .center
imageView.themeTintColor = .black
backgroundView.themeBackgroundColor = .primary
imageView.isHidden = false
label.isHidden = true
profileIconBackgroundTopAlignConstraint.isActive = true
profileIconBackgroundBottomAlignConstraint.isActive = false
trailingAlignConstraint.constant = (size == .expanded ? -8 : 0)
}
}
// MARK: - Content
private func prepareForReuse() {
imageView.image = nil
imageView.shouldAnimateImage = false
imageContainerView.themeBackgroundColor = .backgroundSecondary
additionalImageContainerView.isHidden = true
additionalImageView.image = nil
additionalImageView.shouldAnimateImage = false
imageViewTopConstraint.isActive = false
imageViewLeadingConstraint.isActive = false
imageViewCenterXConstraint.isActive = true
imageViewCenterYConstraint.isActive = true
profileIconBackgroundView.isHidden = true
profileIconBackgroundLeadingAlignConstraint.isActive = false
profileIconBackgroundTrailingAlignConstraint.isActive = false
profileIconImageView.isHidden = true
profileIconLabel.isHidden = true
additionalProfileIconBackgroundView.isHidden = true
additionalProfileIconBackgroundLeadingAlignConstraint.isActive = false
additionalProfileIconBackgroundTrailingAlignConstraint.isActive = false
additionalProfileIconImageView.isHidden = true
additionalProfileIconLabel.isHidden = true
imageEdgeConstraints.forEach { $0.constant = 0 }
additionalImageEdgeConstraints.forEach { $0.constant = 0 }
}
@MainActor public func update(
_ info: Info,
additionalInfo: Info? = nil
) {
prepareForReuse()
// Sort out the icon first
updateIconView(
icon: info.icon,
imageView: profileIconImageView,
label: profileIconLabel,
backgroundView: profileIconBackgroundView,
topConstraint: profileIconTopConstraint,
leadingAlignConstraint: profileIconBackgroundLeadingAlignConstraint,
trailingAlignConstraint: profileIconBackgroundTrailingAlignConstraint,
bottomConstraint: profileIconBottomConstraint
)
// Populate the main imageView
switch (info.source, info.renderingMode) {
case (.image(_, let image), .some(let renderingMode)):
imageView.image = image?.withRenderingMode(renderingMode)
case (.some(let source), _):
let originalOrientation: UIImage.Orientation? = source.knownOrientation
imageView.loadImage(source) { [weak self] buffer in
/// Now that the image has loaded the "proper" orientation information will have been loaded which may be
/// different from the initial value set (because we took a fast path), in that case we need to re-apply the
/// `contentsRect` to ensure it renders correctly
guard
let self,
originalOrientation != self.imageView.imageOrientationMetadata
else { return }
self.imageView.layer.contentsRect = self.contentsRect(
for: info.source,
cropRect: info.cropRect,
orientationMetadata: self.imageView.imageOrientationMetadata
)
}
default: imageView.image = nil
}
imageView.shouldAnimateImage = info.canAnimate
imageView.themeTintColor = info.themeTintColor
imageView.layer.contentsRect = contentsRect(
for: info.source,
cropRect: info.cropRect,
orientationMetadata: imageView.imageOrientationMetadata
)
imageContainerView.themeBackgroundColor = info.backgroundColor
imageContainerView.themeBackgroundColorForced = info.forcedBackgroundColor
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
imageEdgeConstraints.enumerated().forEach { index, constraint in
switch index % 4 {
case 0: constraint.constant = info.inset.top
case 1: constraint.constant = info.inset.left
case 2: constraint.constant = -info.inset.bottom
case 3: constraint.constant = -info.inset.right
default: break
}
}
// Check if there is a second image (if not then set the size and finish)
guard let additionalInfo: Info = additionalInfo else {
imageViewWidthConstraint.constant = size.imageSize
imageViewHeightConstraint.constant = size.imageSize
imageContainerView.layer.cornerRadius = (size.imageSize / 2)
return
}
// Sort out the additional icon first
updateIconView(
icon: additionalInfo.icon,
imageView: additionalProfileIconImageView,
label: additionalProfileIconLabel,
backgroundView: additionalProfileIconBackgroundView,
topConstraint: additionalProfileIconTopConstraint,
leadingAlignConstraint: additionalProfileIconBackgroundLeadingAlignConstraint,
trailingAlignConstraint: additionalProfileIconBackgroundTrailingAlignConstraint,
bottomConstraint: additionalProfileIconBottomConstraint
)
// Set the additional image content and reposition the image views correctly
switch (additionalInfo.source, additionalInfo.renderingMode) {
case (.image(_, let image), .some(let renderingMode)):
additionalImageContainerView.isHidden = false
additionalImageView.image = image?.withRenderingMode(renderingMode)
case (.some(let source), _):
let originalOrientation: UIImage.Orientation? = source.knownOrientation
additionalImageContainerView.isHidden = false
additionalImageView.loadImage(source) { [weak self] buffer in
/// Now that the image has loaded the "proper" orientation information will have been loaded which may be
/// different from the initial value set (because we took a fast path), in that case we need to re-apply the
/// `contentsRect` to ensure it renders correctly
guard
let self,
originalOrientation != self.additionalImageView.imageOrientationMetadata
else { return }
self.additionalImageView.layer.contentsRect = self.contentsRect(
for: info.source,
cropRect: info.cropRect,
orientationMetadata: self.additionalImageView.imageOrientationMetadata
)
}
default:
additionalImageContainerView.isHidden = true
additionalImageView.image = nil
}
additionalImageView.shouldAnimateImage = additionalInfo.canAnimate
additionalImageView.themeTintColor = additionalInfo.themeTintColor
additionalImageView.layer.contentsRect = contentsRect(
for: additionalInfo.source,
cropRect: additionalInfo.cropRect,
orientationMetadata: imageView.imageOrientationMetadata
)
switch (info.backgroundColor, info.forcedBackgroundColor) {
case (_, .some(let color)): additionalImageContainerView.themeBackgroundColorForced = color
case (.some(let color), _): additionalImageContainerView.themeBackgroundColor = color
default: additionalImageContainerView.themeBackgroundColor = .primary
}
additionalImageEdgeConstraints.enumerated().forEach { index, constraint in
switch index % 4 {
case 0: constraint.constant = additionalInfo.inset.top
case 1: constraint.constant = additionalInfo.inset.left
case 2: constraint.constant = -additionalInfo.inset.bottom
case 3: constraint.constant = -additionalInfo.inset.right
default: break
}
}
imageViewTopConstraint.isActive = true
imageViewLeadingConstraint.isActive = true
imageViewCenterXConstraint.isActive = false
imageViewCenterYConstraint.isActive = false
imageViewWidthConstraint.constant = size.multiImageSize
imageViewHeightConstraint.constant = size.multiImageSize
imageContainerView.layer.cornerRadius = (size.multiImageSize / 2)
additionalImageViewWidthConstraint.constant = size.multiImageSize
additionalImageViewHeightConstraint.constant = size.multiImageSize
additionalImageContainerView.layer.cornerRadius = (size.multiImageSize / 2)
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
}
private func contentsRect(
for source: ImageDataManager.DataSource?,
cropRect: CGRect?,
orientationMetadata: UIImage.Orientation?
) -> CGRect {
guard
let source: ImageDataManager.DataSource = source,
let cropRect: CGRect = cropRect
else { return CGRect(x: 0, y: 0, width: 1, height: 1) }
/// Try to use the `orientationMetadata` stored on the `imageView` if present, falling back to the fast `knownOrientation`
/// in the DataSource and lastly `up` if neither are present
let targetOrientation: UIImage.Orientation = ((orientationMetadata ?? source.knownOrientation) ?? .up)
switch targetOrientation {
case .up: return cropRect
case .upMirrored:
return CGRect(
x: (1 - cropRect.maxX),
y: cropRect.minY,
width: cropRect.width,
height: cropRect.height
)
case .down:
return CGRect(
x: (1 - cropRect.maxX),
y: (1 - cropRect.maxY),
width: cropRect.width,
height: cropRect.height
)
case .downMirrored:
return CGRect(
x: cropRect.minX,
y: (1 - cropRect.maxY),
width: cropRect.width,
height: cropRect.height
)
case .left:
return CGRect(
x: (1 - cropRect.maxY),
y: cropRect.minX,
width: cropRect.height,
height: cropRect.width
)
case .leftMirrored:
return CGRect(
x: cropRect.minY,
y: cropRect.minX,
width: cropRect.height,
height: cropRect.width
)
case .right:
return CGRect(
x: cropRect.minY,
y: (1 - cropRect.maxX),
width: cropRect.height,
height: cropRect.width
)
case .rightMirrored:
return CGRect(
x: (1 - cropRect.maxY),
y: (1 - cropRect.maxX),
width: cropRect.height,
height: cropRect.width
)
@unknown default: return cropRect
}
}
public func getTouchedView(from localPoint: CGPoint) -> UIView {
if profileIconBackgroundView.frame.contains(localPoint) {
return profileIconBackgroundView
}
return self
}
}
import SwiftUI
public struct ProfilePictureSwiftUI: UIViewRepresentable {
public typealias UIViewType = ProfilePictureView
var size: ProfilePictureView.Info.Size
var info: ProfilePictureView.Info
var additionalInfo: ProfilePictureView.Info?
let dataManager: ImageDataManagerType
public init(
size: ProfilePictureView.Info.Size,
info: ProfilePictureView.Info,
additionalInfo: ProfilePictureView.Info? = nil,
dataManager: ImageDataManagerType
) {
self.size = size
self.info = info
self.additionalInfo = additionalInfo
self.dataManager = dataManager
}
public func makeUIView(context: Context) -> ProfilePictureView {
ProfilePictureView(
size: size,
dataManager: dataManager
)
}
@MainActor public func updateUIView(_ profilePictureView: ProfilePictureView, context: Context) {
profilePictureView.update(
info,
additionalInfo: additionalInfo
)
}
}