feat: add poll UI/UX for compose scene

This commit is contained in:
CMK 2021-03-23 18:47:21 +08:00
parent 93fe9ce30c
commit b8e062c92e
23 changed files with 1077 additions and 251 deletions

View File

@ -187,6 +187,10 @@
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; };
DB87D4512609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */; };
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */; };
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -481,6 +485,10 @@
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusNewPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBackwardResponseTextField.swift; sourceTree = "<group>"; };
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -665,6 +673,7 @@
2D152A8B25C295CC009AA50C /* StatusView.swift */,
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -752,7 +761,6 @@
isa = PBXGroup;
children = (
DB45FB0425CA87B4005A8AC7 /* APIService */,
DB49A61925FF327D00B98345 /* EmojiService */,
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
@ -843,6 +851,7 @@
DB9D6C1325E4F97A0051B173 /* Container */,
DBA9B90325F1D4420012E7B6 /* Control */,
2D152A8A25C295B8009AA50C /* Content */,
DB87D45C2609DE6600D12C0D /* TextField */,
DB1D187125EF5BBD003F1F23 /* TableView */,
2D7631A625C1533800929FB9 /* TableviewCell */,
);
@ -1153,10 +1162,20 @@
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */,
);
path = CollectionViewCell;
sourceTree = "<group>";
};
DB87D45C2609DE6600D12C0D /* TextField */ = {
isa = PBXGroup;
children = (
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */,
);
path = TextField;
sourceTree = "<group>";
};
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
isa = PBXGroup;
children = (
@ -1792,6 +1811,7 @@
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
@ -1818,6 +1838,7 @@
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
@ -1868,6 +1889,7 @@
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
@ -1931,6 +1953,7 @@
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,

View File

@ -8,6 +8,7 @@
import Foundation
import MastodonSDK
/// Note: update Equatable when change case
enum CategoryPickerItem {
case all
case category(category: Mastodon.Entity.Category)

View File

@ -9,12 +9,17 @@ import Foundation
import Combine
import CoreData
/// Note: update Equatable when change case
enum ComposeStatusItem {
case replyTo(statusObjectID: NSManagedObjectID)
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
case attachment(attachmentService: MastodonAttachmentService)
case poll(attribute: ComposePollAttribute)
case newPoll
}
extension ComposeStatusItem: Equatable { }
extension ComposeStatusItem: Hashable { }
extension ComposeStatusItem {
@ -38,3 +43,20 @@ extension ComposeStatusItem {
}
}
}
extension ComposeStatusItem {
final class ComposePollAttribute: Equatable, Hashable {
private let id = UUID()
let option = CurrentValueSubject<String, Never>("")
static func == (lhs: ComposePollAttribute, rhs: ComposePollAttribute) -> Bool {
return lhs.id == rhs.id &&
lhs.option.value == rhs.option.value
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}

View File

@ -8,6 +8,7 @@
import Foundation
import CoreData
/// Note: update Equatable when change case
enum PollItem {
case opion(objectID: NSManagedObjectID, attribute: Attribute)
}

View File

@ -16,6 +16,7 @@ enum ComposeStatusSection: Equatable, Hashable {
case repliedTo
case status
case attachment
case poll
}
extension ComposeStatusSection {
@ -33,7 +34,9 @@ extension ComposeStatusSection {
managedObjectContext: NSManagedObjectContext,
composeKind: ComposeKind,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusNewPollOptionCollectionViewCellDelegate
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
switch item {
@ -54,11 +57,11 @@ extension ComposeStatusSection {
}
ComposeStatusSection.configure(cell: cell, attribute: attribute)
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
// self size input cell
cell.composeContent
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { text in
// self size input cell
collectionView.collectionViewLayout.invalidateLayout()
// bind input data
attribute.composeContent.value = text
@ -124,6 +127,19 @@ extension ComposeStatusSection {
}
.store(in: &cell.disposeBag)
return cell
case .poll(let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
cell.pollOptionView.optionTextField.text = attribute.option.value
cell.pollOption
.receive(on: DispatchQueue.main)
.assign(to: \.value, on: attribute.option)
.store(in: &cell.disposeBag)
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
return cell
case .newPoll:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusNewPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusNewPollOptionCollectionViewCell
cell.delegate = composeStatusNewPollOptionCollectionViewCellDelegate
return cell
}
}
}

View File

@ -38,7 +38,7 @@ extension PollSection {
pollOption option: PollOption,
pollItemAttribute attribute: PollItem.Attribute
) {
cell.optionLabel.text = option.title
cell.pollOptionView.optionTextField.text = option.title
configure(cell: cell, selectState: attribute.selectState)
configure(cell: cell, voteState: attribute.voteState)
cell.attribute = attribute
@ -52,35 +52,35 @@ extension PollSection {
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
switch state {
case .none:
cell.checkmarkBackgroundView.isHidden = true
cell.checkmarkImageView.isHidden = true
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
cell.pollOptionView.checkmarkImageView.isHidden = true
case .off:
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
cell.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
cell.checkmarkBackgroundView.layer.borderWidth = 1
cell.checkmarkBackgroundView.isHidden = false
cell.checkmarkImageView.isHidden = true
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
cell.pollOptionView.checkmarkImageView.isHidden = true
case .on:
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
cell.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
cell.checkmarkBackgroundView.layer.borderWidth = 0
cell.checkmarkBackgroundView.isHidden = false
cell.checkmarkImageView.isHidden = false
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
cell.pollOptionView.checkmarkImageView.isHidden = false
}
}
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
switch state {
case .hidden:
cell.optionPercentageLabel.isHidden = true
cell.voteProgressStripView.isHidden = true
cell.voteProgressStripView.setProgress(0.0, animated: false)
cell.pollOptionView.optionPercentageLabel.isHidden = true
cell.pollOptionView.voteProgressStripView.isHidden = true
cell.pollOptionView.voteProgressStripView.setProgress(0.0, animated: false)
case .reveal(let voted, let percentage, let animated):
cell.optionPercentageLabel.isHidden = false
cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
cell.voteProgressStripView.isHidden = false
cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
cell.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
cell.pollOptionView.optionPercentageLabel.isHidden = false
cell.pollOptionView.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
cell.pollOptionView.voteProgressStripView.isHidden = false
cell.pollOptionView.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
cell.pollOptionView.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
}
}

View File

@ -27,6 +27,7 @@ internal enum Asset {
}
internal enum Circles {
internal static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill")
internal static let plusCircle = ImageAsset(name: "Circles/plus.circle")
}
internal enum Colors {
internal enum Background {
@ -46,6 +47,7 @@ internal enum Asset {
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
}
internal enum Button {
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "plus.circle.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -0,0 +1,95 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
21.999905 0.000160 m
34.035152 0.000160 44.000000 9.986404 44.000000 22.000080 c
44.000000 34.035332 34.013599 44.000000 21.978350 44.000000 c
9.964656 44.000000 0.000000 34.035332 0.000000 22.000080 c
0.000000 9.986404 9.986255 0.000160 21.999905 0.000160 c
h
21.999905 3.666824 m
11.819542 3.666824 3.688203 11.819717 3.688203 22.000080 c
3.688203 32.180443 11.797986 40.333336 21.978350 40.333336 c
32.158710 40.333336 40.311611 32.180443 40.333256 22.000080 c
40.354897 11.819717 32.180267 3.666824 21.999905 3.666824 c
h
13.782296 20.188307 m
20.166574 20.188307 l
20.166574 13.760918 l
20.166574 12.682493 20.899923 11.949142 21.956793 11.949142 c
23.035217 11.949142 23.790121 12.682493 23.790121 13.760918 c
23.790121 20.188307 l
30.217514 20.188307 l
31.295938 20.188307 32.029289 20.921658 32.029289 21.978525 c
32.029289 23.056950 31.295938 23.811855 30.217514 23.811855 c
23.790121 23.811855 l
23.790121 30.196133 l
23.790121 31.317715 23.035217 32.051018 21.956793 32.051018 c
20.899923 32.051018 20.166574 31.296114 20.166574 30.196133 c
20.166574 23.811855 l
13.782296 23.811855 l
12.660716 23.811855 11.927410 23.056950 11.927410 21.978525 c
11.927410 20.921658 12.682316 20.188307 13.782296 20.188307 c
h
f
n
Q
endstream
endobj
3 0 obj
1347
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 44.000000 44.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001437 00000 n
0000001460 00000 n
0000001633 00000 n
0000001707 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1766
%%EOF

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "255",
"red" : "255"
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.169",
"green" : "0.141",
"red" : "0.125"
"blue" : "0x2B",
"green" : "0x23",
"red" : "0x1F"
}
},
"idiom" : "universal"

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "232",
"green" : "225",
"red" : "217"
"blue" : "0xE8",
"green" : "0xE1",
"red" : "0xD9"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.169",
"green" : "0.141",
"red" : "0.125"
"blue" : "0x2B",
"green" : "0x23",
"red" : "0x1F"
}
},
"idiom" : "universal"

View File

@ -5,9 +5,27 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x43",
"green" : "0x36",
"red" : "0x32"
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2B",
"green" : "0x23",
"red" : "0x1F"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xE8",
"green" : "0xE1",
"red" : "0xD9"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2B",
"green" : "0x23",
"red" : "0x1F"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -66,7 +66,7 @@ extension ComposeStatusContentCollectionViewCell {
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 10),
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
])
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)

View File

@ -0,0 +1,120 @@
//
// ComposeStatusNewPollOptionCollectionViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-23.
//
import os.log
import UIKit
protocol ComposeStatusNewPollOptionCollectionViewCellDelegate: class {
func ComposeStatusNewPollOptionCollectionViewCellDidPressed(_ cell: ComposeStatusNewPollOptionCollectionViewCell)
}
final class ComposeStatusNewPollOptionCollectionViewCell: UICollectionViewCell {
let pollOptionView = PollOptionView()
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
override var isHighlighted: Bool {
didSet {
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color
pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Button.normal.color.withAlphaComponent(0.5) : Asset.Colors.Button.normal.color
}
}
weak var delegate: ComposeStatusNewPollOptionCollectionViewCellDelegate?
override func prepareForReuse() {
super.prepareForReuse()
delegate = nil
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeStatusNewPollOptionCollectionViewCell {
private func _init() {
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(pollOptionView)
NSLayoutConstraint.activate([
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
pollOptionView.checkmarkImageView.isHidden = true
pollOptionView.checkmarkBackgroundView.isHidden = true
pollOptionView.optionPercentageLabel.isHidden = true
pollOptionView.optionTextField.isHidden = true
pollOptionView.plusCircleImageView.isHidden = false
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemBackground.color
setupBorderColor()
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusNewPollOptionCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
}
private func setupBorderColor() {
pollOptionView.roundedBackgroundView.layer.borderWidth = 1
pollOptionView.roundedBackgroundView.layer.borderColor = Asset.Colors.Background.secondarySystemBackground.color.cgColor
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setupBorderColor()
}
}
extension ComposeStatusNewPollOptionCollectionViewCell {
@objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.ComposeStatusNewPollOptionCollectionViewCellDidPressed(self)
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct ComposeStatusNewPollOptionCollectionViewCell_Previews: PreviewProvider {
static var controls: some View {
Group {
UIViewPreview() {
let cell = ComposeStatusNewPollOptionCollectionViewCell()
return cell
}
.previewLayout(.fixed(width: 375, height: 44 + 10))
}
}
static var previews: some View {
Group {
controls.colorScheme(.light)
controls.colorScheme(.dark)
}
.background(Color.gray)
}
}
#endif

View File

@ -0,0 +1,147 @@
//
// ComposeStatusPollOptionCollectionViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-23.
//
import os.log
import UIKit
import Combine
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
}
final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell {
var disposeBag = Set<AnyCancellable>()
weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate?
let pollOptionView = PollOptionView()
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
private var pollOptionSubscription: AnyCancellable?
let pollOption = PassthroughSubject<String, Never>()
override func prepareForReuse() {
super.prepareForReuse()
delegate = nil
disposeBag.removeAll()
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeStatusPollOptionCollectionViewCell {
private func _init() {
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(pollOptionView)
NSLayoutConstraint.activate([
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
pollOptionView.checkmarkImageView.isHidden = true
pollOptionView.optionPercentageLabel.isHidden = true
pollOptionView.optionTextField.text = nil
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
setupBorderColor()
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
pollOptionSubscription = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: pollOptionView.optionTextField)
.receive(on: DispatchQueue.main)
.sink { [weak self] notification in
guard let self = self else { return }
guard let textField = notification.object as? UITextField else { return }
self.pollOption.send(textField.text ?? "")
}
pollOptionView.optionTextField.deleteBackwardDelegate = self
pollOptionView.optionTextField.delegate = self
}
private func setupBorderColor() {
pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setupBorderColor()
}
}
extension ComposeStatusPollOptionCollectionViewCell {
@objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
pollOptionView.optionTextField.becomeFirstResponder()
}
}
// MARK: - DeleteBackwardResponseTextFieldDelegate
extension ComposeStatusPollOptionCollectionViewCell: DeleteBackwardResponseTextFieldDelegate {
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) {
delegate?.composeStatusPollOptionCollectionViewCell(self, textBeforeDeleteBackward: textBeforeDelete)
}
}
// MARK: - UITextFieldDelegate
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
if textField === pollOptionView.optionTextField {
delegate?.composeStatusPollOptionCollectionViewCell(self, pollOptionTextFieldDidReturn: textField)
}
return true
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct ComposeStatusPollOptionCollectionViewCell_Previews: PreviewProvider {
static var controls: some View {
Group {
UIViewPreview() {
let cell = ComposeStatusPollOptionCollectionViewCell()
return cell
}
.previewLayout(.fixed(width: 375, height: 44 + 10))
}
}
static var previews: some View {
Group {
controls.colorScheme(.light)
controls.colorScheme(.dark)
}
.background(Color.gray)
}
}
#endif

View File

@ -45,6 +45,8 @@ final class ComposeViewController: UIViewController, NeedsDependency {
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
collectionView.register(ComposeStatusNewPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusNewPollOptionCollectionViewCell.self))
collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
return collectionView
}()
@ -154,7 +156,9 @@ extension ComposeViewController {
for: collectionView,
dependency: self,
textEditorViewTextAttributesDelegate: self,
composeStatusAttachmentTableViewCellDelegate: self
composeStatusAttachmentTableViewCellDelegate: self,
composeStatusPollOptionCollectionViewCellDelegate: self,
composeStatusNewPollOptionCollectionViewCellDelegate: self
)
// respond scrollView overlap change
@ -205,6 +209,16 @@ extension ComposeViewController {
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: publishBarButtonItem)
.store(in: &disposeBag)
viewModel.isMediaToolbarButtonEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
.store(in: &disposeBag)
viewModel.isPollToolbarButtonEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
.store(in: &disposeBag)
// bind custom emojis
viewModel.customEmojiViewModel
@ -268,6 +282,57 @@ extension ComposeViewController {
textEditorView()?.isEditing = true
}
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
guard case .poll = item else { return nil }
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
guard let indexPath = diffableDataSource.indexPath(for: item),
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
return nil
}
return cell
}
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
let firstPollItem = items.first { item -> Bool in
guard case .poll = item else { return false }
return true
}
guard let item = firstPollItem else {
return nil
}
return pollOptionCollectionViewCell(of: item)
}
private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
let lastPollItem = items.last { item -> Bool in
guard case .poll = item else { return false }
return true
}
guard let item = lastPollItem else {
return nil
}
return pollOptionCollectionViewCell(of: item)
}
private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
guard let cell = firstPollOptionCollectionViewCell() else { return }
cell.pollOptionView.optionTextField.becomeFirstResponder()
}
private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
guard let cell = lastPollOptionCollectionViewCell() else { return }
cell.pollOptionView.optionTextField.becomeFirstResponder()
}
private func showDismissConfirmAlertController() {
let alertController = UIAlertController(
title: L10n.Common.Alerts.DiscardPostContent.title,
@ -490,7 +555,6 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
extension ComposeViewController: ComposeToolbarViewDelegate {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, mediaSelectionType.rawValue)
switch mediaSelectionType {
case .photoLibrary:
present(imagePicker, animated: true, completion: nil)
@ -501,20 +565,31 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
}
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton) {
viewModel.isPollComposing.value.toggle()
// setup initial poll option if needs
if viewModel.isPollComposing.value, viewModel.pollAttributes.value.isEmpty {
viewModel.pollAttributes.value = [ComposeStatusItem.ComposePollAttribute(), ComposeStatusItem.ComposePollAttribute()]
}
if viewModel.isPollComposing.value {
// Magic RunLoop
DispatchQueue.main.async {
self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
}
} else {
markTextEditorViewBecomeFirstResponser()
}
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) {
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) {
}
}
@ -622,3 +697,88 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega
}
}
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
// handle delete backward event for poll option input
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
guard (text ?? "").isEmpty else { return }
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
guard case let .poll(attribute) = item else { return }
var pollAttributes = viewModel.pollAttributes.value
guard let index = pollAttributes.firstIndex(of: attribute) else { return }
// mark previous (fallback to next) item of removed middle poll option become first responder
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
guard index > 0 else { return nil }
let indexBeforeRemoved = pollItems.index(before: indexOfItem)
let itemBeforeRemoved = pollItems[indexBeforeRemoved]
return pollOptionCollectionViewCell(of: itemBeforeRemoved)
}
func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
guard index < pollItems.count - 1 else { return nil }
let indexAfterRemoved = pollItems.index(after: index)
let itemAfterRemoved = pollItems[indexAfterRemoved]
return pollOptionCollectionViewCell(of: itemAfterRemoved)
}
var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
if cell == nil {
cell = cellAfterRemoved()
}
cell?.pollOptionView.optionTextField.becomeFirstResponder()
}
guard pollAttributes.count > 2 else {
return
}
pollAttributes.remove(at: index)
// update data source
viewModel.pollAttributes.value = pollAttributes
}
// handle keyboard return event for poll option input
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let indexPath = collectionView.indexPath(for: cell) else { return }
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll).filter { item in
guard case .poll = item else { return false }
return true
}
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
guard let index = pollItems.firstIndex(of: item) else { return }
if index == pollItems.count - 1 {
// is the last
viewModel.createNewPollOptionIfPossible()
DispatchQueue.main.async {
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
}
} else {
// not the last
let indexAfter = pollItems.index(after: index)
let itemAfter = pollItems[indexAfter]
let cell = pollOptionCollectionViewCell(of: itemAfter)
cell?.pollOptionView.optionTextField.becomeFirstResponder()
}
}
}
// MARK: - ComposeStatusNewPollOptionCollectionViewCellDelegate
extension ComposeViewController: ComposeStatusNewPollOptionCollectionViewCellDelegate {
func ComposeStatusNewPollOptionCollectionViewCellDidPressed(_ cell: ComposeStatusNewPollOptionCollectionViewCell) {
viewModel.createNewPollOptionIfPossible()
DispatchQueue.main.async {
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
}
}
}

View File

@ -14,7 +14,9 @@ extension ComposeViewModel {
for collectionView: UICollectionView,
dependency: NeedsDependency,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusNewPollOptionCollectionViewCellDelegate
) {
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
for: collectionView,
@ -22,7 +24,9 @@ extension ComposeViewModel {
managedObjectContext: context.managedObjectContext,
composeKind: composeKind,
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate
)
// Note: do not allow reorder due to the images display order following the upload time
@ -48,7 +52,7 @@ extension ComposeViewModel {
self.diffableDataSource = diffableDataSource
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
snapshot.appendSections([.repliedTo, .status, .attachment])
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
switch composeKind {
case .reply(let statusObjectID):
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)

View File

@ -19,6 +19,7 @@ final class ComposeViewModel {
let context: AppContext
let composeKind: ComposeStatusSection.ComposeKind
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
@ -41,6 +42,8 @@ final class ComposeViewModel {
let title: CurrentValueSubject<String, Never>
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
// custom emojis
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
@ -48,6 +51,9 @@ final class ComposeViewModel {
// attachment
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
// polls
let pollAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollAttribute], Never>([])
init(
context: AppContext,
composeKind: ComposeStatusSection.ComposeKind
@ -137,49 +143,91 @@ final class ComposeViewModel {
}
.store(in: &disposeBag)
// bind snapshot and drive service upload state
attachmentServices
.receive(on: DispatchQueue.main)
.sink { [weak self] attachmentServices in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = diffableDataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
var items: [ComposeStatusItem] = []
for attachmentService in attachmentServices {
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
items.append(item)
// bind snapshot
Publishers.CombineLatest3(
attachmentServices.eraseToAnyPublisher(),
isPollComposing.eraseToAnyPublisher(),
pollAttributes.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = diffableDataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
var attachmentItems: [ComposeStatusItem] = []
for attachmentService in attachmentServices {
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
attachmentItems.append(item)
}
snapshot.appendItems(attachmentItems, toSection: .attachment)
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .poll))
if isPollComposing {
var pollItems: [ComposeStatusItem] = []
for pollAttribute in pollAttributes {
let item = ComposeStatusItem.poll(attribute: pollAttribute)
pollItems.append(item)
}
snapshot.appendItems(items, toSection: .attachment)
diffableDataSource.apply(snapshot)
// make image upload in the queue
for attachmentService in attachmentServices {
// skip when prefix N task when task finish OR fail OR uploading
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
if currentState is MastodonAttachmentService.UploadState.Fail {
continue
}
if currentState is MastodonAttachmentService.UploadState.Finish {
continue
}
if currentState is MastodonAttachmentService.UploadState.Uploading {
break
}
// trigger uploading one by one
if currentState is MastodonAttachmentService.UploadState.Initial {
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
break
}
snapshot.appendItems(pollItems, toSection: .poll)
if pollAttributes.count < 4 {
snapshot.appendItems([ComposeStatusItem.newPoll], toSection: .poll)
}
}
.store(in: &disposeBag)
diffableDataSource.apply(snapshot)
// drive service upload state
// make image upload in the queue
for attachmentService in attachmentServices {
// skip when prefix N task when task finish OR fail OR uploading
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
if currentState is MastodonAttachmentService.UploadState.Fail {
continue
}
if currentState is MastodonAttachmentService.UploadState.Finish {
continue
}
if currentState is MastodonAttachmentService.UploadState.Uploading {
break
}
// trigger uploading one by one
if currentState is MastodonAttachmentService.UploadState.Initial {
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
break
}
}
}
.store(in: &disposeBag)
Publishers.CombineLatest(
isPollComposing.eraseToAnyPublisher(),
attachmentServices.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
guard let self = self else { return }
let shouldMediaDisable = isPollComposing || attachmentServices.count >= 4
let shouldPollDisable = attachmentServices.count > 0
self.isMediaToolbarButtonEnabled.value = !shouldMediaDisable
self.isPollToolbarButtonEnabled.value = !shouldPollDisable
})
.store(in: &disposeBag)
}
}
extension ComposeViewModel {
func createNewPollOptionIfPossible() {
guard pollAttributes.value.count < 4 else { return }
let attribute = ComposeStatusItem.ComposePollAttribute()
pollAttributes.value = pollAttributes.value + [attribute]
}
}
// MARK: - MastodonAttachmentServiceDelegate
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {

View File

@ -5,14 +5,15 @@
// Created by MainasuK Cirno on 2021-3-12.
//
import os.log
import UIKit
protocol ComposeToolbarViewDelegate: class {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton)
}
final class ComposeToolbarView: UIView {
@ -102,10 +103,10 @@ extension ComposeToolbarView {
mediaButton.menu = createMediaContextMenu()
mediaButton.showsMenuAsPrimaryAction = true
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.visibilityButtonDidPressed(_:)), for: .touchUpInside)
}
}
@ -131,18 +132,21 @@ extension ComposeToolbarView {
var children: [UIMenuElement] = []
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function)
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
}
children.append(photoLibraryAction)
if UIImagePickerController.isSourceTypeAvailable(.camera) {
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .camera)
})
children.append(cameraAction)
}
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .browse)
}
children.append(browseAction)
@ -155,20 +159,24 @@ extension ComposeToolbarView {
extension ComposeToolbarView {
@objc private func gifButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
@objc private func pollButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.composeToolbarView(self, pollButtonDidPressed: sender)
}
@objc private func atButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
@objc private func emojiButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.composeToolbarView(self, emojiButtonDidPressed: sender)
}
@objc private func topicButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
@objc private func contentWarningButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
}
@objc private func locationButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
@objc private func visibilityButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.composeToolbarView(self, visibilityButtonDidPressed: sender)
}
}

