forked from zelo72/mastodon-ios
feat: update register scene UI
This commit is contained in:
parent
a7a36d503a
commit
7bf14c0450
|
@ -199,6 +199,10 @@
|
|||
DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */; };
|
||||
DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */; };
|
||||
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */; };
|
||||
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618022785A7100030EE79 /* RegisterSection.swift */; };
|
||||
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618042785A73D0030EE79 /* RegisterItem.swift */; };
|
||||
DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */; };
|
||||
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */; };
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB0C946426A6FD4D0088FB11 /* AlamofireImage */; };
|
||||
|
@ -396,6 +400,8 @@
|
|||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
|
||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||
DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */; };
|
||||
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */; };
|
||||
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */; };
|
||||
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */; };
|
||||
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; };
|
||||
|
@ -984,6 +990,10 @@
|
|||
DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRuleItem.swift; sourceTree = "<group>"; };
|
||||
DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRulesTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB0618022785A7100030EE79 /* RegisterSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterSection.swift; sourceTree = "<group>"; };
|
||||
DB0618042785A73D0030EE79 /* RegisterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterItem.swift; sourceTree = "<group>"; };
|
||||
DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterAvatarTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1206,6 +1216,8 @@
|
|||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = "<group>"; };
|
||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||
DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterTextFieldTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterPasswordHintTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = "<group>"; };
|
||||
DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = "<group>"; };
|
||||
DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -2057,8 +2069,8 @@
|
|||
DB68A03825E900CC00CFDF14 /* Share */,
|
||||
0FAA0FDD25E0B5700017CCDE /* Welcome */,
|
||||
0FAA102525E1125D0017CCDE /* PickServer */,
|
||||
DBE0821A25CD382900FD6BBD /* Register */,
|
||||
DB72602125E36A2500235243 /* ServerRules */,
|
||||
DBE0821A25CD382900FD6BBD /* Register */,
|
||||
2D364F7025E66D5B00204FDC /* ResendEmail */,
|
||||
2D59819925E4A55C000FB903 /* ConfirmEmail */,
|
||||
);
|
||||
|
@ -2138,6 +2150,24 @@
|
|||
path = Account;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0618082785B2790030EE79 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB06180B2785B2AF0030EE79 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */,
|
||||
DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */,
|
||||
DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2424,6 +2454,8 @@
|
|||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB0617F427855AB90030EE79 /* ServerRuleSection.swift */,
|
||||
DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */,
|
||||
DB0618022785A7100030EE79 /* RegisterSection.swift */,
|
||||
DB0618042785A73D0030EE79 /* RegisterItem.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2633,10 +2665,10 @@
|
|||
DB72602125E36A2500235243 /* ServerRules */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0618082785B2790030EE79 /* Cell */,
|
||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */,
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */,
|
||||
DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */,
|
||||
DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */,
|
||||
);
|
||||
path = ServerRules;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3197,9 +3229,11 @@
|
|||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB06180B2785B2AF0030EE79 /* Cell */,
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */,
|
||||
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */,
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
|
||||
DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */,
|
||||
);
|
||||
path = Register;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3974,6 +4008,7 @@
|
|||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
||||
DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */,
|
||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
|
||||
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */,
|
||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||
|
@ -4020,7 +4055,9 @@
|
|||
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */,
|
||||
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
|
||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
|
||||
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */,
|
||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
||||
DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */,
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||
|
@ -4178,6 +4215,7 @@
|
|||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
|
||||
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
|
||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
|
@ -4215,6 +4253,7 @@
|
|||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */,
|
||||
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */,
|
||||
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
||||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
|
||||
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
|
||||
|
@ -4322,6 +4361,7 @@
|
|||
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */,
|
||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||
DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */,
|
||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>24</integer>
|
||||
<integer>18</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>27</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -102,7 +102,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>26</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -122,7 +122,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>25</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// RegisterItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RegisterItem: Hashable {
|
||||
case header
|
||||
case avatar
|
||||
case name
|
||||
case username
|
||||
case email
|
||||
case password
|
||||
case hint
|
||||
case reason
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// RegisterSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum RegisterSection: Hashable {
|
||||
case main
|
||||
}
|
|
@ -25,7 +25,7 @@ extension ServerRuleSection {
|
|||
return cell
|
||||
case .rule(let ruleContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ServerRulesTableViewCell.self), for: indexPath) as! ServerRulesTableViewCell
|
||||
cell.indexImageView.image = UIImage(systemName: "\(ruleContext.index).circle.fill") ?? UIImage(systemName: "questionmark.circle.fill")
|
||||
cell.indexImageView.image = UIImage(systemName: "\(ruleContext.index + 1).circle.fill") ?? UIImage(systemName: "questionmark.circle.fill")
|
||||
cell.ruleLabel.text = ruleContext.rule.text
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -91,12 +91,14 @@ internal enum Asset {
|
|||
}
|
||||
internal enum Scene {
|
||||
internal enum Onboarding {
|
||||
internal static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder")
|
||||
internal static let navigationBackButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background")
|
||||
internal static let navigationBackButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background.highlighted")
|
||||
internal static let navigationNextButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background")
|
||||
internal static let navigationNextButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background.highlighted")
|
||||
internal static let onboardingBackground = ColorAsset(name: "Scene/Onboarding/onboarding.background")
|
||||
internal static let searchBarBackground = ColorAsset(name: "Scene/Onboarding/search.bar.background")
|
||||
internal static let textFieldBackground = ColorAsset(name: "Scene/Onboarding/textField.background")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
|
|
23
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Contents.json
vendored
Normal file
23
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Frame 82.jpg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame 82@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Frame 82@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82.jpg
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@2x.png
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@3x.png
vendored
Normal file
BIN
Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -375,8 +375,8 @@ extension MastodonPickServerViewController {
|
|||
self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show)
|
||||
} else {
|
||||
let mastodonRegisterViewModel = MastodonRegisterViewModel(
|
||||
domain: server.domain,
|
||||
context: self.context,
|
||||
domain: server.domain,
|
||||
authenticateInfo: response.authenticateInfo,
|
||||
instance: response.instance.value,
|
||||
applicationToken: response.applicationToken.value
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// MastodonRegisterAvatarTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class MastodonRegisterAvatarTableViewCell: UITableViewCell {
|
||||
|
||||
static let containerSize = CGSize(width: 88, height: 88)
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.cornerRadius = 22
|
||||
return view
|
||||
}()
|
||||
|
||||
let avatarButton: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
button.setImage(Asset.Scene.Onboarding.avatarPlaceholder.image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterAvatarTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 22),
|
||||
containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8),
|
||||
containerView.widthAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.width).priority(.required - 1),
|
||||
containerView.heightAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.height).priority(.required - 1),
|
||||
])
|
||||
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(avatarButton)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarButton.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
avatarButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
avatarButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
avatarButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// MastodonRegisterPasswordHintTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class MastodonRegisterPasswordHintTableViewCell: UITableViewCell {
|
||||
|
||||
let passwordRuleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .footnote)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "Your password needs at least:"
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterPasswordHintTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
passwordRuleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(passwordRuleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
passwordRuleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
passwordRuleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
passwordRuleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
passwordRuleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
//
|
||||
// MastodonRegisterTextFieldTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
|
||||
final class MastodonRegisterTextFieldTableViewCell: UITableViewCell {
|
||||
|
||||
static let textFieldHeight: CGFloat = 50
|
||||
static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let textFieldShadowContainer = ShadowBackgroundContainer()
|
||||
let textField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
textField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
||||
textField.layer.masksToBounds = true
|
||||
textField.layer.cornerRadius = 10
|
||||
textField.layer.cornerCurve = .continuous
|
||||
return textField
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
textFieldShadowContainer.shadowColor = .black
|
||||
textFieldShadowContainer.shadowAlpha = 0.25
|
||||
resetTextField()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterTextFieldTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
textFieldShadowContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(textFieldShadowContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
textFieldShadowContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6),
|
||||
textFieldShadowContainer.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
textFieldShadowContainer.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor, constant: 6),
|
||||
])
|
||||
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textFieldShadowContainer.addSubview(textField)
|
||||
NSLayoutConstraint.activate([
|
||||
textField.topAnchor.constraint(equalTo: textFieldShadowContainer.topAnchor),
|
||||
textField.leadingAnchor.constraint(equalTo: textFieldShadowContainer.leadingAnchor),
|
||||
textField.trailingAnchor.constraint(equalTo: textFieldShadowContainer.trailingAnchor),
|
||||
textField.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor),
|
||||
textField.heightAnchor.constraint(equalToConstant: MastodonRegisterTextFieldTableViewCell.textFieldHeight).priority(.required - 1),
|
||||
])
|
||||
|
||||
resetTextField()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterTextFieldTableViewCell {
|
||||
func resetTextField() {
|
||||
textField.keyboardType = .default
|
||||
textField.autocorrectionType = .default
|
||||
textField.autocapitalizationType = .none
|
||||
textField.attributedPlaceholder = nil
|
||||
textField.isSecureTextEntry = false
|
||||
|
||||
let paddingRect = CGRect(x: 0, y: 0, width: 16, height: 10)
|
||||
textField.leftView = UIView(frame: paddingRect)
|
||||
textField.leftViewMode = .always
|
||||
textField.rightView = UIView(frame: paddingRect)
|
||||
textField.rightViewMode = .always
|
||||
}
|
||||
|
||||
func setupTextViewRightView(text: String) {
|
||||
textField.rightView = {
|
||||
let containerView = UIView()
|
||||
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: MastodonRegisterTextFieldTableViewCell.textFieldHeight))
|
||||
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(paddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
paddingView.widthAnchor.constraint(equalToConstant: 8).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
let label = UILabel()
|
||||
label.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = text
|
||||
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(label)
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16),
|
||||
label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
return containerView
|
||||
}()
|
||||
}
|
||||
|
||||
func setupTextViewPlaceholder(text: String) {
|
||||
textField.attributedPlaceholder = NSAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
.font: MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -12,36 +12,6 @@ import PhotosUI
|
|||
import UIKit
|
||||
|
||||
extension MastodonRegisterViewController {
|
||||
func createMediaContextMenu() -> UIMenu {
|
||||
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 }
|
||||
self.present(self.imagePicker, animated: true, completion: nil)
|
||||
}
|
||||
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 }
|
||||
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||
})
|
||||
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 }
|
||||
self.present(self.documentPickerController, animated: true, completion: nil)
|
||||
}
|
||||
children.append(browseAction)
|
||||
if self.viewModel.avatarImage.value != nil {
|
||||
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.avatarImage.value = nil
|
||||
}
|
||||
children.append(deleteAction)
|
||||
}
|
||||
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
private func cropImage(image: UIImage, pickerViewController: UIViewController) {
|
||||
DispatchQueue.main.async {
|
||||
let cropController = CropViewController(croppingStyle: .default, image: image)
|
||||
|
@ -49,6 +19,12 @@ extension MastodonRegisterViewController {
|
|||
cropController.setAspectRatioPreset(.presetSquare, animated: true)
|
||||
cropController.aspectRatioPickerButtonHidden = true
|
||||
cropController.aspectRatioLockEnabled = true
|
||||
|
||||
// fix iPad compatibility issue
|
||||
// ref: https://github.com/TimOliver/TOCropViewController/issues/365#issuecomment-550239604
|
||||
cropController.modalTransitionStyle = .crossDissolve
|
||||
cropController.transitioningDelegate = nil
|
||||
|
||||
pickerViewController.dismiss(animated: true, completion: {
|
||||
self.present(cropController, animated: true, completion: nil)
|
||||
})
|
||||
|
@ -57,7 +33,6 @@ extension MastodonRegisterViewController {
|
|||
}
|
||||
|
||||
// MARK: - PHPickerViewControllerDelegate
|
||||
|
||||
extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else {
|
||||
|
@ -86,7 +61,6 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
|
|||
}
|
||||
|
||||
// MARK: - UIImagePickerControllerDelegate
|
||||
|
||||
extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
picker.dismiss(animated: true, completion: nil)
|
||||
|
@ -103,7 +77,6 @@ extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINa
|
|||
}
|
||||
|
||||
// MARK: - UIDocumentPickerDelegate
|
||||
|
||||
extension MastodonRegisterViewController: UIDocumentPickerDelegate {
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else { return }
|
||||
|
@ -121,10 +94,9 @@ extension MastodonRegisterViewController: UIDocumentPickerDelegate {
|
|||
}
|
||||
|
||||
// MARK: - CropViewControllerDelegate
|
||||
|
||||
extension MastodonRegisterViewController: CropViewControllerDelegate {
|
||||
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
||||
self.viewModel.avatarImage.value = image
|
||||
self.viewModel.avatarImage = image
|
||||
cropViewController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,231 @@
|
|||
//
|
||||
// MastodonRegisterViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
func setupDiffableDataSource(
|
||||
tableView: UITableView
|
||||
) {
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(MastodonRegisterAvatarTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self))
|
||||
tableView.register(MastodonRegisterTextFieldTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self))
|
||||
tableView.register(MastodonRegisterPasswordHintTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self))
|
||||
|
||||
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||
switch item {
|
||||
case .header:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell
|
||||
cell.titleLabel.text = L10n.Scene.Register.title
|
||||
cell.subTitleLabel.isHidden = true
|
||||
return cell
|
||||
case .avatar:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self), for: indexPath) as! MastodonRegisterAvatarTableViewCell
|
||||
self.configureAvatar(cell: cell)
|
||||
return cell
|
||||
case .name:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.DisplayName.placeholder)
|
||||
cell.textField.keyboardType = .default
|
||||
cell.textField.autocapitalizationType = .words
|
||||
cell.textField.text = self.name
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.name, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .username:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewRightView(text: "@" + self.domain)
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Username.placeholder)
|
||||
cell.textField.keyboardType = .alphabet
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.text = self.username
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.username, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$usernameValidateState)
|
||||
return cell
|
||||
case .email:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Email.placeholder)
|
||||
cell.textField.keyboardType = .emailAddress
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.text = self.email
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.email, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$emailValidateState)
|
||||
return cell
|
||||
case .password:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Password.placeholder)
|
||||
cell.textField.keyboardType = .alphabet
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.isSecureTextEntry = true
|
||||
cell.textField.text = self.password
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.password, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$passwordValidateState)
|
||||
return cell
|
||||
case .hint:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self), for: indexPath) as! MastodonRegisterPasswordHintTableViewCell
|
||||
return cell
|
||||
case .reason:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest)
|
||||
cell.textField.keyboardType = .default
|
||||
cell.textField.text = self.reason
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.reason, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$reasonValidateState)
|
||||
return cell
|
||||
default:
|
||||
assertionFailure()
|
||||
return UITableViewCell()
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RegisterSection, RegisterItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.header], toSection: .main)
|
||||
snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main)
|
||||
if approvalRequired {
|
||||
snapshot.appendItems([.reason], toSection: .main)
|
||||
}
|
||||
diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
private func configureAvatar(cell: MastodonRegisterAvatarTableViewCell) {
|
||||
self.$avatarImage
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self, weak cell] image in
|
||||
guard let self = self else { return }
|
||||
guard let cell = cell else { return }
|
||||
let image = image ?? Asset.Scene.Onboarding.avatarPlaceholder.image
|
||||
cell.avatarButton.setImage(image, for: .normal)
|
||||
cell.avatarButton.menu = self.createAvatarMediaContextMenu()
|
||||
cell.avatarButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
enum AvatarMediaMenuAction {
|
||||
case photoLibrary
|
||||
case camera
|
||||
case browse
|
||||
case delete
|
||||
}
|
||||
|
||||
private func createAvatarMediaContextMenu() -> UIMenu {
|
||||
var children: [UIMenuElement] = []
|
||||
|
||||
// Photo Library
|
||||
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 }
|
||||
self.avatarMediaMenuActionPublisher.send(.photoLibrary)
|
||||
}
|
||||
children.append(photoLibraryAction)
|
||||
|
||||
// Camera
|
||||
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 }
|
||||
self.avatarMediaMenuActionPublisher.send(.camera)
|
||||
})
|
||||
children.append(cameraAction)
|
||||
}
|
||||
|
||||
// Browse
|
||||
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 }
|
||||
self.avatarMediaMenuActionPublisher.send(.browse)
|
||||
}
|
||||
children.append(browseAction)
|
||||
|
||||
// Delete
|
||||
if avatarImage != nil {
|
||||
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.avatarMediaMenuActionPublisher.send(.delete)
|
||||
}
|
||||
children.append(deleteAction)
|
||||
}
|
||||
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
private func configureTextFieldCell(
|
||||
cell: MastodonRegisterTextFieldTableViewCell,
|
||||
validateState: Published<ValidateState>.Publisher
|
||||
) {
|
||||
Publishers.CombineLatest(
|
||||
validateState,
|
||||
cell.textField.publisher(for: \.isFirstResponder)
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] validateState, isFirstResponder in
|
||||
guard let cell = cell else { return }
|
||||
switch validateState {
|
||||
case .empty:
|
||||
cell.textFieldShadowContainer.shadowColor = isFirstResponder ? Asset.Colors.brandBlue.color : .black
|
||||
cell.textFieldShadowContainer.shadowAlpha = isFirstResponder ? 1 : 0.25
|
||||
case .valid:
|
||||
cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.valid.color
|
||||
cell.textFieldShadowContainer.shadowAlpha = 1
|
||||
case .invalid:
|
||||
cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.invalid.color
|
||||
cell.textFieldShadowContainer.shadowAlpha = 1
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
}
|
|
@ -14,18 +14,19 @@ final class MastodonRegisterViewModel {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let domain: String
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
let instance: Mastodon.Entity.Instance
|
||||
let applicationToken: Mastodon.Entity.Token
|
||||
let context: AppContext
|
||||
|
||||
let username = CurrentValueSubject<String, Never>("")
|
||||
let displayName = CurrentValueSubject<String, Never>("")
|
||||
let email = CurrentValueSubject<String, Never>("")
|
||||
let password = CurrentValueSubject<String, Never>("")
|
||||
let reason = CurrentValueSubject<String, Never>("")
|
||||
let avatarImage = CurrentValueSubject<UIImage?, Never>(nil)
|
||||
let viewDidAppear = CurrentValueSubject<Void, Never>(Void())
|
||||
|
||||
@Published var avatarImage: UIImage? = nil
|
||||
@Published var name = ""
|
||||
@Published var username = ""
|
||||
@Published var email = ""
|
||||
@Published var password = ""
|
||||
@Published var reason = ""
|
||||
|
||||
let usernameErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
||||
let emailErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
||||
|
@ -33,21 +34,25 @@ final class MastodonRegisterViewModel {
|
|||
let reasonErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<RegisterSection, RegisterItem>?
|
||||
let approvalRequired: Bool
|
||||
let applicationAuthorization: Mastodon.API.OAuth.Authorization
|
||||
let usernameValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
|
||||
let displayNameValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
|
||||
let emailValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
|
||||
let passwordValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
|
||||
let reasonValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
|
||||
|
||||
@Published var usernameValidateState: ValidateState = .empty
|
||||
@Published var displayNameValidateState: ValidateState = .empty
|
||||
@Published var emailValidateState: ValidateState = .empty
|
||||
@Published var passwordValidateState: ValidateState = .empty
|
||||
@Published var reasonValidateState: ValidateState = .empty
|
||||
|
||||
let isRegistering = CurrentValueSubject<Bool, Never>(false)
|
||||
let isAllValid = CurrentValueSubject<Bool, Never>(false)
|
||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||
@Published var isRegistering = false
|
||||
@Published var isAllValid = false
|
||||
@Published var error: Error? = nil
|
||||
|
||||
let avatarMediaMenuActionPublisher = PassthroughSubject<AvatarMediaMenuAction, Never>()
|
||||
|
||||
init(
|
||||
domain: String,
|
||||
context: AppContext,
|
||||
domain: String,
|
||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||
instance: Mastodon.Entity.Instance,
|
||||
applicationToken: Mastodon.Entity.Token
|
||||
|
@ -60,7 +65,15 @@ final class MastodonRegisterViewModel {
|
|||
self.approvalRequired = instance.approvalRequired ?? false
|
||||
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)
|
||||
|
||||
username
|
||||
$name
|
||||
.map { name in
|
||||
guard !name.isEmpty else { return .empty }
|
||||
return .valid
|
||||
}
|
||||
.assign(to: \.displayNameValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$username
|
||||
.map { username in
|
||||
guard !username.isEmpty else { return .empty }
|
||||
var isValid = true
|
||||
|
@ -79,114 +92,120 @@ final class MastodonRegisterViewModel {
|
|||
}
|
||||
return isValid ? .valid : .invalid
|
||||
}
|
||||
.assign(to: \.value, on: usernameValidateState)
|
||||
.assign(to: \.usernameValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
username
|
||||
.filter { !$0.isEmpty }
|
||||
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
.removeDuplicates()
|
||||
.compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
|
||||
guard let self = self else { return nil }
|
||||
let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
|
||||
return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
|
||||
.map {
|
||||
response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
Result.success(response)
|
||||
}
|
||||
.catch { error in
|
||||
Just(Result.failure(error))
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .success:
|
||||
let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
|
||||
self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
|
||||
self.usernameValidateState.value = .invalid
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
usernameValidateState
|
||||
.sink { [weak self] validateState in
|
||||
if validateState == .valid {
|
||||
self?.usernameErrorPrompt.value = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// TODO: check username available
|
||||
// username
|
||||
// .filter { !$0.isEmpty }
|
||||
// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||
// .removeDuplicates()
|
||||
// .compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
|
||||
// guard let self = self else { return nil }
|
||||
// let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
|
||||
// return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
|
||||
// .map {
|
||||
// response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
// Result.success(response)
|
||||
// }
|
||||
// .catch { error in
|
||||
// Just(Result.failure(error))
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
// }
|
||||
// .switchToLatest()
|
||||
// .sink { [weak self] result in
|
||||
// guard let self = self else { return }
|
||||
// switch result {
|
||||
// case .success:
|
||||
// let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
|
||||
// self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
|
||||
// self.usernameValidateState.value = .invalid
|
||||
// case .failure:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// usernameValidateState
|
||||
// .sink { [weak self] validateState in
|
||||
// if validateState == .valid {
|
||||
// self?.usernameErrorPrompt.value = nil
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
|
||||
displayName
|
||||
.map { displayname in
|
||||
guard !displayname.isEmpty else { return .empty }
|
||||
return .valid
|
||||
}
|
||||
.assign(to: \.value, on: displayNameValidateState)
|
||||
.store(in: &disposeBag)
|
||||
email
|
||||
$email
|
||||
.map { email in
|
||||
guard !email.isEmpty else { return .empty }
|
||||
return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid
|
||||
}
|
||||
.assign(to: \.value, on: emailValidateState)
|
||||
.assign(to: \.emailValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
password
|
||||
|
||||
$password
|
||||
.map { password in
|
||||
guard !password.isEmpty else { return .empty }
|
||||
return password.count >= 8 ? .valid : .invalid
|
||||
}
|
||||
.assign(to: \.value, on: passwordValidateState)
|
||||
.assign(to: \.passwordValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
if approvalRequired {
|
||||
reason
|
||||
$reason
|
||||
.map { invite in
|
||||
guard !invite.isEmpty else { return .empty }
|
||||
return .valid
|
||||
}
|
||||
.assign(to: \.value, on: reasonValidateState)
|
||||
.assign(to: \.reasonValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
error
|
||||
.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
let error = error as? Mastodon.API.Error
|
||||
let mastodonError = error?.mastodonError
|
||||
if case let .generic(genericMastodonError) = mastodonError,
|
||||
let details = genericMastodonError.details
|
||||
{
|
||||
self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
} else {
|
||||
self.usernameErrorPrompt.value = nil
|
||||
self.emailErrorPrompt.value = nil
|
||||
self.passwordErrorPrompt.value = nil
|
||||
self.reasonErrorPrompt.value = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// error
|
||||
// .sink { [weak self] error in
|
||||
// guard let self = self else { return }
|
||||
// let error = error as? Mastodon.API.Error
|
||||
// let mastodonError = error?.mastodonError
|
||||
// if case let .generic(genericMastodonError) = mastodonError,
|
||||
// let details = genericMastodonError.details
|
||||
// {
|
||||
// self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
// self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
// self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
// self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||
// } else {
|
||||
// self.usernameErrorPrompt.value = nil
|
||||
// self.emailErrorPrompt.value = nil
|
||||
// self.passwordErrorPrompt.value = nil
|
||||
// self.reasonErrorPrompt.value = nil
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
let publisherOne = Publishers.CombineLatest4(
|
||||
usernameValidateState.eraseToAnyPublisher(),
|
||||
displayNameValidateState.eraseToAnyPublisher(),
|
||||
emailValidateState.eraseToAnyPublisher(),
|
||||
passwordValidateState.eraseToAnyPublisher()
|
||||
$usernameValidateState,
|
||||
$displayNameValidateState,
|
||||
$emailValidateState,
|
||||
$passwordValidateState
|
||||
)
|
||||
.map { $0.0 == .valid && $0.1 == .valid && $0.2 == .valid && $0.3 == .valid }
|
||||
.map {
|
||||
$0.0 == .valid &&
|
||||
$0.1 == .valid &&
|
||||
$0.2 == .valid &&
|
||||
$0.3 == .valid
|
||||
}
|
||||
|
||||
let publisherTwo = $reasonValidateState.map { reasonValidateState -> Bool in
|
||||
guard self.approvalRequired else { return true }
|
||||
return reasonValidateState == .valid
|
||||
}
|
||||
|
||||
Publishers.CombineLatest(
|
||||
publisherOne,
|
||||
approvalRequired ? reasonValidateState.map { $0 == .valid }.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher()
|
||||
publisherTwo
|
||||
)
|
||||
.map { $0 && $1 }
|
||||
.assign(to: \.value, on: isAllValid)
|
||||
.assign(to: \.isAllValid, on: self)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,24 +26,6 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
|||
|
||||
let stackView = UIStackView()
|
||||
|
||||
let largeTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = MastodonServerRulesViewController.largeTitleFont
|
||||
label.textColor = MastodonServerRulesViewController.largeTitleTextColor
|
||||
label.text = L10n.Scene.ServerRules.title
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private(set) lazy var subtitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = MastodonServerRulesViewController.subTitleFont
|
||||
label.textColor = MastodonServerRulesViewController.subTitleTextColor
|
||||
label.text = L10n.Scene.ServerRules.subtitle(viewModel.domain)
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
|
@ -135,7 +117,13 @@ extension MastodonServerRulesViewController {
|
|||
@objc private func nextButtonPressed(_ sender: UIButton) {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
|
||||
let viewModel = MastodonRegisterViewModel(domain: viewModel.domain, context: context, authenticateInfo: viewModel.authenticateInfo, instance: viewModel.instance, applicationToken: viewModel.applicationToken)
|
||||
let viewModel = MastodonRegisterViewModel(
|
||||
context: context,
|
||||
domain: viewModel.domain,
|
||||
authenticateInfo: viewModel.authenticateInfo,
|
||||
instance: viewModel.instance,
|
||||
applicationToken: viewModel.applicationToken
|
||||
)
|
||||
coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import MastodonMeta
|
|||
|
||||
final class ProfileHeaderViewModel {
|
||||
|
||||
static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400)
|
||||
static let maxProfileFieldCount = 4
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
@ -190,8 +191,8 @@ extension ProfileHeaderViewModel {
|
|||
let image: UIImage? = {
|
||||
guard case let .image(_image) = editProfileInfo.avatarImageResource.value else { return nil }
|
||||
guard let image = _image else { return nil }
|
||||
guard image.size.width <= MastodonRegisterViewController.avatarImageMaxSizeInPixel.width else {
|
||||
return image.af.imageScaled(to: MastodonRegisterViewController.avatarImageMaxSizeInPixel)
|
||||
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
|
||||
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
|
||||
}
|
||||
return image
|
||||
}()
|
||||
|
|
|
@ -90,3 +90,47 @@ extension KeyboardResponderService {
|
|||
case dock
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyboardResponderService {
|
||||
public static func configure(
|
||||
scrollView: UIScrollView,
|
||||
layoutNeedsUpdate: AnyPublisher<Void, Never>,
|
||||
additionalSafeAreaInsets: AnyPublisher<UIEdgeInsets, Never> = CurrentValueSubject(.zero).eraseToAnyPublisher()
|
||||
) -> AnyCancellable {
|
||||
let tuple = Publishers.CombineLatest3(
|
||||
KeyboardResponderService.shared.isShow,
|
||||
KeyboardResponderService.shared.state,
|
||||
KeyboardResponderService.shared.endFrame
|
||||
)
|
||||
|
||||
return Publishers.CombineLatest3(
|
||||
tuple,
|
||||
layoutNeedsUpdate,
|
||||
additionalSafeAreaInsets
|
||||
)
|
||||
.sink(receiveValue: { [weak scrollView] tuple, _, additionalSafeAreaInsets in
|
||||
guard let scrollView = scrollView else { return }
|
||||
guard let view = scrollView.superview else { return }
|
||||
|
||||
let (isShow, state, endFrame) = tuple
|
||||
|
||||
guard isShow, state == .dock else {
|
||||
scrollView.contentInset.bottom = additionalSafeAreaInsets.bottom
|
||||
scrollView.verticalScrollIndicatorInsets.bottom = additionalSafeAreaInsets.bottom
|
||||
return
|
||||
}
|
||||
|
||||
// isShow AND dock state
|
||||
let contentFrame = view.convert(scrollView.frame, to: nil)
|
||||
let padding = contentFrame.maxY - endFrame.minY
|
||||
guard padding > 0 else {
|
||||
scrollView.contentInset.bottom = additionalSafeAreaInsets.bottom
|
||||
scrollView.verticalScrollIndicatorInsets.bottom = additionalSafeAreaInsets.bottom
|
||||
return
|
||||
}
|
||||
|
||||
scrollView.contentInset.bottom = padding - scrollView.safeAreaInsets.bottom + additionalSafeAreaInsets.bottom
|
||||
scrollView.verticalScrollIndicatorInsets.bottom = padding - scrollView.safeAreaInsets.bottom + additionalSafeAreaInsets.bottom
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,18 @@ import MastodonExtension
|
|||
|
||||
public final class ShadowBackgroundContainer: UIView {
|
||||
|
||||
public var shadowAlpha: CGFloat = 0.25 {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
public var shadowColor: UIColor = .black {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
public var cornerRadius: CGFloat = 10 {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
public let shadowLayer = CALayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -34,15 +46,15 @@ extension ShadowBackgroundContainer {
|
|||
|
||||
shadowLayer.frame = bounds
|
||||
shadowLayer.setupShadow(
|
||||
color: .black,
|
||||
alpha: 0.25,
|
||||
color: shadowColor,
|
||||
alpha: Float(shadowAlpha),
|
||||
x: 0,
|
||||
y: 1,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
roundedRect: bounds,
|
||||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 10, height: 10)
|
||||
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue