mirror of
https://github.com/writeas/writefreely-swiftui-multiplatform.git
synced 2024-11-15 01:11:02 +00:00
Merge branch 'main' into fix-crash-on-launch
This commit is contained in:
commit
efae5202f8
@ -7,7 +7,7 @@ struct ContentView: View {
|
|||||||
NavigationView {
|
NavigationView {
|
||||||
SidebarView()
|
SidebarView()
|
||||||
|
|
||||||
PostListView(selectedCollection: nil, showAllPosts: true)
|
PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn)
|
||||||
|
|
||||||
Text("Select a post, or create a new local draft.")
|
Text("Select a post, or create a new local draft.")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -10,10 +10,10 @@ struct CollectionListView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) {
|
|
||||||
Text("All Posts")
|
|
||||||
}
|
|
||||||
if model.account.isLoggedIn {
|
if model.account.isLoggedIn {
|
||||||
|
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) {
|
||||||
|
Text("All Posts")
|
||||||
|
}
|
||||||
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
|
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
|
||||||
Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")
|
Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")
|
||||||
}
|
}
|
||||||
@ -26,6 +26,10 @@ struct CollectionListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
|
||||||
|
Text("Drafts")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(
|
.navigationTitle(
|
||||||
|
@ -3,8 +3,8 @@ import CoreData
|
|||||||
|
|
||||||
enum PostAppearance: String {
|
enum PostAppearance: String {
|
||||||
case sans = "OpenSans-Regular"
|
case sans = "OpenSans-Regular"
|
||||||
case mono = "Hack"
|
case mono = "Hack-Regular"
|
||||||
case serif = "Lora"
|
case serif = "Lora-Regular"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PostEditorModel {
|
struct PostEditorModel {
|
||||||
|
@ -53,6 +53,8 @@
|
|||||||
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; };
|
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; };
|
||||||
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; };
|
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; };
|
||||||
17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; };
|
17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; };
|
||||||
|
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */; };
|
||||||
|
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A6325489E900057D763 /* PostBodyTextView.swift */; };
|
||||||
17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; };
|
17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; };
|
||||||
17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; };
|
17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; };
|
||||||
17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; };
|
17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; };
|
||||||
@ -141,6 +143,8 @@
|
|||||||
17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
|
||||||
17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = "<group>"; };
|
17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = "<group>"; };
|
||||||
|
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTitleTextView.swift; sourceTree = "<group>"; };
|
||||||
|
17AD0A6325489E900057D763 /* PostBodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBodyTextView.swift; sourceTree = "<group>"; };
|
||||||
17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||||
17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
|
17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
|
||||||
@ -294,6 +298,8 @@
|
|||||||
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
|
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
|
||||||
173E19D0254318F600440F0F /* RemoteChangePromptView.swift */,
|
173E19D0254318F600440F0F /* RemoteChangePromptView.swift */,
|
||||||
173E19E2254329CC00440F0F /* PostTextEditingView.swift */,
|
173E19E2254329CC00440F0F /* PostTextEditingView.swift */,
|
||||||
|
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */,
|
||||||
|
17AD0A6325489E900057D763 /* PostBodyTextView.swift */,
|
||||||
);
|
);
|
||||||
path = PostEditor;
|
path = PostEditor;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -694,6 +700,8 @@
|
|||||||
170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */,
|
170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */,
|
||||||
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
|
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
|
||||||
17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
|
17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
|
||||||
|
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */,
|
||||||
|
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */,
|
||||||
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
|
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
|
||||||
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
|
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
|
||||||
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
|
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
|
||||||
|
124
iOS/PostEditor/PostBodyTextView.swift
Normal file
124
iOS/PostEditor/PostBodyTextView.swift
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Based on https://stackoverflow.com/a/56508132/1234545 and https://stackoverflow.com/a/48360549/1234545
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
|
||||||
|
@Binding var text: String
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
@Binding var currentTextPosition: UITextRange?
|
||||||
|
var lineSpacingMultiplier: CGFloat
|
||||||
|
var didBecomeFirstResponder: Bool = false
|
||||||
|
var postBodyTextView: PostBodyTextView
|
||||||
|
|
||||||
|
weak var textView: UITextView?
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ textView: PostBodyTextView,
|
||||||
|
text: Binding<String>,
|
||||||
|
isFirstResponder: Binding<Bool>,
|
||||||
|
currentTextPosition: Binding<UITextRange?>,
|
||||||
|
lineSpacingMultiplier: CGFloat
|
||||||
|
) {
|
||||||
|
self.postBodyTextView = textView
|
||||||
|
_text = text
|
||||||
|
_isFirstResponder = isFirstResponder
|
||||||
|
self.lineSpacingMultiplier = lineSpacingMultiplier
|
||||||
|
_currentTextPosition = currentTextPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.postBodyTextView.text = textView.text ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
self.currentTextPosition = textView.selectedTextRange
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
|
if let textPosition = currentTextPosition {
|
||||||
|
textView.selectedTextRange = textPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
|
self.isFirstResponder = false
|
||||||
|
self.didBecomeFirstResponder = false
|
||||||
|
self.currentTextPosition = textView.selectedTextRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func layoutManager(
|
||||||
|
_ layoutManager: NSLayoutManager,
|
||||||
|
lineSpacingAfterGlyphAt glyphIndex: Int,
|
||||||
|
withProposedLineFragmentRect rect: CGRect
|
||||||
|
) -> CGFloat {
|
||||||
|
// HACK: - This seems to be the only way to get line spacing to update dynamically on iPad
|
||||||
|
// when switching between full-screen, split-screen, and slide-over views.
|
||||||
|
if let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first {
|
||||||
|
// Get the width of the window to determine the size class
|
||||||
|
if window.frame.width < 600 {
|
||||||
|
// Use 0.25 multiplier for compact size class
|
||||||
|
return 17 * 0.25
|
||||||
|
} else {
|
||||||
|
// Use 0.5 multiplier otherwise
|
||||||
|
return 17 * 0.5
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 17 * lineSpacingMultiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostBodyTextView: UIViewRepresentable {
|
||||||
|
@Binding var text: String
|
||||||
|
@Binding var textStyle: UIFont
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
@Binding var currentTextPosition: UITextRange?
|
||||||
|
@State var lineSpacing: CGFloat
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<PostBodyTextView>) -> UITextView {
|
||||||
|
let textView = UITextView(frame: .zero)
|
||||||
|
|
||||||
|
textView.isEditable = true
|
||||||
|
textView.isUserInteractionEnabled = true
|
||||||
|
textView.isScrollEnabled = true
|
||||||
|
textView.alwaysBounceVertical = false
|
||||||
|
|
||||||
|
context.coordinator.textView = textView
|
||||||
|
textView.delegate = context.coordinator
|
||||||
|
textView.layoutManager.delegate = context.coordinator
|
||||||
|
|
||||||
|
let font = textStyle
|
||||||
|
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||||
|
textView.font = fontMetrics.scaledFont(for: font)
|
||||||
|
textView.backgroundColor = UIColor.clear
|
||||||
|
|
||||||
|
return textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> PostBodyCoordinator {
|
||||||
|
return Coordinator(
|
||||||
|
self,
|
||||||
|
text: $text,
|
||||||
|
isFirstResponder: $isFirstResponder,
|
||||||
|
currentTextPosition: $currentTextPosition,
|
||||||
|
lineSpacingMultiplier: lineSpacing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<PostBodyTextView>) {
|
||||||
|
uiView.text = text
|
||||||
|
|
||||||
|
let font = textStyle
|
||||||
|
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||||
|
uiView.font = fontMetrics.scaledFont(for: font)
|
||||||
|
|
||||||
|
// We don't want the text field to become first responder every time SwiftUI refreshes the view.
|
||||||
|
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
|
||||||
|
uiView.becomeFirstResponder()
|
||||||
|
context.coordinator.didBecomeFirstResponder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
struct PostEditorView: View {
|
struct PostEditorView: View {
|
||||||
@EnvironmentObject var model: WriteFreelyModel
|
@EnvironmentObject var model: WriteFreelyModel
|
||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
@Environment(\.managedObjectContext) var moc
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
@ObservedObject var post: WFAPost
|
@ObservedObject var post: WFAPost
|
||||||
|
@ -6,7 +6,13 @@ struct PostTextEditingView: View {
|
|||||||
@Binding var updatingTitleFromServer: Bool
|
@Binding var updatingTitleFromServer: Bool
|
||||||
@Binding var updatingBodyFromServer: Bool
|
@Binding var updatingBodyFromServer: Bool
|
||||||
@State private var appearance: PostAppearance = .serif
|
@State private var appearance: PostAppearance = .serif
|
||||||
private let bodyLineSpacingMultiplier: CGFloat = 0.5
|
@State private var titleTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 26)!
|
||||||
|
@State private var titleTextHeight: CGFloat = 50
|
||||||
|
@State private var titleIsFirstResponder: Bool = true
|
||||||
|
@State private var bodyTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 17)!
|
||||||
|
@State private var bodyIsFirstResponder: Bool = false
|
||||||
|
@State private var bodyCursorPosition: UITextRange?
|
||||||
|
private let lineSpacingMultiplier: CGFloat = 0.5
|
||||||
|
|
||||||
init(
|
init(
|
||||||
post: ObservedObject<WFAPost>,
|
post: ObservedObject<WFAPost>,
|
||||||
@ -19,41 +25,69 @@ struct PostTextEditingView: View {
|
|||||||
UITextView.appearance().backgroundColor = .clear
|
UITextView.appearance().backgroundColor = .clear
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var titleFieldHeight: CGFloat {
|
||||||
|
let minHeight: CGFloat = 50
|
||||||
|
if titleTextHeight < minHeight {
|
||||||
|
return minHeight
|
||||||
|
}
|
||||||
|
return titleTextHeight
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
TextField("Title (optional)", text: $post.title)
|
|
||||||
.font(.custom(appearance.rawValue, size: 26, relativeTo: .largeTitle))
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
.onChange(of: post.title) { _ in
|
|
||||||
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
|
|
||||||
post.status = PostStatus.edited.rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if post.body.count == 0 {
|
if post.title.count == 0 {
|
||||||
Text("Write…")
|
Text("Title (optional)")
|
||||||
.font(.custom(appearance.rawValue, size: 17, relativeTo: .body))
|
.font(Font(titleTextStyle))
|
||||||
.foregroundColor(Color(UIColor.placeholderText))
|
.foregroundColor(Color(UIColor.placeholderText))
|
||||||
.padding(.horizontal, 4)
|
.padding(.horizontal, 4)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
TextEditor(text: $post.body)
|
PostTitleTextView(
|
||||||
.font(.custom(appearance.rawValue, size: 17, relativeTo: .body))
|
text: $post.title,
|
||||||
.lineSpacing(
|
textStyle: $titleTextStyle,
|
||||||
17 * (
|
height: $titleTextHeight,
|
||||||
horizontalSizeClass == .compact ? bodyLineSpacingMultiplier / 2 : bodyLineSpacingMultiplier
|
isFirstResponder: $titleIsFirstResponder,
|
||||||
)
|
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
|
||||||
)
|
)
|
||||||
.onChange(of: post.body) { _ in
|
.frame(height: titleFieldHeight)
|
||||||
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
|
.onChange(of: post.title) { _ in
|
||||||
post.status = PostStatus.edited.rawValue
|
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
|
||||||
}
|
post.status = PostStatus.edited.rawValue
|
||||||
if updatingBodyFromServer {
|
|
||||||
updatingBodyFromServer = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if updatingTitleFromServer {
|
||||||
|
updatingTitleFromServer = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
if post.body.count == 0 {
|
||||||
|
Text("Write…")
|
||||||
|
.font(Font(bodyTextStyle))
|
||||||
|
.foregroundColor(Color(UIColor.placeholderText))
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
PostBodyTextView(
|
||||||
|
text: $post.body,
|
||||||
|
textStyle: $bodyTextStyle,
|
||||||
|
isFirstResponder: $bodyIsFirstResponder,
|
||||||
|
currentTextPosition: $bodyCursorPosition,
|
||||||
|
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
|
||||||
|
)
|
||||||
|
.onChange(of: post.body) { _ in
|
||||||
|
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
|
||||||
|
post.status = PostStatus.edited.rawValue
|
||||||
|
}
|
||||||
|
if updatingBodyFromServer {
|
||||||
|
updatingBodyFromServer = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: titleIsFirstResponder, perform: { _ in
|
||||||
|
self.bodyIsFirstResponder.toggle()
|
||||||
|
})
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
switch post.appearance {
|
switch post.appearance {
|
||||||
case "sans":
|
case "sans":
|
||||||
@ -63,6 +97,8 @@ struct PostTextEditingView: View {
|
|||||||
default:
|
default:
|
||||||
self.appearance = .serif
|
self.appearance = .serif
|
||||||
}
|
}
|
||||||
|
self.titleTextStyle = UIFont(name: appearance.rawValue, size: 26)!
|
||||||
|
self.bodyTextStyle = UIFont(name: appearance.rawValue, size: 17)!
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
127
iOS/PostEditor/PostTitleTextView.swift
Normal file
127
iOS/PostEditor/PostTitleTextView.swift
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// Based on https://lostmoa.com/blog/DynamicHeightForTextFieldInSwiftUI and https://stackoverflow.com/a/56508132/1234545
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class PostTitleCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
|
||||||
|
@Binding var text: String
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
var lineSpacingMultiplier: CGFloat
|
||||||
|
var didBecomeFirstResponder: Bool = false
|
||||||
|
var postTitleTextView: PostTitleTextView
|
||||||
|
|
||||||
|
weak var textView: UITextView?
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ textView: PostTitleTextView,
|
||||||
|
text: Binding<String>,
|
||||||
|
isFirstResponder: Binding<Bool>,
|
||||||
|
lineSpacingMultiplier: CGFloat
|
||||||
|
) {
|
||||||
|
self.postTitleTextView = textView
|
||||||
|
_text = text
|
||||||
|
_isFirstResponder = isFirstResponder
|
||||||
|
self.lineSpacingMultiplier = lineSpacingMultiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.postTitleTextView.text = textView.text ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
if text == "\n" {
|
||||||
|
self.isFirstResponder.toggle()
|
||||||
|
self.didBecomeFirstResponder = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func layoutManager(
|
||||||
|
_ layoutManager: NSLayoutManager,
|
||||||
|
didCompleteLayoutFor textContainer: NSTextContainer?,
|
||||||
|
atEnd layoutFinishedFlag: Bool
|
||||||
|
) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let view = self.textView else { return }
|
||||||
|
let size = view.sizeThatFits(view.bounds.size)
|
||||||
|
if self.postTitleTextView.height != size.height {
|
||||||
|
self.postTitleTextView.height = size.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func layoutManager(
|
||||||
|
_ layoutManager: NSLayoutManager,
|
||||||
|
lineSpacingAfterGlyphAt glyphIndex: Int,
|
||||||
|
withProposedLineFragmentRect rect: CGRect
|
||||||
|
) -> CGFloat {
|
||||||
|
// HACK: - This seems to be the only way to get line spacing to update dynamically on iPad
|
||||||
|
// when switching between full-screen, split-screen, and slide-over views.
|
||||||
|
if let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first {
|
||||||
|
// Get the width of the window to determine the size class
|
||||||
|
if window.frame.width < 600 {
|
||||||
|
// Use 0.25 multiplier for compact size class
|
||||||
|
return 17 * 0.25
|
||||||
|
} else {
|
||||||
|
// Use 0.5 multiplier otherwise
|
||||||
|
return 17 * 0.5
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 17 * lineSpacingMultiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostTitleTextView: UIViewRepresentable {
|
||||||
|
@Binding var text: String
|
||||||
|
@Binding var textStyle: UIFont
|
||||||
|
@Binding var height: CGFloat
|
||||||
|
@Binding var isFirstResponder: Bool
|
||||||
|
@State var lineSpacing: CGFloat
|
||||||
|
|
||||||
|
func makeUIView(context: UIViewRepresentableContext<PostTitleTextView>) -> UITextView {
|
||||||
|
let textView = UITextView()
|
||||||
|
|
||||||
|
textView.isEditable = true
|
||||||
|
textView.isUserInteractionEnabled = true
|
||||||
|
textView.isScrollEnabled = true
|
||||||
|
textView.alwaysBounceVertical = false
|
||||||
|
|
||||||
|
context.coordinator.textView = textView
|
||||||
|
textView.delegate = context.coordinator
|
||||||
|
textView.layoutManager.delegate = context.coordinator
|
||||||
|
|
||||||
|
let font = textStyle
|
||||||
|
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||||
|
textView.font = fontMetrics.scaledFont(for: font)
|
||||||
|
|
||||||
|
textView.backgroundColor = UIColor.clear
|
||||||
|
|
||||||
|
return textView
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> PostTitleCoordinator {
|
||||||
|
return Coordinator(
|
||||||
|
self,
|
||||||
|
text: $text,
|
||||||
|
isFirstResponder: $isFirstResponder,
|
||||||
|
lineSpacingMultiplier: lineSpacing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<PostTitleTextView>) {
|
||||||
|
uiView.text = text
|
||||||
|
|
||||||
|
let font = textStyle
|
||||||
|
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||||
|
uiView.font = fontMetrics.scaledFont(for: font)
|
||||||
|
|
||||||
|
// We don't want the text field to become first responder every time SwiftUI refreshes the view.
|
||||||
|
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
|
||||||
|
uiView.becomeFirstResponder()
|
||||||
|
context.coordinator.didBecomeFirstResponder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user