Initial rewrite/redesign of compose screen

Still pending:
- Posting videos
- Image resizing
- Attachment drawer thing
This commit is contained in:
Shadowfacts 2019-01-14 21:59:42 -05:00
parent 5b3a3d9729
commit b8430be00c
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
40 changed files with 3732 additions and 469 deletions

Binary file not shown.

View File

@ -0,0 +1,32 @@
//
// GMAlbumsViewCell.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 22/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMAlbumsViewCell : UITableViewCell
@property (strong) PHFetchResult *assetsFetchResults;
@property (strong) PHAssetCollection *assetCollection;
//The labels
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *infoLabel;
//The imageView
@property (nonatomic, strong) UIImageView *imageView1;
@property (nonatomic, strong) UIImageView *imageView2;
@property (nonatomic, strong) UIImageView *imageView3;
//Video additional information
@property (nonatomic, strong) UIImageView *videoIcon;
@property (nonatomic, strong) UIImageView *slowMoIcon;
@property (nonatomic, strong) UIView *gradientView;
@property (nonatomic, strong) CAGradientLayer *gradient;
//Selection overlay
- (void)setVideoLayout:(BOOL)isVideo;
@end

View File

@ -0,0 +1,131 @@
//
// GMAlbumsViewCell.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 22/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMAlbumsViewCell.h"
#import "GMAlbumsViewController.h"
#import "GMImagePickerController.h"
#import <QuartzCore/QuartzCore.h>
@implementation GMAlbumsViewCell
- (void)awakeFromNib
{
[super awakeFromNib];
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])
{
// self.isAccessibilityElement = YES;
self.contentView.backgroundColor = [UIColor clearColor];
self.backgroundColor = [UIColor clearColor];
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// Border width of 1 pixel:
float borderWidth = 1.0/[UIScreen mainScreen].scale;
// ImageView
_imageView3 = [UIImageView new];
_imageView3.contentMode = UIViewContentModeScaleAspectFill;
_imageView3.frame = CGRectMake(kAlbumLeftToImageSpace+4, 8, kAlbumThumbnailSize3.width, kAlbumThumbnailSize3.height );
[_imageView3.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView3.layer setBorderWidth: borderWidth];
_imageView3.clipsToBounds = YES;
_imageView3.translatesAutoresizingMaskIntoConstraints = YES;
_imageView3.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView3];
// ImageView
_imageView2 = [UIImageView new];
_imageView2.contentMode = UIViewContentModeScaleAspectFill;
_imageView2.frame = CGRectMake(kAlbumLeftToImageSpace+2, 8+2, kAlbumThumbnailSize2.width, kAlbumThumbnailSize2.height );
[_imageView2.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView2.layer setBorderWidth: borderWidth];
_imageView2.clipsToBounds = YES;
_imageView2.translatesAutoresizingMaskIntoConstraints = YES;
_imageView2.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView2];
// ImageView
_imageView1 = [UIImageView new];
_imageView1.contentMode = UIViewContentModeScaleAspectFill;
_imageView1.frame = CGRectMake(kAlbumLeftToImageSpace, 8+4, kAlbumThumbnailSize1.width, kAlbumThumbnailSize1.height );
[_imageView1.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView1.layer setBorderWidth: borderWidth];
_imageView1.clipsToBounds = YES;
_imageView1.translatesAutoresizingMaskIntoConstraints = YES;
_imageView1.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView1];
// The video gradient, label & icon
UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0];
UIColor *midGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.33];
UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.75];
_gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, kAlbumThumbnailSize1.height-kAlbumGradientHeight, kAlbumThumbnailSize1.width, kAlbumGradientHeight)];
_gradient = [CAGradientLayer layer];
_gradient.frame = _gradientView.bounds;
_gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[midGradient CGColor], (id)[botGradient CGColor], nil];
_gradient.locations = @[ @0.0f, @0.5f, @1.0f ];
[_gradientView.layer insertSublayer:_gradient atIndex:0];
_gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
_gradientView.translatesAutoresizingMaskIntoConstraints = YES;
[self.imageView1 addSubview:_gradientView];
_gradientView.hidden = YES;
// VideoIcon
_videoIcon = [UIImageView new];
_videoIcon.contentMode = UIViewContentModeScaleAspectFill;
_videoIcon.frame = CGRectMake(3,kAlbumThumbnailSize1.height - 4 - 8, 15, 8 );
_videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMAlbumsViewCell.class] compatibleWithTraitCollection:nil];
_videoIcon.clipsToBounds = YES;
_videoIcon.translatesAutoresizingMaskIntoConstraints = YES;
_videoIcon.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.imageView1 addSubview:_videoIcon];
_videoIcon.hidden = NO;
// TextLabel
self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:17.0];
self.textLabel.numberOfLines = 1;
self.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:14.0];
self.detailTextLabel.numberOfLines = 1;
}
return self;
}
-(void)layoutSubviews {
[super layoutSubviews];
self.textLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.textLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8, self.textLabel.frame.size.height);
self.detailTextLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.detailTextLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8 - kAlbumImageToTextSpace, self.detailTextLabel.frame.size.height);
}
- (void)setVideoLayout:(BOOL)isVideo
{
// TODO : Add additional icons for slowmo, burst, etc...
if (isVideo) {
_videoIcon.hidden = NO;
_gradientView.hidden = NO;
} else {
_videoIcon.hidden = YES;
_gradientView.hidden = YES;
}
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
@end

View File

@ -0,0 +1,32 @@
//
// GMAlbumsViewController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <UIKit/UIKit.h>
// Measuring IOS8 Photos APP at @2x (iPhone5s):
// The rows are 180px/90pts
// Left image border is 21px/10.5pts
// Separation between image and text is 42px/21pts (double the previouse one)
// The bigger image measures 139px/69.5pts including 1px/0.5pts white border.
// The second image measures 131px/65.6pts including 1px/0.5pts white border. Only 3px/1.5pts visible
// The third image measures 123px/61.5pts including 1px/0.5pts white border. Only 3px/1.5pts visible
static int kAlbumRowHeight = 90;
static int kAlbumLeftToImageSpace = 10;
static int kAlbumImageToTextSpace = 21;
static float const kAlbumGradientHeight = 20.0f;
static CGSize const kAlbumThumbnailSize1 = {70.0f , 70.0f};
static CGSize const kAlbumThumbnailSize2 = {66.0f , 66.0f};
static CGSize const kAlbumThumbnailSize3 = {62.0f , 62.0f};
@interface GMAlbumsViewController : UITableViewController
- (void)selectAllAlbumsCell;
@end

View File

@ -0,0 +1,416 @@
//
// GMAlbumsViewController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import "GMGridViewCell.h"
#import "GMGridViewController.h"
#import "GMAlbumsViewCell.h"
#include <Photos/Photos.h>
@interface GMAlbumsViewController() <PHPhotoLibraryChangeObserver>
@property (strong,nonatomic) NSArray *collectionsFetchResults;
@property (strong,nonatomic) NSArray *collectionsLocalizedTitles;
@property (strong,nonatomic) NSArray *collectionsFetchResultsAssets;
@property (strong,nonatomic) NSArray *collectionsFetchResultsTitles;
@property (nonatomic, weak) GMImagePickerController *picker;
@property (strong,nonatomic) PHCachingImageManager *imageManager;
@end
@implementation GMAlbumsViewController
- (id)init
{
if (self = [super initWithStyle:UITableViewStylePlain]) {
self.preferredContentSize = kPopoverContentSize;
}
return self;
}
static NSString *const AllPhotosReuseIdentifier = @"AllPhotosCell";
static NSString *const CollectionCellReuseIdentifier = @"CollectionCell";
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [self.picker pickerBackgroundColor];
// Navigation bar customization
if (self.picker.customNavigationBarPrompt) {
self.navigationItem.prompt = self.picker.customNavigationBarPrompt;
}
self.imageManager = [[PHCachingImageManager alloc] init];
// Table view aspect
self.tableView.rowHeight = kAlbumRowHeight;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
// Buttons
NSDictionary *barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]};
NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel");
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle
style:UIBarButtonItemStylePlain
target:self.picker
action:@selector(dismiss:)];
if (self.picker.useCustomFontForNavigationBar) {
[self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
if (self.picker.allowsMultipleSelection) {
NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(finishPickingAssets:)];
if (self.picker.useCustomFontForNavigationBar) {
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE);
}
// Bottom toolbar
self.toolbarItems = self.picker.toolbarItems;
// Title
if (!self.picker.title) {
self.title = NSLocalizedStringFromTableInBundle(@"picker.navigation.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Navigation bar default title");
} else {
self.title = self.picker.title;
}
// Fetch PHAssetCollections:
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
self.collectionsFetchResults = @[topLevelUserCollections, smartAlbums];
self.collectionsLocalizedTitles = @[NSLocalizedStringFromTableInBundle(@"picker.table.smart-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Smart Albums"),NSLocalizedStringFromTableInBundle(@"picker.table.user-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Albums")];
[self updateFetchResults];
// Register for changes
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)])
{
self.edgesForExtendedLayout = UIRectEdgeNone;
}
}
- (void)dealloc
{
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return self.picker.pickerStatusBarStyle;
}
- (void)selectAllAlbumsCell {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
}
-(void)updateFetchResults
{
//What I do here is fetch both the albums list and the assets of each album.
//This way I have acces to the number of items in each album, I can load the 3
//thumbnails directly and I can pass the fetched result to the gridViewController.
self.collectionsFetchResultsAssets=nil;
self.collectionsFetchResultsTitles=nil;
//Fetch PHAssetCollections:
PHFetchResult *topLevelUserCollections = [self.collectionsFetchResults objectAtIndex:0];
PHFetchResult *smartAlbums = [self.collectionsFetchResults objectAtIndex:1];
//All album: Sorted by descending creation date.
NSMutableArray *allFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *allFetchResultLabel = [[NSMutableArray alloc] init];
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsWithOptions:options];
[allFetchResultArray addObject:assetsFetchResult];
[allFetchResultLabel addObject:NSLocalizedStringFromTableInBundle(@"picker.table.all-photos-label", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"All photos")];
}
//User albums:
NSMutableArray *userFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *userFetchResultLabel = [[NSMutableArray alloc] init];
for(PHCollection *collection in topLevelUserCollections)
{
if ([collection isKindOfClass:[PHAssetCollection class]])
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
//Albums collections are allways PHAssetCollectionType=1 & PHAssetCollectionSubtype=2
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
[userFetchResultArray addObject:assetsFetchResult];
[userFetchResultLabel addObject:collection.localizedTitle];
}
}
//Smart albums: Sorted by descending creation date.
NSMutableArray *smartFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *smartFetchResultLabel = [[NSMutableArray alloc] init];
for(PHCollection *collection in smartAlbums)
{
if ([collection isKindOfClass:[PHAssetCollection class]])
{
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
//Smart collections are PHAssetCollectionType=2;
if(self.picker.customSmartCollections && [self.picker.customSmartCollections containsObject:@(assetCollection.assetCollectionSubtype)])
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
if(assetsFetchResult.count>0)
{
[smartFetchResultArray addObject:assetsFetchResult];
[smartFetchResultLabel addObject:collection.localizedTitle];
}
}
}
}
self.collectionsFetchResultsAssets= @[allFetchResultArray,smartFetchResultArray,userFetchResultArray];
self.collectionsFetchResultsTitles= @[allFetchResultLabel,smartFetchResultLabel,userFetchResultLabel];
}
#pragma mark - Accessors
- (GMImagePickerController *)picker
{
return (GMImagePickerController *)self.navigationController.parentViewController;
}
#pragma mark - Rotation
- (BOOL)shouldAutorotate
{
return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return (NSInteger)self.collectionsFetchResultsAssets.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section];
return (NSInteger)fetchResult.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
GMAlbumsViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[GMAlbumsViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Increment the cell's tag
NSInteger currentTag = cell.tag + 1;
cell.tag = currentTag;
// Set the label
cell.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize];
cell.textLabel.text = (self.collectionsFetchResultsTitles[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row];
cell.textLabel.textColor = self.picker.pickerTextColor;
// Retrieve the pre-fetched assets for this album:
PHFetchResult *assetsFetchResult = (self.collectionsFetchResultsAssets[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row];
// Display the number of assets
if (self.picker.displayAlbumsNumberOfAssets) {
cell.detailTextLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize];
cell.detailTextLabel.text = [self tableCellSubtitle:assetsFetchResult];
cell.detailTextLabel.textColor = self.picker.pickerTextColor;
}
// Set the 3 images (if exists):
if ([assetsFetchResult count] > 0) {
CGFloat scale = [UIScreen mainScreen].scale;
//Compute the thumbnail pixel size:
CGSize tableCellThumbnailSize1 = CGSizeMake(kAlbumThumbnailSize1.width*scale, kAlbumThumbnailSize1.height*scale);
PHAsset *asset = assetsFetchResult[0];
[cell setVideoLayout:(asset.mediaType==PHAssetMediaTypeVideo)];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize1
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView1.image = result;
}
}];
// Second & third images:
// TODO: Only preload the 3pixels height visible frame!
if ([assetsFetchResult count] > 1) {
//Compute the thumbnail pixel size:
CGSize tableCellThumbnailSize2 = CGSizeMake(kAlbumThumbnailSize2.width*scale, kAlbumThumbnailSize2.height*scale);
PHAsset *asset = assetsFetchResult[1];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize2
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView2.image = result;
}
}];
} else {
cell.imageView2.image = nil;
}
if ([assetsFetchResult count] > 2) {
CGSize tableCellThumbnailSize3 = CGSizeMake(kAlbumThumbnailSize3.width*scale, kAlbumThumbnailSize3.height*scale);
PHAsset *asset = assetsFetchResult[2];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize3
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView3.image = result;
}
}];
} else {
cell.imageView3.image = nil;
}
} else {
[cell setVideoLayout:NO];
cell.imageView3.image = [UIImage imageNamed:@"GMEmptyFolder"];
cell.imageView2.image = [UIImage imageNamed:@"GMEmptyFolder"];
cell.imageView1.image = [UIImage imageNamed:@"GMEmptyFolder"];
}
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Init the GMGridViewController
GMGridViewController *gridViewController = [[GMGridViewController alloc] initWithPicker:[self picker]];
// Set the title
gridViewController.title = cell.textLabel.text;
// Use the prefetched assets!
gridViewController.assetsFetchResults = [[_collectionsFetchResultsAssets objectAtIndex:(NSUInteger)indexPath.section] objectAtIndex:(NSUInteger)indexPath.row];
// Remove selection so it looks better on slide in
[tableView deselectRowAtIndexPath:indexPath animated:true];
// Push GMGridViewController
[self.navigationController pushViewController:gridViewController animated:YES];
}
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
UITableViewHeaderFooterView *header = (UITableViewHeaderFooterView *)view;
// header.contentView.backgroundColor = [UIColor clearColor];
// header.backgroundView.backgroundColor = [UIColor clearColor];
// Default is a bold font, but keep this styled as a normal font
header.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize];
header.textLabel.textColor = self.picker.pickerTextColor;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
//Tip: Returning nil hides the section header!
NSString *title = nil;
if (section > 0) {
// Only show title for non-empty sections:
PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section];
if (fetchResult.count > 0) {
title = self.collectionsLocalizedTitles[(NSUInteger)(section - 1)];
}
}
return title;
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray *updatedCollectionsFetchResults = nil;
for (PHFetchResult *collectionsFetchResult in self.collectionsFetchResults) {
PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:collectionsFetchResult];
if (changeDetails) {
if (!updatedCollectionsFetchResults) {
updatedCollectionsFetchResults = [self.collectionsFetchResults mutableCopy];
}
[updatedCollectionsFetchResults replaceObjectAtIndex:[self.collectionsFetchResults indexOfObject:collectionsFetchResult] withObject:[changeDetails fetchResultAfterChanges]];
}
}
// This only affects to changes in albums level (add/remove/edit album)
if (updatedCollectionsFetchResults) {
self.collectionsFetchResults = updatedCollectionsFetchResults;
}
// However, we want to update if photos are added, so the counts of items & thumbnails are updated too.
// Maybe some checks could be done here , but for now is OKey.
[self updateFetchResults];
[self.tableView reloadData];
});
}
#pragma mark - Cell Subtitle
- (NSString *)tableCellSubtitle:(PHFetchResult*)assetsFetchResult
{
// Just return the number of assets. Album app does this:
return [NSString stringWithFormat:@"%ld", (long)[assetsFetchResult count]];
}
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,32 @@
//
// GMGridViewCell.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMGridViewCell : UICollectionViewCell
@property (nonatomic, strong) PHAsset *asset;
//The imageView
@property (nonatomic, strong) UIImageView *imageView;
//Video additional information
@property (nonatomic, strong) UIImageView *videoIcon;
@property (nonatomic, strong) UILabel *videoDuration;
@property (nonatomic, strong) UIView *gradientView;
@property (nonatomic, strong) CAGradientLayer *gradient;
//Selection overlay
@property (nonatomic) BOOL shouldShowSelection;
@property (nonatomic, strong) UIView *coverView;
@property (nonatomic, strong) UIButton *selectedButton;
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
- (void)bind:(PHAsset *)asset;
@end

View File

@ -0,0 +1,176 @@
//
// GMGridViewCell.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMGridViewCell.h"
@interface GMGridViewCell ()
@end
@implementation GMGridViewCell
static UIFont *titleFont;
static CGFloat titleHeight;
static UIImage *videoIcon;
static UIColor *titleColor;
static UIImage *checkedIcon;
static UIColor *selectedColor;
static UIColor *disabledColor;
+ (void)initialize
{
titleFont = [UIFont systemFontOfSize:12];
titleHeight = 20.0f;
videoIcon = [UIImage imageNamed:@"GMImagePickerVideo"];
titleColor = [UIColor whiteColor];
checkedIcon = [UIImage imageNamed:@"CTAssetsPickerChecked"];
selectedColor = [UIColor colorWithWhite:1 alpha:0.3];
disabledColor = [UIColor colorWithWhite:1 alpha:0.9];
}
- (void)awakeFromNib
{
[super awakeFromNib];
self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.contentView.translatesAutoresizingMaskIntoConstraints = YES;
}
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.opaque = NO;
self.enabled = YES;
CGFloat cellSize = self.contentView.bounds.size.width;
// The image view
_imageView = [UIImageView new];
_imageView.frame = CGRectMake(0, 0, cellSize, cellSize);
_imageView.contentMode = UIViewContentModeScaleAspectFill;
/*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
_imageView.contentMode = UIViewContentModeScaleAspectFit;
}
else
{
_imageView.contentMode = UIViewContentModeScaleAspectFill;
}*/
_imageView.clipsToBounds = YES;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addSubview:_imageView];
// The video gradient, label & icon
float x_offset = 4.0f;
UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0];
UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.8];
_gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, self.bounds.size.height-titleHeight, self.bounds.size.width, titleHeight)];
_gradient = [CAGradientLayer layer];
_gradient.frame = _gradientView.bounds;
_gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[botGradient CGColor], nil];
[_gradientView.layer insertSublayer:_gradient atIndex:0];
_gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
_gradientView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_gradientView];
_gradientView.hidden = YES;
_videoIcon = [UIImageView new];
_videoIcon.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight);
_videoIcon.contentMode = UIViewContentModeLeft;
_videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil];
_videoIcon.translatesAutoresizingMaskIntoConstraints = NO;
_videoIcon.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self addSubview:_videoIcon];
_videoIcon.hidden = YES;
_videoDuration = [UILabel new];
_videoDuration.font = titleFont;
_videoDuration.textColor = titleColor;
_videoDuration.textAlignment = NSTextAlignmentRight;
_videoDuration.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight);
_videoDuration.contentMode = UIViewContentModeRight;
_videoDuration.translatesAutoresizingMaskIntoConstraints = NO;
_videoDuration.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self addSubview:_videoDuration];
_videoDuration.hidden = YES;
// Selection overlay & icon
_coverView = [[UIView alloc] initWithFrame:self.bounds];
_coverView.translatesAutoresizingMaskIntoConstraints = NO;
_coverView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_coverView.backgroundColor = [UIColor colorWithRed:0.24 green:0.47 blue:0.85 alpha:0.6];
[self addSubview:_coverView];
_coverView.hidden = YES;
_selectedButton = [UIButton buttonWithType:UIButtonTypeCustom];
_selectedButton.frame = CGRectMake(2*self.bounds.size.width/3, 0*self.bounds.size.width/3, self.bounds.size.width/3, self.bounds.size.width/3);
_selectedButton.contentMode = UIViewContentModeTopRight;
_selectedButton.adjustsImageWhenHighlighted = NO;
[_selectedButton setImage:nil forState:UIControlStateNormal];
_selectedButton.translatesAutoresizingMaskIntoConstraints = NO;
_selectedButton.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[_selectedButton setImage:[UIImage imageNamed:@"GMSelected" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil] forState:UIControlStateSelected];
_selectedButton.hidden = NO;
_selectedButton.userInteractionEnabled = NO;
[self addSubview:_selectedButton];
}
// Note: the views above are created in case this is toggled per cell, on the fly, etc.!
self.shouldShowSelection = YES;
return self;
}
// Required to resize the CAGradientLayer because it does not support auto resizing.
- (void)layoutSubviews {
[super layoutSubviews];
_gradient.frame = _gradientView.bounds;
}
- (void)bind:(PHAsset *)asset
{
self.asset = asset;
if (self.asset.mediaType == PHAssetMediaTypeVideo) {
_videoIcon.hidden = NO;
_videoDuration.hidden = NO;
_gradientView.hidden = NO;
_videoDuration.text = [self getDurationWithFormat:self.asset.duration];
} else {
_videoIcon.hidden = YES;
_videoDuration.hidden = YES;
_gradientView.hidden = YES;
}
}
// Override setSelected
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
if (!self.shouldShowSelection) {
return;
}
_coverView.hidden = !selected;
_selectedButton.selected = selected;
}
-(NSString*)getDurationWithFormat:(NSTimeInterval)duration
{
NSInteger ti = (NSInteger)duration;
NSInteger seconds = ti % 60;
NSInteger minutes = (ti / 60) % 60;
//NSInteger hours = (ti / 3600);
return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}
@end

View File

@ -0,0 +1,21 @@
//
// GMGridViewController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMImagePickerController.h"
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMGridViewController : UICollectionViewController
@property (strong,nonatomic) PHFetchResult *assetsFetchResults;
-(id)initWithPicker:(GMImagePickerController *)picker;
@end

View File

@ -0,0 +1,611 @@
//
// GMGridViewController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMGridViewController.h"
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import "GMGridViewCell.h"
#include <Photos/Photos.h>
//Helper methods
@implementation NSIndexSet (Convenience)
- (NSArray *)aapl_indexPathsFromIndexesWithSection:(NSUInteger)section {
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:self.count];
[self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[indexPaths addObject:[NSIndexPath indexPathForItem:(NSInteger)idx inSection:(NSInteger)section]];
}];
return indexPaths;
}
@end
@implementation UICollectionView (Convenience)
- (NSArray *)aapl_indexPathsForElementsInRect:(CGRect)rect {
NSArray *allLayoutAttributes = [self.collectionViewLayout layoutAttributesForElementsInRect:rect];
if (allLayoutAttributes.count == 0) { return nil; }
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:allLayoutAttributes.count];
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) {
NSIndexPath *indexPath = layoutAttributes.indexPath;
[indexPaths addObject:indexPath];
}
return indexPaths;
}
@end
@interface GMImagePickerController ()
- (void)finishPickingAssets:(id)sender;
- (void)dismiss:(id)sender;
- (NSString *)toolbarTitle;
- (UIView *)noAssetsView;
@end
@interface GMGridViewController () <PHPhotoLibraryChangeObserver>
@property (nonatomic, weak) GMImagePickerController *picker;
@property (strong,nonatomic) PHCachingImageManager *imageManager;
@property (assign, nonatomic) CGRect previousPreheatRect;
@end
static CGSize AssetGridThumbnailSize;
NSString * const GMGridViewCellIdentifier = @"GMGridViewCellIdentifier";
@implementation GMGridViewController
{
CGFloat screenWidth;
CGFloat screenHeight;
UICollectionViewFlowLayout *portraitLayout;
UICollectionViewFlowLayout *landscapeLayout;
}
-(id)initWithPicker:(GMImagePickerController *)picker
{
//Custom init. The picker contains custom information to create the FlowLayout
self.picker = picker;
//Ipad popover is not affected by rotation!
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
screenWidth = CGRectGetWidth(picker.view.bounds);
screenHeight = CGRectGetHeight(picker.view.bounds);
}
else
{
if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation))
{
screenHeight = CGRectGetWidth(picker.view.bounds);
screenWidth = CGRectGetHeight(picker.view.bounds);
}
else
{
screenWidth = CGRectGetWidth(picker.view.bounds);
screenHeight = CGRectGetHeight(picker.view.bounds);
}
}
UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation];
if (self = [super initWithCollectionViewLayout:layout])
{
//Compute the thumbnail pixel size:
CGFloat scale = [UIScreen mainScreen].scale;
//NSLog(@"This is @%fx scale device", scale);
if(scale >= 3)
{
scale = 2;
}
AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale);
self.collectionView.allowsMultipleSelection = picker.allowsMultipleSelection;
[self.collectionView registerClass:GMGridViewCell.class
forCellWithReuseIdentifier:GMGridViewCellIdentifier];
self.preferredContentSize = kPopoverContentSize;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupViews];
// Navigation bar customization
if (self.picker.customNavigationBarPrompt) {
self.navigationItem.prompt = self.picker.customNavigationBarPrompt;
}
self.imageManager = [[PHCachingImageManager alloc] init];
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)])
{
self.edgesForExtendedLayout = UIRectEdgeNone;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setupButtons];
[self setupToolbar];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self updateCachedAssets];
}
- (void)dealloc
{
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return self.picker.pickerStatusBarStyle;
}
#pragma mark - Rotation
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return;
}
UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:toInterfaceOrientation];
//Update the AssetGridThumbnailSize:
CGFloat scale = [UIScreen mainScreen].scale;
AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale);
[self resetCachedAssets];
//This is optional. Reload visible thumbnails:
for (GMGridViewCell *cell in [self.collectionView visibleCells]) {
NSInteger currentTag = cell.tag;
[self.imageManager requestImageForAsset:cell.asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info)
{
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
[self.collectionView setCollectionViewLayout:layout animated:YES];
}
#pragma mark - Setup
- (void)setupViews
{
self.collectionView.backgroundColor = [UIColor clearColor];
self.view.backgroundColor = [self.picker pickerBackgroundColor];
}
- (void)setupButtons
{
if (self.picker.allowsMultipleSelection) {
NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(finishPickingAssets:)];
self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE);
} else {
NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(dismiss:)];
}
if (self.picker.useCustomFontForNavigationBar) {
if (self.picker.useCustomFontForNavigationBar) {
NSDictionary* barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]};
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
}
}
- (void)setupToolbar
{
self.toolbarItems = self.picker.toolbarItems;
}
#pragma mark - Collection View Layout
- (UICollectionViewFlowLayout *)collectionViewFlowLayoutForOrientation:(UIInterfaceOrientation)orientation
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
if(!portraitLayout)
{
portraitLayout = [[UICollectionViewFlowLayout alloc] init];
portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1)*self.picker.minimumInteritemSpacing);
portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait);
double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait;
double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1);
portraitLayout.minimumLineSpacing = spaceWidth;
}
return portraitLayout;
}
else
{
if(UIInterfaceOrientationIsLandscape(orientation))
{
if(!landscapeLayout)
{
landscapeLayout = [[UICollectionViewFlowLayout alloc] init];
landscapeLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenHeight - (self.picker.colsInLandscape-1)*self.picker.minimumInteritemSpacing);
landscapeLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInLandscape, cellTotalUsableWidth/self.picker.colsInLandscape);
double cellTotalUsedWidth = (double)landscapeLayout.itemSize.width*self.picker.colsInLandscape;
double spaceTotalWidth = (double)screenHeight-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInLandscape-1);
landscapeLayout.minimumLineSpacing = spaceWidth;
}
return landscapeLayout;
}
else
{
if(!portraitLayout)
{
portraitLayout = [[UICollectionViewFlowLayout alloc] init];
portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1) * self.picker.minimumInteritemSpacing);
portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait);
double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait;
double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1);
portraitLayout.minimumLineSpacing = spaceWidth;
}
return portraitLayout;
}
}
}
#pragma mark - Collection View Data Source
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GMGridViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:GMGridViewCellIdentifier
forIndexPath:indexPath];
// Increment the cell's tag
NSInteger currentTag = cell.tag + 1;
cell.tag = currentTag;
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
/*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
NSLog(@"Image manager: Requesting FIT image for iPad");
[self.imageManager requestImageForAsset:asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFit
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
else*/
{
//NSLog(@"Image manager: Requesting FILL image for iPhone");
[self.imageManager requestImageForAsset:asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
[cell bind:asset];
cell.shouldShowSelection = self.picker.allowsMultipleSelection;
// Optional protocol to determine if some kind of assets can't be selected (pej long videos, etc...)
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)]) {
cell.enabled = [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
} else {
cell.enabled = YES;
}
// Setting `selected` property blocks further deselection. Have to call selectItemAtIndexPath too. ( ref: http://stackoverflow.com/a/17812116/1648333 )
if ([self.picker.selectedAssets containsObject:asset]) {
cell.selected = YES;
[collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
} else {
cell.selected = NO;
}
return cell;
}
#pragma mark - Collection View Delegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
GMGridViewCell *cell = (GMGridViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (!cell.isEnabled) {
return NO;
} else if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[self.picker selectAsset:asset];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[self.picker deselectAsset:asset];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
}
}
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
}
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
NSInteger count = (NSInteger)self.assetsFetchResults.count;
return count;
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
dispatch_async(dispatch_get_main_queue(), ^{
// check if there are changes to the assets (insertions, deletions, updates)
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
if (collectionChanges) {
// get the new fetch result
self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];
UICollectionView *collectionView = self.collectionView;
if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
// we need to reload all if the incremental diffs are not available
[collectionView reloadData];
} else {
// if we have incremental diffs, tell the collection view to animate insertions and deletions
[collectionView performBatchUpdates:^{
NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
if ([removedIndexes count]) {
[collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
if ([insertedIndexes count]) {
[collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
if (self.picker.showCameraButton && self.picker.autoSelectCameraImages) {
for (NSIndexPath *path in [insertedIndexes aapl_indexPathsFromIndexesWithSection:0]) {
[self collectionView:collectionView didSelectItemAtIndexPath:path];
}
}
}
NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
if ([changedIndexes count]) {
[collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
} completion:NULL];
}
[self resetCachedAssets];
}
});
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateCachedAssets];
}
#pragma mark - Asset Caching
- (void)resetCachedAssets
{
[self.imageManager stopCachingImagesForAllAssets];
self.previousPreheatRect = CGRectZero;
}
- (void)updateCachedAssets
{
BOOL isViewVisible = [self isViewLoaded] && [[self view] window] != nil;
if (!isViewVisible) { return; }
// The preheat window is twice the height of the visible rect
CGRect preheatRect = self.collectionView.bounds;
preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect));
// If scrolled by a "reasonable" amount...
CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect));
if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0f) {
// Compute the assets to start caching and to stop caching.
NSMutableArray *addedIndexPaths = [NSMutableArray array];
NSMutableArray *removedIndexPaths = [NSMutableArray array];
[self computeDifferenceBetweenRect:self.previousPreheatRect andRect:preheatRect removedHandler:^(CGRect removedRect) {
NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:removedRect];
[removedIndexPaths addObjectsFromArray:indexPaths];
} addedHandler:^(CGRect addedRect) {
NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:addedRect];
[addedIndexPaths addObjectsFromArray:indexPaths];
}];
NSArray *assetsToStartCaching = [self assetsAtIndexPaths:addedIndexPaths];
NSArray *assetsToStopCaching = [self assetsAtIndexPaths:removedIndexPaths];
[self.imageManager startCachingImagesForAssets:assetsToStartCaching
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil];
[self.imageManager stopCachingImagesForAssets:assetsToStopCaching
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil];
self.previousPreheatRect = preheatRect;
}
}
- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler
{
if (CGRectIntersectsRect(newRect, oldRect)) {
CGFloat oldMaxY = CGRectGetMaxY(oldRect);
CGFloat oldMinY = CGRectGetMinY(oldRect);
CGFloat newMaxY = CGRectGetMaxY(newRect);
CGFloat newMinY = CGRectGetMinY(newRect);
if (newMaxY > oldMaxY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
addedHandler(rectToAdd);
}
if (oldMinY > newMinY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
addedHandler(rectToAdd);
}
if (newMaxY < oldMaxY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
removedHandler(rectToRemove);
}
if (oldMinY < newMinY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
removedHandler(rectToRemove);
}
} else {
addedHandler(newRect);
removedHandler(oldRect);
}
}
- (NSArray *)assetsAtIndexPaths:(NSArray *)indexPaths
{
if (indexPaths.count == 0) { return nil; }
NSMutableArray *assets = [NSMutableArray arrayWithCapacity:indexPaths.count];
for (NSIndexPath *indexPath in indexPaths) {
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[assets addObject:asset];
}
return assets;
}
@end

View File

@ -0,0 +1,24 @@
//
// GMImagePicker.h
// GMImagePicker
//
// Created by Shadowfacts on 1/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for GMImagePicker.
FOUNDATION_EXPORT double GMImagePickerVersionNumber;
//! Project version string for GMImagePicker.
FOUNDATION_EXPORT const unsigned char GMImagePickerVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <GMImagePicker/PublicHeader.h>
#import <GMImagePicker/GMImagePickerController.h>
#import <GMImagePicker/GMAlbumsViewCell.h>
#import <GMImagePicker/GMAlbumsViewController.h>
#import <GMImagePicker/GMGridViewCell.h>
#import <GMImagePicker/GMGridViewController.h>

View File

@ -0,0 +1,332 @@
//
// GMImagePickerController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <Photos/Photos.h>
//This is the default image picker size!
//static CGSize const kPopoverContentSize = {320, 480};
//However, the iPad is 1024x768 so it can allow popups up to 768!
static CGSize const kPopoverContentSize = {480, 720};
@protocol GMImagePickerControllerDelegate;
/**
* A controller that allows picking multiple photos and videos from user's photo library.
*/
@interface GMImagePickerController : UIViewController
/**
* The assets pickers delegate object.
*/
@property (nonatomic, weak) id <GMImagePickerControllerDelegate> delegate;
/**
* It contains the selected `PHAsset` objects. The order of the objects is the selection order.
*
* You can add assets before presenting the picker to show the user some preselected assets.
*/
@property (nonatomic, strong) NSMutableArray *selectedAssets;
/** UI Customizations **/
/**
* Determines which smart collections are displayed (int array of enum: PHAssetCollectionSubtypeSmartAlbum)
* The default smart collections are:
* - Favorites
* - RecentlyAdded
* - Videos
* - SlomoVideos
* - Timelapses
* - Bursts
* - Panoramas
*/
@property (nonatomic, strong) NSArray* customSmartCollections;
/**
* Determines which media types are allowed (int array of enum: PHAssetMediaType)
* This defaults to all media types (view, audio and images)
* This can override customSmartCollections behavior (ie, remove video-only smart collections)
*/
@property (nonatomic, strong) NSArray* mediaTypes;
/**
* If set, it displays a this string instead of the localised default of "Done" on the done button. Note also that this
* is not used when a single selection is active since the selection of the chosen photo closes the VC thus rendering
* the button pointless.
*/
@property (nonatomic) NSString* customDoneButtonTitle;
/**
* If set, it displays this string instead of the localised default of "Cancel" on the cancel button
*/
@property (nonatomic) NSString* customCancelButtonTitle;
/**
* If set, it displays a prompt in the navigation bar
*/
@property (nonatomic) NSString* customNavigationBarPrompt;
/**
* Determines whether or not a toolbar with info about user selection is shown.
* The InfoToolbar is visible by default.
*/
@property (nonatomic) BOOL displaySelectionInfoToolbar;
/**
* Determines whether or not the number of assets is shown in the Album list.
* The number of assets is visible by default.
*/
@property (nonatomic, assign) BOOL displayAlbumsNumberOfAssets;
/**
* Automatically disables the "Done" button if nothing is selected. Defaults to YES.
*/
@property (nonatomic, assign) BOOL autoDisableDoneButton;
/**
* Use the picker either for miltiple image selections, or just a single selection. In the case of a single selection
* the VC is closed on selection so the Done button is neither displayed or used. Default is YES.
*/
@property (nonatomic, assign) BOOL allowsMultipleSelection;
/**
* In the case where allowsMultipleSelection = NO, set this to YES to have the user confirm their selection. Default is NO.
*/
@property (nonatomic, assign) BOOL confirmSingleSelection;
/**
* If set, it displays this string (if confirmSingleSelection = YES) instead of the localised default.
*/
@property (nonatomic) NSString *confirmSingleSelectionPrompt;
/**
* True to always show the toolbar, with a camera button allowing new photos to be taken. False to auto show/hide the
* toolbar, and have no camera button. Default is false. If true, this renders displaySelectionInfoToolbar a no-op.
*/
@property (nonatomic, assign) BOOL showCameraButton;
/**
* True to auto select the image(s) taken with the camera if showCameraButton = YES. In the case of allowsMultipleSelection = YES,
* this will trigger the selection handler too.
*/
@property (nonatomic, assign) BOOL autoSelectCameraImages;
/**
* If set, the user is allowed to edit captured still images
*/
@property (nonatomic, assign) BOOL allowsEditingCameraImages;
/**
* Grid customizations:
*
* - colsInPortrait: Number of columns in portrait (3 by default)
* - colsInLandscape: Number of columns in landscape (5 by default)
* - minimumInteritemSpacing: Horizontal and vertical minimum space between grid cells (2.0 by default)
*/
@property (nonatomic) NSInteger colsInPortrait;
@property (nonatomic) NSInteger colsInLandscape;
@property (nonatomic) double minimumInteritemSpacing;
/**
* UI customizations:
*
* - pickerBackgroundColor: The colour for all backgrounds; behind the table and cells. Defaults to [UIColor whiteColor]
* - pickerTextColor: The color for text in the views. This needs to work with pickerBackgroundColor! Default of darkTextColor
* - toolbarBackgroundColor: The background color of the toolbar. Defaults to nil.
* - toolbarBarTintColor: The color for the background tint of the toolbar. Defaults to nil.
* - toolbarTextColor: The color of the text on the toolbar
* - toolbarTintColor: The tint colour used for any buttons on the toolbar
* - navigationBarBackgroundColor: The background of the navigation bar. Defaults to nil.
* - navigationBarBarTintColor: The color for the background tint of the navigation bar. Defaults to nil.
* - navigationBarTextColor: The color for the text in the navigation bar. Defaults to [UIColor darkTextColor]
* - navigationBarTintColor: The tint color used for any buttons on the navigation Bar
* - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue. It is advised if you set this to check, and possibly set, appropriately the custom font sizes. For font information, check http://www.iosfonts.com/
* - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue-Bold. It is advised if you set this to check, and possibly set, appropriately the custom font sizes.
* - pickerFontNormalSize: The size of the custom font used in most places. Defaults to 14.0f
* - pickerFontHeaderSize: The size of the custom font for album names. Defaults to 17.0f
* - pickerStatusBarsStyle: On iPhones this will matter if custom navigation bar colours are being used. Defaults to UIStatusBarStyleDefault
* - useCustomFontForNavigationBar: True to use the custom font (or it's default) in the navigation bar, false to leave to iOS Defaults.
* - arrangeSmartCollectionsFirst: True will put the users smart collections above their albums, false will set it opposite. Default is NO.
*/
@property (nonatomic, strong) UIColor *pickerBackgroundColor;
@property (nonatomic, strong) UIColor *pickerTextColor;
@property (nonatomic, strong) UIColor *toolbarBackgroundColor;
@property (nonatomic, strong) UIColor *toolbarBarTintColor;
@property (nonatomic, strong) UIColor *toolbarTextColor;
@property (nonatomic, strong) UIColor *toolbarTintColor;
@property (nonatomic, strong) UIColor *navigationBarBackgroundColor;
@property (nonatomic, strong) UIColor *navigationBarBarTintColor;
@property (nonatomic, strong) UIColor *navigationBarTextColor;
@property (nonatomic, strong) UIColor *navigationBarTintColor;
@property (nonatomic, strong) NSString *pickerFontName;
@property (nonatomic, strong) NSString *pickerBoldFontName;
@property (nonatomic) CGFloat pickerFontNormalSize;
@property (nonatomic) CGFloat pickerFontHeaderSize;
@property (nonatomic) UIStatusBarStyle pickerStatusBarStyle;
@property (nonatomic) BOOL useCustomFontForNavigationBar;
@property (nonatomic) BOOL arrangeSmartCollectionsFirst;
/**
* A reference to the navigation controller used to manage the whole picking process
*/
@property (nonatomic, strong) UINavigationController *navigationController;
/**
* Managing Asset Selection
*/
- (void)selectAsset:(PHAsset *)asset;
- (void)deselectAsset:(PHAsset *)asset;
/**
* User finish Actions
*/
- (void)dismiss:(id)sender;
- (void)finishPickingAssets:(id)sender;
@end
@protocol GMImagePickerControllerDelegate <NSObject>
/**
* @name Closing the Picker
*/
/**
* Tells the delegate that the user finish picking photos or videos.
* @param picker The controller object managing the assets picker interface.
* @param assets An array containing picked PHAssets objects.
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didFinishPickingAssets:(NSArray *)assets;
@optional
/**
* Tells the delegate that the user cancelled the pick operation.
* @param picker The controller object managing the assets picker interface.
*/
- (void)assetsPickerControllerDidCancel:(GMImagePickerController *)picker;
/**
* @name Enabling Assets
*/
/**
* Ask the delegate if the specified asset should be shown.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be shown.
*
* @return `YES` if the asset should be shown or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldShowAsset:(PHAsset *)asset;
/**
* Ask the delegate if the specified asset should be enabled for selection.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be enabled.
*
* @return `YES` if the asset should be enabled or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldEnableAsset:(PHAsset *)asset;
/**
* @name Managing the Selected Assets
*/
/**
* Asks the delegate if the specified asset should be selected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be selected.
*
* @return `YES` if the asset should be selected or `NO` if it should not.
*
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldSelectAsset:(PHAsset *)asset;
/**
* Tells the delegate that the asset was selected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was selected.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didSelectAsset:(PHAsset *)asset;
/**
* Asks the delegate if the specified asset should be deselected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be deselected.
*
* @return `YES` if the asset should be deselected or `NO` if it should not.
*
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldDeselectAsset:(PHAsset *)asset;
/**
* Tells the delegate that the item at the specified path was deselected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was deselected.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didDeselectAsset:(PHAsset *)asset;
/**
* @name Managing Asset Highlighting
*/
/**
* Asks the delegate if the specified asset should be highlighted.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be highlighted.
*
* @return `YES` if the asset should be highlighted or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldHighlightAsset:(PHAsset *)asset;
/**
* Tells the delegate that asset was highlighted.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was highlighted.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didHighlightAsset:(PHAsset *)asset;
/**
* Tells the delegate that the highlight was removed from the asset.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that had its highlight removed.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didUnhighlightAsset:(PHAsset *)asset;
@end

View File

@ -0,0 +1,388 @@
//
// GMImagePickerController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <MobileCoreServices/MobileCoreServices.h>
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import <Photos/Photos.h>
@interface GMImagePickerController () <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIAlertViewDelegate>
@end
@implementation GMImagePickerController
- (id)init
{
if (self = [super init]) {
_selectedAssets = [[NSMutableArray alloc] init];
// Default values:
_displaySelectionInfoToolbar = YES;
_displayAlbumsNumberOfAssets = YES;
_autoDisableDoneButton = YES;
_allowsMultipleSelection = YES;
_confirmSingleSelection = NO;
_showCameraButton = NO;
// Grid configuration:
_colsInPortrait = 3;
_colsInLandscape = 5;
_minimumInteritemSpacing = 2.0;
// Sample of how to select the collections you want to display:
_customSmartCollections = @[@(PHAssetCollectionSubtypeSmartAlbumFavorites),
@(PHAssetCollectionSubtypeSmartAlbumRecentlyAdded),
@(PHAssetCollectionSubtypeSmartAlbumVideos),
@(PHAssetCollectionSubtypeSmartAlbumSlomoVideos),
@(PHAssetCollectionSubtypeSmartAlbumTimelapses),
@(PHAssetCollectionSubtypeSmartAlbumBursts),
@(PHAssetCollectionSubtypeSmartAlbumPanoramas)];
// If you don't want to show smart collections, just put _customSmartCollections to nil;
//_customSmartCollections=nil;
// Which media types will display
_mediaTypes = @[@(PHAssetMediaTypeAudio),
@(PHAssetMediaTypeVideo),
@(PHAssetMediaTypeImage)];
self.preferredContentSize = kPopoverContentSize;
// UI Customisation
_pickerBackgroundColor = [UIColor whiteColor];
_pickerTextColor = [UIColor darkTextColor];
_pickerFontName = @"HelveticaNeue";
_pickerBoldFontName = @"HelveticaNeue-Bold";
_pickerFontNormalSize = 14.0f;
_pickerFontHeaderSize = 17.0f;
_navigationBarBackgroundColor = [UIColor whiteColor];
_navigationBarTextColor = [UIColor darkTextColor];
_navigationBarTintColor = [UIColor darkTextColor];
_toolbarBarTintColor = [UIColor whiteColor];
_toolbarTextColor = [UIColor darkTextColor];
_toolbarTintColor = [UIColor darkTextColor];
_pickerStatusBarStyle = UIStatusBarStyleDefault;
[self setupNavigationController];
}
return self;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Ensure nav and toolbar customisations are set. Defaults are in place, but the user may have changed them
self.view.backgroundColor = _pickerBackgroundColor;
_navigationController.toolbar.translucent = YES;
_navigationController.toolbar.barTintColor = _toolbarBarTintColor;
_navigationController.toolbar.tintColor = _toolbarTintColor;
_navigationController.navigationBar.backgroundColor = _navigationBarBackgroundColor;
_navigationController.navigationBar.tintColor = _navigationBarTintColor;
NSDictionary *attributes;
if (_useCustomFontForNavigationBar) {
attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor,
NSFontAttributeName : [UIFont fontWithName:_pickerBoldFontName size:_pickerFontHeaderSize]};
} else {
attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor};
}
_navigationController.navigationBar.titleTextAttributes = attributes;
[self updateToolbar];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return _pickerStatusBarStyle;
}
#pragma mark - Setup Navigation Controller
- (void)setupNavigationController
{
GMAlbumsViewController *albumsViewController = [[GMAlbumsViewController alloc] init];
_navigationController = [[UINavigationController alloc] initWithRootViewController:albumsViewController];
_navigationController.delegate = self;
[_navigationController.navigationBar setTranslucent:NO];
[_navigationController willMoveToParentViewController:self];
[_navigationController.view setFrame:self.view.frame];
[self.view addSubview:_navigationController.view];
[self addConstraintsToChildViewControllersView:_navigationController.view];
[self addChildViewController:_navigationController];
[_navigationController didMoveToParentViewController:self];
}
- (void)addConstraintsToChildViewControllersView:(UIView *)view {
view.translatesAutoresizingMaskIntoConstraints = NO;
NSArray * hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(view)];
NSLayoutConstraint * topConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
NSLayoutConstraint * bottomConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
[view.superview addConstraints:@[topConstraint,bottomConstraint]];
[view.superview addConstraints:hConstraints];
}
#pragma mark - UIAlertViewDelegate
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1) {
// Only if OK was pressed do we want to completge the selection
[self finishPickingAssets:self];
}
}
#pragma mark - Select / Deselect Asset
- (void)selectAsset:(PHAsset *)asset
{
[self.selectedAssets insertObject:asset atIndex:self.selectedAssets.count];
[self updateDoneButton];
if (!self.allowsMultipleSelection) {
if (self.confirmSingleSelection) {
NSString *message = self.confirmSingleSelectionPrompt ? self.confirmSingleSelectionPrompt : [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.message", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Do you want to select the image you tapped on?")];
[[[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Are You Sure?")]
message:message
delegate:self
cancelButtonTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.no", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"No")]
otherButtonTitles:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.yes", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Yes")], nil] show];
} else {
[self finishPickingAssets:self];
}
} else if (self.displaySelectionInfoToolbar || self.showCameraButton) {
[self updateToolbar];
}
}
- (void)deselectAsset:(PHAsset *)asset
{
[self.selectedAssets removeObjectAtIndex:[self.selectedAssets indexOfObject:asset]];
if (self.selectedAssets.count == 0) {
[self updateDoneButton];
}
if (self.displaySelectionInfoToolbar || self.showCameraButton) {
[self updateToolbar];
}
}
- (void)updateDoneButton
{
if (!self.allowsMultipleSelection) {
return;
}
UINavigationController *nav = (UINavigationController *)self.childViewControllers[0];
for (UIViewController *viewController in nav.viewControllers) {
viewController.navigationItem.rightBarButtonItem.enabled = (self.autoDisableDoneButton ? self.selectedAssets.count > 0 : TRUE);
}
}
- (void)updateToolbar
{
if (!self.allowsMultipleSelection && !self.showCameraButton) {
return;
}
UINavigationController *nav = (UINavigationController *)self.childViewControllers[0];
for (UIViewController *viewController in nav.viewControllers) {
NSUInteger index = 1;
if (_showCameraButton) {
index++;
}
[[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateNormal];
[[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateDisabled];
[[viewController.toolbarItems objectAtIndex:index] setTitle:[self toolbarTitle]];
[viewController.navigationController setToolbarHidden:(self.selectedAssets.count == 0 && !self.showCameraButton) animated:YES];
}
}
#pragma mark - User finish Actions
- (void)dismiss:(id)sender
{
if ([self.delegate respondsToSelector:@selector(assetsPickerControllerDidCancel:)]) {
[self.delegate assetsPickerControllerDidCancel:self];
}
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)finishPickingAssets:(id)sender
{
if ([self.delegate respondsToSelector:@selector(assetsPickerController:didFinishPickingAssets:)]) {
[self.delegate assetsPickerController:self didFinishPickingAssets:self.selectedAssets];
}
}
#pragma mark - Toolbar Title
- (NSPredicate *)predicateOfAssetType:(PHAssetMediaType)type
{
return [NSPredicate predicateWithBlock:^BOOL(PHAsset *asset, NSDictionary *bindings) {
return (asset.mediaType == type);
}];
}
- (NSString *)toolbarTitle
{
if (self.selectedAssets.count == 0) {
return nil;
}
NSPredicate *photoPredicate = [self predicateOfAssetType:PHAssetMediaTypeImage];
NSPredicate *videoPredicate = [self predicateOfAssetType:PHAssetMediaTypeVideo];
NSInteger nImages = [self.selectedAssets filteredArrayUsingPredicate:photoPredicate].count;
NSInteger nVideos = [self.selectedAssets filteredArrayUsingPredicate:videoPredicate].count;
if (nImages > 0 && nVideos > 0) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-items", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Items Selected" ), @(nImages + nVideos)];
} else if (nImages > 1) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-photos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Photos Selected"), @(nImages)];
} else if (nImages == 1) {
return NSLocalizedStringFromTableInBundle(@"picker.selection.single-photo", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Photo Selected" );
} else if (nVideos > 1) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-videos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Videos Selected"), @(nVideos)];
} else if (nVideos == 1) {
return NSLocalizedStringFromTableInBundle(@"picker.selection.single-video", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Video Selected");
} else {
return nil;
}
}
#pragma mark - Toolbar Items
- (void)cameraButtonPressed:(UIBarButtonItem *)button
{
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"No Camera!"
message:@"Sorry, this device does not have a camera."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
return;
}
// This allows the selection of the image taken to be better seen if the user is not already in that VC
if (self.autoSelectCameraImages && [self.navigationController.topViewController isKindOfClass:[GMAlbumsViewController class]]) {
[((GMAlbumsViewController *)self.navigationController.topViewController) selectAllAlbumsCell];
}
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = @[(NSString *)kUTTypeImage];
picker.allowsEditing = self.allowsEditingCameraImages;
picker.delegate = self;
picker.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popPC = picker.popoverPresentationController;
popPC.permittedArrowDirections = UIPopoverArrowDirectionAny;
popPC.barButtonItem = button;
[self showViewController:picker sender:button];
}
- (NSDictionary *)toolbarTitleTextAttributes {
return @{NSForegroundColorAttributeName : _toolbarTextColor,
NSFontAttributeName : [UIFont fontWithName:_pickerFontName size:_pickerFontHeaderSize]};
}
- (UIBarButtonItem *)titleButtonItem
{
UIBarButtonItem *title = [[UIBarButtonItem alloc] initWithTitle:self.toolbarTitle
style:UIBarButtonItemStylePlain
target:nil
action:nil];
NSDictionary *attributes = [self toolbarTitleTextAttributes];
[title setTitleTextAttributes:attributes forState:UIControlStateNormal];
[title setTitleTextAttributes:attributes forState:UIControlStateDisabled];
[title setEnabled:NO];
return title;
}
- (UIBarButtonItem *)spaceButtonItem
{
return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
}
- (UIBarButtonItem *)cameraButtonItem
{
return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:@selector(cameraButtonPressed:)];
}
- (NSArray *)toolbarItems
{
UIBarButtonItem *camera = [self cameraButtonItem];
UIBarButtonItem *title = [self titleButtonItem];
UIBarButtonItem *space = [self spaceButtonItem];
NSMutableArray *items = [[NSMutableArray alloc] init];
if (_showCameraButton) {
[items addObject:camera];
}
[items addObject:space];
[items addObject:title];
[items addObject:space];
return [NSArray arrayWithArray:items];
}
#pragma mark - Camera Delegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
[picker.presentingViewController dismissViewControllerAnimated:YES completion:nil];
NSString *mediaType = info[UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
UIImage *image = info[UIImagePickerControllerEditedImage] ? : info[UIImagePickerControllerOriginalImage];
UIImageWriteToSavedPhotosAlbum(image,
self,
@selector(image:finishedSavingWithError:contextInfo:),
nil);
}
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
-(void)image:(UIImage *)image finishedSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
if (error) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Image Not Saved"
message:@"Sorry, unable to save the new image!"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
}
// Note: The image view will auto refresh as the photo's are being observed in the other VCs
}
@end

BIN
GMImagePicker/GMSelected.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
GMImagePicker/GMSelected@2x.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

22
GMImagePicker/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,10 @@
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D60A548D21ED515800F1F87C /* GMImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; };
D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; };
D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -67,6 +71,9 @@
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */; };
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */; };
D627FF81217FE8F400CC0648 /* BehaviorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */; };
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B4E21EA695800FE4B39 /* StatusContentType.swift */; };
D6285B5121EA6E6E00FE4B39 /* AdvancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */; };
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5221EA708700FE4B39 /* StatusFormat.swift */; };
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; };
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
@ -110,8 +117,36 @@
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.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 */; };
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D67E0513216438A7000E0927 /* AppearanceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */; };
D67E051521643C77000E0927 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E051421643C77000E0927 /* Tab.swift */; };
D686329521ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686326E21ED8312008C716E /* GMImagePicker.strings */; };
D686329621ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327121ED8312008C716E /* GMImagePicker.strings */; };
D686329721ED8319008C716E /* GMGridViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D686327321ED8312008C716E /* GMGridViewCell.m */; };
D686329821ED8319008C716E /* GMVideoIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327421ED8312008C716E /* GMVideoIcon@2x.png */; };
D686329921ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327621ED8313008C716E /* GMImagePicker.strings */; };
D686329A21ED8319008C716E /* GMAlbumsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686327821ED8313008C716E /* GMAlbumsViewController.m */; };
D686329B21ED8319008C716E /* GMVideoIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327921ED8313008C716E /* GMVideoIcon.png */; };
D686329C21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686327B21ED8313008C716E /* GMImagePicker.strings */; };
D686329D21ED8319008C716E /* GMEmptyFolder@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */; };
D686329E21ED8319008C716E /* GMAlbumsViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D686327E21ED8314008C716E /* GMAlbumsViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
D686329F21ED8319008C716E /* GMGridViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686327F21ED8315008C716E /* GMGridViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
D68632A021ED8319008C716E /* GMSelected.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328021ED8315008C716E /* GMSelected.png */; };
D68632A121ED8319008C716E /* GMAlbumsViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D686328121ED8315008C716E /* GMAlbumsViewCell.m */; };
D68632A221ED8319008C716E /* GMAlbumsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686328221ED8316008C716E /* GMAlbumsViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
D68632A321ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328421ED8316008C716E /* GMImagePicker.strings */; };
D68632A421ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328721ED8317008C716E /* GMImagePicker.strings */; };
D68632A521ED8319008C716E /* GMSelected@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328921ED8317008C716E /* GMSelected@2x.png */; };
D68632A621ED8319008C716E /* GMImagePickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686328A21ED8317008C716E /* GMImagePickerController.m */; };
D68632A721ED8319008C716E /* GMGridViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D686328B21ED8317008C716E /* GMGridViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
D68632A821ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686328D21ED8317008C716E /* GMImagePicker.strings */; };
D68632A921ED8319008C716E /* GMEmptyFolder@1x.png in Resources */ = {isa = PBXBuildFile; fileRef = D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */; };
D68632AA21ED8319008C716E /* GMGridViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D686329021ED8319008C716E /* GMGridViewController.m */; };
D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686329121ED8319008C716E /* GMImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; };
D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686329321ED8319008C716E /* GMImagePicker.strings */; };
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; };
D6A5FAFB217B86CE003DB2D9 /* OnboardingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */; };
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
@ -150,6 +185,13 @@
remoteGlobalIDString = 04496BC7216252E5001F1B23;
remoteInfo = TTTAttributedLabel;
};
D60A549021ED515800F1F87C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
proxyType = 1;
remoteGlobalIDString = D60A548A21ED515800F1F87C;
remoteInfo = GMImagePicker;
};
D61099B52144B0CC00432DC2 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
@ -194,6 +236,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */,
04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */,
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */,
@ -216,6 +259,10 @@
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = "<group>"; };
D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = "<group>"; };
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -266,6 +313,9 @@
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DraftTableViewCell.xib; sourceTree = "<group>"; };
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftTableViewCell.swift; sourceTree = "<group>"; };
D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorTableViewController.swift; sourceTree = "<group>"; };
D6285B4E21EA695800FE4B39 /* StatusContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentType.swift; sourceTree = "<group>"; };
D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedTableViewController.swift; sourceTree = "<group>"; };
D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.swift; sourceTree = "<group>"; };
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
@ -308,8 +358,36 @@
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.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>"; };
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceTableViewController.swift; sourceTree = "<group>"; };
D67E051421643C77000E0927 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = "<group>"; };
D686326F21ED8312008C716E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686327221ED8312008C716E /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686327321ED8312008C716E /* GMGridViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMGridViewCell.m; sourceTree = "<group>"; };
D686327421ED8312008C716E /* GMVideoIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMVideoIcon@2x.png"; sourceTree = "<group>"; };
D686327721ED8313008C716E /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686327821ED8313008C716E /* GMAlbumsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMAlbumsViewController.m; sourceTree = "<group>"; };
D686327921ED8313008C716E /* GMVideoIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GMVideoIcon.png; sourceTree = "<group>"; };
D686327C21ED8313008C716E /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMEmptyFolder@2x.png"; sourceTree = "<group>"; };
D686327E21ED8314008C716E /* GMAlbumsViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMAlbumsViewCell.h; sourceTree = "<group>"; };
D686327F21ED8315008C716E /* GMGridViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMGridViewController.h; sourceTree = "<group>"; };
D686328021ED8315008C716E /* GMSelected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = GMSelected.png; sourceTree = "<group>"; };
D686328121ED8315008C716E /* GMAlbumsViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMAlbumsViewCell.m; sourceTree = "<group>"; };
D686328221ED8316008C716E /* GMAlbumsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMAlbumsViewController.h; sourceTree = "<group>"; };
D686328521ED8316008C716E /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686328821ED8317008C716E /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686328921ED8317008C716E /* GMSelected@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMSelected@2x.png"; sourceTree = "<group>"; };
D686328A21ED8317008C716E /* GMImagePickerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMImagePickerController.m; sourceTree = "<group>"; };
D686328B21ED8317008C716E /* GMGridViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMGridViewCell.h; sourceTree = "<group>"; };
D686328E21ED8317008C716E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = GMImagePicker.strings; sourceTree = "<group>"; };
D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "GMEmptyFolder@1x.png"; sourceTree = "<group>"; };
D686329021ED8319008C716E /* GMGridViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GMGridViewController.m; sourceTree = "<group>"; };
D686329121ED8319008C716E /* GMImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMImagePickerController.h; sourceTree = "<group>"; };
D686329421ED8319008C716E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = GMImagePicker.strings; sourceTree = "<group>"; };
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = "<group>"; };
D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardingViewController.xib; sourceTree = "<group>"; };
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
@ -353,6 +431,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D60A548821ED515800F1F87C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D61099A82144B0CC00432DC2 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -372,6 +457,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */,
04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */,
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
@ -407,6 +493,48 @@
path = TTTAttributedLabel;
sourceTree = "<group>";
};
D60A548C21ED515800F1F87C /* GMImagePicker */ = {
isa = PBXGroup;
children = (
D60A548D21ED515800F1F87C /* GMImagePicker.h */,
D60A548E21ED515800F1F87C /* Info.plist */,
D686327A21ED8313008C716E /* Base.lproj */,
D686327021ED8312008C716E /* ca.lproj */,
D686329221ED8319008C716E /* de.lproj */,
D686328C21ED8317008C716E /* en.lproj */,
D686326D21ED8312008C716E /* es.lproj */,
D686328321ED8316008C716E /* fr.lproj */,
D686327E21ED8314008C716E /* GMAlbumsViewCell.h */,
D686328121ED8315008C716E /* GMAlbumsViewCell.m */,
D686328221ED8316008C716E /* GMAlbumsViewController.h */,
D686327821ED8313008C716E /* GMAlbumsViewController.m */,
D686328F21ED8318008C716E /* GMEmptyFolder@1x.png */,
D686327D21ED8314008C716E /* GMEmptyFolder@2x.png */,
D686328B21ED8317008C716E /* GMGridViewCell.h */,
D686327321ED8312008C716E /* GMGridViewCell.m */,
D686327F21ED8315008C716E /* GMGridViewController.h */,
D686329021ED8319008C716E /* GMGridViewController.m */,
D686329121ED8319008C716E /* GMImagePickerController.h */,
D686328A21ED8317008C716E /* GMImagePickerController.m */,
D686328021ED8315008C716E /* GMSelected.png */,
D686328921ED8317008C716E /* GMSelected@2x.png */,
D686327921ED8313008C716E /* GMVideoIcon.png */,
D686327421ED8312008C716E /* GMVideoIcon@2x.png */,
D686327521ED8313008C716E /* it.lproj */,
D686328621ED8317008C716E /* pt.lproj */,
);
path = GMImagePicker;
sourceTree = "<group>";
};
D60C07E221E817560057FAA8 /* Compose Media */ = {
isa = PBXGroup;
children = (
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */,
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
);
path = "Compose Media";
sourceTree = "<group>";
};
D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
isa = PBXGroup;
children = (
@ -488,6 +616,7 @@
D61099FA214569F600432DC2 /* Report.swift */,
D61099FC21456A1D00432DC2 /* SearchResults.swift */,
D61099FE21456A4C00432DC2 /* Status.swift */,
D6285B4E21EA695800FE4B39 /* StatusContentType.swift */,
D6109A10214607D500432DC2 /* Timeline.swift */,
);
path = Model;
@ -596,6 +725,7 @@
D627FF77217E94F200CC0648 /* Drafts */,
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
);
path = Compose;
sourceTree = "<group>";
@ -615,6 +745,7 @@
children = (
D663626521360DD700C9CBA2 /* Preferences.storyboard */,
D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */,
D6285B5021EA6E6E00FE4B39 /* AdvancedTableViewController.swift */,
D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */,
D627FF80217FE8F400CC0648 /* BehaviorTableViewController.swift */,
D6C693C92161253F007D6A6D /* SilentActionPermissionsTableViewController.swift */,
@ -690,6 +821,7 @@
D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */,
D6333B362137838300CE884A /* AttributedString+Trim.swift */,
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */,
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -707,15 +839,98 @@
path = XCallbackURL;
sourceTree = "<group>";
};
D67C57A721E2649B00C3118B /* Account Detail */ = {
isa = PBXGroup;
children = (
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */,
);
path = "Account Detail";
sourceTree = "<group>";
};
D67C57B021E28F9400C3118B /* Compose Status Reply */ = {
isa = PBXGroup;
children = (
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */,
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */,
);
path = "Compose Status Reply";
sourceTree = "<group>";
};
D686326D21ED8312008C716E /* es.lproj */ = {
isa = PBXGroup;
children = (
D686326E21ED8312008C716E /* GMImagePicker.strings */,
);
path = es.lproj;
sourceTree = "<group>";
};
D686327021ED8312008C716E /* ca.lproj */ = {
isa = PBXGroup;
children = (
D686327121ED8312008C716E /* GMImagePicker.strings */,
);
path = ca.lproj;
sourceTree = "<group>";
};
D686327521ED8313008C716E /* it.lproj */ = {
isa = PBXGroup;
children = (
D686327621ED8313008C716E /* GMImagePicker.strings */,
);
path = it.lproj;
sourceTree = "<group>";
};
D686327A21ED8313008C716E /* Base.lproj */ = {
isa = PBXGroup;
children = (
D686327B21ED8313008C716E /* GMImagePicker.strings */,
);
path = Base.lproj;
sourceTree = "<group>";
};
D686328321ED8316008C716E /* fr.lproj */ = {
isa = PBXGroup;
children = (
D686328421ED8316008C716E /* GMImagePicker.strings */,
);
path = fr.lproj;
sourceTree = "<group>";
};
D686328621ED8317008C716E /* pt.lproj */ = {
isa = PBXGroup;
children = (
D686328721ED8317008C716E /* GMImagePicker.strings */,
);
path = pt.lproj;
sourceTree = "<group>";
};
D686328C21ED8317008C716E /* en.lproj */ = {
isa = PBXGroup;
children = (
D686328D21ED8317008C716E /* GMImagePicker.strings */,
);
path = en.lproj;
sourceTree = "<group>";
};
D686329221ED8319008C716E /* de.lproj */ = {
isa = PBXGroup;
children = (
D686329321ED8319008C716E /* GMImagePicker.strings */,
);
path = de.lproj;
sourceTree = "<group>";
};
D6BED1722126661300F02DA0 /* Views */ = {
isa = PBXGroup;
children = (
04496BD621625361001F1B23 /* ContentLabel.swift */,
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */,
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */,
D60C07E221E817560057FAA8 /* Compose Media */,
D641C78A213DD926004B4513 /* Status */,
D641C78B213DD92F004B4513 /* Profile Header */,
D641C78C213DD937004B4513 /* Notifications */,
@ -759,6 +974,7 @@
D6D4DDCE212518A000E1C4BB /* Tusker */,
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
D60A548C21ED515800F1F87C /* GMImagePicker */,
D6D4DDCD212518A000E1C4BB /* Products */,
D65A37F221472F300087646E /* Frameworks */,
);
@ -773,6 +989,7 @@
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */,
D60A548B21ED515800F1F87C /* GMImagePicker.framework */,
);
name = Products;
sourceTree = "<group>";
@ -847,6 +1064,19 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D60A548621ED515800F1F87C /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
D686329F21ED8319008C716E /* GMGridViewController.h in Headers */,
D68632A221ED8319008C716E /* GMAlbumsViewController.h in Headers */,
D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */,
D686329E21ED8319008C716E /* GMAlbumsViewCell.h in Headers */,
D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */,
D68632A721ED8319008C716E /* GMGridViewCell.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D61099A62144B0CC00432DC2 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@ -876,6 +1106,24 @@
productReference = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */;
productType = "com.apple.product-type.framework";
};
D60A548A21ED515800F1F87C /* GMImagePicker */ = {
isa = PBXNativeTarget;
buildConfigurationList = D60A549421ED515800F1F87C /* Build configuration list for PBXNativeTarget "GMImagePicker" */;
buildPhases = (
D60A548621ED515800F1F87C /* Headers */,
D60A548721ED515800F1F87C /* Sources */,
D60A548821ED515800F1F87C /* Frameworks */,
D60A548921ED515800F1F87C /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = GMImagePicker;
productName = GMImagePicker;
productReference = D60A548B21ED515800F1F87C /* GMImagePicker.framework */;
productType = "com.apple.product-type.framework";
};
D61099AA2144B0CC00432DC2 /* Pachyderm */ = {
isa = PBXNativeTarget;
buildConfigurationList = D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */;
@ -927,6 +1175,7 @@
dependencies = (
D61099BF2144B0CC00432DC2 /* PBXTargetDependency */,
04496BCE216252E5001F1B23 /* PBXTargetDependency */,
D60A549121ED515800F1F87C /* PBXTargetDependency */,
);
name = Tusker;
productName = Tusker;
@ -982,6 +1231,9 @@
04496BC7216252E5001F1B23 = {
CreatedOnToolsVersion = 10.1;
};
D60A548A21ED515800F1F87C = {
CreatedOnToolsVersion = 10.1;
};
D61099AA2144B0CC00432DC2 = {
CreatedOnToolsVersion = 10.0;
LastSwiftMigration = 1000;
@ -1010,6 +1262,12 @@
knownRegions = (
en,
Base,
de,
es,
it,
fr,
pt,
ca,
);
mainGroup = D6D4DDC3212518A000E1C4BB;
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
@ -1022,6 +1280,7 @@
D61099AA2144B0CC00432DC2 /* Pachyderm */,
D61099B22144B0CC00432DC2 /* PachydermTests */,
04496BC7216252E5001F1B23 /* TTTAttributedLabel */,
D60A548A21ED515800F1F87C /* GMImagePicker */,
);
};
/* End PBXProject section */
@ -1034,6 +1293,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D60A548921ED515800F1F87C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D686329C21ED8319008C716E /* GMImagePicker.strings in Resources */,
D686329621ED8319008C716E /* GMImagePicker.strings in Resources */,
D68632A521ED8319008C716E /* GMSelected@2x.png in Resources */,
D686329521ED8319008C716E /* GMImagePicker.strings in Resources */,
D68632A321ED8319008C716E /* GMImagePicker.strings in Resources */,
D686329B21ED8319008C716E /* GMVideoIcon.png in Resources */,
D68632A821ED8319008C716E /* GMImagePicker.strings in Resources */,
D686329921ED8319008C716E /* GMImagePicker.strings in Resources */,
D68632A921ED8319008C716E /* GMEmptyFolder@1x.png in Resources */,
D68632A421ED8319008C716E /* GMImagePicker.strings in Resources */,
D686329821ED8319008C716E /* GMVideoIcon@2x.png in Resources */,
D68632A021ED8319008C716E /* GMSelected.png in Resources */,
D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */,
D686329D21ED8319008C716E /* GMEmptyFolder@2x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D61099A92144B0CC00432DC2 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1064,6 +1344,8 @@
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */,
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */,
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */,
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */,
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */,
@ -1095,6 +1377,18 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D60A548721ED515800F1F87C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D686329A21ED8319008C716E /* GMAlbumsViewController.m in Sources */,
D68632A621ED8319008C716E /* GMImagePickerController.m in Sources */,
D686329721ED8319008C716E /* GMGridViewCell.m in Sources */,
D68632AA21ED8319008C716E /* GMGridViewController.m in Sources */,
D68632A121ED8319008C716E /* GMAlbumsViewCell.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D61099A72144B0CC00432DC2 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -1122,6 +1416,7 @@
D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */,
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */,
D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */,
D6109A072145756700432DC2 /* LoginSettings.swift in Sources */,
@ -1150,9 +1445,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */,
D6285B5121EA6E6E00FE4B39 /* AdvancedTableViewController.swift in Sources */,
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
@ -1166,6 +1463,7 @@
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */,
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
@ -1189,6 +1487,7 @@
D6C693CA2161253F007D6A6D /* SilentActionPermissionsTableViewController.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */,
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */,
D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
@ -1196,6 +1495,7 @@
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */,
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D67E0513216438A7000E0927 /* AppearanceTableViewController.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
@ -1242,6 +1542,11 @@
target = 04496BC7216252E5001F1B23 /* TTTAttributedLabel */;
targetProxy = 04496BCD216252E5001F1B23 /* PBXContainerItemProxy */;
};
D60A549121ED515800F1F87C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D60A548A21ED515800F1F87C /* GMImagePicker */;
targetProxy = D60A549021ED515800F1F87C /* PBXContainerItemProxy */;
};
D61099B62144B0CC00432DC2 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D61099AA2144B0CC00432DC2 /* Pachyderm */;
@ -1270,6 +1575,70 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
D686326E21ED8312008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686326F21ED8312008C716E /* es */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686327121ED8312008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686327221ED8312008C716E /* ca */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686327621ED8313008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686327721ED8313008C716E /* it */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686327B21ED8313008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686327C21ED8313008C716E /* Base */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686328421ED8316008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686328521ED8316008C716E /* fr */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686328721ED8317008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686328821ED8317008C716E /* pt */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686328D21ED8317008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686328E21ED8317008C716E /* en */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D686329321ED8319008C716E /* GMImagePicker.strings */ = {
isa = PBXVariantGroup;
children = (
D686329421ED8319008C716E /* de */,
);
name = GMImagePicker.strings;
sourceTree = "<group>";
};
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
@ -1337,6 +1706,62 @@
};
name = Release;
};
D60A549521ED515800F1F87C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = GMImagePicker/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GMImagePicker;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
D60A549621ED515800F1F87C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = HGYVAQA9FW;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = GMImagePicker/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.GMImagePicker;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
D61099C32144B0CC00432DC2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1690,6 +2115,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D60A549421ED515800F1F87C /* Build configuration list for PBXNativeTarget "GMImagePicker" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D60A549521ED515800F1F87C /* Debug */,
D60A549621ED515800F1F87C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -0,0 +1,23 @@
//
// Array+Uniques.swift
// Tusker
//
// Created by Shadowfacts on 1/6/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
extension Array where Element: Hashable {
func uniques() -> [Element] {
var buffer = [Element]()
var added = Set<Element>()
for elem in self {
if !added.contains(elem) {
buffer.append(elem)
added.insert(elem)
}
}
return buffer
}
}

