Skip to content

Commit efae520

Browse files
Merge branch 'main' into fix-crash-on-launch
2 parents 01bfe7e + d09ddc3 commit efae520

File tree

8 files changed

+325
-25
lines changed

8 files changed

+325
-25
lines changed

Shared/Navigation/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ struct ContentView: View {
77
NavigationView {
88
SidebarView()
99

10-
PostListView(selectedCollection: nil, showAllPosts: true)
10+
PostListView(selectedCollection: nil, showAllPosts: model.account.isLoggedIn)
1111

1212
Text("Select a post, or create a new local draft.")
1313
.foregroundColor(.secondary)

Shared/PostCollection/CollectionListView.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ struct CollectionListView: View {
1010

1111
var body: some View {
1212
List {
13-
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) {
14-
Text("All Posts")
15-
}
1613
if model.account.isLoggedIn {
14+
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: true)) {
15+
Text("All Posts")
16+
}
1717
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
1818
Text(model.account.server == "https://write.as" ? "Anonymous" : "Drafts")
1919
}
@@ -26,6 +26,10 @@ struct CollectionListView: View {
2626
}
2727
}
2828
}
29+
} else {
30+
NavigationLink(destination: PostListView(selectedCollection: nil, showAllPosts: false)) {
31+
Text("Drafts")
32+
}
2933
}
3034
}
3135
.navigationTitle(

Shared/PostEditor/PostEditorModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import CoreData
33

44
enum PostAppearance: String {
55
case sans = "OpenSans-Regular"
6-
case mono = "Hack"
7-
case serif = "Lora"
6+
case mono = "Hack-Regular"
7+
case serif = "Lora-Regular"
88
}
99

1010
struct PostEditorModel {

WriteFreely-MultiPlatform.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
17A5388F24DDEC7400DEFF9A /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5388D24DDEC7400DEFF9A /* AccountView.swift */; };
5454
17A5389324DDED0000DEFF9A /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5389124DDED0000DEFF9A /* PreferencesView.swift */; };
5555
17A67CAF251A5DD7002F163D /* PostEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A67CAE251A5DD7002F163D /* PostEditorView.swift */; };
56+
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A5D25489E810057D763 /* PostTitleTextView.swift */; };
57+
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AD0A6325489E900057D763 /* PostBodyTextView.swift */; };
5658
17B3E965250FAA9000EE9748 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */; };
5759
17B5103B2515448D00E9631F /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17B5103A2515448D00E9631F /* Credits.rtf */; };
5860
17B996D82502D23E0017B536 /* WFAPost+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */; };
@@ -141,6 +143,8 @@
141143
17A5388D24DDEC7400DEFF9A /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
142144
17A5389124DDED0000DEFF9A /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
143145
17A67CAE251A5DD7002F163D /* PostEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorView.swift; sourceTree = "<group>"; };
146+
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTitleTextView.swift; sourceTree = "<group>"; };
147+
17AD0A6325489E900057D763 /* PostBodyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBodyTextView.swift; sourceTree = "<group>"; };
144148
17B3E964250FAA9000EE9748 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
145149
17B5103A2515448D00E9631F /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
146150
17B996D62502D23E0017B536 /* WFAPost+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFAPost+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
@@ -294,6 +298,8 @@
294298
1756AE7624CB2EDD00FD7257 /* PostEditorView.swift */,
295299
173E19D0254318F600440F0F /* RemoteChangePromptView.swift */,
296300
173E19E2254329CC00440F0F /* PostTextEditingView.swift */,
301+
17AD0A5D25489E810057D763 /* PostTitleTextView.swift */,
302+
17AD0A6325489E900057D763 /* PostBodyTextView.swift */,
297303
);
298304
path = PostEditor;
299305
sourceTree = "<group>";
@@ -694,6 +700,8 @@
694700
170DFA34251BBC44001D82A0 /* PostEditorModel.swift in Sources */,
695701
17120DAC24E1B99F002B9F6C /* AccountLoginView.swift in Sources */,
696702
17480CA5251272EE00EB7765 /* Bundle+AppVersion.swift in Sources */,
703+
17AD0A6425489E900057D763 /* PostBodyTextView.swift in Sources */,
704+
17AD0A5E25489E810057D763 /* PostTitleTextView.swift in Sources */,
697705
17120DA924E1B2F5002B9F6C /* AccountLogoutView.swift in Sources */,
698706
171BFDFA24D4AF8300888236 /* CollectionListView.swift in Sources */,
699707
1756DBB324FECDBB00207AB8 /* PostEditorStatusToolbarView.swift in Sources */,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Based on https://stackoverflow.com/a/56508132/1234545 and https://stackoverflow.com/a/48360549/1234545
2+
3+
import SwiftUI
4+
5+
class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
6+
@Binding var text: String
7+
@Binding var isFirstResponder: Bool
8+
@Binding var currentTextPosition: UITextRange?
9+
var lineSpacingMultiplier: CGFloat
10+
var didBecomeFirstResponder: Bool = false
11+
var postBodyTextView: PostBodyTextView
12+
13+
weak var textView: UITextView?
14+
15+
init(
16+
_ textView: PostBodyTextView,
17+
text: Binding<String>,
18+
isFirstResponder: Binding<Bool>,
19+
currentTextPosition: Binding<UITextRange?>,
20+
lineSpacingMultiplier: CGFloat
21+
) {
22+
self.postBodyTextView = textView
23+
_text = text
24+
_isFirstResponder = isFirstResponder
25+
self.lineSpacingMultiplier = lineSpacingMultiplier
26+
_currentTextPosition = currentTextPosition
27+
}
28+
29+
func textViewDidChange(_ textView: UITextView) {
30+
DispatchQueue.main.async {
31+
self.postBodyTextView.text = textView.text ?? ""
32+
}
33+
}
34+
35+
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
36+
self.currentTextPosition = textView.selectedTextRange
37+
return true
38+
}
39+
40+
func textViewDidBeginEditing(_ textView: UITextView) {
41+
if let textPosition = currentTextPosition {
42+
textView.selectedTextRange = textPosition
43+
}
44+
}
45+
46+
func textViewDidEndEditing(_ textView: UITextView) {
47+
self.isFirstResponder = false
48+
self.didBecomeFirstResponder = false
49+
self.currentTextPosition = textView.selectedTextRange
50+
}
51+
52+
func layoutManager(
53+
_ layoutManager: NSLayoutManager,
54+
lineSpacingAfterGlyphAt glyphIndex: Int,
55+
withProposedLineFragmentRect rect: CGRect
56+
) -> CGFloat {
57+
// HACK: - This seems to be the only way to get line spacing to update dynamically on iPad
58+
// when switching between full-screen, split-screen, and slide-over views.
59+
if let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first {
60+
// Get the width of the window to determine the size class
61+
if window.frame.width < 600 {
62+
// Use 0.25 multiplier for compact size class
63+
return 17 * 0.25
64+
} else {
65+
// Use 0.5 multiplier otherwise
66+
return 17 * 0.5
67+
}
68+
} else {
69+
return 17 * lineSpacingMultiplier
70+
}
71+
}
72+
}
73+
74+
struct PostBodyTextView: UIViewRepresentable {
75+
@Binding var text: String
76+
@Binding var textStyle: UIFont
77+
@Binding var isFirstResponder: Bool
78+
@Binding var currentTextPosition: UITextRange?
79+
@State var lineSpacing: CGFloat
80+
81+
func makeUIView(context: UIViewRepresentableContext<PostBodyTextView>) -> UITextView {
82+
let textView = UITextView(frame: .zero)
83+
84+
textView.isEditable = true
85+
textView.isUserInteractionEnabled = true
86+
textView.isScrollEnabled = true
87+
textView.alwaysBounceVertical = false
88+
89+
context.coordinator.textView = textView
90+
textView.delegate = context.coordinator
91+
textView.layoutManager.delegate = context.coordinator
92+
93+
let font = textStyle
94+
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
95+
textView.font = fontMetrics.scaledFont(for: font)
96+
textView.backgroundColor = UIColor.clear
97+
98+
return textView
99+
}
100+
101+
func makeCoordinator() -> PostBodyCoordinator {
102+
return Coordinator(
103+
self,
104+
text: $text,
105+
isFirstResponder: $isFirstResponder,
106+
currentTextPosition: $currentTextPosition,
107+
lineSpacingMultiplier: lineSpacing
108+
)
109+
}
110+
111+
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<PostBodyTextView>) {
112+
uiView.text = text
113+
114+
let font = textStyle
115+
let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle)
116+
uiView.font = fontMetrics.scaledFont(for: font)
117+
118+
// We don't want the text field to become first responder every time SwiftUI refreshes the view.
119+
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
120+
uiView.becomeFirstResponder()
121+
context.coordinator.didBecomeFirstResponder = true
122+
}
123+
}
124+
}

