Show BlurHash previews of attachments

This commit is contained in:
Shadowfacts 2020-09-12 10:48:27 -04:00
parent e44ae29775
commit 9534f19262
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 286 additions and 48 deletions

View File

@ -17,6 +17,7 @@ public class Attachment: Codable {
public let textURL: URL? public let textURL: URL?
public let meta: Metadata? public let meta: Metadata?
public let description: String? public let description: String?
public let blurHash: String?
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> { public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([ return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
@ -35,6 +36,7 @@ public class Attachment: Codable {
self.textURL = try? container.decode(URL?.self, forKey: .textURL) self.textURL = try? container.decode(URL?.self, forKey: .textURL)
self.meta = try? container.decode(Metadata?.self, forKey: .meta) self.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description) self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
@ -46,6 +48,7 @@ public class Attachment: Codable {
case textURL = "text_url" case textURL = "text_url"
case meta case meta
case description case description
case blurHash = "blurhash"
} }
} }
@ -60,7 +63,7 @@ extension Attachment {
} }
extension Attachment { extension Attachment {
public class Metadata: Codable { public struct Metadata: Codable {
public let length: String? public let length: String?
public let duration: Float? public let duration: Float?
public let audioEncoding: String? public let audioEncoding: String?
@ -91,7 +94,7 @@ extension Attachment {
} }
} }
public class ImageMetadata: Codable { public struct ImageMetadata: Codable {
public let width: Int? public let width: Int?
public let height: Int? public let height: Int?
public let size: String? public let size: String?

View File

@ -169,6 +169,7 @@
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; }; D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; }; D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; }; D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; }; D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; }; D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
@ -494,6 +495,7 @@
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; }; D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; }; D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; }; D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; }; D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; }; D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; }; D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
@ -1119,6 +1121,14 @@
path = XCallbackURL; path = XCallbackURL;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D67B506B250B28FF00FAECFB /* Vendor */ = {
isa = PBXGroup;
children = (
D67B506C250B291200FAECFB /* BlurHashDecode.swift */,
);
path = Vendor;
sourceTree = "<group>";
};
D67C57A721E2649B00C3118B /* Account Detail */ = { D67C57A721E2649B00C3118B /* Account Detail */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1337,6 +1347,7 @@
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */, D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */, D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */, D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D67B506B250B28FF00FAECFB /* Vendor */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
D62D241E217AA46B005076CC /* Shortcuts */, D62D241E217AA46B005076CC /* Shortcuts */,
@ -1800,6 +1811,7 @@
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */, D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */, D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */, D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,

View File

@ -77,6 +77,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
case .image: case .image:
let vc = LoadingLargeImageViewController(attachment: attachment) let vc = LoadingLargeImageViewController(attachment: attachment)
vc.shrinkGestureEnabled = false vc.shrinkGestureEnabled = false
vc.animationSourceView = sourceViews[index]
return vc return vc
case .video, .audio: case .video, .audio:
let vc = GalleryPlayerViewController() let vc = GalleryPlayerViewController()

View File

@ -31,7 +31,12 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
@IBOutlet weak var bottomControlsView: UIView! @IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel!
var contentView: ContentView var contentView: ContentView {
didSet {
oldValue.removeFromSuperview()
setupContentView()
}
}
var contentViewLeadingConstraint: NSLayoutConstraint! var contentViewLeadingConstraint: NSLayoutConstraint!
var contentViewTopConstraint: NSLayoutConstraint! var contentViewTopConstraint: NSLayoutConstraint!
@ -76,14 +81,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
contentView.translatesAutoresizingMaskIntoConstraints = false setupContentView()
scrollView.addSubview(contentView)
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
NSLayoutConstraint.activate([
contentViewLeadingConstraint,
contentViewTopConstraint,
])
setControlsVisible(initialControlsVisible, animated: false) setControlsVisible(initialControlsVisible, animated: false)
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
@ -107,6 +105,17 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
view.addGestureRecognizer(doubleTap) view.addGestureRecognizer(doubleTap)
} }
private func setupContentView() {
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
NSLayoutConstraint.activate([
contentViewLeadingConstraint,
contentViewTopConstraint,
])
}
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()

View File

@ -10,14 +10,16 @@ import Pachyderm
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController { class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
private var attachment: Attachment?
let url: URL let url: URL
let cache: ImageCache let cache: ImageCache
let imageDescription: String? let imageDescription: String?
var largeImageVC: LargeImageViewController? private(set) var loaded = false
var loadingVC: LoadingViewController? private(set) var largeImageVC: LargeImageViewController?
private var loadingVC: LoadingViewController?
var imageRequest: ImageCache.Request? private var imageRequest: ImageCache.Request?
private var initialControlsVisible: Bool = true private var initialControlsVisible: Bool = true
var controlsVisible: Bool { var controlsVisible: Bool {
@ -70,6 +72,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
convenience init(attachment: Attachment) { convenience init(attachment: Attachment) {
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description) self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
self.attachment = attachment
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -85,6 +88,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
if let data = cache.get(url) { if let data = cache.get(url) {
createLargeImage(data: data) createLargeImage(data: data)
} else { } else {
createPreview()
loadingVC = LoadingViewController() loadingVC = LoadingViewController()
embedChild(loadingVC!) embedChild(loadingVC!)
imageRequest = cache.get(url) { [weak self] (data) in imageRequest = cache.get(url) { [weak self] (data) in
@ -110,15 +115,32 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
} }
} }
func createLargeImage(data: Data) { private func createLargeImage(data: Data) {
guard !loaded else { return }
loaded = true
guard let image = UIImage(data: data) else { return } guard let image = UIImage(data: data) else { return }
let gifData = url.pathExtension == "gif" ? data : nil let gifData = url.pathExtension == "gif" ? data : nil
createLargeImage(image: image, gifData: gifData)
}
private func createLargeImage(image: UIImage, gifData: Data?) {
let imageView = LargeImageImageContentView(image: image, gifData: gifData) let imageView = LargeImageImageContentView(image: image, gifData: gifData)
if let existing = largeImageVC {
existing.contentView = imageView
} else {
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView) largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false largeImageVC!.shrinkGestureEnabled = false
embedChild(largeImageVC!) embedChild(largeImageVC!)
} }
}
private func createPreview() {
guard !self.loaded,
let image = animationSourceView?.image else { return }
self.createLargeImage(image: image, gifData: nil)
}
} }

148
Tusker/Vendor/BlurHashDecode.swift vendored Normal file
View File

@ -0,0 +1,148 @@
/// BlurHash reference decoder implementation.
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
import UIKit
extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
self.init(cgImage: cgImage)
}
}
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
return rgb
}
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
private extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}

View File

@ -25,6 +25,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
var attachment: Attachment! var attachment: Attachment!
var index: Int! var index: Int!
var expectedSize: CGSize!
private var attachmentRequest: ImageCache.Request? private var attachmentRequest: ImageCache.Request?
@ -35,12 +36,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
public lazy var animator: Animator? = Animator(withDelegate: self) public lazy var animator: Animator? = Animator(withDelegate: self)
init(attachment: Attachment, index: Int) { init(attachment: Attachment, index: Int, expectedSize: CGSize) {
super.init(image: nil) super.init(image: nil)
commonInit() commonInit()
self.attachment = attachment self.attachment = attachment
self.index = index self.index = index
self.expectedSize = expectedSize
loadAttachment() loadAttachment()
} }
@ -90,6 +92,31 @@ class AttachmentView: UIImageView, GIFAnimatable {
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else { guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
preconditionFailure("invalid attachment type") preconditionFailure("invalid attachment type")
} }
if let hash = attachment.blurHash {
DispatchQueue.global(qos: .default).async { [weak self] in
guard let self = self else { return }
let size: CGSize
if let meta = self.attachment.meta,
let width = meta.width, let height = meta.height {
size = CGSize(width: width, height: height)
} else if let orig = self.attachment.meta?.original,
let width = orig.width, let height = orig.height {
size = CGSize(width: width, height: height)
} else {
size = self.expectedSize
}
if let preview = UIImage(blurHash: hash, size: size) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.image == nil {
self.image = preview
}
}
}
}
}
switch attachment.kind { switch attachment.kind {
case .image: case .image:
loadImage() loadImage()

View File

@ -65,18 +65,18 @@ class AttachmentsContainerView: UIView {
switch attachments.count { switch attachments.count {
case 1: case 1:
let attachmentView = createAttachmentView(index: 0) let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
attachmentView.layer.cornerRadius = 5 attachmentView.layer.cornerRadius = 5
attachmentView.layer.masksToBounds = true attachmentView.layer.masksToBounds = true
fillView(attachmentView) fillView(attachmentView)
sendSubviewToBack(attachmentView) sendSubviewToBack(attachmentView)
accessibilityElements.append(attachmentView) accessibilityElements.append(attachmentView)
case 2: case 2:
let left = createAttachmentView(index: 0) let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
left.layer.cornerRadius = 5 left.layer.cornerRadius = 5
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
left.layer.masksToBounds = true left.layer.masksToBounds = true
let right = createAttachmentView(index: 1) let right = createAttachmentView(index: 1, hSize: .half, vSize: .full)
right.layer.cornerRadius = 5 right.layer.cornerRadius = 5
right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
right.layer.masksToBounds = true right.layer.masksToBounds = true
@ -92,15 +92,15 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(left) accessibilityElements.append(left)
accessibilityElements.append(right) accessibilityElements.append(right)
case 3: case 3:
let left = createAttachmentView(index: 0) let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
left.layer.cornerRadius = 5 left.layer.cornerRadius = 5
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
left.layer.masksToBounds = true left.layer.masksToBounds = true
let topRight = createAttachmentView(index: 1) let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5 topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true topRight.layer.masksToBounds = true
let bottomRight = createAttachmentView(index: 2) let bottomRight = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomRight.layer.cornerRadius = 5 bottomRight.layer.cornerRadius = 5
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
bottomRight.layer.masksToBounds = true bottomRight.layer.masksToBounds = true
@ -121,11 +121,11 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(topRight) accessibilityElements.append(topRight)
accessibilityElements.append(bottomRight) accessibilityElements.append(bottomRight)
case 4: case 4:
let topLeft = createAttachmentView(index: 0) let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
topLeft.layer.cornerRadius = 5 topLeft.layer.cornerRadius = 5
topLeft.layer.maskedCorners = .layerMinXMinYCorner topLeft.layer.maskedCorners = .layerMinXMinYCorner
topLeft.layer.masksToBounds = true topLeft.layer.masksToBounds = true
let bottomLeft = createAttachmentView(index: 2) let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomLeft.layer.cornerRadius = 5 bottomLeft.layer.cornerRadius = 5
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
bottomLeft.layer.masksToBounds = true bottomLeft.layer.masksToBounds = true
@ -133,11 +133,11 @@ class AttachmentsContainerView: UIView {
topLeft, topLeft,
bottomLeft bottomLeft
]) ])
let topRight = createAttachmentView(index: 1) let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5 topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true topRight.layer.masksToBounds = true
let bottomRight = createAttachmentView(index: 3) let bottomRight = createAttachmentView(index: 3, hSize: .half, vSize: .half)
bottomRight.layer.cornerRadius = 5 bottomRight.layer.cornerRadius = 5
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
bottomRight.layer.masksToBounds = true bottomRight.layer.masksToBounds = true
@ -177,11 +177,11 @@ class AttachmentsContainerView: UIView {
moreView.addSubview(moreLabel) moreView.addSubview(moreLabel)
moreView.accessibilityLabel = moreLabel.text moreView.accessibilityLabel = moreLabel.text
let topLeft = createAttachmentView(index: 0) let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
topLeft.layer.cornerRadius = 5 topLeft.layer.cornerRadius = 5
topLeft.layer.maskedCorners = .layerMinXMinYCorner topLeft.layer.maskedCorners = .layerMinXMinYCorner
topLeft.layer.masksToBounds = true topLeft.layer.masksToBounds = true
let bottomLeft = createAttachmentView(index: 2) let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
bottomLeft.layer.cornerRadius = 5 bottomLeft.layer.cornerRadius = 5
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
bottomLeft.layer.masksToBounds = true bottomLeft.layer.masksToBounds = true
@ -189,7 +189,7 @@ class AttachmentsContainerView: UIView {
topLeft, topLeft,
bottomLeft bottomLeft
]) ])
let topRight = createAttachmentView(index: 1) let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
topRight.layer.cornerRadius = 5 topRight.layer.cornerRadius = 5
topRight.layer.maskedCorners = .layerMaxXMinYCorner topRight.layer.maskedCorners = .layerMaxXMinYCorner
topRight.layer.masksToBounds = true topRight.layer.masksToBounds = true
@ -225,8 +225,24 @@ class AttachmentsContainerView: UIView {
contentHidden = Preferences.shared.blurAllMedia || status.sensitive contentHidden = Preferences.shared.blurAllMedia || status.sensitive
} }
private func createAttachmentView(index: Int) -> AttachmentView { private func createAttachmentView(index: Int, hSize: RelativeSize, vSize: RelativeSize) -> AttachmentView {
let attachmentView = AttachmentView(attachment: attachments[index], index: index) let width: CGFloat
switch hSize {
case .full:
width = bounds.width
case .half:
width = (bounds.width - 4) / 2
}
let height: CGFloat
switch vSize {
case .full:
height = bounds.height
case .half:
height = (bounds.height - 4) / 2
}
let size = CGSize(width: width, height: height)
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
attachmentView.delegate = delegate attachmentView.delegate = delegate
attachmentView.translatesAutoresizingMaskIntoConstraints = false attachmentView.translatesAutoresizingMaskIntoConstraints = false
attachmentView.isAccessibilityElement = true attachmentView.isAccessibilityElement = true
@ -376,8 +392,7 @@ class AttachmentsContainerView: UIView {
} }
fileprivate extension UIView { fileprivate enum RelativeSize {
enum RelativeSize {
case full, half case full, half
var multiplier: CGFloat { var multiplier: CGFloat {
@ -390,6 +405,7 @@ fileprivate extension UIView {
} }
} }
fileprivate extension UIView {
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint { func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2) return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
} }