View File

@ -0,0 +1,200 @@
//
// PollOptionView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-23.
//
import UIKit
import Combine
final class PollOptionView: UIView {
static let height: CGFloat = optionHeight + 2 * verticalMargin
static let optionHeight: CGFloat = 44
static let verticalMargin: CGFloat = 5
static let checkmarkImageSize = CGSize(width: 26, height: 26)
private var viewStateDisposeBag = Set<AnyCancellable>()
let roundedBackgroundView = UIView()
let voteProgressStripView: StripProgressView = {
let view = StripProgressView()
view.tintColor = Asset.Colors.Background.Poll.highlight.color
return view
}()
let checkmarkBackgroundView: UIView = {
let view = UIView()
view.backgroundColor = .systemBackground
return view
}()
let checkmarkImageView: UIImageView = {
let imageView = UIImageView()
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
imageView.image = image.withRenderingMode(.alwaysTemplate)
imageView.tintColor = Asset.Colors.Button.normal.color
return imageView
}()
let plusCircleImageView: UIImageView = {
let imageView = UIImageView()
let image = Asset.Circles.plusCircle.image
imageView.image = image.withRenderingMode(.alwaysTemplate)
imageView.tintColor = Asset.Colors.Button.normal.color
return imageView
}()
let optionTextField: DeleteBackwardResponseTextField = {
let textField = DeleteBackwardResponseTextField()
textField.font = .systemFont(ofSize: 15, weight: .medium)
textField.textColor = Asset.Colors.Label.primary.color
textField.text = "Option"
textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
return textField
}()
let optionLabelMiddlePaddingView = UIView()
let optionPercentageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .regular)
label.textColor = Asset.Colors.Label.primary.color
label.text = "50%"
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension PollOptionView {
private func _init() {
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
addSubview(roundedBackgroundView)
NSLayoutConstraint.activate([
roundedBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 5),
roundedBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
roundedBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.optionHeight).priority(.defaultHigh),
])
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(voteProgressStripView)
NSLayoutConstraint.activate([
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
])
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(checkmarkBackgroundView)
NSLayoutConstraint.activate([
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.width).priority(.required - 1),
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.height).priority(.required - 1),
])
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
checkmarkBackgroundView.addSubview(checkmarkImageView)
NSLayoutConstraint.activate([
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
])
plusCircleImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(plusCircleImageView)
NSLayoutConstraint.activate([
plusCircleImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor),
plusCircleImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor),
plusCircleImageView.trailingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor),
plusCircleImageView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor),
])
optionTextField.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionTextField)
NSLayoutConstraint.activate([
optionTextField.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
optionTextField.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
optionTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
])
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
NSLayoutConstraint.activate([
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionTextField.trailingAnchor),
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
])
optionLabelMiddlePaddingView.setContentHuggingPriority(.required - 1, for: .horizontal)
optionLabelMiddlePaddingView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionPercentageLabel)
NSLayoutConstraint.activate([
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
])
optionPercentageLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
plusCircleImageView.isHidden = true
}
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
}
extension PollOptionView {
private func updateCornerRadius() {
roundedBackgroundView.layer.masksToBounds = true
roundedBackgroundView.layer.cornerRadius = PollOptionView.optionHeight * 0.5
roundedBackgroundView.layer.cornerCurve = .circular
checkmarkBackgroundView.layer.masksToBounds = true
checkmarkBackgroundView.layer.cornerRadius = PollOptionView.checkmarkImageSize.width * 0.5
checkmarkBackgroundView.layer.cornerCurve = .circular
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct PollOptionView_Previews: PreviewProvider {
static var previews: some View {
Group {
UIViewPreview(width: 375) {
PollOptionView()
}
.previewLayout(.fixed(width: 375, height: 100))
}
}
}
#endif

View File

@ -10,54 +10,8 @@ import Combine
final class PollOptionTableViewCell: UITableViewCell {
static let height: CGFloat = optionHeight + 2 * verticalMargin
static let optionHeight: CGFloat = 44
static let verticalMargin: CGFloat = 5
static let checkmarkImageSize = CGSize(width: 26, height: 26)
private var viewStateDisposeBag = Set<AnyCancellable>()
let pollOptionView = PollOptionView()
var attribute: PollItem.Attribute?
let roundedBackgroundView = UIView()
let voteProgressStripView: StripProgressView = {
let view = StripProgressView()
view.tintColor = Asset.Colors.Background.Poll.highlight.color
return view
}()
let checkmarkBackgroundView: UIView = {
let view = UIView()
view.backgroundColor = .systemBackground
return view
}()
let checkmarkImageView: UIView = {
let imageView = UIImageView()
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
imageView.image = image.withRenderingMode(.alwaysTemplate)
imageView.tintColor = Asset.Colors.Button.normal.color
return imageView
}()
let optionLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 15, weight: .medium)
label.textColor = Asset.Colors.Label.primary.color
label.text = "Option"
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
return label
}()
let optionLabelMiddlePaddingView = UIView()
let optionPercentageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .regular)
label.textColor = Asset.Colors.Label.primary.color
label.text = "50%"
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -76,7 +30,7 @@ final class PollOptionTableViewCell: UITableViewCell {
switch voteState {
case .hidden:
let color = Asset.Colors.Background.systemGroupedBackground.color
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
case .reveal:
break
}
@ -89,7 +43,7 @@ final class PollOptionTableViewCell: UITableViewCell {
switch voteState {
case .hidden:
let color = Asset.Colors.Background.systemGroupedBackground.color
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
case .reveal:
break
}
@ -102,125 +56,55 @@ extension PollOptionTableViewCell {
private func _init() {
selectionStyle = .none
backgroundColor = .clear
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
pollOptionView.optionTextField.isUserInteractionEnabled = false
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(roundedBackgroundView)
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(pollOptionView)
NSLayoutConstraint.activate([
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
pollOptionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
pollOptionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(voteProgressStripView)
NSLayoutConstraint.activate([
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
])
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(checkmarkBackgroundView)
NSLayoutConstraint.activate([
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
])
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
checkmarkBackgroundView.addSubview(checkmarkImageView)
NSLayoutConstraint.activate([
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
])
optionLabel.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionLabel)
NSLayoutConstraint.activate([
optionLabel.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
optionLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
])
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
NSLayoutConstraint.activate([
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionLabel.trailingAnchor),
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
])
optionLabelMiddlePaddingView.setContentHuggingPriority(.defaultLow, for: .horizontal)
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
roundedBackgroundView.addSubview(optionPercentageLabel)
NSLayoutConstraint.activate([
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
])
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
}
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
updateTextAppearance()
}
private func updateCornerRadius() {
roundedBackgroundView.layer.masksToBounds = true
roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5
roundedBackgroundView.layer.cornerCurve = .circular
checkmarkBackgroundView.layer.masksToBounds = true
checkmarkBackgroundView.layer.cornerRadius = PollOptionTableViewCell.checkmarkImageSize.width * 0.5
checkmarkBackgroundView.layer.cornerCurve = .circular
}
func updateTextAppearance() {
guard let voteState = attribute?.voteState else {
optionLabel.textColor = Asset.Colors.Label.primary.color
optionLabel.layer.removeShadow()
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
pollOptionView.optionTextField.layer.removeShadow()
return
}
switch voteState {
case .hidden:
optionLabel.textColor = Asset.Colors.Label.primary.color
optionLabel.layer.removeShadow()
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
pollOptionView.optionTextField.layer.removeShadow()
case .reveal(_, let percentage, _):
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.minX {
optionLabel.textColor = .white
optionLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.minX {
pollOptionView.optionTextField.textColor = .white
pollOptionView.optionTextField.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
} else {
optionLabel.textColor = Asset.Colors.Label.primary.color
optionLabel.layer.removeShadow()
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
pollOptionView.optionTextField.layer.removeShadow()
}
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.maxX {
optionPercentageLabel.textColor = .white
optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.maxX {
pollOptionView.optionPercentageLabel.textColor = .white
pollOptionView.optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
} else {
optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
optionPercentageLabel.layer.removeShadow()
pollOptionView.optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
pollOptionView.optionPercentageLabel.layer.removeShadow()
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateTextAppearance()
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI

View File

@ -0,0 +1,24 @@
//
// DeleteBackwardResponseTextField.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-23.
//
import UIKit
protocol DeleteBackwardResponseTextFieldDelegate: class {
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?)
}
final class DeleteBackwardResponseTextField: UITextField {
weak var deleteBackwardDelegate: DeleteBackwardResponseTextFieldDelegate?
override func deleteBackward() {
let text = self.text
super.deleteBackward()
deleteBackwardDelegate?.deleteBackwardResponseTextField(self, textBeforeDelete: text)
}
}