forked from zelo72/mastodon-ios
feat: implement boost toot. Add stacked style avatar
This commit is contained in:
parent
2ac2eb7c77
commit
1256ef1d8e
|
@ -149,6 +149,7 @@
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||||
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */; };
|
||||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
||||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||||
|
@ -396,6 +397,7 @@
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||||
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerView.swift; sourceTree = "<group>"; };
|
||||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -1168,6 +1170,7 @@
|
||||||
children = (
|
children = (
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
||||||
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerView.swift */,
|
||||||
);
|
);
|
||||||
path = Container;
|
path = Container;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1684,6 +1687,7 @@
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerView.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||||
|
|
|
@ -86,13 +86,23 @@ extension StatusSection {
|
||||||
return L10n.Common.Controls.Status.userBoosted(name)
|
return L10n.Common.Controls.Status.userBoosted(name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// set name username avatar
|
// set name username
|
||||||
cell.statusView.nameLabel.text = {
|
cell.statusView.nameLabel.text = {
|
||||||
let author = (toot.reblog ?? toot).author
|
let author = (toot.reblog ?? toot).author
|
||||||
return author.displayName.isEmpty ? author.username : author.displayName
|
return author.displayName.isEmpty ? author.username : author.displayName
|
||||||
}()
|
}()
|
||||||
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL()))
|
// set avatar
|
||||||
|
if let reblog = toot.reblog {
|
||||||
|
cell.statusView.avatarButton.isHidden = true
|
||||||
|
cell.statusView.avatarStackedContainerButton.isHidden = false
|
||||||
|
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
|
||||||
|
cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
||||||
|
} else {
|
||||||
|
cell.statusView.avatarButton.isHidden = false
|
||||||
|
cell.statusView.avatarStackedContainerButton.isHidden = true
|
||||||
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL()))
|
||||||
|
}
|
||||||
|
|
||||||
// set text
|
// set text
|
||||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
||||||
|
|
|
@ -23,7 +23,13 @@ extension AvatarConfigurableView {
|
||||||
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
public func configure(with configuration: AvatarConfigurableViewConfiguration) {
|
||||||
let placeholderImage: UIImage = {
|
let placeholderImage: UIImage = {
|
||||||
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
let placeholderImage = configuration.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageSize, color: .systemFill)
|
||||||
return placeholderImage.af.imageRoundedIntoCircle()
|
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
|
||||||
|
return placeholderImage
|
||||||
|
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
|
||||||
|
.af.imageRounded(withCornerRadius: 4, divideRadiusByImageScale: true)
|
||||||
|
} else {
|
||||||
|
return placeholderImage.af.imageRoundedIntoCircle()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// cancel previous task
|
// cancel previous task
|
||||||
|
@ -65,7 +71,8 @@ extension AvatarConfigurableView {
|
||||||
)
|
)
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
avatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||||
avatarImageView.layer.cornerCurve = .circular
|
avatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
|
||||||
|
|
||||||
default:
|
default:
|
||||||
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
||||||
avatarImageView.af.setImage(
|
avatarImageView.af.setImage(
|
||||||
|
@ -92,7 +99,7 @@ extension AvatarConfigurableView {
|
||||||
)
|
)
|
||||||
avatarButton.layer.masksToBounds = true
|
avatarButton.layer.masksToBounds = true
|
||||||
avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
avatarButton.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||||
avatarButton.layer.cornerCurve = .continuous
|
avatarButton.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous : .circular
|
||||||
default:
|
default:
|
||||||
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
||||||
avatarButton.af.setImage(
|
avatarButton.af.setImage(
|
||||||
|
|
|
@ -45,10 +45,18 @@ extension HomeTimelineViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToTopGapAction(action)
|
self.moveToTopGapAction(action)
|
||||||
}),
|
}),
|
||||||
|
UIAction(title: "First Reblog Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.moveToFirstReblogToot(action)
|
||||||
|
}),
|
||||||
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
|
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.moveToFirstPollToot(action)
|
self.moveToFirstPollToot(action)
|
||||||
}),
|
}),
|
||||||
|
UIAction(title: "First Audio Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.moveToFirstAudioToot(action)
|
||||||
|
}),
|
||||||
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
|
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
// guard let self = self else { return }
|
// guard let self = self else { return }
|
||||||
// self.moveToFirstReplyToot(action)
|
// self.moveToFirstReplyToot(action)
|
||||||
|
@ -101,6 +109,26 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func moveToFirstReblogToot(_ sender: UIAction) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
|
return homeTimelineIndex.toot.reblog != nil
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||||
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
|
} else {
|
||||||
|
print("Not found reblog toot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func moveToFirstPollToot(_ sender: UIAction) {
|
@objc private func moveToFirstPollToot(_ sender: UIAction) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
@ -122,6 +150,27 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func moveToFirstAudioToot(_ sender: UIAction) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||||
|
switch item {
|
||||||
|
case .homeTimelineIndex(let objectID, _):
|
||||||
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
|
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
||||||
|
return toot.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||||
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
|
} else {
|
||||||
|
print("Not found audio toot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
|
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
//
|
||||||
|
// AvatarStackContainerView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AvatarStackedImageView: UIImageView { }
|
||||||
|
|
||||||
|
// MARK: - AvatarConfigurableView
|
||||||
|
extension AvatarStackedImageView: AvatarConfigurableView {
|
||||||
|
static var configurableAvatarImageSize: CGSize { CGSize(width: 28, height: 28) }
|
||||||
|
static var configurableAvatarImageCornerRadius: CGFloat { 4 }
|
||||||
|
var configurableAvatarImageView: UIImageView? { self }
|
||||||
|
var configurableAvatarButton: UIButton? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIControl.State: Hashable { }
|
||||||
|
|
||||||
|
final class AvatarStackContainerButton: UIControl {
|
||||||
|
|
||||||
|
static let containerSize = CGSize(width: 42, height: 42)
|
||||||
|
static let maskOffset: CGFloat = 2
|
||||||
|
|
||||||
|
// UIControl.Event - Application: 0x0F000000
|
||||||
|
static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000
|
||||||
|
var primaryActionState: UIControl.State = .normal
|
||||||
|
|
||||||
|
let topLeadingAvatarStackedImageView = AvatarStackedImageView()
|
||||||
|
let bottomTrailingAvatarStackedImageView = AvatarStackedImageView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AvatarStackContainerButton {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(topLeadingAvatarStackedImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topLeadingAvatarStackedImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
topLeadingAvatarStackedImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
topLeadingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh),
|
||||||
|
topLeadingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
bottomTrailingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(bottomTrailingAvatarStackedImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomTrailingAvatarStackedImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
bottomTrailingAvatarStackedImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
bottomTrailingAvatarStackedImageView.widthAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.width).priority(.defaultHigh),
|
||||||
|
bottomTrailingAvatarStackedImageView.heightAnchor.constraint(equalToConstant: AvatarStackedImageView.configurableAvatarImageSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
// mask topLeadingAvatarStackedImageView
|
||||||
|
let offset: CGFloat = 2
|
||||||
|
let path: CGPath = {
|
||||||
|
let path = CGMutablePath()
|
||||||
|
path.addRect(CGRect(origin: .zero, size: AvatarStackContainerButton.containerSize))
|
||||||
|
if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft {
|
||||||
|
path.addPath(UIBezierPath(
|
||||||
|
roundedRect: CGRect(
|
||||||
|
x: AvatarStackedImageView.configurableAvatarImageSize.width + offset,
|
||||||
|
y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset,
|
||||||
|
width: AvatarStackedImageView.configurableAvatarImageSize.width,
|
||||||
|
height: AvatarStackedImageView.configurableAvatarImageSize.height
|
||||||
|
),
|
||||||
|
cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius
|
||||||
|
).cgPath)
|
||||||
|
} else {
|
||||||
|
path.addPath(UIBezierPath(
|
||||||
|
roundedRect: CGRect(
|
||||||
|
x: AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset,
|
||||||
|
y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset,
|
||||||
|
width: AvatarStackedImageView.configurableAvatarImageSize.width,
|
||||||
|
height: AvatarStackedImageView.configurableAvatarImageSize.height
|
||||||
|
),
|
||||||
|
cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius
|
||||||
|
).cgPath)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}()
|
||||||
|
let maskShapeLayer = CAShapeLayer()
|
||||||
|
maskShapeLayer.backgroundColor = UIColor.black.cgColor
|
||||||
|
maskShapeLayer.fillRule = .evenOdd
|
||||||
|
maskShapeLayer.path = path
|
||||||
|
topLeadingAvatarStackedImageView.layer.mask = maskShapeLayer
|
||||||
|
|
||||||
|
topLeadingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
|
||||||
|
bottomTrailingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return AvatarStackContainerButton.containerSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
updateState(touch: touch, event: event)
|
||||||
|
return super.beginTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
updateState(touch: touch, event: event)
|
||||||
|
return super.continueTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
resetState()
|
||||||
|
|
||||||
|
if let touch = touch {
|
||||||
|
if AvatarStackContainerButton.isTouching(touch, view: self, event: event) {
|
||||||
|
sendActions(for: AvatarStackContainerButton.primaryAction)
|
||||||
|
} else {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.endTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cancelTracking(with event: UIEvent?) {
|
||||||
|
defer { updateAppearance() }
|
||||||
|
|
||||||
|
resetState()
|
||||||
|
super.cancelTracking(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AvatarStackContainerButton {
|
||||||
|
|
||||||
|
private func updateAppearance() {
|
||||||
|
topLeadingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
|
||||||
|
bottomTrailingAvatarStackedImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isTouching(_ touch: UITouch, view: UIView, event: UIEvent?) -> Bool {
|
||||||
|
let location = touch.location(in: view)
|
||||||
|
return view.point(inside: location, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetState() {
|
||||||
|
primaryActionState = .normal
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateState(touch: UITouch, event: UIEvent?) {
|
||||||
|
primaryActionState = AvatarStackContainerButton.isTouching(touch, view: self, event: event) ? .highlighted : .normal
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AvatarStackContainerButton_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 42) {
|
||||||
|
let avatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
avatarStackContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackContainerButton.widthAnchor.constraint(equalToConstant: 42),
|
||||||
|
avatarStackContainerButton.heightAnchor.constraint(equalToConstant: 42),
|
||||||
|
])
|
||||||
|
return avatarStackContainerButton
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 42, height: 42))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -60,6 +60,7 @@ final class StatusView: UIView {
|
||||||
button.setImage(placeholderImage, for: .normal)
|
button.setImage(placeholderImage, for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
|
||||||
let nameLabel: UILabel = {
|
let nameLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
@ -238,6 +239,14 @@ extension StatusView {
|
||||||
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarView.addSubview(avatarStackedContainerButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackedContainerButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
||||||
|
avatarStackedContainerButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
||||||
|
avatarStackedContainerButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
|
avatarStackedContainerButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
// author meta container: [title container | subtitle container]
|
// author meta container: [title container | subtitle container]
|
||||||
let authorMetaContainerStackView = UIStackView()
|
let authorMetaContainerStackView = UIStackView()
|
||||||
|
@ -360,6 +369,7 @@ extension StatusView {
|
||||||
pollStatusStackView.isHidden = true
|
pollStatusStackView.isHidden = true
|
||||||
audioView.isHidden = true
|
audioView.isHidden = true
|
||||||
|
|
||||||
|
avatarStackedContainerButton.isHidden = true
|
||||||
contentWarningBlurContentImageView.isHidden = true
|
contentWarningBlurContentImageView.isHidden = true
|
||||||
statusContentWarningContainerStackView.isHidden = true
|
statusContentWarningContainerStackView.isHidden = true
|
||||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||||
|
@ -429,6 +439,7 @@ import SwiftUI
|
||||||
struct StatusView_Previews: PreviewProvider {
|
struct StatusView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static let avatarFlora = UIImage(named: "tiraya-adam")
|
static let avatarFlora = UIImage(named: "tiraya-adam")
|
||||||
|
static let avatarMarkus = UIImage(named: "markus-spiske")
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
|
@ -443,6 +454,49 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
return statusView
|
return statusView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("Normal")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let statusView = StatusView()
|
||||||
|
statusView.headerContainerStackView.isHidden = false
|
||||||
|
statusView.avatarButton.isHidden = true
|
||||||
|
statusView.avatarStackedContainerButton.isHidden = false
|
||||||
|
statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarFlora
|
||||||
|
)
|
||||||
|
)
|
||||||
|
statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarMarkus
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return statusView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("Boost")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
||||||
|
statusView.configure(
|
||||||
|
with: AvatarConfigurableViewConfiguration(
|
||||||
|
avatarImageURL: nil,
|
||||||
|
placeholderImage: avatarFlora
|
||||||
|
)
|
||||||
|
)
|
||||||
|
statusView.headerContainerStackView.isHidden = false
|
||||||
|
let images = MosaicImageView_Previews.images
|
||||||
|
let imageViews = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
imageView.image = images[i]
|
||||||
|
}
|
||||||
|
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||||
|
statusView.statusMosaicImageViewContainer.blurVisualEffectView.isHidden = true
|
||||||
|
statusView.isStatusTextSensitive = false
|
||||||
|
return statusView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 380))
|
||||||
|
.previewDisplayName("Image Meida")
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
let statusView = StatusView(frame: CGRect(x: 0, y: 0, width: 375, height: 500))
|
||||||
statusView.configure(
|
statusView.configure(
|
||||||
|
@ -466,6 +520,7 @@ struct StatusView_Previews: PreviewProvider {
|
||||||
return statusView
|
return statusView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 380))
|
.previewLayout(.fixed(width: 375, height: 380))
|
||||||
|
.previewDisplayName("Content Sensitive")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue