diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 5962e0a85..edf60c85b 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -251,7 +251,7 @@ extension StatusSection { default: return 0.7 } }() - return CGSize(width: maxWidth, height: maxWidth * scale) + return CGSize(width: maxWidth, height: floor(maxWidth * scale)) }() let mosaics: [MosaicImageViewContainer.ConfigurableMosaic] = { if mosaicImageViewModel.metas.count == 1 { @@ -268,13 +268,16 @@ extension StatusSection { let blurhashOverlayImageView = mosaic.blurhashOverlayImageView let meta = mosaicImageViewModel.metas[i] + // set blurhash image meta.blurhashImagePublisher() - .receive(on: RunLoop.main) .sink { image in blurhashOverlayImageView.image = image } .store(in: &cell.disposeBag) + let isSingleMosaicLayout = mosaics.count == 1 + + // set image let imageSize = CGSize( width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale, height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale @@ -282,12 +285,18 @@ extension StatusSection { let request = ImageRequest( url: meta.url, processors: [ - ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill) + ImageProcessors.Resize( + size: imageSize, + unit: .pixels, + contentMode: isSingleMosaicLayout ? .aspectFill : .aspectFit, + crop: isSingleMosaicLayout + ) ] ) let options = ImageLoadingOptions( transition: .fadeIn(duration: 0.2) ) + Nuke.loadImage( with: request, options: options, @@ -296,28 +305,17 @@ extension StatusSection { switch result { case .failure: break - case .success: + case .success(let response) statusItemAttribute.isImageLoaded.value = true } } - //imageView.af.setImage( - // withURL: meta.url, - // placeholderImage: UIImage.placeholder(color: .systemFill), - // imageTransition: .crossDissolve(0.2) - //) { response in - // switch response.result { - // case .success: - // statusItemAttribute.isImageLoaded.value = true - // case .failure: - // break - // } - //} + imageView.accessibilityLabel = meta.altText Publishers.CombineLatest( statusItemAttribute.isImageLoaded, statusItemAttribute.isRevealing ) - .receive(on: RunLoop.main) + .receive(on: DispatchQueue.main) // needs call immediately .sink { [weak cell] isImageLoaded, isMediaRevealing in guard let cell = cell else { return } guard isImageLoaded else { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 68ed2a62c..2bb32288f 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -104,7 +104,7 @@ extension MosaicImageViewContainer { imageViews = [] blurhashOverlayImageViews = [] - container.spacing = 1 + container.spacing = UIView.separatorLineHeight(of: self) * 2 // 2px } struct ConfigurableMosaic { @@ -120,12 +120,16 @@ extension MosaicImageViewContainer { contentView.translatesAutoresizingMaskIntoConstraints = false container.addArrangedSubview(contentView) - let rect = AVMakeRect( - aspectRatio: aspectRatio, - insideRect: CGRect(origin: .zero, size: maxSize) - ) + let imageViewSize: CGSize = { + let rect = AVMakeRect( + aspectRatio: aspectRatio, + insideRect: CGRect(origin: .zero, size: maxSize) + ).integral + return rect.size + }() + let imageViewFrame = CGRect(origin: .zero, size: imageViewSize) - let imageView = UIImageView() + let imageView = UIImageView(frame: imageViewFrame) imageViews.append(imageView) imageView.layer.masksToBounds = true imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius @@ -138,9 +142,9 @@ extension MosaicImageViewContainer { imageView.topAnchor.constraint(equalTo: contentView.topAnchor), imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - imageView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1), + imageView.widthAnchor.constraint(equalToConstant: imageViewFrame.width).priority(.required - 1), ]) - containerHeightLayoutConstraint.constant = floor(rect.height) + containerHeightLayoutConstraint.constant = imageViewFrame.height containerHeightLayoutConstraint.isActive = true let blurhashOverlayImageView = UIImageView() @@ -170,7 +174,7 @@ extension MosaicImageViewContainer { return ConfigurableMosaic( imageView: imageView, blurhashOverlayImageView: blurhashOverlayImageView, - imageViewSize: maxSize + imageViewSize: imageViewSize ) } @@ -181,6 +185,7 @@ extension MosaicImageViewContainer { } let maxHeight = maxSize.height + let spacing: CGFloat = 1 containerHeightLayoutConstraint.constant = maxHeight containerHeightLayoutConstraint.isActive = true @@ -190,7 +195,7 @@ extension MosaicImageViewContainer { [contentLeftStackView, contentRightStackView].forEach { stackView in stackView.axis = .vertical stackView.distribution = .fillEqually - stackView.spacing = 1 + stackView.spacing = spacing } container.addArrangedSubview(contentLeftStackView) container.addArrangedSubview(contentRightStackView) @@ -310,22 +315,23 @@ extension MosaicImageViewContainer { let imageViewSize: CGSize = { switch (i, count) { case (_, 4): - return CGSize(width: maxSize.width * 0.5, height: maxSize.height * 0.5) + return CGSize(width: maxSize.width * 0.5 - spacing, height: maxSize.height * 0.5 - spacing) case (i, 3): - let width = maxSize.width * 0.5 + let width = maxSize.width * 0.5 - spacing if i == 0 { return CGSize(width: width, height: maxSize.height) } else { - return CGSize(width: width, height: maxSize.height * 0.5) + return CGSize(width: width, height: maxSize.height * 0.5 - spacing) } case (_, 2): - let width = maxSize.width * 0.5 + let width = maxSize.width * 0.5 - spacing return CGSize(width: width, height: maxSize.height) default: assertionFailure() return maxSize } }() + imageView.frame.size = imageViewSize let mosaic = ConfigurableMosaic( imageView: imageView, blurhashOverlayImageView: blurhashOverlayImageView, diff --git a/Mastodon/Service/BlurhashImageCacheService.swift b/Mastodon/Service/BlurhashImageCacheService.swift index 54526dcbc..be729a2f8 100644 --- a/Mastodon/Service/BlurhashImageCacheService.swift +++ b/Mastodon/Service/BlurhashImageCacheService.swift @@ -15,22 +15,25 @@ final class BlurhashImageCacheService { let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent) func image(blurhash: String, size: CGSize, url: URL) -> AnyPublisher { + let key = Key(blurhash: blurhash, size: size, url: url) + + if let image = self.cache.object(forKey: key) { + return Just(image).eraseToAnyPublisher() + } + return Future { promise in self.workingQueue.async { - let key = Key(blurhash: blurhash, size: size, url: url) - guard let image = self.cache.object(forKey: key) else { - if let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) { - self.cache.setObject(image, forKey: key) - promise(.success(image)) - } else { - promise(.success(nil)) - } + guard let image = BlurhashImageCacheService.blurhashImage(blurhash: blurhash, size: size, url: url) else { + promise(.success(nil)) return } + self.cache.setObject(image, forKey: key) promise(.success(image)) } } + .receive(on: RunLoop.main) .eraseToAnyPublisher() + } static func blurhashImage(blurhash: String, size: CGSize, url: URL) -> UIImage? { @@ -55,13 +58,7 @@ final class BlurhashImageCacheService { } extension BlurhashImageCacheService { - class Key: Hashable { - static func == (lhs: BlurhashImageCacheService.Key, rhs: BlurhashImageCacheService.Key) -> Bool { - return lhs.blurhash == rhs.blurhash - && lhs.size == rhs.size - && lhs.url == rhs.url - } - + class Key: NSObject { let blurhash: String let size: CGSize let url: URL @@ -72,11 +69,19 @@ extension BlurhashImageCacheService { self.url = url } - func hash(into hasher: inout Hasher) { - hasher.combine(blurhash) - hasher.combine(size.width) - hasher.combine(size.height) - hasher.combine(url) + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? Key else { return false } + return object.blurhash == blurhash + && object.size == size + && object.url == url } + + override var hash: Int { + return blurhash.hashValue ^ + size.width.hashValue ^ + size.height.hashValue ^ + url.hashValue + } + } } diff --git a/Mastodon/Service/PlaceholderImageCacheService.swift b/Mastodon/Service/PlaceholderImageCacheService.swift index ca56b331d..ecfde6d49 100644 --- a/Mastodon/Service/PlaceholderImageCacheService.swift +++ b/Mastodon/Service/PlaceholderImageCacheService.swift @@ -33,7 +33,7 @@ final class PlaceholderImageCacheService { } extension PlaceholderImageCacheService { - class Key: Hashable { + class Key: NSObject { let color: UIColor let size: CGSize let cornerRadius: CGFloat @@ -44,17 +44,18 @@ extension PlaceholderImageCacheService { self.cornerRadius = cornerRadius } - static func == (lhs: PlaceholderImageCacheService.Key, rhs: PlaceholderImageCacheService.Key) -> Bool { - return lhs.color == rhs.color - && lhs.size == rhs.size - && lhs.cornerRadius == rhs.cornerRadius + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? Key else { return false } + return object.color == color + && object.size == size + && object.cornerRadius == cornerRadius } - func hash(into hasher: inout Hasher) { - hasher.combine(color) - hasher.combine(size.width) - hasher.combine(size.height) - hasher.combine(cornerRadius) + override var hash: Int { + return color.hashValue ^ + size.width.hashValue ^ + size.height.hashValue ^ + cornerRadius.hashValue } } }