Browse Source

Large image view

pixelfed
Shadowfacts 3 years ago
parent
commit
e523d69e33
Signed by: shadowfacts GPG Key ID: 94A5AB95422746E5
  1. 40
      Tusker.xcodeproj/project.pbxproj
  2. 69
      Tusker/Assets.xcassets/Close.imageset/Close.pdf
  3. 24
      Tusker/Assets.xcassets/Close.imageset/Contents.json
  4. 24
      Tusker/Assets.xcassets/Download.imageset/Contents.json
  5. 69
      Tusker/Assets.xcassets/Download.imageset/Download.pdf
  6. 4
      Tusker/AvatarCache.swift
  7. 113
      Tusker/Extensions/UIViewController+Delegates.swift
  8. 61
      Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift
  9. 6
      Tusker/Info.plist
  10. 152
      Tusker/Storyboards/LargeImage.storyboard
  11. 206
      Tusker/View Controllers/LargeImageViewController.swift
  12. 61
      Tusker/View Controllers/Transitions/LargeImageExpandAnimationController.swift
  13. 60
      Tusker/View Controllers/Transitions/LargeImageInteractionController.swift
  14. 67
      Tusker/View Controllers/Transitions/LargeImageShrinkAnimationController.swift
  15. 64
      Tusker/Views/AttachmentView.swift
  16. 51
      Tusker/Views/StatusTableViewCell.swift
  17. 19
      Tusker/Views/StatusTableViewCell.xib

40
Tusker.xcodeproj/project.pbxproj

@ -13,6 +13,9 @@
D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; };
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A0CD22132153900640E3B /* HTMLContentLabel.swift */; };
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; };
@ -35,13 +38,16 @@
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; };
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; };
D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */; };
D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */; };
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; };
D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; };
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; };
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD3212518A000E1C4BB /* Main.storyboard */; };
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
@ -94,6 +100,9 @@
D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = "<group>"; };
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
D64A0CD22132153900640E3B /* HTMLContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLContentLabel.swift; sourceTree = "<group>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
@ -117,12 +126,15 @@
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = "<group>"; };
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = "<group>"; };
D667E5EE2134C39F0057A976 /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; };
D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+StatusTableViewCellDelegate.swift"; sourceTree = "<group>"; };
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
D667E5F22135BC260057A976 /* Conversation.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Conversation.storyboard; sourceTree = "<group>"; };
D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; };
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = "<group>"; };
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
D6D4DDD4212518A000E1C4BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@ -168,6 +180,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D646C954213B364600269FB5 /* Transitions */ = {
isa = PBXGroup;
children = (
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */,
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */,
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */,
);
path = Transitions;
sourceTree = "<group>";
};
D663626021360A9600C9CBA2 /* Preferences */ = {
isa = PBXGroup;
children = (
@ -181,7 +203,7 @@
D667E5F62135C2ED0057A976 /* Extensions */ = {
isa = PBXGroup;
children = (
D667E5F02134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift */,
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */,
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
D66362722136FFC600C9CBA2 /* UITextView+Placeholder.swift */,
@ -204,6 +226,7 @@
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -272,6 +295,7 @@
D6F953E9212519B800CF0F2B /* View Controllers */ = {
isa = PBXGroup;
children = (
D646C954213B364600269FB5 /* Transitions */,
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */,
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
@ -279,6 +303,7 @@
D667E5F42135BCD50057A976 /* ConversationViewController.swift */,
D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
);
path = "View Controllers";
sourceTree = "<group>";
@ -293,6 +318,7 @@
D667E5F22135BC260057A976 /* Conversation.storyboard */,
D663626521360DD700C9CBA2 /* Preferences.storyboard */,
D663626E213632A000C9CBA2 /* Compose.storyboard */,
D6C94D842139DFD800CB5196 /* LargeImage.storyboard */,
);
path = Storyboards;
sourceTree = "<group>";
@ -418,6 +444,7 @@
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */,
D663626F213632A000C9CBA2 /* Compose.storyboard in Resources */,
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */,
D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */,
D667E5E3213499F70057A976 /* Profile.storyboard in Resources */,
D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */,
@ -451,12 +478,16 @@
D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */,
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
@ -466,11 +497,12 @@
D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */,
D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
D663626A2136163000C9CBA2 /* PreferencesAdaptive.swift in Sources */,
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+StatusTableViewCellDelegate.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,

69
Tusker/Assets.xcassets/Close.imageset/Close.pdf

@ -0,0 +1,69 @@
%PDF-1.5
%µí®û
3 0 obj
<< /Length 4 0 R
/Filter /FlateDecode
>>
stream
xœ]‘KÂ0 D÷>…/€‰óÏ18ªÔ²( àþI[uQM:Ïs_àp<ï ¯w‡Û"qŽè#y×ð‰\#¥ÔлHÙEÜ‘›#怞û;¥®¹Ä¦Å™À"Ëü”§5œô¸ÊkD†e‰1¿à(` ­”{àÂjÅF…“
)|8{ð8,‡£¨8ÂIŒ h >þYW=˜ÅŸs�‘J •Ž'̉!ŽÚ* TJ;µku]ääÒ=‹ûj—®º²Ý`çNMÊÊÈàLo°+Üà ¹e¤
endstream
endobj
4 0 obj
206
endobj
2 0 obj
<<
/ExtGState <<
/a0 << /CA 1 /ca 1 >>
>>
>>
endobj
5 0 obj
<< /Type /Page
/Parent 1 0 R
/MediaBox [ 0 0 208.76799 208.76799 ]
/Contents 3 0 R
/Group <<
/Type /Group
/S /Transparency
/I true
/CS /DeviceRGB
>>
/Resources 2 0 R
>>
endobj
1 0 obj
<< /Type /Pages
/Kids [ 5 0 R ]
/Count 1
>>
endobj
6 0 obj
<< /Creator (cairo 1.14.8 (http://cairographics.org))
/Producer (cairo 1.14.8 (http://cairographics.org))
>>
endobj
7 0 obj
<< /Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 8
0000000000 65535 f
0000000618 00000 n
0000000320 00000 n
0000000015 00000 n
0000000298 00000 n
0000000392 00000 n
0000000683 00000 n
0000000810 00000 n
trailer
<< /Size 8
/Root 7 0 R
/Info 6 0 R
>>
startxref
862
%%EOF

24
Tusker/Assets.xcassets/Close.imageset/Contents.json

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Close.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

24
Tusker/Assets.xcassets/Download.imageset/Contents.json

@ -0,0 +1,24 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Download.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

69
Tusker/Assets.xcassets/Download.imageset/Download.pdf

@ -0,0 +1,69 @@
%PDF-1.5
%µí®û
3 0 obj
<< /Length 4 0 R
/Filter /FlateDecode
>>
stream
xœ}RKnC1Üû¾@¨ÁàÏ1z„êIIÉ¢íý¥Â³�ÕDªÞ†™7†áóR´ïûß>R¼ýd@n‘d®ñ©tÈXq�T+ Ð"¨eà"‘R‡gž.ñpf&¾;Q4/yºKòRK~IAŸ éÅœŽ0­ÿÇ µÔ]Ãéb¦ÿtôÅçK“We:`k±ØV*Ô®�[Éxö � p§ˆšºvÈÐÔÂT[‘=«¼
Hå…JiÅÕ… �Í35Œ\€²Íó…Èã‡z¢ºáp;õš@Ì·DAEßÐduÉÃênŒà©a��¶(ÒµÓ-·ÀÚÍ×$¸–¦63PR[‚Z–ö°°m±ªCv�´–ýù„3ýáÄt`«ß”΋gÜguÖt|¸)W Ïa§xêóÇKï×@ZªöxŒ=Õº†fóÐS#ŸóDs Sö–ÆVÿ]*ï¥R±ãñ3¹‡Ó�9Y‡äxžÙz1 [Š³ß)É™+lâ©G›Åkß×ð~dˆÎÉ
endstream
endobj
4 0 obj
395
endobj
2 0 obj
<<
/ExtGState <<
/a0 << /CA 1 /ca 1 >>
>>
>>
endobj
5 0 obj
<< /Type /Page
/Parent 1 0 R
/MediaBox [ 0 0 283.464569 283.464569 ]
/Contents 3 0 R
/Group <<
/Type /Group
/S /Transparency
/I true
/CS /DeviceRGB
>>
/Resources 2 0 R
>>
endobj
1 0 obj
<< /Type /Pages
/Kids [ 5 0 R ]
/Count 1
>>
endobj
6 0 obj
<< /Creator (cairo 1.14.8 (http://cairographics.org))
/Producer (cairo 1.14.8 (http://cairographics.org))
>>
endobj
7 0 obj
<< /Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 8
0000000000 65535 f
0000000809 00000 n
0000000509 00000 n
0000000015 00000 n
0000000487 00000 n
0000000581 00000 n
0000000874 00000 n
0000001001 00000 n
trailer
<< /Size 8
/Root 7 0 R
/Info 6 0 R
>>
startxref
1053
%%EOF

4
Tusker/AvatarCache.swift

@ -32,9 +32,7 @@ class AvatarCache {
} else {
requestCallbacks[url] = [completion]
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil,
let data = data,
let image = UIImage(data: data) else {
guard error == nil, let data = data, let image = UIImage(data: data) else {
let callbacks = self.requestCallbacks.removeValue(forKey: url)
callbacks?.forEach({ callback in
// todo: default avatar for failed requests

113
Tusker/Extensions/UIViewController+Delegates.swift

@ -0,0 +1,113 @@
//
// UIViewController+StatusTableViewCellDelegate.swift
// Tusker
//
// Created by Shadowfacts on 8/27/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import MastodonKit
import SafariServices
extension UIViewController: StatusTableViewCellDelegate {
func selected(account: Account) {
// don't open if the account is the same as the current one
if let profileController = self as? ProfileTableViewController,
profileController.account == account {
return
}
guard let navigationController = navigationController else {
fatalError("Can't show profile VC when not in navigation controller")
}
let vc = ProfileTableViewController.create(for: account)
navigationController.pushViewController(vc, animated: true)
}
func selected(mention: Mention) {
}
func selected(tag: Tag) {
}
func selected(url: URL) {
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
func selected(status: Status) {
// don't open if the conversation is the same as the current one
if let conversationController = self as? ConversationViewController,
conversationController.mainStatus == status {
return
}
guard let navigationController = navigationController else {
fatalError("Can't show conversation VC when not in navigation controller")
}
let vc = ConversationViewController.create(for: status)
navigationController.pushViewController(vc, animated: true)
}
func reply(to status: Status) {
let vc = ComposeViewController.create(inReplyTo: status)
present(vc, animated: true)
}
func showLargeAttachment(for attachmentView: AttachmentView) {
let vc = LargeImageViewController.create(image: attachmentView.image!, description: attachmentView.attachment.description)
vc.delegate = self
var frame = attachmentView.convert(attachmentView.bounds, to: view)
if let scrollView = view as? UIScrollView {
let scale = scrollView.zoomScale
let width = frame.width * scale
let height = frame.height * scale
let x = frame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
let y = frame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
frame = CGRect(x: x, y: y, width: width, height: height)
}
vc.originFrame = frame
vc.transitioningDelegate = self
present(vc, animated: true)
}
}
extension UIViewController: LargeImageViewControllerDelegate {
func closeLargeImage() {
dismiss(animated: true)
}
}
extension UIViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if presented is LargeImageViewController {
return LargeImageExpandAnimationController()
} else {
return nil
}
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let dismissed = dismissed as? LargeImageViewController {
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
} else {
return nil
}
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if let animator = animator as? LargeImageShrinkAnimationController,
let interactionController = animator.interactionController,
interactionController.inProgress {
return interactionController
} else {
return nil
}
}
}

61
Tusker/Extensions/UIViewController+StatusTableViewCellDelegate.swift

@ -1,61 +0,0 @@
//
// UIViewController+StatusTableViewCellDelegate.swift
// Tusker
//
// Created by Shadowfacts on 8/27/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import MastodonKit
import SafariServices
extension UIViewController: StatusTableViewCellDelegate {
func selected(account: Account) {
// don't open if the account is the same as the current one
if let profileController = self as? ProfileTableViewController,
profileController.account == account {
return
}
guard let navigationController = navigationController else {
fatalError("Can't show profile VC when not in navigation controller")
}
let vc = ProfileTableViewController.create(for: account)
navigationController.pushViewController(vc, animated: true)
}
func selected(mention: Mention) {
}
func selected(tag: Tag) {
}
func selected(url: URL) {
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
func selected(status: Status) {
// don't open if the conversation is the same as the current one
if let conversationController = self as? ConversationViewController,
conversationController.mainStatus == status {
return
}
guard let navigationController = navigationController else {
fatalError("Can't show conversation VC when not in navigation controller")
}
let vc = ConversationViewController.create(for: status)
navigationController.pushViewController(vc, animated: true)
}
func reply(to status: Status) {
let vc = ComposeViewController.create(inReplyTo: status)
present(vc, animated: true)
}
}

6
Tusker/Info.plist

@ -2,10 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people&apos;s posts.</string>
<key>NSCameraUsageDescription</key>
<string>To post photos from the camera.</string>
<string>Post photos from the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>To post photos from the photo library.</string>
<string>Post photos from the photo library.</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>

152
Tusker/Storyboards/LargeImage.storyboard

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.13.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="8pH-Ao-p7G">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Large Image View Controller-->
<scene sceneID="jFU-tD-Rk9">
<objects>
<viewController id="8pH-Ao-p7G" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="w1M-d4-X1A">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="2wV-8N-EFe">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<imageView contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Pd6-Mn-fcS">
<rect key="frame" x="0.0" y="-10" width="375" height="647"/>
<gestureRecognizers/>
</imageView>
</subviews>
<gestureRecognizers/>
<constraints>
<constraint firstAttribute="bottom" secondItem="Pd6-Mn-fcS" secondAttribute="bottom" id="4DC-Cz-yrd"/>
<constraint firstItem="Pd6-Mn-fcS" firstAttribute="leading" secondItem="2wV-8N-EFe" secondAttribute="leading" id="8NV-u8-aEP"/>
<constraint firstItem="Pd6-Mn-fcS" firstAttribute="top" secondItem="2wV-8N-EFe" secondAttribute="top" id="HLI-4J-tF7"/>
<constraint firstAttribute="trailing" secondItem="Pd6-Mn-fcS" secondAttribute="trailing" id="eYw-ke-jQP"/>
</constraints>
<connections>
<outletCollection property="gestureRecognizers" destination="vji-Fm-RJ6" appends="YES" id="62b-Mz-RSo"/>
</connections>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1EF-uu-YnK">
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Jjh-Jw-5E9">
<rect key="frame" x="16" y="16" width="20" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="cBe-e6-Eng"/>
<constraint firstAttribute="height" constant="20" id="uYQ-fp-4x9"/>
</constraints>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<state key="normal" image="Download"/>
<connections>
<action selector="downloadPressed:" destination="8pH-Ao-p7G" eventType="touchUpInside" id="4LG-gn-IWR"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Z1X-K2-dVx">
<rect key="frame" x="339" y="16" width="20" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="1Oj-Ht-ytC"/>
<constraint firstAttribute="height" constant="20" id="6VA-5F-Y8k"/>
</constraints>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<state key="normal" image="Close"/>
<connections>
<action selector="closeButtonPressed:" destination="8pH-Ao-p7G" eventType="touchUpInside" id="o1j-v4-hr8"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="Jjh-Jw-5E9" firstAttribute="top" secondItem="1EF-uu-YnK" secondAttribute="top" constant="16" id="9zK-pO-i0Z"/>
<constraint firstAttribute="height" secondItem="Z1X-K2-dVx" secondAttribute="height" constant="16" id="IYQ-ii-Ii4"/>
<constraint firstItem="Jjh-Jw-5E9" firstAttribute="leading" secondItem="1EF-uu-YnK" secondAttribute="leading" constant="16" id="P1u-fC-6Ln"/>
<constraint firstItem="Z1X-K2-dVx" firstAttribute="top" secondItem="1EF-uu-YnK" secondAttribute="top" constant="16" id="Tsg-Oc-1jH"/>
<constraint firstAttribute="trailing" secondItem="Z1X-K2-dVx" secondAttribute="trailing" constant="16" id="wEw-jP-Teu"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8yv-cK-4o1">
<rect key="frame" x="0.0" y="630.5" width="375" height="36.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gvy-hM-kiM">
<rect key="frame" x="16" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="gvy-hM-kiM" secondAttribute="trailing" constant="16" id="3YX-UN-rIa"/>
<constraint firstAttribute="height" secondItem="gvy-hM-kiM" secondAttribute="height" constant="16" id="WrI-2J-9Z5"/>
<constraint firstAttribute="bottom" secondItem="gvy-hM-kiM" secondAttribute="bottom" constant="16" id="bB4-b5-pmK"/>
<constraint firstItem="gvy-hM-kiM" firstAttribute="leading" secondItem="8yv-cK-4o1" secondAttribute="leading" constant="16" id="giM-Iz-Owe"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="2wV-8N-EFe" firstAttribute="centerX" secondItem="w1M-d4-X1A" secondAttribute="centerX" id="5tb-e1-ObD"/>
<constraint firstItem="MqP-FN-iGs" firstAttribute="trailing" secondItem="1EF-uu-YnK" secondAttribute="trailing" id="8Aw-Lq-1Pa"/>
<constraint firstItem="8yv-cK-4o1" firstAttribute="leading" secondItem="MqP-FN-iGs" secondAttribute="leading" id="9qz-OC-7av"/>
<constraint firstItem="2wV-8N-EFe" firstAttribute="centerY" secondItem="w1M-d4-X1A" secondAttribute="centerY" id="Z9C-TF-I3s"/>
<constraint firstItem="1EF-uu-YnK" firstAttribute="top" secondItem="w1M-d4-X1A" secondAttribute="top" id="Zgr-Ck-Ly5"/>
<constraint firstItem="2wV-8N-EFe" firstAttribute="height" secondItem="w1M-d4-X1A" secondAttribute="height" id="adX-Xo-gwg"/>
<constraint firstItem="MqP-FN-iGs" firstAttribute="bottom" secondItem="8yv-cK-4o1" secondAttribute="bottom" id="bn5-Qt-ggu"/>
<constraint firstItem="MqP-FN-iGs" firstAttribute="trailing" secondItem="8yv-cK-4o1" secondAttribute="trailing" id="eSp-qn-PwQ"/>
<constraint firstItem="2wV-8N-EFe" firstAttribute="width" secondItem="w1M-d4-X1A" secondAttribute="width" id="mZZ-WZ-o8E"/>
<constraint firstItem="1EF-uu-YnK" firstAttribute="leading" secondItem="MqP-FN-iGs" secondAttribute="leading" id="xLf-u4-n14"/>
</constraints>
<viewLayoutGuide key="safeArea" id="MqP-FN-iGs"/>
<connections>
<outletCollection property="gestureRecognizers" destination="EgN-eL-bDf" appends="YES" id="CeU-w7-YSb"/>
</connections>
</view>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="bottomControlsView" destination="8yv-cK-4o1" id="iA6-0y-hiN"/>
<outlet property="closeButton" destination="Z1X-K2-dVx" id="kR7-e7-Q8T"/>
<outlet property="closeButtonTopConstraint" destination="Tsg-Oc-1jH" id="07w-Ob-ker"/>
<outlet property="closeButtonTrailingConstraint" destination="wEw-jP-Teu" id="j0N-dF-qzf"/>
<outlet property="descriptionLabel" destination="gvy-hM-kiM" id="cbi-iB-Lob"/>
<outlet property="downloadButton" destination="Jjh-Jw-5E9" id="EAp-fo-yxs"/>
<outlet property="downloadButtonLeadingConstraint" destination="P1u-fC-6Ln" id="v2c-eL-7Pa"/>
<outlet property="downloadButtonTopConstraint" destination="9zK-pO-i0Z" id="eSj-yq-jk2"/>
<outlet property="imageView" destination="Pd6-Mn-fcS" id="Eh4-q1-L4S"/>
<outlet property="imageViewBottomConstraint" destination="4DC-Cz-yrd" id="X7o-5I-sQT"/>
<outlet property="imageViewLeadingConstraint" destination="8NV-u8-aEP" id="Hvu-dn-5O9"/>
<outlet property="imageViewTopConstraint" destination="HLI-4J-tF7" id="vpw-JN-YHC"/>
<outlet property="imageViewTrailingConstraint" destination="eYw-ke-jQP" id="7Ln-IV-VZW"/>
<outlet property="scrollView" destination="2wV-8N-EFe" id="dLq-Dq-32G"/>
<outlet property="topControlsHeightConstraint" destination="IYQ-ii-Ii4" id="Mht-sy-5fZ"/>
<outlet property="topControlsView" destination="1EF-uu-YnK" id="Syt-Ib-He6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="6Ga-cx-zlp" userLabel="First Responder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="vji-Fm-RJ6">
<connections>
<action selector="scrollViewPressed:" destination="8pH-Ao-p7G" id="kYC-qd-03P"/>
</connections>
</tapGestureRecognizer>
<tapGestureRecognizer numberOfTapsRequired="2" id="EgN-eL-bDf">
<connections>
<action selector="scrollViewDoubleTapped:" destination="8pH-Ao-p7G" id="JZe-G5-okr"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="-434.39999999999998" y="139.880059970015"/>
</scene>
</scenes>
<resources>
<image name="Close" width="209" height="209"/>
<image name="Download" width="284" height="284"/>
</resources>
</document>

206
Tusker/View Controllers/LargeImageViewController.swift

@ -0,0 +1,206 @@
//
// LargeImageViewController.swift
// Tusker
//
// Created by Shadowfacts on 8/31/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Photos
protocol LargeImageViewControllerDelegate {
func closeLargeImage()
}
class LargeImageViewController: UIViewController, UIScrollViewDelegate {
static func create(image: UIImage, description: String?) -> LargeImageViewController {
guard let vc = UIStoryboard(name: "LargeImage", bundle: nil).instantiateInitialViewController() as? LargeImageViewController else { fatalError() }
vc.image = image
vc.imageDescription = description
return vc
}
var delegate: LargeImageViewControllerDelegate?
var originFrame: CGRect?
var dismissInteractionController: LargeImageInteractionController?
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var topControlsView: UIView!
@IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var downloadButton: UIButton!
@IBOutlet weak var downloadButtonTopConstraint: NSLayoutConstraint!
@IBOutlet weak var downloadButtonLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButton: UIButton!
@IBOutlet weak var closeButtonTopConstraint: NSLayoutConstraint!
@IBOutlet weak var closeButtonTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel!
var image: UIImage?
var imageDescription: String?
var controlsVisible = true {
didSet {
UIView.animate(withDuration: 0.2) {
let topOffset = self.controlsVisible ? 0 : -self.topControlsView.bounds.height
self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
if self.imageDescription != nil {
let bottomOffset = self.controlsVisible ? 0 : self.bottomControlsView.bounds.height + self.view.safeAreaInsets.bottom
self.bottomControlsView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
}
}
}
}
var prevZoomScale: CGFloat?
override var prefersStatusBarHidden: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
scrollView.delegate = self
imageView.bounds = CGRect(origin: .zero, size: image!.size)
if let imageDescription = imageDescription {
descriptionLabel.text = imageDescription
} else {
bottomControlsView.isHidden = true
}
// if running on iPhone X
if UIScreen.main.nativeBounds.height == 2436 {
topControlsHeightConstraint.constant = 35
downloadButtonLeadingConstraint.constant = 35
closeButtonTrailingConstraint.constant = 35
}
dismissInteractionController = LargeImageInteractionController(viewController: self)
}
override func viewDidLayoutSubviews() {
let widthScale = view.bounds.size.width / imageView.bounds.width
let heightScale = view.bounds.size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerImage()
let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
if scrollView.zoomScale <= scrollView.minimumZoomScale {
controlsVisible = true
} else if scrollView.zoomScale > prevZoomScale {
controlsVisible = false
}
self.prevZoomScale = scrollView.zoomScale
}
func centerImage() {
let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
}
func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.width = imageView.frame.width / scale
zoomRect.size.height = imageView.frame.height / scale
let newCenter = scrollView.convert(center, to: imageView)
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
return zoomRect
}
func animateZoomOut() {
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.zoomScale = self.scrollView.minimumZoomScale
self.view.layoutIfNeeded()
})
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
@IBAction func scrollViewPressed(_ sender: UITapGestureRecognizer) {
if scrollView.zoomScale > scrollView.minimumZoomScale {
animateZoomOut()
} else {
controlsVisible = !controlsVisible
}
}
@IBAction func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) {
if scrollView.zoomScale <= scrollView.minimumZoomScale {
let point = recognizer.location(in: recognizer.view)
let scale: CGFloat
if scrollView.minimumZoomScale < 1 {
if 1 - scrollView.zoomScale <= 0.5 {
scale = scrollView.zoomScale + 1
} else {
scale = 1
}
} else {
scale = scrollView.maximumZoomScale
}
let rect = zoomRectFor(scale: scale, center: point)
UIView.animate(withDuration: 0.3) {
self.scrollView.zoom(to: rect, animated: false)
self.view.layoutIfNeeded()
}
} else {
animateZoomOut()
}
}
@IBAction func closeButtonPressed(_ sender: Any) {
delegate?.closeLargeImage()
}
@IBAction func downloadPressed(_ sender: Any) {
guard let image = image else { return }
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}, completionHandler: { success, error in
if success {
return
} else if let error = error {
print("Couldn't save photo: \(error)")
} else {
return
}
})
}
}

61
Tusker/View Controllers/Transitions/LargeImageExpandAnimationController.swift

@ -0,0 +1,61 @@
//
// LargeImageExpandAnimationController.swift
// Tusker
//
// Created by Shadowfacts on 9/1/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController,
let originFrame = toVC.originFrame else {
return
}
let containerView = transitionContext.containerView
let finalVCFrame = transitionContext.finalFrame(for: toVC)
let image = toVC.image!
let ratio = image.size.width / image.size.height
let width = finalVCFrame.width
let height = width / ratio
let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height)
let imageView = UIImageView(frame: originFrame)
imageView.image = toVC.image!
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 5
imageView.layer.masksToBounds = true
let blackView = UIView(frame: finalVCFrame)
blackView.backgroundColor = .black
blackView.alpha = 0
containerView.addSubview(toVC.view)
containerView.addSubview(blackView)
containerView.addSubview(imageView)
toVC.view.isHidden = true
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
imageView.frame = finalFrame
blackView.alpha = 1
}, completion: { _ in
toVC.view.isHidden = false
fromVC.view.isHidden = false
blackView.removeFromSuperview()
imageView.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}

60
Tusker/View Controllers/Transitions/LargeImageInteractionController.swift

@ -0,0 +1,60 @@
//
// LargeImageInteractionController.swift
// Tusker
//
// Created by Shadowfacts on 9/1/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
var inProgress = false
var direction: CGFloat?
var shouldCompleteTransition = false
private weak var viewController: UIViewController!
init(viewController: UIViewController) {
super.init()
self.viewController = viewController
viewController.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:))))
}
@objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: recognizer.view!.superview!)
var progress = translation.y / 200
if let direction = direction {
progress *= direction
} else {
direction = progress > 0 ? 1 : -1
progress = abs(progress)
}
progress = min(max(progress, 0), 1)
let velocity = abs(recognizer.velocity(in: recognizer.view!.superview!).y)
switch recognizer.state {
case .began:
inProgress = true
viewController.dismiss(animated: true)
case .changed:
shouldCompleteTransition = progress > 0.5 || velocity > 1000
update(progress)
case .cancelled:
inProgress = false
cancel()
case .ended:
inProgress = false
direction = nil
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
}

67
Tusker/View Controllers/Transitions/LargeImageShrinkAnimationController.swift

@ -0,0 +1,67 @@
//
// LargeImageShrinkAnimationController.swift
// Tusker
//
// Created by Shadowfacts on 9/1/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let interactionController: LargeImageInteractionController?
init(interactionController: LargeImageInteractionController?) {
self.interactionController = interactionController
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController,
let toVC = transitionContext.viewController(forKey: .to),
let finalFrame = fromVC.originFrame else {
return
}
let originalVCFrame = fromVC.view.frame
let containerView = transitionContext.containerView
let image = fromVC.image!
let ratio = image.size.width / image.size.height
let width = originalVCFrame.width
let height = width / ratio
let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height)
let imageView = UIImageView(frame: originalFrame)
imageView.image = fromVC.image!
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 5
imageView.layer.masksToBounds = true
let blackView = UIView(frame: originalVCFrame)
blackView.backgroundColor = .black
blackView.alpha = 1
containerView.addSubview(toVC.view)
containerView.addSubview(blackView)
containerView.addSubview(imageView)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
imageView.frame = finalFrame
blackView.alpha = 0
}, completion: { _ in
blackView.removeFromSuperview()
imageView.removeFromSuperview()
if transitionContext.transitionWasCancelled {
toVC.view.removeFromSuperview()
}
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}

64
Tusker/Views/AttachmentView.swift

@ -0,0 +1,64 @@
//
// AttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 8/31/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import MastodonKit
protocol AttachmentViewDelegate {
func showLargeAttachment(for attachmentView: AttachmentView)
}
class AttachmentView: UIImageView {
var delegate: AttachmentViewDelegate?
var attachment: Attachment!
var task: URLSessionDataTask?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
convenience init(frame: CGRect, attachment: Attachment) {
self.init(frame: frame)
self.attachment = attachment
loadImage()
}
func commonInit() {
contentMode = .scaleAspectFill
layer.masksToBounds = true
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
}
func loadImage() {
guard let url = URL(string: attachment.url) else { fatalError("Invalid URL: \(attachment.url)") }
task = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil, let data = data, let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.image = image
}
}
task!.resume()
}
@objc func imagePressed() {
if image != nil {
delegate?.showLargeAttachment(for: self)
}
}
}

51
Tusker/Views/StatusTableViewCell.swift

@ -23,6 +23,8 @@ protocol StatusTableViewCellDelegate {
func reply(to status: Status)
func showLargeAttachment(for attachmentView: AttachmentView)
}
class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
@ -35,6 +37,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var reblogLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var attachmentsView: UIView!
var status: Status!
var account: Account!
@ -44,6 +47,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
var updateTimestampWorkItem: DispatchWorkItem?
var attachmentDataTasks: [URLSessionDataTask] = []
override func awakeFromNib() {
displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
displayNameLabel.isUserInteractionEnabled = true
@ -54,6 +59,8 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.isUserInteractionEnabled = true
avatarImageView.layer.masksToBounds = true
attachmentsView.layer.cornerRadius = 5
attachmentsView.layer.masksToBounds = true
}
func updateUIForPreferences() {
@ -95,6 +102,35 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
}
updateTimestamp()
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
let attachments = status.mediaAttachments.filter({ $0.type == .image })
if attachments.count > 0 {
attachmentsView.isHidden = false
let width = attachmentsView.bounds.width
let height: CGFloat = 200
switch attachments.count {
case 1:
addAttachmentView(frame: CGRect(x: 0, y: 0, width: width, height: height), attachment: attachments[0])
case 2:
addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0])
addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height), attachment: attachments[1])
case 3:
addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height), attachment: attachments[0])
addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1])
addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2])
case 4:
addAttachmentView(frame: CGRect(x: 0, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[0])
addAttachmentView(frame: CGRect(x: 0, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[1])
addAttachmentView(frame: CGRect(x: width / 2 + 4, y: 0, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[2])
addAttachmentView(frame: CGRect(x: width / 2 + 4, y: height / 2 + 4, width: width / 2 - 4, height: height / 2 - 4), attachment: attachments[3])
default:
fatalError("Too many attachments")
}
} else {
attachmentsView.isHidden = true
}
contentLabel.status = status
contentLabel.delegate = self
}
@ -120,12 +156,21 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
}
}
func addAttachmentView(frame: CGRect, attachment: Attachment) {
let attachmentView = AttachmentView(frame: frame, attachment: attachment)
attachmentView.delegate = self
attachmentsView.addSubview(attachmentView)
}
override func prepareForReuse() {
if let url = avatarURL {
AvatarCache.shared.cancel(url)
}
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { view in
(view as? AttachmentView)?.task?.cancel()
}
}
@IBAction func replyPressed(_ sender: Any) {
@ -162,3 +207,9 @@ extension StatusTableViewCell: HTMLContentLabelDelegate {
}
}
extension StatusTableViewCell: AttachmentViewDelegate {
func showLargeAttachment(for attachmentView: AttachmentView) {
delegate?.showLargeAttachment(for: attachmentView)
}
}

19
Tusker/Views/StatusTableViewCell.xib

@ -15,11 +15,11 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="yNh-ac-v6c">
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="yNh-ac-v6c">
<rect key="frame" x="16" y="8" width="343" height="142"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Reblogged by Person" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lDH-50-AJZ">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<rect key="frame" x="0.0" y="0.0" width="163.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@ -78,11 +78,18 @@
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA">
<rect key="frame" x="0.0" y="108" width="343" height="0.0"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
<rect key="frame" x="0.0" y="112" width="343" height="30"/>
<rect key="frame" x="0.0" y="112" width="39" height="30"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
<rect key="frame" x="0.0" y="0.0" width="343" height="30"/>
<rect key="frame" x="0.0" y="0.0" width="39" height="30"/>
<state key="normal" title="Reply"/>
<connections>
<action selector="replyPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Ohg-uU-d3Z"/>
@ -91,6 +98,9 @@
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="nbq-yr-2mA" firstAttribute="width" secondItem="yNh-ac-v6c" secondAttribute="width" id="3Ag-HE-h4m"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -103,6 +113,7 @@
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="GkU-Xk-pc0"/>
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="CAl-hK-i3j"/>
<outlet property="contentLabel" destination="HrJ-t9-KcD" id="tbD-3T-nNP"/>
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="63y-He-xy1"/>

Loading…
Cancel
Save