View File

@ -45,6 +45,7 @@ class Preferences: Codable {
// MARK: - Advanced
var silentActions: [String: Permission] = [:]
var statusContentType: StatusContentType = .plain
// MARK: - Utility Methods
func tabIndex(_ tab: Tab) -> Int {

View File

@ -9,67 +9,86 @@
import UIKit
import Pachyderm
import Intents
import Photos
import GMImagePicker
import MobileCoreServices
class ComposeViewController: UIViewController {
let router: AppRouter
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var inReplyToContainerView: UIView!
@IBOutlet weak var inReplyToAvatarImageView: UIImageView!
@IBOutlet weak var inReplyToDisplayNameLabel: UILabel!
@IBOutlet weak var inReplyToUsernameLabel: UILabel!
@IBOutlet weak var inReplyToContentLabel: StatusContentLabel!
@IBOutlet weak var inReplyToLabel: UILabel!
@IBOutlet weak var statusTextView: UITextView!
@IBOutlet weak var placeholderLabel: UILabel!
@IBOutlet weak var charactersRemainingLabel: UILabel!
@IBOutlet weak var visibilityButton: UIButton!
@IBOutlet weak var postButton: UIButton!
@IBOutlet weak var contentWarningTextField: UITextField!
@IBOutlet weak var mediaStackView: UIStackView!
@IBOutlet weak var paddingView: UIView!
@IBOutlet weak var progressView: SteppedProgressView!
var scrolled = false
var inReplyToID: String?
// TODO: cleanup this
var mentioningAcct: String?
var text: String?
var accountsToMention: [String]
var initialText: String?
var contentWarningEnabled = false {
didSet {
contentWarningStateChanged()
}
}
var visibility: Status.Visibility! {
didSet {
visibilityChanged()
}
}
var selectedAssets: [PHAsset] = [] {
didSet {
updateAttachmentViews()
}
}
var draft: DraftsManager.Draft?
var currentDraft: DraftsManager.Draft?
// Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released
weak var xcbSession: XCBSession?
var postedStatus: Status?
var contentWarning = false {
didSet {
contentWarningTextField.isHidden = !contentWarning
}
}
var visibility = Preferences.shared.defaultPostVisibility {
didSet {
visibilityButton.setTitle(visibility.displayName, for: .normal)
}
}
weak var postBarButtonItem: UIBarButtonItem!
var visibilityBarButtonItem: UIBarButtonItem!
var contentWarningBarButtonItem: UIBarButtonItem!
var status: Status?
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var contentView: UIView!
@IBOutlet weak var stackView: UIStackView!
var replyView: ComposeStatusReplyView?
var replyAvatarImageViewTopConstraint: NSLayoutConstraint?
@IBOutlet weak var selfDetailView: LargeAccountDetailView!
@IBOutlet weak var charactersRemainingLabel: UILabel!
@IBOutlet weak var statusTextView: UITextView!
@IBOutlet weak var placeholderLabel: UILabel!
@IBOutlet weak var contentWarningContainerView: UIView!
@IBOutlet weak var contentWarningTextField: UITextField!
@IBOutlet weak var attachmentsStackView: UIStackView!
@IBOutlet weak var addAttachmentButton: UIButton!
@IBOutlet weak var postProgressView: SteppedProgressView!
init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, router: AppRouter) {
self.inReplyToID = inReplyToID
self.mentioningAcct = mentioningAcct
self.text = text
self.router = router
self.inReplyToID = inReplyToID
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
accountsToMention = [inReplyTo.account.acct] + inReplyTo.mentions.map { $0.acct }
} else if let mentioningAcct = mentioningAcct {
accountsToMention = [mentioningAcct]
} else {
accountsToMention = []
}
if let ownAccount = MastodonController.account {
accountsToMention.removeAll(where: { acct in ownAccount.acct == acct })
}
accountsToMention = accountsToMention.uniques()
super.init(nibName: "ComposeViewController", bundle: nil)
title = "Compose"
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsPressed))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .done, target: self, action: #selector(postButtonPressed))
postBarButtonItem = navigationItem.rightBarButtonItem
}
required init?(coder aDecoder: NSCoder) {
@ -79,97 +98,141 @@ class ComposeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
statusTextView.layer.cornerRadius = 5
statusTextView.layer.masksToBounds = true
scrollView.delegate = self
statusTextView.delegate = self
visibilityButton.setTitle(visibility.displayName, for: .normal)
contentWarningTextField.delegate = self
statusTextView.becomeFirstResponder()
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30))
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(keyboardDoneButtonPressed))
toolbar.setItems([flexSpace, done], animated: false)
toolbar.sizeToFit()
let toolbar = UIToolbar()
contentWarningBarButtonItem = UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(contentWarningButtonPressed))
visibilityBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(visbilityButtonPressed))
toolbar.items = [
contentWarningBarButtonItem,
visibilityBarButtonItem,
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
] + createFormattingButtons() + [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPressed))
]
toolbar.translatesAutoresizingMaskIntoConstraints = false
statusTextView.inputAccessoryView = toolbar
contentWarningTextField.inputAccessoryView = toolbar
if let inReplyToID = inReplyToID,
let inReplyTo = MastodonCache.status(for: inReplyToID) {
inReplyToDisplayNameLabel.text = inReplyTo.account.realDisplayName
inReplyToUsernameLabel.text = "@\(inReplyTo.account.username)"
inReplyToContentLabel.statusID = inReplyToID
inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView)
inReplyToAvatarImageView.layer.masksToBounds = true
inReplyToAvatarImageView.image = nil
ImageCache.avatars.get(inReplyTo.account.avatar) { (data) in
guard let data = data else { return }
DispatchQueue.main.async {
self.inReplyToAvatarImageView.image = UIImage(data: data)
}
statusTextView.text = accountsToMention.map({ acct in "@\(acct) " }).joined()
initialText = statusTextView.text
MastodonController.getOwnAccount { (account) in
DispatchQueue.main.async {
self.selfDetailView.update(account: account)
}
inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
if inReplyTo.account != MastodonController.account {
statusTextView.text = "@\(inReplyTo.account.acct) "
}
statusTextView.text += inReplyTo.mentions.filter({ $0.id != MastodonController.account.id }).map({ "@\($0.acct) " }).joined()
contentWarning = inReplyTo.sensitive
contentWarningTextField.text = inReplyTo.spoilerText
}
if let inReplyToID = inReplyToID, let inReplyTo = MastodonCache.status(for: inReplyToID) {
visibility = inReplyTo.visibility
} else {
inReplyToLabel.isHidden = true
inReplyToContainerView.isHidden = true
}
if let mentioningAcct = mentioningAcct {
statusTextView.text += "@\(mentioningAcct) "
}
if let text = text {
statusTextView.text += text
}
let replyView = ComposeStatusReplyView.create()
replyView.updateUI(for: inReplyTo)
stackView.insertArrangedSubview(replyView, at: 0)
self.replyView = replyView
initialText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
replyAvatarImageViewTopConstraint = replyView.avatarImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8)
replyAvatarImageViewTopConstraint!.isActive = true
let replyLabelContainer = UIView()
replyLabelContainer.translatesAutoresizingMaskIntoConstraints = false
let replyLabel = UILabel()
replyLabel.translatesAutoresizingMaskIntoConstraints = false
replyLabel.text = "In reply to \(inReplyTo.account.realDisplayName)"
replyLabel.textColor = .darkGray
replyLabelContainer.addSubview(replyLabel)
NSLayoutConstraint.activate([
replyLabel.leadingAnchor.constraint(equalTo: replyLabelContainer.leadingAnchor, constant: 8),
replyLabel.trailingAnchor.constraint(equalTo: replyLabelContainer.trailingAnchor, constant: -8),
replyLabel.topAnchor.constraint(equalTo: replyLabelContainer.topAnchor),
replyLabel.bottomAnchor.constraint(equalTo: replyLabelContainer.bottomAnchor)
])
stackView.insertArrangedSubview(replyLabelContainer, at: 1)
}
updateCharactersRemaining()
updatePlaceholder()
progressView.progress = 0
contentWarningEnabled = false
NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField)
if let mentioningAcct = mentioningAcct {
let req = MastodonController.client.searchForAccount(query: mentioningAcct)
MastodonController.client.run(req) { [weak self] (response) in
if case let .success(accounts, _) = response {
self?.userActivity = UserActivityManager.newPostActivity(mentioning: accounts.first)
} else {
self?.userActivity = UserActivityManager.newPostActivity()
}
}
} else {
self.userActivity = UserActivityManager.newPostActivity()
if inReplyToID == nil {
visibility = Preferences.shared.defaultPostVisibility
}
}
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
override func viewDidLayoutSubviews() {
if inReplyToID != nil && !scrolled {
scrollView.contentOffset = CGPoint(x: 0, y: inReplyToContainerView.bounds.height - 44)
scrolled = true
super.viewDidLayoutSubviews()
// if inReplyToID != nil {
// scrollView.contentOffset = CGPoint(x: 0, y: stackView.arrangedSubviews.first!.frame.height)
// }
}
func createFormattingButtons() -> [UIBarButtonItem] {
guard Preferences.shared.statusContentType != .plain else {
return []
}
return StatusFormat.allCases.map { (format) in
let (str, attributes) = format.title
let item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
item.setTitleTextAttributes(attributes, for: .normal)
item.setTitleTextAttributes(attributes, for: .highlighted)
item.tag = StatusFormat.allCases.firstIndex(of: format)!
return item
}
}
func addMedia(for image: UIImage) {
let mediaView = ComposeMediaView(image: image)
mediaView.delegate = self
mediaStackView.addArrangedSubview(mediaView)
@objc func adjustForKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
scrollView.contentInset = .zero
} else {
// let accessoryFrame = view.convert(statusTextView.inputAccessoryView!.frame, from: view.window)
let offset = keyboardViewEndFrame.height// + accessoryFrame.height
// TODO: radar for incorrect keyboard end frame height (either converted or screen)
// the value returned is somewhere between the height of the keyboard and the height of the keyboard + accessory
// actually maybe not??
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: offset, right: 0)
}
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
func updateCharactersRemaining() {
// TODO: include CW char count
let count = CharacterCounter.count(text: statusTextView.text)
let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count
let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0
let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount
if remaining < 0 {
charactersRemainingLabel.textColor = .red
postButton.isEnabled = false
postBarButtonItem.isEnabled = false
} else {
charactersRemainingLabel.textColor = .darkGray
postButton.isEnabled = true
postBarButtonItem.isEnabled = true
}
charactersRemainingLabel.text = remaining.description
}
@ -178,147 +241,65 @@ class ComposeViewController: UIViewController {
placeholderLabel.isHidden = !statusTextView.text.isEmpty
}
// MARK: - Navigation
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
statusTextView.resignFirstResponder()
contentWarningTextField.resignFirstResponder()
super.dismiss(animated: flag, completion: completion)
func updateAddAttachmentButton() {
addAttachmentButton.isEnabled = selectedAssets.count < 5 // 4 attachments + 1 button
}
// MARK: - Interaction
@IBAction func visibilityPressed(_ sender: Any) {
let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in
guard let visibility = visibility else { return }
UIView.performWithoutAnimation {
self.visibility = visibility
self.visibilityButton.layoutIfNeeded()
func updateAttachmentViews() {
for view in attachmentsStackView.arrangedSubviews {
if view is ComposeMediaView {
view.removeFromSuperview()
}
}
present(alertController, animated: true)
}
@IBAction func contentWarningPressed(_ sender: Any) {
contentWarning = !contentWarning
}
@IBAction func mediaPressed(_ sender: Any) {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
let alertController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet)
if UIImagePickerController.isSourceTypeAvailable(.camera) {
alertController.addAction(UIAlertAction(title: "Camera", style: .default, handler: { _ in
imagePicker.sourceType = .camera
self.present(imagePicker, animated: true)
}))
for asset in selectedAssets {
let mediaView = ComposeMediaView.create()
mediaView.delegate = self
mediaView.update(asset: asset)
attachmentsStackView.insertArrangedSubview(mediaView, at: attachmentsStackView.arrangedSubviews.count - 1)
updateAddAttachmentButton()
}
if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
alertController.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { __ in
imagePicker.sourceType = .photoLibrary
self.present(imagePicker, animated: true)
}))
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true)
}
@IBAction func postPressed(_ sender: Any) {
guard let text = statusTextView.text,
!text.isEmpty else { return }
postButton.isEnabled = false
let contentWarning: String?
if self.contentWarning,
let text = contentWarningTextField.text,
!text.isEmpty {
contentWarning = text
func contentWarningStateChanged() {
contentWarningContainerView.isHidden = !contentWarningEnabled
contentWarningBarButtonItem.style = contentWarningEnabled ? .done : .plain
if contentWarningEnabled {
contentWarningTextField.becomeFirstResponder()
} else {
contentWarning = nil
}
let sensitive = contentWarning != nil
let visibility = self.visibility
var attachments: [Attachment?] = []
let group = DispatchGroup()
for view in mediaStackView.arrangedSubviews {
guard let mediaView = view as? ComposeMediaView,
let image = mediaView.image,
let data = image.pngData() else { continue }
let index = attachments.count
attachments.append(nil)
progressView.steps += 1
group.enter()
let request = MastodonController.client.upload(attachment: FormAttachment(pngData: data), description: mediaView.mediaDescription)
MastodonController.client.run(request) { response in
guard case let .success(attachment, _) = response else { fatalError() }
attachments[index] = attachment
self.progressView.step()
group.leave()
}
}
progressView.steps = 2 + attachments.count
progressView.currentStep = 1
group.notify(queue: .main) {
let attachments = attachments.compactMap { $0 }
let request = MastodonController.client.createStatus(text: text,
inReplyTo: self.inReplyToID,
media: attachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: visibility)
MastodonController.client.run(request) { response in
guard case let .success(status, _) = response else { fatalError() }
self.status = status
MastodonCache.add(status: status)
if let draft = self.draft {
DraftsManager.shared.remove(draft)
}
DispatchQueue.main.async {
self.progressView.step()
self.dismiss(animated: true)
// TODO: reorganize routing/navigation
(((self.presentingViewController as! MainTabBarViewController).selectedViewController as! UINavigationController).topViewController as! TuskerNavigationDelegate).selected(status: status.id)
self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString,
"statusURI": status.uri
])
}
}
statusTextView.becomeFirstResponder()
}
}
@objc func imagePressed(_ gesture: UITapGestureRecognizer) {
gesture.view!.superview!.removeFromSuperview()
}
@objc func keyboardDoneButtonPressed() {
statusTextView.endEditing(false)
func visibilityChanged() {
// TODO: update visiblity button image
}
func saveDraft() {
if let draft = draft {
draft.update(text: statusTextView.text)
// TODO: save attachmenst to drafts
// TODO: save CW to draft
if let currentDraft = currentDraft {
currentDraft.update(text: statusTextView.text)
} else {
DraftsManager.shared.create(text: statusTextView.text)
}
}
func close() {
@objc func close() {
dismiss(animated: true)
xcbSession?.complete(with: .cancel)
}
@objc func cancelPressed() {
// MARK: - Navigation
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
statusTextView.resignFirstResponder()
super.dismiss(animated: flag, completion: completion)
}
// MARK: - Interaction
@objc func cancelButtonPressed() {
guard statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) != initialText else {
close()
return
@ -336,28 +317,177 @@ class ComposeViewController: UIViewController {
self.close()
}))
alert.addAction(UIAlertAction(title: "Delete draft", style: .destructive, handler: { (_) in
if let draft = self.draft {
DraftsManager.shared.remove(draft)
if let currentDraft = self.currentDraft {
DraftsManager.shared.remove(currentDraft)
}
self.close()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
router.present(alert, animated: true)
}
@objc func draftsPressed() {
let drafts = router.drafts()
drafts.delegate = self
router.present(UINavigationController(rootViewController: drafts), animated: true)
@objc func contentWarningButtonPressed() {
contentWarningEnabled = !contentWarningEnabled
}
@objc func contentWarningTextFieldDidChange() {
updateCharactersRemaining()
}
@objc func visbilityButtonPressed() {
let alertController = UIAlertController(currentVisibility: self.visibility) { (visibility) in
guard let visibility = visibility else { return }
self.visibility = visibility
}
present(alertController, animated: true)
}
@objc func formatButtonPressed(_ button: UIBarButtonItem) {
guard statusTextView.isFirstResponder else {
return
}
let format = StatusFormat.allCases[button.tag]
guard let insertionResult = format.insertionResult else {
return
}
let currentSelectedRange = statusTextView.selectedRange
if currentSelectedRange.length == 0 {
statusTextView.insertText(insertionResult.prefix + insertionResult.suffix)
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
} else {
let start = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.lowerBound)
let end = statusTextView.text.index(statusTextView.text.startIndex, offsetBy: currentSelectedRange.upperBound)
let selectedText = statusTextView.text[start..<end]
statusTextView.insertText(String(insertionResult.prefix + selectedText + insertionResult.suffix))
statusTextView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.prefix.count, length: currentSelectedRange.length)
}
}
@objc func draftsButtonPressed() {
let draftsVC = router.drafts()
draftsVC.delegate = self
router.present(UINavigationController(rootViewController: draftsVC), animated: true)
}
@IBAction func addAttachmentPressed(_ sender: Any) {
let picker = GMImagePickerController()
picker.delegate = self
picker.toolbarTintColor = view.tintColor
picker.navigationBarTintColor = view.tintColor
picker.title = "Choose Attachment"
present(picker, animated: true)
}
@objc func postButtonPressed() {
guard let text = statusTextView.text,
!text.isEmpty else { return }
// disable post button while sending post request
postBarButtonItem.isEnabled = false
let contentWarning: String?
if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty {
contentWarning = cwText
} else {
contentWarning = nil
}
let sensitive = contentWarning != nil
let visibility = self.visibility!
let group = DispatchGroup()
var attachments: [Attachment?] = []
for asset in selectedAssets {
let index = attachments.count
attachments.append(nil)
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
let description = mediaView.descriptionTextView.text
group.enter()
let options = PHImageRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
options.resizeMode = .none
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageData(for: asset, options: options) { (data, dataUTI, orientation, info) in
guard let data = data, let dataUTI = dataUTI else { fatalError() }
let mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
self.postProgressView.step()
let request = MastodonController.client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
MastodonController.client.run(request) { (response) in
guard case let .success(attachment, _) = response else { fatalError() }
attachments[index] = attachment
self.postProgressView.step()
group.leave()
}
}
}
postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment
postProgressView.currentStep = 1
group.notify(queue: .main) {
let attachments = attachments.compactMap { $0 }
let request = MastodonController.client.createStatus(text: text,
contentType: Preferences.shared.statusContentType,
inReplyTo: self.inReplyToID,
media: attachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: visibility,
language: nil)
MastodonController.client.run(request) { (response) in
guard case let .success(status, _) = response else { fatalError() }
self.postedStatus = status
MastodonCache.add(status: status)
if let draft = self.currentDraft {
DraftsManager.shared.remove(draft)
}
DispatchQueue.main.async {
self.postProgressView.step()
self.dismiss(animated: true)
let conversationVC = self.router.conversation(for: status.id)
self.router.push(conversationVC, animated: true)
self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString,
"statusURI": status.uri
])
}
}
}
}
}
extension ComposeViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(false)
return true
extension ComposeViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let replyView = replyView else { return }
var constant: CGFloat = 8
if scrollView.contentOffset.y < 0 {
constant -= scrollView.contentOffset.y
replyAvatarImageViewTopConstraint?.constant = 8 - scrollView.contentOffset.y
} else if scrollView.contentOffset.y > replyView.frame.height - replyView.avatarImageView.frame.height - 16 {
constant += replyView.frame.height - replyView.avatarImageView.frame.height - 16 - scrollView.contentOffset.y
}
replyAvatarImageViewTopConstraint?.constant = constant
}
}
@ -368,27 +498,23 @@ extension ComposeViewController: UITextViewDelegate {
}
}
extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
addMedia(for: selectedImage)
dismiss(animated: true)
}
extension ComposeViewController: GMImagePickerControllerDelegate {
func assetsPickerController(_ picker: GMImagePickerController!, didFinishPickingAssets assets: [Any]!) {
let assets = assets as! [PHAsset]
selectedAssets.append(contentsOf: assets)
picker.dismiss(animated: true)
}
func assetsPickerController(_ picker: GMImagePickerController!, shouldSelect asset: PHAsset!) -> Bool {
return selectedAssets.count + picker.selectedAssets.count < 4
}
}
extension ComposeViewController: ComposeMediaViewDelegate {
func editDescription(for media: ComposeMediaView) {
let alertController = UIAlertController(title: "Media Description", message: nil, preferredStyle: .alert)
alertController.addTextField { textField in
textField.text = media.mediaDescription
}
alertController.addAction(UIAlertAction(title: "Confirm", style: .default, handler: { _ in
let description = alertController.textFields![0].text
media.mediaDescription = description
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true)
func didRemoveMedia(_ mediaView: ComposeMediaView) {
let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)!
selectedAssets.remove(at: index)
updateAddAttachmentButton()
}
}
@ -397,8 +523,9 @@ extension ComposeViewController: DraftsTableViewControllerDelegate {
}
func draftSelected(_ draft: DraftsManager.Draft) {
self.draft = draft
self.currentDraft = draft
statusTextView.text = draft.text
updatePlaceholder()
updateCharactersRemaining()
}
}

View File

@ -1,33 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.23.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.16.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ComposeViewController" customModule="Tusker" customModuleProvider="target">
<connections>
<outlet property="charactersRemainingLabel" destination="3Ti-cT-JnH" id="hhy-k7-jBN"/>
<outlet property="contentWarningTextField" destination="FA4-UR-Pc9" id="UnZ-6T-Phn"/>
<outlet property="inReplyToAvatarImageView" destination="cOi-cF-kIg" id="bCo-zq-NYI"/>
<outlet property="inReplyToContainerView" destination="YTO-vO-WZ5" id="gsc-Qh-Roi"/>
<outlet property="inReplyToContentLabel" destination="a2k-Ax-jJd" id="wSz-ga-J2W"/>
<outlet property="inReplyToDisplayNameLabel" destination="77P-er-bd4" id="GrH-Cu-Sqw"/>
<outlet property="inReplyToLabel" destination="lZc-Z2-0vV" id="KSM-Kh-lgR"/>
<outlet property="inReplyToUsernameLabel" destination="ZNM-ey-3j8" id="hMp-DJ-7kB"/>
<outlet property="mediaStackView" destination="wHz-h7-939" id="2gr-Vs-ymW"/>
<outlet property="paddingView" destination="YyO-d2-4bx" id="wPp-TI-5Zc"/>
<outlet property="placeholderLabel" destination="blO-2d-WiC" id="cG4-Gt-c4T"/>
<outlet property="postButton" destination="i4l-LR-1kp" id="BmN-Zr-OcQ"/>
<outlet property="progressView" destination="Ncd-80-ldI" id="ZNz-kk-Rjl"/>
<outlet property="scrollView" destination="cp4-lI-aME" id="dqM-XD-7EF"/>
<outlet property="statusTextView" destination="5Yb-vo-0yN" id="r6k-fK-U5F"/>
<outlet property="addAttachmentButton" destination="eEV-Yt-Njk" id="o9g-pP-dtd"/>
<outlet property="attachmentsStackView" destination="P0F-3w-gI1" id="Bi5-EK-N3a"/>
<outlet property="charactersRemainingLabel" destination="PMB-Wa-Ht0" id="PN9-wr-Pzu"/>
<outlet property="contentView" destination="pcX-rB-RxJ" id="o95-Qa-6N7"/>
<outlet property="contentWarningContainerView" destination="kU2-7l-MSy" id="Gnq-Jb-kCA"/>
<outlet property="contentWarningTextField" destination="T05-p6-vTz" id="Ivu-Ll-ByO"/>
<outlet property="placeholderLabel" destination="EW3-YK-vPC" id="Rsw-Nv-TNz"/>
<outlet property="postProgressView" destination="Tq7-6P-hMT" id="amT-F1-JI0"/>
<outlet property="scrollView" destination="6Z0-Vy-hMX" id="ya0-2T-QaV"/>
<outlet property="selfDetailView" destination="zZ3-Gv-4P5" id="jou-Vl-TQE"/>
<outlet property="stackView" destination="bOB-hF-O9w" id="lD7-b2-MWl"/>
<outlet property="statusTextView" destination="9pn-0T-IHb" id="u7j-KW-zCo"/>
<outlet property="view" destination="7XG-Dk-OGm" id="09I-sr-hnP"/>
<outlet property="visibilityButton" destination="6CJ-b2-876" id="jkz-te-T3u"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
@ -35,166 +31,179 @@
<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" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="cp4-lI-aME">
<rect key="frame" x="0.0" y="-44" width="375" height="711"/>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" translatesAutoresizingMaskIntoConstraints="NO" id="6Z0-Vy-hMX">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="ovj-HJ-0pD">
<rect key="frame" x="16" y="16" width="343" height="779"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pcX-rB-RxJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="342"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YTO-vO-WZ5">
<rect key="frame" x="0.0" y="0.0" width="343" height="128"/>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bOB-hF-O9w">
<rect key="frame" x="0.0" y="0.0" width="375" height="342"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="cOi-cF-kIg">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="IMp-xZ-1wk"/>
<constraint firstAttribute="height" constant="50" id="p8i-9U-qdf"/>
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="77P-er-bd4">
<rect key="frame" x="58" y="0.0" width="105" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZNM-ey-3j8">
<rect key="frame" x="171" y="0.0" width="172" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a2k-Ax-jJd" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="58" y="28.5" width="285" height="99.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="a2k-Ax-jJd" firstAttribute="top" secondItem="77P-er-bd4" secondAttribute="bottom" constant="8" id="6oq-wI-C5d"/>
<constraint firstAttribute="trailing" secondItem="ZNM-ey-3j8" secondAttribute="trailing" id="9v0-fI-wi5"/>
<constraint firstItem="cOi-cF-kIg" firstAttribute="top" secondItem="YTO-vO-WZ5" secondAttribute="top" id="Fnv-I3-PLN"/>
<constraint firstItem="cOi-cF-kIg" firstAttribute="leading" secondItem="YTO-vO-WZ5" secondAttribute="leading" id="J0g-iO-Hsy"/>
<constraint firstItem="77P-er-bd4" firstAttribute="top" secondItem="YTO-vO-WZ5" secondAttribute="top" id="anu-L8-kpt"/>
<constraint firstAttribute="trailing" secondItem="a2k-Ax-jJd" secondAttribute="trailing" id="cIX-qN-Bnr"/>
<constraint firstAttribute="bottom" secondItem="a2k-Ax-jJd" secondAttribute="bottom" id="cYK-IY-C6V"/>
<constraint firstItem="ZNM-ey-3j8" firstAttribute="leading" secondItem="77P-er-bd4" secondAttribute="trailing" constant="8" id="dV5-U5-sJL"/>
<constraint firstItem="77P-er-bd4" firstAttribute="leading" secondItem="cOi-cF-kIg" secondAttribute="trailing" constant="8" id="hva-Nf-90R"/>
<constraint firstItem="ZNM-ey-3j8" firstAttribute="top" secondItem="YTO-vO-WZ5" secondAttribute="top" id="oET-4l-974"/>
<constraint firstItem="a2k-Ax-jJd" firstAttribute="leading" secondItem="cOi-cF-kIg" secondAttribute="trailing" constant="8" id="rt2-Lx-cgH"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="dpq-un-p7Y">
<rect key="frame" x="0.0" y="136" width="343" height="643"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="In reply to Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lZc-Z2-0vV">
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="5Yb-vo-0yN">
<rect key="frame" x="0.0" y="26.5" width="343" height="200"/>
<color key="backgroundColor" red="0.8980392157" green="0.8980392157" blue="0.91764705879999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="URa-6h-Hbt"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HTy-eW-OvQ">
<rect key="frame" x="0.0" y="232.5" width="343" height="30"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6V0-mH-Mhu">
<rect key="frame" x="0.0" y="0.0" width="375" height="66"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pZv-DT-CdW">
<rect key="frame" x="0.0" y="0.0" width="42" height="30"/>
<state key="normal" title="Media"/>
<connections>
<action selector="mediaPressed:" destination="-1" eventType="touchUpInside" id="7Uf-WL-RnQ"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="H1c-9o-wdb">
<rect key="frame" x="50" y="0.0" width="30" height="30"/>
<state key="normal" title="CW"/>
<connections>
<action selector="contentWarningPressed:" destination="-1" eventType="touchUpInside" id="MrZ-09-UDE"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6CJ-b2-876">
<rect key="frame" x="88" y="0.0" width="42" height="30"/>
<state key="normal" title="Public"/>
<connections>
<action selector="visibilityPressed:" destination="-1" eventType="touchUpInside" id="rg9-0L-AZ9"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="251" text="500" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Ti-cT-JnH">
<rect key="frame" x="138" y="0.0" width="166" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<view contentMode="scaleToFill" placeholderIntrinsicWidth="infinite" placeholderIntrinsicHeight="66" translatesAutoresizingMaskIntoConstraints="NO" id="zZ3-Gv-4P5" customClass="LargeAccountDetailView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="336" height="66"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="500" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PMB-Wa-Ht0">
<rect key="frame" x="336" y="8" width="31" 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"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="i4l-LR-1kp">
<rect key="frame" x="312" y="0.0" width="31" height="30"/>
<state key="normal" title="Post"/>
<connections>
<action selector="postPressed:" destination="-1" eventType="touchUpInside" id="KKB-fz-Xra"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="PMB-Wa-Ht0" secondAttribute="trailing" constant="8" id="5KJ-rz-Heh"/>
<constraint firstItem="zZ3-Gv-4P5" firstAttribute="leading" secondItem="6V0-mH-Mhu" secondAttribute="leading" id="f6Q-fK-zq1"/>
<constraint firstItem="zZ3-Gv-4P5" firstAttribute="top" secondItem="6V0-mH-Mhu" secondAttribute="top" id="fjf-mn-l9f"/>
<constraint firstItem="PMB-Wa-Ht0" firstAttribute="top" secondItem="6V0-mH-Mhu" secondAttribute="top" constant="8" id="q3V-aY-t9K"/>
<constraint firstAttribute="bottom" secondItem="zZ3-Gv-4P5" secondAttribute="bottom" id="rOO-0n-odM"/>
<constraint firstItem="PMB-Wa-Ht0" firstAttribute="leading" secondItem="zZ3-Gv-4P5" secondAttribute="trailing" id="sVv-tH-7eB"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kU2-7l-MSy">
<rect key="frame" x="0.0" y="66" width="375" height="46"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Write your warning here" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="T05-p6-vTz">
<rect key="frame" x="4" y="8" width="367" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="yzY-MF-Ukx"/>
</constraints>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<textInputTraits key="textInputTraits"/>
</textField>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="T05-p6-vTz" secondAttribute="trailing" constant="4" id="8tG-eW-TG4"/>
<constraint firstAttribute="bottom" secondItem="T05-p6-vTz" secondAttribute="bottom" constant="8" id="SUL-Hk-uvM"/>
<constraint firstItem="T05-p6-vTz" firstAttribute="leading" secondItem="kU2-7l-MSy" secondAttribute="leading" constant="4" id="WGG-B2-lPC"/>
<constraint firstItem="T05-p6-vTz" firstAttribute="top" secondItem="kU2-7l-MSy" secondAttribute="top" constant="8" id="lvW-S0-4k4"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lhQ-ae-pe9">
<rect key="frame" x="0.0" y="112" width="375" height="150"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="9pn-0T-IHb">
<rect key="frame" x="4" y="0.0" width="367" height="150"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="150" id="ISI-jm-FxV"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="What's on your mind?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EW3-YK-vPC">
<rect key="frame" x="8" y="8" width="188" height="24"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="9pn-0T-IHb" secondAttribute="bottom" id="UAs-fL-Riv"/>
<constraint firstItem="9pn-0T-IHb" firstAttribute="leading" secondItem="lhQ-ae-pe9" secondAttribute="leading" constant="4" id="ezI-15-Yd4"/>
<constraint firstItem="9pn-0T-IHb" firstAttribute="top" secondItem="lhQ-ae-pe9" secondAttribute="top" id="n8v-pK-I9E"/>
<constraint firstItem="EW3-YK-vPC" firstAttribute="leading" secondItem="9pn-0T-IHb" secondAttribute="leading" constant="4" id="n9v-mJ-gz3"/>
<constraint firstItem="EW3-YK-vPC" firstAttribute="top" secondItem="9pn-0T-IHb" secondAttribute="top" constant="8" id="q5e-yM-bS4"/>
<constraint firstAttribute="trailing" secondItem="9pn-0T-IHb" secondAttribute="trailing" constant="4" id="x7Z-8w-xgm"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="P0F-3w-gI1">
<rect key="frame" x="0.0" y="262" width="375" height="80"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="752-dD-eAO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Aqk-LY-jEj">
<rect key="frame" x="0.0" y="0.0" width="375" height="1"/>
<color key="backgroundColor" red="0.90978598590000004" green="0.90964758400000001" blue="0.91777461770000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="0C7-KP-bIQ"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cVn-xc-LH9">
<rect key="frame" x="0.0" y="79" width="375" height="1"/>
<color key="backgroundColor" red="0.90978598590000004" green="0.90964758400000001" blue="0.91777461770000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="1" id="SZ4-5b-Hcf"/>
<constraint firstAttribute="height" constant="1" id="VIz-vl-Um4"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eEV-Yt-Njk">
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
<constraints>
<constraint firstAttribute="height" constant="80" id="sGZ-uD-CtS"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title=" Add image or video" image="More">
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="addAttachmentPressed:" destination="-1" eventType="touchUpInside" id="aUR-nx-O9u"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Aqk-LY-jEj" firstAttribute="top" secondItem="752-dD-eAO" secondAttribute="top" id="6i1-Jt-AEM"/>
<constraint firstAttribute="bottom" secondItem="eEV-Yt-Njk" secondAttribute="bottom" id="C6D-yq-PU3"/>
<constraint firstItem="Aqk-LY-jEj" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" id="Y5a-qr-Dby"/>
<constraint firstAttribute="bottom" secondItem="cVn-xc-LH9" secondAttribute="bottom" id="dCy-ov-086"/>
<constraint firstItem="eEV-Yt-Njk" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" constant="8" id="enN-pq-hxK"/>
<constraint firstAttribute="trailing" secondItem="Aqk-LY-jEj" secondAttribute="trailing" id="h1Q-QT-wB9"/>
<constraint firstItem="cVn-xc-LH9" firstAttribute="leading" secondItem="752-dD-eAO" secondAttribute="leading" id="oNI-gt-O9v"/>
<constraint firstAttribute="trailing" secondItem="eEV-Yt-Njk" secondAttribute="trailing" constant="8" id="qe1-4r-oaa"/>
<constraint firstItem="eEV-Yt-Njk" firstAttribute="top" secondItem="752-dD-eAO" secondAttribute="top" id="rpc-rE-Q57"/>
<constraint firstAttribute="trailing" secondItem="cVn-xc-LH9" secondAttribute="trailing" id="uSI-lv-mqY"/>
</constraints>
</view>
</subviews>
</stackView>
<textField hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Content Warning" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="FA4-UR-Pc9">
<rect key="frame" x="0.0" y="265.5" width="343" height="0.0"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
</textField>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="wHz-h7-939">
<rect key="frame" x="0.0" y="268.5" width="343" height="100"/>
<constraints>
<constraint firstAttribute="height" constant="100" id="0qA-Te-SOn"/>
</constraints>
</stackView>
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="YyO-d2-4bx">
<rect key="frame" x="0.0" y="374.5" width="343" height="268.5"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
</stackView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="What is on your mind?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="blO-2d-WiC">
<rect key="frame" x="20.5" y="186.5" width="170" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="bOB-hF-O9w" secondAttribute="trailing" id="GAR-qc-jte"/>
<constraint firstAttribute="height" secondItem="bOB-hF-O9w" secondAttribute="height" id="KO2-zF-s7P"/>
<constraint firstItem="bOB-hF-O9w" firstAttribute="top" secondItem="pcX-rB-RxJ" secondAttribute="top" id="aBm-Ub-TpI"/>
<constraint firstItem="bOB-hF-O9w" firstAttribute="leading" secondItem="pcX-rB-RxJ" secondAttribute="leading" id="yOt-hH-L57"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="ovj-HJ-0pD" secondAttribute="bottom" id="GqL-42-cue"/>
<constraint firstItem="ovj-HJ-0pD" firstAttribute="leading" secondItem="cp4-lI-aME" secondAttribute="leading" constant="16" id="I7Y-uR-bf0"/>
<constraint firstItem="blO-2d-WiC" firstAttribute="top" secondItem="5Yb-vo-0yN" secondAttribute="top" constant="8" id="Wfi-2X-213"/>
<constraint firstItem="ovj-HJ-0pD" firstAttribute="top" secondItem="cp4-lI-aME" secondAttribute="top" constant="16" id="bq1-LT-laf"/>
<constraint firstItem="blO-2d-WiC" firstAttribute="leading" secondItem="5Yb-vo-0yN" secondAttribute="leading" constant="4.5" id="dhw-kL-D2B"/>
<constraint firstAttribute="trailing" secondItem="ovj-HJ-0pD" secondAttribute="trailing" constant="16" id="hJy-JL-iae"/>
<constraint firstItem="dpq-un-p7Y" firstAttribute="height" secondItem="cp4-lI-aME" secondAttribute="height" constant="-68" id="oE5-6c-QYI"/>
<constraint firstItem="pcX-rB-RxJ" firstAttribute="leading" secondItem="6Z0-Vy-hMX" secondAttribute="leading" id="X0f-ja-XBF"/>
<constraint firstAttribute="bottom" secondItem="pcX-rB-RxJ" secondAttribute="bottom" id="X8a-PA-r8F"/>
<constraint firstAttribute="trailing" secondItem="pcX-rB-RxJ" secondAttribute="trailing" id="lh7-xn-MGp"/>
<constraint firstItem="pcX-rB-RxJ" firstAttribute="top" secondItem="6Z0-Vy-hMX" secondAttribute="top" id="yMM-IS-8K1"/>
</constraints>
</scrollView>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Ncd-80-ldI" customClass="SteppedProgressView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="2.5"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Tq7-6P-hMT" customClass="SteppedProgressView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="2"/>
<color key="trackTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</progressView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ncd-80-ldI" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="0eq-md-iJK"/>
<constraint firstItem="ovj-HJ-0pD" firstAttribute="width" secondItem="7XG-Dk-OGm" secondAttribute="width" constant="-32" id="0sw-Bf-MLK"/>
<constraint firstItem="cp4-lI-aME" firstAttribute="top" secondItem="7XG-Dk-OGm" secondAttribute="topMargin" constant="-64" id="7ef-so-AaW"/>
<constraint firstItem="cp4-lI-aME" firstAttribute="leading" secondItem="7XG-Dk-OGm" secondAttribute="leading" id="8fs-On-3th"/>
<constraint firstItem="Heg-g4-sYM" firstAttribute="bottom" secondItem="cp4-lI-aME" secondAttribute="bottom" id="S1A-db-SQZ"/>
<constraint firstItem="Ncd-80-ldI" firstAttribute="leading" secondItem="7XG-Dk-OGm" secondAttribute="leading" id="Wy2-V4-Jkp"/>
<constraint firstAttribute="trailing" secondItem="cp4-lI-aME" secondAttribute="trailing" id="afV-LD-XZS"/>
<constraint firstAttribute="trailing" secondItem="Ncd-80-ldI" secondAttribute="trailing" id="k6I-L5-xZc"/>
<constraint firstAttribute="trailing" secondItem="Tq7-6P-hMT" secondAttribute="trailing" id="GeN-8q-weq"/>
<constraint firstItem="Heg-g4-sYM" firstAttribute="bottom" secondItem="6Z0-Vy-hMX" secondAttribute="bottom" id="Hf3-Cc-mVX"/>
<constraint firstItem="Tq7-6P-hMT" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="LgA-xu-VGE"/>
<constraint firstItem="Tq7-6P-hMT" firstAttribute="leading" secondItem="7XG-Dk-OGm" secondAttribute="leading" id="agM-ZO-c3E"/>
<constraint firstItem="Heg-g4-sYM" firstAttribute="trailing" secondItem="6Z0-Vy-hMX" secondAttribute="trailing" id="hjY-W6-wTQ"/>
<constraint firstItem="bOB-hF-O9w" firstAttribute="width" secondItem="7XG-Dk-OGm" secondAttribute="width" id="i0p-NE-ca1"/>
<constraint firstItem="6Z0-Vy-hMX" firstAttribute="leading" secondItem="Heg-g4-sYM" secondAttribute="leading" id="lFF-yC-ql9"/>
<constraint firstItem="6Z0-Vy-hMX" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="osv-zq-seP"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Heg-g4-sYM"/>
</view>
</objects>
<resources>
<image name="More" width="60" height="30"/>
</resources>
</document>

View File

@ -0,0 +1,74 @@
//
// StatusFormat.swift
// Tusker
//
// Created by Shadowfacts on 1/12/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
enum StatusFormat: CaseIterable {
case italics, bold, strikethrough, code
var insertionResult: FormatInsertionResult? {
switch Preferences.shared.statusContentType {
case .plain:
return nil
case .markdown:
return Markdown.format(self)
case .html:
return HTML.format(self)
}
}
var title: (String, [NSAttributedString.Key: Any]) {
switch self {
case .italics:
return ("I", [.font: UIFont.italicSystemFont(ofSize: 17)])
case .bold:
return ("B", [.font: UIFont.boldSystemFont(ofSize: 17)])
case .strikethrough:
return ("S", [.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)])
case .code:
return ("</>", [.font: UIFont(name: "Menlo", size: 17)!])
}
}
}
typealias FormatInsertionResult = (prefix: String, suffix: String, insertionPoint: Int)
protocol FormatType {
static func format(_ format: StatusFormat) -> FormatInsertionResult
}
extension StatusFormat {
struct Markdown: FormatType {
static var formats: [StatusFormat: String] = [
.italics: "_",
.bold: "**",
.strikethrough: "~~",
.code: "`"
]
static func format(_ format: StatusFormat) -> FormatInsertionResult {
let str = formats[format]!
return (str, str, str.count)
}
}
struct HTML: FormatType {
static var tags: [StatusFormat: String] = [
.italics: "em",
.bold: "strong",
.strikethrough: "del",
.code: "code"
]
static func format(_ format: StatusFormat) -> FormatInsertionResult {
let tag = tags[format]!
return ("<\(tag)>", "</\(tag)>", tag.count + 2)
}
}
}