iOS/PostEditor/PostEditorView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33
struct PostEditorView: View {
44
@EnvironmentObject var model: WriteFreelyModel
55
@Environment(\.horizontalSizeClass) var horizontalSizeClass
6+
@Environment(\.managedObjectContext) var moc
67
@Environment(\.presentationMode) var presentationMode
78

89
@ObservedObject var post: WFAPost

iOS/PostEditor/PostTextEditingView.swift

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ struct PostTextEditingView: View {
66
@Binding var updatingTitleFromServer: Bool
77
@Binding var updatingBodyFromServer: Bool
88
@State private var appearance: PostAppearance = .serif
9-
private let bodyLineSpacingMultiplier: CGFloat = 0.5
9+
@State private var titleTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 26)!
10+
@State private var titleTextHeight: CGFloat = 50
11+
@State private var titleIsFirstResponder: Bool = true
12+
@State private var bodyTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 17)!
13+
@State private var bodyIsFirstResponder: Bool = false
14+
@State private var bodyCursorPosition: UITextRange?
15+
private let lineSpacingMultiplier: CGFloat = 0.5
1016

1117
init(
1218
post: ObservedObject<WFAPost>,
@@ -19,41 +25,69 @@ struct PostTextEditingView: View {
1925
UITextView.appearance().backgroundColor = .clear
2026
}
2127

28+
var titleFieldHeight: CGFloat {
29+
let minHeight: CGFloat = 50
30+
if titleTextHeight < minHeight {
31+
return minHeight
32+
}
33+
return titleTextHeight
34+
}
35+
2236
var body: some View {
2337
VStack {
24-
TextField("Title (optional)", text: $post.title)
25-
.font(.custom(appearance.rawValue, size: 26, relativeTo: .largeTitle))
26-
.padding(.horizontal, 4)
38+
ZStack(alignment: .topLeading) {
39+
if post.title.count == 0 {
40+
Text("Title (optional)")
41+
.font(Font(titleTextStyle))
42+
.foregroundColor(Color(UIColor.placeholderText))
43+
.padding(.horizontal, 4)
44+
.padding(.vertical, 8)
45+
}
46+
PostTitleTextView(
47+
text: $post.title,
48+
textStyle: $titleTextStyle,
49+
height: $titleTextHeight,
50+
isFirstResponder: $titleIsFirstResponder,
51+
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
52+
)
53+
.frame(height: titleFieldHeight)
2754
.onChange(of: post.title) { _ in
2855
if post.status == PostStatus.published.rawValue && !updatingTitleFromServer {
2956
post.status = PostStatus.edited.rawValue
3057
}
58+
if updatingTitleFromServer {
59+
updatingTitleFromServer = false
60+
}
3161
}
62+
}
3263
ZStack(alignment: .topLeading) {
3364
if post.body.count == 0 {
3465
Text("Write…")
35-
.font(.custom(appearance.rawValue, size: 17, relativeTo: .body))
66+
.font(Font(bodyTextStyle))
3667
.foregroundColor(Color(UIColor.placeholderText))
3768
.padding(.horizontal, 4)
3869
.padding(.vertical, 8)
3970
}
40-
TextEditor(text: $post.body)
41-
.font(.custom(appearance.rawValue, size: 17, relativeTo: .body))
42-
.lineSpacing(
43-
17 * (
44-
horizontalSizeClass == .compact ? bodyLineSpacingMultiplier / 2 : bodyLineSpacingMultiplier
45-
)
46-
)
47-
.onChange(of: post.body) { _ in
48-
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
49-
post.status = PostStatus.edited.rawValue
50-
}
51-
if updatingBodyFromServer {
52-
updatingBodyFromServer = false
53-
}
71+
PostBodyTextView(
72+
text: $post.body,
73+
textStyle: $bodyTextStyle,
74+
isFirstResponder: $bodyIsFirstResponder,
75+
currentTextPosition: $bodyCursorPosition,
76+
lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier
77+
)
78+
.onChange(of: post.body) { _ in
79+
if post.status == PostStatus.published.rawValue && !updatingBodyFromServer {
80+
post.status = PostStatus.edited.rawValue
5481
}
82+
if updatingBodyFromServer {
83+
updatingBodyFromServer = false
84+
}
85+
}
5586
}
5687
}
88+
.onChange(of: titleIsFirstResponder, perform: { _ in
89+
self.bodyIsFirstResponder.toggle()
90+
})
5791
.onAppear(perform: {
5892
switch post.appearance {
5993
case "sans":
@@ -63,6 +97,8 @@ struct PostTextEditingView: View {
6397
default:
6498
self.appearance = .serif
6599
}
100+
self.titleTextStyle = UIFont(name: appearance.rawValue, size: 26)!
101+
self.bodyTextStyle = UIFont(name: appearance.rawValue, size: 17)!
66102
})
67103
}
68104
}

0 commit comments

Comments
 (0)