Add UITextView with color hashtags.
This commit is contained in:
parent
ba53dd9b7d
commit
a5ad0a3caa
|
@ -76,6 +76,8 @@
|
||||||
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */; };
|
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */; };
|
||||||
F87AEB942986C51B00434FB6 /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB932986C51B00434FB6 /* AppConstants.swift */; };
|
F87AEB942986C51B00434FB6 /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB932986C51B00434FB6 /* AppConstants.swift */; };
|
||||||
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB962986D16D00434FB6 /* AuthorisationError.swift */; };
|
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F87AEB962986D16D00434FB6 /* AuthorisationError.swift */; };
|
||||||
|
F8864CE929ACAF820020C534 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8864CE829ACAF820020C534 /* TextView.swift */; };
|
||||||
|
F8864CEB29ACBAA80020C534 /* TextFieldViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8864CEA29ACBAA80020C534 /* TextFieldViewModel.swift */; };
|
||||||
F886F257297859E300879356 /* CacheImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F886F256297859E300879356 /* CacheImageService.swift */; };
|
F886F257297859E300879356 /* CacheImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F886F256297859E300879356 /* CacheImageService.swift */; };
|
||||||
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; };
|
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; };
|
||||||
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; };
|
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; };
|
||||||
|
@ -218,6 +220,8 @@
|
||||||
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = "<group>"; };
|
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = "<group>"; };
|
||||||
F87AEB932986C51B00434FB6 /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = "<group>"; };
|
F87AEB932986C51B00434FB6 /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = "<group>"; };
|
||||||
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = "<group>"; };
|
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = "<group>"; };
|
||||||
|
F8864CE829ACAF820020C534 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
|
||||||
|
F8864CEA29ACBAA80020C534 /* TextFieldViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldViewModel.swift; sourceTree = "<group>"; };
|
||||||
F886F256297859E300879356 /* CacheImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheImageService.swift; sourceTree = "<group>"; };
|
F886F256297859E300879356 /* CacheImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheImageService.swift; sourceTree = "<group>"; };
|
||||||
F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||||
F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -460,6 +464,7 @@
|
||||||
F876418A298AC1B80057D362 /* NoDataView.swift */,
|
F876418A298AC1B80057D362 /* NoDataView.swift */,
|
||||||
F86FB554298BF83F000131F0 /* FavouriteTouch.swift */,
|
F86FB554298BF83F000131F0 /* FavouriteTouch.swift */,
|
||||||
F8FA991F299FDDC3007AB130 /* TextInputField.swift */,
|
F8FA991F299FDDC3007AB130 /* TextInputField.swift */,
|
||||||
|
F8864CE829ACAF820020C534 /* TextView.swift */,
|
||||||
);
|
);
|
||||||
path = Widgets;
|
path = Widgets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -687,6 +692,7 @@
|
||||||
children = (
|
children = (
|
||||||
F878842029A494E3003CFAD2 /* Subviews */,
|
F878842029A494E3003CFAD2 /* Subviews */,
|
||||||
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
|
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
|
||||||
|
F8864CEA29ACBAA80020C534 /* TextFieldViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ComposeView;
|
path = ComposeView;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -822,6 +828,7 @@
|
||||||
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
|
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
|
||||||
F8C5E55F2988E92600ADF6A7 /* AccountModel.swift in Sources */,
|
F8C5E55F2988E92600ADF6A7 /* AccountModel.swift in Sources */,
|
||||||
F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */,
|
F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */,
|
||||||
|
F8864CE929ACAF820020C534 /* TextView.swift in Sources */,
|
||||||
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */,
|
F89AC00529A1F9B500F4159F /* AppMetadata.swift in Sources */,
|
||||||
F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */,
|
F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */,
|
||||||
F89992CC296D9231005994BF /* StatusModel.swift in Sources */,
|
F89992CC296D9231005994BF /* StatusModel.swift in Sources */,
|
||||||
|
@ -898,6 +905,7 @@
|
||||||
F8FA9920299FDDC3007AB130 /* TextInputField.swift in Sources */,
|
F8FA9920299FDDC3007AB130 /* TextInputField.swift in Sources */,
|
||||||
F86A4303299A9AF500DF7645 /* TipsStore.swift in Sources */,
|
F86A4303299A9AF500DF7645 /* TipsStore.swift in Sources */,
|
||||||
F8C5E56229892CC300ADF6A7 /* FirstAppear.swift in Sources */,
|
F8C5E56229892CC300ADF6A7 /* FirstAppear.swift in Sources */,
|
||||||
|
F8864CEB29ACBAA80020C534 /* TextFieldViewModel.swift in Sources */,
|
||||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||||
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */,
|
F83CBEFB298298A1002972C8 /* ImageCarouselPicture.swift in Sources */,
|
||||||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
||||||
|
@ -1035,7 +1043,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 36;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
@ -1072,7 +1080,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 36;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "247",
|
||||||
|
"green" : "247",
|
||||||
|
"red" : "247"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "42",
|
||||||
|
"green" : "40",
|
||||||
|
"red" : "40"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ extension Color {
|
||||||
static let mainTextColor = Color("MainTextColor")
|
static let mainTextColor = Color("MainTextColor")
|
||||||
static let selectedRowColor = Color("SelectedRowColor")
|
static let selectedRowColor = Color("SelectedRowColor")
|
||||||
static let viewBackgroundColor = Color("ViewBackgroundColor")
|
static let viewBackgroundColor = Color("ViewBackgroundColor")
|
||||||
|
static let keyboardToolbarColor = Color("KeyboardToolbar")
|
||||||
static let viewTextColor = Color("ViewTextColor")
|
static let viewTextColor = Color("ViewTextColor")
|
||||||
static let accentColor1 = Color("AccentColor1")
|
static let accentColor1 = Color("AccentColor1")
|
||||||
static let accentColor2 = Color("AccentColor2")
|
static let accentColor2 = Color("AccentColor2")
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct ComposeView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State var statusViewModel: StatusModel?
|
@State var statusViewModel: StatusModel?
|
||||||
@State private var text = String.empty()
|
|
||||||
@State private var isSensitive = false
|
@State private var isSensitive = false
|
||||||
@State private var spoilerText = String.empty()
|
@State private var spoilerText = String.empty()
|
||||||
@State private var commentsDisabled = false
|
@State private var commentsDisabled = false
|
||||||
|
@ -60,167 +60,171 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
||||||
private let keyboardFontSize = 14.0
|
private let keyboardFontSize = 22.0
|
||||||
|
|
||||||
|
@StateObject private var textFieldViewModel: TextFieldViewModel
|
||||||
|
|
||||||
|
public init(statusViewModel: StatusModel? = nil) {
|
||||||
|
_textFieldViewModel = StateObject(wrappedValue: .init())
|
||||||
|
self.statusViewModel = statusViewModel
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ScrollView {
|
ZStack(alignment: .bottom) {
|
||||||
VStack (alignment: .leading){
|
|
||||||
if self.isSensitive {
|
ScrollView {
|
||||||
TextField("Write content warning", text: $spoilerText, axis: .vertical)
|
VStack (alignment: .leading){
|
||||||
.padding(8)
|
if self.isSensitive {
|
||||||
.lineLimit(1...2)
|
TextField("Write content warning", text: $spoilerText, axis: .vertical)
|
||||||
.focused($focusedField, equals: .spoilerText)
|
.padding(8)
|
||||||
.keyboardType(.default)
|
.lineLimit(1...2)
|
||||||
.background(Color.dangerColor.opacity(0.4))
|
.focused($focusedField, equals: .spoilerText)
|
||||||
}
|
.keyboardType(.default)
|
||||||
|
.background(Color.dangerColor.opacity(0.4))
|
||||||
if self.commentsDisabled {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Text("Comments will be disabled")
|
|
||||||
.textCase(.uppercase)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.dangerColor)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let accountData = applicationState.account {
|
|
||||||
HStack {
|
|
||||||
UsernameRow(
|
|
||||||
accountId: accountData.id,
|
|
||||||
accountAvatar: accountData.avatar,
|
|
||||||
accountDisplayName: accountData.displayName,
|
|
||||||
accountUsername: accountData.username)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
if self.commentsDisabled {
|
||||||
Menu {
|
|
||||||
Button {
|
|
||||||
self.visibility = .pub
|
|
||||||
self.visibilityText = "Everyone"
|
|
||||||
self.visibilityImage = "globe.europe.africa"
|
|
||||||
} label: {
|
|
||||||
Label("Everyone", systemImage: "globe.europe.africa")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
self.visibility = .unlisted
|
|
||||||
self.visibilityText = "Unlisted"
|
|
||||||
self.visibilityImage = "lock.open"
|
|
||||||
} label: {
|
|
||||||
Label("Unlisted", systemImage: "lock.open")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
self.visibility = .priv
|
|
||||||
self.visibilityText = "Followers"
|
|
||||||
self.visibilityImage = "lock"
|
|
||||||
} label: {
|
|
||||||
Label("Followers", systemImage: "lock")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
HStack {
|
||||||
Label(self.visibilityText, systemImage: self.visibilityImage)
|
Spacer()
|
||||||
Image(systemName: "chevron.down")
|
Text("Comments will be disabled")
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.dangerColor)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.accentColor, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
if let accountData = applicationState.account {
|
||||||
|
HStack {
|
||||||
if let name = self.place?.name, let country = self.place?.country {
|
UsernameRow(
|
||||||
Group {
|
accountId: accountData.id,
|
||||||
Image(systemName: "mappin.and.ellipse")
|
accountAvatar: accountData.avatar,
|
||||||
Text("\(name), \(country)")
|
accountDisplayName: accountData.displayName,
|
||||||
|
accountUsername: accountData.username)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.foregroundColor(.lightGrayColor)
|
.padding(.horizontal, 8)
|
||||||
.padding(.trailing, 8)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.font(.footnote)
|
HStack {
|
||||||
.padding(.horizontal, 8)
|
Menu {
|
||||||
|
Button {
|
||||||
|
self.visibility = .pub
|
||||||
TextField(self.placeholder(), text: $text, axis: .vertical)
|
self.visibilityText = "Everyone"
|
||||||
|
self.visibilityImage = "globe.europe.africa"
|
||||||
|
} label: {
|
||||||
|
Label("Everyone", systemImage: "globe.europe.africa")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
self.visibility = .unlisted
|
||||||
|
self.visibilityText = "Unlisted"
|
||||||
|
self.visibilityImage = "lock.open"
|
||||||
|
} label: {
|
||||||
|
Label("Unlisted", systemImage: "lock.open")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
self.visibility = .priv
|
||||||
|
self.visibilityText = "Followers"
|
||||||
|
self.visibilityImage = "lock"
|
||||||
|
} label: {
|
||||||
|
Label("Followers", systemImage: "lock")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Label(self.visibilityText, systemImage: self.visibilityImage)
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(Color.accentColor, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let name = self.place?.name, let country = self.place?.country {
|
||||||
|
Group {
|
||||||
|
Image(systemName: "mappin.and.ellipse")
|
||||||
|
Text("\(name), \(country)")
|
||||||
|
}
|
||||||
|
.foregroundColor(.lightGrayColor)
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
|
||||||
|
|
||||||
|
TextView($textFieldViewModel.text, getTextView: { textView in
|
||||||
|
self.textFieldViewModel.textView = textView
|
||||||
|
})
|
||||||
|
.placeholder(self.placeholder())
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.lineLimit(2...12)
|
|
||||||
.focused($focusedField, equals: .content)
|
.focused($focusedField, equals: .content)
|
||||||
.keyboardType(.default)
|
|
||||||
.onFirstAppear {
|
.onFirstAppear {
|
||||||
self.focusedField = .content
|
self.focusedField = .content
|
||||||
}
|
}
|
||||||
.onChange(of: self.text) { newValue in
|
|
||||||
self.refreshScreenState()
|
HStack(alignment: .center) {
|
||||||
}
|
LazyVGrid(columns: [GridItem(.adaptive(minimum:80))]) {
|
||||||
.toolbar {
|
ForEach(self.photosAttachment, id: \.id) { photoAttachment in
|
||||||
self.keyboardToolbar()
|
ImageUploadView(photoAttachment: photoAttachment) {
|
||||||
}
|
self.showSheet = .photoDetails(photoAttachment)
|
||||||
|
} delete: {
|
||||||
HStack(alignment: .center) {
|
self.photosAttachment = self.photosAttachment.filter({ item in
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum:80))]) {
|
item != photoAttachment
|
||||||
ForEach(self.photosAttachment, id: \.id) { photoAttachment in
|
})
|
||||||
ImageUploadView(photoAttachment: photoAttachment) {
|
|
||||||
self.showSheet = .photoDetails(photoAttachment)
|
self.selectedItems = self.selectedItems.filter({ item in
|
||||||
} delete: {
|
item != photoAttachment.photosPickerItem
|
||||||
self.photosAttachment = self.photosAttachment.filter({ item in
|
})
|
||||||
item != photoAttachment
|
|
||||||
})
|
|
||||||
|
|
||||||
self.selectedItems = self.selectedItems.filter({ item in
|
|
||||||
item != photoAttachment.photosPickerItem
|
|
||||||
})
|
|
||||||
|
|
||||||
self.refreshScreenState()
|
|
||||||
} upload: {
|
|
||||||
Task {
|
|
||||||
photoAttachment.error = nil
|
|
||||||
await self.upload(photoAttachment)
|
|
||||||
self.refreshScreenState()
|
self.refreshScreenState()
|
||||||
|
} upload: {
|
||||||
|
Task {
|
||||||
|
photoAttachment.error = nil
|
||||||
|
await self.upload(photoAttachment)
|
||||||
|
self.refreshScreenState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
|
|
||||||
if let status = self.statusViewModel {
|
|
||||||
HStack (alignment: .top) {
|
|
||||||
UserAvatar(accountAvatar: status.account.avatar, size: .comment)
|
|
||||||
|
|
||||||
VStack (alignment: .leading, spacing: 0) {
|
|
||||||
HStack (alignment: .top) {
|
|
||||||
Text(statusViewModel?.account.displayNameWithoutEmojis ?? "")
|
|
||||||
.foregroundColor(.mainTextColor)
|
|
||||||
.font(.footnote)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownFormattedText(status.content.asMarkdown, withFontSize: 14, andWidth: contentWidth)
|
|
||||||
.environment(\.openURL, OpenURLAction { url in .handled })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(Color.selectedRowColor)
|
|
||||||
|
if let status = self.statusViewModel {
|
||||||
|
HStack (alignment: .top) {
|
||||||
|
UserAvatar(accountAvatar: status.account.avatar, size: .comment)
|
||||||
|
|
||||||
|
VStack (alignment: .leading, spacing: 0) {
|
||||||
|
HStack (alignment: .top) {
|
||||||
|
Text(statusViewModel?.account.displayNameWithoutEmojis ?? "")
|
||||||
|
.foregroundColor(.mainTextColor)
|
||||||
|
.font(.footnote)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownFormattedText(status.content.asMarkdown, withFontSize: 14, andWidth: contentWidth)
|
||||||
|
.environment(\.openURL, OpenURLAction { url in .handled })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.background(Color.selectedRowColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onTapGesture {
|
self.keyboardToolbar()
|
||||||
self.hideKeyboard()
|
|
||||||
}
|
}
|
||||||
.frame(alignment: .topLeading)
|
.frame(alignment: .topLeading)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
@ -240,6 +244,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: self.textFieldViewModel.text) { newValue in
|
||||||
|
self.refreshScreenState()
|
||||||
|
}
|
||||||
.onChange(of: self.selectedItems) { selectedItem in
|
.onChange(of: self.selectedItems) { selectedItem in
|
||||||
Task {
|
Task {
|
||||||
await self.loadPhotos()
|
await self.loadPhotos()
|
||||||
|
@ -266,10 +273,12 @@ struct ComposeView: View {
|
||||||
.interactiveDismissDisabled(self.interactiveDismissDisabled)
|
.interactiveDismissDisabled(self.interactiveDismissDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
private func keyboardToolbar() -> some View {
|
||||||
private func keyboardToolbar() -> some ToolbarContent {
|
VStack(spacing: 0) {
|
||||||
ToolbarItemGroup(placement: .keyboard) {
|
Divider()
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center, spacing: 22) {
|
||||||
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
self.focusedField = .unknown
|
self.focusedField = .unknown
|
||||||
|
@ -277,7 +286,7 @@ struct ComposeView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle")
|
Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation(.easeInOut) {
|
withAnimation(.easeInOut) {
|
||||||
self.isSensitive.toggle()
|
self.isSensitive.toggle()
|
||||||
|
@ -299,7 +308,7 @@ struct ComposeView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: self.commentsDisabled ? "person.2.slash" : "person.2.fill")
|
Image(systemName: self.commentsDisabled ? "person.2.slash" : "person.2.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
if self.place != nil {
|
if self.place != nil {
|
||||||
withAnimation(.easeInOut) {
|
withAnimation(.easeInOut) {
|
||||||
|
@ -313,24 +322,27 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
self.text.append("#")
|
self.textFieldViewModel.append(content: "#")
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "number")
|
Image(systemName: "number")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
self.text.append("@")
|
self.textFieldViewModel.append(content: "@")
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "at")
|
Image(systemName: "at")
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("\(self.applicationState.statusMaxCharacters - text.string.utf16.count)")
|
Text("\(self.applicationState.statusMaxCharacters - textFieldViewModel.text.string.utf16.count)")
|
||||||
.foregroundColor(.lightGrayColor)
|
.foregroundColor(.lightGrayColor)
|
||||||
|
.font(.system(size: 16.0))
|
||||||
}
|
}
|
||||||
|
.padding(8)
|
||||||
.font(.system(size: self.keyboardFontSize))
|
.font(.system(size: self.keyboardFontSize))
|
||||||
}
|
}
|
||||||
|
.background(Color.keyboardToolbarColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func placeholder() -> String {
|
private func placeholder() -> String {
|
||||||
|
@ -339,7 +351,7 @@ struct ComposeView: View {
|
||||||
|
|
||||||
private func isPublishButtonDisabled() -> Bool {
|
private func isPublishButtonDisabled() -> Bool {
|
||||||
// Publish always disabled when there is not status text.
|
// Publish always disabled when there is not status text.
|
||||||
if self.text.isEmpty {
|
if self.textFieldViewModel.text.string.isEmpty {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,7 +369,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isInteractiveDismissDisabled() -> Bool {
|
private func isInteractiveDismissDisabled() -> Bool {
|
||||||
if self.text.isEmpty == false {
|
if self.textFieldViewModel.text.string.isEmpty == false {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,8 +406,8 @@ struct ComposeView: View {
|
||||||
|
|
||||||
// Now we have to get from photos images as JPEG.
|
// Now we have to get from photos images as JPEG.
|
||||||
for item in self.photosAttachment.filter({ $0.photoData == nil }) {
|
for item in self.photosAttachment.filter({ $0.photoData == nil }) {
|
||||||
if var imageFileTransferable = try await item.photosPickerItem.loadTransferable(type: ImageFileTranseferable.self) {
|
if let data = try await item.photosPickerItem.loadTransferable(type: Data.self) {
|
||||||
item.photoData = imageFileTransferable.data
|
item.photoData = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,7 +499,7 @@ struct ComposeView: View {
|
||||||
|
|
||||||
private func createStatus() -> Pixelfed.Statuses.Components {
|
private func createStatus() -> Pixelfed.Statuses.Components {
|
||||||
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id,
|
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id,
|
||||||
text: self.text,
|
text: self.textFieldViewModel.text.string,
|
||||||
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
|
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
|
||||||
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
|
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
|
||||||
visibility: self.visibility,
|
visibility: self.visibility,
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public class TextFieldViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
var textView: UITextView?
|
||||||
|
|
||||||
|
var selectedRange: NSRange {
|
||||||
|
get {
|
||||||
|
guard let textView else {
|
||||||
|
return .init(location: 0, length: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return textView.selectedRange
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
textView?.selectedRange = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var markedTextRange: UITextRange? {
|
||||||
|
guard let textView else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return textView.markedTextRange
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var text = NSMutableAttributedString(string: "") {
|
||||||
|
didSet {
|
||||||
|
let range = selectedRange
|
||||||
|
processText()
|
||||||
|
// checkEmbed()
|
||||||
|
textView?.attributedText = text
|
||||||
|
selectedRange = range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var urlLengthAdjustments: Int = 0
|
||||||
|
private let maxLengthOfUrl = 23
|
||||||
|
|
||||||
|
public func append(content: String) {
|
||||||
|
let attrString = self.text
|
||||||
|
attrString.append(NSAttributedString(string: content))
|
||||||
|
self.text = attrString
|
||||||
|
|
||||||
|
selectedRange.location += content.utf16.count
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processText() {
|
||||||
|
guard markedTextRange == nil else { return }
|
||||||
|
|
||||||
|
text.addAttributes([.foregroundColor: UIColor(Color.label),
|
||||||
|
.font: UIFont.systemFont(ofSize: TextView.bodyFontSize),
|
||||||
|
.backgroundColor: UIColor.clear,
|
||||||
|
.underlineColor: UIColor.clear],
|
||||||
|
range: NSMakeRange(0, text.string.utf16.count))
|
||||||
|
|
||||||
|
let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})"
|
||||||
|
let mentionPattern = "(@+[a-zA-Z0-9(_).-]{1,})"
|
||||||
|
let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)"
|
||||||
|
|
||||||
|
do {
|
||||||
|
let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: [])
|
||||||
|
let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: [])
|
||||||
|
let urlRegex = try NSRegularExpression(pattern: urlPattern, options: [])
|
||||||
|
|
||||||
|
let range = NSMakeRange(0, text.string.utf16.count)
|
||||||
|
var ranges = hashtagRegex.matches(in: text.string, options: [], range: range).map { $0.range }
|
||||||
|
ranges.append(contentsOf: mentionRegex.matches(in: text.string, options: [], range: range).map { $0.range })
|
||||||
|
|
||||||
|
let urlRanges = urlRegex.matches(in: text.string, options: [], range: range).map { $0.range }
|
||||||
|
|
||||||
|
// var foundSuggestionRange = false
|
||||||
|
for nsRange in ranges {
|
||||||
|
text.addAttributes([.foregroundColor: UIColor(.accentColor)], range: nsRange)
|
||||||
|
|
||||||
|
// if selectedRange.location == (nsRange.location + nsRange.length),
|
||||||
|
// let range = Range(nsRange, in: text.string) {
|
||||||
|
// foundSuggestionRange = true
|
||||||
|
// currentSuggestionRange = nsRange
|
||||||
|
// loadAutoCompleteResults(query: String(text.string[range]))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// if !foundSuggestionRange || ranges.isEmpty {
|
||||||
|
// resetAutoCompletion()
|
||||||
|
// }
|
||||||
|
|
||||||
|
var totalUrlLength = 0
|
||||||
|
var numUrls = 0
|
||||||
|
|
||||||
|
for range in urlRanges {
|
||||||
|
if range.length > maxLengthOfUrl {
|
||||||
|
numUrls += 1
|
||||||
|
totalUrlLength += range.length
|
||||||
|
}
|
||||||
|
|
||||||
|
text.addAttributes([.foregroundColor: UIColor(.accentColor),
|
||||||
|
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||||
|
.underlineColor: UIColor(.accentColor)],
|
||||||
|
range: NSRange(location: range.location, length: range.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
urlLengthAdjustments = totalUrlLength - (maxLengthOfUrl * numUrls)
|
||||||
|
|
||||||
|
text.enumerateAttributes(in: range) { attributes, range, _ in
|
||||||
|
if attributes[.link] != nil {
|
||||||
|
text.removeAttribute(.link, range: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct TextView: View {
|
||||||
|
@Environment(\.layoutDirection) private var layoutDirection
|
||||||
|
|
||||||
|
@Binding private var text: NSMutableAttributedString
|
||||||
|
@Binding private var isEmpty: Bool
|
||||||
|
|
||||||
|
@State private var calculatedHeight: CGFloat = 44
|
||||||
|
|
||||||
|
private var getTextView: ((UITextView) -> Void)?
|
||||||
|
public static let bodyFontSize = 17.0
|
||||||
|
|
||||||
|
var placeholderView: AnyView?
|
||||||
|
var keyboard: UIKeyboardType = .default
|
||||||
|
|
||||||
|
public init(_ text: Binding<NSMutableAttributedString>,
|
||||||
|
getTextView: ((UITextView) -> Void)? = nil)
|
||||||
|
{
|
||||||
|
_text = text
|
||||||
|
_isEmpty = Binding(
|
||||||
|
get: { text.wrappedValue.string.isEmpty },
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
|
||||||
|
self.getTextView = getTextView
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Representable(
|
||||||
|
text: $text,
|
||||||
|
calculatedHeight: $calculatedHeight,
|
||||||
|
keyboard: keyboard,
|
||||||
|
getTextView: getTextView
|
||||||
|
)
|
||||||
|
.frame(
|
||||||
|
minHeight: calculatedHeight,
|
||||||
|
maxHeight: calculatedHeight
|
||||||
|
)
|
||||||
|
.background(
|
||||||
|
placeholderView?
|
||||||
|
.foregroundColor(Color(.placeholderText))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(Font.body)
|
||||||
|
.padding(.horizontal, 0)
|
||||||
|
.padding(.vertical, 0)
|
||||||
|
.opacity(isEmpty ? 1 : 0),
|
||||||
|
alignment: .topLeading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UIKitTextView: UITextView {
|
||||||
|
override var keyCommands: [UIKeyCommand]? {
|
||||||
|
return (super.keyCommands ?? []) + [
|
||||||
|
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func escape(_: Any) {
|
||||||
|
resignFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextView {
|
||||||
|
struct Representable: UIViewRepresentable {
|
||||||
|
|
||||||
|
@Binding var text: NSMutableAttributedString
|
||||||
|
@Binding var calculatedHeight: CGFloat
|
||||||
|
|
||||||
|
let keyboard: UIKeyboardType
|
||||||
|
var getTextView: ((UITextView) -> Void)?
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIKitTextView {
|
||||||
|
context.coordinator.textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_: UIKitTextView, context: Context) {
|
||||||
|
context.coordinator.update(representable: self)
|
||||||
|
if !context.coordinator.didBecomeFirstResponder {
|
||||||
|
context.coordinator.textView.becomeFirstResponder()
|
||||||
|
context.coordinator.didBecomeFirstResponder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(
|
||||||
|
text: $text,
|
||||||
|
calculatedHeight: $calculatedHeight,
|
||||||
|
getTextView: getTextView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextView.Representable {
|
||||||
|
final class Coordinator: NSObject, UITextViewDelegate {
|
||||||
|
internal let textView: UIKitTextView
|
||||||
|
|
||||||
|
private var originalText: NSMutableAttributedString = .init()
|
||||||
|
private var text: Binding<NSMutableAttributedString>
|
||||||
|
private var calculatedHeight: Binding<CGFloat>
|
||||||
|
|
||||||
|
var didBecomeFirstResponder = false
|
||||||
|
|
||||||
|
var getTextView: ((UITextView) -> Void)?
|
||||||
|
|
||||||
|
init(text: Binding<NSMutableAttributedString>,
|
||||||
|
calculatedHeight: Binding<CGFloat>,
|
||||||
|
getTextView: ((UITextView) -> Void)?) {
|
||||||
|
|
||||||
|
textView = UIKitTextView()
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
|
||||||
|
self.text = text
|
||||||
|
self.calculatedHeight = calculatedHeight
|
||||||
|
self.getTextView = getTextView
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
textView.delegate = self
|
||||||
|
|
||||||
|
textView.font = .systemFont(ofSize: TextView.bodyFontSize)
|
||||||
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
|
textView.autocapitalizationType = .sentences
|
||||||
|
textView.autocorrectionType = .yes
|
||||||
|
textView.isEditable = true
|
||||||
|
textView.isSelectable = true
|
||||||
|
textView.dataDetectorTypes = []
|
||||||
|
textView.allowsEditingTextAttributes = false
|
||||||
|
textView.returnKeyType = .default
|
||||||
|
textView.allowsEditingTextAttributes = true
|
||||||
|
|
||||||
|
self.getTextView?(textView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidBeginEditing(_: UITextView) {
|
||||||
|
originalText = text.wrappedValue
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.recalculateHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.text.wrappedValue = NSMutableAttributedString(attributedString: textView.attributedText)
|
||||||
|
self.recalculateHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText _: String) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextView.Representable.Coordinator {
|
||||||
|
func update(representable: TextView.Representable) {
|
||||||
|
textView.keyboardType = representable.keyboard
|
||||||
|
recalculateHeight()
|
||||||
|
textView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recalculateHeight() {
|
||||||
|
let newSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude))
|
||||||
|
guard calculatedHeight.wrappedValue != newSize.height else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async { // call in next render cycle.
|
||||||
|
self.calculatedHeight.wrappedValue = newSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension TextView {
|
||||||
|
/// Specify a placeholder text
|
||||||
|
/// - Parameter placeholder: The placeholder text
|
||||||
|
func placeholder(_ placeholder: String) -> TextView {
|
||||||
|
self.placeholder(placeholder) { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify a placeholder with the specified configuration
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// TextView($text)
|
||||||
|
/// .placeholder("placeholder") { view in
|
||||||
|
/// view.foregroundColor(.red)
|
||||||
|
/// }
|
||||||
|
func placeholder<V: View>(_ placeholder: String, _ configure: (Text) -> V) -> TextView {
|
||||||
|
var view = self
|
||||||
|
let text = Text(placeholder)
|
||||||
|
view.placeholderView = AnyView(configure(text))
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify a custom placeholder view
|
||||||
|
func placeholder<V: View>(_ placeholder: V) -> TextView {
|
||||||
|
var view = self
|
||||||
|
view.placeholderView = AnyView(placeholder)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func setKeyboardType(_ keyboardType: UIKeyboardType) -> TextView {
|
||||||
|
var view = self
|
||||||
|
view.keyboard = keyboardType
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue