mirror of
https://github.com/writeas/writefreely-swiftui-multiplatform.git
synced 2024-11-15 01:11:02 +00:00
Improve editor scrolling on ios (#229)
* Unset isScrollEnabled property on UITextViews * Begin implementing common MultilineTextView * Remove legacy text views * Fix firstResponder issues * Bump version and build number and update change log * Fix smart-dashes replacement in MultilineTextView * Wait 10ms before navigating to the editor after creating a new post * Wait before navigating to editor after creating a new post * Bump build number
This commit is contained in:
parent
ceb7eaa2ac
commit
f256996161
@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- [Mac] Fixed a bug where alerts weren't presented for login errors.
|
||||
- [Mac] Fixed some build warnings in the project.
|
||||
- [Mac] Bumped WriteFreely package to v0.3.6 to handle decoding of fractional seconds in dates.
|
||||
- [iOS] Fixed an issue that made it tricky to scroll in the post editor.
|
||||
- [iOS] Fixed a bug that didn't navigate to the post editor after tapping the new-post button.
|
||||
|
||||
## [1.0.12-ios] - 2022-10-06
|
||||
|
||||
|
@ -47,7 +47,9 @@ struct PostListView: View {
|
||||
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
|
||||
withAnimation {
|
||||
self.model.showAllPosts = false
|
||||
self.model.selectedPost = managedPost
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.model.selectedPost = managedPost
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
ZStack {
|
||||
|
@ -14,7 +14,7 @@ struct PostStatusBadgeView: View {
|
||||
.padding(EdgeInsets(top: 2.5, leading: 7.5, bottom: 2.5, trailing: 7.5))
|
||||
.background(badgeColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5.0, style: .circular))
|
||||
.frame(width: .infinity)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
func setupBadgeProperties(for status: PostStatus) -> (String, Color) {
|
||||
|
@ -90,8 +90,6 @@
|
||||
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; };
|
||||
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.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 */; };
|
||||
17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; };
|
||||
17B37C4C25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; };
|
||||
17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */; };
|
||||
@ -138,6 +136,7 @@
|
||||
17DFDE8B251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; };
|
||||
17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; };
|
||||
17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; };
|
||||
375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -227,8 +226,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+Keychain.swift"; sourceTree = "<group>"; };
|
||||
17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+API.swift"; sourceTree = "<group>"; };
|
||||
17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+APIHandlers.swift"; sourceTree = "<group>"; };
|
||||
@ -270,6 +267,7 @@
|
||||
17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Lora-Cyrillic-OFL.txt"; sourceTree = "<group>"; };
|
||||
17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = "<group>"; };
|
||||
17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = "<group>"; };
|
||||
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -436,8 +434,7 @@
|
||||
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
|
||||
173E19D0254318F600440F0F /* RemoteChangePromptView.swift */,
|
||||
173E19E2254329CC00440F0F /* PostTextEditingView.swift */,
|
||||
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */,
|
||||
17AD0A6325489E900057D763 /* PostBodyTextView.swift */,
|
||||
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */,
|
||||
);
|
||||
path = PostEditor;
|
||||
sourceTree = "<group>";
|
||||
@ -926,13 +923,12 @@
|
||||
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
|
||||
17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */,
|
||||
17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
|
||||
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */,
|
||||
17B37C5D25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */,
|
||||
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */,
|
||||
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
|
||||
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
|
||||
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
|
||||
17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */,
|
||||
375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */,
|
||||
171DC677272C7D0B002B9B8A /* UserDefaults+Extensions.swift in Sources */,
|
||||
1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */,
|
||||
17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */,
|
||||
@ -1054,7 +1050,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 680;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
|
||||
@ -1066,7 +1062,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
MARKETING_VERSION = 1.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1085,7 +1081,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 680;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
|
||||
@ -1097,7 +1093,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
MARKETING_VERSION = 1.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1228,7 +1224,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 680;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
@ -1237,7 +1233,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
MARKETING_VERSION = 1.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform";
|
||||
PRODUCT_NAME = "WriteFreely-MultiPlatform";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1254,7 +1250,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 680;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
@ -1263,7 +1259,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
MARKETING_VERSION = 1.0.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform";
|
||||
PRODUCT_NAME = "WriteFreely-MultiPlatform";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1282,7 +1278,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 676;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1309,7 +1305,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 676;
|
||||
CURRENT_PROJECT_VERSION = 687;
|
||||
DEVELOPMENT_TEAM = TPPAB4YBA6;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1489,7 +1485,7 @@
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 2.0.0;
|
||||
minimumVersion = 2.3.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
159
iOS/PostEditor/MultilineTextView.swift
Normal file
159
iOS/PostEditor/MultilineTextView.swift
Normal file
@ -0,0 +1,159 @@
|
||||
// Credit: https://stackoverflow.com/a/58639072
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
private struct UITextViewWrapper: UIViewRepresentable {
|
||||
typealias UIViewType = UITextView
|
||||
|
||||
@Binding var text: String
|
||||
@Binding var calculatedHeight: CGFloat
|
||||
@Binding var isEditing: Bool
|
||||
var textStyle: UIFont
|
||||
var onDone: (() -> Void)?
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
||||
let textField = UITextView()
|
||||
textField.delegate = context.coordinator
|
||||
|
||||
textField.isEditable = true
|
||||
textField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
textField.isSelectable = true
|
||||
textField.isUserInteractionEnabled = true
|
||||
textField.isScrollEnabled = false
|
||||
textField.backgroundColor = UIColor.clear
|
||||
textField.smartDashesType = .no
|
||||
|
||||
let font = textStyle
|
||||
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
|
||||
textField.font = fontMetrics.scaledFont(for: font)
|
||||
|
||||
if nil != onDone {
|
||||
textField.returnKeyType = .next
|
||||
}
|
||||
|
||||
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
return textField
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
|
||||
if uiView.text != self.text {
|
||||
uiView.text = self.text
|
||||
}
|
||||
|
||||
if uiView.window != nil, isEditing {
|
||||
uiView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
|
||||
}
|
||||
|
||||
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
|
||||
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
if result.wrappedValue != newSize.height {
|
||||
DispatchQueue.main.async {
|
||||
result.wrappedValue = newSize.height // !! must be called asynchronously
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(text: $text, height: $calculatedHeight, isFirstResponder: $isEditing, onDone: onDone)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
@Binding var isFirstResponder: Bool
|
||||
var text: Binding<String>
|
||||
var calculatedHeight: Binding<CGFloat>
|
||||
var onDone: (() -> Void)?
|
||||
|
||||
init(
|
||||
text: Binding<String>,
|
||||
height: Binding<CGFloat>,
|
||||
isFirstResponder: Binding<Bool>,
|
||||
onDone: (() -> Void)? = nil
|
||||
) {
|
||||
self.text = text
|
||||
self.calculatedHeight = height
|
||||
self._isFirstResponder = isFirstResponder
|
||||
self.onDone = onDone
|
||||
}
|
||||
|
||||
func textViewDidChange(_ uiView: UITextView) {
|
||||
text.wrappedValue = uiView.text
|
||||
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if let onDone = self.onDone, text == "\n" {
|
||||
textView.resignFirstResponder()
|
||||
onDone()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
self.isFirstResponder = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct MultilineTextField: View {
|
||||
|
||||
private var placeholder: String
|
||||
private var textStyle: UIFont
|
||||
private var onCommit: (() -> Void)?
|
||||
|
||||
@Binding var isFirstResponder: Bool
|
||||
@Binding private var text: String
|
||||
private var internalText: Binding<String> {
|
||||
Binding<String>(get: { self.text }) { // swiftlint:disable:this multiple_closures_with_trailing_closure
|
||||
self.text = $0
|
||||
self.showingPlaceholder = $0.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@State private var dynamicHeight: CGFloat = 100
|
||||
@State private var showingPlaceholder = false
|
||||
|
||||
init (
|
||||
_ placeholder: String = "",
|
||||
text: Binding<String>,
|
||||
font: UIFont,
|
||||
isFirstResponder: Binding<Bool>,
|
||||
onCommit: (() -> Void)? = nil
|
||||
) {
|
||||
self.placeholder = placeholder
|
||||
self.onCommit = onCommit
|
||||
self.textStyle = font
|
||||
self._isFirstResponder = isFirstResponder
|
||||
self._text = text
|
||||
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
UITextViewWrapper(
|
||||
text: self.internalText,
|
||||
calculatedHeight: $dynamicHeight,
|
||||
isEditing: $isFirstResponder,
|
||||
textStyle: textStyle,
|
||||
onDone: onCommit
|
||||
)
|
||||
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
|
||||
.background(placeholderView, alignment: .topLeading)
|
||||
}
|
||||
|
||||
var placeholderView: some View {
|
||||
Group {
|
||||
if showingPlaceholder {
|
||||
let font = Font(textStyle)
|
||||
Text(placeholder).foregroundColor(.gray)
|
||||
.padding(.leading, 4)
|
||||
.padding(.top, 8)
|
||||
.font(font)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
// Based on https://stackoverflow.com/a/56508132 and https://stackoverflow.com/a/48360549
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
|
||||
@Binding var text: String
|
||||
@Binding var isFirstResponder: Bool
|
||||
var lineSpacingMultiplier: CGFloat
|
||||
var didBecomeFirstResponder: Bool = false
|
||||
var postBodyTextView: PostBodyTextView
|
||||
|
||||
weak var textView: UITextView?
|
||||
|
||||
init(
|
||||
_ textView: PostBodyTextView,
|
||||
text: Binding<String>,
|
||||
isFirstResponder: Binding<Bool>,
|
||||
lineSpacingMultiplier: CGFloat
|
||||
) {
|
||||
self.postBodyTextView = textView
|
||||
_text = text
|
||||
_isFirstResponder = isFirstResponder
|
||||
self.lineSpacingMultiplier = lineSpacingMultiplier
|
||||
|
||||
super.init()
|
||||
|
||||
updateSize()
|
||||
}
|
||||
|
||||
func updateSize() {
|
||||
DispatchQueue.main.async {
|
||||
guard let view = self.textView else { return }
|
||||
let size = view.sizeThatFits(view.bounds.size)
|
||||
if self.postBodyTextView.height != size.height {
|
||||
self.postBodyTextView.height = size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
DispatchQueue.main.async {
|
||||
self.postBodyTextView.text = textView.text ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
self.isFirstResponder = false
|
||||
self.didBecomeFirstResponder = false
|
||||
}
|
||||
|
||||
func layoutManager(
|
||||
_ layoutManager: NSLayoutManager,
|
||||
didCompleteLayoutFor textContainer: NSTextContainer?,
|
||||
atEnd layoutFinishedFlag: Bool
|
||||
) {
|
||||
updateSize()
|
||||
}
|
||||
|
||||
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 height: CGFloat
|
||||
@Binding var isFirstResponder: Bool
|
||||
@State var lineSpacing: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<PostBodyTextView>) -> UITextView {
|
||||
let textView = UITextView()
|
||||
|
||||
textView.isEditable = true
|
||||
textView.isUserInteractionEnabled = true
|
||||
textView.isScrollEnabled = true
|
||||
textView.alwaysBounceVertical = false
|
||||
textView.smartDashesType = .no
|
||||
|
||||
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,
|
||||
lineSpacingMultiplier: lineSpacing
|
||||
)
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<PostBodyTextView>) {
|
||||
if uiView.text != text {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,6 @@ struct PostTextEditingView: View {
|
||||
@Binding var updatingBodyFromServer: Bool
|
||||
@State private var appearance: PostAppearance = .serif
|
||||
@State private var titleTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 26)!
|
||||
@State private var titleTextHeight: CGFloat = 50
|
||||
@State private var bodyTextHeight: 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
|
||||
@ -26,77 +24,39 @@ struct PostTextEditingView: View {
|
||||
UITextView.appearance().backgroundColor = .clear
|
||||
}
|
||||
|
||||
var titleFieldHeight: CGFloat {
|
||||
let minHeight: CGFloat = textEditorHeight
|
||||
if titleTextHeight < minHeight {
|
||||
return minHeight
|
||||
}
|
||||
return titleTextHeight
|
||||
}
|
||||
var bodyFieldHeight: CGFloat {
|
||||
let minHeight: CGFloat = textEditorHeight
|
||||
if bodyTextHeight < minHeight {
|
||||
return minHeight
|
||||
}
|
||||
return bodyTextHeight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
if post.title.count == 0 {
|
||||
Text("Title (optional)")
|
||||
.font(Font(titleTextStyle))
|
||||
.foregroundColor(Color(UIColor.placeholderText))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 8)
|
||||
.accessibilityHidden(true)
|
||||
MultilineTextField(
|
||||
"Title (optional)",
|
||||
text: $post.title,
|
||||
font: titleTextStyle,
|
||||
isFirstResponder: $titleIsFirstResponder,
|
||||
onCommit: didFinishEditingTitle
|
||||
)
|
||||
.accessibilityLabel(Text("Title (optional)"))
|
||||
.accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body"))
|
||||
.onChange(of: post.title) { _ in
|
||||
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
|
||||
post.status = PostStatus.edited.rawValue
|
||||
}
|
||||
PostTitleTextView(
|
||||
text: $post.title,
|
||||
textStyle: $titleTextStyle,
|
||||
height: $titleTextHeight,
|
||||
isFirstResponder: $titleIsFirstResponder,
|
||||
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
|
||||
)
|
||||
.accessibilityLabel(Text("Title (optional)"))
|
||||
.accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body"))
|
||||
.frame(height: titleFieldHeight)
|
||||
.onChange(of: post.title) { _ in
|
||||
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
|
||||
post.status = PostStatus.edited.rawValue
|
||||
}
|
||||
if updatingTitleFromServer {
|
||||
updatingTitleFromServer = 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)
|
||||
.accessibilityHidden(true)
|
||||
MultilineTextField(
|
||||
"Write...",
|
||||
text: $post.body,
|
||||
font: bodyTextStyle,
|
||||
isFirstResponder: $bodyIsFirstResponder
|
||||
)
|
||||
.accessibilityLabel(Text("Body"))
|
||||
.accessibilityHint(Text("Add or edit the body of your post"))
|
||||
.onChange(of: post.body) { _ in
|
||||
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
|
||||
post.status = PostStatus.edited.rawValue
|
||||
}
|
||||
PostBodyTextView(
|
||||
text: $post.body,
|
||||
textStyle: $bodyTextStyle,
|
||||
height: $bodyTextHeight,
|
||||
isFirstResponder: $bodyIsFirstResponder,
|
||||
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
|
||||
)
|
||||
.frame(height: bodyFieldHeight)
|
||||
.accessibilityLabel(Text("Body"))
|
||||
.accessibilityHint(Text("Add or edit the body of your post"))
|
||||
.onChange(of: post.body) { _ in
|
||||
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
|
||||
post.status = PostStatus.edited.rawValue
|
||||
}
|
||||
if updatingBodyFromServer {
|
||||
updatingBodyFromServer = false
|
||||
}
|
||||
if updatingBodyFromServer {
|
||||
updatingBodyFromServer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,4 +76,9 @@ struct PostTextEditingView: View {
|
||||
self.bodyTextStyle = UIFont(name: appearance.rawValue, size: 17)!
|
||||
})
|
||||
}
|
||||
|
||||
private func didFinishEditingTitle() {
|
||||
self.titleIsFirstResponder = false
|
||||
self.bodyIsFirstResponder = true
|
||||
}
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
// Based on https://lostmoa.com/blog/DynamicHeightForTextFieldInSwiftUI and https://stackoverflow.com/a/56508132
|
||||
|
||||
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>) {
|
||||
if uiView.text != text {
|
||||
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