Skip to content

Commit f256996

Browse files
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
1 parent ceb7eaa commit f256996

File tree

8 files changed

+212
-351
lines changed

8 files changed

+212
-351
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
- [Mac] Fixed a bug where alerts weren't presented for login errors.
4545
- [Mac] Fixed some build warnings in the project.
4646
- [Mac] Bumped WriteFreely package to v0.3.6 to handle decoding of fractional seconds in dates.
47+
- [iOS] Fixed an issue that made it tricky to scroll in the post editor.
48+
- [iOS] Fixed a bug that didn't navigate to the post editor after tapping the new-post button.
4749

4850
## [1.0.12-ios] - 2022-10-06
4951

Shared/PostList/PostListView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ struct PostListView: View {
4747
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
4848
withAnimation {
4949
self.model.showAllPosts = false
50-
self.model.selectedPost = managedPost
50+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
51+
self.model.selectedPost = managedPost
52+
}
5153
}
5254
}, label: {
5355
ZStack {

Shared/PostList/PostStatusBadgeView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct PostStatusBadgeView: View {
1414
.padding(EdgeInsets(top: 2.5, leading: 7.5, bottom: 2.5, trailing: 7.5))
1515
.background(badgeColor)
1616
.clipShape(RoundedRectangle(cornerRadius: 5.0, style: .circular))
17-
.frame(width: .infinity)
17+
.frame(maxWidth: .infinity)
1818
}
1919

2020
func setupBadgeProperties(for status: PostStatus) -> (String, Color) {

WriteFreely-MultiPlatform.xcodeproj/project.pbxproj

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@
9090
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; };
9191
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; };
9292
17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; };
93-
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */; };
94-
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A6325489E900057D763 /* PostBodyTextView.swift */; };
9593
17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; };
9694
17B37C4C25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */; };
9795
17B37C5625C8679800FE75E9 /* WriteFreelyModel+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */; };
@@ -138,6 +136,7 @@
138136
17DFDE8B251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; };
139137
17DFDE8C251D309400A25F31 /* OpenSans-License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 17DFDE86251D309400A25F31 /* OpenSans-License.txt */; };
140138
17E5DF8A2543610700DCDC9B /* PostTextEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E5DF892543610700DCDC9B /* PostTextEditingView.swift */; };
139+
375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375A67E728FC555C007A1AC0 /* MultilineTextView.swift */; };
141140
/* End PBXBuildFile section */
142141

143142
/* Begin PBXContainerItemProxy section */
@@ -227,8 +226,6 @@
227226
17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
228227
17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
229228
17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = "<group>"; };
230-
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTitleTextView.swift; sourceTree = "<group>"; };
231-
17AD0A6325489E900057D763 /* PostBodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBodyTextView.swift; sourceTree = "<group>"; };
232229
17B37C4A25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+Keychain.swift"; sourceTree = "<group>"; };
233230
17B37C5525C8679800FE75E9 /* WriteFreelyModel+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+API.swift"; sourceTree = "<group>"; };
234231
17B37C5C25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WriteFreelyModel+APIHandlers.swift"; sourceTree = "<group>"; };
@@ -270,6 +267,7 @@
270267
17DFDE85251D309400A25F31 /* Lora-Cyrillic-OFL.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Lora-Cyrillic-OFL.txt"; sourceTree = "<group>"; };
271268
17DFDE86251D309400A25F31 /* OpenSans-License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OpenSans-License.txt"; sourceTree = "<group>"; };
272269
17E5DF892543610700DCDC9B /* PostTextEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTextEditingView.swift; sourceTree = "<group>"; };
270+
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextView.swift; sourceTree = "<group>"; };
273271
/* End PBXFileReference section */
274272