View File

@ -0,0 +1,56 @@
//
// AdvancedTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 1/12/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class AdvancedTableViewController: UITableViewController {
@IBOutlet weak var postContentTypeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
postContentTypeLabel.text = Preferences.shared.statusContentType.displayName
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard indexPath.section == 1 && indexPath.row == 0 else {
return
}
tableView.deselectRow(at: indexPath, animated: true)
let alertController = UIAlertController(title: "Post Content Type", message: nil, preferredStyle: .actionSheet)
for contentType in StatusContentType.allCases {
let action = UIAlertAction(title: contentType.displayName, style: .default) { (_) in
Preferences.shared.statusContentType = contentType
self.postContentTypeLabel.text = contentType.displayName
}
if contentType == Preferences.shared.statusContentType {
action.setValue(true, forKey: "checked")
}
alertController.addAction(action)
}
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alertController, animated: true)
}
}
extension StatusContentType {
var displayName: String {
switch self {
case .plain:
return "Plain"
case .markdown:
return "Markdown"
case .html:
return "HTML"
}
}
}

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="VJJ-jC-9g8">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="VJJ-jC-9g8">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -311,7 +311,7 @@
<!--Advanced-->
<scene sceneID="xgj-Fx-53j">
<objects>
<tableViewController id="3mv-l7-6We" sceneMemberID="viewController">
<tableViewController id="3mv-l7-6We" customClass="AdvancedTableViewController" customModule="Tusker" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="hcR-ju-th0">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -344,6 +344,39 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="aNt-ow-hHp">
<string key="footerTitle">This option is only supported for Pleroma instances with formatting enabled. On all other instances, formatting symbols will remain in the plain, unformatted text.</string>
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="L56-bm-5R5">
<rect key="frame" x="0.0" y="135.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="L56-bm-5R5" id="mtr-Qa-CSI">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Post Content Type" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ThO-WW-QsH">
<rect key="frame" x="16" y="11.5" width="142" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Plain" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uq5-Fi-mpp">
<rect key="frame" x="322" y="11.5" width="37" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="uq5-Fi-mpp" firstAttribute="centerY" secondItem="mtr-Qa-CSI" secondAttribute="centerY" id="BMn-9K-DXz"/>
<constraint firstItem="ThO-WW-QsH" firstAttribute="centerY" secondItem="mtr-Qa-CSI" secondAttribute="centerY" id="p7P-du-z4q"/>
<constraint firstItem="ThO-WW-QsH" firstAttribute="leading" secondItem="mtr-Qa-CSI" secondAttribute="leadingMargin" id="upT-aD-0JW"/>
<constraint firstItem="uq5-Fi-mpp" firstAttribute="trailing" secondItem="mtr-Qa-CSI" secondAttribute="trailingMargin" id="vW4-hc-eeu"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="3mv-l7-6We" id="Ots-vG-253"/>
@ -351,6 +384,9 @@
</connections>
</tableView>
<navigationItem key="navigationItem" title="Advanced" id="Rav-Eq-k22"/>
<connections>
<outlet property="postContentTypeLabel" destination="uq5-Fi-mpp" id="LN1-20-FMA"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4wC-Hp-AZk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>

View File

@ -0,0 +1,69 @@
//
// LargeAccountDetailViewController.swift
// Tusker
//
// Created by Shadowfacts on 1/6/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class LargeAccountDetailView: UIView, PreferencesAdaptive {
var avatarImageView = UIImageView()
var displayNameLabel = UILabel()
var usernameLabel = UILabel()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
avatarImageView.layer.masksToBounds = true
addSubview(avatarImageView)
displayNameLabel.translatesAutoresizingMaskIntoConstraints = false
displayNameLabel.font = .systemFont(ofSize: 20, weight: .semibold)
addSubview(displayNameLabel)
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.font = .systemFont(ofSize: 17, weight: .light)
usernameLabel.textColor = .darkGray
addSubview(usernameLabel)
NSLayoutConstraint.activate([
avatarImageView.heightAnchor.constraint(equalToConstant: 50),
avatarImageView.widthAnchor.constraint(equalToConstant: 50),
avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
displayNameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
displayNameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
usernameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
usernameLabel.topAnchor.constraint(equalTo: displayNameLabel.bottomAnchor)
])
}
override func layoutSubviews() {
super.layoutSubviews()
updateUIForPreferences()
}
func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
}
func update(account: Account) {
displayNameLabel.text = account.realDisplayName
usernameLabel.text = "@\(account.acct)"
ImageCache.avatars.get(account.avatar) { (data) in
guard let data = data else { return }
DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data)
}
}
}
}

View File

@ -0,0 +1,57 @@
//
// ComposeAttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 1/10/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Photos
protocol ComposeMediaViewDelegate {
func didRemoveMedia(_ mediaView: ComposeMediaView)
}
class ComposeMediaView: UIView {
var delegate: ComposeMediaViewDelegate?
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var descriptionTextView: UITextView!
@IBOutlet weak var placeholderLabel: UILabel!
static func create() -> ComposeMediaView {
return UINib(nibName: "ComposeMediaView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeMediaView
}
override func awakeFromNib() {
super.awakeFromNib()
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 10 // 0.1 * imageView.frame.width
descriptionTextView.delegate = self
}
func update(asset: PHAsset) {
// let size = imageView.frame.size // is this initialized yet?
let size = CGSize(width: 80, height: 80)
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
self.imageView.image = image
}
}
// MARK: - Interaction
@IBAction func removePressed(_ sender: Any) {
delegate?.didRemoveMedia(self)
}
}
extension ComposeMediaView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
}
}

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ComposeMediaView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="u7I-sx-kUe">
<rect key="frame" x="8" y="293.5" width="80" height="80"/>
<constraints>
<constraint firstAttribute="height" constant="80" id="CgF-eC-We6"/>
<constraint firstAttribute="width" constant="80" id="S3g-yM-TRb"/>
</constraints>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G1g-Lw-Ren">
<rect key="frame" x="345" y="322.5" width="22" height="22"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="dyG-5Y-91s"/>
<constraint firstAttribute="width" constant="22" id="sWQ-3z-Z5r"/>
</constraints>
<state key="normal" image="Remove"/>
<connections>
<action selector="removePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="aor-Cq-YjJ"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="O6b-Zs-u8r">
<rect key="frame" x="96" y="20" width="241" height="647"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="GsE-uM-fhe"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Describe for the visually impaired..." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rkD-NP-09H">
<rect key="frame" x="100" y="28" width="233" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0h4-wv-2R8">
<rect key="frame" x="0.0" y="20" width="375" height="1"/>
<color key="backgroundColor" red="0.90978598594665527" green="0.90964758396148682" blue="0.91777461767196655" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="aQa-2T-uYY"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="O6b-Zs-u8r" secondAttribute="bottom" id="3sv-wo-gxe"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="5Qs-i7-glv"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="top" relation="greaterThanOrEqual" secondItem="0h4-wv-2R8" secondAttribute="bottom" constant="8" id="86L-Cb-Lsk"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="9QY-MR-Yc2"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0h4-wv-2R8" secondAttribute="trailing" id="FCS-un-JT6"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="UWq-Lf-zGB"/>
<constraint firstItem="rkD-NP-09H" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="leading" constant="4" id="aQd-T9-n1I"/>
<constraint firstItem="0h4-wv-2R8" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="edp-4a-YGq"/>
<constraint firstItem="G1g-Lw-Ren" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="hJA-JF-MHp"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="leading" secondItem="u7I-sx-kUe" secondAttribute="trailing" constant="8" id="hvZ-m4-TOV"/>
<constraint firstItem="G1g-Lw-Ren" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="trailing" constant="8" id="ith-0X-3Yz"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="u7I-sx-kUe" secondAttribute="bottom" constant="8" id="lGN-Qg-mBO"/>
<constraint firstItem="rkD-NP-09H" firstAttribute="top" secondItem="O6b-Zs-u8r" secondAttribute="top" constant="8" id="mRb-3M-7uz"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="G1g-Lw-Ren" secondAttribute="trailing" constant="8" id="o4F-MV-ahd"/>
<constraint firstItem="0h4-wv-2R8" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="sFo-wp-MYP"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="trailing" secondItem="rkD-NP-09H" secondAttribute="trailing" constant="4" id="vwg-7l-8ca"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="descriptionTextView" destination="O6b-Zs-u8r" id="TNy-7h-0sY"/>
<outlet property="imageView" destination="u7I-sx-kUe" id="o3a-O0-m85"/>
<outlet property="placeholderLabel" destination="rkD-NP-09H" id="WtV-2h-L3n"/>
</connections>
<point key="canvasLocation" x="136.80000000000001" y="142.57871064467767"/>
</view>
</objects>
<resources>
<image name="Remove" width="81" height="81"/>
</resources>
</document>

View File

@ -0,0 +1,46 @@
//
// ComposeStatusReplyView.swift
// Tusker
//
// Created by Shadowfacts on 1/6/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class ComposeStatusReplyView: UIView, PreferencesAdaptive {
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentLabel: StatusContentLabel!
static func create() -> ComposeStatusReplyView {
return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView
}
override func awakeFromNib() {
super.awakeFromNib()
updateUIForPreferences()
}
func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
}
func updateUI(for status: Status) {
displayNameLabel.text = status.account.realDisplayName
usernameLabel.text = "@\(status.account.acct)"
contentLabel.statusID = status.id
ImageCache.avatars.get(status.account.avatar) { (data) in
guard let data = data else { return }
DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data)
}
}
}
}

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ComposeStatusReplyView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ypn-Ed-MTq">
<rect key="frame" x="8" y="28" width="50" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="8qi-gl-5ci"/>
<constraint firstAttribute="width" constant="50" id="Dy2-jh-AJj"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2cE-sS-Uut">
<rect key="frame" x="66" y="28" width="301" height="631"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sdv-dB-Plm">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0yZ-71-eTj">
<rect key="frame" x="115" y="0.0" width="178" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OEF-Hj-v3f" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="24.5" width="301" height="606.5"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="6v5-7p-9gm"/>
<constraint firstAttribute="bottom" secondItem="OEF-Hj-v3f" secondAttribute="bottom" id="IEQ-Ab-tsP"/>
<constraint firstItem="OEF-Hj-v3f" firstAttribute="top" secondItem="Sdv-dB-Plm" secondAttribute="bottom" constant="4" id="J5s-TU-odB"/>
<constraint firstItem="Sdv-dB-Plm" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="YmP-yU-sfe"/>
<constraint firstItem="OEF-Hj-v3f" firstAttribute="leading" secondItem="2cE-sS-Uut" secondAttribute="leading" id="bbW-07-e2x"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="top" secondItem="2cE-sS-Uut" secondAttribute="top" id="bdX-ge-bMT"/>
<constraint firstAttribute="trailing" secondItem="0yZ-71-eTj" secondAttribute="trailing" constant="8" id="hU7-aZ-ibI"/>
<constraint firstItem="0yZ-71-eTj" firstAttribute="leading" secondItem="Sdv-dB-Plm" secondAttribute="trailing" constant="8" id="m0X-YU-m3V"/>
<constraint firstAttribute="trailing" secondItem="OEF-Hj-v3f" secondAttribute="trailing" id="xqX-4X-lJl"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="2cE-sS-Uut" firstAttribute="height" relation="greaterThanOrEqual" secondItem="Ypn-Ed-MTq" secondAttribute="height" id="Fn3-o4-RGx"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="2cE-sS-Uut" secondAttribute="bottom" constant="8" id="G2d-Kz-c4e"/>
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="MbW-9d-3gC"/>
<constraint firstItem="2cE-sS-Uut" firstAttribute="leading" secondItem="Ypn-Ed-MTq" secondAttribute="trailing" constant="8" id="TS2-Sr-PB3"/>
<constraint firstItem="2cE-sS-Uut" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="cat-Cr-PSV"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="2cE-sS-Uut" secondAttribute="trailing" constant="8" id="eH4-lG-5UR"/>
<constraint firstItem="Ypn-Ed-MTq" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" placeholder="YES" id="xCn-8G-jUZ"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="avatarImageView" destination="Ypn-Ed-MTq" id="eea-bc-klc"/>
<outlet property="contentLabel" destination="OEF-Hj-v3f" id="GBI-ib-5T0"/>
<outlet property="displayNameLabel" destination="Sdv-dB-Plm" id="RxW-Ra-Ups"/>
<outlet property="usernameLabel" destination="0yZ-71-eTj" id="VQm-Dq-3zP"/>
</connections>
<point key="canvasLocation" x="138.40000000000001" y="-72.863568215892059"/>
</view>
</objects>
</document>

View File

@ -1,57 +0,0 @@
//
// ComposeAttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 8/30/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
protocol ComposeMediaViewDelegate {
func editDescription(for media: ComposeMediaView)
}
class ComposeMediaView: UIImageView {
var delegate: ComposeMediaViewDelegate?
var remove: UIImageView
var mediaDescription: String?
required init?(coder aDecoder: NSCoder) {
return nil
}
init(image: UIImage) {
remove = UIImageView(image: UIImage(named: "Remove"))
super.init(image: image)
contentMode = .scaleAspectFill
layer.cornerRadius = 5
layer.masksToBounds = true
isUserInteractionEnabled = true
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
remove.isUserInteractionEnabled = true
remove.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(removePressed)))
remove.translatesAutoresizingMaskIntoConstraints = false
addSubview(remove)
remove.widthAnchor.constraint(equalToConstant: 20).isActive = true
remove.heightAnchor.constraint(equalToConstant: 20).isActive = true
remove.topAnchor.constraint(equalTo: topAnchor, constant: 5).isActive = true
remove.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true
}
@objc func removePressed() {
removeFromSuperview()
}
@objc func longPressed() {
delegate?.editDescription(for: self)
}
}