mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 01:31:02 +00:00
wip
This commit is contained in:
parent
f16426877c
commit
2cb8370e68
6 changed files with 87 additions and 5 deletions
13
Extensions/UITextInput+Extensions.swift
Normal file
13
Extensions/UITextInput+Extensions.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UITextInput {
|
||||||
|
var textToSelectedRange: String? {
|
||||||
|
guard let selectedRange = selectedTextRange,
|
||||||
|
let range = textRange(from: beginningOfDocument, to: selectedRange.end)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return text(in: range)
|
||||||
|
}
|
||||||
|
}
|
|
@ -171,6 +171,8 @@
|
||||||
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */; };
|
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */; };
|
||||||
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
|
||||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||||
|
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
||||||
|
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
||||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||||
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
|
||||||
|
@ -378,6 +380,7 @@
|
||||||
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
|
||||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
||||||
|
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInput+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -760,6 +763,7 @@
|
||||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||||
D0BE980D25D241CE0057E161 /* UIImage+Extensions.swift */,
|
D0BE980D25D241CE0057E161 /* UIImage+Extensions.swift */,
|
||||||
D08DFB0025CE228E0005DA98 /* UIScrollView+Extensions.swift */,
|
D08DFB0025CE228E0005DA98 /* UIScrollView+Extensions.swift */,
|
||||||
|
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */,
|
||||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
||||||
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
||||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||||
|
@ -1010,6 +1014,7 @@
|
||||||
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||||
|
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */,
|
||||||
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
||||||
|
@ -1166,6 +1171,7 @@
|
||||||
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||||
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
||||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||||
|
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */,
|
||||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */,
|
D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */,
|
||||||
D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */,
|
D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */,
|
||||||
|
|
|
@ -10,12 +10,16 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
public let id = Id()
|
public let id = Id()
|
||||||
public var isPosted = false
|
public var isPosted = false
|
||||||
@Published public var text = ""
|
@Published public var text = ""
|
||||||
|
@Published public var textToSelectedRange = ""
|
||||||
@Published public var contentWarning = ""
|
@Published public var contentWarning = ""
|
||||||
|
@Published public var contentWarningTextToSelectedRange = ""
|
||||||
@Published public var displayContentWarning = false
|
@Published public var displayContentWarning = false
|
||||||
@Published public var sensitive = false
|
@Published public var sensitive = false
|
||||||
@Published public var displayPoll = false
|
@Published public var displayPoll = false
|
||||||
@Published public var pollMultipleChoice = false
|
@Published public var pollMultipleChoice = false
|
||||||
@Published public var pollExpiresIn = PollExpiry.oneDay
|
@Published public var pollExpiresIn = PollExpiry.oneDay
|
||||||
|
@Published public private(set) var autocompleteQuery: String?
|
||||||
|
@Published public private(set) var contentWarningAutocompleteQuery: String?
|
||||||
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
|
@Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")]
|
||||||
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
|
@Published public private(set) var attachmentViewModels = [AttachmentViewModel]()
|
||||||
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
||||||
|
@ -38,11 +42,14 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
textPresent || attachmentPresent
|
textPresent || attachmentPresent
|
||||||
}
|
}
|
||||||
.assign(to: &$isPostable)
|
.assign(to: &$isPostable)
|
||||||
|
|
||||||
$attachmentViewModels
|
$attachmentViewModels
|
||||||
.combineLatest($attachmentUpload, $displayPoll)
|
.combineLatest($attachmentUpload, $displayPoll)
|
||||||
.map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 }
|
.map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 }
|
||||||
.assign(to: &$canAddAttachment)
|
.assign(to: &$canAddAttachment)
|
||||||
|
|
||||||
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
||||||
|
|
||||||
$text.map {
|
$text.map {
|
||||||
let tokens = $0.components(separatedBy: " ")
|
let tokens = $0.components(separatedBy: " ")
|
||||||
|
|
||||||
|
@ -51,7 +58,18 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
.combineLatest($displayContentWarning, $contentWarning)
|
.combineLatest($displayContentWarning, $contentWarning)
|
||||||
.map { Self.maxCharacters - ($0 + ($1 ? $2.count : 0)) }
|
.map { Self.maxCharacters - ($0 + ($1 ? $2.count : 0)) }
|
||||||
.assign(to: &$remainingCharacters)
|
.assign(to: &$remainingCharacters)
|
||||||
|
|
||||||
$displayContentWarning.filter { $0 }.assign(to: &$sensitive)
|
$displayContentWarning.filter { $0 }.assign(to: &$sensitive)
|
||||||
|
|
||||||
|
$textToSelectedRange
|
||||||
|
.map { Self.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: false) }
|
||||||
|
.removeDuplicates()
|
||||||
|
.assign(to: &$autocompleteQuery)
|
||||||
|
|
||||||
|
$contentWarningTextToSelectedRange
|
||||||
|
.map { Self.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: true) }
|
||||||
|
.removeDuplicates()
|
||||||
|
.assign(to: &$contentWarningAutocompleteQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func attachmentSelected(viewModel: AttachmentViewModel) {
|
public func attachmentSelected(viewModel: AttachmentViewModel) {
|
||||||
|
@ -86,11 +104,17 @@ public extension CompositionViewModel {
|
||||||
class PollOption: ObservableObject {
|
class PollOption: ObservableObject {
|
||||||
public let id = Id()
|
public let id = Id()
|
||||||
@Published public var text: String
|
@Published public var text: String
|
||||||
|
@Published public var textToSelectedRange = ""
|
||||||
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
|
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
|
||||||
|
@Published public private(set) var autocompleteQuery: String?
|
||||||
|
|
||||||
public init(text: String) {
|
public init(text: String) {
|
||||||
self.text = text
|
self.text = text
|
||||||
$text.map { Self.maxCharacters - $0.count }.assign(to: &$remainingCharacters)
|
$text.map { Self.maxCharacters - $0.count }.assign(to: &$remainingCharacters)
|
||||||
|
$textToSelectedRange
|
||||||
|
.map { CompositionViewModel.extractAutocompleteQuery(textToSelectedRange: $0, emojiOnly: true) }
|
||||||
|
.removeDuplicates()
|
||||||
|
.assign(to: &$autocompleteQuery)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +258,18 @@ public extension CompositionViewModel.PollOption {
|
||||||
|
|
||||||
private extension CompositionViewModel {
|
private extension CompositionViewModel {
|
||||||
static let maxAttachmentCount = 4
|
static let maxAttachmentCount = 4
|
||||||
|
static let autocompleteQueryRegularExpression = #"([@#:]\S+)\z"#
|
||||||
|
static let emojiOnlyAutocompleteQueryRegularExpression = #"(:\S+)\z"#
|
||||||
|
|
||||||
|
static func extractAutocompleteQuery(textToSelectedRange: String, emojiOnly: Bool) -> String? {
|
||||||
|
guard let range = textToSelectedRange.range(
|
||||||
|
of: emojiOnly ? emojiOnlyAutocompleteQueryRegularExpression: autocompleteQueryRegularExpression,
|
||||||
|
options: .regularExpression,
|
||||||
|
locale: .current)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return String(textToSelectedRange[range])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension String {
|
private extension String {
|
||||||
|
|
|
@ -11,11 +11,15 @@ final class CompositionInputAccessoryView: UIToolbar {
|
||||||
|
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
private let parentViewModel: NewStatusViewModel
|
private let parentViewModel: NewStatusViewModel
|
||||||
|
private let autocompleteQueryPublisher: AnyPublisher<String?, Never>
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) {
|
init(viewModel: CompositionViewModel,
|
||||||
|
parentViewModel: NewStatusViewModel,
|
||||||
|
autocompleteQueryPublisher: AnyPublisher<String?, Never>) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
|
self.autocompleteQueryPublisher = autocompleteQueryPublisher
|
||||||
|
|
||||||
super.init(
|
super.init(
|
||||||
frame: .init(
|
frame: .init(
|
||||||
|
@ -170,6 +174,11 @@ private extension CompositionInputAccessoryView {
|
||||||
.sink { addButton.isEnabled = $0 }
|
.sink { addButton.isEnabled = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
autocompleteQueryPublisher
|
||||||
|
.print()
|
||||||
|
.sink { _ in /* TODO */ }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parentViewModel.$visibility
|
parentViewModel.$visibility
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
visibilityButton.image = UIImage(systemName: $0.systemImageName)
|
visibilityButton.image = UIImage(systemName: $0.systemImageName)
|
||||||
|
|
|
@ -46,12 +46,20 @@ private extension CompositionPollOptionView {
|
||||||
textField.font = .preferredFont(forTextStyle: .body)
|
textField.font = .preferredFont(forTextStyle: .body)
|
||||||
let textInputAccessoryView = CompositionInputAccessoryView(
|
let textInputAccessoryView = CompositionInputAccessoryView(
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
parentViewModel: parentViewModel)
|
parentViewModel: parentViewModel,
|
||||||
|
autocompleteQueryPublisher: option.$autocompleteQuery.eraseToAnyPublisher())
|
||||||
textField.inputAccessoryView = textInputAccessoryView
|
textField.inputAccessoryView = textInputAccessoryView
|
||||||
textField.tag = textInputAccessoryView.tagForInputView
|
textField.tag = textInputAccessoryView.tagForInputView
|
||||||
textField.addAction(
|
textField.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
self?.option.text = self?.textField.text ?? "" },
|
guard let self = self, let text = self.textField.text else { return }
|
||||||
|
|
||||||
|
self.option.text = text
|
||||||
|
|
||||||
|
if let textToSelectedRange = self.textField.textToSelectedRange {
|
||||||
|
self.option.textToSelectedRange = textToSelectedRange
|
||||||
|
}
|
||||||
|
},
|
||||||
for: .editingChanged)
|
for: .editingChanged)
|
||||||
textField.text = option.text
|
textField.text = option.text
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,10 @@ extension CompositionView {
|
||||||
extension CompositionView: UITextViewDelegate {
|
extension CompositionView: UITextViewDelegate {
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
viewModel.text = textView.text
|
viewModel.text = textView.text
|
||||||
|
|
||||||
|
if let textToSelectedRange = textView.textToSelectedRange {
|
||||||
|
viewModel.textToSelectedRange = textToSelectedRange
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +85,8 @@ private extension CompositionView {
|
||||||
|
|
||||||
let spoilerTextinputAccessoryView = CompositionInputAccessoryView(
|
let spoilerTextinputAccessoryView = CompositionInputAccessoryView(
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
parentViewModel: parentViewModel)
|
parentViewModel: parentViewModel,
|
||||||
|
autocompleteQueryPublisher: viewModel.$contentWarningAutocompleteQuery.eraseToAnyPublisher())
|
||||||
|
|
||||||
stackView.addArrangedSubview(spoilerTextField)
|
stackView.addArrangedSubview(spoilerTextField)
|
||||||
spoilerTextField.borderStyle = .roundedRect
|
spoilerTextField.borderStyle = .roundedRect
|
||||||
|
@ -95,13 +100,18 @@ private extension CompositionView {
|
||||||
guard let self = self, let text = self.spoilerTextField.text else { return }
|
guard let self = self, let text = self.spoilerTextField.text else { return }
|
||||||
|
|
||||||
self.viewModel.contentWarning = text
|
self.viewModel.contentWarning = text
|
||||||
|
|
||||||
|
if let textToSelectedRange = self.spoilerTextField.textToSelectedRange {
|
||||||
|
self.viewModel.contentWarningTextToSelectedRange = textToSelectedRange
|
||||||
|
}
|
||||||
},
|
},
|
||||||
for: .editingChanged)
|
for: .editingChanged)
|
||||||
|
|
||||||
let textViewFont = UIFont.preferredFont(forTextStyle: .body)
|
let textViewFont = UIFont.preferredFont(forTextStyle: .body)
|
||||||
let textInputAccessoryView = CompositionInputAccessoryView(
|
let textInputAccessoryView = CompositionInputAccessoryView(
|
||||||
viewModel: viewModel,
|
viewModel: viewModel,
|
||||||
parentViewModel: parentViewModel)
|
parentViewModel: parentViewModel,
|
||||||
|
autocompleteQueryPublisher: viewModel.$autocompleteQuery.eraseToAnyPublisher())
|
||||||
|
|
||||||
stackView.addArrangedSubview(textView)
|
stackView.addArrangedSubview(textView)
|
||||||
textView.keyboardType = .twitter
|
textView.keyboardType = .twitter
|
||||||
|
|
Loading…
Reference in a new issue