275273
/* Begin PBXFrameworksBuildPhase section */
@@ -436,8 +434,7 @@
436434
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
437435
173E19D0254318F600440F0F /* RemoteChangePromptView.swift */,
438436
173E19E2254329CC00440F0F /* PostTextEditingView.swift */,
439-
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */,
440-
17AD0A6325489E900057D763 /* PostBodyTextView.swift */,
437+
375A67E728FC555C007A1AC0 /* MultilineTextView.swift */,
441438
);
442439
path = PostEditor;
443440
sourceTree = "<group>";
@@ -926,13 +923,12 @@
926923
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
927924
17B37C4B25C8661300FE75E9 /* WriteFreelyModel+Keychain.swift in Sources */,
928925
17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
929-
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */,
930926
17B37C5D25C8698900FE75E9 /* WriteFreelyModel+APIHandlers.swift in Sources */,
931-
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */,
932927
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
933928
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
934929
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
935930
17120DB224E1E19C002B9F6C /* SettingsHeaderView.swift in Sources */,
931+
375A67E828FC555C007A1AC0 /* MultilineTextView.swift in Sources */,
936932
171DC677272C7D0B002B9B8A /* UserDefaults+Extensions.swift in Sources */,
937933
1756DBB724FED3A400207AB8 /* LocalStorageModel.xcdatamodeld in Sources */,
938934
17B996DA2502D23E0017B536 /* WFAPost+CoreDataProperties.swift in Sources */,
@@ -1054,7 +1050,7 @@
10541050
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
10551051
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
10561052
CODE_SIGN_STYLE = Automatic;
1057-
CURRENT_PROJECT_VERSION = 680;
1053+
CURRENT_PROJECT_VERSION = 687;
10581054
DEVELOPMENT_TEAM = TPPAB4YBA6;
10591055
GENERATE_INFOPLIST_FILE = YES;
10601056
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@@ -1066,7 +1062,7 @@
10661062
"@executable_path/Frameworks",
10671063
"@executable_path/../../Frameworks",
10681064
);
1069-
MARKETING_VERSION = 1.0.12;
1065+
MARKETING_VERSION = 1.0.13;
10701066
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS";
10711067
PRODUCT_NAME = "$(TARGET_NAME)";
10721068
SDKROOT = iphoneos;
@@ -1085,7 +1081,7 @@
10851081
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
10861082
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
10871083
CODE_SIGN_STYLE = Automatic;
1088-
CURRENT_PROJECT_VERSION = 680;
1084+
CURRENT_PROJECT_VERSION = 687;
10891085
DEVELOPMENT_TEAM = TPPAB4YBA6;
10901086
GENERATE_INFOPLIST_FILE = YES;
10911087
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@@ -1097,7 +1093,7 @@
10971093
"@executable_path/Frameworks",
10981094
"@executable_path/../../Frameworks",
10991095
);
1100-
MARKETING_VERSION = 1.0.12;
1096+
MARKETING_VERSION = 1.0.13;
11011097
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform.ActionExtension-iOS";
11021098
PRODUCT_NAME = "$(TARGET_NAME)";
11031099
SDKROOT = iphoneos;
@@ -1228,7 +1224,7 @@
12281224
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
12291225
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
12301226
CODE_SIGN_STYLE = Automatic;
1231-
CURRENT_PROJECT_VERSION = 680;
1227+
CURRENT_PROJECT_VERSION = 687;
12321228
DEVELOPMENT_TEAM = TPPAB4YBA6;
12331229
ENABLE_PREVIEWS = YES;
12341230
INFOPLIST_FILE = iOS/Info.plist;
@@ -1237,7 +1233,7 @@
12371233
"$(inherited)",
12381234
"@executable_path/Frameworks",
12391235
);
1240-
MARKETING_VERSION = 1.0.12;
1236+
MARKETING_VERSION = 1.0.13;
12411237
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform";
12421238
PRODUCT_NAME = "WriteFreely-MultiPlatform";
12431239
SDKROOT = iphoneos;
@@ -1254,7 +1250,7 @@
12541250
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
12551251
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
12561252
CODE_SIGN_STYLE = Automatic;
1257-
CURRENT_PROJECT_VERSION = 680;
1253+
CURRENT_PROJECT_VERSION = 687;
12581254
DEVELOPMENT_TEAM = TPPAB4YBA6;
12591255
ENABLE_PREVIEWS = YES;
12601256
INFOPLIST_FILE = iOS/Info.plist;
@@ -1263,7 +1259,7 @@
12631259
"$(inherited)",
12641260
"@executable_path/Frameworks",
12651261
);
1266-
MARKETING_VERSION = 1.0.12;
1262+
MARKETING_VERSION = 1.0.13;
12671263
PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform";
12681264
PRODUCT_NAME = "WriteFreely-MultiPlatform";
12691265
SDKROOT = iphoneos;
@@ -1282,7 +1278,7 @@
12821278
CODE_SIGN_IDENTITY = "Apple Development";
12831279
CODE_SIGN_STYLE = Automatic;
12841280
COMBINE_HIDPI_IMAGES = YES;
1285-
CURRENT_PROJECT_VERSION = 676;
1281+
CURRENT_PROJECT_VERSION = 687;
12861282
DEVELOPMENT_TEAM = TPPAB4YBA6;
12871283
ENABLE_HARDENED_RUNTIME = YES;
12881284
ENABLE_PREVIEWS = YES;
@@ -1309,7 +1305,7 @@
13091305
CODE_SIGN_IDENTITY = "Apple Development";
13101306
CODE_SIGN_STYLE = Automatic;
13111307
COMBINE_HIDPI_IMAGES = YES;
1312-
CURRENT_PROJECT_VERSION = 676;
1308+
CURRENT_PROJECT_VERSION = 687;
13131309
DEVELOPMENT_TEAM = TPPAB4YBA6;
13141310
ENABLE_HARDENED_RUNTIME = YES;
13151311
ENABLE_PREVIEWS = YES;
@@ -1489,7 +1485,7 @@
14891485
repositoryURL = "https://github.com/sparkle-project/Sparkle";
14901486
requirement = {
14911487
kind = upToNextMinorVersion;
1492-
minimumVersion = 2.0.0;
1488+
minimumVersion = 2.3.0;
14931489
};
14941490
};
14951491
/* End XCRemoteSwiftPackageReference section */
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Credit: https://stackoverflow.com/a/58639072
2+
3+
import SwiftUI
4+
import UIKit
5+
6+
private struct UITextViewWrapper: UIViewRepresentable {
7+
typealias UIViewType = UITextView
8+
9+
@Binding var text: String
10+
@Binding var calculatedHeight: CGFloat
11+
@Binding var isEditing: Bool
12+
var textStyle: UIFont
13+
var onDone: (() -> Void)?
14+
15+
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
16+
let textField = UITextView()
17+
textField.delegate = context.coordinator
18+
19+
textField.isEditable = true
20+
textField.font = UIFont.preferredFont(forTextStyle: .body)
21+
textField.isSelectable = true
22+
textField.isUserInteractionEnabled = true
23+
textField.isScrollEnabled = false
24+
textField.backgroundColor = UIColor.clear
25+
textField.smartDashesType = .no
26+
27+
let font = textStyle
28+
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
29+
textField.font = fontMetrics.scaledFont(for: font)
30+
31+
if nil != onDone {
32+
textField.returnKeyType = .next
33+
}
34+
35+
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
36+
return textField
37+
}
38+
39+
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
40+
if uiView.text != self.text {
41+
uiView.text = self.text
42+
}
43+
44+
if uiView.window != nil, isEditing {
45+
uiView.becomeFirstResponder()
46+
}
47+
48+
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
49+
}
50+
51+
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
52+
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
53+
if result.wrappedValue != newSize.height {
54+
DispatchQueue.main.async {
55+
result.wrappedValue = newSize.height // !! must be called asynchronously
56+
}
57+
}
58+
}
59+
60+
func makeCoordinator() -> Coordinator {
61+
return Coordinator(text: $text, height: $calculatedHeight, isFirstResponder: $isEditing, onDone: onDone)
62+
}
63+
64+
final class Coordinator: NSObject, UITextViewDelegate {
65+
@Binding var isFirstResponder: Bool
66+
var text: Binding<String>
67+
var calculatedHeight: Binding<CGFloat>
68+
var onDone: (() -> Void)?
69+
70+
init(
71+
text: Binding<String>,
72+
height: Binding<CGFloat>,
73+
isFirstResponder: Binding<Bool>,
74+
onDone: (() -> Void)? = nil
75+
) {
76+
self.text = text
77+
self.calculatedHeight = height
78+
self._isFirstResponder = isFirstResponder
79+
self.onDone = onDone
80+
}
81+
82+
func textViewDidChange(_ uiView: UITextView) {
83+
text.wrappedValue = uiView.text
84+
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
85+
}
86+
87+
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
88+
if let onDone = self.onDone, text == "\n" {
89+
textView.resignFirstResponder()
90+
onDone()
91+
return false
92+
}
93+
return true
94+
}
95+
96+
func textViewDidEndEditing(_ textView: UITextView) {
97+
self.isFirstResponder = false
98+
}
99+
}
100+
101+
}
102+
103+
struct MultilineTextField: View {
104+
105+
private var placeholder: String
106+
private var textStyle: UIFont
107+
private var onCommit: (() -> Void)?
108+
109+
@Binding var isFirstResponder: Bool
110+
@Binding private var text: String
111+
private var internalText: Binding<String> {
112+
Binding<String>(get: { self.text }) { // swiftlint:disable:this multiple_closures_with_trailing_closure
113+
self.text = $0
114+
self.showingPlaceholder = $0.isEmpty
115+
}
116+
}
117+
118+
@State private var dynamicHeight: CGFloat = 100
119+
@State private var showingPlaceholder = false
120+
121+
init (
122+
_ placeholder: String = "",
123+
text: Binding<String>,
124+
font: UIFont,
125+
isFirstResponder: Binding<Bool>,
126+
onCommit: (() -> Void)? = nil
127+
) {
128+
self.placeholder = placeholder
129+
self.onCommit = onCommit
130+
self.textStyle = font
131+
self._isFirstResponder = isFirstResponder
132+
self._text = text
133+
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
134+
}
135+
136+
var body: some View {
137+
UITextViewWrapper(
138+
text: self.internalText,
139+
calculatedHeight: $dynamicHeight,
140+
isEditing: $isFirstResponder,
141+
textStyle: textStyle,
142+
onDone: onCommit
143+
)
144+
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
145+
.background(placeholderView, alignment: .topLeading)
146+
}
147+
148+
var placeholderView: some View {
149+
Group {
150+
if showingPlaceholder {
151+
let font = Font(textStyle)
152+
Text(placeholder).foregroundColor(.gray)
153+
.padding(.leading, 4)
154+
.padding(.top, 8)
155+
.font(font)
156+
}
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)